From 906c7ac853ece6470e3a59d24ac42427f92099a0 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:24:38 +0100 Subject: [PATCH] [Feature] 613 - Countdown Improvements (#1184) * Added CountdownEdit view * Added countdowns UI element * . * Fixed migration of countdowns * . * . * style countdown interface, application and ownership dialog * fix buttons height in ownsership selection * . * Added coloured pips to UI cooldowns to signify player visibility if not every player has it * . * Added max-height and overflow * Sync countdown current with max when equal (#1221) * Update module/applications/ui/countdownEdit.mjs Co-authored-by: Carlos Fernandez * . --------- Co-authored-by: moliloo Co-authored-by: Carlos Fernandez --- daggerheart.mjs | 30 +- lang/en.json | 31 +- .../dialogs/ownershipSelection.mjs | 67 +-- .../sidebar/tabs/daggerheartMenu.mjs | 7 +- module/applications/ui/_module.mjs | 3 +- module/applications/ui/combatTracker.mjs | 6 - module/applications/ui/countdownEdit.mjs | 199 ++++++++ module/applications/ui/countdowns.mjs | 459 ++++++------------ module/config/flagsConfig.mjs | 3 +- module/config/generalConfig.mjs | 27 ++ module/data/countdowns.mjs | 119 ++++- module/systemRegistration/migrations.mjs | 31 ++ module/systemRegistration/socket.mjs | 10 +- styles/less/ui/countdown/countdown-edit.less | 142 ++++++ styles/less/ui/countdown/countdown.less | 176 +++++-- styles/less/ui/index.less | 1 + .../ownership-selection.less | 22 +- styles/less/ui/sidebar/daggerheartMenu.less | 14 +- templates/dialogs/ownershipSelection.hbs | 24 +- templates/levelup/tabs/viewMode.hbs | 2 +- templates/sidebar/daggerheart-menu/main.hbs | 4 + .../ui/combatTracker/combatTrackerHeader.hbs | 1 - templates/ui/countdown-edit.hbs | 76 +++ templates/ui/countdowns.hbs | 68 ++- 24 files changed, 1024 insertions(+), 498 deletions(-) create mode 100644 module/applications/ui/countdownEdit.mjs create mode 100644 styles/less/ui/countdown/countdown-edit.less create mode 100644 templates/ui/countdown-edit.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index d7aba401..e079703a 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -8,10 +8,8 @@ import * as fields from './module/data/fields/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.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 { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; -import { registerCountdownHooks } from './module/data/countdowns.mjs'; import { handlebarsRegistration, runMigrations, @@ -140,6 +138,7 @@ Hooks.once('init', () => { CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.ui.resources = applications.ui.DhFearTracker; + CONFIG.ui.countdowns = applications.ui.DhCountdowns; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; 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') ui.resources.render({ force: true }); + ui.countdowns = new CONFIG.ui.countdowns(); + ui.countdowns.render({ force: true }); + if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) ui.compendiumBrowser = new applications.ui.ItemBrowser(); - registerCountdownHooks(); socketRegistration.registerSocketHooks(); registerRollDiceHooks(); 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', - ` - ` - ); - - buttons.querySelector('#narrative-countdown-button').onclick = async () => { - new NarrativeCountdowns().open(); - }; - } -}); - Hooks.on('moveToken', async (movedToken, data) => { const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; if (!effectsAutomation.rangeDependent) return; diff --git a/lang/en.json b/lang/en.json index 38a498fe..c21c1d5b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -331,7 +331,8 @@ "label": { "label": "Label", "hint": "Used for custom" }, "value": { "label": "Value" } } - } + }, + "type": { "label": "Countdown Type" } } } }, @@ -346,6 +347,26 @@ "encounter": "Encounter" } }, + "CountdownEdit": { + "title": "Countdown Edit", + "viewTitle": "Countdowns", + "editTitle": "Edit Countdowns", + "newCountdown": "New Countdown", + "removeCountdownTitle": "Remove Countdown", + "removeCountdownText": "Are you sure you want to remove the countdown: {name}?", + "current": "Current", + "max": "Max", + "currentCountdownValue": "Current: {value}", + "currentCountdownMax": "Max: {value}", + "category": "Category", + "type": "Type", + "defaultOwnershipTooltip": "The default player ownership of countdowns", + "hideNewCountdowns": "Hide New Countdowns" + }, + "DaggerheartMenu": { + "title": "GM Tools", + "countdowns": "Edit Countdowns" + }, "DeleteConfirmation": { "title": "Delete {type} - {name}", "text": "Are you sure you want to delete {name}?" @@ -2455,6 +2476,11 @@ "playerMessage": "{user} rerolled their {name}" } }, + "Countdowns": { + "title": "Countdowns", + "toggleIconMode": "Toggle Icon Only", + "noPlayerAccess": "This countdown isn't visible to any players" + }, "ItemBrowser": { "title": "Daggerheart Compendium Browser", "hint": "Select a Folder in sidebar to start browsing through the compendium", @@ -2562,7 +2588,8 @@ "subclassesAlreadyPresent": "You already have a class and multiclass subclass", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "gmMenuRefresh": "You refreshed all actions and resources {types}", - "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class." + "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.", + "gmRequired": "This action requires an online GM" }, "Sidebar": { "daggerheartMenu": { diff --git a/module/applications/dialogs/ownershipSelection.mjs b/module/applications/dialogs/ownershipSelection.mjs index e4a7e628..049f4d99 100644 --- a/module/applications/dialogs/ownershipSelection.mjs +++ b/module/applications/dialogs/ownershipSelection.mjs @@ -1,18 +1,20 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(resolve, reject, name, ownership) { + constructor(name, ownership, defaultOwnership) { super({}); - this.resolve = resolve; - this.reject = reject; this.name = name; - this.ownership = ownership; + this.ownership = foundry.utils.deepClone(ownership); + this.defaultOwnership = defaultOwnership; } static DEFAULT_OPTIONS = { tag: 'form', - classes: ['daggerheart', 'views', 'ownership-selection'], + classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'], + window: { + icon: 'fa-solid fa-users' + }, position: { width: 600, height: 'auto' @@ -30,43 +32,48 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name }); } + getOwnershipData(id) { + return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT; + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({ - value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level], - label: game.i18n.localize(`OWNERSHIP.${level}`) - })); - context.ownership = { - default: this.ownership.default, - players: Object.keys(this.ownership.players).reduce((acc, x) => { - const user = game.users.get(x); - if (!user.isGM) { - acc[x] = { - img: user.character?.img ?? 'icons/svg/cowled.svg', - name: user.name, - ownership: this.ownership.players[x].value - }; - } + context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels; + context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels; + context.defaultOwnership = this.defaultOwnership; + context.ownership = game.users.reduce((acc, user) => { + if (!user.isGM) { + acc[user.id] = { + ...user, + img: user.character?.img ?? 'icons/svg/cowled.svg', + ownership: this.getOwnershipData(user.id) + }; + } - return acc; - }, {}) - }; + return acc; + }, {}); return context; } static async updateData(event, _, formData) { - const { ownership } = foundry.utils.expandObject(formData.object); - - this.resolve(ownership); - this.close(true); + const data = foundry.utils.expandObject(formData.object); + this.close(data); } - async close(fromSave) { - if (!fromSave) { - this.reject(); + async close(data) { + if (data) { + this.saveData = data; } await super.close(); } + + static async configure(name, ownership, defaultOwnership) { + return new Promise(resolve => { + const app = new this(name, ownership, defaultOwnership); + app.addEventListener('close', () => resolve(app.saveData), { once: true }); + app.render({ force: true }); + }); + } } diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index cf7aeae3..1fb5d09f 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -29,7 +29,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract }, actions: { selectRefreshable: DaggerheartMenu.#selectRefreshable, - refreshActors: DaggerheartMenu.#refreshActors + refreshActors: DaggerheartMenu.#refreshActors, + editCountdowns: DaggerheartMenu.#editCountdowns } }; @@ -157,4 +158,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract this.render(); } + + static async #editCountdowns() { + new game.system.api.applications.ui.CountdownEdit().render(true); + } } diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index 815fc4e7..35a58566 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -1,6 +1,7 @@ +export { default as CountdownEdit } from './countdownEdit.mjs'; +export { default as DhCountdowns } from './countdowns.mjs'; export { default as DhChatLog } from './chatLog.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs'; -export * as DhCountdowns from './countdowns.mjs'; export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhHotbar } from './hotbar.mjs'; export { ItemBrowser } from './itemBrowser.mjs'; diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 101e20d6..b70f8c71 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -1,5 +1,3 @@ -import { EncounterCountdowns } from '../ui/countdowns.mjs'; - export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { static DEFAULT_OPTIONS = { actions: { @@ -184,8 +182,4 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C await combatant.update({ 'system.actionTokens': newIndex }); this.render(); } - - static openCountdowns() { - new EncounterCountdowns().open(); - } } diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs new file mode 100644 index 00000000..2098fb10 --- /dev/null +++ b/module/applications/ui/countdownEdit.mjs @@ -0,0 +1,199 @@ +import { DhCountdown } from '../../data/countdowns.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super(); + + this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + this.editingCountdowns = new Set(); + this.currentEditCountdown = null; + this.hideNewCountdowns = false; + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dialog', 'dh-style', 'countdown-edit'], + tag: 'form', + position: { width: 600 }, + window: { + title: 'DAGGERHEART.APPLICATIONS.CountdownEdit.title', + icon: 'fa-solid fa-clock-rotate-left' + }, + actions: { + addCountdown: CountdownEdit.#addCountdown, + toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit, + editCountdownImage: CountdownEdit.#editCountdownImage, + editCountdownOwnership: CountdownEdit.#editCountdownOwnership, + removeCountdown: CountdownEdit.#removeCountdown + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + countdowns: { + template: 'systems/daggerheart/templates/ui/countdown-edit.hbs', + scrollable: ['.expanded-view', '.edit-content'] + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.isGM = game.user.isGM; + context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels; + context.defaultOwnership = this.data.defaultOwnership; + context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes; + context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes; + context.hideNewCountdowns = this.hideNewCountdowns; + context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => { + const countdown = this.data.countdowns[key]; + acc[key] = { + ...countdown, + typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].name), + progress: { + ...countdown.progress, + typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.progress.type].label) + }, + editing: this.editingCountdowns.has(key) + }; + + return acc; + }, {}); + + return context; + } + + /** @override */ + async _postRender(_context, _options) { + if (this.currentEditCountdown) { + setTimeout(() => { + const input = this.element.querySelector( + `.countdown-edit-container[data-id="${this.currentEditCountdown}"] input` + ); + if (input) { + input.select(); + this.currentEditCountdown = null; + } + }, 100); + } + } + + canPerformEdit() { + if (game.user.isGM) return true; + + if (!game.users.activeGM) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired')); + return false; + } + + return true; + } + + async updateSetting(update) { + const noGM = !game.users.find(x => x.isGM && x.active); + if (noGM) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired')); + return; + } + + await this.data.updateSource(update); + await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { + refreshType: RefreshType.Countdown + }); + + this.render(); + } + + static async updateData(_event, _, formData) { + const { hideNewCountdowns, ...settingsData } = foundry.utils.expandObject(formData.object); + + // Sync current and max if max is changing and they were equal before + for (const [id, countdown] of Object.entries(settingsData.countdowns ?? {})) { + const existing = this.data.countdowns[id]; + const wasEqual = existing && existing.progress.current === existing.progress.max; + if (wasEqual && countdown.progress.max !== existing.progress.max) { + countdown.progress.current = countdown.progress.max; + } else { + countdown.progress.current = Math.min(countdown.progress.current, countdown.progress.max); + } + } + + this.hideNewCountdowns = hideNewCountdowns; + this.updateSetting(settingsData); + } + + 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() { + const id = foundry.utils.randomID(); + this.editingCountdowns.add(id); + this.currentEditCountdown = id; + this.updateSetting({ + [`countdowns.${id}`]: DhCountdown.defaultCountdown(null, this.hideNewCountdowns) + }); + } + + static #editCountdownImage(_, target) { + const countdown = this.data.countdowns[target.id]; + const fp = new foundry.applications.apps.FilePicker.implementation({ + current: countdown.img, + type: 'image', + callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }), + top: this.position.top + 40, + left: this.position.left + 10 + }); + return fp.browse(); + } + + static #toggleCountdownEdit(_, button) { + const { countdownId } = button.dataset; + + const isEditing = this.editingCountdowns.has(countdownId); + if (isEditing) this.editingCountdowns.delete(countdownId); + else { + this.editingCountdowns.add(countdownId); + this.currentEditCountdown = countdownId; + } + + this.render(); + } + + static async #editCountdownOwnership(_, button) { + const countdown = this.data.countdowns[button.dataset.countdownId]; + const data = await game.system.api.applications.dialogs.OwnershipSelection.configure( + countdown.name, + countdown.ownership, + this.data.defaultOwnership + ); + if (!data) return; + + this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data }); + } + + static async #removeCountdown(event, button) { + const { countdownId } = button.dataset; + + if (!event.shiftKey) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle') + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', { + name: this.data.countdowns[countdownId].name + }) + }); + if (!confirmed) return; + } + + if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId); + this.updateSetting({ [`countdowns.-=${countdownId}`]: null }); + } +} diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 5e3ad1ab..07eac74b 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -1,355 +1,218 @@ -import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; -import constructHTMLButton from '../../helpers/utils.mjs'; -import OwnershipSelection from '../dialogs/ownershipSelection.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(basePath) { - super({}); +/** + * A UI element which displays the countdowns in this world. + * + * @extends ApplicationV2 + * @mixes HandlebarsApplication + */ - this.basePath = basePath; - } - - get title() { - return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', { - type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`) - }); +export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(options = {}) { + super(options); + + this.sidebarCollapsed = true; + this.setupHooks(); } + /** @inheritDoc */ static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'dh-style', 'countdown'], - tag: 'form', - position: { width: 740, height: 700 }, + id: 'countdowns', + tag: 'div', + classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'], window: { + icon: 'fa-solid fa-clock-rotate-left', frame: true, - title: 'Countdowns', - resizable: true, + title: 'DAGGERHEART.UI.Countdowns.title', + positioned: false, + resizable: false, minimizable: false }, actions: { - addCountdown: this.addCountdown, - removeCountdown: this.removeCountdown, - editImage: this.onEditImage, - openOwnership: this.openOwnership, - openCountdownOwnership: this.openCountdownOwnership, - toggleSimpleView: this.toggleSimpleView + toggleViewMode: DhCountdowns.#toggleViewMode, + decreaseCountdown: (_, target) => this.editCountdown(false, target), + increaseCountdown: (_, target) => this.editCountdown(true, target) }, - form: { handler: this.updateData, submitOnChange: true } - }; - - static PARTS = { - countdowns: { - template: 'systems/daggerheart/templates/ui/countdowns.hbs', - scrollable: ['.expanded-view'] + position: { + width: 400, + height: 222, + top: 50 } }; - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); + /** @override */ + static PARTS = { + resources: { + root: true, + template: 'systems/daggerheart/templates/ui/countdowns.hbs' + } + }; - htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { - element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false)); - element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true)); - }); - } - - async _preFirstRender(context, options) { - options.position = - 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); + get element() { + return document.body.querySelector('.daggerheart.dh-style.countdowns'); } + /**@inheritdoc */ async _renderFrame(options) { const frame = await super._renderFrame(options); - if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) { - const button = constructHTMLButton({ - label: '', - classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'], - dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' } - }); - this.window.controls.after(button); - } + const iconOnly = + game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === + CONFIG.DH.GENERAL.countdownAppMode.iconOnly; + if (iconOnly) frame.classList.add('icon-only'); + else frame.classList.remove('icon-only'); + + const header = frame.querySelector('.window-header'); + header.querySelector('button[data-action="close"]').remove(); + + const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode'); + const minimizeButton = ``; + header.insertAdjacentHTML('beforeEnd', minimizeButton); return frame; } - testUserPermission(level, exact, altSettings) { - if (game.user.isGM) return true; - - const settings = - altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; - const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level; - const userAllowed = exact - ? settings.playerOwnership[game.user.id]?.value === level - : settings.playerOwnership[game.user.id]?.value >= level; - return defaultAllowed || userAllowed; - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[ - this.basePath - ]; - + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); context.isGM = game.user.isGM; - context.base = this.basePath; + context.sidebarCollapsed = this.sidebarCollapsed; + context.iconOnly = + game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === + CONFIG.DH.GENERAL.countdownAppMode.iconOnly; - 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]; + const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + context.countdowns = Object.keys(setting.countdowns).reduce((acc, key) => { + const countdown = setting.countdowns[key]; + const ownership = DhCountdowns.#getPlayerOwnership(game.user, setting, countdown); + if (ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) return acc; - if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) { - acc[key] = { - ...countdown, - canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown) - }; + const playersWithAccess = game.users.reduce((acc, user) => { + const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown); + if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { + acc.push(user); } - return acc; - }, {}) - }; - context.systemFields = countdownData.schema.fields; - context.countdownFields = context.systemFields.countdowns.element.fields; - context.simple = this.simpleView; + }, []); + const nonGmPlayers = game.users.filter(x => !x.isGM); + acc[key] = { + ...countdown, + editable: game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, + playerAccess: playersWithAccess.length !== nonGmPlayers.length ? playersWithAccess : [], + noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0 + }; + return acc; + }, {}); return context; } - static async updateData(event, _, formData) { - const data = foundry.utils.expandObject(formData.object); - const newSetting = foundry.utils.mergeObject( - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(), - data - ); + static #getPlayerOwnership(user, setting, countdown) { + const playerOwnership = countdown.ownership[user.id]; + return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + ? setting.defaultOwnership + : playerOwnership; + } - if (game.user.isGM) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting); - 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: newSetting - } - }); + toggleCollapsedPosition = async (_, collapsed) => { + this.sidebarCollapsed = collapsed; + if (!collapsed) this.element.classList.add('expanded'); + else this.element.classList.remove('expanded'); + }; + + cooldownRefresh = ({ refreshType }) => { + if (refreshType === RefreshType.Countdown) this.render(); + }; + + static canPerformEdit() { + if (game.user.isGM) return true; + + const noGM = !game.users.find(x => x.isGM && x.active); + if (noGM) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired')); + return false; } + + return true; } - async updateSetting(update) { - if (game.user.isGM) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.Countdown, - application: `${this.basePath}-countdowns` - } - }); + static async #toggleViewMode() { + const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); + const appMode = CONFIG.DH.GENERAL.countdownAppMode; + const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon; + await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode); - 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) { - const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]; - const current = setting.countdowns[target.dataset.countdown].img; - 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) { - const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - await setting.updateSource({ - [`${this.basePath}.countdowns.${countdown}.img`]: path - }); - - 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); + if (newMode === appMode.iconOnly) this.element.classList.add('icon-only'); + else this.element.classList.remove('icon-only'); 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]; + static async editCountdown(increase, target) { + if (!DhCountdowns.canPerformEdit()) return; - 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 + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + const countdown = settings.countdowns[target.id]; + const newCurrent = increase + ? Math.min(countdown.progress.current + 1, countdown.progress.max) + : 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 }); - - await this.updateSetting(countdownSetting.toObject()); } - static async addCountdown() { + static 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 }); + } + + setupHooks() { + Hooks.on('collapseSidebar', this.toggleCollapsedPosition.bind()); + Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind()); + } + + close(options) { + Hooks.off('collapseSidebar', this.toggleCollapsedPosition); + Hooks.off(socketEvent.Refresh, this.cooldownRefresh); + super.close(options); + } + + static async updateCountdowns(progressType) { 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 } - } - } + const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => { + const countdown = countdownSetting.countdowns[key]; + if (countdown.progress.type === progressType && countdown.progress.current > 0) { + acc.push(key); } - }); - await this.updateSetting(countdownSetting.toObject()); - } + return acc; + }, []); - 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; + const countdownData = countdownSetting.toObject(); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, { + ...countdownData, + countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { + const countdown = foundry.utils.deepClone(countdownData.countdowns[key]); + if (updatedCountdowns.includes(key)) { + countdown.progress.current -= 1; } + acc[key] = countdown; 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); + const data = { refreshType: RefreshType.Countdown }; + await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data + }); + Hooks.callAll(socketEvent.Refresh, data); + } } diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index 32088bc1..12ab3ee1 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -23,5 +23,6 @@ export const compendiumBrowserLite = { export const itemAttachmentSource = 'attachmentSource'; export const userFlags = { - welcomeMessage: 'welcome-message' + welcomeMessage: 'welcome-message', + countdownMode: 'countdown-mode' }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 7afcbdea..900dcfbc 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -650,3 +650,30 @@ export const fearDisplay = { bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } }; + +export const basicOwnershiplevels = { + 0: { value: 0, label: 'OWNERSHIP.NONE' }, + 2: { value: 2, label: 'OWNERSHIP.OBSERVER' }, + 3: { value: 3, label: 'OWNERSHIP.OWNER' } +}; + +export const simpleOwnershiplevels = { + [-1]: { value: -1, label: 'OWNERSHIP.INHERIT' }, + ...basicOwnershiplevels +}; + +export const countdownBaseTypes = { + narrative: { + id: 'narrative', + name: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative' + }, + encounter: { + id: 'encounter', + name: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter' + } +}; + +export const countdownAppMode = { + textIcon: 'text-icon', + iconOnly: 'icon-only' +}; diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 62036c38..6db4cbeb 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -1,25 +1,28 @@ -import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; - export default class DhCountdowns extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; return { + /* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */ narrative: new fields.EmbeddedDataField(DhCountdownData), - encounter: new fields.EmbeddedDataField(DhCountdownData) + encounter: new fields.EmbeddedDataField(DhCountdownData), + /**/ + countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), + defaultOwnership: new fields.NumberField({ + required: true, + choices: CONFIG.DH.GENERAL.basicOwnershiplevels, + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER + }) }; } - - static CountdownCategories = { narrative: 'narrative', combat: 'combat' }; } +/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */ class DhCountdownData extends foundry.abstract.DataModel { - static LOCALIZATION_PREFIXES = ['DAGGERHEART.APPLICATIONS.Countdown']; // Nots ure why this won't work. Setting labels manually for now - static defineSchema() { const fields = foundry.data.fields; return { - countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), + countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhOldCountdown)), ownership: new fields.SchemaField({ default: new fields.NumberField({ required: true, @@ -56,7 +59,8 @@ class DhCountdownData extends foundry.abstract.DataModel { } } -class DhCountdown extends foundry.abstract.DataModel { +/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */ +class DhOldCountdown extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; return { @@ -129,17 +133,88 @@ 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(); - } +export class DhCountdown extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + type: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.countdownBaseTypes, + label: 'DAGGERHEART.GENERAL.type' + }), + name: new fields.StringField({ + required: true, + label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label' + }), + img: new fields.FilePathField({ + categories: ['IMAGE'], + base64: false, + initial: 'icons/magic/time/hourglass-yellow-green.webp' + }), + ownership: new fields.TypedObjectField( + new fields.NumberField({ + required: true, + choices: CONFIG.DH.GENERAL.simpleOwnershiplevels, + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }) + ), + progress: new fields.SchemaField({ + current: new fields.NumberField({ + required: true, + integer: true, + initial: 1, + label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.current.label' + }), + max: new fields.NumberField({ + required: true, + integer: true, + initial: 1, + label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label' + }), + type: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.countdownTypes, + initial: CONFIG.DH.GENERAL.countdownTypes.custom.id, + label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.type.label' + }) + }) + }; + } - return false; - } - }); -}; + static defaultCountdown(type, playerHidden) { + const ownership = playerHidden + ? game.users.reduce((acc, user) => { + if (!user.isGM) { + acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE; + } + return acc; + }, {}) + : undefined; + + return { + type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id, + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'), + img: 'icons/magic/time/hourglass-yellow-green.webp', + ownership: ownership, + progress: { + current: 1, + max: 1 + } + }; + } + + get playerOwnership() { + return Array.from(game.users).reduce((acc, user) => { + acc[user.id] = { + value: user.isGM + ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER + : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 + ? this.ownership.players[user.id].type + : this.ownership.default, + isGM: user.isGM + }; + + return acc; + }, {}); + } +} diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index 015c19cb..e27fa3bd 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -97,6 +97,7 @@ export async function runMigrations() { } if (foundry.utils.isNewerVersion('1.2.0', lastMigrationVersion)) { + /* Migrate old action costs */ const lockedPacks = []; const compendiumItems = []; for (let pack of game.packs) { @@ -148,6 +149,36 @@ export async function runMigrations() { await pack.configure({ locked: true }); } + /* Migrate old countdown structure */ + const countdownSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + const getCountdowns = (data, type) => { + return Object.keys(data.countdowns).reduce((acc, key) => { + const countdown = data.countdowns[key]; + acc[key] = { + ...countdown, + type: type, + ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => { + acc[key] = countdown.ownership.players[key].type; + return acc; + }, {}), + progress: { + ...countdown.progress, + type: countdown.progress.type.value + } + }; + + return acc; + }, {}); + }; + + await countdownSettings.updateSource({ + countdowns: { + ...getCountdowns(countdownSettings.narrative, CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id), + ...getCountdowns(countdownSettings.encounter, CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id) + } + }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings); + lastMigrationVersion = '1.2.0'; } diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index f75c7b36..14b4cec1 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -25,6 +25,7 @@ export const GMUpdateEvent = { UpdateEffect: 'DhGMUpdateEffect', UpdateSetting: 'DhGMUpdateSetting', UpdateFear: 'DhGMUpdateFear', + UpdateCountdowns: 'DhGMUpdateCountdowns', UpdateSaveMessage: 'DhGMUpdateSaveMessage' }; @@ -60,6 +61,10 @@ export const registerSocketHooks = () => { ) ); 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: const action = await fromUuid(data.update.action), message = game.messages.get(data.update.message); @@ -84,14 +89,15 @@ export const registerUserQueries = () => { 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) { return await game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.GMUpdate, data: { action: eventName, uuid, - update + update, + refresh } }); } else return callback(update); diff --git a/styles/less/ui/countdown/countdown-edit.less b/styles/less/ui/countdown/countdown-edit.less new file mode 100644 index 00000000..1460c6ef --- /dev/null +++ b/styles/less/ui/countdown/countdown-edit.less @@ -0,0 +1,142 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.theme-light .daggerheart.application.dh-style.countdown-edit { + background-image: url('../assets/parchments/dh-parchment-light.png'); +} + +.daggerheart.application.dh-style.countdown-edit { + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + .edit-container { + display: flex; + flex-direction: column; + gap: 8px; + + h2 { + text-align: center; + color: light-dark(@dark, @golden); + } + + .header-tools { + display: grid; + grid-template-columns: 2fr 1fr 144px; + gap: 8px; + + .hide-tools { + white-space: nowrap; + flex-wrap: nowrap; + display: flex; + align-items: center; + + input { + position: relative; + top: 2px; + } + } + + .header-main-button { + height: 32px; + flex: 1; + } + + .default-ownership-tools { + display: flex; + align-items: center; + gap: 8px; + + select { + flex: 1; + background: light-dark(@beige, @dark-blue); + } + } + } + + .edit-content { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + overflow-x: hidden; + max-height: 500px; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + + .countdown-edit-container { + display: grid; + grid-template-columns: 48px 1fr 64px; + align-items: center; + gap: 8px; + + img { + width: 52px; + height: 52px; + border-radius: 6px; + } + + .countdown-edit-text { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + + .countdown-edit-subtext { + display: flex; + gap: 10px; + + .countdown-edit-sub-tag { + padding: 3px 5px; + font-size: var(--font-size-12); + font: @font-body; + + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; + } + } + } + + .countdown-edit-tools { + display: flex; + gap: 8px; + + &.same-row { + margin-top: 17.5px; + } + + a { + font-size: 16px; + } + } + } + + .countdown-edit-subrow { + display: flex; + gap: 16px; + margin: 0 72px 0 56px; + } + + .countdown-edit-input { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + + &.tiny { + flex: 0; + input { + min-width: 2.5rem; + } + } + + input, + select { + background: light-dark(@beige, @dark-blue); + color: light-dark(@dark, @beige); + } + } + } + } +} diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 5e46989a..9fa42ec7 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -1,60 +1,130 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; -.daggerheart.dh-style.countdown { - fieldset { - align-items: center; - margin-top: 5px; - border-radius: 6px; - border-color: light-dark(@dark-blue, @golden); +.theme-dark { + .daggerheart.dh-style.countdowns { + background-image: url(../assets/parchments/dh-parchment-dark.png); - legend { - font-weight: bold; - color: light-dark(@dark-blue, @golden); - - a { - text-shadow: none; - } + .window-header { + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + } +} + +.daggerheart.dh-style.countdowns { + z-index: var(--z-index-ui) !important; + border: 0; + border-radius: 4px; + box-shadow: none; + width: 300px; + top: 16px; + right: 64px; + transition: + right ease 250ms, + opacity var(--ui-fade-duration) ease, + opacity var(--ui-fade-duration); + + .window-title { + font-family: @font-body; + } + + &.expanded { + right: 364px; + } + + &.icon-only { + width: 180px; + min-width: 180px; + } + + .window-header { + cursor: default; + border-bottom: 0; + } + + .window-content { + padding-top: 4px; + padding-bottom: 16px; + overflow: auto; + max-height: 312px; + + .countdowns-container { + display: flex; + flex-direction: column; + gap: 8px; + + .countdown-container { + display: flex; + justify-content: space-between; + + &.icon-only { + gap: 8px; + + .countdown-main-container { + .countdown-content { + justify-content: center; + + .countdown-tools { + gap: 8px; + } + } + } + } + + .countdown-main-container { + display: flex; + align-items: center; + gap: 16px; + + img { + width: 44px; + height: 44px; + border-radius: 6px; + } + + .countdown-content { + 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); + } + } + } + } + + .countdown-access-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: min-content; + width: 38px; + gap: 4px; + + .countdown-access { + height: 10px; + width: 10px; + border-radius: 50%; + border: 1px solid light-dark(@dark-blue, @beige-80); + content: ''; + } + } + + .countdown-no-access-container { + width: 38px; + display: flex; + align-items: center; + justify-content: center; + } + } } } - - .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; - } } diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 8b0c53f6..0a89afc3 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -13,6 +13,7 @@ @import './item-browser/item-browser.less'; @import './countdown/countdown.less'; +@import './countdown/countdown-edit.less'; @import './countdown/sheet.less'; @import './ownership-selection/ownership-selection.less'; diff --git a/styles/less/ui/ownership-selection/ownership-selection.less b/styles/less/ui/ownership-selection/ownership-selection.less index 56fddd4f..76ae0930 100644 --- a/styles/less/ui/ownership-selection/ownership-selection.less +++ b/styles/less/ui/ownership-selection/ownership-selection.less @@ -6,9 +6,15 @@ flex-direction: column; gap: 8px; + .ownership-list { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 10px; + } + .ownership-container { display: flex; - border: 2px solid light-dark(@dark-blue, @golden); border-radius: 6px; padding: 0 4px 0 0; align-items: center; @@ -17,12 +23,24 @@ img { height: 40px; width: 40px; - border-radius: 6px 0 0 6px; + border-radius: 50%; + } + + span { + flex: 3; } select { + flex: 1; margin: 4px 0; } } + + footer { + margin-top: 10px; + button { + height: 32px; + } + } } } diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index e975954c..80eda9a1 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -1,5 +1,17 @@ .tab.sidebar-tab.daggerheartMenu-sidebar { - padding: 0 4px; + padding: 4px; + + div[data-application-part] { + display: flex; + flex-direction: column; + gap: 8px; + } + + h2 { + margin-top: 8px; + text-align: center; + font-weight: bold; + } .menu-refresh-container { display: flex; diff --git a/templates/dialogs/ownershipSelection.hbs b/templates/dialogs/ownershipSelection.hbs index 43711c07..b16e5d75 100644 --- a/templates/dialogs/ownershipSelection.hbs +++ b/templates/dialogs/ownershipSelection.hbs @@ -2,20 +2,22 @@
- + {{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true }}
- {{#each ownership.players as |player id|}} -
- -
{{player.name}}
- -
- {{/each}} +
    + {{#each ownership as |player id|}} +
  • + + {{player.name}} + +
  • + {{/each}} +
diff --git a/templates/levelup/tabs/viewMode.hbs b/templates/levelup/tabs/viewMode.hbs index 12e7cbcd..b41623d7 100644 --- a/templates/levelup/tabs/viewMode.hbs +++ b/templates/levelup/tabs/viewMode.hbs @@ -2,7 +2,7 @@
{{#each this.tiers as |tier key|}}
- {{tier.name}} + {{localize tier.name}} {{#each tier.groups}}
diff --git a/templates/sidebar/daggerheart-menu/main.hbs b/templates/sidebar/daggerheart-menu/main.hbs index b00001eb..137f2e3d 100644 --- a/templates/sidebar/daggerheart-menu/main.hbs +++ b/templates/sidebar/daggerheart-menu/main.hbs @@ -1,4 +1,6 @@
+

{{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.title"}}

+
{{localize "Refresh Features"}} @@ -19,4 +21,6 @@
+ +
diff --git a/templates/ui/combatTracker/combatTrackerHeader.hbs b/templates/ui/combatTracker/combatTrackerHeader.hbs index 9ecff9e6..fe7d33a0 100644 --- a/templates/ui/combatTracker/combatTrackerHeader.hbs +++ b/templates/ui/combatTracker/combatTrackerHeader.hbs @@ -67,7 +67,6 @@ {{!-- Combat Controls --}}
{{#if hasCombat}} -
+
+ + +
+ {{#if isGM}} +
+ + +
+ {{/if}} +
+ +
+ {{#each countdowns as | countdown id | }} +
+ + {{#unless countdown.editing}} +
+

{{countdown.name}}

+
+
{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownValue" value=countdown.progress.current}}
+
{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownMax" value=countdown.progress.max}}
+
{{countdown.typeName}}
+
{{countdown.progress.typeName}}
+
+
+ {{else}} +
+ + +
+ {{/unless}} +
+ + + +
+
+ {{#if countdown.editing}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ {{/if}} + {{/each}} +
+
+ \ No newline at end of file diff --git a/templates/ui/countdowns.hbs b/templates/ui/countdowns.hbs index 11a112ae..122b36e6 100644 --- a/templates/ui/countdowns.hbs +++ b/templates/ui/countdowns.hbs @@ -1,45 +1,33 @@
- {{#if simple}} - - {{else}} -
-
- {{#if canCreate}}{{/if}} - {{#if isGM}}{{/if}} -
- -
- {{#each source.countdowns}} -
- - {{this.name}} - {{#if this.canEdit}}{{/if}} - {{#if @root.isGM}}{{/if}} - - - -
- -
- {{formGroup @root.countdownFields.name name=(concat @root.base ".countdowns." @key ".name") value=this.name localize=true disabled=(not this.canEdit)}} -
- {{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)}} -
- {{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)}} +
+ {{#each countdowns as | countdown id |}} +
+
+ +
+ {{#unless ../iconOnly}}{{/unless}} +
+ {{#if countdown.editable}}{{/if}} +
+ {{countdown.progress.current}}/{{countdown.progress.max}}
+ {{#if countdown.editable}}{{/if}}
-
- {{/each}} +
+
+ {{#if (and @root.isGM (not ../iconOnly))}} + {{#if (gt countdown.playerAccess.length 0)}} +
+ {{#each countdown.playerAccess as |player|}} +
+ {{/each}} +
+ {{/if}} + {{#if countdown.noPlayerAccess}} +
+ {{/if}} + {{/if}}
- - {{/if}} + {{/each}} + \ No newline at end of file