From c25baa255432e177fff299eb55bcb37e76e7a6b7 Mon Sep 17 00:00:00 2001 From: CPTN Cosmo Date: Mon, 22 Dec 2025 03:04:15 +0100 Subject: [PATCH] feat: Add new Fear Tracker application with customizable UI, drag/resize, and settings integration. --- module.json | 4 +- scripts/.module.js.kate-swp | Bin 0 -> 76 bytes scripts/fear-tracker-app.js | 541 +++++++++++++++++++++++++++++ scripts/module.js | 663 +++++++++++------------------------- styles/module.css | 387 ++++++++++++++++++--- templates/fear-tracker.hbs | 66 ++++ 6 files changed, 1157 insertions(+), 504 deletions(-) create mode 100644 scripts/.module.js.kate-swp create mode 100644 scripts/fear-tracker-app.js create mode 100644 templates/fear-tracker.hbs diff --git a/module.json b/module.json index b7a4f66..c8c3bc7 100644 --- a/module.json +++ b/module.json @@ -1,7 +1,7 @@ { "id": "dh-feartrackerplus", "title": "Daggerheart Fear Tracker Plus", - "version": "1.3.0", + "version": "2.0.0", "compatibility": { "minimum": "13", "verified": "13" @@ -32,6 +32,6 @@ ], "url": "https://github.com/cptn-cosmo/dh-feartrackerplus", "manifest": "https://github.com/cptn-cosmo/dh-feartrackerplus/releases/latest/download/module.json", - "download": "https://github.com/cptn-cosmo/dh-feartrackerplus/releases/download/1.3.0/dh-feartrackerplus.zip", + "download": "https://github.com/cptn-cosmo/dh-feartrackerplus/releases/download/2.0.0/dh-feartrackerplus.zip", "description": "Customizes the Fear Tracker for Daggerheart." } \ No newline at end of file diff --git a/scripts/.module.js.kate-swp b/scripts/.module.js.kate-swp new file mode 100644 index 0000000000000000000000000000000000000000..8cc68b2f38cc91785ceafd60c900ff110ebb0252 GIT binary patch literal 76 zcmZQzU=Z?7EJ;-eE>A2_aLdd|RWQ;sU|?VnnPpwbDxnv0L&WZ}sduT9*CFoXYQaH3 QnHV7E2V!AY6s~|P0KX#-u>b%7 literal 0 HcmV?d00001 diff --git a/scripts/fear-tracker-app.js b/scripts/fear-tracker-app.js new file mode 100644 index 0000000..c9a1137 --- /dev/null +++ b/scripts/fear-tracker-app.js @@ -0,0 +1,541 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export class FearTrackerApp extends HandlebarsApplicationMixin(ApplicationV2) { + static instance; + + constructor(options = {}) { + super(options); + this._dragData = { + isDragging: false, + startX: 0, + startY: 0, + startLeft: 0, + startTop: 0 + }; + } + + static DEFAULT_OPTIONS = { + id: "dh-feartrackerplus-app", + tag: "div", + classes: ["dh-feartrackerplus-window"], + window: { + frame: false, + positioned: true, + resizable: true + }, + position: { + width: 300, + height: "auto", + }, + actions: { + increaseFear: FearTrackerApp.#onIncrease, + decreaseFear: FearTrackerApp.#onDecrease, + toggleViewMode: FearTrackerApp.#onToggleView, + toggleLock: FearTrackerApp.#onToggleLock + } + }; + + static PARTS = { + content: { + template: "modules/dh-feartrackerplus/templates/fear-tracker.hbs", + }, + }; + + static initialize() { + if (this.instance) return; + this.instance = new FearTrackerApp(); + const pos = game.settings.get("dh-feartrackerplus", "position") || { top: 100, left: 100 }; + const width = game.settings.get("dh-feartrackerplus", "width"); + if (width) pos.width = width; + this.instance.render({ force: true, position: pos }); + } + + _onRender(context, options) { + super._onRender(context, options); + + // Apply saved width + const savedWidth = game.settings.get("dh-feartrackerplus", "width"); + if (savedWidth) { + this.element.style.width = `${savedWidth}px`; + if (savedWidth < 100) { + this.element.classList.add('narrow'); + } else { + this.element.classList.remove('narrow'); + } + } + + // Apply Icon Size + const iconSize = game.settings.get("dh-feartrackerplus", "iconSize") || 32; + this.element.style.setProperty('--fear-token-size', `${iconSize}px`); + + // Sync locked class explicitly + const isLocked = game.settings.get("dh-feartrackerplus", "locked"); + if (isLocked) this.element.classList.add('locked'); + else this.element.classList.remove('locked'); + + this.#setupDragging(); + this.#setupResizing(); + + // Setup conditional hover for locked mode + this.element.addEventListener('mouseenter', (e) => { + const isLocked = game.settings.get("dh-feartrackerplus", "locked"); + if (isLocked) { + // Check if Ctrl is already held + if (e.ctrlKey) { + this.element.classList.add('force-expand'); + } + + // Listen for Ctrl key changes while hovering + this._hoverKeyHandler = (ke) => { + if (ke.key === 'Control') { + if (ke.type === 'keydown') this.element.classList.add('force-expand'); + if (ke.type === 'keyup') this.element.classList.remove('force-expand'); + } + }; + window.addEventListener('keydown', this._hoverKeyHandler); + window.addEventListener('keyup', this._hoverKeyHandler); + } + }); + + this.element.addEventListener('mouseleave', () => { + this.element.classList.remove('force-expand'); + if (this._hoverKeyHandler) { + window.removeEventListener('keydown', this._hoverKeyHandler); + window.removeEventListener('keyup', this._hoverKeyHandler); + this._hoverKeyHandler = null; + } + }); + this.#setupTokenInteractions(); + this.#animateMaxFear(); + + // Context Menu for Unlocking (Right-click) + // Use namespaced class and provide empty options to avoid system error + new foundry.applications.ux.ContextMenu(this.element, ".fear-tracker-window", [ + { + name: "Unlock Position", + icon: '', + condition: () => game.settings.get("dh-feartrackerplus", "locked"), + callback: async () => { + await game.settings.set("dh-feartrackerplus", "locked", false); + this.render(); + } + }, + { + name: "Lock Position", + icon: '', + condition: () => !game.settings.get("dh-feartrackerplus", "locked"), + callback: async () => { + await game.settings.set("dh-feartrackerplus", "locked", true); + this.render(); + } + } + ], {}); + } + + updateFearData(data) { + // Store context from system render + this._systemData = data; + this.render(); + } + + async _prepareContext(options) { + const isGM = game.user.isGM; + const isMinimized = game.settings.get("dh-feartrackerplus", "minimized"); + const isLocked = game.settings.get("dh-feartrackerplus", "locked"); + + // Get Fear Data from System Settings + // Use "daggerheart" scope and specific keys found in system config + let currentFear = 0; + let maxFear = 6; + + try { + currentFear = game.settings.get("daggerheart", "ResourcesFear"); + const homebrew = game.settings.get("daggerheart", "Homebrew"); + if (homebrew?.maxFear) { + maxFear = homebrew.maxFear; + } + } catch (e) { + console.warn("FearTracker | Could not read system settings:", e); + } + + // Settings + const iconShape = game.settings.get("dh-feartrackerplus", "iconShape"); + const iconType = game.settings.get("dh-feartrackerplus", "iconType"); + const colorTheme = game.settings.get("dh-feartrackerplus", "colorTheme"); + const viewMode = game.settings.get("dh-feartrackerplus", "viewMode"); + const showFearNumber = game.settings.get("dh-feartrackerplus", "showFearNumber"); + const showControlButtons = game.settings.get("dh-feartrackerplus", "showControlButtons"); + const progressBarColor = game.settings.get("dh-feartrackerplus", "progressBarColor"); + + const useBar = viewMode === 'bar'; + let barStyle = ''; + + if (useBar) { + // Calculate Percentage + const pct = Math.min(100, Math.max(0, (currentFear / maxFear) * 100)); + + // Determine Color + let color = progressBarColor; + + // If theme is NOT custom, attempt to use theme colors. + if (colorTheme !== 'custom') { + const themes = { + 'hope-fear': { start: '#FFC107', end: '#512DA8' }, + 'blood-moon': { start: '#5c0000', end: '#ff0000' }, + 'ethereal': { start: '#00FFFF', end: '#0000FF' }, + 'toxic': { start: '#00FF00', end: '#FFFF00' }, + 'foundryborne': { start: '#0a388c', end: '#791d7d' } + }; + const t = themes[colorTheme]; + if (t) { + // Gradient bar for theme? + color = `linear-gradient(90deg, ${t.start}, ${t.end})`; + } + } + + barStyle = `width: ${pct}%; background: ${color};`; + } + + // Build Tokens (only if not bar) + const fearTokens = []; + if (!useBar) { + for (let i = 0; i < maxFear; i++) { + fearTokens.push({ + index: i, + active: i < currentFear, + icon: this.#getIconHtml(iconType, i), + style: this.#getTokenStyle(i, maxFear, colorTheme, i < currentFear) + }); + } + } + + return { + isGM, + isMinimized, + isLocked, + currentFear, + maxFear, + fearTokens, + iconShape, + showFearValue: !isMinimized && showFearNumber, // Respect setting + showControlButtons, + useBar, + barStyle + }; + } + + #getIconHtml(iconType, index) { + let iconClass = 'fas fa-skull'; + const presetIcon = game.settings.get("dh-feartrackerplus", "presetIcon"); + const customIcon = game.settings.get("dh-feartrackerplus", "customIcon"); + + if (iconType === 'preset') iconClass = presetIcon; + else if (iconType === 'custom') iconClass = customIcon; + else if (iconType === 'custom-svg') { + const svgPath = game.settings.get("dh-feartrackerplus", "customSvgPath"); + return ``; + } + + if (iconClass.includes('.svg')) { + return ``; + } + + const iconColor = game.settings.get("dh-feartrackerplus", "iconColor"); + let style = ""; + if (iconColor && iconColor !== '#ffffff') { + if (iconColor.includes('gradient')) { + style = `background: ${iconColor}; -webkit-background-clip: text; background-clip: text; color: transparent;`; + } else { + style = `color: ${iconColor};`; + } + } + + return ``; + } + + #getTokenStyle(index, total, theme, isActive) { + if (!isActive) return `background: #444;`; + let color = '#cf0000'; + + if (theme === 'custom') { + const fullColor = game.settings.get("dh-feartrackerplus", "fullColor"); + if (fullColor.includes('gradient')) { + const posPercent = total > 1 ? (index / (total - 1)) * 100 : 0; + return `background: ${fullColor} no-repeat; background-size: ${total * 100}% 100%; background-position: ${posPercent}% 0%;`; + } + color = fullColor; + } else { + const themes = { + 'foundryborne': { start: '#0a388c', end: '#791d7d' }, + 'hope-fear': { start: '#FFC107', end: '#512DA8' }, + 'blood-moon': { start: '#5c0000', end: '#ff0000' }, + 'ethereal': { start: '#00FFFF', end: '#0000FF' }, + 'toxic': { start: '#00FF00', end: '#FFFF00' } + }; + const t = themes[theme]; + if (t) { + const gradient = `linear-gradient(90deg, ${t.start}, ${t.end})`; + // Prevent wrapping artifacts with no-repeat. + // Handle single token case to avoid /0 NaN. + const posPercent = total > 1 ? (index / (total - 1)) * 100 : 0; + return `background: ${gradient} no-repeat; background-size: ${total * 100}% 100%; background-position: ${posPercent}% 0%;`; + } + } + + return `background: ${color};`; + } + + static async #onIncrease(event, target) { + if (!game.user.isGM) return; // Only GM can modify fear via this method for now + + try { + const currentFear = game.settings.get("daggerheart", "ResourcesFear"); + const homebrew = game.settings.get("daggerheart", "Homebrew"); + const maxFear = homebrew?.maxFear || 6; + + if (currentFear < maxFear) { + await game.settings.set("daggerheart", "ResourcesFear", currentFear + 1); + } + } catch (e) { + console.error("FearTracker | Error increasing fear:", e); + } + } + + static async #onDecrease(event, target) { + if (!game.user.isGM) return; + + try { + const currentFear = game.settings.get("daggerheart", "ResourcesFear"); + + if (currentFear > 0) { + await game.settings.set("daggerheart", "ResourcesFear", currentFear - 1); + } + } catch (e) { + console.error("FearTracker | Error decreasing fear:", e); + } + } + + static async #onToggleView(event, target) { + const current = game.settings.get("dh-feartrackerplus", "minimized"); + await game.settings.set("dh-feartrackerplus", "minimized", !current); + FearTrackerApp.instance?.render(); + } + + static async #onToggleLock(event, target) { + const current = game.settings.get("dh-feartrackerplus", "locked"); + await game.settings.set("dh-feartrackerplus", "locked", !current); + // Re-render handled by setting hook usually, but let's ensure + FearTrackerApp.instance?.render(); + } + + + + #setupTokenInteractions() { + if (!game.user.isGM) return; + const tokens = this.element.querySelectorAll('.fear-token'); + tokens.forEach(t => { + t.addEventListener('click', this.#onTokenClick.bind(this)); + t.style.cursor = 'pointer'; // Ensure visual cue + }); + } + + async #onTokenClick(e) { + e.preventDefault(); + if (!game.user.isGM) return; + + const token = e.currentTarget; + const index = parseInt(token.dataset.index); + + let currentActive = 0; + let maxFear = 6; + try { + currentActive = game.settings.get("daggerheart", "ResourcesFear"); + const homebrew = game.settings.get("daggerheart", "Homebrew"); + if (homebrew?.maxFear) { + maxFear = homebrew.maxFear; + } + } catch (e) { + console.error("FearTracker | Error reading settings in click handler:", e); + return; + } + + let targetValue = index + 1; + + // Toggle off if clicking the first one while it is the only one active + if (index === 0 && currentActive === 1) { + targetValue = 0; + } + + // Safety checks + if (targetValue < 0) targetValue = 0; + if (targetValue > maxFear) targetValue = maxFear; + + if (targetValue !== currentActive) { + await game.settings.set("daggerheart", "ResourcesFear", targetValue); + } + } + + #animateMaxFear() { + const animate = game.settings.get("dh-feartrackerplus", "maxFearAnimation"); + if (!animate) return; + + const fear = game.settings.get("daggerheart", "ResourcesFear"); + const homebrew = game.settings.get("daggerheart", "Homebrew"); + const maxFear = homebrew?.maxFear || 6; + + if (fear >= maxFear) { + this.element.classList.add('max-fear'); + this.element.querySelectorAll('.fear-token i, .fear-token img').forEach(icon => { + icon.classList.add('fear-pulse'); + }); + } else { + this.element.classList.remove('max-fear'); + } + } + + #setupDragging() { + if (game.settings.get("dh-feartrackerplus", "locked")) return; + const dragHandle = this.element.querySelector('.drag-handle'); + if (!dragHandle) return; + dragHandle.addEventListener('mousedown', this.#onDragStart.bind(this)); + } + + #onDragStart(e) { + if (e.button !== 0) return; + this._dragData.isDragging = true; + this._dragData.startX = e.clientX; + this._dragData.startY = e.clientY; + const rect = this.element.getBoundingClientRect(); + this._dragData.startLeft = rect.left; + this._dragData.startTop = rect.top; + this.element.style.cursor = 'grabbing'; + + // Bind to window to catch movements outside the element + this._dragHandler = this.#onDragging.bind(this); + this._dragEndHandler = this.#onDragEnd.bind(this); + window.addEventListener('mousemove', this._dragHandler); + window.addEventListener('mouseup', this._dragEndHandler); + } + + #onDragging(e) { + if (!this._dragData.isDragging) return; + const dx = e.clientX - this._dragData.startX; + const dy = e.clientY - this._dragData.startY; + this.element.style.left = `${this._dragData.startLeft + dx}px`; + this.element.style.top = `${this._dragData.startTop + dy}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 }; + game.settings.set("dh-feartrackerplus", "position", pos); + this.setPosition(pos); + } + + #setupResizing() { + if (game.settings.get("dh-feartrackerplus", "locked")) return; + 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(); // Prevent drag start + + let maxAllowedWidth = 10000; // Default high + const tokens = this.element.querySelectorAll('.fear-token'); + if (tokens.length > 0) { + // Calculate max width for single line of tokens + const iconSize = game.settings.get("dh-feartrackerplus", "iconSize") || 32; + const tokenWidth = iconSize; + const tokenGap = 6; + const count = tokens.length; + const idealTokensWidth = (count * tokenWidth) + (Math.max(0, count - 1) * tokenGap); + + // Siblings (Buttons) in fear-visuals + let siblingsWidth = 0; + const visuals = this.element.querySelector('.fear-visuals'); + if (visuals) { + // Count children that are NOT the tokens-container + let otherCount = 0; + Array.from(visuals.children).forEach(child => { + if (!child.classList.contains('tokens-container')) { + siblingsWidth += child.offsetWidth; + otherCount++; + } + }); + + // Add gaps between visuals children + // Total items = otherCount + 1 (tokens container) + const visualsGap = 8; + const totalVisualsGaps = otherCount * visualsGap; + + // Window Padding (8px * 2) + Border (1px * 2) + const chromeWidth = 18; + + maxAllowedWidth = idealTokensWidth + siblingsWidth + totalVisualsGaps + chromeWidth; + } + } + + this._resizeData = { + isResizing: true, + startX: e.clientX, + startY: e.clientY, + startWidth: this.element.offsetWidth, + startHeight: this.element.offsetHeight, + maxAllowedWidth: Math.max(50, maxAllowedWidth) // Lower min constraint + }; + + 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); // Lower min constraint + + // Apply constraint + const width = Math.min(potentialWidth, this._resizeData.maxAllowedWidth); + + this.element.style.width = `${width}px`; + + // Toggle narrow class + 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); + + // Save Width Setting + // Use inline style width to capture the "logical" width, ignoring hover expansion + let width = parseFloat(this.element.style.width); + + // Fallback if style.width is missing or invalid (shouldn't happen after resize) + if (isNaN(width)) { + width = this.element.getBoundingClientRect().width; + } + + game.settings.set("dh-feartrackerplus", "width", width); + this.setPosition({ width: width }); + } +} diff --git a/scripts/module.js b/scripts/module.js index 2a0909b..907322e 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -1,10 +1,12 @@ +import { FearTrackerApp } from './fear-tracker-app.js'; + const MODULE_ID = 'dh-feartrackerplus'; Hooks.once('init', () => { console.log(`${MODULE_ID} | Initializing Daggerheart Fear Tracker Plus`); const refreshFearTracker = () => { - if (ui.resources?.render) ui.resources.render({ force: true }); + FearTrackerApp.instance?.render(); }; // Register Settings @@ -67,10 +69,6 @@ Hooks.once('init', () => { onChange: refreshFearTracker }); - - - // Removed trackerLocked setting as requested - game.settings.register(MODULE_ID, 'colorTheme', { name: 'Color Theme', hint: 'Choose a color preset or Custom to set your own colors below.', @@ -95,11 +93,10 @@ Hooks.once('init', () => { scope: 'client', config: true, type: String, - default: '#000000', + default: '#ff0000', onChange: refreshFearTracker }); - // --- New Settings for Icon Color/Shape --- game.settings.register(MODULE_ID, 'iconShape', { name: 'Icon Shape', hint: 'Select the background shape for the icons.', @@ -125,30 +122,131 @@ Hooks.once('init', () => { onChange: refreshFearTracker }); - game.settings.register(MODULE_ID, 'trackerScale', { - name: 'Tracker Scale', - hint: 'Resize the fear tracker (0.25x to 2.0x).', + game.settings.register(MODULE_ID, 'iconSize', { + name: 'Icon Size (px)', + hint: 'Size of the fear tokens in pixels. (Default: 32)', scope: 'client', config: true, type: Number, - range: { - min: 0.25, - max: 2.0, - step: 0.05 - }, - default: 1.0, + range: { min: 20, max: 64, step: 2 }, + default: 32, onChange: refreshFearTracker }); - game.settings.register(MODULE_ID, 'maxFearAnimation', { - name: 'Max Fear Animation', - hint: 'animate the fear tracker when it reaches maximum value.', + game.settings.register(MODULE_ID, 'viewMode', { + name: 'View Mode', + hint: 'Choose between Icon view or Progress Bar view.', + scope: 'client', + config: true, + type: String, + choices: { + 'icons': 'Icons', + 'bar': 'Progress Bar' + }, + default: 'icons', + onChange: refreshFearTracker + }); + + game.settings.register(MODULE_ID, 'showFearNumber', { + name: 'Show Fear Number', + hint: 'Display the numerical value (e.g., 3 / 6) at the bottom.', scope: 'client', config: true, type: Boolean, default: true, onChange: refreshFearTracker }); + + game.settings.register(MODULE_ID, 'showControlButtons', { + name: 'Show Control Buttons', + hint: 'Display the +/- buttons for manually adjusting fear.', + scope: 'client', + config: true, + type: Boolean, + default: false, + onChange: refreshFearTracker + }); + + game.settings.register(MODULE_ID, 'progressBarColor', { + name: 'Progress Bar Color', + hint: 'CSS color or gradient for the progress bar (if View Mode is Bar). Overrides themes if set.', + scope: 'client', + config: true, + type: String, + default: '#cf0000', + onChange: refreshFearTracker + }); + + game.settings.register(MODULE_ID, 'maxFearAnimation', { + name: 'Max Fear Animation', + hint: 'Animate the fear tracker when it reaches maximum value.', + scope: 'client', + config: true, + type: Boolean, + default: true, + onChange: refreshFearTracker + }); + + // --- Window State Settings (Hidden) --- + game.settings.register(MODULE_ID, 'position', { + scope: 'client', + config: false, + type: Object, + default: { top: 100, left: 100 } + }); + + game.settings.register(MODULE_ID, 'width', { + scope: 'client', + config: false, + type: Number, + default: 300 + }); + + game.settings.register(MODULE_ID, 'minimized', { + scope: 'client', + config: false, + type: Boolean, + default: false + }); + + game.settings.register(MODULE_ID, 'locked', { + scope: 'client', + config: false, + type: Boolean, + default: false + }); +}); + +Hooks.once('ready', () => { + FearTrackerApp.initialize(); +}); + +// Watch for Fear changes (Settings update) +Hooks.on('updateSetting', (setting, updates, options, userId) => { + if (setting.key === 'daggerheart.Fear' || setting.key === 'daggerheart.Homebrew') { + FearTrackerApp.instance?.render(); + } +}); + +Hooks.on('renderFearTracker', (app, html, data) => { + // Capture fear data from the system's render hook + if (FearTrackerApp.instance) { + // Data likely contains the fear values. + // check if Dice So Nice is rolling + if (game.dice3d?.isRolling?.()) { + Hooks.once('diceSoNiceRollComplete', () => { + FearTrackerApp.instance.updateFearData(data); + }); + } else { + FearTrackerApp.instance.updateFearData(data); + } + + // Store reference to system app to potentially call methods directly + FearTrackerApp.systemApp = app; + } + + // Legacy injection disabled details + // injectFearCustomization(html); }); /** @@ -156,465 +254,110 @@ Hooks.once('init', () => { */ Hooks.on('renderSettingsConfig', (app, html, data) => { const $html = $(html); - // Use a more robust selector for the select elements, in case implicit binding changes const iconTypeSelect = $html.find(`select[name="${MODULE_ID}.iconType"]`); const themeSelect = $html.find(`select[name="${MODULE_ID}.colorTheme"]`); - // If we can't find the main selects, we can't do anything (likely custom settings window or other issue) if (!iconTypeSelect.length || !themeSelect.length) return; - { - // Helper to find setting group - const findGroup = (key) => { - // Try data-setting-id first (standard in V10/V11+) - let group = $html.find(`.form-group[data-setting-id="${MODULE_ID}.${key}"]`); - if (group.length) return group; + // Helper to find setting group + const findGroup = (key) => { + let group = $html.find(`.form-group[data-setting-id="${MODULE_ID}.${key}"]`); + if (group.length) return group; + const input = $html.find(`[name="${MODULE_ID}.${key}"]`); + if (input.length) return input.closest('.form-group'); + return null; + }; - // Fallback: Find input/select by name and go up to form-group - const input = $html.find(`[name="${MODULE_ID}.${key}"]`); - if (input.length) return input.closest('.form-group'); + const updateVisibility = () => { + // Safe check for elements, they might not exist if settings window structure changes + if (!iconTypeSelect.length) return; - return null; - }; + // Get Values + const iconType = iconTypeSelect.val(); + const theme = themeSelect.val(); + const viewMode = game.settings.get(MODULE_ID, 'viewMode'); // We might need to look up the input if it's not saved yet, but settings config usually reads saved. + // Actually, for live toggle in settings config, we need to find the viewMode select input. + const viewModeSelect = $html.find(`select[name="${MODULE_ID}.viewMode"]`); + const currentViewMode = viewModeSelect.length ? viewModeSelect.val() : 'icons'; - const updateVisibility = () => { - const iconType = iconTypeSelect.val(); - const theme = themeSelect.val(); + // Groups + const iconTypeGroup = findGroup('iconType'); + const presetGroup = findGroup('presetIcon'); + const customIconGroup = findGroup('customIcon'); + const customSvgGroup = findGroup('customSvgPath'); + const iconShapeGroup = findGroup('iconShape'); + const iconColorGroup = findGroup('iconColor'); + const fullColorGroup = findGroup('fullColor'); + const progressBarColorGroup = findGroup('progressBarColor'); - // Locate Groups - const presetGroup = findGroup('presetIcon'); - const customIconGroup = findGroup('customIcon'); - const customSvgGroup = findGroup('customSvgPath'); + // Logic + if (currentViewMode === 'bar') { + // Hide Icon Settings + if (iconTypeGroup) iconTypeGroup.hide(); + if (presetGroup) presetGroup.hide(); + if (customIconGroup) customIconGroup.hide(); + if (customSvgGroup) customSvgGroup.hide(); + if (iconShapeGroup) iconShapeGroup.hide(); + if (iconColorGroup) iconColorGroup.hide(); + if (fullColorGroup) fullColorGroup.hide(); - // Reset Visibility + // Show Bar Settings + if (progressBarColorGroup) progressBarColorGroup.show(); + + } else { + // ICONS MODE + if (iconTypeGroup) iconTypeGroup.show(); + if (iconShapeGroup) iconShapeGroup.show(); + if (iconColorGroup) iconColorGroup.show(); + if (progressBarColorGroup) progressBarColorGroup.hide(); + + // Icon Type Sub-settings if (presetGroup) presetGroup.hide(); if (customIconGroup) customIconGroup.hide(); if (customSvgGroup) customSvgGroup.hide(); - // Apply Logic - if (iconType === 'preset' && presetGroup) { - presetGroup.show(); - } else if (iconType === 'custom' && customIconGroup) { - customIconGroup.show(); - } else if (iconType === 'custom-svg' && customSvgGroup) { - customSvgGroup.show(); - } - - // Color Inputs - const fullColorGroup = findGroup('fullColor'); + if (iconType === 'preset' && presetGroup) presetGroup.show(); + else if (iconType === 'custom' && customIconGroup) customIconGroup.show(); + else if (iconType === 'custom-svg' && customSvgGroup) customSvgGroup.show(); + // Full Color (Custom Theme) if (fullColorGroup) { - if (theme === 'custom') { - fullColorGroup.show(); - } else { - fullColorGroup.hide(); + if (theme === 'custom') fullColorGroup.show(); + else fullColorGroup.hide(); + } + } + }; + + iconTypeSelect.on('change', updateVisibility); + themeSelect.on('change', updateVisibility); + + // Listen to View Mode change + const viewModeSelect = $html.find(`select[name="${MODULE_ID}.viewMode"]`); + if (viewModeSelect.length) { + viewModeSelect.on('change', updateVisibility); + } + + updateVisibility(); + + // Icon Preview Logic + const customIconGroup = findGroup('customIcon'); + if (customIconGroup) { + const input = customIconGroup.find(`input[name="${MODULE_ID}.customIcon"]`); + if (input.length) { + const previewSpan = $(``); + const icon = $(``); + previewSpan.append(icon); + input.after(previewSpan); + const updatePreview = () => { + const val = input.val(); + icon.attr('class', ''); + if (val) { + icon.addClass(val); } - } - - // Tracker Scale Reset Button - const scaleGroup = findGroup('trackerScale'); - if (scaleGroup && !scaleGroup.find('.scale-reset-btn').length) { - const input = scaleGroup.find('input[type="range"]'); // Try range first - const numberInput = scaleGroup.find(`input[name="${MODULE_ID}.trackerScale"]`); // Fallback/Number input - const rangeValue = scaleGroup.find('.range-value'); - - // We want to control the input that actually stores the value - const targetInput = numberInput.length ? numberInput : input; - - if (targetInput.length) { - const resetBtn = $(``); - - resetBtn.on('click', () => { - // Use Native DOM events for maximum compatibility with Foundry - // 1. Update Number Input (if exists) - if (numberInput.length) { - const nativeNum = numberInput[0]; - nativeNum.value = 1.0; - nativeNum.dispatchEvent(new Event('change', { bubbles: true })); - } - - // 2. Update Range Input - if (input.length) { - const nativeRange = input[0]; - nativeRange.value = 1.0; - nativeRange.dispatchEvent(new Event('input', { bubbles: true })); - nativeRange.dispatchEvent(new Event('change', { bubbles: true })); - } - - // 3. Fallback visual update - if (rangeValue.length) rangeValue.text("1.0"); - }); - - // Append logic - const container = scaleGroup.find('.form-fields'); - if (container.length) { - container.append(resetBtn); - } else if (rangeValue.length) { - rangeValue.after(resetBtn); - } else { - targetInput.after(resetBtn); - } - } - } - - - // Icon Color Reset Button - const iconColorGroup = findGroup('iconColor'); - if (iconColorGroup && !iconColorGroup.find('.icon-color-reset-btn').length) { - const input = iconColorGroup.find(`input[name="${MODULE_ID}.iconColor"]`); - - if (input.length) { - const resetBtn = $(``); - - resetBtn.on('click', () => { - input.val('#ffffff'); - input.trigger('change'); - }); - - input.after(resetBtn); - } - } - }; - - iconTypeSelect.on('change', updateVisibility); - themeSelect.on('change', updateVisibility); - updateVisibility(); - - // Icon Preview Logic - const customIconGroup = findGroup('customIcon'); - if (customIconGroup) { - const input = customIconGroup.find(`input[name="${MODULE_ID}.customIcon"]`); - if (input.length) { - // Create Preview Element - const previewSpan = $(``); - const icon = $(``); - previewSpan.append(icon); - - // Append to container - input.after(previewSpan); - - // Update Function - const updatePreview = () => { - const val = input.val(); - // Reset classes - icon.attr('class', ''); - if (val) { - icon.addClass(val); - } - }; - - // Listeners - input.on('input', updatePreview); - - // Initial update - updatePreview(); - } + }; + input.on('input', updatePreview); + updatePreview(); } } }); -/** - * Handle Fear Tracker Rendering - */ -Hooks.on('renderFearTracker', (app, html, data) => { - injectFearCustomization(html); -}); - -// Helper to interpolate colors -function interpolateColor(color1, color2, factor) { - if (arguments.length < 3) return color1; - - let result = "#"; - const c1 = parseColor(color1); - const c2 = parseColor(color2); - - for (let i = 0; i < 3; i++) { - const val = Math.round(c1[i] + factor * (c2[i] - c1[i])); - let hex = val.toString(16); - if (hex.length < 2) hex = "0" + hex; - result += hex; - } - return result; -} - - -function parseColor(color) { - if (!color) return [0, 0, 0]; - - // Handle RGB/RGBA - if (color.startsWith('rgb')) { - const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (match) { - return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]; - } - } - - // Handle Hex - if (color.startsWith('#')) { - let hex = color.slice(1); - if (hex.length === 3) { - hex = hex.split('').map(char => char + char).join(''); - } - const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? [ - parseInt(result[1], 16), - parseInt(result[2], 16), - parseInt(result[3], 16) - ] : [0, 0, 0]; - } - - return [0, 0, 0]; -} - -function injectFearCustomization(html) { - const container = html instanceof HTMLElement ? html : html[0]; - const fearContainer = container.querySelector('#resource-fear'); - - // Removed Window Lock Injection logic - - if (!fearContainer) return; - - // Get Settings - const iconType = game.settings.get(MODULE_ID, 'iconType'); - const presetIcon = game.settings.get(MODULE_ID, 'presetIcon'); - const customIcon = game.settings.get(MODULE_ID, 'customIcon'); - const colorTheme = game.settings.get(MODULE_ID, 'colorTheme'); - const iconShape = game.settings.get(MODULE_ID, 'iconShape'); - const iconColor = game.settings.get(MODULE_ID, 'iconColor'); - - let fullColor = game.settings.get(MODULE_ID, 'fullColor'); - let emptyColor = '#444444'; // Default for custom - - // Theme Data for Interpolation - let themeStart = null; - let themeEnd = null; - - // Handle Themes - if (colorTheme !== 'custom') { // Removed check for 'foundryborne' to treat it as a preset - const themes = { - 'foundryborne': { start: null, end: null, empty: 'transparent' }, - 'hope-fear': { start: '#FFC107', end: '#512DA8', empty: '#2e1c4a' }, - 'blood-moon': { start: '#5c0000', end: '#ff0000', empty: '#2a0000' }, - 'ethereal': { start: '#00FFFF', end: '#0000FF', empty: '#002a33' }, - 'toxic': { start: '#00FF00', end: '#FFFF00', empty: '#003300' } - }; - - const theme = themes[colorTheme]; - if (theme) { - // Standard Interpolated Preset - themeStart = theme.start; - themeEnd = theme.end; - fullColor = theme.start; - - emptyColor = theme.empty; - } - - - - emptyColor = theme.empty; - } - - - // Apply Scaling - const scale = game.settings.get(MODULE_ID, 'trackerScale'); - if (scale !== 1.0) { - fearContainer.style.zoom = scale; - } else { - fearContainer.style.zoom = 'normal'; - } - - // Determine Icon Class - let iconClass = 'fas fa-skull'; - if (iconType === 'preset') { - iconClass = presetIcon; - } else if (iconType === 'custom') { - iconClass = customIcon; - } else if (iconType === 'custom-svg') { - const svgPath = game.settings.get(MODULE_ID, 'customSvgPath'); - iconClass = svgPath || 'icons/svg/mystery-man.svg'; // Fallback - } - - const isSVG = iconClass.includes('.svg') || iconType === 'custom-svg'; - const icons = fearContainer.querySelectorAll('i'); - const totalIcons = icons.length; - - icons.forEach((icon, index) => { - // 1. Reset Icon State - // Remove common FA prefixes just in case - icon.classList.remove('fa-skull', 'fas', 'far', 'fal', 'fad', 'fab', 'dh-fear-plus-bg-override', 'dh-fear-plus-icon-override'); - - const isInactive = icon.classList.contains('inactive'); - - // Remove old SVG img if present from previous renders - const oldImg = icon.querySelector('img.fear-tracker-icon'); - if (oldImg) oldImg.remove(); - - // 2. Handle Icon Content - if (isSVG) { - // It's an SVG (Capybara or other) - // Create IMG element - const img = document.createElement('img'); - img.src = iconClass; - img.classList.add('fear-tracker-icon'); - img.style.width = '60%'; - img.style.height = '60%'; - - // White for visibility on dark backgrounds - img.style.filter = 'brightness(0) invert(1)'; - img.style.border = 'none'; - img.style.pointerEvents = 'none'; - - icon.appendChild(img); - } else { - // It's a FontAwesome Class - const newClasses = iconClass.split(' ').filter(c => c.trim() !== ''); - icon.classList.add(...newClasses, 'fear-tracker-plus-custom'); - - } - - // 3. Remove System Styling (Module Overrides) - // Skip this for Foundryborne to keep default system look (filters/brightness) - // UPDATE: We now WANT to override Foundryborne to apply our custom gradient, BUT we want to keep its filters! - if (colorTheme !== 'foundryborne') { - icon.style.filter = 'none'; - icon.style.opacity = '1'; - } - - - - // CSS Variables to be applied - let cssBg = 'transparent'; - let cssBgSize = 'cover'; - let cssBgPos = 'center'; - let cssIconColor = '#ffffff'; - let cssIconBgSize = 'cover'; - let cssIconBgPos = 'center'; - - // 4. Handle Icon Color (Glyph) - if (!isSVG) { - if (iconColor && iconColor !== '#ffffff') { - cssIconColor = iconColor; - - // Spanning Logic for Icon Gradient - if (iconColor.includes('gradient') && totalIcons > 0) { - cssIconBgSize = `${totalIcons * 100}% 100%`; - let pos = 0; - if (totalIcons > 1) { - pos = (index / (totalIcons - 1)) * 100; - } - cssIconBgPos = `${pos}% 0%`; - } - } else { - cssIconColor = '#ffffff'; - } - } - - // 5. Handle Background Color (Shape) - // Enable custom coloring for ALL themes now, but toggle classes selectively for Hybrid "Foundryborne" - - // Always apply icon override for gradients - icon.classList.add('dh-fear-plus-icon-override'); - - if (colorTheme !== 'foundryborne') { - // Apply background override for all OTHER themes - icon.classList.add('dh-fear-plus-bg-override'); - } - if (isInactive) { - cssBg = emptyColor; - cssBgSize = 'cover'; - cssBgPos = 'center'; - } else { - // Active - if (themeStart && themeEnd && totalIcons > 1) { - // Interpolate (Preset Themes) - const factor = index / (totalIcons - 1); - const color = interpolateColor(themeStart, themeEnd, factor); - - // Apply Theme Color to BACKGROUND only - cssBg = color; - cssBgSize = 'cover'; - cssBgPos = 'center'; - } else { - // Custom Theme - // Check if fullColor appears to be a gradient - const isGradient = fullColor.includes('gradient'); - - if (isGradient && totalIcons > 0) { - cssBg = fullColor; - cssBgSize = `${totalIcons * 100}% 100%`; - - // Calculate position - let pos = 0; - if (totalIcons > 1) { - pos = (index / (totalIcons - 1)) * 100; - } - cssBgPos = `${pos}% 0%`; - } else { - // Solid Color - cssBg = fullColor; - cssBgSize = 'cover'; - cssBgPos = 'center'; - } - } - } - // } - - // 6. Handle Shape - let borderRadius = '50%'; - if (iconShape === 'rounded') borderRadius = '20%'; - else if (iconShape === 'square') borderRadius = '0%'; - - // 7. Apply CSS Variables - icon.style.setProperty('--dh-fear-bg', cssBg); - icon.style.setProperty('--dh-fear-bg-size', cssBgSize); - icon.style.setProperty('--dh-fear-bg-pos', cssBgPos); - icon.style.setProperty('--dh-fear-icon-color', cssIconColor); - icon.style.setProperty('--dh-fear-icon-bg-size', cssIconBgSize); - icon.style.setProperty('--dh-fear-icon-bg-pos', cssIconBgPos); - icon.style.setProperty('--dh-fear-border-radius', borderRadius); - - // Clean up direct styles that might interfere/confuse - icon.style.background = ''; - icon.style.color = ''; - icon.style.webkitBackgroundClip = ''; - icon.style.backgroundClip = ''; - icon.style.webkitTextFillColor = ''; - icon.style.borderRadius = ''; - }); - - // Remove legacy container class if present - fearContainer.classList.remove('fear-tracker-plus-container-gradient'); - - // Always clear container background to ensure our icon colors are visible and not obscured by system or previous styles - fearContainer.style.background = 'none'; - - // Max Fear Animation - const animateMax = game.settings.get(MODULE_ID, 'maxFearAnimation'); - if (animateMax) { - // Check if all available icons are active - const activeIcons = Array.from(icons).filter(icon => !icon.classList.contains('inactive')).length; - - // If totalIcons > 0 and activeIcons === totalIcons, apply animation - if (totalIcons > 0 && activeIcons === totalIcons) { - // Prevent system "Blue" overrides - fearContainer.classList.remove('complete', 'max', 'full'); - - icons.forEach(icon => { - icon.classList.add('fear-tracker-plus-animate'); - // Force filter reset to ensure our animation works and system color doesn't override - icon.style.filter = 'none'; - }); - } else { - icons.forEach(icon => { - icon.classList.remove('fear-tracker-plus-animate'); - // Restore filter if we touched it (only relevant if not custom theme) - if (colorTheme === 'foundryborne') { - icon.style.filter = ''; - } - }); - } - } else { - icons.forEach(icon => icon.classList.remove('fear-tracker-plus-animate')); - } -} - diff --git a/styles/module.css b/styles/module.css index eba885d..e3d9bb3 100644 --- a/styles/module.css +++ b/styles/module.css @@ -1,56 +1,263 @@ -/* CSS Variable Defaults */ -:root { - --dh-fear-bg: transparent; - --dh-fear-bg-size: cover; - --dh-fear-bg-pos: center; - --dh-fear-icon-color: #ffffff; - --dh-fear-border-radius: 50%; +/* Hide Default Daggerheart Fear Tracker Window */ +#resources { + display: none !important; } -/* Base Icon Styling */ -#resource-fear i.fear-tracker-plus-custom { - position: relative; - z-index: 1; - transition: all 0.5s ease; - border-radius: var(--dh-fear-border-radius); +/* Also hide the inner content just in case */ +#resource-fear { + display: none !important; +} - /* Background Shape Styling (Applied to container) - REMOVED */ +/* Try to hide parent container if it creates a ghost box */ +/* Assuming standard system classes, might need adjustment */ +/* .daggerheart-resources:has(#resource-fear) { + /* We can't easily hide the parent without risking hiding siblings */ +/* But we can try to minimize the impact of the child */ +/* } */ - /* Ensure icon is centered */ - display: inline-flex; +/* New Fear Tracker Window Styles */ +.dh-feartrackerplus-window { + position: fixed; + pointer-events: none; + /* Allow clicks to pass through transparent areas */ + z-index: 100; +} + +.fear-tracker-window { + pointer-events: all; + background: rgba(20, 20, 25, 0.25); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 12px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; + box-shadow: none; + transition: all 0.3s ease; + min-width: 50px; + /* Allow narrower width for vertical column */ + color: #eee; + font-family: 'Inter', 'Roboto', sans-serif; + overflow: visible; +} + +.fear-tracker-window:hover { + background: rgba(20, 20, 25, 0.85); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +} + +/* Narrow Mode (Vertical Layout) */ +.fear-tracker-window.narrow { + padding: 12px; + /* Increased to accommodate shadows */ +} + +/* Auto-expand on hover to show controls */ +/* Only if NOT locked, OR if force-expand is active (Ctrl key) */ +.fear-tracker-window.narrow:not(.locked):hover, +.fear-tracker-window.narrow.force-expand { + min-width: 90px !important; +} + +.fear-tracker-window.narrow .fear-label { + display: none !important; + /* Hide title text */ +} + +.fear-tracker-window.narrow .tracker-header { justify-content: center; + gap: 0; +} + +/* Stack header controls vertically or ensure they fit */ +.fear-tracker-window.narrow .header-controls { + gap: 4px; +} + +/* Force vertical stack for content in narrow mode */ +.fear-tracker-window.narrow .fear-visuals { + flex-direction: column; +} + +/* Constrain tokens to single column even when window expands on hover */ +.fear-tracker-window.narrow .tokens-container { + flex-direction: column; + max-width: none; + /* Remove fixed constraint */ +} + +.fear-tracker-window.minimized { + min-width: fit-content; + padding: 8px 12px; +} + +.fear-tracker-window.minimized .fear-visuals { + gap: 0; +} + +.fear-tracker-window.minimized:hover .fear-visuals { + gap: 8px; +} + +/* Header and Drag Handle */ +.tracker-header { + display: flex; + justify-content: space-between; align-items: center; + opacity: 0; + height: 0; + overflow: hidden; + transition: all 0.2s ease; } -/* Scoped Override for Custom Themes */ -/* Scoped Override for Custom Themes */ -#resource-fear i.fear-tracker-plus-custom.dh-fear-plus-bg-override { - background: var(--dh-fear-bg) !important; - background-size: var(--dh-fear-bg-size) !important; - background-position: var(--dh-fear-bg-pos) !important; - background-repeat: no-repeat !important; +/* Show header on hover if unlocked, OR if force-expanded via Ctrl */ +.fear-tracker-window:not(.locked):hover .tracker-header, +.fear-tracker-window.force-expand .tracker-header { + opacity: 1; + height: 24px; + margin-bottom: 4px; } -/* Icon Glyph Styling (Applied to ::before which contains the character) */ -/* Icon Glyph Styling (Applied to ::before which contains the character) */ -#resource-fear i.fear-tracker-plus-custom.dh-fear-plus-icon-override::before { - /* Apply Gradient or Solid Color to Text */ - background: var(--dh-fear-icon-color); - background-size: var(--dh-fear-icon-bg-size, cover); - background-position: var(--dh-fear-icon-bg-pos, center); - background-repeat: no-repeat; - - -webkit-background-clip: text !important; - background-clip: text !important; - -webkit-text-fill-color: transparent !important; - color: transparent !important; - - /* Ensure it overlays correctly */ - display: inline-block; +.drag-handle { + cursor: grab; + color: rgba(255, 255, 255, 0.5); + padding: 2px 8px; } +.fear-tracker-window.locked .drag-handle { + display: none; +} +.header-controls { + display: flex; + gap: 8px; + padding-right: 4px; +} +.control-btn { + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + font-size: 14px; +} + +.control-btn:hover { + color: #fff; + text-shadow: 0 0 8px rgba(255, 255, 255, 0.5); +} + +/* Content */ +.fear-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.fear-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 2px; + color: rgba(255, 255, 255, 0.6); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); + margin-right: auto; + margin-left: 8px; + display: none; +} + +.fear-tracker-window:hover .fear-label { + display: block; +} + +.fear-visuals { + display: flex; + align-items: center; + gap: 8px; + transition: gap 0.3s ease; + width: 100%; +} + +.tokens-container { + display: flex; + gap: 6px; + flex-wrap: wrap; + justify-content: center; + flex: 1; +} + +/* Tokens */ +.fear-token { + width: var(--fear-token-size, 32px); + height: var(--fear-token-size, 32px); + display: flex; + align-items: center; + justify-content: center; + background: #444; + /* Default inactive */ + border: 2px solid rgba(255, 255, 255, 0.1); + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); + transition: all 0.3s ease; + overflow: hidden; +} + +.fear-token.circle { + border-radius: 50%; +} + +.fear-token.rounded { + border-radius: 8px; +} + +.fear-token.square { + border-radius: 0; +} + +.fear-token.active { + border-color: rgba(255, 255, 255, 0.3); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); +} + +.fear-token i { + font-size: calc(var(--fear-token-size, 32px) * 0.5); + color: #fff; + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)); + transition: transform 0.3s ease; +} + +.fear-token img.fear-icon-img { + width: 70%; + height: 70%; + object-fit: contain; + filter: brightness(0) invert(1) drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)); +} + +/* Progress Bar */ +.fear-bar-container { + width: 200px; + /* Base width, window is resizable so this is flexible in context */ + height: 24px; + background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + overflow: hidden; + position: relative; + box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.6); +} + +.fear-bar-track { + width: 100%; + height: 100%; +} + +.fear-bar-fill { + height: 100%; + width: 0%; + transition: width 0.5s ease-out, background 0.3s ease; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +} + +/* Animations */ @keyframes fearPulse { 0% { transform: scale(1); @@ -58,8 +265,8 @@ } 50% { - transform: scale(1.1); - filter: drop-shadow(0 0 5px rgba(255, 0, 0, 0.5)); + transform: scale(1.15); + filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.6)); } 100% { @@ -68,6 +275,102 @@ } } -#resource-fear i.fear-tracker-plus-animate { +.fear-pulse { animation: fearPulse 2s infinite ease-in-out; +} + +@keyframes maxFearPulseBorder { + 0% { + box-shadow: 0 0 0 0 rgba(220, 20, 60, 0); + border-color: rgba(255, 255, 255, 0.05); + } + + 50% { + box-shadow: 0 0 20px 0 rgba(220, 20, 60, 0.5); + border-color: rgba(220, 20, 60, 0.8); + } + + 100% { + box-shadow: 0 0 0 0 rgba(220, 20, 60, 0); + border-color: rgba(255, 255, 255, 0.05); + } +} + +.fear-tracker-window.max-fear { + animation: maxFearPulseBorder 2s infinite ease-in-out; +} + +/* Controls */ +.value-control { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + cursor: pointer; + color: #ccc; + font-size: 12px; + opacity: 0; + transition: all 0.2s ease; +} + +.fear-tracker-window:hover .value-control { + opacity: 1; +} + +.value-control:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; +} + +.value-control.plus:hover { + background: rgba(74, 222, 128, 0.2); + color: #4ade80; +} + +.value-control.minus:hover { + background: rgba(248, 113, 113, 0.2); + color: #f87171; +} + + +.fear-value { + font-size: 11px; + color: rgba(255, 255, 255, 0.4); + font-family: monospace; +} + +/* Resize Handle */ +.resize-handle { + position: absolute; + bottom: 2px; + right: 2px; + cursor: nwse-resize; + color: rgba(255, 255, 255, 0.3); + font-size: 10px; + padding: 2px; + line-height: 1; + opacity: 0; + transition: opacity 0.2s; +} + +.fear-tracker-window:hover .resize-handle { + opacity: 1; +} + +.fear-tracker-window.locked .resize-handle { + display: none; +} + +/* Constrain context menu width to avoid excessive whitespace */ +#context-menu { + min-width: unset; + width: fit-content; + max-width: 200px; +} + +#context-menu .context-items .context-item { + padding-right: 12px; } \ No newline at end of file diff --git a/templates/fear-tracker.hbs b/templates/fear-tracker.hbs new file mode 100644 index 0000000..0f58e7a --- /dev/null +++ b/templates/fear-tracker.hbs @@ -0,0 +1,66 @@ +
+
+
+ +
+ {{#unless isMinimized}} +
FEAR
+ {{/unless}} +
+ + + +
+
+ + +
+ + + +
+ {{#if isGM}} + {{#if showControlButtons}} + + + + {{/if}} + {{/if}} + + {{#if useBar}} +
+
+
+
+
+ {{else}} +
+ {{#each fearTokens as |token|}} +
+ {{{token.icon}}} +
+ {{/each}} +
+ {{/if}} + + {{#if isGM}} + {{#if showControlButtons}} + + + + {{/if}} + {{/if}} +
+ + {{#if showFearValue}} +
+ {{currentFear}} / {{maxFear}} +
+ {{/if}} +
+
+ +
+
\ No newline at end of file