diff --git a/lang/en.json b/lang/en.json index 2b355ae0..1096036f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -318,6 +318,7 @@ "DamageReduction": { "armorMarks": "Armor Marks", "armorWithStress": "Spend 1 stress to use an extra mark", + "thresholdImmunities": "Threshold Immunities", "stress": "Stress", "stressReduction": "Reduce By Stress", "title": "Damage Reduction", @@ -952,6 +953,12 @@ "name": "Dice Set" } }, + "RuleChoice": { + "off": "Off", + "offWithToggle": "Off With Toggle", + "on": "On", + "onWithToggle": "On With Toggle" + }, "SelectAction": { "selectType": "Select Action Type", "selectAction": "Action Selection" @@ -1662,7 +1669,8 @@ "major": "Major", "severe": "Severe", "majorThreshold": "Major Damage Threshold", - "severeThreshold": "Severe Damage Threshold" + "severeThreshold": "Severe Damage Threshold", + "with": "{threshold} Damage Threshold" }, "Dice": { "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." }, "stress": { + "any": { + "label": "Stress Damage Reduction: Any", + "hint": "The cost in stress you can pay to reduce incoming damage down one threshold" + }, "severe": { "label": "Stress Damage Reduction: Severe", "hint": "The cost in stress you can pay to reduce severe damage down to major." @@ -1850,6 +1862,7 @@ }, "actorName": "Actor Name", "amount": "Amount", + "any": "Any", "armorScore": "Armor Score", "activeEffects": "Active Effects", "armorSlots": "Armor Slots", @@ -2059,6 +2072,10 @@ "hint": "Automatically increase the GM's fear pool on a fear duality roll result." }, "FIELDS": { + "damageReductionRulesDefault": { + "label": "Damage Reduction Rules Default", + "hint": "Wether using armor and reductions has rules on by default" + }, "hopeFear": { "label": "Hope & Fear", "gm": { "label": "GM" }, @@ -2312,7 +2329,9 @@ "appliedEvenIfSuccessful": "Applied even if save succeeded", "diceIsRerolled": "The dice has been rerolled (x{times})", "pendingSaves": "Pending Reaction Rolls", - "openSheetSettings": "Open Settings" + "openSheetSettings": "Open Settings", + "rulesOn": "Rules On", + "rulesOff": "Rules Off" } } } diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index e0841324..d8541396 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -10,14 +10,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.reject = reject; this.actor = actor; 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 maxArmorMarks = canApplyArmor - ? Math.min( - actor.system.armorScore - actor.system.armor.system.marks.value, - actor.system.rules.damageReduction.maxArmorMarked.value - ) - : 0; + const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value; + const maxArmorMarks = canApplyArmor ? availableArmor : 0; const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { acc[foundry.utils.randomID()] = { selected: false }; @@ -42,6 +46,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap acc[damage] = { cost: dr.cost, selected: false, + any: key === 'any', from: getDamageLabel(damage), to: getDamageLabel(damage - 1) }; @@ -51,16 +56,28 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap }, 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 = { tag: 'form', classes: ['daggerheart', 'views', 'damage-reduction'], position: { - width: 240, + width: 280, height: 'auto' }, actions: { + toggleRules: this.toggleRules, setMarks: this.setMarks, useStressReduction: this.useStressReduction, takeDamage: this.takeDamage @@ -89,6 +106,12 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap async _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 } = this.getDamageInfo(); @@ -110,12 +133,22 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } : 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.damage = getDamageLabel(this.damage); context.reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null; context.currentDamage = context.reducedDamage ?? context.damage; + context.currentDamageNr = currentDamage; return context; } @@ -136,22 +169,48 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const armorMarkReduction = 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 }; }; + 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) { const currentMark = this.marks[target.dataset.type][target.dataset.key]; const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo(); + if (!currentMark.selected && currentDamage === 0) { ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone')); return; } - if (!currentMark.selected && currentMarks === this.actor.system.armorScore) { - ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks')); - return; + if (this.rulesOn) { + if (!currentMark.selected && currentMarks === this.actor.system.armorScore) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks')); + return; + } } if (currentMark.selected) { diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index c41f7ab5..e5b624f8 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -2,6 +2,25 @@ export const compendiumJournals = { 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 = { self: { id: 'self', diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 0e78e96a..04a9e2c4 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -239,7 +239,8 @@ export default class DhCharacter extends BaseDataActor { stressDamageReduction: new fields.SchemaField({ severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'), 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({ integer: true, @@ -248,7 +249,11 @@ export default class DhCharacter extends BaseDataActor { hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' }), 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({ damage: new fields.SchemaField({ diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 66e685d0..84b7469c 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -34,6 +34,12 @@ export default class DhAutomation extends foundry.abstract.DataModel { initial: true, 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' }) }; } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 9a3612f8..156e9f31 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -464,14 +464,17 @@ export default class DhpActor extends Actor { } #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 canUseArmor = this.system.armor && this.system.armor.system.marks.value < this.system.armorScore && type.every(t => this.system.armorApplicableDamageTypes[t] === true); - const canUseStress = Object.keys(this.system.rules.damageReduction.stressDamageReduction).reduce((acc, x) => { - const rule = this.system.rules.damageReduction.stressDamageReduction[x]; + const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => { + const rule = stressDamageReduction[x]; if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost); return acc; }, false); diff --git a/styles/less/dialog/damage-reduction/damage-reduction-container.less b/styles/less/dialog/damage-reduction/damage-reduction-container.less index 7dab4f5d..9e1d1472 100644 --- a/styles/less/dialog/damage-reduction/damage-reduction-container.less +++ b/styles/less/dialog/damage-reduction/damage-reduction-container.less @@ -2,11 +2,35 @@ .daggerheart.views.damage-reduction { .damage-reduction-container { + position: relative; + padding: 8px 0; display: flex; flex-direction: column; align-items: center; 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 { display: flex; flex-direction: column; @@ -44,7 +68,7 @@ .mark-selection-inner { display: flex; - gap: 2px; + gap: 8px; .mark-container { cursor: pointer; @@ -58,10 +82,6 @@ justify-content: center; opacity: 0.4; - &:not(:last-child) { - margin-right: 8px; - } - &.selected { opacity: 1; } @@ -79,11 +99,11 @@ } } - .stress-reduction-container { + .chip-container { margin: 0; width: 100%; - .stress-reduction { + .chip-inner-container { border: 1px solid light-dark(@dark-blue, @golden); border-radius: 6px; height: 26px; @@ -113,6 +133,14 @@ } } + .threshold-label { + opacity: 0.6; + + &.active { + opacity: 1; + } + } + .markers-subtitle { margin: -4px 0 0 0; diff --git a/styles/less/dialog/damage-reduction/sheets.less b/styles/less/dialog/damage-reduction/sheets.less index b73d7518..153cb364 100644 --- a/styles/less/dialog/damage-reduction/sheets.less +++ b/styles/less/dialog/damage-reduction/sheets.less @@ -2,6 +2,6 @@ .daggerheart.views.damage-reduction { .window-content { - padding: 8px 0; + padding: 0; } } diff --git a/templates/dialogs/damageReduction.hbs b/templates/dialogs/damageReduction.hbs index 43f55e86..c9f344d5 100644 --- a/templates/dialogs/damageReduction.hbs +++ b/templates/dialogs/damageReduction.hbs @@ -1,4 +1,9 @@