More rules

This commit is contained in:
WBHarry 2025-08-04 13:31:53 +02:00
parent e53b0a8a73
commit 323fff73cd
10 changed files with 194 additions and 33 deletions

View file

@ -318,6 +318,7 @@
"DamageReduction": { "DamageReduction": {
"armorMarks": "Armor Marks", "armorMarks": "Armor Marks",
"armorWithStress": "Spend 1 stress to use an extra mark", "armorWithStress": "Spend 1 stress to use an extra mark",
"thresholdImmunities": "Threshold Immunities",
"stress": "Stress", "stress": "Stress",
"stressReduction": "Reduce By Stress", "stressReduction": "Reduce By Stress",
"title": "Damage Reduction", "title": "Damage Reduction",
@ -952,6 +953,12 @@
"name": "Dice Set" "name": "Dice Set"
} }
}, },
"RuleChoice": {
"off": "Off",
"offWithToggle": "Off With Toggle",
"on": "On",
"onWithToggle": "On With Toggle"
},
"SelectAction": { "SelectAction": {
"selectType": "Select Action Type", "selectType": "Select Action Type",
"selectAction": "Action Selection" "selectAction": "Action Selection"
@ -1662,7 +1669,8 @@
"major": "Major", "major": "Major",
"severe": "Severe", "severe": "Severe",
"majorThreshold": "Major Damage Threshold", "majorThreshold": "Major Damage Threshold",
"severeThreshold": "Severe Damage Threshold" "severeThreshold": "Severe Damage Threshold",
"with": "{threshold} Damage Threshold"
}, },
"Dice": { "Dice": {
"single": "Die", "single": "Die",
@ -1772,6 +1780,10 @@
"hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum." "hint": "If this value is set you can use up to that much stress to spend additional Armor Marks beyond your normal maximum."
}, },
"stress": { "stress": {
"any": {
"label": "Stress Damage Reduction: Any",
"hint": "The cost in stress you can pay to reduce incoming damage down one threshold"
},
"severe": { "severe": {
"label": "Stress Damage Reduction: Severe", "label": "Stress Damage Reduction: Severe",
"hint": "The cost in stress you can pay to reduce severe damage down to major." "hint": "The cost in stress you can pay to reduce severe damage down to major."
@ -1850,6 +1862,7 @@
}, },
"actorName": "Actor Name", "actorName": "Actor Name",
"amount": "Amount", "amount": "Amount",
"any": "Any",
"armorScore": "Armor Score", "armorScore": "Armor Score",
"activeEffects": "Active Effects", "activeEffects": "Active Effects",
"armorSlots": "Armor Slots", "armorSlots": "Armor Slots",
@ -2059,6 +2072,10 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result." "hint": "Automatically increase the GM's fear pool on a fear duality roll result."
}, },
"FIELDS": { "FIELDS": {
"damageReductionRulesDefault": {
"label": "Damage Reduction Rules Default",
"hint": "Wether using armor and reductions has rules on by default"
},
"hopeFear": { "hopeFear": {
"label": "Hope & Fear", "label": "Hope & Fear",
"gm": { "label": "GM" }, "gm": { "label": "GM" },
@ -2312,7 +2329,9 @@
"appliedEvenIfSuccessful": "Applied even if save succeeded", "appliedEvenIfSuccessful": "Applied even if save succeeded",
"diceIsRerolled": "The dice has been rerolled (x{times})", "diceIsRerolled": "The dice has been rerolled (x{times})",
"pendingSaves": "Pending Reaction Rolls", "pendingSaves": "Pending Reaction Rolls",
"openSheetSettings": "Open Settings" "openSheetSettings": "Open Settings",
"rulesOn": "Rules On",
"rulesOff": "Rules Off"
} }
} }
} }

View file

@ -10,14 +10,18 @@ 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.rulesDefault = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).damageReductionRulesDefault;
this.rulesOn = [CONFIG.DH.GENERAL.ruleChoice.on.id, CONFIG.DH.GENERAL.ruleChoice.onWithToggle.id].includes(
this.rulesDefault
);
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true); const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
const maxArmorMarks = canApplyArmor const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
? Math.min( const maxArmorMarks = canApplyArmor ? availableArmor : 0;
actor.system.armorScore - actor.system.armor.system.marks.value,
actor.system.rules.damageReduction.maxArmorMarked.value
)
: 0;
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false }; acc[foundry.utils.randomID()] = { selected: false };
@ -42,6 +46,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
acc[damage] = { acc[damage] = {
cost: dr.cost, cost: dr.cost,
selected: false, selected: false,
any: key === 'any',
from: getDamageLabel(damage), from: getDamageLabel(damage),
to: getDamageLabel(damage - 1) to: getDamageLabel(damage - 1)
}; };
@ -51,16 +56,28 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
}, },
null null
); );
this.thresholdImmunities = Object.keys(actor.system.rules.damageReduction.thresholdImmunities).reduce(
(acc, key) => {
if (actor.system.rules.damageReduction.thresholdImmunities[key])
acc[damageKeyToNumber(key)] = game.i18n.format(`DAGGERHEART.GENERAL.DamageThresholds.with`, {
threshold: game.i18n.localize(`DAGGERHEART.GENERAL.DamageThresholds.${key}`)
});
return acc;
},
{}
);
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'damage-reduction'], classes: ['daggerheart', 'views', 'damage-reduction'],
position: { position: {
width: 240, width: 280,
height: 'auto' height: 'auto'
}, },
actions: { actions: {
toggleRules: this.toggleRules,
setMarks: this.setMarks, setMarks: this.setMarks,
useStressReduction: this.useStressReduction, useStressReduction: this.useStressReduction,
takeDamage: this.takeDamage takeDamage: this.takeDamage
@ -89,6 +106,12 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.rulesOn = this.rulesOn;
context.rulesToggleable = [
CONFIG.DH.GENERAL.ruleChoice.onWithToggle.id,
CONFIG.DH.GENERAL.ruleChoice.offWithToggle.id
].includes(this.rulesDefault);
context.thresholdImmunities = this.thresholdImmunities;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
this.getDamageInfo(); this.getDamageInfo();
@ -110,12 +133,22 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
: null; : null;
context.marks = this.marks; const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
context.marks = {
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
return acc;
}, {}),
stress: this.marks.stress
};
context.availableStressReductions = this.availableStressReductions; context.availableStressReductions = this.availableStressReductions;
context.damage = getDamageLabel(this.damage); context.damage = getDamageLabel(this.damage);
context.reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null; context.reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null;
context.currentDamage = context.reducedDamage ?? context.damage; context.currentDamage = context.reducedDamage ?? context.damage;
context.currentDamageNr = currentDamage;
return context; return context;
} }
@ -136,22 +169,48 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const armorMarkReduction = const armorMarkReduction =
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark; selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
const currentDamage = this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length; let currentDamage = Math.max(
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0
);
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
}; };
static toggleRules() {
this.rulesOn = !this.rulesOn;
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
this.marks = {
armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
return acc;
}, {}),
stress: this.marks.stress
};
this.render();
}
static setMarks(_, target) { static setMarks(_, target) {
const currentMark = this.marks[target.dataset.type][target.dataset.key]; const currentMark = this.marks[target.dataset.type][target.dataset.key];
const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo(); const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = 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'));
return; return;
} }
if (!currentMark.selected && currentMarks === this.actor.system.armorScore) { if (this.rulesOn) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks')); if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
return; ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
return;
}
} }
if (currentMark.selected) { if (currentMark.selected) {

View file

@ -2,6 +2,25 @@ export const compendiumJournals = {
welcome: 'Compendium.daggerheart.journals.JournalEntry.g7NhKvwltwafmMyR' welcome: 'Compendium.daggerheart.journals.JournalEntry.g7NhKvwltwafmMyR'
}; };
export const ruleChoice = {
on: {
id: 'on',
label: 'DAGGERHEART.CONFIG.RuleChoice.on'
},
of: {
id: 'off',
label: 'DAGGERHEART.CONFIG.RuleChoice.off'
},
onWithToggle: {
id: 'onWithToggle',
label: 'DAGGERHEART.CONFIG.RuleChoice.onWithToggle'
},
offWithToggle: {
id: 'offWithToggle',
label: 'DAGGERHEART.CONFIG.RuleChoice.offWithToggle'
}
};
export const range = { export const range = {
self: { self: {
id: 'self', id: 'self',

View file

@ -239,7 +239,8 @@ export default class DhCharacter extends BaseDataActor {
stressDamageReduction: new fields.SchemaField({ stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'), severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'),
major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'), major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'),
minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor') minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor'),
any: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.any')
}), }),
increasePerArmorMark: new fields.NumberField({ increasePerArmorMark: new fields.NumberField({
integer: true, integer: true,
@ -248,7 +249,11 @@ export default class DhCharacter extends BaseDataActor {
hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint'
}), }),
magical: new fields.BooleanField({ initial: false }), magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false }) physical: new fields.BooleanField({ initial: false }),
thresholdImmunities: new fields.SchemaField({
minor: new fields.BooleanField({ initial: false })
}),
disabledArmor: new fields.BooleanField({ intial: false })
}), }),
attack: new fields.SchemaField({ attack: new fields.SchemaField({
damage: new fields.SchemaField({ damage: new fields.SchemaField({

View file

@ -34,6 +34,12 @@ export default class DhAutomation extends foundry.abstract.DataModel {
initial: true, initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.effects.rangeDependent.label' label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.effects.rangeDependent.label'
}) })
}),
damageReductionRulesDefault: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.ruleChoice,
initial: CONFIG.DH.GENERAL.ruleChoice.onWithToggle.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.damageReductionRulesDefault.label'
}) })
}; };
} }

View file

@ -464,14 +464,17 @@ export default class DhpActor extends Actor {
} }
#canReduceDamage(hpDamage, type) { #canReduceDamage(hpDamage, type) {
const { stressDamageReduction, disabledArmor } = this.system.rules.damageReduction;
if (disabledArmor) return false;
const availableStress = this.system.resources.stress.max - this.system.resources.stress.value; const availableStress = this.system.resources.stress.max - this.system.resources.stress.value;
const canUseArmor = const canUseArmor =
this.system.armor && this.system.armor &&
this.system.armor.system.marks.value < this.system.armorScore && this.system.armor.system.marks.value < this.system.armorScore &&
type.every(t => this.system.armorApplicableDamageTypes[t] === true); type.every(t => this.system.armorApplicableDamageTypes[t] === true);
const canUseStress = Object.keys(this.system.rules.damageReduction.stressDamageReduction).reduce((acc, x) => { const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => {
const rule = this.system.rules.damageReduction.stressDamageReduction[x]; const rule = stressDamageReduction[x];
if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost); if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost);
return acc; return acc;
}, false); }, false);

View file

@ -2,11 +2,35 @@
.daggerheart.views.damage-reduction { .daggerheart.views.damage-reduction {
.damage-reduction-container { .damage-reduction-container {
position: relative;
padding: 8px 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
.rules-button {
position: absolute;
top: 4px;
right: 4px;
border-radius: 50%;
&.inactive {
opacity: 0.4;
::after {
position: absolute;
content: '/';
color: red;
font-weight: 700;
font-size: 1.8em;
left: 5px;
top: 0;
rotate: 13deg;
}
}
}
.section-container { .section-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -44,7 +68,7 @@
.mark-selection-inner { .mark-selection-inner {
display: flex; display: flex;
gap: 2px; gap: 8px;
.mark-container { .mark-container {
cursor: pointer; cursor: pointer;
@ -58,10 +82,6 @@
justify-content: center; justify-content: center;
opacity: 0.4; opacity: 0.4;
&:not(:last-child) {
margin-right: 8px;
}
&.selected { &.selected {
opacity: 1; opacity: 1;
} }
@ -79,11 +99,11 @@
} }
} }
.stress-reduction-container { .chip-container {
margin: 0; margin: 0;
width: 100%; width: 100%;
.stress-reduction { .chip-inner-container {
border: 1px solid light-dark(@dark-blue, @golden); border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 6px; border-radius: 6px;
height: 26px; height: 26px;
@ -113,6 +133,14 @@
} }
} }
.threshold-label {
opacity: 0.6;
&.active {
opacity: 1;
}
}
.markers-subtitle { .markers-subtitle {
margin: -4px 0 0 0; margin: -4px 0 0 0;

View file

@ -2,6 +2,6 @@
.daggerheart.views.damage-reduction { .daggerheart.views.damage-reduction {
.window-content { .window-content {
padding: 8px 0; padding: 0;
} }
} }

View file

@ -1,4 +1,9 @@
<div class="damage-reduction-container"> <div class="damage-reduction-container">
{{#if rulesToggleable}}
<button type="button" class="rules-button {{#unless rulesOn}}inactive{{/unless}}" data-action="toggleRules" data-tooltip-text="{{#if rulesOn}}{{localize "DAGGERHEART.UI.Tooltip.rulesOn"}}{{else}}{{localize "DAGGERHEART.UI.Tooltip.rulesOff"}}{{/if}}">
<i class="fa-solid fa-book"></i>
</button>
{{/if}}
<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">
@ -25,8 +30,6 @@
<i class="fa-solid fa-shield"></i> <i class="fa-solid fa-shield"></i>
</div> </div>
{{/each}} {{/each}}
</div>
<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}}"
@ -40,19 +43,25 @@
<div class="markers-subtitle bold">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.usedMarks"}}</div> <div class="markers-subtitle bold">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.usedMarks"}}</div>
</div> </div>
{{#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">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stressReduction"}}</h4>
</div> </div>
</div> </div>
{{/if}}
{{#each availableStressReductions}} {{#each availableStressReductions}}
<div class="section-container"> <div class="section-container">
<h4 class="stress-reduction-container divider"> <h4 class="chip-container divider">
<div class="stress-reduction {{#if (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}}">
{{this.from}} {{#if this.any}}
<i class="fa-solid fa-arrow-right-long"></i> {{localize "DAGGERHEART.GENERAL.any"}}
{{this.to}} {{else}}
{{this.from}}
<i class="fa-solid fa-arrow-right-long"></i>
{{this.to}}
{{/if}}
<div class="stress-reduction-cost"> <div class="stress-reduction-cost">
{{this.cost}} {{this.cost}}
<i class="fa-solid fa-bolt"></i> <i class="fa-solid fa-bolt"></i>
@ -62,6 +71,18 @@
</div> </div>
{{/each}} {{/each}}
{{#if thresholdImmunities}}
<div class="resources-container">
<div class="resource-container">
<h4 class="armor-title">{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.thresholdImmunities"}}</h4>
</div>
</div>
{{/if}}
{{#each thresholdImmunities as | immunity key |}}
<div class="threshold-label {{#if (gte key @root.currentDamageNr)}}active{{/if}}">{{immunity}}</div>
{{/each}}
<footer class="padded"> <footer class="padded">
<button type="button" data-action="takeDamage"> <button type="button" data-action="takeDamage">
{{localize "Take"}} {{localize "Take"}}

View file

@ -12,6 +12,7 @@
{{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}
{{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}} {{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}}
{{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}} {{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}}
{{formGroup settingFields.schema.fields.damageReductionRulesDefault value=settingFields._source.damageReductionRulesDefault localize=true}}
<footer class="form-footer"> <footer class="form-footer">
<button data-action="reset"> <button data-action="reset">