From 86c20bf3e479e898e3b36819c2f3eac7b6d8b129 Mon Sep 17 00:00:00 2001 From: Mykhailo Budiakov Date: Thu, 10 Jul 2025 21:50:36 +0300 Subject: [PATCH] add hbs for percentile roll add skillPath for optimization checks update lang files add market skill feature --- css/deltagreen.css | 34 +++++- lang/ca.json | 5 +- lang/cn.json | 5 +- lang/de.json | 5 +- lang/en.json | 3 + lang/es.json | 5 +- lang/fr.json | 5 +- lang/ja.json | 5 +- lang/pl.json | 5 +- lang/pt-BR.json | 5 +- lang/sv.json | 5 +- module/deltagreen.js | 13 +++ module/other/inline.js | 36 ++++++ module/other/utility-functions.js | 11 ++ module/roll/roll.js | 178 +++++++++++++++-------------- templates/roll/percentile-roll.hbs | 30 +++++ 16 files changed, 256 insertions(+), 94 deletions(-) create mode 100644 module/other/inline.js create mode 100644 templates/roll/percentile-roll.hbs diff --git a/css/deltagreen.css b/css/deltagreen.css index bcb0e59..ded1f8e 100644 --- a/css/deltagreen.css +++ b/css/deltagreen.css @@ -1235,4 +1235,36 @@ body:not(.no-theme) #interface:not(.theme-light) #action-bar .slot.full:hover { .target-proficiency-mod { color: #247c14; -} \ No newline at end of file +} + +.rollback-section { + order: 99; + display: flex; + margin-top: 5px; + align-items: center; + justify-content: center; + + > i { + margin: 5px; + } +} + +[data-action="rollback-skill-failure-state"] { + align-items: center; + display: inline-flex; + justify-content: center; + min-height: unset; + width: 20px; + height: 20px; + margin-left: 5px; + font-size: 12px; +} + +[data-tooltip] { + position: relative; + cursor: pointer; +} + +.strike { + text-decoration: line-through; +} diff --git a/lang/ca.json b/lang/ca.json index c358c52..53045db 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -271,5 +271,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Activation Time", "DG.ItemWindow.Ritual.ActivationCosts": "Activation Costs", "DG.ItemWindow.Ritual.StudyTime": "Study Time", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage", + + "DG.Messages.Skillsmark.Marked": "Aquesta habilitat està marcada", + "DG.Messages.Skillsmark.Tooltip": "Vegeu la p. 29 del Manual de l'agent" } diff --git a/lang/cn.json b/lang/cn.json index 935569d..a630732 100644 --- a/lang/cn.json +++ b/lang/cn.json @@ -309,5 +309,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "激活耗时", "DG.ItemWindow.Ritual.ActivationCosts": "激活代价", "DG.ItemWindow.Ritual.StudyTime": "学习时长", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "学习造成的理智损失" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "学习造成的理智损失", + + "DG.Messages.Skillsmark.Marked": "此技能被标记", + "DG.Messages.Skillsmark.Tooltip": "参见第 29 页《代理人手册》" } diff --git a/lang/de.json b/lang/de.json index a26da88..972e302 100644 --- a/lang/de.json +++ b/lang/de.json @@ -289,5 +289,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Activation Time", "DG.ItemWindow.Ritual.ActivationCosts": "Activation Costs", "DG.ItemWindow.Ritual.StudyTime": "Study Time", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage", + + "DG.Messages.Skillsmark.Marked": "Diese Fähigkeit ist gekennzeichnet.", + "DG.Messages.Skillsmark.Tooltip": "Siehe S. 29 des Agentenhandbuchs" } diff --git a/lang/en.json b/lang/en.json index 26a047a..f9bdbf7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -391,4 +391,7 @@ "DG.Settings.hypergeometry.hint": "Always show Hypergeometry item section for players, even if they don't have any items of that type.", "DG.Settings.landscapes.name": "Show Impossible Landscapes Content", "DG.Settings.landscapes.hint": "Show Impossible Landscapes-specific fields from character sheets." + + "DG.Messages.Skillsmark.Marked": "This skill is marked", + "DG.Messages.Skillsmark.Tooltip": "See p. 29 Agent's Handbook" } diff --git a/lang/es.json b/lang/es.json index 0ae20b8..a976872 100644 --- a/lang/es.json +++ b/lang/es.json @@ -276,5 +276,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Activation Time", "DG.ItemWindow.Ritual.ActivationCosts": "Activation Costs", "DG.ItemWindow.Ritual.StudyTime": "Study Time", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage", + + "DG.Messages.Skillsmark.Marked": "Esta habilidad está marcada.", + "DG.Messages.Skillsmark.Tooltip": "Ver pág. 29 del Manual del Agente." } diff --git a/lang/fr.json b/lang/fr.json index 554a272..8e68ba9 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -313,5 +313,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Temp d'Activation", "DG.ItemWindow.Ritual.ActivationCosts": "Coût d'Activation", "DG.ItemWindow.Ritual.StudyTime": "Temps d'étude", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Dégâts SAN Étude" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Dégâts SAN Étude", + + "DG.Messages.Skillsmark.Marked": "Cette compétence est marquée.", + "DG.Messages.Skillsmark.Tooltip": "Voir p. 29 du Manuel de l'agent." } diff --git a/lang/ja.json b/lang/ja.json index 1936dc5..54107ca 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -352,5 +352,8 @@ "DG.Skills.ruby.swim": "すいえい", "DG.Skills.ruby.unarmed_combat": "としゅかくとう", "DG.Skills.ruby.unnatural": "ひしぜん", - "DG.Skills.ruby.flight": "ひこう" + "DG.Skills.ruby.flight": "ひこう", + + "DG.Messages.Skillsmark.Marked": "このスキルはマークされています", + "DG.Messages.Skillsmark.Tooltip": "エージェントハンドブック29ページ参照" } diff --git a/lang/pl.json b/lang/pl.json index 2c04d39..9cd3841 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -313,5 +313,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Czas Aktywacji", "DG.ItemWindow.Ritual.ActivationCosts": "Koszt Aktywacji", "DG.ItemWindow.Ritual.StudyTime": "Czas Nauki", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Utrata PP po Nauce" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Utrata PP po Nauce", + + "DG.Messages.Skillsmark.Marked": "Ta umiejętność jest oznaczona.", + "DG.Messages.Skillsmark.Tooltip": "Zobacz str. 29 Podręcznika Agenta." } diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 8bfea92..8ab62a1 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -311,5 +311,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Tempo de Ativação", "DG.ItemWindow.Ritual.ActivationCosts": "Custos de Ativação", "DG.ItemWindow.Ritual.StudyTime": "Tempo de Estudo", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Dano de SAN ao Aprender" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Dano de SAN ao Aprender", + + "DG.Messages.Skillsmark.Marked": "Esta perícia é marcada", + "DG.Messages.Skillsmark.Tooltip": "Veja pág. 29 do Manual do Agente" } diff --git a/lang/sv.json b/lang/sv.json index 033f7b0..c87f62a 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -311,5 +311,8 @@ "DG.ItemWindow.Ritual.ActivationTime": "Aktiveringstid", "DG.ItemWindow.Ritual.ActivationCosts": "Aktiveringskostnader", "DG.ItemWindow.Ritual.StudyTime": "Studietid", - "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage" + "DG.ItemWindow.Ritual.LearnedSanityDamage": "Learn SAN Damage", + + "DG.Messages.Skillsmark.Marked": "Denna färdighet är markerad", + "DG.Messages.Skillsmark.Tooltip": "Se s. 29 Agenthandbok" } diff --git a/module/deltagreen.js b/module/deltagreen.js index 8f91de9..aecf462 100644 --- a/module/deltagreen.js +++ b/module/deltagreen.js @@ -15,6 +15,8 @@ import { rollSkillMacro, rollSkillTestAndDamageForOwnedItem, } from "./other/macro-functions.js"; +import DGUtils from "./other/utility-functions.js"; +import { handleInlineActions } from "./other/inline.js"; Hooks.once("init", async () => { game.deltagreen = { @@ -197,3 +199,14 @@ Hooks.on("renderGamePause", function (_, html, options) { html.querySelector("img").classList.remove("fa-spin"); // I don't like the logo spinning personally } catch {} }); + +Hooks.on("renderChatLog", async (app, element, context, options) => { + element.addEventListener("click", (event) => { + let btnWithAction = DGUtils.htmlClosest(event.target, "button[data-action]"); + let message = DGUtils.htmlClosest(event.target, "li[data-message-id]"); + + if (btnWithAction && message && message?.dataset?.messageId) { + handleInlineActions(btnWithAction, message?.dataset?.messageId); + } + }); +}); \ No newline at end of file diff --git a/module/other/inline.js b/module/other/inline.js new file mode 100644 index 0000000..aa6fe44 --- /dev/null +++ b/module/other/inline.js @@ -0,0 +1,36 @@ +export async function handleInlineActions(btnWithAction, messageId) { + let action = btnWithAction.dataset?.action; + let message = game.messages.get(messageId); + let actor = message?.speakerActor; + if (!action || !message || !actor) { + return; + } + + if (action === 'rollback-skill-failure-state') { + let rollbackFlag = foundry.utils.deepClone(message.getFlag("deltagreen", "rollbacks")); + await actor.update(foundry.utils.deepClone(rollbackFlag)); + + toggleAllSkillFailures(rollbackFlag) + + let label = btnWithAction + .closest(".rollback-section") + ?.querySelector("label"); + let oldHtml = label.outerHTML; + label.classList.toggle('strike'); + + message.update({ + [`flags.deltagreen.rollbacks`]: rollbackFlag, + content: message.content + .replace(oldHtml, label.outerHTML) + }) + } +} + +function toggleAllSkillFailures(data) { + for (const skill of Object.values(data.system?.skills || {})) { + skill.failure = !skill.failure; + } + for (const skill of Object.values(data.system?.typedSkills || {})) { + skill.failure = !skill.failure; + } +} \ No newline at end of file diff --git a/module/other/utility-functions.js b/module/other/utility-functions.js index dabf602..887bb30 100644 --- a/module/other/utility-functions.js +++ b/module/other/utility-functions.js @@ -33,4 +33,15 @@ export default class DGUtils { static formatStringWithLeadingPlus(number) { return number > 0 ? `+${number}` : number.toString(); } + + static htmlClosest(element, selectors) { + if (element instanceof Element) { + return element.closest(selectors); + } + return null + } + + static getValueByPath(obj, path) { + return path.split('.').reduce((acc, key) => acc?.[key], obj); + } } diff --git a/module/roll/roll.js b/module/roll/roll.js index cb39c47..4648359 100644 --- a/module/roll/roll.js +++ b/module/roll/roll.js @@ -77,12 +77,13 @@ export class DGPercentileRoll extends DGRoll { super("1D100", {}, options); // Set roll info for Skill, Stat, Typed Skill, and non-custom Weapon Percentile rolls. - const { target, localizedKey } = this.getRollInfoFromKey( + const { target, localizedKey, skillPath } = this.getRollInfoFromKey( this.key, this.actor.system, ); this.target = target; this.localizedKey = localizedKey; + this.skillPath = skillPath; // Set roll info for other Percentile rolls switch (this.type) { @@ -272,50 +273,7 @@ export class DGPercentileRoll extends DGRoll { this.options.rollMode = "blindroll"; } - let label = `${game.i18n.localize("DG.Roll.Rolling")} ${ - this.localizedKey - }
${game.i18n.localize("DG.Roll.Target")} ${this.effectiveTarget}%`; - // "Inhuman" stat being rolled. See function for details. - if (this.isInhuman) { - label = `${game.i18n.localize("DG.Roll.Rolling")} ${ - this.localizedKey - } [${game.i18n - .localize("DG.Roll.Inhuman") - .toUpperCase()}] ${game.i18n.localize("DG.Roll.Target")} ${ - this.effectiveTarget - }`; - } - - let isExhausted = false; - let exhaustedCheckPenalty = -20; - - try { - // I suspect (but am not entirely certain) that being tired doesn't make you less lucky) - if (this.type !== "luck") { - isExhausted = this.actor.system.physical.exhausted; - exhaustedCheckPenalty = this.actor.system.physical.exhaustedPenalty; - exhaustedCheckPenalty = -1 * Math.abs(exhaustedCheckPenalty); - } - } catch { - isExhausted = false; - exhaustedCheckPenalty = -20; - } - - if (this.modifier || isExhausted) { - label += ` (${this.target}%`; - - if (this.modifier) { - label += `${DGUtils.formatStringWithLeadingPlus(this.modifier)}%`; - } - - if (isExhausted) { - label += `${DGUtils.formatStringWithLeadingPlus( - exhaustedCheckPenalty, - )}%`; - } - - label += `)`; - } + let label = this.createLabel(); let resultString = ""; let styleOverride = ""; @@ -338,31 +296,36 @@ export class DGPercentileRoll extends DGRoll { } } - let html = ""; - html += `
`; - html += `
`; - html += `
${resultString}
`; - html += `
`; - html += `
`; - html += `
`; - html += `
`; - html += ` `; - html += ` ${this.formula}`; - html += ` `; - html += ` `; - html += ` ${this.total}`; - html += ` `; - html += `
`; - html += `
    `; - html += `
  1. ${this.total}
  2. `; - html += `
`; - html += `
`; - html += `
`; - html += `
`; - html += `

${this.total}

`; - html += `
`; + let failureMark = !this.isSuccess && this.skillPath + && !DGUtils.getValueByPath(this.actor, this.skillPath).failure; - return this.toMessage({ content: html, flavor: label }); + const html = await renderTemplate( + "systems/deltagreen/templates/roll/percentile-roll.hbs", + { + styleOverride, + resultString, + formula: this.formula, + total: this.total, + failureMark + }); + + //TODO: add setting for it? + if (failureMark) { + let keyForUpdate = `${this.skillPath}.failure`; + + //TODO: auto-update actor or post icon with manual apply + await this.actor.update({ + [keyForUpdate]: true + }) + + return this.toMessage({ flags: { deltagreen: { + rollbacks: { + [keyForUpdate]: false + } + } }, content: html, flavor: label }); + } else { + return this.toMessage({ content: html, flavor: label }); + } } /** @@ -382,6 +345,7 @@ export class DGPercentileRoll extends DGRoll { let target = null; let localizedKey = null; + let skillPath = null;//for optimization failure checks if (statKeys.includes(this.key)) { target = actorData.statistics[this.key].x5; localizedKey = game.i18n.localize(`DG.Attributes.${this.key}`); @@ -390,17 +354,76 @@ export class DGPercentileRoll extends DGRoll { // use calculated target proficiency (effects and etc like aim + 20%) target = actorData.skills[this.key].targetProficiency || actorData.skills[this.key].proficiency; localizedKey = game.i18n.localize(`DG.Skills.${this.key}`); + skillPath = `system.skills.${this.key}`; } if (typedSkillKeys.includes(this.key)) { const skill = actorData.typedSkills[this.key]; target = skill.proficiency; localizedKey = `${skill.group} (${skill.label})`; + skillPath = `system.typedSkills.${this.key}`; } if (this.key === "ritual") { target = actorData.sanity.ritual; localizedKey = game.i18n.localize(`DG.Skills.ritual`); } - return { target, localizedKey }; + return { target, localizedKey, skillPath }; + } + + /** + * Create label based on result of roll + * + * todo: do we want make isInhuman more similar to base label? + * + * @returns {string} + */ + createLabel() { + let startOfLabel = game.i18n.localize("DG.Roll.Rolling") + + ` ${this.localizedKey}` + let endOfLabel =`${game.i18n.localize("DG.Roll.Target")} ${this.effectiveTarget}` + + let label = this.isInhuman + // "Inhuman" stat being rolled. See function for details. + ? `${startOfLabel} [${game.i18n.localize("DG.Roll.Inhuman").toUpperCase()}] ${endOfLabel}` + : `${startOfLabel}
${endOfLabel}%`; + + let {isExhausted, exhaustedCheckPenalty} = this.exhausted + + if (this.modifier || isExhausted) { + label += ` (${this.target}%`; + + if (this.modifier) { + label += `${DGUtils.formatStringWithLeadingPlus(this.modifier)}%`; + } + + if (isExhausted) { + label += `${DGUtils.formatStringWithLeadingPlus( + exhaustedCheckPenalty, + )}%`; + } + + label += `)`; + } + + return label; + } + + get exhausted() { + let isExhausted = false; + let exhaustedCheckPenalty = -20; + + try { + // I suspect (but am not entirely certain) that being tired doesn't make you less lucky) + if (this.type !== "luck") { + isExhausted = this.actor.system.physical.exhausted; + exhaustedCheckPenalty = this.actor.system.physical.exhaustedPenalty; + exhaustedCheckPenalty = -1 * Math.abs(exhaustedCheckPenalty); + } + } catch { + isExhausted = false; + exhaustedCheckPenalty = -20; + } + + return {isExhausted, exhaustedCheckPenalty}; } /** @@ -481,20 +504,7 @@ export class DGPercentileRoll extends DGRoll { get effectiveTarget() { let target = 1; - let isExhausted = false; - let exhaustedCheckPenalty = -20; - - try { - // I suspect (but am not entirely certain) that being tired doesn't make you less lucky) - if (this.type !== "luck") { - isExhausted = this.actor.system.physical.exhausted; - exhaustedCheckPenalty = this.actor.system.physical.exhaustedPenalty; - exhaustedCheckPenalty = -1 * Math.abs(exhaustedCheckPenalty); - } - } catch { - isExhausted = false; - exhaustedCheckPenalty = -20; - } + let {isExhausted, exhaustedCheckPenalty} = this.exhausted if (!this.target || Number.isNaN(this.target)) { return null; diff --git a/templates/roll/percentile-roll.hbs b/templates/roll/percentile-roll.hbs new file mode 100644 index 0000000..1c83e39 --- /dev/null +++ b/templates/roll/percentile-roll.hbs @@ -0,0 +1,30 @@ +
+
+
{{resultString}}
+
+
+
+
+ {{formula}} + {{total}} +
+
    +
  1. {{total}}
  2. +
+
+
+
+

{{total}}

+
+ + {{#if failureMark}} +
+ + + +
+ {{/if}} + +
\ No newline at end of file