From 30bfe2d1e2bd81055c8aafa8e4780a49ff56ee11 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Thu, 4 Jun 2020 18:27:28 -0400 Subject: [PATCH 01/20] Hooks have been moved to their own module/function. --- module/hooks.js | 117 +++++++++++++++++++++++++++++++++++++++++++++++ numenera.js | 119 ++---------------------------------------------- 2 files changed, 122 insertions(+), 114 deletions(-) create mode 100644 module/hooks.js diff --git a/module/hooks.js b/module/hooks.js new file mode 100644 index 00000000..85f8d366 --- /dev/null +++ b/module/hooks.js @@ -0,0 +1,117 @@ +import { rollText } from './roll.js'; + + +/** + * This function is simply meant to be the place where all hooks are registered. + * + * @export + */ +export function registerHooks() { + + + /* + Display an NPC's difficulty between parentheses in the Actors list + */ + Hooks.on('renderActorDirectory', (app, html, options) => { + const found = html.find(".entity-name"); + + app.entities + .filter(actor => actor.data.type === 'npc') + .forEach(actor => { + found.filter((i, elem) => elem.innerText === actor.data.name) + .each((i, elem) => elem.innerText += ` (${actor.data.data.level * 3})`); + }) + }); + + Hooks.on('renderCompendium', async (app, html, options) => { + const npcs = game.actors.entities.filter(e => e.constructor === NumeneraNPCActor); + + html.find(".entry-name") + .each((i, el) => { + const actor = npcs.find(npc => el.innerText.indexOf(npc.data.name) !== -1); + if (!actor) + return; + + //Display the NPC's target between parentheses + el.innerHTML += ` (${actor.data.data.level * 3})`; + }); + + }); + + Hooks.on("renderChatMessage", (app, html, data) => { + if (!data.message.roll) + return; + + const roll = JSON.parse(data.message.roll); + + //Don't apply ChatMessage enhancement to recovery rolls + if (roll && roll.dice[0].faces === 20) + { + const special = rollText(roll.total); + const dt = html.find("h4.dice-total")[0]; + + //"special" refers to special attributes: minor/major effect or GM intrusion text, special background, etc. + if (special) { + const { text, color } = special; + const newContent = `${text}`; + + $(newContent).insertBefore(dt); + } + + if (game.settings.get("numenera", "d20Rolling") === "taskLevels") { + const rolled = roll.dice[0].rolls[0].roll; + const taskLevel = Math.floor(rolled / 3); + const skillLevel = (roll.total - rolled) / 3; + const sum = taskLevel + skillLevel; + + let text = `${game.i18n.localize("NUMENERA.successLevel")} ${sum}`; + + if (skillLevel !== 0) { + const sign = sum > 0 ? "+" : "-"; + text += ` (${taskLevel}${sign}${skillLevel})`; + } + + dt.textContent = text; + } + + } + }); + + /** + * Add additional system-specific sidebar directory context menu options for D&D5e Actor entities + * @param {jQuery} html The sidebar HTML + * @param {Array} entryOptions The default array of context menu options + */ + Hooks.on("getActorDirectoryEntryContext", (html, entryOptions) => { + entryOptions.push({ + name: game.i18n.localize("NUMENERA.gmIntrusion"), + icon: '', + callback: li => { + const actor = game.actors.get(li.data("entityId")); + const ownerIds = Object.entries(actor.data.permission) + .filter(entry => { + const [id, permissionLevel] = entry; + return permissionLevel >= ENTITY_PERMISSIONS.OWNER + && id !== game.user.id + }) + .map(usersPermissions => usersPermissions[0]); + + game.socket.emit("system.numenera", {type: "gmIntrusion", data: { + userIds: ownerIds, + actorId: actor.data._id, + }}); + + ChatMessage.create({ + content: `

${game.i18n.localize("NUMENERA.gmIntrusion")}


${game.i18n.localize("NUMENERA.gmIntrusionText")} ${actor.data.name}`, + }); + }, + condition: li => { + if (!game.user.isGM) + return false; + + const actor = game.actors.get(li.data("entityId")); + return actor && actor.data.type === "pc"; + } + }); + }); +} \ No newline at end of file diff --git a/numenera.js b/numenera.js index d9551dbb..a4cd32b6 100644 --- a/numenera.js +++ b/numenera.js @@ -13,10 +13,8 @@ import { NumeneraOddityItemSheet } from './module/item/sheets/NumeneraOddityItem import { NumeneraSkillItemSheet } from './module/item/sheets/NumeneraSkillItemSheet.js'; import { NumeneraWeaponItemSheet } from './module/item/sheets/NumeneraWeaponItemSheet.js'; - import { NUMENERA } from './module/config.js'; import { getInitiativeFormula, rollInitiative } from './module/combat.js'; -import { rollText } from './module/roll.js'; import { preloadHandlebarsTemplates } from './module/templates.js'; import { registerSystemSettings } from './module/settings.js'; import { migrateWorld } from './module/migrations/migrate.js'; @@ -24,6 +22,7 @@ import { numeneraSocketListeners } from './module/socket.js'; import { RecoveryDialog } from './module/apps/RecoveryDialog.js'; import { registerHandlebarHelpers } from './module/handlebarHelpers.js'; import { add3rdBarToPCTokens, cypherToken } from './module/token.js'; +import { registerHooks } from './module/hooks.js'; Hooks.once("init", function() { console.log('Numenera | Initializing Numenera System'); @@ -65,119 +64,11 @@ Hooks.once("init", function() { preloadHandlebarsTemplates(); }); +//Place asy clean, well-behaved hook here Hooks.once("init", cypherToken); Hooks.once("ready", add3rdBarToPCTokens); - -//TODO cleanup the functions here, it's gonna get messy real quick - -/* -Display an NPC's difficulty between parentheses in the Actors list -*/ -Hooks.on('renderActorDirectory', (app, html, options) => { - const found = html.find(".entity-name"); - - app.entities - .filter(actor => actor.data.type === 'npc') - .forEach(actor => { - found.filter((i, elem) => elem.innerText === actor.data.name) - .each((i, elem) => elem.innerText += ` (${actor.data.data.level * 3})`); - }) -}); - -Hooks.on('renderCompendium', async (app, html, options) => { - const npcs = game.actors.entities.filter(e => e.constructor === NumeneraNPCActor); - - html.find(".entry-name") - .each((i, el) => { - const actor = npcs.find(npc => el.innerText.indexOf(npc.data.name) !== -1); - if (!actor) - return; - - //Display the NPC's target between parentheses - el.innerHTML += ` (${actor.data.data.level * 3})`; - }); - -}); - -Hooks.on("renderChatMessage", (app, html, data) => { - if (!data.message.roll) - return; - - const roll = JSON.parse(data.message.roll); - - //Don't apply ChatMessage enhancement to recovery rolls - if (roll && roll.dice[0].faces === 20) - { - const special = rollText(roll.total); - const dt = html.find("h4.dice-total")[0]; - - //"special" refers to special attributes: minor/major effect or GM intrusion text, special background, etc. - if (special) { - const { text, color } = special; - const newContent = `${text}`; - - $(newContent).insertBefore(dt); - } - - if (game.settings.get("numenera", "d20Rolling") === "taskLevels") { - const rolled = roll.dice[0].rolls[0].roll; - const taskLevel = Math.floor(rolled / 3); - const skillLevel = (roll.total - rolled) / 3; - const sum = taskLevel + skillLevel; - - let text = `${game.i18n.localize("NUMENERA.successLevel")} ${sum}`; - - if (skillLevel !== 0) { - const sign = sum > 0 ? "+" : "-"; - text += ` (${taskLevel}${sign}${skillLevel})`; - } - - dt.textContent = text; - } - - } -}); - -/** - * Add additional system-specific sidebar directory context menu options for D&D5e Actor entities - * @param {jQuery} html The sidebar HTML - * @param {Array} entryOptions The default array of context menu options - */ -Hooks.on("getActorDirectoryEntryContext", (html, entryOptions) => { - entryOptions.push({ - name: game.i18n.localize("NUMENERA.gmIntrusion"), - icon: '', - callback: li => { - const actor = game.actors.get(li.data("entityId")); - const ownerIds = Object.entries(actor.data.permission) - .filter(entry => { - const [id, permissionLevel] = entry; - return permissionLevel >= ENTITY_PERMISSIONS.OWNER - && id !== game.user.id - }) - .map(usersPermissions => usersPermissions[0]); - - game.socket.emit("system.numenera", {type: "gmIntrusion", data: { - userIds: ownerIds, - actorId: actor.data._id, - }}); - - ChatMessage.create({ - content: `

${game.i18n.localize("NUMENERA.gmIntrusion")}


${game.i18n.localize("NUMENERA.gmIntrusionText")} ${actor.data.name}`, - }); - }, - condition: li => { - if (!game.user.isGM) - return false; - - const actor = game.actors.get(li.data("entityId")); - return actor && actor.data.type === "pc"; - } - }); -}); - -/** - * Once the entire VTT framework is initialized, check to see if we should perform a data migration - */ Hooks.once("ready", migrateWorld); Hooks.once("ready", numeneraSocketListeners); + +//Random hooks should go in there +registerHooks(); \ No newline at end of file From b6b1eab55775080d810b57e8c76e45b272b65fcd Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Thu, 4 Jun 2020 18:29:54 -0400 Subject: [PATCH 02/20] Remove unneeded event handler --- module/actor/sheets/NumeneraPCActorSheet.js | 1 - 1 file changed, 1 deletion(-) diff --git a/module/actor/sheets/NumeneraPCActorSheet.js b/module/actor/sheets/NumeneraPCActorSheet.js index c17ce54f..6eccd5d1 100644 --- a/module/actor/sheets/NumeneraPCActorSheet.js +++ b/module/actor/sheets/NumeneraPCActorSheet.js @@ -359,7 +359,6 @@ export class NumeneraPCActorSheet extends ActorSheet { super.activateListeners(html); const abilitiesTable = html.find("table.abilities"); - abilitiesTable.find("*").off("change"); //TODO remove this brutal thing when transition to 0.5.6+ is done abilitiesTable.on("click", ".ability-create", this.onAbilityCreate.bind(this)); abilitiesTable.on("click", ".ability-delete", this.onAbilityDelete.bind(this)); abilitiesTable.on("blur", "input,select,textarea", this.onAbilityEdit.bind(this)); From c8245bcd6c429cd4ab94819cb620aaa68120069d Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Thu, 4 Jun 2020 21:47:12 -0400 Subject: [PATCH 03/20] Renamed some language properties to avoid plurals problem --- lang/en.json | 24 ++++++++++----------- lang/fr.json | 24 ++++++++++----------- module/actor/sheets/NumeneraPCActorSheet.js | 2 +- module/config.js | 4 ++-- module/item/NumeneraAbilityItem.js | 2 +- templates/actor/characterSheet.html | 20 ++++++++--------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lang/en.json b/lang/en.json index 32e2a6b1..f4061835 100644 --- a/lang/en.json +++ b/lang/en.json @@ -41,18 +41,18 @@ "NUMENERA.pcActorSheet.tab.equipment": "Equipment", "NUMENERA.pcActorSheet.tab.bio": "Bio", - "NUMENERA.abilities.newAbility": "New Ability", - "NUMENERA.abilities.name": "Abilities", - "NUMENERA.abilities.type.action": "Action", - "NUMENERA.abilities.type.enabler": "Enabler", - "NUMENERA.abilities.tab.createTooltip": "Create", - "NUMENERA.abilities.tab.deleteTooltip": "Delete", - "NUMENERA.abilities.tab.rollTooltip": "Ability Roll", - "NUMENERA.abilities.tab.cost": "Cost", - "NUMENERA.abilities.tab.description": "Description", - "NUMENERA.abilities.tab.name": "Name", - "NUMENERA.abilities.tab.range": "Range", - "NUMENERA.abilities.tab.instructions": "No abilities yet. Create one by clicking the icon on the top right of the table or pick one from the Items Directory or a Compendium and drag it here.", + "NUMENERA.item.ability.newAbility": "New Ability", + "NUMENERA.item.ability.name": "Abilities", + "NUMENERA.item.ability.type.action": "Action", + "NUMENERA.item.ability.type.enabler": "Enabler", + "NUMENERA.item.ability.tab.createTooltip": "Create", + "NUMENERA.item.ability.tab.deleteTooltip": "Delete", + "NUMENERA.item.ability.tab.rollTooltip": "Ability Roll", + "NUMENERA.item.ability.tab.cost": "Cost", + "NUMENERA.item.ability.tab.description": "Description", + "NUMENERA.item.ability.tab.name": "Name", + "NUMENERA.item.ability.tab.range": "Range", + "NUMENERA.item.ability.tab.instructions": "No abilities yet. Create one by clicking the icon on the top right of the table or pick one from the Items Directory or a Compendium and drag it here.", "NUMENERA.pc.advances.statPools": "+4 to stat pools", "NUMENERA.pc.advances.effort": "+1 to Effort", diff --git a/lang/fr.json b/lang/fr.json index 9e879fa6..30874608 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -41,18 +41,18 @@ "NUMENERA.pcActorSheet.tab.equipment": "Équipement", "NUMENERA.pcActorSheet.tab.bio": "Bio", - "NUMENERA.abilities.newAbility": "Nouvelle Aptitude", - "NUMENERA.abilities.name": "Aptitudes", - "NUMENERA.abilities.type.action": "Action", - "NUMENERA.abilities.type.enabler": "Catalyseur", - "NUMENERA.abilities.tab.createTooltip": "Créer", - "NUMENERA.abilities.tab.deleteTooltip": "Supprimer", - "NUMENERA.abilities.tab.rollTooltip": "Jet d'aptitude", - "NUMENERA.abilities.tab.cost": "Coût", - "NUMENERA.abilities.tab.description": "Description", - "NUMENERA.abilities.tab.name": "Nom", - "NUMENERA.abilities.tab.range": "Portée", - "NUMENERA.abilities.tab.instructions": "Pas d'aptitudes. Créez-en une en cliquant sur l'icône en haut à droite du tableau ou sélectionnez-en une du ITEMS DIRECTORY ou d'un Compendium et DRAGGEZ-la jusque ici.", + "NUMENERA.item.ability.newAbility": "Nouvelle Aptitude", + "NUMENERA.item.ability.name": "Aptitudes", + "NUMENERA.item.ability.type.action": "Action", + "NUMENERA.item.ability.type.enabler": "Catalyseur", + "NUMENERA.item.ability.tab.createTooltip": "Créer", + "NUMENERA.item.ability.tab.deleteTooltip": "Supprimer", + "NUMENERA.item.ability.tab.rollTooltip": "Jet d'aptitude", + "NUMENERA.item.ability.tab.cost": "Coût", + "NUMENERA.item.ability.tab.description": "Description", + "NUMENERA.item.ability.tab.name": "Nom", + "NUMENERA.item.ability.tab.range": "Portée", + "NUMENERA.item.ability.tab.instructions": "Pas d'aptitudes. Créez-en une en cliquant sur l'icône en haut à droite du tableau ou sélectionnez-en une du ITEMS DIRECTORY ou d'un Compendium et DRAGGEZ-la jusque ici.", "NUMENERA.pc.advances.statPools": "+4 aux Réserves", "NUMENERA.pc.advances.effort": "+1 en Effort", diff --git a/module/actor/sheets/NumeneraPCActorSheet.js b/module/actor/sheets/NumeneraPCActorSheet.js index 6eccd5d1..8e147487 100644 --- a/module/actor/sheets/NumeneraPCActorSheet.js +++ b/module/actor/sheets/NumeneraPCActorSheet.js @@ -32,7 +32,7 @@ function onItemCreate(itemType, itemClass, callback = null) { if (event) event.preventDefault(); - const newName = game.i18n.localize(`NUMENERA.item.${itemType}s.new${itemType.capitalize()}`); + const newName = game.i18n.localize(`NUMENERA.item.${itemType}.new${itemType.capitalize()}`); const itemData = { name: newName, diff --git a/module/config.js b/module/config.js index c46a3b2a..fe91c023 100644 --- a/module/config.js +++ b/module/config.js @@ -122,8 +122,8 @@ NUMENERA.ranges = [ NUMENERA.optionalRanges = ["N/A"].concat(NUMENERA.ranges); NUMENERA.abilityTypes = [ - "NUMENERA.abilities.type.action", - "NUMENERA.abilities.type.enabler", + "NUMENERA.item.ability.type.action", + "NUMENERA.item.ability.type.enabler", ]; NUMENERA.cypherTypes = [ diff --git a/module/item/NumeneraAbilityItem.js b/module/item/NumeneraAbilityItem.js index 8ee1c5a9..ac6af4f8 100644 --- a/module/item/NumeneraAbilityItem.js +++ b/module/item/NumeneraAbilityItem.js @@ -11,7 +11,7 @@ export class NumeneraAbilityItem extends Item { const itemData = this.data.data || {}; - itemData.name = this.data ? this.data.name : game.i18n.localize("NUMENERA.abilities.newAbility"); + itemData.name = this.data ? this.data.name : game.i18n.localize("NUMENERA.item.ability.newAbility"); itemData.category = itemData.category || ""; itemData.categoryValue = itemData.categoryValue || ""; itemData.isAction = itemData.isAction || false; diff --git a/templates/actor/characterSheet.html b/templates/actor/characterSheet.html index d6a89eda..74e58d42 100644 --- a/templates/actor/characterSheet.html +++ b/templates/actor/characterSheet.html @@ -171,11 +171,11 @@

{{localize "NUMENERA.pc.armor.label"}}  {{/if}} - {{localize "NUMENERA.abilities.tab.name"}} - {{localize "NUMENERA.abilities.tab.cost"}} - {{localize "NUMENERA.abilities.tab.range"}} - {{localize "NUMENERA.abilities.tab.description"}} - + {{localize "NUMENERA.item.ability.tab.name"}} + {{localize "NUMENERA.item.ability.tab.cost"}} + {{localize "NUMENERA.item.ability.tab.range"}} + {{localize "NUMENERA.item.ability.tab.description"}} + @@ -195,8 +195,8 @@

{{localize "NUMENERA.pc.armor.label"}} 
@@ -225,8 +225,8 @@

{{localize "NUMENERA.pc.armor.label"}} {{ability.data.notes}} - d20 - + d20 + {{else}} @@ -236,7 +236,7 @@

{{localize "NUMENERA.pc.armor.label"}}  {{/if}} - {{{localize "NUMENERA.abilities.tab.instructions"}}} + {{{localize "NUMENERA.item.ability.tab.instructions"}}} {{/each}} From 41bdf12c9502668b5fcc515cb70e8a2643b292e1 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:25:51 -0400 Subject: [PATCH 04/20] Bug fix: empty Other Items table would not display a message --- templates/actor/characterSheet.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/actor/characterSheet.html b/templates/actor/characterSheet.html index 74e58d42..030d737a 100644 --- a/templates/actor/characterSheet.html +++ b/templates/actor/characterSheet.html @@ -180,6 +180,7 @@

{{localize "NUMENERA.pc.armor.label"}}  @@ -584,7 +585,7 @@

Shins + @@ -609,7 +610,7 @@

Shins - {{{"NUMENERA.pc.otherItems.none"}}} + {{{localize "NUMENERA.pc.otherItems.none"}}} {{/each}} From ed507bf644bf27bc0b4eb87621b3d049cb6045f1 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:29:49 -0400 Subject: [PATCH 05/20] New NPC attack item --- module/item/NumeneraItem.js | 6 ++++++ module/item/NumeneraNPCAttack.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 module/item/NumeneraNPCAttack.js diff --git a/module/item/NumeneraItem.js b/module/item/NumeneraItem.js index 143c1408..44fd7cbb 100644 --- a/module/item/NumeneraItem.js +++ b/module/item/NumeneraItem.js @@ -4,6 +4,7 @@ import { NumeneraArmorItem } from "./NumeneraArmorItem.js"; import { NumeneraCypherItem } from "./NumeneraCypherItem.js"; import { NumeneraEquipmentItem } from "./NumeneraEquipmentItem.js"; import { NumeneraOddityItem } from "./NumeneraOddityItem.js"; +import { NumeneraNpcAttackItem } from "./NumeneraNPCAttack.js"; import { NumeneraSkillItem } from "./NumeneraSkillItem.js"; import { NumeneraWeaponItem } from "./NumeneraWeaponItem.js"; @@ -36,6 +37,8 @@ export const NumeneraItem = new Proxy(function () {}, { return new NumeneraCypherItem(...args); case "equipment": return new NumeneraEquipmentItem(...args); + case "npcattack": + return new NumeneraNpcAttackItem(...args); case "oddity": return new NumeneraOddityItem(...args); case "skill": @@ -61,6 +64,8 @@ export const NumeneraItem = new Proxy(function () {}, { return NumeneraCypherItem.create(data, options); case "equipment": return NumeneraEquipmentItem.create(data, options); + case "npcattack": + return NumeneraNpcAttackItem.create(data, options); case "oddity": return NumeneraOddityItem.create(data, options); case "skill": @@ -80,6 +85,7 @@ export const NumeneraItem = new Proxy(function () {}, { instance instanceof NumeneraCypherItem || instance instanceof NumeneraEquipmentItem || instance instanceof NumeneraOddityItem || + instance instanceof NumeneraNpcAttackItem || instance instanceof NumeneraSkillItem || instance instanceof NumeneraWeaponItem ); diff --git a/module/item/NumeneraNPCAttack.js b/module/item/NumeneraNPCAttack.js new file mode 100644 index 00000000..cafa075d --- /dev/null +++ b/module/item/NumeneraNPCAttack.js @@ -0,0 +1,14 @@ +export class NumeneraNpcAttackItem extends Item { + get type() { + return "npcattack"; + } + + prepareData() { + super.prepareData(); + + const itemData = this.data.data || {}; +debugger; + itemData.notes = itemData.notes || ""; + itemData.info = itemData.info || ""; + } +} \ No newline at end of file From 45c114ad4b0e98c7f61461c1db1f3c1fa0ccebfc Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:30:45 -0400 Subject: [PATCH 06/20] Updated NPC sheet size and removed that disgusting hack I was BURNING from removing out of my sight. A dream come true, at long last. --- module/actor/sheets/NumeneraNPCActorSheet.js | 86 ++------------------ 1 file changed, 8 insertions(+), 78 deletions(-) diff --git a/module/actor/sheets/NumeneraNPCActorSheet.js b/module/actor/sheets/NumeneraNPCActorSheet.js index 13a763a7..df28abf1 100644 --- a/module/actor/sheets/NumeneraNPCActorSheet.js +++ b/module/actor/sheets/NumeneraNPCActorSheet.js @@ -12,8 +12,14 @@ export class NumeneraNPCActorSheet extends ActorSheet { */ static get defaultOptions() { return mergeObject(super.defaultOptions, { - width: 750, - height: 700, + width: 850, + height: 650, + tabs: [ + { + navSelector: ".tabs", + contentSelector: "#npc-sheet-body", + }, + ], }); } @@ -100,80 +106,4 @@ export class NumeneraNPCActorSheet extends ActorSheet { throw new Error("Unhandled case in onAttackControl"); } } - - /** - * Implement the _updateObject method as required by the parent class spec - * This defines how to update the subject of the form when the form is submitted - * - * Mostly handles the funky behavior of dynamic tables inside the form. - * - * @private - */ - async _updateObject(event, formData) { - //TODO this is repeated in NumeneraPCActorSheet, try to abstract all of this a bit plz - const fd = expandObject(formData); - - const formAttacks = fd.data.attacks || {}; - - //*************************** - //DISGUSTING WORKAROUND ALERT - //*************************** - - //TODO FIX THIS SHIT - //For some extra-weird reason, I get NaN sometimes as an ID, so just swap it around - let nAnPatch = 1000; - - for (let at of Object.values(formAttacks)) { - if (typeof at.id !== "string") { - console.warn("Oops! Weird NaN problem here, buddy"); - - //Avoid collisions, in case this is not the first time this happens - while (Object.values(formAttacks).some(at => at.id == nAnPatch)) - ++nAnPatch; - - at.id = nAnPatch.toString(); - ++nAnPatch; - } - } - - //******************************* - //END DISGUSTING WORKAROUND ALERT - //******************************* - - const formDataReduceFunction = function (obj, v) { - if (v.hasOwnProperty("id")) { - const id = v["id"].trim(); - if (id) obj[id] = v; - } - - return obj; - }; - - const attacks = Object.values(formAttacks).reduce(formDataReduceFunction, {}); - - // Remove attacks which are no longer used - for (let at of Object.keys(this.object.data.data.attacks)) { - if (at && !attacks.hasOwnProperty(at)) attacks[`-=${at}`] = null; - } - - // Re-combine formData - formData = Object.entries(formData) - .filter((e) => !e[0].startsWith("data.attacks")) - .reduce( - (obj, e) => { - obj[e[0]] = e[1]; - return obj; - }, - { - _id: this.object._id, - "data.attacks": attacks, - } - ); - - // Update the Actor - await this.object.update(formData); - - //In case the NPC level changed, re-render the ActorDirectory - ui.actors.render(); - } } From 9f69f6069bb6dc85eb633c958418454c2b21f176 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:32:07 -0400 Subject: [PATCH 07/20] New NPC attack items, most descriptive fields moved into the "info" property --- template.json | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/template.json b/template.json index bed80fc5..b76a2613 100644 --- a/template.json +++ b/template.json @@ -50,29 +50,22 @@ }, "npc": { "level": 1, - "attacks": {}, - "description": null, - "motive": null, - "environment": null, + "description": "", + "info": "", "health": { "value": 1, "max": 1 }, - "combat": null, "damage": 0, "armor": 0, "movement": "short", - "modifications": null, - "interaction": null, - "use": null, - "loot": null, - "gmIntrusion": null, + "modifications": "", "image": null, - "version": 1 + "version": 2 } }, "Item": { - "types": ["ability", "armor", "artifact", "cypher", "equipment", "oddity", "skill", "weapon"], + "types": ["ability", "armor", "artifact", "cypher", "equipment", "npcAttack", "oddity", "skill", "weapon"], "templates": { "common": { "notes": "", @@ -128,6 +121,9 @@ "templates": ["common", "sellable"], "quantity": 0 }, + "npcAttack": { + "templates": ["common"] + }, "oddity": { "templates": ["carriable", "common"] }, From 85f5cd3c4be2d0701541ff2f39dedc973cd885ff Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:32:42 -0400 Subject: [PATCH 08/20] NPC sheet update, added mixin for easier editor placement and sizing --- sass/numenera.scss | 72 ++++++++++++++--- templates/actor/npcSheet.html | 148 +++++++++++++++++++--------------- 2 files changed, 141 insertions(+), 79 deletions(-) diff --git a/sass/numenera.scss b/sass/numenera.scss index 2fd78657..1483b5f4 100644 --- a/sass/numenera.scss +++ b/sass/numenera.scss @@ -7,6 +7,20 @@ $foundry-border-color: #782e22; $border-color: #325d6d; $font-stack: Ogirema; +@mixin editor($class, $height, $id) { + div.#{$class} { + height: #{$height}; + + .editor { + height: 100%; + } + + button#save-#{$id} { + width: 99%; + } + } +} + span.numenera-message-special { font-weight: bold; font-size: large; @@ -150,14 +164,35 @@ form.numenera { div > div.npc-info { flex: 0.25; - input.levelDie { - width: 2ch; - align-content: center; + h1 { + margin-left: 0.5em; + margin-right: 0.5em; + + input.charname { + width: 70%; + } + + input.level { + width: 3em; + align-content: center; + } } } div > div.npc-stats { - flex: 0.5; + flex: 0.15; + padding: 1em; + text-align: center; + + label { + padding-top: 2px; + font-size: 16px; + font-weight: bold; + } + + input { + width: 2ch; + } } ul { @@ -342,15 +377,22 @@ form.numenera { border-bottom: 1px solid $border-color; } - div.description { - height: 425px; - - .editor { - height: 100%; - } + //TinyMCE editors + @include editor("description", 425px, "bio"); - button#save-bio { - width: 99%; + $totalDescriptionHeight: 285px; + $buttonHeight: 34px; + $descriptionHeight: $totalDescriptionHeight - $buttonHeight; + + @include editor("npc-description", $descriptionHeight, "npc-description"); + @include editor("npc-info", $descriptionHeight, "npc-info"); + + div.npc-modifications { + display: flex; + flex-direction: column; + + textarea { + flex: 1; } } @@ -404,6 +446,10 @@ form.numenera { margin: 0.5em 0 0.5em 0; border: 1px solid black; border-radius: 8px; + + &.sheet-tabs { + flex: 0; + } } a.item { @@ -645,4 +691,4 @@ form.numenera-recovery { padding: 4px 12px 4px 12px; margin: 8px; border: 1px solid #737373; -} \ No newline at end of file +} diff --git a/templates/actor/npcSheet.html b/templates/actor/npcSheet.html index f82ccb3b..b56469de 100644 --- a/templates/actor/npcSheet.html +++ b/templates/actor/npcSheet.html @@ -13,76 +13,92 @@

-
-

- -

-

- - -

-
-
-

- -

-

- - -

-
-
-
-

{{localize "NUMENERA.npc.attacks.sectionName"}}

- - - - - - - - - - {{#each data.attacks as |attack|}} - - - - + + + + + -
{{localize "NUMENERA.npc.attacks.description"}}
- - -
+ {{/select}} + + + + + +
+
+

{{localize "NUMENERA.npc.modifications"}}

+
-
- - - - - - - - + {{!-- Sheet Tab Navigation --}} + - - -
+
+
+ + + + + + + + + + + {{#each data.attacks as |attack|}} + + + + + + {{else}} + + + + {{/each}} + +
+ + {{localize "NUMENERA.npc.attacks.description"}} + +
+ + + + + +
+ {{{localize "NUMENERA.npc.attacks.none"}}} +
+
+
+ {{editor content=data.notes target="data.notes" button=true editable=editable}} + +
+
+ {{editor content=data.info target="data.info" button=true editable=editable}} + +
+
\ No newline at end of file From d35f353bf9eea53a64ca52cf366445909fdb9a0c Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:38:21 -0400 Subject: [PATCH 09/20] Default structure for info field (felt cleaner than placing it inside the template.js) --- module/actor/NumeneraNPCActor.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/module/actor/NumeneraNPCActor.js b/module/actor/NumeneraNPCActor.js index 769ad24a..dd5f6a42 100644 --- a/module/actor/NumeneraNPCActor.js +++ b/module/actor/NumeneraNPCActor.js @@ -1,4 +1,19 @@ export class NumeneraNPCActor extends Actor { + static defaultInfo() { + return `

${game.i18n.localize("NUMENERA.npc.motive")}

...

+

${game.i18n.localize("NUMENERA.npc.combat")}

...

+

${game.i18n.localize("NUMENERA.npc.interaction")}

...

+

${game.i18n.localize("NUMENERA.npc.use")}

...

+

${game.i18n.localize("NUMENERA.npc.loot")}

...

+

${game.i18n.localize("NUMENERA.npc.gmIntrusion")}

...

`; + } + + constructor(...args) { + super(...args); + + this.data.data.info = this.data.data.info || NumeneraNPCActor.defaultInfo(); + } + getInitiativeFormula() { /* TODO: improve this The init system expects a formula for initiative: fixed values don't seem to work. From 433cb999a3de077ec1efd930e9091403c68557d4 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 09:38:32 -0400 Subject: [PATCH 10/20] Related language changes --- lang/en.json | 14 +++++++++++++- lang/fr.json | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lang/en.json b/lang/en.json index f4061835..2533c0db 100644 --- a/lang/en.json +++ b/lang/en.json @@ -165,12 +165,24 @@ "NUMENERA.npc.movement": "Movement", "NUMENERA.npc.armor": "Armor", "NUMENERA.npc.damage": "Damage", - "NUMENERA.npc.attacks.sectionName": "Attacks", "NUMENERA.npc.attacks.description": "Description", + "NUMENERA.npc.attacks.placeholder": "Attack Description", + "NUMENERA.npc.attacks.createTooltip": "Create", + "NUMENERA.npc.attacks.deleteTooltip": "Delete", + "NUMENERA.npc.attacks.none": "No attacks yet. Create one by clicking the icon on the top right of the table or pick one from the Items Directory or a Compendium and drag it here.", + "NUMENERA.npc.modifications": "Modifications", "NUMENERA.npc.description": "Description", "NUMENERA.npc.use": "Use", "NUMENERA.npc.environment": "Environment", "NUMENERA.npc.loot": "Loot", + "NUMENERA.npc.motive": "Motive", + "NUMENERA.npc.combat": "Combat", + "NUMENERA.npc.interaction": "Interaction", + "NUMENERA.npc.gmIntrusion": "GM Intrusion", + + "NUMENERA.npcActorSheet.tab.attacks": "Attacks", + "NUMENERA.npcActorSheet.tab.description": "Description", + "NUMENERA.npcActorSheet.tab.information": "Information", "NUMENERA.item.ability.isAction": "This ability is an Action", "NUMENERA.item.ability.isEnabler": "This ability is an Enabler", diff --git a/lang/fr.json b/lang/fr.json index 30874608..cdf9492a 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -165,12 +165,24 @@ "NUMENERA.npc.movement": "Mouvement", "NUMENERA.npc.armor": "Armure", "NUMENERA.npc.damage": "Dommages", - "NUMENERA.npc.attacks.sectionName": "Attaques", "NUMENERA.npc.attacks.description": "Description", + "NUMENERA.npc.attacks.placeholder": "Description de l'attaque", + "NUMENERA.npc.attacks.createTooltip": "Créer", + "NUMENERA.npc.attacks.deleteTooltip": "Supprimer", + "NUMENERA.npc.attacks.none": "Pas d'attaques. Créez-en une en cliquant sur l'icône en haut à droite du tableau ou sélectionnez-en une du ITEMS DIRECTORY ou d'un Compendium et DRAGGEZ-la jusque ici.", + "NUMENERA.npc.modifications": "Modifications", "NUMENERA.npc.description": "Description", "NUMENERA.npc.use": "Utilisation", "NUMENERA.npc.environment": "Environnement", "NUMENERA.npc.loot": "Butin", + "NUMENERA.npc.motive": "Motivations", + "NUMENERA.npc.combat": "Combat", + "NUMENERA.npc.interaction": "Interaction", + "NUMENERA.npc.gmIntrusion": "Intrusion du MJ", + + "NUMENERA.npcActorSheet.tab.attacks": "Attaques", + "NUMENERA.npcActorSheet.tab.description": "Description", + "NUMENERA.npcActorSheet.tab.information": "Informations", "NUMENERA.item.ability.isAction": "Cette aptitude est une Action", "NUMENERA.item.ability.isEnabler": "Cette aptitude est un ENABLER", From bc0422fa9e58122b73634dadd6d14f4728c117f6 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 10:19:19 -0400 Subject: [PATCH 11/20] Adding, deleting and editing NPC attacks-as-items now work --- module/actor/sheets/NumeneraNPCActorSheet.js | 121 ++++++++++++++++++- templates/actor/npcSheet.html | 26 ++-- 2 files changed, 126 insertions(+), 21 deletions(-) diff --git a/module/actor/sheets/NumeneraNPCActorSheet.js b/module/actor/sheets/NumeneraNPCActorSheet.js index df28abf1..a0a6cd0b 100644 --- a/module/actor/sheets/NumeneraNPCActorSheet.js +++ b/module/actor/sheets/NumeneraNPCActorSheet.js @@ -1,4 +1,104 @@ +import { confirmDeletion } from "../../apps/ConfirmationDialog.js"; import { NUMENERA } from "../../config.js"; +import { NumeneraNpcAttackItem } from "../../item/NumeneraNPCAttack.js"; + +//TODO copied from PCSheet, should be in a separate, shared file. + + +//Sort function for order +const sortFunction = (a, b) => a.data.order < b.data.order ? -1 : a.data.order > b.data.order ? 1 : 0; + +/** + * Higher order function that generates an item creation handler. + * + * @param {String} itemType The type of the Item (eg. 'ability', 'cypher', etc.) + * @param {*} itemClass + * @param {*} [callback=null] + * @returns + */ +function onItemCreateGenerator(itemType, itemClass, callback = null) { + return async function(event = null) { + if (event) + event.preventDefault(); + + const newName = game.i18n.localize(`NUMENERA.item.${itemType}.new${itemType.capitalize()}`); + + const itemData = { + name: newName, + type: itemType, + data: new itemClass({}), + }; + + const newItem = await this.actor.createOwnedItem(itemData); + if (callback) + callback(newItem); + + return newItem; + } +} + +function onItemEditGenerator(editClass, callback = null) { + return async function (event) { + event.preventDefault(); + event.stopPropagation(); //Important! otherwise we get double rendering + + const elem = event.currentTarget.closest(editClass); + + if (!elem) + throw new Error(`Missing ${editClass} class element`); + else if (!elem.dataset.itemId) + throw new Error(`No itemID on ${editClass} element`); + + const updated = {_id: elem.dataset.itemId}; + + const splitName = event.currentTarget.name.split("."); + const idIndex = splitName.indexOf(updated._id); + const parts = splitName.splice(idIndex + 1); + + //Add the newly added property to the object + //This next block is necessary to support properties at various depths + //e.g support actor.name as well as actor.data.cost.pool + + let previous = updated; + for (let i = 0; i < parts.length; i++) { + const name = parts[i]; + + if (i === parts.length - 1) { + //Last part, the actual property + if (event.target.type === "checkbox") { + previous[name] = event.currentTarget.checked; + } else if (event.target.dataset.dtype === "Boolean") { + previous[name] = (event.currentTarget.value === "true"); + } else { + previous[name] = event.currentTarget.value; + } + } else { + previous[name] = {}; + previous = previous[name]; + } + } + + const updatedItem = await this.actor.updateEmbeddedEntity("OwnedItem", updated); + if (callback) + callback(updatedItem); + } +} + +function onItemDeleteGenerator(deleteType, callback = null) { + return async function (event) { + event.preventDefault(); + + if (await confirmDeletion(deleteType)) { + const elem = event.currentTarget.closest("." + deleteType); + const itemId = elem.dataset.itemId; + const toDelete = this.actor.data.items.find(i => i._id === itemId); + this.actor.deleteOwnedItem(itemId); + + if (callback) + callback(toDelete); + } + } +} /** * Extend the basic ActorSheet class to do all the Numenera things! @@ -23,6 +123,14 @@ export class NumeneraNPCActorSheet extends ActorSheet { }); } + constructor(...args) { + super(...args); + + this.onAttackCreate = onItemCreateGenerator("npcAttack", NumeneraNpcAttackItem); + this.onAttackEdit = onItemEditGenerator(".npcAttack"); + this.onAttackDelete = onItemDeleteGenerator("npcAttack"); + } + /** * Get the correct HTML template path to use for rendering this particular sheet * @type {String} @@ -39,6 +147,12 @@ export class NumeneraNPCActorSheet extends ActorSheet { sheetData.ranges = NUMENERA.ranges.map(r => game.i18n.localize(r)); + sheetData.data.items = sheetData.actor.items || {}; + + const items = sheetData.data.items; + if (!sheetData.data.items.attacks) + sheetData.data.items.attacks = items.filter(i => i.type === "npcAttack").sort(sortFunction); + return sheetData; } @@ -51,9 +165,10 @@ export class NumeneraNPCActorSheet extends ActorSheet { activateListeners(html) { super.activateListeners(html); - html - .find("table.attacks") - .on("click", ".attack-control", this.onAttackControl.bind(this)); + const attacksTable = html.find("table.attacks"); + attacksTable.on("click", ".attack-create", this.onAttackCreate.bind(this)); + attacksTable.on("click", ".attack-delete", this.onAttackDelete.bind(this)); + attacksTable.on("change", "input", this.onAttackEdit.bind(this)); } /** diff --git a/templates/actor/npcSheet.html b/templates/actor/npcSheet.html index b56469de..85f3b292 100644 --- a/templates/actor/npcSheet.html +++ b/templates/actor/npcSheet.html @@ -46,40 +46,30 @@

{{localize "NUMENERA.npc.modifications"}}

- - + - {{#each data.attacks as |attack|}} - + {{#each data.items.attacks as |attack|}} + {{else}} From 3df7b03bbe5fc29fe8cdf4da29561573e3d02af7 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 10:19:52 -0400 Subject: [PATCH 12/20] Bug fix: Foundry would throw an Error because deleted NPC attack items do not have an ItemSheet --- module/actor/sheets/NumeneraNPCActorSheet.js | 57 ++++---------------- numenera.js | 3 ++ 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/module/actor/sheets/NumeneraNPCActorSheet.js b/module/actor/sheets/NumeneraNPCActorSheet.js index a0a6cd0b..b9892374 100644 --- a/module/actor/sheets/NumeneraNPCActorSheet.js +++ b/module/actor/sheets/NumeneraNPCActorSheet.js @@ -172,53 +172,18 @@ export class NumeneraNPCActorSheet extends ActorSheet { } /** - * Handles the click event on add/delete attack controls. - * - * @param {*} event - * @memberof NumeneraNPCActorSheet + * @override */ - async onAttackControl(event) { - event.preventDefault(); - - const a = event.currentTarget; - const action = a.dataset.action; - - switch (action) { - case "create": - const table = a.closest("table"); - const template = table.getElementsByTagName("template")[0]; - const body = table.getElementsByTagName("tbody")[0]; - - if (!template) - throw new Error(`No row template found in attacks table`); - - //Let's keep things simple here: get the largest existing id and add one - const id = - Math.max( - ...[...body.children].map((c) => c.children[0].children[0].value || 0) - ) + 1 + ""; - - const newRow = template.content.cloneNode(true); - body.appendChild(newRow); - - //That "newRow"? A DocumentFragment. AN IMPOSTOR. - const actualRow = body.children[body.children.length - 1]; - actualRow.children[0].children[0].name = `data.attacks.${id}.id`; - actualRow.children[0].children[0].value = id; - actualRow.children[0].children[1].name = `data.attacks.${id}.description`; - - await this._onSubmit(event); - break; - - case "delete": - const row = a.closest(".attack"); - row.parentElement.removeChild(row); - - await this._onSubmit(event); - break; - - default: - throw new Error("Unhandled case in onAttackControl"); + _onDeleteEmbeddedEntity(...args) { + /* Necessary because, after deleting an item, Foundry fetches the Item's sheet + class and, well, NPC attacks don't have one. Intercept the exception and, in that + particular case, ignore it */ + try { + super._onDeleteEmbeddedEntity(...args); + } catch (e) { + if (!e.message.includes("No valid Item sheet found for type npcAttack")) { + throw e; + } } } } diff --git a/numenera.js b/numenera.js index a4cd32b6..0cfa02e1 100644 --- a/numenera.js +++ b/numenera.js @@ -58,6 +58,9 @@ Hooks.once("init", function() { Items.registerSheet("numenera", NumeneraOddityItemSheet, { types: ["oddity"], makeDefault: true }); Items.registerSheet("numenera", NumeneraSkillItemSheet, { types: ["skill"], makeDefault: true }); Items.registerSheet("numenera", NumeneraWeaponItemSheet, { types: ["weapon"], makeDefault: true }); + + //May seem weird but otherwise + Items.registerSheet("numenera", ActorSheet, { types: ["npcAttack"], makeDefault: true }); registerSystemSettings(); registerHandlebarHelpers(); From 90e233240142fdffc3b24f1a9dd63ba6e85ec55d Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 10:20:21 -0400 Subject: [PATCH 13/20] Bug fix: wrong case for NPC attack item type --- module/item/NumeneraItem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/item/NumeneraItem.js b/module/item/NumeneraItem.js index 44fd7cbb..92828807 100644 --- a/module/item/NumeneraItem.js +++ b/module/item/NumeneraItem.js @@ -37,7 +37,7 @@ export const NumeneraItem = new Proxy(function () {}, { return new NumeneraCypherItem(...args); case "equipment": return new NumeneraEquipmentItem(...args); - case "npcattack": + case "npcAttack": return new NumeneraNpcAttackItem(...args); case "oddity": return new NumeneraOddityItem(...args); @@ -64,7 +64,7 @@ export const NumeneraItem = new Proxy(function () {}, { return NumeneraCypherItem.create(data, options); case "equipment": return NumeneraEquipmentItem.create(data, options); - case "npcattack": + case "npcAttack": return NumeneraNpcAttackItem.create(data, options); case "oddity": return NumeneraOddityItem.create(data, options); From 2718bb0189cf649ea07d68b9bab1b452058c483b Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sat, 6 Jun 2020 10:20:21 -0400 Subject: [PATCH 14/20] Bug fix: wrong case for NPC attack item type --- module/item/NumeneraItem.js | 4 ++-- module/item/NumeneraNPCAttack.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/item/NumeneraItem.js b/module/item/NumeneraItem.js index 44fd7cbb..92828807 100644 --- a/module/item/NumeneraItem.js +++ b/module/item/NumeneraItem.js @@ -37,7 +37,7 @@ export const NumeneraItem = new Proxy(function () {}, { return new NumeneraCypherItem(...args); case "equipment": return new NumeneraEquipmentItem(...args); - case "npcattack": + case "npcAttack": return new NumeneraNpcAttackItem(...args); case "oddity": return new NumeneraOddityItem(...args); @@ -64,7 +64,7 @@ export const NumeneraItem = new Proxy(function () {}, { return NumeneraCypherItem.create(data, options); case "equipment": return NumeneraEquipmentItem.create(data, options); - case "npcattack": + case "npcAttack": return NumeneraNpcAttackItem.create(data, options); case "oddity": return NumeneraOddityItem.create(data, options); diff --git a/module/item/NumeneraNPCAttack.js b/module/item/NumeneraNPCAttack.js index cafa075d..0460decc 100644 --- a/module/item/NumeneraNPCAttack.js +++ b/module/item/NumeneraNPCAttack.js @@ -7,7 +7,7 @@ export class NumeneraNpcAttackItem extends Item { super.prepareData(); const itemData = this.data.data || {}; -debugger; + itemData.notes = itemData.notes || ""; itemData.info = itemData.info || ""; } From 06611fcf0afea032b9a3a63fe05db794ee21b979 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sun, 7 Jun 2020 08:47:34 -0400 Subject: [PATCH 15/20] Finished attack migration --- module/migrations/NPCActorMigrations.js | 57 +++++++++++++++++++++++++ module/migrations/migrate.js | 27 ++++++------ templates/actor/npcSheet.html | 4 +- 3 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 module/migrations/NPCActorMigrations.js diff --git a/module/migrations/NPCActorMigrations.js b/module/migrations/NPCActorMigrations.js new file mode 100644 index 00000000..3ef8096d --- /dev/null +++ b/module/migrations/NPCActorMigrations.js @@ -0,0 +1,57 @@ +import { Migrator } from "./Migrator.js"; +import { NumeneraItem } from "../item/NumeneraItem.js"; + +//Keep migrators in order: v1 to v2, v2 to v3, etc. +const NPCActorv1ToV2Migrator = Object.create(Migrator); + +NPCActorv1ToV2Migrator.forVersion = 2; +NPCActorv1ToV2Migrator.forType = "npc"; + +/* Summary of changes: +* - NPC attacks are now items, which will make them easier to manage internally +* - the various properties related to NPC descritions (use, loot, etc) have + been moved to a single "info" property +*/ +NPCActorv1ToV2Migrator.migrationFunction = async function(actor, obj = {}) { + const newData = Object.assign({ _id: actor._id}, obj); + + debugger; + //Convert attack POJOs into Items + const attacks = Object.values(actor.data.data.attacks); + for (let attack of attacks) { + await actor.createOwnedItem({ + type: "npcAttack", + data: { + notes: attack.description, + } + }); + } + + newData["data.-=attacks"] = null; + + let newInfo = ""; + if (actor.data.data.use) + newInfo += `

Use

\n${actor.data.data.use}\n`; + + if (actor.data.data.interaction) + newInfo += `

Interaction

\n${actor.data.data.interaction}\n`; + + if (actor.data.data.loot) + newInfo += `

Loot

\n${actor.data.data.loot}\n`; + + newData["data.info"] = newInfo; + newData["data.-=combat"] = null; + newData["data.-=environment"] = null; + newData["data.-=motive"] = null; + newData["data.-=gmIntrusion"] = null; + newData["data.-=interaction"] = null; + newData["data.-=use"] = null; + newData["data.-=loot"] = null; + + newData["data.version"] = this.forVersion; + + return newData; +}; + +//Only export the latest migrator +export const NPCActorMigrator = NPCActorv1ToV2Migrator; diff --git a/module/migrations/migrate.js b/module/migrations/migrate.js index 66bf0176..f43f0418 100644 --- a/module/migrations/migrate.js +++ b/module/migrations/migrate.js @@ -1,3 +1,4 @@ +import { NPCActorMigrator } from "./NPCActorMigrations.js"; import { PCActorMigrator } from "./PCActorMigrations.js"; import { ItemMigrator } from "./ItemMigrations.js"; @@ -9,7 +10,7 @@ export async function migrateWorld() { return; const currentPCActorVersion = PCActorMigrator.forVersion; - const currentNPCActorVersion = 1; //NPCActorMigrator.forVersion; + const currentNPCActorVersion = NPCActorMigrator.forVersion; const currentItemVersion = Item.forVersion; let pcActors = game.actors.entities.filter(actor => actor.data.type === 'pc' && actor.data.data.version < currentPCActorVersion); @@ -19,6 +20,7 @@ export async function migrateWorld() { if (pcActors && pcActors.length > 0 || npcActors && npcActors.length > 0 || items && items.length > 0) { ui.notifications.info(`Applying Numenera system migrations. Please be patient and do not close your game or shut down your server.`, {permanent: true}); + //TODO these 3 migration blocks are exactly the same, refactor plz try { if (pcActors && pcActors.length > 0) { const updatedPcData = await Promise.all(pcActors.map(async actor => await PCActorMigrator.migrate(actor))); @@ -33,21 +35,20 @@ export async function migrateWorld() { console.error("Error in PC migrations", e); } - //No NPC migrations yet - // try { - // if (npcActors) - // npcActors = await Promise.all(pcActors.map(async actor => await NPCActorMigrator.migrate(actor))); + try { + if (npcActors && npcActors.length > 0) { + const updatedNpcData = await Promise.all(npcActors.map(async actor => await NPCActorMigrator.migrate(actor))); - // for (const npcActor of npcActors) { - // await NumeneraNPCActor.update(npcActor.data); - // } + for (let i = 0; i < npcActors.length; i++) { + await npcActors[i].update(updatedNpcData[i]); + } - // console.log("NPC Actor migration succeeded!"); - // } catch (e) {console.l - // console.error("Error in NPC migrations", e); - // } + console.log("NPC Actor migration succeeded!"); + } + } catch (e) { + console.error("Error in NPC migrations", e); + } - //No separate Item migrations yet try { if (items && items.length > 0) { const updatedItems = await Promise.all(items.map(async item => await ItemMigrator.migrate(item))); diff --git a/templates/actor/npcSheet.html b/templates/actor/npcSheet.html index 85f3b292..dff0d761 100644 --- a/templates/actor/npcSheet.html +++ b/templates/actor/npcSheet.html @@ -61,12 +61,12 @@

{{localize "NUMENERA.npc.modifications"}}

{{#each data.items.attacks as |attack|}} - +
{{localize "NUMENERA.npc.attacks.description"}} - + {{localize "NUMENERA.npc.attacks.description"}} + +
- + - +
- + From 4843d6bbf2362fe0fdcd81a85da4f0921235dcb4 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sun, 7 Jun 2020 08:55:30 -0400 Subject: [PATCH 16/20] Bug fix: missing localizations --- lang/en.json | 4 ++++ lang/fr.json | 4 ++++ templates/actor/characterSheet.html | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lang/en.json b/lang/en.json index f4061835..8fc11a74 100644 --- a/lang/en.json +++ b/lang/en.json @@ -78,7 +78,11 @@ "NUMENERA.pc.numenera.artifact.name": "Artifacts", "NUMENERA.pc.numenera.artifact.none": "No artifacts", "NUMENERA.pc.numenera.artifact.roll": "Roll", + "NUMENERA.pc.numenera.artifact.effect": "Effect", + "NUMENERA.pc.numenera.artifact.form": "Form", "NUMENERA.pc.numenera.artifact.unidentified": "Unidentified Artifact", + "NUMENERA.pc.numenera.artifact.identified": "Identified?", + "NUMENERA.pc.numenera.artifact.level": "Level", "NUMENERA.pc.numenera.cypher.deleteTooltip": "Delete", "NUMENERA.pc.numenera.cypher.effect": "Effect", diff --git a/lang/fr.json b/lang/fr.json index 30874608..2ce55f74 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -78,7 +78,11 @@ "NUMENERA.pc.numenera.artifact.name": "Artefacts", "NUMENERA.pc.numenera.artifact.none": "Pas d'artefacts", "NUMENERA.pc.numenera.artifact.roll": "Jet", + "NUMENERA.pc.numenera.artifact.effect": "Effet", + "NUMENERA.pc.numenera.artifact.form": "Forme", "NUMENERA.pc.numenera.artifact.unidentified": "Artefact non identifié", + "NUMENERA.pc.numenera.artifact.identified": "Identifié ?", + "NUMENERA.pc.numenera.artifact.level": "Niveau", "NUMENERA.pc.numenera.cypher.deleteTooltip": "Supprimer", "NUMENERA.pc.numenera.cypher.effect": "Effet", diff --git a/templates/actor/characterSheet.html b/templates/actor/characterSheet.html index 74e58d42..4f1535ca 100644 --- a/templates/actor/characterSheet.html +++ b/templates/actor/characterSheet.html @@ -272,11 +272,11 @@

{{cypher.name}}

{{#if cypher.editable}} -

- {{localize "NUMENERA.pc.numenera.form"}}: {{cypher.data.form}} + {{localize "NUMENERA.pc.numenera.cypher.form"}}: {{cypher.data.form}}

- {{localize "NUMENERA.pc.numenera.effect"}}: {{cypher.data.effect}} + {{localize "NUMENERA.pc.numenera.cypher.effect"}}: {{cypher.data.effect}}

@@ -340,11 +340,11 @@

{{artifact.name}}

{{#if artifact.editable}} -

- {{localize "NUMENERA.pc.numenera.form"}}: {{artifact.data.form}} + {{localize "NUMENERA.pc.numenera.artifact.form"}}: {{artifact.data.form}}

- {{localize "NUMENERA.pc.numenera.effect"}}: {{artifact.data.effect}} + {{localize "NUMENERA.pc.numenera.artifact.effect"}}: {{artifact.data.effect}}

{{#if artifact.data.depletion.isDepleting}}

From e6429d9f2745aee13db15fa26208e46076b2388f Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sun, 7 Jun 2020 09:29:02 -0400 Subject: [PATCH 17/20] Bug fix: init skill rolls would use the die roll + bonus value instead of the "nat" value. Cleared some TODOs. --- module/actor/NumeneraPCActor.js | 40 ++++++++++----------- module/actor/sheets/NumeneraPCActorSheet.js | 4 +-- module/hooks.js | 4 +-- module/roll.js | 10 ++++-- module/socket.js | 1 - 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/module/actor/NumeneraPCActor.js b/module/actor/NumeneraPCActor.js index fa3a0cf5..a01a3a2f 100644 --- a/module/actor/NumeneraPCActor.js +++ b/module/actor/NumeneraPCActor.js @@ -1,4 +1,4 @@ -import { numeneraRoll } from "../roll.js"; +import { numeneraRoll, numeneraRollFormula } from "../roll.js"; const effortObject = { cost: 0, @@ -19,21 +19,11 @@ export class NumeneraPCActor extends Actor { } getInitiativeFormula() { - //TODO: use numeneraRoll() here instead of duplicating roll logic - //Check for an initiative skill - const initSkill = this.items.find(i => i.type === "skill" && i.name.toLowerCase() === "Initiative") - let initSkillLevel = 0; - if (initSkill) - initSkillLevel = 3 * this.getSkillLevel(initSkill); - - //TODO possible assets, effort on init roll - let formula = "1d20" - if (initSkill !== 0) { - formula += `+${initSkillLevel}`; - } + const initSkill = this.items.find(i => i.type === "skill" && i.name.toLowerCase() === "initiative") - return formula; + //TODO possible assets, effort on init roll + return this.getSkillFormula(initSkill); } get effort() { @@ -56,6 +46,19 @@ export class NumeneraPCActor extends Actor { }).length; } + getSkillFormula(skill) { + if (!skill) + return; + + const skillLevel = this.getSkillLevel(skill); + return numeneraRollFormula(skillLevel); + } + + rollSkillById(skillId) { + const skill = this.getOwnedItem(skillId); + return this.rollSkill(skill); + } + /** * Given a skill ID, fetch the skill level bonus and roll a d20, adding the skill * bonus. @@ -64,8 +67,8 @@ export class NumeneraPCActor extends Actor { * @returns * @memberof NumeneraPCActor */ - rollSkill(skillId) { - if (!skillId) + rollSkill(skill) { + if (!skill) return; switch (this.data.data.damageTrack) { @@ -78,10 +81,7 @@ export class NumeneraPCActor extends Actor { return; } - const skill = this.getOwnedItem(skillId); - const skillLevel = this.getSkillLevel(skill); - - const roll = numeneraRoll(skillLevel); + const roll = new Roll(this.getSkillFormula(skill)).roll(); roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), diff --git a/module/actor/sheets/NumeneraPCActorSheet.js b/module/actor/sheets/NumeneraPCActorSheet.js index 8e147487..938c5fbe 100644 --- a/module/actor/sheets/NumeneraPCActorSheet.js +++ b/module/actor/sheets/NumeneraPCActorSheet.js @@ -438,7 +438,7 @@ export class NumeneraPCActorSheet extends ActorSheet { event.preventDefault(); const skillId = event.target.closest(".skill").dataset.itemId; - return this.actor.rollSkill(skillId); + return this.actor.rollSkillById(skillId); } onAbilityUse(event) { @@ -455,7 +455,7 @@ export class NumeneraPCActorSheet extends ActorSheet { return; } - return this.actor.rollSkill(skill._id); + return this.actor.rollSkillById(skill._id); } onArtifactDepletionRoll(event) { diff --git a/module/hooks.js b/module/hooks.js index 85f8d366..7996ac0b 100644 --- a/module/hooks.js +++ b/module/hooks.js @@ -47,7 +47,7 @@ export function registerHooks() { //Don't apply ChatMessage enhancement to recovery rolls if (roll && roll.dice[0].faces === 20) { - const special = rollText(roll.total); + const special = rollText(roll.dice[0].rolls[0].roll); const dt = html.find("h4.dice-total")[0]; //"special" refers to special attributes: minor/major effect or GM intrusion text, special background, etc. @@ -67,7 +67,7 @@ export function registerHooks() { let text = `${game.i18n.localize("NUMENERA.successLevel")} ${sum}`; if (skillLevel !== 0) { - const sign = sum > 0 ? "+" : "-"; + const sign = sum > 0 ? "+" : ""; text += ` (${taskLevel}${sign}${skillLevel})`; } diff --git a/module/roll.js b/module/roll.js index 611aed9d..fcab2d3c 100644 --- a/module/roll.js +++ b/module/roll.js @@ -2,11 +2,15 @@ Rolls a d20 and then determines your success level. */ -export function numeneraRoll(level = 0) { +export function numeneraRollFormula(level = 0) { let formula = "d20"; - if (level) formula += "+" + 3 * level; + if (level > 0) formula += "+" + 3 * level; + + return formula; +} - return new Roll(formula).roll(); +export function numeneraRoll(level = 0) { + return new Roll(numeneraRollFormula(level)); } export function rollText(dieRoll) { diff --git a/module/socket.js b/module/socket.js index ab8265e4..48949f6d 100644 --- a/module/socket.js +++ b/module/socket.js @@ -13,7 +13,6 @@ function handleGMIntrusion(args) { return; //TODO disable or don't show Refuse button if PC has 0 XP - //TODO display message for everyone about 1) intrusion and 2) choice const actor = game.actors.entities.find(a => a.data._id === actorId); const dialog = new GMIntrusionDialog(actor); dialog.render(true); From 010ed8342f4656457605d0477ba8135056f97b22 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sun, 7 Jun 2020 10:05:09 -0400 Subject: [PATCH 18/20] Trademark notices to respect MCG's Fan Use Policy --- module/hooks.js | 7 ++++++- module/settings.js | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/module/hooks.js b/module/hooks.js index 7996ac0b..ef4eb2cd 100644 --- a/module/hooks.js +++ b/module/hooks.js @@ -7,7 +7,12 @@ import { rollText } from './roll.js'; * @export */ export function registerHooks() { - + Hooks.on("ready", () => ui.notifications.info( + `Numenera and its logo are trademarks of Monte Cook Games, LLC in the U.S.A. and other countries. + All Monte Cook Games characters and character names, and the distinctive likenesses thereof, + are trademarks of Monte Cook Games, LLC. Content derived from Monte Cook Games publications is + © 2013-2019 Monte Cook Games, LLC.`) + ); /* Display an NPC's difficulty between parentheses in the Actors list diff --git a/module/settings.js b/module/settings.js index 1e4f6059..e70037bb 100644 --- a/module/settings.js +++ b/module/settings.js @@ -79,4 +79,13 @@ export const registerSystemSettings = function() { type: Boolean, default: true }); + + game.settings.register("numenera", "trademarkNotice", { + name: "Trademark Notice", + hint: "The Monte Cook Games logo, Numenera, and the Numenera logo are trademarks of Monte Cook Games, LLC in the U.S.A. and other countries. All Monte Cook Games characters and character names, and the distinctive likenesses thereof, are trademarks of Monte Cook Games, LLC. Content on this site or associated files derived from Monte Cook Games publications is © 2013-2019 Monte Cook Games, LLC. Monte Cook Games permits web sites and similar fan-created publications for their games, subject to the policy given at http://www.montecookgames.com/fan-use-policy/. The contents of this site are for personal, non-commercial use only. Monte Cook Games is not responsible for this site or any of the content, that did not originate directly from Monte Cook Games, on or in it. Use of Monte Cook Games’s trademarks and copyrighted materials anywhere on this site and its associated files should not be construed as a challenge to those trademarks or copyrights. Materials on this site may not be reproduced or distributed except with the permission of the site owner and in compliance with Monte Cook Games policy given at http://www.montecookgames.com/fan-use-policy/.", + scope: "world", + config: true, + type: null, + default: null + }); } From 73bccae37d1303dd4962ec02133e66d129dfa509 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sun, 7 Jun 2020 10:12:32 -0400 Subject: [PATCH 19/20] Version 0.11.0 --- package.json | 2 +- system.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1bdea2e5..eef481fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "numenera-foundryvtt", - "version": "0.10.0", + "version": "0.11.0", "description": "Support for the Numenera role playing game for the Foundry virtual tabletop", "dependencies": {}, "devDependencies": { diff --git a/system.json b/system.json index cc5d9cf0..75553c30 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "name": "numenera", "title": "Numenera", "description": "Simple support for Numenera for the Fountry Virtual TableTop.", - "version": "0.10.0", + "version": "0.11.0", "author": "SolarBear", "scripts": ["lib/dragula/dragula.min.js"], "esmodules": ["numenera.js"], @@ -21,9 +21,9 @@ } ], "socket": true, - "compatibleCoreVersion": "0.6.1", + "compatibleCoreVersion": "0.6.2", "minimumCoreVersion" : "0.5.5", "url": "https://github.com/SolarBear/Numenera-FoundryVTT", "manifest": "https://raw.githubusercontent.com/SolarBear/Numenera-FoundryVTT/master/system.json", - "download": "https://github.com/SolarBear/Numenera-FoundryVTT/releases/download/0.10.0/numenera-foundryvtt-0.10.0.zip" + "download": "https://github.com/SolarBear/Numenera-FoundryVTT/releases/download/0.11.0/numenera-foundryvtt-0.11.0.zip" } From ab32e705561f89c93a3d76dec67a64acd3ba7335 Mon Sep 17 00:00:00 2001 From: SolarBear <7608394+SolarBear@users.noreply.github.com> Date: Sun, 7 Jun 2020 10:15:20 -0400 Subject: [PATCH 20/20] Removed leftover debug --- module/migrations/NPCActorMigrations.js | 1 - 1 file changed, 1 deletion(-) diff --git a/module/migrations/NPCActorMigrations.js b/module/migrations/NPCActorMigrations.js index 3ef8096d..707285f2 100644 --- a/module/migrations/NPCActorMigrations.js +++ b/module/migrations/NPCActorMigrations.js @@ -15,7 +15,6 @@ NPCActorv1ToV2Migrator.forType = "npc"; NPCActorv1ToV2Migrator.migrationFunction = async function(actor, obj = {}) { const newData = Object.assign({ _id: actor._id}, obj); - debugger; //Convert attack POJOs into Items const attacks = Object.values(actor.data.data.attacks); for (let attack of attacks) {