diff --git a/lang/en.json b/lang/en.json index ddd6a3c4..be0bdd91 100755 --- a/lang/en.json +++ b/lang/en.json @@ -207,6 +207,12 @@ "Session": "Session", "Shortrest": "Short Rest", "Longrest": "Long Rest" + }, + "Damage": { + "Severe": "Severe", + "Major": "Major", + "Minor": "Minor", + "None": "None" } }, "ActionType": { @@ -1084,6 +1090,16 @@ "Title": "Ownership Selection - {name}", "Default": "Default Ownership" }, + "DamageReduction": { + "Title": "Damage Reduction", + "ArmorMarks": "Armor Marks", + "UsedMarks": "Used Marks", + "Stress": "Stress", + "Notifications": { + "DamageAlreadyNone": "The damage has already been reduced to none", + "NotEnoughArmor": "You don't have enough unspent armor marks" + } + }, "Sheets": { "PC": { "Name": "Name", diff --git a/module/applications/damageReductionDialog.mjs b/module/applications/damageReductionDialog.mjs new file mode 100644 index 00000000..3783560c --- /dev/null +++ b/module/applications/damageReductionDialog.mjs @@ -0,0 +1,112 @@ +import { getDamageLabel } from '../helpers/utils.mjs'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(resolve, reject, actor, damage) { + super({}); + + this.resolve = resolve; + this.reject = reject; + this.actor = actor; + this.damage = damage; + + this.availableArmorMarks = { + max: actor.system.rules.maxArmorMarked.total + (actor.system.rules.stressExtra ?? 0), + maxUseable: actor.system.armorScore - actor.system.armor.system.marks.value, + stressIndex: + (actor.system.rules.stressExtra ?? 0) > 0 ? actor.system.rules.maxArmorMarked.total : undefined, + selected: 0 + }; + } + + get title() { + return game.i18n.localize('DAGGERHEART.DamageReduction.Title'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'views', 'damage-reduction'], + position: { + width: 240, + height: 'auto' + }, + actions: { + setMarks: this.setMarks, + takeDamage: this.takeDamage + }, + form: { + handler: this.updateData, + submitOnChange: true, + closeOnSubmit: false + } + }; + + /** @override */ + static PARTS = { + damageSelection: { + id: 'damageReduction', + template: 'systems/daggerheart/templates/views/damageReduction.hbs' + } + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + get title() { + return `Damage Options`; + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.armorScore = this.actor.system.armorScore; + context.armorMarks = this.actor.system.armor.system.marks.value + this.availableArmorMarks.selected; + context.availableArmorMarks = this.availableArmorMarks; + + context.damage = getDamageLabel(this.damage); + context.reducedDamage = + this.availableArmorMarks.selected > 0 + ? getDamageLabel(this.damage - this.availableArmorMarks.selected) + : null; + + return context; + } + + static updateData(event, _, formData) { + const form = foundry.utils.expandObject(formData.object); + this.render(true); + } + + static setMarks(_, target) { + const index = Number(target.dataset.index); + if (index >= this.availableArmorMarks.maxUseable) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.NotEnoughArmor')); + return; + } + + const isDecreasing = index < this.availableArmorMarks.selected; + if (!isDecreasing && this.damage - this.availableArmorMarks.selected === 0) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.DamageReduction.Notifications.DamageAlreadyNone')); + return; + } + + this.availableArmorMarks.selected = isDecreasing ? index : index + 1; + this.render(); + } + + static async takeDamage() { + const armorSpent = this.availableArmorMarks.selected; + const modifiedDamage = this.damage - armorSpent; + + this.resolve({ modifiedDamage, armorSpent }); + await this.close(true); + } + + async close(fromSave) { + if (!fromSave) { + this.reject(); + } + + await super.close({}); + } +} diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 273b7a72..13ded6a7 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -83,6 +83,18 @@ export default class DhCharacter extends BaseDataActor { attack: new fields.NumberField({ integer: true, initial: 0 }), spellcast: new fields.NumberField({ integer: true, initial: 0 }), armorScore: new fields.NumberField({ integer: true, initial: 0 }) + }), + rules: new fields.SchemaField({ + maxArmorMarked: new fields.SchemaField({ + value: new fields.NumberField({ required: true, integer: true, initial: 1 }), + bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }), + stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 }) + }), + stressDamageReduction: new fields.SchemaField({ + enabled: new fields.BooleanField({ required: true, initial: false }), + cost: new fields.NumberField({ integer: true }), + fromSeverity: new fields.NumberField({ integer: true, max: 3 }) + }) }) }; } @@ -232,6 +244,9 @@ export default class DhCharacter extends BaseDataActor { experience.total = experience.value + experience.bonus; } + this.rules.maxArmorMarked.total = this.rules.maxArmorMarked.value + this.rules.maxArmorMarked.bonus; + + this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0; this.resources.hitPoints.maxTotal = this.resources.hitPoints.max + 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 b0fdf0ae..c7f5af0b 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -29,7 +29,6 @@ export default class DHArmor extends BaseDataItem { }) ), marks: new fields.SchemaField({ - max: new fields.NumberField({ initial: 6, integer: true }), value: new fields.NumberField({ initial: 0, integer: true }) }), baseThresholds: new fields.SchemaField({ diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 3b5fe734..02643c04 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -4,6 +4,7 @@ import RollSelectionDialog from '../applications/rollSelectionDialog.mjs'; import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import DHDualityRoll from '../data/chat-message/dualityRoll.mjs'; +import DamageReductionDialog from '../applications/damageReductionDialog.mjs'; export default class DhpActor extends Actor { async _preCreate(data, options, user) { @@ -266,21 +267,21 @@ export default class DhpActor extends Actor { */ async diceRoll(config, action) { // console.log(config) - config.source = {...(config.source ?? {}), actor: this.id}; + config.source = { ...(config.source ?? {}), actor: this.id }; const newConfig = { // data: { - ...config, - /* action, */ - // actor: this.getRollData(), - actor: this.system + ...config, + /* action, */ + // actor: this.getRollData(), + actor: this.system // }, // options: { - // dialog: false, + // dialog: false, // }, // event: config.event - } + }; // console.log(this, newConfig) - const roll = CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll'].build(newConfig) + const roll = CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll'].build(newConfig); return config; /* let hopeDice = 'd12', fearDice = 'd12', @@ -508,67 +509,67 @@ export default class DhpActor extends Actor { async takeDamage(damage, type) { const hpDamage = damage >= this.system.damageThresholds.severe - ? -3 + ? 3 : damage >= this.system.damageThresholds.major - ? -2 + ? 2 : damage >= this.system.damageThresholds.minor - ? -1 + ? 1 : 0; - await this.modifyResource([{value: hpDamage, type}]); - /* const update = { - 'system.resources.hitPoints.value': Math.min( - this.system.resources.hitPoints.value + hpDamage, - this.system.resources.hitPoints.max - ) - }; - if (game.user.isGM) { - await this.update(update); - } else { - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: this.uuid, - update: update - } + if ( + this.type === 'character' && + this.system.armor && + this.system.armor.system.marks.value < this.system.armorScore + ) { + new Promise((resolve, reject) => { + new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); + }).then(async ({ modifiedDamage, armorSpent }) => { + const resources = [ + { value: modifiedDamage, type: 'hitPoints' }, + ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []) + ]; + await this.modifyResource(resources); }); - } */ + } else { + await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]); + } } async modifyResource(resources) { - if(!resources.length) return; - let updates = { actor: { target: this, resources: {} }, armor: { target: this.armor, resources: {} } }; + if (!resources.length) return; + let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; resources.forEach(r => { - switch (type) { - case 'armorStrack': - // resource = 'system.stacks.value'; - // target = this.armor; - // update = Math.min(this.marks.value + value, this.marks.max); - updates.armor.resources['system.stacks.value'] = Math.min(this.marks.value + value, this.marks.max); + switch (r.type) { + case 'armorStack': + updates.armor.resources['system.marks.value'] = Math.min( + this.system.armor.system.marks.value + r.value, + this.system.armorScore + ); break; default: - // resource = `system.resources.${type}`; - // target = this; - // update = Math.min(this.resources[type].value + value, this.resources[type].max); - updates.armor.resources[`system.resources.${type}`] = Math.min(this.resources[type].value + value, this.resources[type].max); + updates.actor.resources[`system.resources.${r.type}.value`] = Math.min( + this.system.resources[r.type].value + r.value, + this.system.resources[r.type].max + ); break; } - }) - Object.values(updates).forEach(async (u) => { - if (game.user.isGM) { - await u.target.update(u.resources); - } else { - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: u.target.uuid, - update: u.resources - } - }); + }); + Object.values(updates).forEach(async u => { + if (Object.keys(u.resources).length > 0) { + if (game.user.isGM) { + await u.target.update(u.resources); + } else { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateDocument, + uuid: u.target.uuid, + update: u.resources + } + }); + } } - }) + }); } /* async takeHealing(healing, type) { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 1e12e430..dc1601ad 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -235,3 +235,16 @@ Roll.replaceFormulaData = function (formula, data, { missing, warn = false } = { formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula); return nativeReplaceFormulaData(formula, data, { missing, warn }); }; + +export const getDamageLabel = damage => { + switch (damage) { + case 3: + return game.i18n.localize('DAGGERHEART.General.Damage.Severe'); + case 2: + return game.i18n.localize('DAGGERHEART.General.Damage.Major'); + case 1: + return game.i18n.localize('DAGGERHEART.General.Damage.Minor'); + case 0: + return game.i18n.localize('DAGGERHEART.General.Damage.None'); + } +}; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 44e011ee..387c7c75 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -3113,6 +3113,80 @@ div.daggerheart.views.multiclass { .daggerheart.views.ownership-selection .ownership-outer-container .ownership-container select { margin: 4px 0; } +.daggerheart.views.damage-reduction .window-content { + padding: 8px 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .section-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container .padded { + padding: 0 8px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .armor-title { + margin: 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + margin: 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container { + cursor: pointer; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + height: 26px; + width: 26px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.4; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container.selected { + opacity: 1; +} +.daggerheart.views.damage-reduction .damage-reduction-container .mark-selection .mark-container.disabled { + cursor: initial; +} +.daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle { + margin: -4px 0 0 0; +} +.daggerheart.views.damage-reduction .damage-reduction-container .markers-subtitle.bold { + font-variant: all-small-caps; + font-weight: bold; +} +.daggerheart.views.damage-reduction .damage-reduction-container .damage-container { + display: flex; + justify-content: center; + gap: 4px; + font-weight: bold; + height: 18px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .damage-container i { + font-size: 18px; +} +.daggerheart.views.damage-reduction .damage-reduction-container .damage-container .reduced-value { + opacity: 0.4; + text-decoration: line-through; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer { + display: flex; + width: 100%; +} +.daggerheart.views.damage-reduction .damage-reduction-container footer button { + flex: 1; +} :root { --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 9094d312..32317832 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -11,6 +11,7 @@ @import './characterCreation.less'; @import './levelup.less'; @import './ownershipSelection.less'; +@import './damageReduction.less'; @import './resources.less'; @import './countdown.less'; @import './settings.less'; diff --git a/styles/damageReduction.less b/styles/damageReduction.less new file mode 100644 index 00000000..723a7022 --- /dev/null +++ b/styles/damageReduction.less @@ -0,0 +1,91 @@ +.daggerheart.views.damage-reduction { + .window-content { + padding: 8px 0; + } + + .damage-reduction-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + .section-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + } + + .padded { + padding: 0 8px; + } + + .armor-title { + margin: 0; + } + + .mark-selection { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + margin: 0; + + .mark-container { + cursor: pointer; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + height: 26px; + width: 26px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.4; + + &.selected { + opacity: 1; + } + + &.disabled { + cursor: initial; + } + } + } + + .markers-subtitle { + margin: -4px 0 0 0; + + &.bold { + font-variant: all-small-caps; + font-weight: bold; + } + } + + .damage-container { + display: flex; + justify-content: center; + gap: 4px; + font-weight: bold; + height: 18px; + + i { + font-size: 18px; + } + + .reduced-value { + opacity: 0.4; + text-decoration: line-through; + } + } + + footer { + display: flex; + width: 100%; + + button { + flex: 1; + } + } + } +} diff --git a/templates/views/damageReduction.hbs b/templates/views/damageReduction.hbs new file mode 100644 index 00000000..0fcc47b0 --- /dev/null +++ b/templates/views/damageReduction.hbs @@ -0,0 +1,40 @@ +
+
+

{{localize "DAGGERHEART.DamageReduction.ArmorMarks"}}

+
{{armorMarks}}/{{armorScore}}
+
+ +
+

+ {{#times availableArmorMarks.max}} +
+ {{#if (or (not @root.availableArmorMarks.stressIndex) (lt this @root.availableArmorMarks.stressIndex))}} + + {{else}} + + {{/if}} + +
+ {{/times}} +

+
{{localize "DAGGERHEART.DamageReduction.UsedMarks"}}
+
+ +
+
{{localize "Incoming Damage"}}
+
+
{{this.damage}}
+ {{#if this.reducedDamage}} + +
{{this.reducedDamage}}
+ {{/if}} +
+
+ + +
\ No newline at end of file