From e32454b7c7a8bfe7aee479659d0a41017d4ca578 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 29 Nov 2025 13:22:10 +0100 Subject: [PATCH] Added Modifiers --- module/applications/ui/combatTracker.mjs | 36 +-- module/config/actorConfig.mjs | 3 +- module/config/encounterConfig.mjs | 28 ++ module/data/combat.mjs | 9 +- module/documents/tooltipManager.mjs | 180 ++++++++---- styles/less/ux/tooltip/battlepoints.less | 16 ++ styles/less/ux/tooltip/tooltip.less | 261 +++++++++--------- .../ui/combatTracker/combatTrackerHeader.hbs | 4 +- templates/ui/tooltip/battlepoints.hbs | 12 +- 9 files changed, 344 insertions(+), 205 deletions(-) diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 8ee685bb..f843ddb2 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -1,3 +1,5 @@ +import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs'; + export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { static DEFAULT_OPTIONS = { actions: { @@ -37,36 +39,16 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C async _prepareCombatContext(context, options) { await super._prepareCombatContext(context, options); - const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes(); - const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length); - const currentBP = context.adversaries - .reduce((acc, adversary) => { - const existingEntry = acc.find( - x => x.adversary.name === adversary.name && x.adversary.type === adversary.type - ); - if (existingEntry) { - existingEntry.nr += 1; - } else { - acc.push({ adversary, nr: 1 }); - } - return acc; - }, []) - .reduce((acc, entry) => { - const adversary = entry.adversary; - const type = adversaryTypes[adversary.type]; - const bpCost = type.bpCost ?? 0; - if (type.partyAmountPerBP) { - acc += context.characters.length === 0 ? 0 : Math.ceil(entry.nr / context.characters.length); - } else { - acc += bpCost; - } - - return acc; - }, 0); + const modifierBP = + this.combats + .find(x => x.active) + ?.system?.battleToggles?.reduce((acc, toggle) => acc + toggle.category, 0) ?? 0; + const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP; + const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters); Object.assign(context, { fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), - battlepoints: { max: maxBP, current: currentBP } + battlepoints: { max: maxBP, current: currentBP, hasModifierBP: Boolean(modifierBP) } }); } diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index 6540946d..7ff42754 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -121,7 +121,8 @@ export const adversaryTypes = { id: 'leader', label: 'DAGGERHEART.CONFIG.AdversaryType.leader.label', description: 'DAGGERHEART.ACTORS.Adversary.leader.description', - bpCost: 3 + bpCost: 3, + bpDescription: 'DAGGERHEART.CONFIG.AdversaryType.leader.' }, minion: { id: 'minion', diff --git a/module/config/encounterConfig.mjs b/module/config/encounterConfig.mjs index 1e6e0a44..509a79b2 100644 --- a/module/config/encounterConfig.mjs +++ b/module/config/encounterConfig.mjs @@ -1,5 +1,33 @@ export const BaseBPPerEncounter = nrCharacters => 3 * nrCharacters + 2; +export const AdversaryBPPerEncounter = (adversaries, characters) => { + const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes(); + return adversaries + .reduce((acc, adversary) => { + const existingEntry = acc.find( + x => x.adversary.name === adversary.name && x.adversary.type === adversary.type + ); + if (existingEntry) { + existingEntry.nr += 1; + } else { + acc.push({ adversary, nr: 1 }); + } + return acc; + }, []) + .reduce((acc, entry) => { + const adversary = entry.adversary; + const type = adversaryTypes[adversary.type]; + const bpCost = type.bpCost ?? 0; + if (type.partyAmountPerBP) { + acc += characters.length === 0 ? 0 : Math.ceil(entry.nr / characters.length); + } else { + acc += bpCost; + } + + return acc; + }, 0); +}; + export const adversaryTypeCostBrackets = { 1: [ { diff --git a/module/data/combat.mjs b/module/data/combat.mjs index e0490286..76ccf1b9 100644 --- a/module/data/combat.mjs +++ b/module/data/combat.mjs @@ -1,6 +1,13 @@ export default class DhCombat extends foundry.abstract.TypeDataModel { static defineSchema() { const fields = foundry.data.fields; - return {}; + return { + battleToggles: new fields.ArrayField( + new fields.SchemaField({ + category: new fields.NumberField({ required: true, integer: true }), + grouping: new fields.StringField({ required: true }) + }) + ) + }; } } diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index c174935f..71386bf8 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -1,8 +1,28 @@ +import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounterConfig.mjs'; + export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { + #wide = false; + async activate(element, options = {}) { const { TextEditor } = foundry.applications.ux; let html = options.html; + if (element.dataset.tooltip?.startsWith('#battlepoints#')) { + this.#wide = true; + + html = await this.getBattlepointHTML(element.dataset.combatId); + options.direction = this._determineItemTooltipDirection(element); + super.activate(element, { ...options, html: html }); + + const lockedTooltip = this.lockTooltip(); + lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => { + element.addEventListener('input', this.toggleModifier.bind(this)); + }); + return; + } else { + this.#wide = false; + } + if (element.dataset.tooltip?.startsWith('#item#')) { const itemUuid = element.dataset.tooltip.slice(6); const item = await foundry.utils.fromUuid(itemUuid); @@ -24,53 +44,6 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti this.tooltip.innerHTML = html; options.direction = this._determineItemTooltipDirection(element); } - } else if (element.dataset.tooltip?.startsWith('#battlepoints#')) { - const combat = game.combats.get(element.dataset.combatId); - const nrCharacters = Number(element.dataset.nrCharacters); - const currentBP = element.dataset.bpCurrent; - const maxBP = element.dataset.bpMax; - - const categories = combat.combatants.reduce((acc, combatant) => { - if (combatant.actor.type === 'adversary') { - const keyData = Object.keys(acc).reduce((identifiers, categoryKey) => { - if (identifiers) return identifiers; - const category = acc[categoryKey]; - const groupingIndex = category.findIndex(grouping => - grouping.types.includes(combatant.actor.system.type) - ); - if (groupingIndex !== -1) identifiers = { categoryKey, groupingIndex }; - - return identifiers; - }, null); - if (keyData) { - const { categoryKey, groupingIndex } = keyData; - const grouping = acc[categoryKey][groupingIndex]; - const partyAmount = - CONFIG.DH.ACTOR.adversaryTypes[combatant.actor.system.type].partyAmountPerBP; - grouping.individuals = (grouping.individuals ?? 0) + 1; - - const currentNr = grouping.nr ?? 0; - grouping.nr = partyAmount - ? Math.ceil(grouping.individuals / (nrCharacters ?? 0)) - : currentNr + 1; - } - } - - return acc; - }, foundry.utils.deepClone(CONFIG.DH.ENCOUNTER.adversaryTypeCostBrackets)); - - html = await foundry.applications.handlebars.renderTemplate( - `systems/daggerheart/templates/ui/tooltip/battlepoints.hbs`, - { - categories, - nrCharacters, - currentBP, - maxBP - } - ); - - this.tooltip.innerHTML = html; - options.direction = this._determineItemTooltipDirection(element); } else { const attack = element.dataset.tooltip?.startsWith('#attack#'); if (attack) { @@ -222,4 +195,117 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti } } } + + /**@inheritdoc */ + _setStyle(position = {}) { + super._setStyle(position); + + if (this.#wide) { + this.tooltip.classList.add('wide'); + } + } + + /**@inheritdoc */ + lockTooltip() { + const clone = super.lockTooltip(); + clone.classList.add('wide'); + + return clone; + } + /** Get HTML for Battlepoints tooltip */ + async getBattlepointHTML(combatId) { + const combat = game.combats.get(combatId); + const adversaries = + combat.turns?.filter(x => x.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? []; + const characters = combat.turns?.filter(x => !x.isNPC) ?? []; + + const nrCharacters = characters.length; + const currentBP = AdversaryBPPerEncounter(adversaries, characters); + const maxBP = combat.system.battleToggles.reduce( + (acc, toggle) => acc + toggle.category, + BaseBPPerEncounter(nrCharacters) + ); + + const categories = combat.combatants.reduce((acc, combatant) => { + if (combatant.actor.type === 'adversary') { + const keyData = Object.keys(acc).reduce((identifiers, categoryKey) => { + if (identifiers) return identifiers; + const category = acc[categoryKey]; + const groupingIndex = category.findIndex(grouping => + grouping.types.includes(combatant.actor.system.type) + ); + if (groupingIndex !== -1) identifiers = { categoryKey, groupingIndex }; + + return identifiers; + }, null); + if (keyData) { + const { categoryKey, groupingIndex } = keyData; + const grouping = acc[categoryKey][groupingIndex]; + const partyAmount = CONFIG.DH.ACTOR.adversaryTypes[combatant.actor.system.type].partyAmountPerBP; + grouping.individuals = (grouping.individuals ?? 0) + 1; + + const currentNr = grouping.nr ?? 0; + grouping.nr = partyAmount ? Math.ceil(grouping.individuals / (nrCharacters ?? 0)) : currentNr + 1; + } + } + + return acc; + }, foundry.utils.deepClone(CONFIG.DH.ENCOUNTER.adversaryTypeCostBrackets)); + + const toggles = Object.keys(CONFIG.DH.ENCOUNTER.BPModifiers) + .reduce((acc, categoryKey) => { + const category = CONFIG.DH.ENCOUNTER.BPModifiers[categoryKey]; + acc.push( + ...Object.keys(category).reduce((acc, toggleKey) => { + acc.push({ + ...category[toggleKey], + categoryKey: Number(categoryKey), + toggleKey, + checked: combat.system.battleToggles.find( + x => x.category == categoryKey && x.grouping === toggleKey + ) + }); + + return acc; + }, []) + ); + return acc; + }, []) + .sort((a, b) => { + if (a.categoryKey < b.categoryKey) return -1; + if (a.categoryKey > b.categoryKey) return 1; + else return a.toggleKey.localeCompare(b.toggleKey); + }); + + return await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/battlepoints.hbs`, + { + combatId: combat.id, + nrCharacters, + currentBP, + maxBP, + categories, + toggles + } + ); + } + + /** Enable/disable a BP modifier */ + async toggleModifier(event) { + const { combatId, category, grouping } = event.target.dataset; + const combat = game.combats.get(combatId); + await combat.update({ + system: { + battleToggles: combat.system.battleToggles.some(x => x.category == category && x.grouping === grouping) + ? combat.system.battleToggles.filter(x => x.category != category && x.grouping !== grouping) + : [...combat.system.battleToggles, { category: Number(category), grouping }] + } + }); + + this.tooltip.innerHTML = await this.getBattlepointHTML(combatId); + const lockedTooltip = this.lockTooltip(); + lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => { + element.addEventListener('input', this.toggleModifier.bind(this)); + }); + } } diff --git a/styles/less/ux/tooltip/battlepoints.less b/styles/less/ux/tooltip/battlepoints.less index d304183f..16427c37 100644 --- a/styles/less/ux/tooltip/battlepoints.less +++ b/styles/less/ux/tooltip/battlepoints.less @@ -3,6 +3,7 @@ display: flex; flex-direction: column; gap: 8px; + margin-bottom: 16px; .battlepoint-grouping-container { display: flex; @@ -13,4 +14,19 @@ } } } + + .battlepoint-toggles-container { + display: flex; + flex-direction: column; + gap: 8px; + + .battlepoint-toggle-container { + display: flex; + gap: 4px; + + input { + margin-top: 0; + } + } + } } diff --git a/styles/less/ux/tooltip/tooltip.less b/styles/less/ux/tooltip/tooltip.less index 1d7079ee..4579a3d8 100644 --- a/styles/less/ux/tooltip/tooltip.less +++ b/styles/less/ux/tooltip/tooltip.less @@ -1,140 +1,147 @@ -.daggerheart.dh-style.tooltip { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - border-width: 0; - - .tooltip-title-container { - width: 100%; - display: flex; - align-items: center; - gap: 16px; - - .tooltip-image { - height: 40px; - width: 40px; - border-radius: 6px; - border: 1px solid @golden; - } +#tooltip, +.locked-tooltip { + &.wide { + max-width: 480px; } - .tooltip-title { - margin: 0; - text-align: center; - } - - .tooltip-image { - height: 180px; - width: 180px; - } - - .tooltip-description { - font-style: italic; - text-align: start; - } - - .simple-info { - font-style: italic; - } - - .tooltip-sub-title { - margin: 0; - color: light-dark(@dark-blue, @beige); - } - - .tooltip-information-section { - width: 100%; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px; - - &.spaced { - margin-top: 8px; - } - - &.triple { - grid-template-columns: 1fr 1fr 1fr; - } - - .tooltip-information { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - - &.full-width { - grid-column: span 2; - } - - label { - font-weight: bold; - } - - label, - div { - white-space: nowrap; - } - } - } - - .tooltip-chips { - display: flex; - justify-content: space-around; - flex-wrap: wrap; - gap: 8px; - - .tooltip-chip { - font-size: var(--font-size-18); - padding: 2px 4px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - color: light-dark(@dark, @beige); - background-image: url(../assets/parchments/dh-parchment-dark.png); - } - } - - .tooltip-tags { - width: 100%; + .daggerheart.dh-style.tooltip { display: flex; flex-direction: column; + align-items: center; gap: 4px; + border-width: 0; - .tooltip-tag { + .tooltip-title-container { width: 100%; - display: grid; - grid-template-columns: 80px 1fr; - align-items: start; - gap: 8px; - padding: 4px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; + display: flex; + align-items: center; + gap: 16px; - .tooltip-tag-label-container { - display: flex; - align-items: center; - flex-direction: column; - gap: 2px; - - .tooltip-tag-image { - width: 40px; - height: 40px; - } - } - - .tooltip-tag-label { - font-weight: bold; - text-align: center; - } - - .tooltip-tag-description { - display: flex; - flex-wrap: wrap; + .tooltip-image { + height: 40px; + width: 40px; + border-radius: 6px; + border: 1px solid @golden; } } - } - .spaced { - margin-bottom: 4px; + .tooltip-title { + margin: 0; + text-align: center; + } + + .tooltip-image { + height: 180px; + width: 180px; + } + + .tooltip-description { + font-style: italic; + text-align: start; + } + + .simple-info { + font-style: italic; + } + + .tooltip-sub-title { + margin: 0; + color: light-dark(@dark-blue, @beige); + } + + .tooltip-information-section { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + + &.spaced { + margin-top: 8px; + } + + &.triple { + grid-template-columns: 1fr 1fr 1fr; + } + + .tooltip-information { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + + &.full-width { + grid-column: span 2; + } + + label { + font-weight: bold; + } + + label, + div { + white-space: nowrap; + } + } + } + + .tooltip-chips { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: 8px; + + .tooltip-chip { + font-size: var(--font-size-18); + padding: 2px 4px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@dark, @beige); + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + } + + .tooltip-tags { + width: 100%; + display: flex; + flex-direction: column; + gap: 4px; + + .tooltip-tag { + width: 100%; + display: grid; + grid-template-columns: 80px 1fr; + align-items: start; + gap: 8px; + padding: 4px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + + .tooltip-tag-label-container { + display: flex; + align-items: center; + flex-direction: column; + gap: 2px; + + .tooltip-tag-image { + width: 40px; + height: 40px; + } + } + + .tooltip-tag-label { + font-weight: bold; + text-align: center; + } + + .tooltip-tag-description { + display: flex; + flex-wrap: wrap; + } + } + } + + .spaced { + margin-bottom: 4px; + } } } diff --git a/templates/ui/combatTracker/combatTrackerHeader.hbs b/templates/ui/combatTracker/combatTrackerHeader.hbs index a3b1fd37..f9654639 100644 --- a/templates/ui/combatTracker/combatTrackerHeader.hbs +++ b/templates/ui/combatTracker/combatTrackerHeader.hbs @@ -64,7 +64,9 @@ {{/if}} -
{{battlepoints.current}}/{{battlepoints.max}} BP
+
+ {{battlepoints.current}}/{{battlepoints.max}} BP{{#if battlepoints.hasModifierBP}}*{{/if}} +
{{!-- Combat Controls --}}
diff --git a/templates/ui/tooltip/battlepoints.hbs b/templates/ui/tooltip/battlepoints.hbs index c97e877c..2e4ab2d6 100644 --- a/templates/ui/tooltip/battlepoints.hbs +++ b/templates/ui/tooltip/battlepoints.hbs @@ -1,10 +1,11 @@
+

{{localize "Adversaries"}} ({{currentBP}}/{{maxBP}})

{{#each categories as |category key|}} {{#each category as |grouping index|}}
{{#if grouping.nr}} - + {{else}} {{/if}} @@ -12,4 +13,13 @@ {{/each}} {{/each}}
+
+

{{localize "Modifiers"}}

+ {{#each toggles as |toggle|}} +
+ + +
+ {{/each}} +
\ No newline at end of file