import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; /** * A UI element which displays the Users defined for this world. * Currently active users are always displayed, while inactive users can be displayed on toggle. * * @extends ApplicationV2 * @mixes HandlebarsApplication */ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV2) { constructor(options = {}) { super(options); this._dragData = { isDragging: false, startX: 0, startY: 0, startLeft: 0, startTop: 0 } } /** @inheritDoc */ static DEFAULT_OPTIONS = { id: 'resources', classes: [], tag: 'div', window: { frame: false, title: 'DAGGERHEART.GENERAL.fear', positioned: true, resizable: true, minimizable: false }, classes: ['daggerheart', 'dh-style', 'fear-tracker'], actions: { setFear: FearTracker.setFear, increaseFear: FearTracker.increaseFear }, position: { width: 540, height: 'auto' } }; /** @override */ static PARTS = { resources: { root: true, template: 'systems/daggerheart/templates/ui/fearTracker.hbs' } }; get currentFear() { return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear); } get maxFear() { return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear; } get fearPosition() { return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition; } /* -------------------------------------------- */ /* Rendering */ /* -------------------------------------------- */ /** @override */ async _prepareContext(_options) { const display = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).displayFear, current = this.currentFear, max = this.maxFear, percent = (current / max) * 100, isGM = game.user.isGM, locked = false, isFree = this.fearPosition == 'free'; return { display, current, max, percent, isGM, locked, isFree }; } /** @override */ async _onRender(context, options) { await super._onRender(context, options); this.#setupDragging(); this.#setupResizing(); const fearPosition = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition; if (options.isFirstRender) FearTracker.handleOffSet(); if (!options.force) return; switch (fearPosition) { case 'topCenter': this.handleStyleElement(fearPosition); document.getElementById('ui-top')?.appendChild(this.element); break; case 'bottomCenter': this.handleStyleElement(fearPosition); document.getElementById('ui-bottom')?.prepend(this.element); break; case 'rightTop': this.handleStyleElement(fearPosition); document.getElementById('ui-right-column-1')?.appendChild(this.element); break; case 'leftBottom': this.handleStyleElement(fearPosition); document.getElementById('ui-left-column-1')?.appendChild(this.element); break; default: this.handleStyleElement(fearPosition); document.body?.appendChild(this.element); const position = game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position; this.setPosition(position); break; } } /** @override */ async _preRender(context, options) { if (this.currentFear > this.maxFear && game.user.isGM) await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); } handleStyleElement(fearPosition) { const positions = { free: 'free', topCenter: 'top-center', bottomCenter: 'bottom-center', rightTop: 'right-top', leftBottom: 'left-bottom' } const containsStyle = position => this.element.classList.contains(position); Object.entries(positions).forEach(([key, value]) => { if (containsStyle(value)) this.element.classList.remove(value); }) this.element.classList.add(positions[fearPosition]); } static handleOffSet() { const fearTracker = document.getElementById('resources'); const hotbar = document.getElementById('hotbar'); if (!fearTracker) return; const offset = Number(hotbar.style.getPropertyValue('--offset').replace(/px$/, '')) || 0; if (offset > 0) return; fearTracker.style.setProperty('--offset', `${offset - 13}px`); } _onPosition(position) { game.user.setFlag(CONFIG.DH.id, 'app.resources.position', position); } static async setFear(event, target) { if (!game.user.isGM) return; const fearCount = Number(target.dataset.index ?? 0); await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1); } static async increaseFear(event, target) { if (!game.user.isGM) return; let value = target.dataset.increment ?? 0, operator = value.split('')[0] ?? null; value = Number(value); await this.updateFear(operator ? this.currentFear + value : value); } async updateFear(value) { return emitGMUpdate( GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value ); } /* -------------------------------------------- */ /* Dragging handlers */ /* -------------------------------------------- */ #setupDragging() { const dragHandle = this.element.querySelector('.drag-handle'); if (!dragHandle) return; dragHandle.addEventListener('mousedown', this.#onDragStart.bind(this)); } #onDragStart(event) { if (event.button !== 0) return; this._dragData.isDragging = true; this._dragData.startX = event.clientX; this._dragData.startY = event.clientY; const rect = this.element.getBoundingClientRect(); this._dragData.startLeft = rect.left; this._dragData.startTop = rect.top; this.element.style.cursor = 'grabbing'; this._dragHandler = this.#onDragging.bind(this); this._dragEndHandler = this.#onDragEnd.bind(this); window.addEventListener('mousemove', this._dragHandler); window.addEventListener('mouseup', this._dragEndHandler); } #onDragging(event) { if (!this._dragData.isDragging) return; const dragX = event.clientX - this._dragData.startX; const dragY = event.clientY - this._dragData.startY; this.element.style.left = `${this._dragData.startLeft + dragX}px`; this.element.style.top = `${this._dragData.startTop + dragY}px`; } #onDragEnd() { if (!this._dragData.isDragging) return; this._dragData.isDragging = false; this.element.style.cursor = ''; if (this._dragHandler) window.removeEventListener('mousemove', this._dragHandler); if (this._dragEndHandler) window.removeEventListener('mouseup', this._dragEndHandler); const rect = this.element.getBoundingClientRect(); const pos = { top: rect.top, left: rect.left }; this.setPosition(pos); } /* -------------------------------------------- */ /* Resize handlers */ /* -------------------------------------------- */ #setupResizing() { const resizeHandle = this.element.querySelector('.resize-handle'); if (!resizeHandle) return; resizeHandle.addEventListener('mousedown', this.#onResizeStart.bind(this)); } #onResizeStart(e) { if (e.button !== 0) return; e.stopPropagation(); let maxAllowedWidth = 10000; this._resizeData = { isResizing: true, startX: e.clientX, startY: e.clientY, startWidth: this.element.offsetWidth, startHeight: this.element.offsetHeight, maxAllowedWidth: Math.max(50, maxAllowedWidth) }; this._resizeHandler = this.#onResizing.bind(this); this._resizeEndHandler = this.#onResizeEnd.bind(this); window.addEventListener('mousemove', this._resizeHandler); window.addEventListener('mouseup', this._resizeEndHandler); } #onResizing(e) { if (!this._resizeData?.isResizing) return; const currentDx = e.clientX - this._resizeData.startX; const potentialWidth = Math.max(50, this._resizeData.startWidth + currentDx); const width = Math.min(potentialWidth, this._resizeData.maxAllowedWidth); this.element.style.width = `${width}px`; if (width < 100) { this.element.classList.add('narrow'); } else { this.element.classList.remove('narrow'); } } #onResizeEnd() { if (!this._resizeData?.isResizing) return; this._resizeData.isResizing = false; if (this._resizeHandler) window.removeEventListener('mousemove', this._resizeHandler); if (this._resizeEndHandler) window.removeEventListener('mouseup', this._resizeEndHandler); let width = parseFloat(this.element.style.width); if (isNaN(width)) { width = this.element.getBoundingClientRect().width; } this.setPosition({ width: width }); } }