From 6f040532aa6ea646143ce6ae2b70cff1c1b48ad7 Mon Sep 17 00:00:00 2001 From: Murilo Brito Date: Tue, 21 Jul 2026 11:45:29 -0300 Subject: [PATCH] rework fear tracker application, add static positions to hold the tracker, enhance token and bar display, create custom dragger and resize methods --- lang/en.json | 10 + module/applications/ui/chatLog.mjs | 6 + module/applications/ui/fearTracker.mjs | 215 ++++++++++++- module/config/generalConfig.mjs | 8 + module/data/settings/Appearance.mjs | 5 + styles/less/ui/countdown/countdown.less | 3 +- styles/less/ui/resources/resources.less | 284 ++++++++++++------ .../settings/appearance-settings/main.hbs | 4 + templates/ui/fearTracker.hbs | 46 ++- 9 files changed, 469 insertions(+), 112 deletions(-) diff --git a/lang/en.json b/lang/en.json index 508a771d..0791008f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2687,6 +2687,9 @@ "displayFear": { "label": "Display Fear" }, + "fearPosition": { + "label": "Fear Position" + }, "displayCountdownUI": { "label": "Display Countdown UI" }, @@ -2738,6 +2741,13 @@ "token": "Tokens", "bar": "Bar", "hide": "Hide" + }, + "fearPosition": { + "free": "Free", + "topCenter": "Top + Center", + "bottomCenter": "Bottom + Center", + "rightTop": "Right + Top", + "leftBottom": "Left + Bottom" } }, "Automation": { diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 199ee87d..1145a55c 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,6 +1,7 @@ import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs'; import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs'; +import FearTracker from './fearTracker.mjs'; export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { @@ -297,4 +298,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const actor = game.actors.get(event.target.dataset.actorId); new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); } + + _toggleNotifications({ closing = false } = {}) { + super._toggleNotifications(closing) + FearTracker.handleOffSet(); + } } diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index 8c247f79..cfdf05a5 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -13,6 +13,14 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV2) { constructor(options = {}) { super(options); + + this._dragData = { + isDragging: false, + startX: 0, + startY: 0, + startLeft: 0, + startTop: 0 + } } /** @inheritDoc */ @@ -21,19 +29,20 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV classes: [], tag: 'div', window: { - frame: true, + 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: 222, - height: 222 + width: 540, + height: 'auto' } }; @@ -53,6 +62,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV 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 */ /* -------------------------------------------- */ @@ -63,15 +76,51 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV current = this.currentFear, max = this.maxFear, percent = (current / max) * 100, - isGM = game.user.isGM; + isGM = game.user.isGM, + locked = false, + isFree = this.fearPosition == 'free'; - return { display, current, max, percent, isGM }; + return { display, current, max, percent, isGM, locked, isFree }; } /** @override */ - async _preFirstRender(context, options) { - options.position = - game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position; + 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 */ @@ -80,13 +129,39 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); } - _onPosition(position) { - game.user.setFlag(CONFIG.DH.id, 'app.resources.position', position); + 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]); } - async close(options = {}) { - if (!options.allowed) return; - else super.close(options); + 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) { @@ -110,4 +185,118 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV 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 }); + } } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 188efafb..9dff6331 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -924,6 +924,14 @@ export const fearDisplay = { hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } }; +export const fearPosition = { + free: { value: 'free', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.free' }, + topCenter: { value: 'topCenter', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.topCenter' }, + bottomCenter: { value: 'bottomCenter', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.bottomCenter' }, + rightTop: { value: 'rightTop', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.rightTop' }, + leftBottom: { value: 'leftBottom', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.leftBottom' } +}; + export const basicOwnershiplevels = { 0: { value: 0, label: 'OWNERSHIP.NONE' }, 2: { value: 2, label: 'OWNERSHIP.OBSERVER' }, diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index 4db27be0..d9fdd3d9 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -41,6 +41,11 @@ export default class DhAppearance extends foundry.abstract.DataModel { choices: CONFIG.DH.GENERAL.fearDisplay, initial: CONFIG.DH.GENERAL.fearDisplay.token.value }), + fearPosition: new StringField({ + required: true, + choices: CONFIG.DH.GENERAL.fearPosition, + initial: CONFIG.DH.GENERAL.fearPosition.free.value + }), displayCountdownUI: new BooleanField({ initial: true }), diceSoNice: new SchemaField({ hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }), diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 96e01ffd..4e64d50a 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -32,7 +32,7 @@ background: var(--background); border-radius: 4px; opacity: var(--ui-fade-opacity); - transition: opacity var(--ui-fade-duration); + transition: opacity var(--ui-fade-delay) var(--ui-fade-duration); } &:not(.performance-low, .noblur) { @@ -41,6 +41,7 @@ &:hover::before { opacity: 1; + transition: opacity var(--ui-fade-duration); } #ui-right:has(#effects-display .effect-container) & { diff --git a/styles/less/ui/resources/resources.less b/styles/less/ui/resources/resources.less index 3982d990..708769fd 100644 --- a/styles/less/ui/resources/resources.less +++ b/styles/less/ui/resources/resources.less @@ -1,119 +1,221 @@ :root { - --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; - --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; + --hotbar-size: 60px; +} + +#interface.theme-dark, +body.theme-dark { + .daggerheart.dh-style.fear-tracker { + --background: url(../assets/parchments/dh-parchment-dark.png); + } +} + +#interface.theme-light, +body.theme-light { + .daggerheart.dh-style.fear-tracker { + --background: url('../assets/parchments/dh-parchment-light.png') no-repeat center; + } +} + +#ui-middle:has(#hotbar.sm), +#ui-middle:has(#hotbar.md.offset) { + #resources { + &.top-center, + &.bottom-center { + width: calc((var(--hotbar-size) * 5) + 32px) !important; + } + } } #resources { + position: static; min-height: calc(var(--header-height) + 4rem); min-width: 4rem; color: #d3d3d3; - transition: var(--fear-animation); - header, - .controls, - .window-resize-handle { - transition: var(--fear-animation); + pointer-events: all; + padding: var(--spacer-8); + max-width: 540px; + min-width: 100px; + + &::before { + content: ' '; + position: absolute; + inset: 0; + background: var(--background); + border-radius: 8px; + opacity: var(--ui-fade-opacity); + transition: opacity var(--ui-fade-delay) var(--ui-fade-duration); + } - .window-content { - padding: 0.5rem; - #resource-fear { + + &:hover::before { + opacity: 1; + transition: opacity var(--ui-fade-duration); + } + + &.free { + position: absolute; + } + + &.top-center, + &.bottom-center { + margin: 1rem 0; + width: 100% !important; + transform: translateX(var(--offset)); + transition: all 250ms ease; + } + + &.right-top { + width: 300px; + max-width: 300px; + } + + &.left-bottom { + width: 200px; + max-width: 200px; + background: transparent; + } + + &:not(.performance-low, .noblur) { + backdrop-filter: blur(5px); + } + + #ui-right:has(#effects-display .effect-container) & { + right: 62px; + } + + #resource-fear { + position: relative; + + &:hover { + .fear-header { + opacity: 1; + height: 18.75px; + visibility: visible; + } + + .resize-handle { + opacity: 1; + } + } + + .fear-header { + display: flex; + gap: 5px; + pointer-events: all; + margin-bottom: 0.5rem; + opacity: 0; + height: 0; + visibility: hidden; + transition: all 0.3s ease; + + .drag-handle { + cursor: grab; + } + + .fear-title { + font-size: var(--font-size-13); + } + } + + .fear-tokens { display: flex; flex-direction: row; - gap: 0.5rem 0.25rem; flex-wrap: wrap; - i { - font-size: var(--font-size-18); - border: 1px solid rgba(0, 0, 0, 0.5); + justify-content: center; + gap: 0.5rem 0.25rem; + + .fear-token { + font-size: var(--font-size-16); + border: 1.5px double light-dark(@dark-15, @dark-golden-80); border-radius: 50%; aspect-ratio: 1; display: flex; justify-content: center; align-items: center; - width: 3rem; + width: 2.5rem; background-color: @primary-color-fear; - -webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); - box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75); - color: #d3d3d3; + color: @beige; flex-grow: 0; + text-shadow: none; + &.inactive { filter: grayscale(1) !important; opacity: 0.5; } } - .controls, - .resource-bar { - border: 2px solid rgb(153 122 79); - background-color: rgb(24 22 46); - } - .controls { - display: flex; - align-self: center; - border-radius: 50%; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - font-size: var(--font-size-20); - cursor: pointer; - &:hover { - font-size: 1.5rem; - } - &.disabled { - opacity: 0.5; - } - } - .resource-bar { - display: flex; - justify-content: center; - border-radius: 6px; - font-size: var(--font-size-20); - overflow: hidden; - position: relative; - padding: 0.25rem 0.5rem; - flex: 1; - text-shadow: var(--shadow-text-stroke); - &:before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: var(--fear-percent); - max-width: 100%; - background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%); - z-index: 0; - border-radius: 4px; - } - span { - position: inherit; - z-index: 1; - } - &.fear { - } - } - &.isGM { - i { - cursor: pointer; - &:hover { - font-size: var(--font-size-20); - } - } - } } - } - button[data-action='close'] { - display: none; - } - &:not(:hover):not(.minimized) { - background: transparent; - box-shadow: unset; - border-color: transparent; - header, - #resource-fear .controls, - .window-resize-handle { + + .resize-handle { + position: absolute; + bottom: -12px; + right: -9px; + cursor: nwse-resize; opacity: 0; + transition: all 0.3s ease; + } + + + .resource-bar { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + height: 30px; + width: 100%; + + .progress-bar { + position: absolute; + appearance: none; + width: 100%; + height: 100%; + border: 1px solid light-dark(@dark-15, @dark-golden-80); + border-radius: 999px; + z-index: 0; + background: @dark-blue; + + &::-webkit-progress-bar { + border: none; + background: @dark-blue; + border-radius: 999px; + } + &::-webkit-progress-value { + background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%); + border-radius: 999px; + } + } + + .label { + margin: auto 0; + z-index: 2; + height: auto; + text-align: center; + font-size: var(--font-size-18); + color: @beige; + } + } + + .controls { + display: flex; + gap: 8px; + justify-content: center; + align-items: center; + margin-top: 0.5rem; + color: light-dark(@dark, @beige); + + .disabled { + opacity: 0.5; + } + } + + &.isGM { + .fear-token { + cursor: pointer; + transition: box-shadow 0.15s ease; + + &:hover { + box-shadow: 0 0 8px @primary-color-fear ; + } + } } - } - &:has(.fear-bar) { - min-width: 200px; } } diff --git a/templates/settings/appearance-settings/main.hbs b/templates/settings/appearance-settings/main.hbs index 32dd9e63..324ea653 100644 --- a/templates/settings/appearance-settings/main.hbs +++ b/templates/settings/appearance-settings/main.hbs @@ -8,6 +8,10 @@ value=setting.displayFear localize=true}} {{formGroup + fields.fearPosition + value=setting.fearPosition + localize=true}} + {{formGroup fields.displayCountdownUI value=setting.displayCountdownUI localize=true}} diff --git a/templates/ui/fearTracker.hbs b/templates/ui/fearTracker.hbs index 6832ab90..deae762a 100644 --- a/templates/ui/fearTracker.hbs +++ b/templates/ui/fearTracker.hbs @@ -1,16 +1,48 @@
-
+
+ {{#if isFree}} +
+
+ +
+ {{localize 'DAGGERHEART.GENERAL.fear'}} +
+ {{/if}} + {{#if (eq display 'token')}} - {{#times max}} - - {{/times}} +
+ {{#times max}} + + + + {{/times}} +
{{/if}} {{#if (eq display 'bar')}} - {{#if isGM}}
-
{{/if}}
- {{current}}/{{max}} + +

{{current}} / {{max}}

- {{#if isGM}}
+
{{/if}} + {{#if isGM}} +
+ + +
+ {{/if}} + {{/if}} + + {{#if isFree}} + + + {{/if}}
\ No newline at end of file