diff --git a/lang/en.json b/lang/en.json index 32e2a6b1..b1430083 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", @@ -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", @@ -165,12 +169,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 9e879fa6..c45e5945 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", @@ -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", @@ -165,12 +169,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", 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 `
...
+...
+...
+...
+...
+...
`; + } + + 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. 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/NumeneraNPCActorSheet.js b/module/actor/sheets/NumeneraNPCActorSheet.js index 13a763a7..b9892374 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! @@ -12,11 +112,25 @@ 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", + }, + ], }); } + 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} @@ -33,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; } @@ -45,135 +165,25 @@ 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)); } /** - * 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"); - } - } - - /** - * 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; + _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; } } - - //******************************* - //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(); } } diff --git a/module/actor/sheets/NumeneraPCActorSheet.js b/module/actor/sheets/NumeneraPCActorSheet.js index c17ce54f..938c5fbe 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, @@ -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)); @@ -439,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) { @@ -456,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/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/hooks.js b/module/hooks.js new file mode 100644 index 00000000..ef4eb2cd --- /dev/null +++ b/module/hooks.js @@ -0,0 +1,122 @@ +import { rollText } from './roll.js'; + + +/** + * This function is simply meant to be the place where all hooks are registered. + * + * @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 + */ + 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.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. + if (special) { + const { text, color } = special; + const newContent = ` `; + + $(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: `- {{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}}
- {{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}}@@ -584,7 +585,7 @@
-
-
-
-