From 0e1320e31d1dab76da1663e4a4d848a4ae9306b8 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 24 Jun 2025 21:35:21 +0200 Subject: [PATCH] Fixed Stress Reductions --- lang/en.json | 4 +- module/applications/damageReductionDialog.mjs | 92 ++++++++++++++++--- module/data/actor/character.mjs | 12 ++- module/documents/actor.mjs | 25 +++-- module/helpers/utils.mjs | 13 +++ styles/daggerheart.css | 69 ++++++++++---- styles/damageReduction.less | 83 ++++++++++++----- templates/views/damageReduction.hbs | 55 ++++++++--- 8 files changed, 279 insertions(+), 74 deletions(-) diff --git a/lang/en.json b/lang/en.json index 7297fa5d..6c658919 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1095,9 +1095,11 @@ "ArmorMarks": "Armor Marks", "UsedMarks": "Used Marks", "Stress": "Stress", + "ArmorWithStress": "Spend 1 stress to use an extra mark", + "StressReduction": "Reduce By Stress", "Notifications": { "DamageAlreadyNone": "The damage has already been reduced to none", - "NotEnoughArmor": "You don't have enough unspent armor marks" + "DamageIgnore": "{character} did not take damage" } }, "Sheets": { diff --git a/module/applications/damageReductionDialog.mjs b/module/applications/damageReductionDialog.mjs index 3783560c..923d4df5 100644 --- a/module/applications/damageReductionDialog.mjs +++ b/module/applications/damageReductionDialog.mjs @@ -1,4 +1,4 @@ -import { getDamageLabel } from '../helpers/utils.mjs'; +import { damageKeyToNumber, getDamageLabel } from '../helpers/utils.mjs'; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; @@ -11,13 +11,32 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.actor = actor; this.damage = damage; + const maxUseable = actor.system.armorScore - actor.system.armor.system.marks.value; this.availableArmorMarks = { - max: actor.system.rules.maxArmorMarked.total + (actor.system.rules.stressExtra ?? 0), - maxUseable: actor.system.armorScore - actor.system.armor.system.marks.value, - stressIndex: - (actor.system.rules.stressExtra ?? 0) > 0 ? actor.system.rules.maxArmorMarked.total : undefined, + max: Math.min( + maxUseable, + actor.system.rules.maxArmorMarked.total + (actor.system.rules.maxArmorMarked.stressExtra ?? 0) + ), + stressIndex: actor.system.rules.maxArmorMarked.total, selected: 0 }; + + this.availableStressReductions = Object.keys(actor.system.rules.stressDamageReduction).reduce((acc, key) => { + const dr = actor.system.rules.stressDamageReduction[key]; + if (dr.enabled) { + if (acc === null) acc = {}; + + const damage = damageKeyToNumber(key); + acc[damage] = { + cost: dr.cost, + selected: false, + from: getDamageLabel(damage), + to: getDamageLabel(damage - 1) + }; + } + + return acc; + }, null); } get title() { @@ -33,6 +52,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap }, actions: { setMarks: this.setMarks, + useStressReduction: this.useStressReduction, takeDamage: this.takeDamage }, form: { @@ -61,13 +81,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const context = await super._prepareContext(_options); context.armorScore = this.actor.system.armorScore; context.armorMarks = this.actor.system.armor.system.marks.value + this.availableArmorMarks.selected; + + const selectedStressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const stressReductionStress = this.availableStressReductions + ? selectedStressReductions.reduce((acc, red) => acc + red.cost, 0) + : 0; + context.stress = + this.availableArmorMarks.stressIndex || this.availableStressReductions + ? { + value: + this.actor.system.resources.stress.value + + (Math.max(this.availableArmorMarks.selected - this.availableArmorMarks.stressIndex, 0) + + stressReductionStress), + maxTotal: this.actor.system.resources.stress.maxTotal + } + : null; + context.availableArmorMarks = this.availableArmorMarks; + context.availableStressReductions = this.availableStressReductions; context.damage = getDamageLabel(this.damage); context.reducedDamage = - this.availableArmorMarks.selected > 0 - ? getDamageLabel(this.damage - this.availableArmorMarks.selected) + this.availableArmorMarks.selected > 0 || selectedStressReductions.length > 0 + ? getDamageLabel(this.damage - this.availableArmorMarks.selected - selectedStressReductions.length) : null; + context.currentDamage = context.reducedDamage ?? context.damage; return context; } @@ -79,21 +117,51 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap static setMarks(_, target) { const index = Number(target.dataset.index); - if (index >= this.availableArmorMarks.maxUseable) { - ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NotEnoughArmor')); - return; - } - const isDecreasing = index < this.availableArmorMarks.selected; if (!isDecreasing && this.damage - this.availableArmorMarks.selected === 0) { ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.DamageAlreadyNone')); return; } + if (isDecreasing) { + const selectedStressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const reducedDamage = + this.availableArmorMarks.selected > 0 || selectedStressReductions.length > 0 + ? getDamageLabel(this.damage - this.availableArmorMarks.selected - selectedStressReductions.length) + : null; + const currentDamage = reducedDamage ?? getDamageLabel(this.damage); + for (let reduction of selectedStressReductions) { + if (reduction.selected && reduction.to === currentDamage) { + reduction.selected = false; + } + } + } + this.availableArmorMarks.selected = isDecreasing ? index : index + 1; this.render(); } + static useStressReduction(_, target) { + const damageValue = Number(target.dataset.reduction); + const stressReduction = this.availableStressReductions[damageValue]; + if (stressReduction.selected) { + stressReduction.selected = false; + this.render(); + } else { + const selectedStressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const reducedDamage = + this.availableArmorMarks.selected > 0 || selectedStressReductions.length > 0 + ? getDamageLabel(this.damage - this.availableArmorMarks.selected - selectedStressReductions.length) + : null; + const currentDamage = reducedDamage ?? getDamageLabel(this.damage); + + if (stressReduction.from !== currentDamage) return; + + stressReduction.selected = true; + this.render(); + } + } + static async takeDamage() { const armorSpent = this.availableArmorMarks.selected; const modifiedDamage = this.damage - armorSpent; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 98c76c64..6653e71c 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -17,6 +17,12 @@ const resourceField = max => max: new foundry.data.fields.NumberField({ initial: max, integer: true }) }); +const stressDamageReductionRule = () => + new foundry.data.fields.SchemaField({ + enabled: new foundry.data.fields.BooleanField({ required: true, initial: false }), + cost: new foundry.data.fields.NumberField({ integer: true }) + }); + export default class DhCharacter extends BaseDataActor { static get metadata() { return foundry.utils.mergeObject(super.metadata, { @@ -98,9 +104,9 @@ export default class DhCharacter extends BaseDataActor { stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 }) }), stressDamageReduction: new fields.SchemaField({ - enabled: new fields.BooleanField({ required: true, initial: false }), - cost: new fields.NumberField({ integer: true }), - fromSeverity: new fields.NumberField({ integer: true, max: 3 }) + severe: stressDamageReductionRule(), + major: stressDamageReductionRule(), + minor: stressDamageReductionRule() }) }) }; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 70772ec5..c95e0bb0 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -515,13 +515,24 @@ export default class DhpActor extends Actor { ) { new Promise((resolve, reject) => { new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); - }).then(async ({ modifiedDamage, armorSpent }) => { - const resources = [ - { value: modifiedDamage, type: 'hitPoints' }, - ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []) - ]; - await this.modifyResource(resources); - }); + }) + .then(async ({ modifiedDamage, armorSpent }) => { + const resources = [ + { value: modifiedDamage, type: 'hitPoints' }, + ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []) + ]; + await this.modifyResource(resources); + }) + .catch(() => { + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + user: game.user.id, + content: game.i18n.format('DAGGERHEART.DamageReduction.Notifications.DamageIgnore', { + character: this.name + }) + }); + cls.create(msg.toObject()); + }); } else { await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]); } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index dc1601ad..6b257f03 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -248,3 +248,16 @@ export const getDamageLabel = damage => { return game.i18n.localize('DAGGERHEART.General.Damage.None'); } }; + +export const damageKeyToNumber = key => { + switch (key) { + case 'severe': + return 3; + case 'major': + return 2; + case 'minor': + return 1; + case 'none': + return 0; + } +}; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 359f0505..52520076 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -3135,6 +3135,18 @@ div.daggerheart.views.multiclass { } .daggerheart.views.damage-reduction .damage-reduction-container .armor-title { margin: 0; + white-space: nowrap; +} +.daggerheart.views.damage-reduction .damage-reduction-container .resources-container { + display: flex; + gap: 8px; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container .resources-container .resource-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; } .daggerheart.views.damage-reduction .damage-reduction-container .mark-selection { display: flex; @@ -3148,7 +3160,7 @@ div.daggerheart.views.multiclass { border: 1px solid light-dark(#18162e, #f3c267); border-radius: 6px; height: 26px; - width: 26px; + padding: 0 1px; font-size: 18px; display: flex; align-items: center; @@ -3158,8 +3170,38 @@ div.daggerheart.views.multiclass { .daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container.selected { opacity: 1; } -.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container.disabled { - cursor: initial; +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container .fa-shield { + position: relative; + right: 0.5px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container { + margin: 0; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction { + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + height: 26px; + padding: 0 4px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + opacity: 0.4; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction.active { + opacity: 1; + cursor: pointer; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction.selected { + opacity: 1; + background: var(--color-warm-2); + color: white; +} +.daggerheart.views.damage-reduction .damage-reduction-container .stress-reduction-container .stress-reduction .stress-reduction-cost { + display: flex; + align-items: center; } .daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle { margin: -4px 0 0 0; @@ -3168,20 +3210,6 @@ div.daggerheart.views.multiclass { font-variant: all-small-caps; font-weight: bold; } -.daggerheart.views.damage-reduction .damage-reduction-container .damage-container { - display: flex; - justify-content: center; - gap: 4px; - font-weight: bold; - height: 18px; -} -.daggerheart.views.damage-reduction .damage-reduction-container .damage-container i { - font-size: 18px; -} -.daggerheart.views.damage-reduction .damage-reduction-container .damage-container .reduced-value { - opacity: 0.4; - text-decoration: line-through; -} .daggerheart.views.damage-reduction .damage-reduction-container footer { display: flex; width: 100%; @@ -3189,6 +3217,13 @@ div.daggerheart.views.multiclass { .daggerheart.views.damage-reduction .damage-reduction-container footer button { flex: 1; } +.daggerheart.views.damage-reduction .damage-reduction-container footer button .damage-value { + font-weight: bold; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer button .damage-value.reduced-value { + opacity: 0.4; + text-decoration: line-through; +} :root { --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; diff --git a/styles/damageReduction.less b/styles/damageReduction.less index 723a7022..3b3ff0e2 100644 --- a/styles/damageReduction.less +++ b/styles/damageReduction.less @@ -22,6 +22,20 @@ .armor-title { margin: 0; + white-space: nowrap; + } + + .resources-container { + display: flex; + gap: 8px; + width: 100%; + + .resource-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + } } .mark-selection { @@ -33,10 +47,10 @@ .mark-container { cursor: pointer; - border: 1px solid light-dark(#18162e, #f3c267); + border: 1px solid light-dark(@dark-blue, @golden); border-radius: 6px; height: 26px; - width: 26px; + padding: 0 1px; font-size: 18px; display: flex; align-items: center; @@ -47,8 +61,43 @@ opacity: 1; } - &.disabled { - cursor: initial; + .fa-shield { + position: relative; + right: 0.5px; + } + } + } + + .stress-reduction-container { + margin: 0; + width: 100%; + + .stress-reduction { + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + height: 26px; + padding: 0 4px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + opacity: 0.4; + + &.active { + opacity: 1; + cursor: pointer; + } + + &.selected { + opacity: 1; + background: var(--color-warm-2); + color: white; + } + + .stress-reduction-cost { + display: flex; + align-items: center; } } } @@ -62,29 +111,21 @@ } } - .damage-container { - display: flex; - justify-content: center; - gap: 4px; - font-weight: bold; - height: 18px; - - i { - font-size: 18px; - } - - .reduced-value { - opacity: 0.4; - text-decoration: line-through; - } - } - footer { display: flex; width: 100%; button { flex: 1; + + .damage-value { + font-weight: bold; + + &.reduced-value { + opacity: 0.4; + text-decoration: line-through; + } + } } } } diff --git a/templates/views/damageReduction.hbs b/templates/views/damageReduction.hbs index 0fcc47b0..2749192c 100644 --- a/templates/views/damageReduction.hbs +++ b/templates/views/damageReduction.hbs @@ -1,15 +1,25 @@
-

{{localize "DAGGERHEART.DamageReduction.ArmorMarks"}}

-
{{armorMarks}}/{{armorScore}}
+
+
+

{{localize "DAGGERHEART.DamageReduction.ArmorMarks"}}

+
{{armorMarks}}/{{armorScore}}
+
+ {{#if this.stress}} +
+

{{localize "DAGGERHEART.DamageReduction.Stress"}}

+
{{this.stress.value}}/{{this.stress.maxTotal}}
+
+ {{/if}} +

{{#times availableArmorMarks.max}}
{{#if (or (not @root.availableArmorMarks.stressIndex) (lt this @root.availableArmorMarks.stressIndex))}} @@ -23,18 +33,37 @@
{{localize "DAGGERHEART.DamageReduction.UsedMarks"}}
-
-
{{localize "Incoming Damage"}}
-
-
{{this.damage}}
- {{#if this.reducedDamage}} - -
{{this.reducedDamage}}
- {{/if}} +
+
+

{{localize "DAGGERHEART.DamageReduction.StressReduction"}}

+ {{#each availableStressReductions}} +
+

+
+ {{this.from}} + + {{this.to}} +
+ {{this.cost}} + +
+
+

+
+ {{/each}} +
\ No newline at end of file