Fixed DamageReductionDialog

This commit is contained in:
WBHarry 2026-02-13 23:01:55 +01:00
parent 3b011e2580
commit 8f0b1f6ed3
6 changed files with 172 additions and 69 deletions

View file

@ -437,7 +437,7 @@
"text": "Are you sure you want to delete {name}?" "text": "Are you sure you want to delete {name}?"
}, },
"DamageReduction": { "DamageReduction": {
"armorMarks": "Armor Marks", "maxUseableArmor": "Useable Armor Slots",
"armorWithStress": "Spend 1 stress to use an extra mark", "armorWithStress": "Spend 1 stress to use an extra mark",
"thresholdImmunities": "Threshold Immunities", "thresholdImmunities": "Threshold Immunities",
"stress": "Stress", "stress": "Stress",

View file

@ -10,7 +10,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject; this.reject = reject;
this.actor = actor; this.actor = actor;
this.damage = damage; this.damage = damage;
this.damageType = damageType; // this.damageType = damageType;
this.damageType = ['physical'];
this.rulesDefault = game.settings.get( this.rulesDefault = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation CONFIG.DH.SETTINGS.gameSettings.Automation
@ -20,14 +21,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.rulesDefault this.rulesDefault
); );
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true); const allArmorEffects = Array.from(actor.allApplicableEffects()).filter(x => x.type === 'armor');
const availableArmor = actor.system.armorScore.max - actor.system.armorScore.value; const orderedArmorEffects = game.system.api.data.activeEffects.ArmorEffect.orderEffectsForAutoChange(
const maxArmorMarks = canApplyArmor ? availableArmor : 0; allArmorEffects,
true
);
const armor = orderedArmorEffects.reduce((acc, effect) => {
if (effect.type !== 'armor') return acc;
const { value, max } = effect.system.armorData;
acc.push({
effect: effect,
marks: [...Array(max).keys()].reduce((acc, _, index) => {
const spent = index < value;
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
return acc;
}, {})
});
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
return acc; return acc;
}, {}); }, []);
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce( const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
(acc, _) => { (acc, _) => {
acc[foundry.utils.randomID()] = { selected: false }; acc[foundry.utils.randomID()] = { selected: false };
@ -121,13 +133,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
context.thresholdImmunities = context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null; Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } =
this.getDamageInfo(); this.getDamageInfo();
context.armorScore = this.actor.system.armorScore.max; context.armorScore = this.actor.system.armorScore.max;
context.armorMarks = currentMarks; context.armorMarks = currentMarks;
context.basicMarksUsed =
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
const stressReductionStress = this.availableStressReductions const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0) ? stressReductions.reduce((acc, red) => acc + red.cost, 0)
@ -141,16 +151,27 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
: null; : null;
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value; context.maxArmorUsed = maxArmorUsed;
context.marks = { context.availableArmor = availableArmor;
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => { context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
const mark = this.marks.armor[key];
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
return acc; const armorSources = [];
}, {}), for (const source of this.marks.armor) {
const parent = source.effect.origin
? await foundry.utils.fromUuid(source.effect.origin)
: source.effect.parent;
armorSources.push({
label: parent.name,
uuid: source.effect.uuid,
marks: source.marks
});
}
context.marks = {
armor: armorSources,
stress: this.marks.stress stress: this.marks.stress
}; };
context.usesStressArmor = Object.keys(context.marks.stress).length;
context.availableStressReductions = this.availableStressReductions; context.availableStressReductions = this.availableStressReductions;
context.damage = getDamageLabel(this.damage); context.damage = getDamageLabel(this.damage);
@ -167,27 +188,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
getDamageInfo = () => { getDamageInfo = () => {
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected));
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = this.availableStressReductions const stressReductions = this.availableStressReductions
? Object.values(this.availableStressReductions).filter(red => red.selected) ? Object.values(this.availableStressReductions).filter(red => red.selected)
: []; : [];
const currentMarks = const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length;
this.actor.system.armorScore.value + selectedArmorMarks.length + selectedStressMarks.length;
const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length;
const availableArmor =
maxArmorUsed -
this.marks.armor.reduce((acc, source) => {
acc += Object.values(source.marks).filter(x => x.selected).length;
return acc;
}, 0);
const armorMarkReduction = const armorMarkReduction =
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark; selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
let currentDamage = Math.max( let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0);
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0
);
if (this.reduceSeverity) { if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0); currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
} }
if (this.thresholdImmunities[currentDamage]) currentDamage = 0; if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor };
}; };
static toggleRules() { static toggleRules() {
@ -209,8 +234,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
static setMarks(_, target) { static setMarks(_, target) {
const currentMark = this.marks[target.dataset.type][target.dataset.key]; const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path);
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo(); const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo();
if (!currentMark.selected && currentDamage === 0) { if (!currentMark.selected && currentDamage === 0) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone')); ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
@ -218,12 +243,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
if (this.rulesOn) { if (this.rulesOn) {
if (!currentMark.selected && currentMarks === this.actor.system.armorScore.max) { if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks')); ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
return; return;
} }
} }
const stressUsed = selectedStressMarks.length;
if (target.dataset.type === 'armor' && stressUsed) {
const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected);
if (updateResult === false) return;
}
if (currentMark.selected) { if (currentMark.selected) {
const currentDamageLabel = getDamageLabel(currentDamage); const currentDamageLabel = getDamageLabel(currentDamage);
for (let reduction of stressReductions) { for (let reduction of stressReductions) {
@ -232,8 +263,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
} }
if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) { if (target.dataset.type === 'stress' && currentMark.armorMarkId) {
selectedStressMarks.forEach(mark => (mark.selected = false)); for (const source of this.marks.armor) {
const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId);
if (match) {
source.marks[match].selected = false;
break;
}
}
currentMark.armorMarkId = null;
} }
} }
@ -241,6 +280,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.render(); this.render();
} }
updateStressArmor(armorMarkId, select) {
let stressMarkKey = null;
if (select) {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId
);
} else {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].armorMarkId === armorMarkId
);
if (!stressMarkKey)
stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected);
}
if (!stressMarkKey) return false;
this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null;
}
static useStressReduction(_, target) { static useStressReduction(_, target) {
const damageValue = Number(target.dataset.reduction); const damageValue = Number(target.dataset.reduction);
const stressReduction = this.availableStressReductions[damageValue]; const stressReduction = this.availableStressReductions[damageValue];
@ -279,11 +337,19 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
static async takeDamage() { static async takeDamage() {
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorSpent = selectedArmorMarks.length + selectedStressMarks.length; const armorChanges = this.marks.armor.reduce((acc, source) => {
const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0); const amount = Object.values(source.marks).filter(x => x.selected).length;
if (!amount) return acc;
this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent }); acc.push({ uuid: source.effect.uuid, amount });
return acc;
}, []);
const stressSpent =
selectedStressMarks.filter(x => x.armorMarkId).length +
stressReductions.reduce((acc, red) => acc + red.cost, 0);
this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent });
await this.close(true); await this.close(true);
} }

View file

@ -559,6 +559,18 @@ export default class DhCharacter extends BaseDataActor {
doc.updateEmbeddedDocuments('ActiveEffect', updates, { render: index === updateValues.length - 1 }); doc.updateEmbeddedDocuments('ActiveEffect', updates, { render: index === updateValues.length - 1 });
} }
async updateArmorEffectValue({ uuid, value }) {
const effect = await foundry.utils.fromUuid(uuid);
await effect.update({
'system.changes': [
{
...effect.system.armorChange,
value: effect.system.armorChange.value + value
}
]
});
}
get sheetLists() { get sheetLists() {
const ancestryFeatures = [], const ancestryFeatures = [],
communityFeatures = [], communityFeatures = [],

View file

@ -626,12 +626,10 @@ export default class DhpActor extends Actor {
} }
); );
if (armorSlotResult) { if (armorSlotResult) {
const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult; const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
updates.find(u => u.key === 'hitPoints').value = modifiedDamage; updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
if (armorSpent) { for (const armorChange of armorChanges) {
const armorUpdate = updates.find(u => u.key === 'armor'); updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid });
if (armorUpdate) armorUpdate.value += armorSpent;
else updates.push({ value: armorSpent, key: 'armor' });
} }
if (stressSpent) { if (stressSpent) {
const stressUpdate = updates.find(u => u.key === 'stress'); const stressUpdate = updates.find(u => u.key === 'stress');
@ -778,7 +776,8 @@ export default class DhpActor extends Actor {
); );
break; break;
case 'armor': case 'armor':
this.system.updateArmorValue(r); if (!r.uuid) this.system.updateArmorValue(r);
else this.system.updateArmorEffectValue(r);
break; break;
default: default:
if (this.system.resources?.[r.key]) { if (this.system.resources?.[r.key]) {

View file

@ -35,7 +35,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%;
&.full-width {
width: 100%;
}
} }
.padded { .padded {
@ -45,6 +48,7 @@
.armor-title { .armor-title {
margin: 0; margin: 0;
white-space: nowrap; white-space: nowrap;
width: 100%;
} }
.resources-container { .resources-container {
@ -62,12 +66,17 @@
.mark-selection { .mark-selection {
display: flex; display: flex;
align-items: center; flex-direction: column;
width: 100%; width: 100%;
margin: 0; margin: 0;
h4 {
margin: 0;
}
.mark-selection-inner { .mark-selection-inner {
display: flex; display: flex;
justify-content: center;
gap: 8px; gap: 8px;
.mark-container { .mark-container {
@ -91,6 +100,19 @@
opacity: 0.2; opacity: 0.2;
} }
&.spent {
::after {
position: absolute;
content: '/';
color: red;
font-weight: 700;
font-size: 1.8em;
left: -1px;
top: -7px;
rotate: 13deg;
}
}
.fa-shield { .fa-shield {
position: relative; position: relative;
right: 0.5px; right: 0.5px;

View file

@ -7,53 +7,57 @@
<div class="section-container padded"> <div class="section-container padded">
<div class="resources-container"> <div class="resources-container">
<div class="resource-container"> <div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.armorMarks"}}</h4> <h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.maxUseableArmor"}}</h4>
<div class="markers-subtitle">{{armorMarks}}/{{armorScore}}</div> <div class="markers-subtitle">{{availableArmor}}</div>
</div> </div>
{{#if this.stress}}
<div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stress"}}</h4>
<div class="markers-subtitle">{{this.stress.value}}/{{this.stress.max}}</div>
</div>
{{/if}}
</div> </div>
</div> </div>
<div class="section-container"> <div class="section-container full-width">
<h4 class="mark-selection divider"> {{#each marks.armor as |source|}}
<div class="mark-selection-inner"> <div class="mark-selection">
{{#each marks.armor}} <h4 class="divider">{{source.label}}</h4>
<div <div class="mark-selection-inner">
class="mark-container {{#if this.selected}}selected{{/if}}" {{#each source.marks}}
data-action="setMarks" data-key="{{@key}}" data-type="armor" <a
> class="mark-container {{#if this.selected}}selected{{/if}} {{#if this.spent}}spent{{/if}} {{#if this.disabled}}inactive{{/if}}"
<i class="fa-solid fa-shield"></i> data-action="setMarks" data-type="armor" data-path="{{concat "armor." @../key ".marks." @key}}" data-id="{{@key}}"
</div> {{#if this.disabled}}disabled{{/if}}
{{/each}} >
<i class="fa-solid fa-shield"></i>
</a>
{{/each}}
</div>
</div>
{{/each}}
{{#if usesStressArmor}}
<div class="mark-selection">
<h4 class="divider">{{localize "Stress"}}</h4>
<div class="mark-selection-inner">
{{#each marks.stress}} {{#each marks.stress}}
<div <div
class="mark-container {{#if this.selected}}selected{{/if}} {{#if (not @root.basicMarksUsed)}}inactive{{/if}}" class="mark-container {{#if this.selected}}selected{{/if}} {{#if (not @root.basicMarksUsed)}}inactive{{/if}}"
{{#if @root.basicMarksUsed}}data-action="setMarks"{{/if}} data-key="{{@key}}" data-type="stress" data-tooltip="{{#if @root.basicMarksUsed}}{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.armorWithStress"}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.unncessaryStress"}}{{/if}}" {{#if @root.basicMarksUsed}}data-action="setMarks"{{/if}} data-type="stress" data-path="{{concat "stress." @key}}" data-tooltip="{{#if @root.basicMarksUsed}}{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.armorWithStress"}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.unncessaryStress"}}{{/if}}"
> >
<i class="fa-solid fa-bolt"></i> <i class="fa-solid fa-bolt"></i>
</div> </div>
{{/each}} {{/each}}
</div>
</div> </div>
</h4> {{/if}}
<div class="markers-subtitle bold">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.usedMarks"}}</div>
</div> </div>
{{#if availableStressReductions}} {{#if availableStressReductions}}
<div class="resources-container"> <div class="resources-container">
<div class="resource-container"> <div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stressReduction"}}</h4> <h4 class="armor-title divider">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stressReduction"}}</h4>
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#each availableStressReductions}} {{#each availableStressReductions}}
<div class="section-container"> <div class="section-container">
<h4 class="chip-container divider"> <h4 class="chip-container">
<div class="chip-inner-container selectable {{#if (or this.any (eq this.from @root.currentDamage))}}active{{/if}} {{#if this.selected}}selected{{/if}}" data-action="useStressReduction" data-reduction="{{@key}}"> <div class="chip-inner-container selectable {{#if (or this.any (eq this.from @root.currentDamage))}}active{{/if}} {{#if this.selected}}selected{{/if}}" data-action="useStressReduction" data-reduction="{{@key}}">
{{#if this.any}} {{#if this.any}}
{{localize "DAGGERHEART.GENERAL.any"}} {{localize "DAGGERHEART.GENERAL.any"}}
@ -74,7 +78,7 @@
{{#if reduceSeverity}} {{#if reduceSeverity}}
<div class="resources-container"> <div class="resources-container">
<div class="resource-container"> <div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.reduceSeverity" nr=reduceSeverity}}</h4> <h4 class="armor-title divider">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.reduceSeverity" nr=reduceSeverity}}</h4>
</div> </div>
</div> </div>
{{/if}} {{/if}}
@ -82,7 +86,7 @@
{{#if thresholdImmunities}} {{#if thresholdImmunities}}
<div class="resources-container"> <div class="resources-container">
<div class="resource-container"> <div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.thresholdImmunities"}}</h4> <h4 class="armor-title divider">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.thresholdImmunities"}}</h4>
</div> </div>
</div> </div>
{{/if}} {{/if}}