From c6bf482b07731cbe31adae65d7caa857e61f25ad Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sun, 8 Mar 2026 13:59:08 +0100 Subject: [PATCH] Initial --- daggerheart.mjs | 24 ++++ .../applications/sheets/actors/character.mjs | 66 ++++++++++ module/config/actorConfig.mjs | 123 +++++++++++------- module/data/actor/base.mjs | 2 +- module/data/actor/character.mjs | 46 ++++--- module/data/item/base.mjs | 7 +- module/helpers/utils.mjs | 10 +- .../less/sheets/actors/character/header.less | 9 +- styles/less/ux/index.less | 2 + .../less/ux/tooltip/resource-management.less | 37 ++++++ templates/sheets/actors/character/header.hbs | 35 ++--- templates/ui/tooltip/resourceManagement.hbs | 13 ++ 12 files changed, 280 insertions(+), 94 deletions(-) create mode 100644 styles/less/ux/tooltip/resource-management.less create mode 100644 templates/ui/tooltip/resourceManagement.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 05b57ac9..ac9772cd 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -97,6 +97,30 @@ Hooks.once('init', () => { fields }; + CONFIG.DH.ACTOR.characterResources.corruption = { + id: 'corruption', + initial: 0, + max: 4, + reverse: false, + label: 'Corruption' + }; + + CONFIG.DH.ACTOR.characterResources.hunger = { + id: 'hunger', + initial: 0, + max: 6, + reverse: false, + label: 'Hunger' + }; + + CONFIG.DH.ACTOR.characterResources.glitched = { + id: 'glitched', + initial: 0, + max: 6, + reverse: false, + label: 'Glitched' + }; + game.system.registeredTriggers = new game.system.api.data.RegisteredTriggers(); const { DocumentSheetConfig } = foundry.applications.apps; diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 4ecaeb06..fbe52578 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -33,6 +33,7 @@ export default class CharacterSheet extends DHBaseActorSheet { handleResourceDice: CharacterSheet.#handleResourceDice, advanceResourceDie: CharacterSheet.#advanceResourceDie, cancelBeastform: CharacterSheet.#cancelBeastform, + toggleResourceManagement: CharacterSheet.#toggleResourceManagement, useDowntime: this.useDowntime, viewParty: CharacterSheet.#viewParty }, @@ -942,6 +943,71 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } + static async #toggleResourceManagement(_event, button) { + const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container'); + if (existingTooltip) { + game.tooltip.dismissLockedTooltips(); + return; + } + + const extraResources = Object.values(CONFIG.DH.ACTOR.characterResources).reduce((acc, resource) => { + if (CONFIG.DH.ACTOR.characterBaseResources[resource.id]) return acc; + + const resourceData = this.document.system.resources[resource.id]; + acc[resource.id] = { + id: resource.id, + label: game.i18n.localize(resource.label), + value: resourceData.value, + max: resourceData.max + }; + + return acc; + }, {}); + + const html = document.createElement('div'); + html.innerHTML = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/resourceManagement.hbs`, + { + resources: extraResources + } + ); + + const target = button.closest('.resource-section'); + + game.tooltip.dismissLockedTooltips(); + game.tooltip.activate(target, { + html, + locked: true, + cssClass: 'bordered-tooltip', + direction: 'DOWN' + }); + + for (const element of html.querySelectorAll('.resource-value')) + element.addEventListener('click', CharacterSheet.resourceUpdate.bind(this)); + } + + static async resourceUpdate(event) { + const target = event.target.closest('.resource-value'); + const { resource, value: textValue } = target.dataset; + + const inputValue = Number.parseInt(textValue); + const decreasing = inputValue <= this.document.system.resources[resource].value; + const value = decreasing ? inputValue - 1 : inputValue; + await this.document.update({ [`system.resources.${resource}.value`]: value }); + + /* Update resource symbols */ + const section = target.closest('.resource-section'); + for (const element of section.querySelectorAll('.resource-value')) { + if (Number.parseInt(element.dataset.value) <= value) { + element.querySelector('.fa-diamond').classList.remove('hidden'); + element.querySelector('.fa-circle').classList.add('hidden'); + } else { + element.querySelector('.fa-diamond').classList.add('hidden'); + element.querySelector('.fa-circle').classList.remove('hidden'); + } + } + } + /** * Open the downtime application. * @type {ApplicationClickAction} diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index ac55117a..ae92a893 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -55,24 +55,47 @@ export const abilities = { } }; -export const scrollingTextResource = { +const baseResources = { hitPoints: { + id: 'hitPoints', + initial: 0, + max: 0, + reverse: true, label: 'DAGGERHEART.GENERAL.HitPoints.plural', - reversed: true + maxLabel: 'DAGGERHEART.ACTORS.Character.maxHPBonus' }, stress: { - label: 'DAGGERHEART.GENERAL.stress', - reversed: true + id: 'stress', + initial: 0, + max: 6, + reverse: true, + label: 'DAGGERHEART.GENERAL.stress' }, hope: { + id: 'hope', + initial: 2, + min: 0, + reverse: false, label: 'DAGGERHEART.GENERAL.hope' - }, - armor: { - label: 'DAGGERHEART.GENERAL.armor', - reversed: true } }; +export const characterBaseResources = { + ...baseResources +}; + +export const characterResources = { + ...characterBaseResources +}; + +export const getScrollingTextResources = actorType => ({ + armor: { + label: 'DAGGERHEART.GENERAL.armor', + reverse: true + }, + ...(actorType === 'character' ? characterResources : {}) +}); + export const featureProperties = { agility: { name: 'DAGGERHEART.CONFIG.Traits.agility.name', @@ -506,8 +529,8 @@ export const subclassFeatureLabels = { * @property {number[]} damage */ -/** - * @type {Record} +/** + * @type {Record} * Scaling data used to change an adversary's tier. Each rank is applied incrementally. */ export const adversaryScalingData = { @@ -518,7 +541,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 2, - attack: 2, + attack: 2 }, 3: { difficulty: 2, @@ -526,7 +549,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 0, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -534,7 +557,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 1, stress: 0, - attack: 2, + attack: 2 } }, horde: { @@ -544,7 +567,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 2, stress: 0, - attack: 0, + attack: 0 }, 3: { difficulty: 2, @@ -552,7 +575,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 0, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -560,7 +583,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 2, stress: 0, - attack: 0, + attack: 0 } }, leader: { @@ -570,7 +593,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -578,7 +601,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 0, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -586,7 +609,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 1, stress: 1, - attack: 3, + attack: 3 } }, minion: { @@ -596,7 +619,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -604,7 +627,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -612,7 +635,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 0, - attack: 1, + attack: 1 } }, ranged: { @@ -622,7 +645,7 @@ export const adversaryScalingData = { severeThreshold: 6, hp: 1, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -630,7 +653,7 @@ export const adversaryScalingData = { severeThreshold: 14, hp: 1, stress: 1, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -638,7 +661,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } }, skulk: { @@ -648,7 +671,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -656,7 +679,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -664,7 +687,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } }, solo: { @@ -674,7 +697,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 0, stress: 1, - attack: 2, + attack: 2 }, 3: { difficulty: 2, @@ -682,7 +705,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 2, stress: 1, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -690,7 +713,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 0, stress: 1, - attack: 3, + attack: 3 } }, standard: { @@ -700,7 +723,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -708,7 +731,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -716,7 +739,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 0, stress: 1, - attack: 1, + attack: 1 } }, support: { @@ -726,7 +749,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -734,7 +757,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -742,27 +765,27 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } } }; -/** +/** * Scaling data used for an adversary's damage. * Tier 4 is missing certain adversary types and therefore skews upwards. * We manually set tier 4 data to hopefully lead to better results */ export const adversaryExpectedDamage = { - basic: { - 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, - 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, - 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, - 4: { mean: 26, deviation: 5.2 } - }, - minion: { - 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, - 2: { mean: 5, deviation: 0.816496580927726 }, - 3: { mean: 6.5, deviation: 2.1213203435596424 }, - 4: { mean: 11, deviation: 1 } - } + basic: { + 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, + 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, + 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, + 4: { mean: 26, deviation: 5.2 } + }, + minion: { + 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, + 2: { mean: 5, deviation: 0.816496580927726 }, + 3: { mean: 6.5, deviation: 2.1213203435596424 }, + 4: { mean: 11, deviation: 1 } + } }; diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 5e16bac9..913bdc5e 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -213,7 +213,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { const textData = Object.keys(changes.system.resources).reduce((acc, key) => { const resource = changes.system.resources[key]; if (resource.value !== undefined && resource.value !== this.resources[key].value) { - acc.push(getScrollTextData(this.resources, resource, key)); + acc.push(getScrollTextData(this.resources, resource, key, this.parent.type)); } return acc; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 10fba63c..34b397e9 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -28,26 +28,32 @@ export default class DhCharacter extends DhCreature { return { ...super.defineSchema(), resources: new fields.SchemaField({ - hitPoints: resourceField( - 0, - 0, - 'DAGGERHEART.GENERAL.HitPoints.plural', - true, - 'DAGGERHEART.ACTORS.Character.maxHPBonus' - ), - stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), - hope: new fields.SchemaField( - { - value: new fields.NumberField({ - initial: 2, - min: 0, - integer: true, - label: 'DAGGERHEART.GENERAL.hope' - }), - isReversed: new fields.BooleanField({ initial: false }) - }, - { label: 'DAGGERHEART.GENERAL.hope' } - ) + ...Object.values(CONFIG.DH.ACTOR.characterResources).reduce((acc, resource) => { + if (resource.max !== undefined) { + acc[resource.id] = resourceField( + resource.max, + resource.initial, + resource.label, + resource.reverse, + resource.maxLabel + ); + } else { + acc[resource.id] = new fields.SchemaField( + { + value: new fields.NumberField({ + initial: resource.initial, + min: resource.min, + integer: true, + label: resource.label + }), + isReversed: new fields.BooleanField({ initial: resource.reverse }) + }, + { label: resource.label } + ); + } + + return acc; + }, {}) }), traits: new fields.SchemaField({ agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 84f39103..3b11e945 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -224,7 +224,12 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const armorChanged = changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value; if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') { - const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor'); + const armorData = getScrollTextData( + this.parent.parent.system.resources, + changed.system.marks, + 'armor', + this.parent.parent.type + ); options.scrollingTextData = [armorData]; } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index c8b62ff6..bdb72586 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -378,17 +378,17 @@ export const arraysEqual = (a, b) => export const setsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value)); -export function getScrollTextData(resources, resource, key) { - const { reversed, label } = CONFIG.DH.ACTOR.scrollingTextResource[key]; +export function getScrollTextData(resources, resource, key, actorType) { + const { reverse, label } = CONFIG.DH.ACTOR.getScrollingTextResources(actorType)[key]; const { BOTTOM, TOP } = CONST.TEXT_ANCHOR_POINTS; const increased = resources[key].value < resource.value; const value = -1 * (resources[key].value - resource.value); const text = `${game.i18n.localize(label)} ${value.signedString()}`; - const stroke = increased ? (reversed ? 0xffffff : 0x000000) : reversed ? 0x000000 : 0xffffff; - const fill = increased ? (reversed ? 0x0032b1 : 0xffe760) : reversed ? 0xffe760 : 0x0032b1; - const direction = increased ? (reversed ? BOTTOM : TOP) : reversed ? TOP : BOTTOM; + const stroke = increased ? (reverse ? 0xffffff : 0x000000) : reverse ? 0x000000 : 0xffffff; + const fill = increased ? (reverse ? 0x0032b1 : 0xffe760) : reverse ? 0xffe760 : 0x0032b1; + const direction = increased ? (reverse ? BOTTOM : TOP) : reverse ? TOP : BOTTOM; return { text, stroke, fill, direction }; } diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 5e8ef002..a90e6a2e 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -133,8 +133,15 @@ padding: 0; margin-bottom: 15px; - .hope-section { + .resource-section { + display: flex; + align-items: center; + gap: 4px; margin-right: 20px; + + .resource-manager { + color: light-dark(@dark-blue, @golden); + } } .downtime-section { diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index 0bd1b71e..c6c40f78 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -4,3 +4,5 @@ @import './tooltip/domain-cards.less'; @import './autocomplete/autocomplete.less'; + +@import './tooltip/resource-management.less'; diff --git a/styles/less/ux/tooltip/resource-management.less b/styles/less/ux/tooltip/resource-management.less new file mode 100644 index 00000000..57878662 --- /dev/null +++ b/styles/less/ux/tooltip/resource-management.less @@ -0,0 +1,37 @@ +.bordered-tooltip.locked-tooltip .daggerheart.resource-management-container { + display: flex; + flex-direction: column; + gap: 16px; + + .resource-section { + position: relative; + display: flex; + gap: 10px; + background-color: light-dark(transparent, @dark-blue); + color: light-dark(@dark-blue, @golden); + padding: 5px 10px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + align-items: center; + width: fit-content; + height: 30px; + + h4 { + font-family: var(--dh-font-body, 'Montserrat'), sans-serif; + font-size: var(--font-size-14); + font-weight: bold; + text-transform: uppercase; + color: light-dark(@dark-blue, @golden); + margin: 0; + } + + .resource-value { + display: flex; + cursor: pointer; + + .hidden { + display: none; + } + } + } +} diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index d2c01f3c..fec866a2 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -65,22 +65,25 @@
-
-

{{localize "DAGGERHEART.GENERAL.hope"}}

- {{#times document.system.resources.hope.max}} - - {{#if (gte ../document.system.resources.hope.value (add this 1))}} - - {{else}} - - {{/if}} - - {{/times}} - {{#times document.system.scars}} - - - - {{/times}} +
+
+

{{localize "DAGGERHEART.GENERAL.hope"}}

+ {{#times document.system.resources.hope.max}} + + {{#if (gte ../document.system.resources.hope.value (add this 1))}} + + {{else}} + + {{/if}} + + {{/times}} + {{#times document.system.scars}} + + + + {{/times}} +
+
{{#if document.system.class.value}}
diff --git a/templates/ui/tooltip/resourceManagement.hbs b/templates/ui/tooltip/resourceManagement.hbs new file mode 100644 index 00000000..4030819a --- /dev/null +++ b/templates/ui/tooltip/resourceManagement.hbs @@ -0,0 +1,13 @@ +
+ {{#each resources as |resource|}} +
+

{{resource.label}}

+ {{#times resource.max}} + + + + + {{/times}} +
+ {{/each}} +
\ No newline at end of file