daggerheart/module/documents/tooltipManager.mjs
WBHarry 451bef4c92
[Feature] Encounter Battlepoints (#1346)
* Added BP calculation and tooltip breakdown of BP sources

* Added Modifiers

* Fixed automatic battleToggles

* Corrected 'NoToughies' conditional

* Fixed GM-only visibility

* Fixed combatant isNPC
2025-12-06 21:11:34 +01:00

366 lines
15 KiB
JavaScript

import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounterConfig.mjs';
export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager {
#wide = false;
#bordered = 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 === '#effect-display#') {
this.#bordered = true;
let effect = {};
if (element.dataset.uuid) {
const effectData = (await foundry.utils.fromUuid(element.dataset.uuid)).toObject();
effect = {
...effectData,
name: game.i18n.localize(effectData.name),
description: game.i18n.localize(effectData.description ?? effectData.parent.system.description)
};
} else {
const conditions = CONFIG.DH.GENERAL.conditions();
const condition = conditions[element.dataset.condition];
effect = {
...condition,
name: game.i18n.localize(condition.name),
description: game.i18n.localize(condition.description),
appliedBy: element.dataset.appliedBy,
isLockedCondition: true
};
}
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{
effect
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
} else {
this.#bordered = false;
}
if (element.dataset.tooltip?.startsWith('#item#')) {
const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid);
if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect;
await this.enrichText(item, isAction || isEffect);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{
item: item,
description: item.system?.enrichedDescription ?? item.enrichedDescription,
config: CONFIG.DH
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
}
} else {
const attack = element.dataset.tooltip?.startsWith('#attack#');
if (attack) {
const actorUuid = element.dataset.tooltip.slice(8);
const actor = await foundry.utils.fromUuid(actorUuid);
const attack = actor.system.attack;
const description = await TextEditor.enrichHTML(attack.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{
attack: attack,
description: description,
parent: actor,
config: CONFIG.DH
}
);
this.tooltip.innerHTML = html;
}
const shortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const longRest = element.dataset.tooltip?.startsWith('#longRest#');
if (shortRest || longRest) {
const key = element.dataset.tooltip.slice(shortRest ? 11 : 10);
const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[
element.dataset.restType
].moves;
const move = moves[key];
const description = await TextEditor.enrichHTML(move.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move,
description: description
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
}
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
if (isAdvantage || isDisadvantage) {
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
}
}
const deathMove = element.dataset.tooltip?.startsWith('#deathMove#');
if (deathMove) {
const name = element.dataset.deathName;
const img = element.dataset.deathImg;
const description = element.dataset.deathDescription;
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/death-move.hbs`,
{
move: { name: name, img: img, description: description }
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
}
}
super.activate(element, { ...options, html: html });
}
_setStyle(position = {}) {
super._setStyle(position);
if (this.#bordered) {
this.tooltip.classList.add('bordered-tooltip');
}
}
_determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
const pos = element.getBoundingClientRect();
const dirs = this.constructor.TOOLTIP_DIRECTIONS;
switch (prefered) {
case this.constructor.TOOLTIP_DIRECTIONS.LEFT:
return dirs[
pos.x - this.tooltip.offsetWidth < 0
? this.constructor.TOOLTIP_DIRECTIONS.DOWN
: this.constructor.TOOLTIP_DIRECTIONS.LEFT
];
case this.constructor.TOOLTIP_DIRECTIONS.UP:
return dirs[
pos.y - this.tooltip.offsetHeight < 0
? this.constructor.TOOLTIP_DIRECTIONS.RIGHT
: this.constructor.TOOLTIP_DIRECTIONS.UP
];
case this.constructor.TOOLTIP_DIRECTIONS.RIGHT:
return dirs[
pos.x + this.tooltip.offsetWidth > document.body.clientWidth
? this.constructor.TOOLTIP_DIRECTIONS.DOWN
: this.constructor.TOOLTIP_DIRECTIONS.RIGHT
];
case this.constructor.TOOLTIP_DIRECTIONS.DOWN:
return dirs[
pos.y + this.tooltip.offsetHeight > document.body.clientHeight
? this.constructor.TOOLTIP_DIRECTIONS.LEFT
: this.constructor.TOOLTIP_DIRECTIONS.DOWN
];
}
}
async enrichText(item, flatStructure) {
const { TextEditor } = foundry.applications.ux;
const enrichPaths = [
{ path: flatStructure ? '' : 'system', name: 'description' },
{ path: 'system', name: 'features' },
{ path: 'system', name: 'actions' },
{ path: 'system', name: 'customActions' }
];
for (let data of enrichPaths) {
const basePath = `${data.path ? `${data.path}.` : ''}${data.name}`;
const pathValue = foundry.utils.getProperty(item, basePath);
if (!pathValue) continue;
if (Array.isArray(pathValue) || pathValue.size) {
for (const [index, itemValue] of pathValue.entries()) {
const itemIsAction = itemValue instanceof game.system.api.models.actions.actionsTypes.base;
const value = itemIsAction || !itemValue?.item ? itemValue : itemValue.item;
const enrichedValue = await TextEditor.enrichHTML(value.system?.description ?? value.description);
if (itemIsAction) value.enrichedDescription = enrichedValue;
else foundry.utils.setProperty(item, `${basePath}.${index}.enrichedDescription`, enrichedValue);
}
} else {
const enrichedValue = await TextEditor.enrichHTML(pathValue);
foundry.utils.setProperty(
item,
`${data.path ? `${data.path}.` : ''}enriched${data.name.capitalize()}`,
enrichedValue
);
}
}
}
/**@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.actor?.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.extendedBattleToggles.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 extendedBattleToggles = combat.system.extendedBattleToggles;
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) => {
const grouping = category[toggleKey];
acc.push({
...grouping,
categoryKey: Number(categoryKey),
toggleKey,
checked: extendedBattleToggles.find(
x => x.category == categoryKey && x.grouping === toggleKey
),
disabled: grouping.automatic
});
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 }]
}
});
await combat.toggleModifierEffects(
event.target.checked,
combat.combatants.filter(x => x.actor.type === 'adversary').map(x => x.actor),
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));
});
}
}