diff --git a/modules/vadentis-actor-sheet.js b/modules/vadentis-actor-sheet.js new file mode 100644 index 0000000..8ca9c42 --- /dev/null +++ b/modules/vadentis-actor-sheet.js @@ -0,0 +1,155 @@ +/** + * Extend the basic ActorSheet with some very simple modifications + * @extends {ActorSheet} + */ + +import { VadentisUtility } from "./vadentis-utility.js"; + +/* -------------------------------------------- */ +export class VadentisActorSheet extends ActorSheet { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sos", "sheet", "actor"], + template: "systems/foundryvtt-vadentis/templates/actor-sheet.html", + width: 640, + height: 720, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "stats" }], + dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }], + editStatSkill: false + }); + } + + /* -------------------------------------------- */ + getData() { + let data = super.getData(); + + this.actor.checkDeck(); + + + return data; + } + + /* -------------------------------------------- */ + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + //HtmlUtility._showControlWhen($(".gm-only"), game.user.isGM); + + // Everything below here is only needed if the sheet is editable + if (!this.options.editable) return; + + // Update Inventory Item + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.getOwnedItem(li.data("item-id")); + item.sheet.render(true); + }); + html.find('.item-equip').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.equipObject( li.data("item-id") ); + this.render(true); + }); + html.find('.item-worn').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.wornObject( li.data("item-id") ); + this.render(true); + }); + + // Delete Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + SoSUtility.confirmDelete(this, li); + }); + + html.find('.stat-label a').click((event) => { + let statName = event.currentTarget.attributes.name.value; + this.actor.rollStat(statName); + }); + html.find('.skill-label a').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const skill = this.actor.getOwnedItem(li.data("item-id")); + this.actor.rollSkill(skill); + }); + html.find('.weapon-label a').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const weapon = this.actor.getOwnedItem(li.data("item-id")); + this.actor.rollWeapon(weapon); + }); + html.find('.skill-value').change((event) => { + let skillName = event.currentTarget.attributes.skillname.value; + //console.log("Competence changed :", skillName); + this.actor.updateSkill(skillName, parseInt(event.target.value)); + }); + html.find('.skill-xp').change((event) => { + let skillName = event.currentTarget.attributes.skillname.value; + //console.log("Competence changed :", skillName); + this.actor.updateSkillExperience(skillName, parseInt(event.target.value)); + }); + html.find('.wound-value').change((event) => { + let woundName = event.currentTarget.attributes.woundname.value; + //console.log("Competence changed :", skillName); + this.actor.updateWound(woundName, parseInt(event.target.value)); + }); + html.find('.reset-deck-full').click((event) => { + this.actor.resetDeckFull(); + this.render(true); + }); + html.find('.draw-new-edge').click((event) => { + this.actor.drawNewEdge(); + this.render(true); + }); + html.find('.reset-deck').click((event) => { + this.actor.resetDeck(); + this.render(true); + }); + html.find('.discard-card').click((event) => { + const cardName = $(event.currentTarget).data("discard"); + this.actor.discardEdge( cardName ); + }); + html.find('.consequence-severity').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const item = this.actor.getOwnedItem(li.data("item-id")); + let severity = $(event.currentTarget).val(); + this.actor.updateOwnedItem( { _id: item._id, 'data.severity': severity}); + this.render(true); + }); + html.find('.lock-unlock-sheet').click((event) => { + this.options.editStatSkill = !this.options.editStatSkill; + this.render(true); + }); + html.find('.item-link a').click((event) => { + const itemId = $(event.currentTarget).data("item-id"); + const item = this.actor.getOwnedItem(itemId); + item.sheet.render(true); + }); + + } + + /* -------------------------------------------- */ + async _onDrop(event) { + let toSuper = await SoSUtility.processItemDropEvent(this, event); + if ( toSuper) { + super._onDrop(event); + } + } + + /* -------------------------------------------- */ + /** @override */ + setPosition(options = {}) { + const position = super.setPosition(options); + const sheetBody = this.element.find(".sheet-body"); + const bodyHeight = position.height - 192; + sheetBody.css("height", bodyHeight); + return position; + } + + /* -------------------------------------------- */ + /** @override */ + _updateObject(event, formData) { + // Update the Actor + return this.object.update(formData); + } +} diff --git a/modules/vadentis-actor.js b/modules/vadentis-actor.js new file mode 100644 index 0000000..0d92442 --- /dev/null +++ b/modules/vadentis-actor.js @@ -0,0 +1,52 @@ +import { VadentisUtility } from "./vadentis-utility.js"; + + +/* -------------------------------------------- */ +/** + * Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system. + * @extends {Actor} + */ +export class VadentisActor extends Actor { + + /* -------------------------------------------- */ + /** + * Override the create() function to provide additional SoS functionality. + * + * This overrided create() function adds initial items + * Namely: Basic skills, money, + * + * @param {Object} data Barebones actor data which this function adds onto. + * @param {Object} options (Unused) Additional options which customize the creation workflow. + * + */ + + static async create(data, options) { + + // Case of compendium global import + if (data instanceof Array) { + return super.create(data, options); + } + // If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic + if (data.items) { + let actor = super.create(data, options); + return actor; + } + + data.items = []; + let compendiumName = "foundryvtt-vadentis.competences"; + if ( compendiumName ) { + let skills = await SoSUtility.loadCompendium(compendiumName); + data.items = data.items.concat( skills ); + } + + return super.create(data, options); + } + + /* -------------------------------------------- */ + async prepareData() { + super.prepareData(); + + } + + +} diff --git a/modules/vadentis-combat.js b/modules/vadentis-combat.js new file mode 100644 index 0000000..b24d9ef --- /dev/null +++ b/modules/vadentis-combat.js @@ -0,0 +1,189 @@ +import { VadentisUtility } from "./vadentis-utility.js"; + +/* -------------------------------------------- */ +export class VadentisCombat extends Combat { + + /* -------------------------------------------- */ + requestActions() { + if ( game.user.isGM && !this.actionsRequested) { + console.log("REQUEST ACTIONS !!!"); + this.actionsRequested = true; + this.phaseSetup = {}; // Reset each new round/update + for( let combatant of this.combatants) { + this.setInitiative(combatant._id, -1 ); // Reset init + let uniq = randomID(16); + const name = combatant.actor ? combatant.actor.data.name : combatant.name; + if ( combatant.players[0]) { + // A player controls this combatant -> message ! + ChatMessage.create( { content: `New round ! Click on the button below to declare the actions of ${name} for round ${this.round} !
+ Declare actions`, + whisper: [ combatant.players[0].data._id] } ); + } else { + ChatMessage.create( { content: `New round ! Click on the button below to declare the actions of ${name} for round ${this.round} !
+ Declare actions`, + whisper: [ ChatMessage.getWhisperRecipients("GM") ] } ); + } + } + } + } + + /* -------------------------------------------- */ + async nextRound() { + this.actionsRequested = false; + super.nextRound(); + } + + /* -------------------------------------------- */ + gotoNextTurn() { + this.phaseNumber -= 1; + if ( this.phaseNumber <= 0) { + this.applyConsequences(); + this.nextRound(); // Auto-switch to next round + } else { + this.nextTurn(); + } + } + + /* -------------------------------------------- */ + async nextTurn() { + console.log("Going to phase !", this.phaseNumber ); + // Get all actions for this phase + let phaseIndex = this.phaseNumber - 1; + let actionList = []; + let actionMsg = `

Actions for phase ${this.phaseNumber}

`; + for (let combatantId in this.phaseSetup ) { + let actionData = this.phaseSetup[combatantId]; + if ( actionData.phaseArray[phaseIndex].name != 'No Action' ) { + let combatant = this.combatants.find( comb => comb._id == actionData.combatantId); + const name = combatant.actor ? combatant.actor.data.name : combatant.name; + actionList.push( { combatant: combatant, + action: actionData.phaseArray[phaseIndex], + isDone: false + }); + actionMsg += `
${name} is going to : ${actionData.phaseArray[phaseIndex].name}`; + } + } + if ( actionList.length == 0) { + actionMsg += "
No actions for the phase !"; + this.gotoNextTurn(); + } + // Display a nice message + ChatMessage.create( { content: actionMsg }); + + // Now push specific messages + for ( let action of actionList) { + let uniq = randomID(16); + action.uniqId = uniq; // Easy tracking with chat messages + const name = action.combatant.actor ? action.combatant.actor.data.name : action.combatant.name; + if ( action.combatant.players[0]) { + // A player controls this combatant -> message ! + ChatMessage.create( { content: `Phase ${this.phaseNumber} ! ${name} must perform a ${action.action.name} action. + When done, click on the button below to close the action. + Action is done !`, + whisper: [ action.combatant.players[0].data._id] } ); + } else { + ChatMessage.create( { content: `Phase ${this.phaseNumber} ! ${name} must perform a ${action.action.name} action.
+ When done, click on the button below to close the action. + Action is done !`, + whisper: [ ChatMessage.getWhisperRecipients("GM") ] } ); + } + } + // Save for easy access + this.currentActions = actionList; + } + + /* -------------------------------------------- */ + applyConsequences( ) { + if (game.user.isGM ) { + for( let combatant of this.combatants) { + if (!combatant.actor) continue; // Can't check tokens without assigned actors, Maybe print chat message about bleeding happening so that the GM can manually track this? + let bleeding = combatant.actor.data.items.find( item => item.type == 'consequence' && item.name == 'Bleeding'); + combatant.actor.applyConsequenceWound( bleeding.data.severity, "bleeding" ); + } + } + } + + /* -------------------------------------------- */ + closeAction( uniqId) { + // Delete message ! + const toDelete = game.messages.filter(it => it.data.content.includes( uniqId )); + toDelete.forEach(it => it.delete()); + + let action = this.currentActions.find( _action => _action.uniqId == uniqId ); + if (action) { + action.isDone = true; + + let filtered = this.currentActions.filter( _action => action.isDone ); + if ( filtered.length == this.currentActions.length) { // All actions closed ! + console.log("Going next turn !!!"); + this.gotoNextTurn(); + } + } + } + + /* -------------------------------------------- */ + getPhaseRank( actionConf) { + for (let i=2; i>=0; i--) { + let action = actionConf.phaseArray[i]; + if (action.name != "No Action") { + return i+1; + } + } + return 0; + } + + /* -------------------------------------------- */ + getAPFromActor( actorId ) { + for( let combatant of this.combatants) { + //console.log(combatant); + if ( combatant.actor.data._id == actorId ) { + let phase = this.phaseSetup[combatant._id]; + return phase.remainingAP; + } + } + return 0; + } + + /* -------------------------------------------- */ + decreaseAPFromActor( actorId ) { + for( let combatant of this.combatants) { + //console.log(combatant); + if ( combatant.actor.data._id == actorId ) { + let phase = this.phaseSetup[combatant._id]; + phase.remainingAP -= 1; + if ( phase.remainingAP < 0 ) phase.remainingAP = 0; + } + } + } + + /* -------------------------------------------- */ + async setupActorActions(actionConf) { + console.log("Setting combat for phase : ", actionConf, actionConf.uniqId); + + // Delete message ! + const toDelete = game.messages.filter(it => it.data.content.includes( actionConf.uniqId )); + console.log("MESSAGE : ", toDelete); + toDelete.forEach(it => it.delete()); + + if ( !this.phaseSetup) this.phaseSetup = {}; // Opportunistic init + + // Keep track + this.phaseSetup[actionConf.combatantId] = actionConf; + console.log( this.combatants ); + //let combatant = this.combatants.find( comb => comb._id == actionConf.combatantId); + await this.setInitiative( actionConf.combatantId, this.getPhaseRank( actionConf ) ); + + let actionsDone = true + for( let combatant of this.combatants) { + if ( combatant.initiative == -1 ) actionsDone = false; + } + if ( actionsDone ) { + this.actionsRequested = false; + ChatMessage.create( { content: `Action declaration has been completed ! Now proceeding with actions.`, + whisper: [ ChatMessage.getWhisperRecipients("GM") ] } ); + this.phaseNumber = 3; + this.nextTurn(); + } + } + +} diff --git a/modules/vadentis-item-sheet.js b/modules/vadentis-item-sheet.js new file mode 100644 index 0000000..4cc35db --- /dev/null +++ b/modules/vadentis-item-sheet.js @@ -0,0 +1,85 @@ +import { VadentisUtility } from "./vadentis-utility.js"; + +/** + * Extend the basic ItemSheet with some very simple modifications + * @extends {ItemSheet} + */ +export class VadentisItemSheet extends ItemSheet { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["foundryvtt-vadentis", "sheet", "item"], + template: "systems/foundryvtt-vadentis/templates/item-sheet.html", + width: 550, + height: 550 + //tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + // Add "Post to chat" button + // We previously restricted this to GM and editable items only. If you ever find this comment because it broke something: eh, sorry! + buttons.unshift( + { + class: "post", + icon: "fas fa-comment", + onclick: ev => {} //new RdDItem(this.item.data).postItem() + }) + return buttons + } + + /* -------------------------------------------- */ + /** @override */ + setPosition(options={}) { + const position = super.setPosition(options); + const sheetBody = this.element.find(".sheet-body"); + const bodyHeight = position.height - 192; + sheetBody.css("height", bodyHeight); + return position; + } + + /* -------------------------------------------- */ + async getData() { + let data = super.getData(); + data.isGM = game.user.isGM; + return data; + } + + /* -------------------------------------------- */ + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Everything below here is only needed if the sheet is editable + if (!this.options.editable) return; + + // Update Inventory Item + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.object.options.actor.getOwnedItem(li.data("item-id")); + item.sheet.render(true); + }); + // Update Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + this.object.options.actor.deleteOwnedItem( li.data("item-id") ).then( this.render(true)); + }); + + } + + /* -------------------------------------------- */ + get template() + { + let type = this.item.type; + return `systems/foundryvtt-vadentis/templates/item-${type}-sheet.html`; + } + + /* -------------------------------------------- */ + /** @override */ + _updateObject(event, formData) { + return this.object.update(formData); + } +} diff --git a/modules/vadentis-main.js b/modules/vadentis-main.js new file mode 100644 index 0000000..e606e67 --- /dev/null +++ b/modules/vadentis-main.js @@ -0,0 +1,106 @@ +/** + * RdD system + * Author: Uberwald + * Software License: Prop + */ + +/* -------------------------------------------- */ + +/* -------------------------------------------- */ +// Import Modules +import { VadentisActor } from "./actor.js"; +import { VadentisItemSheet } from "./item-sheet.js"; +import { VadentisActorSheet } from "./actor-sheet.js"; +import { VadentisUtility } from "./vadentis-utility.js"; +import { VadentisCombat } from "./vadentis-combat.js"; + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ + +/************************************************************************************/ +Hooks.once("init", async function () { + console.log(`Initializing Vadentis`); + + /* -------------------------------------------- */ + // preload handlebars templates + VadentisUtility.preloadHandlebarsTemplates(); + + /* -------------------------------------------- */ + // Set an initiative formula for the system + CONFIG.Combat.initiative = { + formula: "1d20", + decimals: 0 + }; + + /* -------------------------------------------- */ + game.socket.on("system.foundryvtt-vadentis", data => { + VadentisUtility.onSocketMesssage(data); + }); + + /* -------------------------------------------- */ + // Define custom Entity classes + CONFIG.Actor.entityClass = VadentisActor; + CONFIG.Combat.entityClass = VadentisCombat; + CONFIG.Vadentis = { + } + + /* -------------------------------------------- */ + // Register sheet application classes + Actors.unregisterSheet("core", ActorSheet); + Actors.registerSheet("foundryvtt-vadentis", VadentisActorSheet, { types: ["character"], makeDefault: true }); + Items.unregisterSheet("core", ItemSheet); + Items.registerSheet("foundryvtt-vadentis", VadentisItemSheet, { makeDefault: true }); + + // Init/registers + Hooks.on('renderChatLog', (log, html, data) => { + VadentisUtility.registerChatCallbacks(html); + }); + // Init/registers + Hooks.on('updateCombat', (combat, round, diff, id) => { + VadentisUtility.updateCombat(combat, round, diff, id); + }); + +}); + +/* -------------------------------------------- */ +function welcomeMessage() { + //ChatUtility.removeMyChatMessageContaining('
'); + ChatMessage.create({ + user: game.user._id, + whisper: [game.user._id], + content: `
Bienvenue !
+ ` }); +} + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ +Hooks.once("ready", function () { + + // User warning + if (!game.user.isGM && game.user.character == undefined) { + ui.notifications.info("Attention ! Vous n'est connecté à aucun personnage"); + ChatMessage.create({ + content: "WARNING Le joueur " + game.user.name + " n'est pas connecté à un personnage !", + user: game.user._id + }); + } + welcomeMessage(); +}); + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ +Hooks.on("chatMessage", (html, content, msg) => { + if (content[0] == '/') { + let regExp = /(\S+)/g; + let commands = content.toLowerCase().match(regExp); + console.log(commands); + //if ( commands[0] == '/gmdeck') { + //game.system.sos.gmDeck.render( true ); + //return false; + //} + } + return true; +}); diff --git a/modules/vadentis-utility.js b/modules/vadentis-utility.js new file mode 100644 index 0000000..b002086 --- /dev/null +++ b/modules/vadentis-utility.js @@ -0,0 +1,55 @@ +/* -------------------------------------------- */ +import { VadentisCombat } from "./vadentis-combat.js"; + +/* -------------------------------------------- */ +export class VadentisUtility extends Entity { + + /* -------------------------------------------- */ + static async preloadHandlebarsTemplates() { + + const templatePaths = [ + 'systems/foundryvtt-vadentis/templates/actor-sheet.html', + 'systems/foundryvtt-vadentis/templates/item-sheet.html' + ] + return loadTemplates(templatePaths); + } + + /* -------------------------------------------- */ + static fillRange (start, end) { + return Array(end - start + 1).fill().map((item, index) => start + index); + } + + /* -------------------------------------------- */ + static onSocketMesssage( msg ) { + if( !game.user.isGM ) return; // Only GM + + if (msg.name == 'msg_declare_actions' ) { + } + } + + /* -------------------------------------------- */ + static async loadCompendiumNames(compendium) { + const pack = game.packs.get(compendium); + let competences; + await pack.getIndex().then(index => competences = index); + return competences; + } + + /* -------------------------------------------- */ + static async loadCompendium(compendium, filter = item => true) { + let compendiumItems = await SoSUtility.loadCompendiumNames(compendium); + + const pack = game.packs.get(compendium); + let list = []; + for (let compendiumItem of compendiumItems) { + await pack.getEntity(compendiumItem._id).then(it => { + const item = it.data; + if (filter(item)) { + list.push(item); + } + }); + }; + return list; + } + +} \ No newline at end of file diff --git a/system.json b/system.json new file mode 100644 index 0000000..972e166 --- /dev/null +++ b/system.json @@ -0,0 +1,35 @@ +{ + "name": "foundryvtt-vadentis", + "title": "Vadentis", + "description": "Système Vadentis pour FoundryVTT", + "version": "0.0.1", + "manifestPlusVersion": "1.0.0", + "minimumCoreVersion": "0.7.5", + "compatibleCoreVersion": "0.7.9", + "templateVersion": 1, + "author": "Uberwald", + "esmodules": [ "module/vadentis-main.js" ], + "styles": ["styles/simple.css"], + "background" : "", + "media": [ + ], + "packs": [ + ], + "library": false, + "languages": [ + { + "lang": "fr", + "name": "French", + "path": "lang/fr.json" + } + ], + "gridDistance": 5, + "gridUnits": "m", + "primaryTokenAttribute": "", + "secondaryTokenAttribute": "", + "socket": true, + "url": "https://gitlab.com/LeRatierBretonnien/foundryvtt-shadows-over-sol/", + "manifest": "https://gitlab.com/LeRatierBretonnien/foundryvtt-shadows-over-sol/-/raw/master/system.json", + "download": "https://gitlab.com/LeRatierBretonnien/foundryvtt-shadows-over-sol/-/archive/master/foundryvtt-shadows-over-sol.zip", + "license": "LICENSE.txt" +} diff --git a/template.json b/template.json new file mode 100644 index 0000000..3564fa8 --- /dev/null +++ b/template.json @@ -0,0 +1,148 @@ +{ +"Actor": { + "types": ["character"], + "templates": { + "common": { + "stats": { + "donnee": { + "label": "Données", + "list": [] + }, + "experience": { + "total": 0, + "disponibe": 0, + "label": "Expérience" + }, + "pointsvie": { + "value": 0, + "max": 0, + "label": "Points de Vie" + }, + "pointsenergie": { + "value": 0, + "max": 0, + "label": "Points d'Energie" + }, + "pointsadrenaline": { + "value": 0, + "max": 0, + "label": "Points d'Adrénaline" + }, + "force": { + "base": 0, + "malus": 0, + "bonus": 0, + "label": "Force" + }, + "esquive": { + "value": 0, + "label": "Esquive" + }, + "attaque": { + "base": 0, + "malus": 0, + "bonus": 0, + "label": "Attaque" + }, + "defense": { + "base": 0, + "malus": 0, + "bonus": 0, + "label": "Défense" + }, + "matriseelementaire": { + "base": 0, + "malus": 0, + "bonus": 0, + "label": "Maîtrise élémentaire" + }, + "devotion": { + "base": 0, + "malus": 0, + "bonus": 0, + "label": "Dévotion" + } + } + }, + "background": { + "race": "", + "history": "Background", + "notes": "Notes", + "gmnotes": "Notes du MJ", + "eyes": "", + "hair": "", + "weight": "", + "genre": "", + "age": 0 + } + }, + "character": { + "templates": [ "background", "common" ] + } +}, +"Item": { + "types": ["competence", "attribut", "technique", "sort", "arme", "tir", "armurebouclier", "equipement" ], + "attribut": { + "effect": "", + "xp": 0, + "notes": "" + }, + "technique": { + "condition": "", + "effect": "", + "pacost": 1, + "xp": 0, + "notes": "" + }, + "sort": { + "type": "", + "datachurch": "", + "xp": "", + "pe": "", + "target": "", + "difficulty": "", + "description": "", + "effect": "", + "critical": "", + "notes": "", + "damage": "" + }, + "competence": { + "xp": "", + "base": "", + "bonus": "", + "malus": "", + "description": "" + }, + "arme": { + "type": "", + "damage": "", + "criticaldamage": "", + "description": "", + "enc": 0, + "cost": 0 + }, + "tir": { + "type": "", + "damage": "", + "criticaldamage": "", + "munition": "", + "distance": "", + "description": "", + "enc": 0, + "cost": 0 + }, + "armurebouclier": { + "type": "", + "bonus": "", + "malus": "", + "enc": 0, + "cost": 0 + }, + "equipement": { + "description": "", + "enc": 0, + "cost": 0 + } + } +}