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 0000000..8cc68b2
Binary files /dev/null and b/scripts/.module.js.kate-swp differ
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 @@
+