Added countdowns UI element

This commit is contained in:
WBHarry 2025-09-14 19:52:28 +02:00
parent 2c51f06f86
commit 3c8116c4dc
11 changed files with 206 additions and 461 deletions

View file

@ -8,10 +8,8 @@ import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations, runMigrations,
@ -140,6 +138,7 @@ Hooks.once('init', () => {
CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TooltipManager = documents.DhTooltipManager;
@ -166,10 +165,12 @@ Hooks.on('ready', async () => {
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
ui.resources.render({ force: true }); ui.resources.render({ force: true });
ui.countdowns = new CONFIG.ui.countdowns();
ui.countdowns.render({ force: true });
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
ui.compendiumBrowser = new applications.ui.ItemBrowser(); ui.compendiumBrowser = new applications.ui.ItemBrowser();
registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerRollDiceHooks(); registerRollDiceHooks();
socketRegistration.registerUserQueries(); socketRegistration.registerUserQueries();
@ -242,29 +243,6 @@ Hooks.on('chatMessage', (_, message) => {
} }
}); });
Hooks.on('renderJournalDirectory', async (tab, html, _, options) => {
if (tab.id === 'journal') {
if (options.parts && !options.parts.includes('footer')) return;
const buttons = tab.element.querySelector('.directory-footer.action-buttons');
const title = game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.types.narrative')
});
buttons.insertAdjacentHTML(
'afterbegin',
`
<button id="narrative-countdown-button">
<i class="fa-solid fa-stopwatch"></i>
<span style="font-weight: 400; font-family: var(--font-sans);">${title}</span>
</button>`
);
buttons.querySelector('#narrative-countdown-button').onclick = async () => {
new NarrativeCountdowns().open();
};
}
});
Hooks.on('moveToken', async (movedToken, data) => { Hooks.on('moveToken', async (movedToken, data) => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return; if (!effectsAutomation.rangeDependent) return;

View file

@ -2470,6 +2470,10 @@
"playerMessage": "{user} rerolled their {name}" "playerMessage": "{user} rerolled their {name}"
} }
}, },
"Countdowns": {
"title": "Countdowns",
"minimize": "Minimize"
},
"ItemBrowser": { "ItemBrowser": {
"title": "Daggerheart Compendium Browser", "title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium", "hint": "Select a Folder in sidebar to start browsing through the compendium",

View file

@ -1,7 +1,7 @@
export { default as CountdownEdit } from './countdownEdit.mjs'; export { default as CountdownEdit } from './countdownEdit.mjs';
export { default as DhCountdowns } from './countdowns.mjs';
export { default as DhChatLog } from './chatLog.mjs'; export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { ItemBrowser } from './itemBrowser.mjs'; export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -1,5 +1,3 @@
import { EncounterCountdowns } from '../ui/countdowns.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
actions: { actions: {
@ -168,8 +166,4 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex }); await combatant.update({ 'system.actionTokens': newIndex });
this.render(); this.render();
} }
static openCountdowns() {
new EncounterCountdowns().open();
}
} }

View file

@ -1,4 +1,5 @@
import { DhCountdown } from '../../data/countdowns.mjs'; import { DhCountdown } from '../../data/countdowns.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -62,7 +63,10 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
async updateSetting(update) { async updateSetting(update) {
await this.data.updateSource(update); await this.data.updateSource(update);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, this.data); await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown
});
this.render(); this.render();
} }
@ -70,6 +74,15 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
this.updateSetting(foundry.utils.expandObject(formData.object)); this.updateSetting(foundry.utils.expandObject(formData.object));
} }
async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
static #addCountdown() { static #addCountdown() {
this.updateSetting({ this.updateSetting({
[`countdowns.${foundry.utils.randomID()}`]: DhCountdown.defaultCountdown() [`countdowns.${foundry.utils.randomID()}`]: DhCountdown.defaultCountdown()

View file

@ -1,355 +1,139 @@
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { /**
constructor(basePath) { * A UI element which displays the countdowns in this world.
super({}); *
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
this.basePath = basePath; export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
} constructor(options = {}) {
super(options);
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', { this.sidebarCollapsed = true;
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`) this.setupHooks();
});
} }
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown'], id: 'countdowns',
tag: 'form', tag: 'div',
position: { width: 740, height: 700 }, classes: ['daggerheart', 'dh-style', 'countdowns'],
window: { window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true, frame: true,
title: 'Countdowns', title: 'Fear',
resizable: true, positioned: false,
resizable: false,
minimizable: false minimizable: false
}, },
actions: { actions: {
addCountdown: this.addCountdown, decreaseCountdown: (_, target) => this.editCountdown(false, target),
removeCountdown: this.removeCountdown, increaseCountdown: (_, target) => this.editCountdown(true, target)
editImage: this.onEditImage,
openOwnership: this.openOwnership,
openCountdownOwnership: this.openCountdownOwnership,
toggleSimpleView: this.toggleSimpleView
}, },
form: { handler: this.updateData, submitOnChange: true } position: {
}; width: 400,
height: 222,
static PARTS = { top: 50
countdowns: {
template: 'systems/daggerheart/templates/ui/countdowns.hbs',
scrollable: ['.expanded-view']
} }
}; };
_attachPartListeners(partId, htmlElement, options) { /** @override */
super._attachPartListeners(partId, htmlElement, options); static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
}
};
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { get title() {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false)); return game.i18n.localize('DAGGERHEART.UI.Countdowns.title');
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true));
});
} }
async _preFirstRender(context, options) { get element() {
options.position = return document.body.querySelector('.daggerheart.dh-style.countdowns');
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position) ??
Countdowns.DEFAULT_OPTIONS.position;
const viewSetting =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
this.simpleView =
game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
context.simple = this.simpleView;
}
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position, position);
} }
/**@inheritdoc */
async _renderFrame(options) { async _renderFrame(options) {
const frame = await super._renderFrame(options); const frame = await super._renderFrame(options);
const header = frame.querySelector('.window-header');
header.querySelector('button[data-action="close"]').remove();
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) { const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.minimize');
const button = constructHTMLButton({ const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="minimize"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
label: '', header.insertAdjacentHTML('beforeEnd', minimizeButton);
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' }
});
this.window.controls.after(button);
}
return frame; return frame;
} }
testUserPermission(level, exact, altSettings) { /** @override */
if (game.user.isGM) return true; async _prepareContext(options) {
const context = await super._prepareContext(options);
context.sidebarCollapsed = this.sidebarCollapsed;
const settings = const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; context.countdowns = Object.keys(setting.countdowns).reduce((acc, key) => {
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level; const countdown = setting.countdowns[key];
const userAllowed = exact const playerOwnership = countdown.ownership[game.user.id];
? settings.playerOwnership[game.user.id]?.value === level const ownership =
: settings.playerOwnership[game.user.id]?.value >= level; playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
return defaultAllowed || userAllowed; ? setting.defaultOwnership
} : playerOwnership;
if (ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) return acc;
async _prepareContext(_options) { acc[key] = {
const context = await super._prepareContext(_options); ...countdown,
const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[ editable: game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
this.basePath };
]; return acc;
}, {});
context.isGM = game.user.isGM;
context.base = this.basePath;
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
context.source = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = countdownData.countdowns[key];
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
acc[key] = {
...countdown,
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
};
}
return acc;
}, {})
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.simple = this.simpleView;
return context; return context;
} }
static async updateData(event, _, formData) { toggleCollapsedPosition = async (_, collapsed) => {
const data = foundry.utils.expandObject(formData.object); this.sidebarCollapsed = collapsed;
const newSetting = foundry.utils.mergeObject( if (!collapsed) this.element.classList.add('expanded');
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(), else this.element.classList.remove('expanded');
data };
);
if (game.user.isGM) { cooldownRefresh = ({ refreshType }) => {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting); if (refreshType === RefreshType.Countdown) this.render();
this.render(); };
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, { static async editCountdown(increase, target) {
action: socketEvent.GMUpdate, const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
data: { const countdown = settings.countdowns[target.id];
action: GMUpdateEvent.UpdateSetting, const newCurrent = increase
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns, ? Math.min(countdown.progress.current + 1, countdown.progress.max)
update: newSetting : Math.max(countdown.progress.current - 1, 0);
} await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
}); await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
} refreshType: RefreshType.Countdown
});
} }
async updateSetting(update) { static async gmSetSetting(data) {
if (game.user.isGM) { await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update); game.socket.emit(`system.${CONFIG.DH.id}`, {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { data: { refreshType: RefreshType.Countdown }
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
this.render();
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
} }
static onEditImage(_, target) { setupHooks() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; Hooks.on('collapseSidebar', this.toggleCollapsedPosition.bind());
const current = setting.countdowns[target.dataset.countdown].img; Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
} }
async updateImage(path, countdown) { close(options) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); Hooks.off('collapseSidebar', this.toggleCollapsedPosition);
await setting.updateSource({ Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
[`${this.basePath}.countdowns.${countdown}.img`]: path super.close(options);
});
await this.updateSetting(setting);
}
static openOwnership(_, target) {
new Promise((resolve, reject) => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
static async toggleSimpleView() {
this.simpleView = !this.simpleView;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
this.render();
}
async updateCountdownValue(event, increase) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
return;
}
const currentValue = countdown.progress.current;
if (increase && currentValue === countdown.progress.max) return;
if (!increase && currentValue === 0) return;
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
});
await this.updateSetting(countdownSetting.toObject());
}
static async addCountdown() {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
ownership: game.user.isGM
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
}
});
await this.updateSetting(countdownSetting.toObject());
}
static async removeCountdown(_, target) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns
).length > 0
) {
this.minimize();
}
} }
} }
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export async function updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
return foundry.utils.mergeObject(
update,
Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
const countdown = countdownSetting[typeKey].countdowns[countdownKey];
if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
}
return acc;
}, {})
);
}, {});
await countdownSetting.updateSource(update);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
const data = { refreshType: RefreshType.Countdown };
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data
});
Hooks.callAll(socketEvent.Refresh, data);
}

View file

@ -1,5 +1,3 @@
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
export default class DhCountdowns extends foundry.abstract.DataModel { export default class DhCountdowns extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -136,18 +134,3 @@ export class DhCountdown extends foundry.abstract.DataModel {
}, {}); }, {});
} }
} }
export const registerCountdownHooks = () => {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
if (refreshType === RefreshType.Countdown) {
if (application) {
foundry.applications.instances.get(application)?.render();
} else {
foundry.applications.instances.get('narrative-countdowns')?.render();
foundry.applications.instances.get('encounter-countdowns')?.render();
}
return false;
}
});
};

View file

@ -25,6 +25,7 @@ export const GMUpdateEvent = {
UpdateEffect: 'DhGMUpdateEffect', UpdateEffect: 'DhGMUpdateEffect',
UpdateSetting: 'DhGMUpdateSetting', UpdateSetting: 'DhGMUpdateSetting',
UpdateFear: 'DhGMUpdateFear', UpdateFear: 'DhGMUpdateFear',
UpdateCountdowns: 'DhGMUpdateCountdowns',
UpdateSaveMessage: 'DhGMUpdateSaveMessage' UpdateSaveMessage: 'DhGMUpdateSaveMessage'
}; };
@ -60,6 +61,10 @@ export const registerSocketHooks = () => {
) )
); );
break; break;
case GMUpdateEvent.UpdateCountdowns:
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
break;
case GMUpdateEvent.UpdateSaveMessage: case GMUpdateEvent.UpdateSaveMessage:
const action = await fromUuid(data.update.action), const action = await fromUuid(data.update.action),
message = game.messages.get(data.update.message); message = game.messages.get(data.update.message);
@ -84,14 +89,15 @@ export const registerUserQueries = () => {
CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery; CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery;
}; };
export const emitAsGM = async (eventName, callback, update, uuid = null) => { export const emitAsGM = async (eventName, callback, update, uuid = null, refresh = null) => {
if (!game.user.isGM) { if (!game.user.isGM) {
return await game.socket.emit(`system.${CONFIG.DH.id}`, { return await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate, action: socketEvent.GMUpdate,
data: { data: {
action: eventName, action: eventName,
uuid, uuid,
update update,
refresh
} }
}); });
} else return callback(update); } else return callback(update);

View file

@ -1,60 +1,70 @@
@import '../../utils/colors.less'; .theme-dark {
@import '../../utils/fonts.less'; .daggerheart.dh-style.countdowns {
background-image: url(../assets/parchments/dh-parchment-dark.png);
.daggerheart.dh-style.countdown { .window-header {
fieldset { background-image: url(../assets/parchments/dh-parchment-dark.png);
align-items: center; }
margin-top: 5px; }
border-radius: 6px; }
border-color: light-dark(@dark-blue, @golden);
.daggerheart.dh-style.countdowns {
legend { z-index: var(--z-index-ui) !important;
font-weight: bold; border: 0;
color: light-dark(@dark-blue, @golden); border-radius: 4px;
box-shadow: none;
a { width: 300px;
text-shadow: none; top: 16px;
} right: 64px;
transition: right ease 250ms;
&.expanded {
right: 364px;
}
.window-header {
cursor: default;
border-bottom: 0;
}
.window-content {
padding-top: 4px;
padding-bottom: 16px;
.countdowns-container {
display: flex;
flex-direction: column;
gap: 8px;
.countdown-container {
display: flex;
gap: 16px;
img {
width: 44px;
height: 44px;
}
.countdown-content {
height: 44px;
display: flex;
flex-direction: column;
justify-content: space-between;
.countdown-tools {
display: flex;
align-items: center;
gap: 16px;
.progress-tag {
border: 1px solid;
border-radius: 4px;
padding: 2px 4px;
background-color: light-dark(@beige, @dark-blue);
}
}
}
}
} }
} }
.minimized-view {
display: flex;
gap: 8px;
flex-wrap: wrap;
.mini-countdown-container {
width: fit-content;
display: flex;
align-items: center;
gap: 8px;
border: 2px solid light-dark(@dark-blue, @golden);
border-radius: 6px;
padding: 0 4px 0 0;
background-image: url('../assets/parchments/dh-parchment-light.png');
color: light-dark(@beige, @dark);
cursor: pointer;
&.disabled {
cursor: initial;
}
img {
width: 30px;
height: 30px;
border-radius: 6px 0 0 6px;
}
.mini-countdown-name {
white-space: nowrap;
}
.mini-countdown-value {
}
}
}
.hidden {
display: none;
}
} }

View file

@ -67,7 +67,6 @@
{{!-- Combat Controls --}} {{!-- Combat Controls --}}
<div class="inner-controls"> <div class="inner-controls">
{{#if hasCombat}} {{#if hasCombat}}
<button data-action="openCountdowns">{{localize "DAGGERHEART.APPLICATIONS.CombatTracker.openCountdowns"}}</button>
<div class="control-buttons right flexrow"> <div class="control-buttons right flexrow">
<div class="spacer"></div> <div class="spacer"></div>
<button type="button" class="encounter-context-menu inline-control combat-control icon fa-solid fa-ellipsis-vertical" <button type="button" class="encounter-context-menu inline-control combat-control icon fa-solid fa-ellipsis-vertical"

View file

@ -1,45 +1,19 @@
<div> <div>
{{#if simple}} <div class="countdowns-container">
<div class="minimized-view"> {{#each countdowns as | countdown id |}}
{{#each source.countdowns}} <div class="countdown-container">
<a class="mini-countdown-container {{#if (not this.canEdit)}}disabled{{/if}}" data-countdown="{{@key}}"> <img src="{{countdown.img}}" />
<img src="{{this.img}}" /> <div class="countdown-content">
<div class="mini-countdown-name">{{this.name}}</div> <label>{{countdown.name}}</label>
<div class="mini-countdown-value">{{this.progress.current}}/{{this.progress.max}}</div> <div class="countdown-tools">
</a> {{#if countdown.editable}}<a data-action="decreaseCountdown" id="{{id}}"><i class="fa-solid fa-minus"></i></a>{{/if}}
{{/each}} <div class="progress-tag">
</div> {{countdown.progress.current}}/{{countdown.progress.max}}
{{else}}
<div class="expanded-view">
<div class="countdowns-menu">
{{#if canCreate}}<button class="flex" data-action="addCountdown">{{localize "DAGGERHEART.APPLICATIONS.Countdown.addCountdown"}}</button>{{/if}}
{{#if isGM}}<button data-action="openOwnership" data-tooltip="{{localize "DAGGERHEART.APPLICATIONS.Countdown.openOwnership"}}"><i class="fa-solid fa-users"></i></button>{{/if}}
</div>
<div class="countdowns-container">
{{#each source.countdowns}}
<fieldset class="countdown-fieldset">
<legend>
{{this.name}}
{{#if this.canEdit}}<a><i class="fa-solid fa-trash icon-button" data-action="removeCountdown" data-countdown="{{@key}}"></i></a>{{/if}}
{{#if @root.isGM}}<a><i class="fa-solid fa-users icon-button" data-action="openCountdownOwnership" data-countdown="{{@key}}" data-tooltip="{{localize "DAGGERHEART.APPLICATIONS.Countdown.openOwnership"}}"></i></a>{{/if}}
</legend>
<div class="countdown-container">
<img src="{{this.img}}" {{#if this.canEdit}}data-action='editImage'{{else}}class="disabled"{{/if}} data-countdown="{{@key}}" />
<div class="countdown-inner-container">
{{formGroup @root.countdownFields.name name=(concat @root.base ".countdowns." @key ".name") value=this.name localize=true disabled=(not this.canEdit)}}
<div class="countdown-value-container">
{{formGroup @root.countdownFields.progress.fields.current name=(concat @root.base ".countdowns." @key ".progress.current") value=this.progress.current localize=true disabled=(not this.canEdit)}}
{{formGroup @root.countdownFields.progress.fields.max name=(concat @root.base ".countdowns." @key ".progress.max") value=this.progress.max localize=true disabled=(not this.canEdit)}}
</div>
{{formGroup @root.countdownFields.progress.fields.type.fields.value name=(concat @root.base ".countdowns." @key ".progress.type.value") value=this.progress.type.value localize=true localize=true disabled=(not this.canEdit)}}
</div>
</div> </div>
</fieldset> {{#if countdown.editable}}<a data-action="increaseCountdown" id="{{id}}"><i class="fa-solid fa-plus"></i></a>{{/if}}
{{/each}} </div>
</div>
</div> </div>
</div> {{/each}}
{{/if}} </div>
</div> </div>