diff --git a/lang/en.json b/lang/en.json index 54c70ae9..59b058ab 100755 --- a/lang/en.json +++ b/lang/en.json @@ -437,7 +437,7 @@ "text": "Are you sure you want to delete {name}?" }, "DamageReduction": { - "armorMarks": "Armor Marks", + "maxUseableArmor": "Useable Armor Slots", "armorWithStress": "Spend 1 stress to use an extra mark", "thresholdImmunities": "Threshold Immunities", "stress": "Stress", diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index d4a2b4d3..31e4f72d 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -10,7 +10,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.reject = reject; this.actor = actor; this.damage = damage; - this.damageType = damageType; + // this.damageType = damageType; + this.damageType = ['physical']; this.rulesDefault = game.settings.get( CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation @@ -20,14 +21,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.rulesDefault ); - const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true); - const availableArmor = actor.system.armorScore.max - actor.system.armorScore.value; - const maxArmorMarks = canApplyArmor ? availableArmor : 0; + const allArmorEffects = Array.from(actor.allApplicableEffects()).filter(x => x.type === 'armor'); + const orderedArmorEffects = game.system.api.data.activeEffects.ArmorEffect.orderEffectsForAutoChange( + 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; - }, {}); + }, []); const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce( (acc, _) => { acc[foundry.utils.randomID()] = { selected: false }; @@ -121,13 +133,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap context.thresholdImmunities = Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null; - const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = + const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } = this.getDamageInfo(); context.armorScore = this.actor.system.armorScore.max; context.armorMarks = currentMarks; - context.basicMarksUsed = - selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value; const stressReductionStress = this.availableStressReductions ? stressReductions.reduce((acc, red) => acc + red.cost, 0) @@ -141,16 +151,27 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } : null; - 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; + context.maxArmorUsed = maxArmorUsed; + context.availableArmor = availableArmor; + context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length; - 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 }; + + context.usesStressArmor = Object.keys(context.marks.stress).length; context.availableStressReductions = this.availableStressReductions; context.damage = getDamageLabel(this.damage); @@ -167,27 +188,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } 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 stressReductions = this.availableStressReductions ? Object.values(this.availableStressReductions).filter(red => red.selected) : []; - const currentMarks = - this.actor.system.armorScore.value + selectedArmorMarks.length + selectedStressMarks.length; + const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.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 = selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark; - let currentDamage = Math.max( - this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length, - 0 - ); + let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0); if (this.reduceSeverity) { currentDamage = Math.max(currentDamage - this.reduceSeverity, 0); } if (this.thresholdImmunities[currentDamage]) currentDamage = 0; - return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; + return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor }; }; static toggleRules() { @@ -209,8 +234,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } static setMarks(_, target) { - const currentMark = this.marks[target.dataset.type][target.dataset.key]; - const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo(); + const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path); + const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo(); if (!currentMark.selected && currentDamage === 0) { 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 (!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')); 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) { const currentDamageLabel = getDamageLabel(currentDamage); for (let reduction of stressReductions) { @@ -232,8 +263,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } } - if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) { - selectedStressMarks.forEach(mark => (mark.selected = false)); + if (target.dataset.type === 'stress' && currentMark.armorMarkId) { + 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(); } + 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) { const damageValue = Number(target.dataset.reduction); const stressReduction = this.availableStressReductions[damageValue]; @@ -279,11 +337,19 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } static async takeDamage() { - const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); - const armorSpent = selectedArmorMarks.length + selectedStressMarks.length; - const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0); + const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); + const armorChanges = this.marks.armor.reduce((acc, source) => { + 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); } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index c9a6047b..b9b6a123 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -966,11 +966,16 @@ export default class CharacterSheet extends DHBaseActorSheet { if (armorSources.length <= 1) return; + const useResourcePips = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.appearance + ).useResourcePips; const html = document.createElement('div'); html.innerHTML = await foundry.applications.handlebars.renderTemplate( `systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`, { - sources: armorSources + sources: armorSources, + useResourcePips } ); @@ -986,6 +991,10 @@ export default class CharacterSheet extends DHBaseActorSheet { element.addEventListener('blur', CharacterSheet.armorSourceUpdate); element.addEventListener('input', CharacterSheet.armorSourceInput); }); + + html.querySelectorAll('.armor-slot').forEach(element => { + element.addEventListener('click', CharacterSheet.armorSourcePipUpdate); + }); } static async armorSourceInput(event) { @@ -1000,12 +1009,11 @@ export default class CharacterSheet extends DHBaseActorSheet { static async armorSourceUpdate(event) { const effect = await foundry.utils.fromUuid(event.target.dataset.uuid); if (effect.system.changes.length !== 1) return; - const armorEffect = effect.system.changes[0]; const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0); const newChanges = [ { - ...armorEffect, + ...effect.system.changes[0], value } ]; @@ -1017,6 +1025,38 @@ export default class CharacterSheet extends DHBaseActorSheet { await effect.update({ 'system.changes': newChanges }); } + static async armorSourcePipUpdate(event) { + const target = event.target.closest('.armor-slot'); + const effect = await foundry.utils.fromUuid(target.dataset.uuid); + if (effect.system.changes.length !== 1) return; + const { value, max } = effect.system.armorData; + + const inputValue = Number.parseInt(target.dataset.value); + const decreasing = value >= inputValue; + const newValue = decreasing ? inputValue - 1 : inputValue; + + const newChanges = [ + { + ...effect.system.changes[0], + value: newValue + } + ]; + + const container = target.closest('.slot-bar'); + for (const armorSlot of container.querySelectorAll('.armor-slot i')) { + const index = Number.parseInt(armorSlot.dataset.index); + if (decreasing && index >= newValue) { + armorSlot.classList.remove('fa-shield'); + armorSlot.classList.add('fa-shield-halved'); + } else if (!decreasing && index < newValue) { + armorSlot.classList.add('fa-shield'); + armorSlot.classList.remove('fa-shield-halved'); + } + } + + await effect.update({ 'system.changes': newChanges }); + } + /** * Open the downtime application. * @type {ApplicationClickAction} diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 87f203ec..1ede6554 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -559,6 +559,18 @@ export default class DhCharacter extends BaseDataActor { 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() { const ancestryFeatures = [], communityFeatures = [], diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 64e877c9..2bc0179a 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -626,12 +626,10 @@ export default class DhpActor extends Actor { } ); if (armorSlotResult) { - const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult; + const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; updates.find(u => u.key === 'hitPoints').value = modifiedDamage; - if (armorSpent) { - const armorUpdate = updates.find(u => u.key === 'armor'); - if (armorUpdate) armorUpdate.value += armorSpent; - else updates.push({ value: armorSpent, key: 'armor' }); + for (const armorChange of armorChanges) { + updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid }); } if (stressSpent) { const stressUpdate = updates.find(u => u.key === 'stress'); @@ -778,7 +776,8 @@ export default class DhpActor extends Actor { ); break; case 'armor': - this.system.updateArmorValue(r); + if (!r.uuid) this.system.updateArmorValue(r); + else this.system.updateArmorEffectValue(r); break; default: if (this.system.resources?.[r.key]) { diff --git a/styles/less/dialog/damage-reduction/damage-reduction-container.less b/styles/less/dialog/damage-reduction/damage-reduction-container.less index 2f343fb3..e8242bdd 100644 --- a/styles/less/dialog/damage-reduction/damage-reduction-container.less +++ b/styles/less/dialog/damage-reduction/damage-reduction-container.less @@ -35,7 +35,10 @@ display: flex; flex-direction: column; align-items: center; - width: 100%; + + &.full-width { + width: 100%; + } } .padded { @@ -45,6 +48,7 @@ .armor-title { margin: 0; white-space: nowrap; + width: 100%; } .resources-container { @@ -62,12 +66,17 @@ .mark-selection { display: flex; - align-items: center; + flex-direction: column; width: 100%; margin: 0; + h4 { + margin: 0; + } + .mark-selection-inner { display: flex; + justify-content: center; gap: 8px; .mark-container { @@ -91,6 +100,19 @@ 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 { position: relative; right: 0.5px; diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index 42e40386..ddd3a348 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -276,6 +276,23 @@ } } + .slot-label { + .slot-value-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + + i { + position: absolute; + right: 0; + font-size: 12px; + color: light-dark(@beige, @dark-blue); + } + } + } + .status-value { padding: 0 5px; } diff --git a/styles/less/ux/tooltip/armorManagement.less b/styles/less/ux/tooltip/armorManagement.less index bc716fa0..390c0a00 100644 --- a/styles/less/ux/tooltip/armorManagement.less +++ b/styles/less/ux/tooltip/armorManagement.less @@ -97,4 +97,27 @@ } } } + + .slot-bar { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 5px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + z-index: 1; + background: @dark-blue; + justify-content: center; + color: light-dark(@dark-blue, @golden); + + .armor-slot { + cursor: pointer; + transition: all 0.3s ease; + font-size: var(--font-size-12); + + .fa-shield-halved { + color: light-dark(@dark-blue-40, @golden-40); + } + } + } } diff --git a/templates/dialogs/damageReduction.hbs b/templates/dialogs/damageReduction.hbs index 57d7ee61..50fe3422 100644 --- a/templates/dialogs/damageReduction.hbs +++ b/templates/dialogs/damageReduction.hbs @@ -7,53 +7,57 @@