diff --git a/lang/en.json b/lang/en.json index df2bfb5d..cf3a0f22 100755 --- a/lang/en.json +++ b/lang/en.json @@ -478,8 +478,8 @@ "name": "Impenetrable", "description": "Once per short rest, when you would mark your last Hit Point, you can instead mark a Stress." }, - "magic": { - "name": "Magic", + "magical": { + "name": "Magical", "description": "You can't mark an Armor Slot to reduce physical damage." }, "physical": { diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 64da237f..7d89eb31 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -3,7 +3,7 @@ import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs'; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(resolve, reject, actor, damage) { + constructor(resolve, reject, actor, damage, damageType) { super({}); this.resolve = resolve; @@ -11,10 +11,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.actor = actor; this.damage = damage; - const maxArmorMarks = Math.min( - actor.system.armorScore - actor.system.armor.system.marks.value, - actor.system.rules.damageReduction.maxArmorMarked.total - ); + const canApplyArmor = actor.system.armorApplicableDamageTypes[damageType]; + const maxArmorMarks = canApplyArmor + ? Math.min( + actor.system.armorScore - actor.system.armor.system.marks.value, + actor.system.rules.damageReduction.maxArmorMarked.total + ) + : 0; const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { acc[foundry.utils.randomID()] = { selected: false }; @@ -129,12 +132,15 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap getDamageInfo = () => { const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); - const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const stressReductions = this.availableStressReductions + ? Object.values(this.availableStressReductions).filter(red => red.selected) + : []; const currentMarks = this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; - const currentDamage = - this.damage - selectedArmorMarks.length - selectedStressMarks.length - stressReductions.length; + const armorMarkReduction = + selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark; + const currentDamage = this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length; return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; }; diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index bb98c8c3..21fbfea8 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -35,7 +35,7 @@ export default class ArmorSheet extends DHBaseItemSheet { switch (partId) { case 'settings': - context.features = this.document.system.features.map(x => x.value); + context.features = this.document.system.armorFeatures.map(x => x.value); break; } @@ -47,6 +47,6 @@ export default class ArmorSheet extends DHBaseItemSheet { * @param {Array} selectedOptions - The currently selected tag objects. */ static async #onFeatureSelect(selectedOptions) { - await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); + await this.document.update({ 'system.armorFeatures': selectedOptions.map(x => ({ value: x.value })) }); } } diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index c89bcf2b..cdc8a235 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -190,19 +190,19 @@ export const armorFeatures = { } ] }, - magic: { - label: 'DAGGERHEART.CONFIG.ArmorFeature.magic.name', - description: 'DAGGERHEART.CONFIG.ArmorFeature.magic.description', + magical: { + label: 'DAGGERHEART.CONFIG.ArmorFeature.magical.name', + description: 'DAGGERHEART.CONFIG.ArmorFeature.magical.description', effects: [ { - name: 'DAGGERHEART.CONFIG.ArmorFeature.magic.name', - description: 'DAGGERHEART.CONFIG.ArmorFeature.magic.description', + name: 'DAGGERHEART.CONFIG.ArmorFeature.magical.name', + description: 'DAGGERHEART.CONFIG.ArmorFeature.magical.description', img: 'icons/magic/defensive/barrier-shield-dome-blue-purple.webp', changes: [ { - key: 'system.rules.damageReduction.magic', + key: 'system.rules.damageReduction.magical', mode: 5, - value: '1' + value: 1 } ] } @@ -220,7 +220,7 @@ export const armorFeatures = { { key: 'system.rules.damageReduction.physical', mode: 5, - value: '1' + value: 1 } ] } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 2f50a0a7..651fbcfd 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -152,7 +152,15 @@ export default class DhCharacter extends BaseDataActor { initial: null }), weapon: new fields.SchemaField({ + /* Unimplemented + -> Should remove the lowest damage dice from weapon damage + -> Reflect this in the chat message somehow so players get feedback that their choice is helping them. + */ dropLowestDamageDice: new fields.BooleanField({ initial: false }), + /* Unimplemented + -> Should flip any lowest possible dice rolls for weapon damage to highest + -> Reflect this in the chat message somehow so players get feedback that their choice is helping them. + */ flipMinDiceValue: new fields.BooleanField({ intial: false }) }), runeWard: new fields.BooleanField({ initial: false }) @@ -302,6 +310,13 @@ export default class DhCharacter extends BaseDataActor { ); } + get armorApplicableDamageTypes() { + return { + physical: !this.rules.damageReduction.magical, + magical: !this.rules.damageReduction.physical + }; + } + static async unequipBeforeEquip(itemToEquip) { const primary = this.primaryWeapon, secondary = this.secondaryWeapon; @@ -368,6 +383,7 @@ export default class DhCharacter extends BaseDataActor { } const armor = this.armor; + this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0; // Bonuses to armorScore won't have been applied yet. Need to solve in documentPreparation somehow this.damageThresholds = { major: armor ? armor.system.baseThresholds.major + this.levelData.level.current @@ -395,7 +411,6 @@ export default class DhCharacter extends BaseDataActor { this.rules.damageReduction.maxArmorMarked.total = this.rules.damageReduction.maxArmorMarked.value + this.rules.damageReduction.maxArmorMarked.bonus; - this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0; this.resources.hitPoints.maxTotal = (this.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus; this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus; this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus; diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 4e5a26e6..696a95a2 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -22,7 +22,7 @@ export default class DHArmor extends BaseDataItem { tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }), equipped: new fields.BooleanField({ initial: false }), baseScore: new fields.NumberField({ integer: true, initial: 0 }), - features: new fields.ArrayField( + armorFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ required: true, @@ -44,25 +44,22 @@ export default class DHArmor extends BaseDataItem { }; } - get featureInfo() { - return this.feature ? CONFIG.DH.ITEM.armorFeatures[this.feature] : null; - } - async _preUpdate(changes, options, user) { const allowed = await super._preUpdate(changes, options, user); if (allowed === false) return false; - if (changes.system.features) { - const removed = this.features.filter(x => !changes.system.features.includes(x)); - const added = changes.system.features.filter(x => !this.features.includes(x)); + if (changes.system.armorFeatures) { + const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x)); + const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x)); + const effectIds = []; + const actionIds = []; for (var feature of removed) { - for (var effectId of feature.effectIds) { - await this.parent.effects.get(effectId).delete(); - } - - changes.system.actions = this.actions.filter(x => !feature.actionIds.includes(x._id)); + effectIds.push(...feature.effectIds); + actionIds.push(...feature.actionIds); } + await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds); + changes.system.actions = this.actions.filter(x => !actionIds.includes(x._id)); for (var feature of added) { const featureData = armorFeatures[feature.value]; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 812f7963..eab6d0fc 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -3,6 +3,7 @@ import { GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs'; import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; +import { damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; export default class DhpActor extends Actor { async _preCreate(data, options, user) { @@ -430,12 +431,31 @@ export default class DhpActor extends Actor { cls.create(msg.toObject()); } - async takeDamage(damage, type) { + #canReduceDamage(hpDamage, type) { + const availableStress = this.system.resources.stress.maxTotal - this.system.resources.stress.value; + + const canUseArmor = + this.system.armor && + this.system.armor.system.marks.value < this.system.armorScore && + this.system.armorApplicableDamageTypes[type]; + const canUseStress = Object.keys(this.system.rules.damageReduction.stressDamageReduction).reduce((acc, x) => { + const rule = this.system.rules.damageReduction.stressDamageReduction[x]; + if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost); + return acc; + }, false); + + return canUseArmor || canUseStress; + } + + async takeDamage(baseDamage, type) { if (this.type === 'companion') { await this.modifyResource([{ value: 1, type: 'stress' }]); return; } + const flatReduction = this.system.bonuses.damageReduction[type]; + const damage = Math.max(baseDamage - (flatReduction ?? 0), 0); + const hpDamage = damage >= this.system.damageThresholds.severe ? 3 @@ -445,13 +465,9 @@ export default class DhpActor extends Actor { ? 1 : 0; - if ( - this.type === 'character' && - this.system.armor && - this.system.armor.system.marks.value < this.system.armorScore - ) { + if (hpDamage && this.type === 'character' && this.#canReduceDamage(hpDamage, type)) { new Promise((resolve, reject) => { - new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); + new DamageReductionDialog(resolve, reject, this, hpDamage, type).render(true); }) .then(async ({ modifiedDamage, armorSpent, stressSpent }) => { const resources = [ diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index e719b25e..f284b958 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -190,6 +190,8 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => }); tagifyElement.on('add', event => { + if (event.detail.data.__isValid === 'not allowed') return; + const input = event.detail.tagify.DOM.originalInput; const currentList = input.value ? JSON.parse(input.value) : []; onChange([...currentList, event.detail.data], { option: event.detail.data.value, removed: false }, input); @@ -233,19 +235,23 @@ Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false return nativeReplaceFormulaData(formula, data, { missing, warn }); }; -export const getDamageLabel = damage => { +export const getDamageKey = damage => { switch (damage) { case 3: - return game.i18n.localize('DAGGERHEART.GENERAL.Damage.severe'); + return 'severe'; case 2: - return game.i18n.localize('DAGGERHEART.GENERAL.Damage.major'); + return 'major'; case 1: - return game.i18n.localize('DAGGERHEART.GENERAL.Damage.minor'); + return 'minor'; case 0: - return game.i18n.localize('DAGGERHEART.GENERAL.Damage.none'); + return 'none'; } }; +export const getDamageLabel = damage => { + return game.i18n.localize(`DAGGERHEART.GENERAL.Damage.${getDamageKey(damage)}`); +}; + export const damageKeyToNumber = key => { switch (key) { case 'severe':