feat: Add new Fear Tracker application with customizable UI, drag/resize, and settings integration.
This commit is contained in:
parent
97ff59e703
commit
c25baa2554
6 changed files with 1157 additions and 504 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "dh-feartrackerplus",
|
"id": "dh-feartrackerplus",
|
||||||
"title": "Daggerheart Fear Tracker Plus",
|
"title": "Daggerheart Fear Tracker Plus",
|
||||||
"version": "1.3.0",
|
"version": "2.0.0",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "13"
|
"verified": "13"
|
||||||
|
|
@ -32,6 +32,6 @@
|
||||||
],
|
],
|
||||||
"url": "https://github.com/cptn-cosmo/dh-feartrackerplus",
|
"url": "https://github.com/cptn-cosmo/dh-feartrackerplus",
|
||||||
"manifest": "https://github.com/cptn-cosmo/dh-feartrackerplus/releases/latest/download/module.json",
|
"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."
|
"description": "Customizes the Fear Tracker for Daggerheart."
|
||||||
}
|
}
|
||||||
BIN
scripts/.module.js.kate-swp
Normal file
BIN
scripts/.module.js.kate-swp
Normal file
Binary file not shown.
541
scripts/fear-tracker-app.js
Normal file
541
scripts/fear-tracker-app.js
Normal file
|
|
@ -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: '<i class="fas fa-lock-open"></i>',
|
||||||
|
condition: () => game.settings.get("dh-feartrackerplus", "locked"),
|
||||||
|
callback: async () => {
|
||||||
|
await game.settings.set("dh-feartrackerplus", "locked", false);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Lock Position",
|
||||||
|
icon: '<i class="fas fa-lock"></i>',
|
||||||
|
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 `<img src="${svgPath || 'icons/svg/mystery-man.svg'}" class="fear-icon-img" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconClass.includes('.svg')) {
|
||||||
|
return `<img src="${iconClass}" class="fear-icon-img" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `<i class="${iconClass}" style="${style}"></i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
import { FearTrackerApp } from './fear-tracker-app.js';
|
||||||
|
|
||||||
const MODULE_ID = 'dh-feartrackerplus';
|
const MODULE_ID = 'dh-feartrackerplus';
|
||||||
|
|
||||||
Hooks.once('init', () => {
|
Hooks.once('init', () => {
|
||||||
console.log(`${MODULE_ID} | Initializing Daggerheart Fear Tracker Plus`);
|
console.log(`${MODULE_ID} | Initializing Daggerheart Fear Tracker Plus`);
|
||||||
|
|
||||||
const refreshFearTracker = () => {
|
const refreshFearTracker = () => {
|
||||||
if (ui.resources?.render) ui.resources.render({ force: true });
|
FearTrackerApp.instance?.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register Settings
|
// Register Settings
|
||||||
|
|
@ -67,10 +69,6 @@ Hooks.once('init', () => {
|
||||||
onChange: refreshFearTracker
|
onChange: refreshFearTracker
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Removed trackerLocked setting as requested
|
|
||||||
|
|
||||||
game.settings.register(MODULE_ID, 'colorTheme', {
|
game.settings.register(MODULE_ID, 'colorTheme', {
|
||||||
name: 'Color Theme',
|
name: 'Color Theme',
|
||||||
hint: 'Choose a color preset or Custom to set your own colors below.',
|
hint: 'Choose a color preset or Custom to set your own colors below.',
|
||||||
|
|
@ -95,11 +93,10 @@ Hooks.once('init', () => {
|
||||||
scope: 'client',
|
scope: 'client',
|
||||||
config: true,
|
config: true,
|
||||||
type: String,
|
type: String,
|
||||||
default: '#000000',
|
default: '#ff0000',
|
||||||
onChange: refreshFearTracker
|
onChange: refreshFearTracker
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- New Settings for Icon Color/Shape ---
|
|
||||||
game.settings.register(MODULE_ID, 'iconShape', {
|
game.settings.register(MODULE_ID, 'iconShape', {
|
||||||
name: 'Icon Shape',
|
name: 'Icon Shape',
|
||||||
hint: 'Select the background shape for the icons.',
|
hint: 'Select the background shape for the icons.',
|
||||||
|
|
@ -125,30 +122,131 @@ Hooks.once('init', () => {
|
||||||
onChange: refreshFearTracker
|
onChange: refreshFearTracker
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.register(MODULE_ID, 'trackerScale', {
|
game.settings.register(MODULE_ID, 'iconSize', {
|
||||||
name: 'Tracker Scale',
|
name: 'Icon Size (px)',
|
||||||
hint: 'Resize the fear tracker (0.25x to 2.0x).',
|
hint: 'Size of the fear tokens in pixels. (Default: 32)',
|
||||||
scope: 'client',
|
scope: 'client',
|
||||||
config: true,
|
config: true,
|
||||||
type: Number,
|
type: Number,
|
||||||
range: {
|
range: { min: 20, max: 64, step: 2 },
|
||||||
min: 0.25,
|
default: 32,
|
||||||
max: 2.0,
|
|
||||||
step: 0.05
|
|
||||||
},
|
|
||||||
default: 1.0,
|
|
||||||
onChange: refreshFearTracker
|
onChange: refreshFearTracker
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.register(MODULE_ID, 'maxFearAnimation', {
|
game.settings.register(MODULE_ID, 'viewMode', {
|
||||||
name: 'Max Fear Animation',
|
name: 'View Mode',
|
||||||
hint: 'animate the fear tracker when it reaches maximum value.',
|
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',
|
scope: 'client',
|
||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
onChange: refreshFearTracker
|
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) => {
|
Hooks.on('renderSettingsConfig', (app, html, data) => {
|
||||||
const $html = $(html);
|
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 iconTypeSelect = $html.find(`select[name="${MODULE_ID}.iconType"]`);
|
||||||
const themeSelect = $html.find(`select[name="${MODULE_ID}.colorTheme"]`);
|
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;
|
if (!iconTypeSelect.length || !themeSelect.length) return;
|
||||||
|
|
||||||
{
|
// Helper to find setting group
|
||||||
// Helper to find setting group
|
const findGroup = (key) => {
|
||||||
const findGroup = (key) => {
|
let group = $html.find(`.form-group[data-setting-id="${MODULE_ID}.${key}"]`);
|
||||||
// Try data-setting-id first (standard in V10/V11+)
|
if (group.length) return group;
|
||||||
let group = $html.find(`.form-group[data-setting-id="${MODULE_ID}.${key}"]`);
|
const input = $html.find(`[name="${MODULE_ID}.${key}"]`);
|
||||||
if (group.length) return group;
|
if (input.length) return input.closest('.form-group');
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Fallback: Find input/select by name and go up to form-group
|
const updateVisibility = () => {
|
||||||
const input = $html.find(`[name="${MODULE_ID}.${key}"]`);
|
// Safe check for elements, they might not exist if settings window structure changes
|
||||||
if (input.length) return input.closest('.form-group');
|
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 = () => {
|
// Groups
|
||||||
const iconType = iconTypeSelect.val();
|
const iconTypeGroup = findGroup('iconType');
|
||||||
const theme = themeSelect.val();
|
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
|
// Logic
|
||||||
const presetGroup = findGroup('presetIcon');
|
if (currentViewMode === 'bar') {
|
||||||
const customIconGroup = findGroup('customIcon');
|
// Hide Icon Settings
|
||||||
const customSvgGroup = findGroup('customSvgPath');
|
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 (presetGroup) presetGroup.hide();
|
||||||
if (customIconGroup) customIconGroup.hide();
|
if (customIconGroup) customIconGroup.hide();
|
||||||
if (customSvgGroup) customSvgGroup.hide();
|
if (customSvgGroup) customSvgGroup.hide();
|
||||||
|
|
||||||
// Apply Logic
|
if (iconType === 'preset' && presetGroup) presetGroup.show();
|
||||||
if (iconType === 'preset' && presetGroup) {
|
else if (iconType === 'custom' && customIconGroup) customIconGroup.show();
|
||||||
presetGroup.show();
|
else if (iconType === 'custom-svg' && customSvgGroup) customSvgGroup.show();
|
||||||
} else if (iconType === 'custom' && customIconGroup) {
|
|
||||||
customIconGroup.show();
|
|
||||||
} else if (iconType === 'custom-svg' && customSvgGroup) {
|
|
||||||
customSvgGroup.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color Inputs
|
|
||||||
const fullColorGroup = findGroup('fullColor');
|
|
||||||
|
|
||||||
|
// Full Color (Custom Theme)
|
||||||
if (fullColorGroup) {
|
if (fullColorGroup) {
|
||||||
if (theme === 'custom') {
|
if (theme === 'custom') fullColorGroup.show();
|
||||||
fullColorGroup.show();
|
else fullColorGroup.hide();
|
||||||
} 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 = $(`<span class="icon-preview" style="margin-left: 10px; font-size: 1.5em; width: 30px; text-align: center; display: inline-block;"></span>`);
|
||||||
|
const icon = $(`<i></i>`);
|
||||||
|
previewSpan.append(icon);
|
||||||
|
input.after(previewSpan);
|
||||||
|
const updatePreview = () => {
|
||||||
|
const val = input.val();
|
||||||
|
icon.attr('class', '');
|
||||||
|
if (val) {
|
||||||
|
icon.addClass(val);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
input.on('input', updatePreview);
|
||||||
// Tracker Scale Reset Button
|
updatePreview();
|
||||||
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 = $(`<button type="button" class="scale-reset-btn" title="Reset to 1.0x" style="flex: 0 0 30px; margin-left: 5px;"><i class="fas fa-undo"></i></button>`);
|
|
||||||
|
|
||||||
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 = $(`<button type="button" class="icon-color-reset-btn" title="Reset to White" style="flex: 0 0 30px; margin-left: 5px;"><i class="fas fa-undo"></i></button>`);
|
|
||||||
|
|
||||||
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 = $(`<span class="icon-preview" style="margin-left: 10px; font-size: 1.5em; width: 30px; text-align: center; display: inline-block;"></span>`);
|
|
||||||
const icon = $(`<i></i>`);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,263 @@
|
||||||
/* CSS Variable Defaults */
|
/* Hide Default Daggerheart Fear Tracker Window */
|
||||||
:root {
|
#resources {
|
||||||
--dh-fear-bg: transparent;
|
display: none !important;
|
||||||
--dh-fear-bg-size: cover;
|
|
||||||
--dh-fear-bg-pos: center;
|
|
||||||
--dh-fear-icon-color: #ffffff;
|
|
||||||
--dh-fear-border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base Icon Styling */
|
/* Also hide the inner content just in case */
|
||||||
#resource-fear i.fear-tracker-plus-custom {
|
#resource-fear {
|
||||||
position: relative;
|
display: none !important;
|
||||||
z-index: 1;
|
}
|
||||||
transition: all 0.5s ease;
|
|
||||||
border-radius: var(--dh-fear-border-radius);
|
|
||||||
|
|
||||||
/* 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 */
|
/* New Fear Tracker Window Styles */
|
||||||
display: inline-flex;
|
.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;
|
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;
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scoped Override for Custom Themes */
|
/* Show header on hover if unlocked, OR if force-expanded via Ctrl */
|
||||||
/* Scoped Override for Custom Themes */
|
.fear-tracker-window:not(.locked):hover .tracker-header,
|
||||||
#resource-fear i.fear-tracker-plus-custom.dh-fear-plus-bg-override {
|
.fear-tracker-window.force-expand .tracker-header {
|
||||||
background: var(--dh-fear-bg) !important;
|
opacity: 1;
|
||||||
background-size: var(--dh-fear-bg-size) !important;
|
height: 24px;
|
||||||
background-position: var(--dh-fear-bg-pos) !important;
|
margin-bottom: 4px;
|
||||||
background-repeat: no-repeat !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon Glyph Styling (Applied to ::before which contains the character) */
|
.drag-handle {
|
||||||
/* Icon Glyph Styling (Applied to ::before which contains the character) */
|
cursor: grab;
|
||||||
#resource-fear i.fear-tracker-plus-custom.dh-fear-plus-icon-override::before {
|
color: rgba(255, 255, 255, 0.5);
|
||||||
/* Apply Gradient or Solid Color to Text */
|
padding: 2px 8px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
@keyframes fearPulse {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
|
|
@ -58,8 +265,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: scale(1.1);
|
transform: scale(1.15);
|
||||||
filter: drop-shadow(0 0 5px rgba(255, 0, 0, 0.5));
|
filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.6));
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
|
|
@ -68,6 +275,102 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#resource-fear i.fear-tracker-plus-animate {
|
.fear-pulse {
|
||||||
animation: fearPulse 2s infinite ease-in-out;
|
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;
|
||||||
}
|
}
|
||||||
66
templates/fear-tracker.hbs
Normal file
66
templates/fear-tracker.hbs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<div class="fear-tracker-window {{#if isMinimized}}minimized{{/if}} {{#if isLocked}}locked{{/if}}">
|
||||||
|
<div class="tracker-header">
|
||||||
|
<div class="drag-handle" data-tooltip="Drag to move">
|
||||||
|
<i class="fa-solid fa-grip-vertical"></i>
|
||||||
|
</div>
|
||||||
|
{{#unless isMinimized}}
|
||||||
|
<div class="fear-label">FEAR</div>
|
||||||
|
{{/unless}}
|
||||||
|
<div class="header-controls">
|
||||||
|
<a class="control-btn" data-action="toggleLock"
|
||||||
|
data-tooltip="{{#if isLocked}}Unlock Window{{else}}Lock Window{{/if}}">
|
||||||
|
<i class="fa-solid {{#if isLocked}}fa-lock{{else}}fa-lock-open{{/if}}"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="fear-content">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="fear-visuals">
|
||||||
|
{{#if isGM}}
|
||||||
|
{{#if showControlButtons}}
|
||||||
|
<a class="value-control minus" data-action="decreaseFear">
|
||||||
|
<i class="fa-solid fa-minus"></i>
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if useBar}}
|
||||||
|
<div class="fear-bar-container">
|
||||||
|
<div class="fear-bar-track">
|
||||||
|
<div class="fear-bar-fill" style="{{barStyle}}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="tokens-container">
|
||||||
|
{{#each fearTokens as |token|}}
|
||||||
|
<div class="fear-token {{../iconShape}} {{#if token.active}}active{{else}}inactive{{/if}}"
|
||||||
|
data-index="{{token.index}}" style="{{token.style}}">
|
||||||
|
{{{token.icon}}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isGM}}
|
||||||
|
{{#if showControlButtons}}
|
||||||
|
<a class="value-control plus" data-action="increaseFear">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if showFearValue}}
|
||||||
|
<div class="fear-value">
|
||||||
|
{{currentFear}} / {{maxFear}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="resize-handle" data-tooltip="Drag to resize">
|
||||||
|
<i class="fa-solid fa-chevron-right" style="transform: rotate(45deg);"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue