feat: Rework countdown visuals with new fill, border, and number color settings, including dynamic settings visibility and a "both" display mode.

This commit is contained in:
CPTN Cosmo 2025-12-22 00:01:20 +01:00
parent 02a3855ea1
commit d00248593a
4 changed files with 282 additions and 37 deletions

View file

@ -54,12 +54,18 @@ export class CountdownTrackerApp extends HandlebarsApplicationMixin(ApplicationV
const iconShape = game.settings.get("dh-improved-countdowns", "iconShape"); const iconShape = game.settings.get("dh-improved-countdowns", "iconShape");
const displayMode = game.settings.get("dh-improved-countdowns", "displayMode"); const displayMode = game.settings.get("dh-improved-countdowns", "displayMode");
const barOrientation = game.settings.get("dh-improved-countdowns", "barOrientation"); const barOrientation = game.settings.get("dh-improved-countdowns", "barOrientation");
const visualColor = game.settings.get("dh-improved-countdowns", "visualColor");
const enableVisualOverlay = game.settings.get("dh-improved-countdowns", "enableVisualOverlay"); const enableVisualOverlay = game.settings.get("dh-improved-countdowns", "enableVisualOverlay");
const fillType = game.settings.get("dh-improved-countdowns", "fillType");
const invertProgress = game.settings.get("dh-improved-countdowns", "invertProgress");
const numberColor = game.settings.get("dh-improved-countdowns", "numberColor");
const fillColor = game.settings.get("dh-improved-countdowns", "fillColor");
const enableVisualBorder = game.settings.get("dh-improved-countdowns", "enableVisualBorder"); const enableVisualBorder = game.settings.get("dh-improved-countdowns", "enableVisualBorder");
const invertBorder = game.settings.get("dh-improved-countdowns", "invertBorder");
const borderColor = game.settings.get("dh-improved-countdowns", "borderColor");
const gmAlwaysShowNumbers = game.settings.get("dh-improved-countdowns", "gmAlwaysShowNumbers"); const gmAlwaysShowNumbers = game.settings.get("dh-improved-countdowns", "gmAlwaysShowNumbers");
const showNumbers = (isGM && gmAlwaysShowNumbers) || displayMode === "number"; const showNumbers = (isGM && gmAlwaysShowNumbers) || displayMode === "number" || displayMode === "both";
const showVisuals = displayMode === "visual" || displayMode === "both";
// Fetch countdowns from system settings // Fetch countdowns from system settings
const systemCountdownSetting = game.settings.get("daggerheart", "Countdowns"); const systemCountdownSetting = game.settings.get("daggerheart", "Countdowns");
@ -71,12 +77,20 @@ export class CountdownTrackerApp extends HandlebarsApplicationMixin(ApplicationV
if (ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { if (ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
const current = countdown.progress.current; const current = countdown.progress.current;
const max = countdown.progress.start; const max = countdown.progress.start;
const percentage = Math.max(0, Math.min(100, (current / max) * 100)); let percentage = Math.max(0, Math.min(100, (current / max) * 100));
const pctRemaining = 100 - percentage;
/*
if (invertProgress) {
// We handle inversion in template now to preserve position
}
*/
countdowns[id] = { countdowns[id] = {
...countdown, ...countdown,
editable: isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, editable: isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER,
percentage, percentage,
pctRemaining,
cssClass: `shape-${iconShape}` cssClass: `shape-${iconShape}`
}; };
} }
@ -89,11 +103,17 @@ export class CountdownTrackerApp extends HandlebarsApplicationMixin(ApplicationV
isMinimized, isMinimized,
isLocked, isLocked,
showNumbers, showNumbers,
showVisuals,
iconShape, iconShape,
barOrientation, barOrientation,
visualColor,
enableVisualOverlay, enableVisualOverlay,
enableVisualBorder fillType,
fillColor,
enableVisualBorder,
invertBorder,
borderColor,
invertProgress,
numberColor
}; };
} }
@ -130,13 +150,13 @@ export class CountdownTrackerApp extends HandlebarsApplicationMixin(ApplicationV
static async #onToggleView(event, target) { static async #onToggleView(event, target) {
const current = game.settings.get("dh-improved-countdowns", "minimized"); const current = game.settings.get("dh-improved-countdowns", "minimized");
await game.settings.set("dh-improved-countdowns", "minimized", !current); await game.settings.set("dh-improved-countdowns", "minimized", !current);
this.instance.render(); CountdownTrackerApp.instance?.render();
} }
static async #onToggleLock(event, target) { static async #onToggleLock(event, target) {
const current = game.settings.get("dh-improved-countdowns", "locked"); const current = game.settings.get("dh-improved-countdowns", "locked");
await game.settings.set("dh-improved-countdowns", "locked", !current); await game.settings.set("dh-improved-countdowns", "locked", !current);
this.instance.render(); CountdownTrackerApp.instance?.render();
} }
_onRender(context, options) { _onRender(context, options) {

View file

@ -14,7 +14,7 @@ Hooks.once('init', () => {
name: "Lock Tracker Position", name: "Lock Tracker Position",
hint: "Prevents the countdown tracker from being dragged.", hint: "Prevents the countdown tracker from being dragged.",
scope: "client", scope: "client",
config: true, config: false, // Hidden from settings menu, toggled via UI
type: Boolean, type: Boolean,
default: false, default: false,
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
@ -29,6 +29,21 @@ Hooks.once('init', () => {
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
}); });
game.settings.register("dh-improved-countdowns", "displayMode", {
name: "Display Mode",
hint: "Choose how the countdown value is displayed.",
scope: "world",
config: true,
type: String,
choices: {
"number": "Number Only",
"visual": "Visual Only",
"both": "Visual + Number"
},
default: "number",
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "iconShape", { game.settings.register("dh-improved-countdowns", "iconShape", {
name: "Icon Shape", name: "Icon Shape",
hint: "Choose the shape of the countdown icons.", hint: "Choose the shape of the countdown icons.",
@ -43,17 +58,13 @@ Hooks.once('init', () => {
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
}); });
game.settings.register("dh-improved-countdowns", "displayMode", { game.settings.register("dh-improved-countdowns", "numberColor", {
name: "Display Mode", name: "Number Color",
hint: "Choose how the countdown value is displayed.", hint: "Color for the numerical text.",
scope: "world", scope: "client",
config: true, config: true,
type: String, type: String,
choices: { default: "#ffffff",
"number": "Number",
"visual": "Visual (Bar/Clock)"
},
default: "number",
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
}); });
@ -71,16 +82,6 @@ Hooks.once('init', () => {
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
}); });
game.settings.register("dh-improved-countdowns", "visualColor", {
name: "Visual Color",
hint: "Choose the color for the progress overlay and border.",
scope: "client",
config: true,
type: String,
default: "#ffffff",
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "enableVisualOverlay", { game.settings.register("dh-improved-countdowns", "enableVisualOverlay", {
name: "Enable Fill Overlay", name: "Enable Fill Overlay",
hint: "Show the filled progress overlay (Bar or Clock).", hint: "Show the filled progress overlay (Bar or Clock).",
@ -91,6 +92,40 @@ Hooks.once('init', () => {
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
}); });
game.settings.register("dh-improved-countdowns", "fillType", {
name: "Fill Type",
hint: "Choose between a color overlay or a grayscale filter method.",
scope: "client",
config: true,
type: String,
choices: {
"color": "Color Overlay",
"grayscale": "Grayscale Filter"
},
default: "color",
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "invertProgress", {
name: "Invert Fill Overlay",
hint: "Fill the empty space instead of the current value.",
scope: "client",
config: true,
type: Boolean,
default: false,
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "fillColor", {
name: "Fill Overlay Color",
hint: "Color for the filled progress overlay.",
scope: "client",
config: true,
type: String,
default: "#ffffff",
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "enableVisualBorder", { game.settings.register("dh-improved-countdowns", "enableVisualBorder", {
name: "Enable Border Progress", name: "Enable Border Progress",
hint: "Show a progress border around the icon.", hint: "Show a progress border around the icon.",
@ -101,6 +136,26 @@ Hooks.once('init', () => {
onChange: () => CountdownTrackerApp.instance?.render() onChange: () => CountdownTrackerApp.instance?.render()
}); });
game.settings.register("dh-improved-countdowns", "invertBorder", {
name: "Invert Border Progress",
hint: "Fill the empty space instead of the current value for the border.",
scope: "client",
config: true,
type: Boolean,
default: false,
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "borderColor", {
name: "Border Color",
hint: "Color for the progress border.",
scope: "client",
config: true,
type: String,
default: "#ffffff",
onChange: () => CountdownTrackerApp.instance?.render()
});
game.settings.register("dh-improved-countdowns", "gmAlwaysShowNumbers", { game.settings.register("dh-improved-countdowns", "gmAlwaysShowNumbers", {
name: "GM Always Shows Numbers", name: "GM Always Shows Numbers",
hint: "If enabled, the GM will always see the numerical value even if Display Mode is set to Visual.", hint: "If enabled, the GM will always see the numerical value even if Display Mode is set to Visual.",
@ -112,6 +167,111 @@ Hooks.once('init', () => {
}); });
}); });
Hooks.on('renderSettingsConfig', (app, html, data) => {
// Ensure html is a jQuery object
html = $(html);
const moduleId = "dh-improved-countdowns";
console.log("Improved Countdowns | Settings Config Rendered");
// Helper to find the form group for a setting
const getGroup = (settingName) => {
const input = html.find(`[name="${moduleId}.${settingName}"]`);
const group = input.closest(".form-group");
if (!group.length) console.warn(`Improved Countdowns | Could not find form group for ${settingName}`);
return group;
};
const enableOverlayInput = html.find(`[name="${moduleId}.enableVisualOverlay"]`);
const enableBorderInput = html.find(`[name="${moduleId}.enableVisualBorder"]`);
const displayModeInput = html.find(`[name="${moduleId}.displayMode"]`);
// Debug findings
if (!enableOverlayInput.length) console.warn("Improved Countdowns | Enable Overlay Input not found");
// if (!displayModeInput.length) console.warn("Improved Countdowns | Display Mode Input not found");
const fillTypeGroup = getGroup("fillType");
const invertProgressGroup = getGroup("invertProgress");
const fillColorGroup = getGroup("fillColor");
const invertBorderGroup = getGroup("invertBorder");
const borderColorGroup = getGroup("borderColor");
const barOrientationGroup = getGroup("barOrientation");
// Number specific groups
const numberColorGroup = getGroup("numberColor");
// Visual specific groups - we already have them above
const enableOverlayGroup = getGroup("enableVisualOverlay");
const enableBorderGroup = getGroup("enableVisualBorder");
const updateVisibility = () => {
const displayMode = displayModeInput.val();
const showVisualSettings = displayMode === "visual" || displayMode === "both";
const showNumberSettings = displayMode === "number" || displayMode === "both";
// Number Settings Visibility
if (showNumberSettings) {
numberColorGroup.show();
} else {
numberColorGroup.hide();
}
// Visual Settings Visibility
if (showVisualSettings) {
enableOverlayGroup.show();
enableBorderGroup.show();
// Nested visual settings logic
const overlayEnabled = enableOverlayInput.prop("checked");
const borderEnabled = enableBorderInput.prop("checked");
const fillType = html.find(`[name="${moduleId}.fillType"]`).val();
if (overlayEnabled) {
fillTypeGroup.show();
invertProgressGroup.show();
barOrientationGroup.show();
if (fillType === "grayscale") {
fillColorGroup.hide();
} else {
fillColorGroup.show();
}
} else {
fillTypeGroup.hide();
invertProgressGroup.hide();
fillColorGroup.hide();
barOrientationGroup.hide();
}
if (borderEnabled) {
invertBorderGroup.show();
borderColorGroup.show();
} else {
invertBorderGroup.hide();
borderColorGroup.hide();
}
} else {
// Hide all visual settings
enableOverlayGroup.hide();
enableBorderGroup.hide();
fillTypeGroup.hide();
invertProgressGroup.hide();
fillColorGroup.hide();
barOrientationGroup.hide();
invertBorderGroup.hide();
borderColorGroup.hide();
}
};
// Listeners
displayModeInput.on("change", updateVisibility);
if (enableOverlayInput.length && enableBorderInput.length) {
html.find(`[name="${moduleId}.fillType"]`).on("change", updateVisibility);
enableOverlayInput.on("change", updateVisibility);
enableBorderInput.on("change", updateVisibility);
updateVisibility(); // Initial check
}
});
Hooks.once('ready', () => { Hooks.once('ready', () => {
// Hide default countdown tracker via CSS (handled in countdown.css) // Hide default countdown tracker via CSS (handled in countdown.css)

View file

@ -214,6 +214,14 @@
/* Rotation handled inline to start at top */ /* Rotation handled inline to start at top */
} }
/* Grayscale Filter */
.grayscale-filter {
backdrop-filter: grayscale(100%) !important;
background: transparent !important;
opacity: 1 !important;
box-shadow: none !important;
}
.bar-visual.vertical { .bar-visual.vertical {
width: 100%; width: 100%;
/* height is set inline */ /* height is set inline */

View file

@ -38,41 +38,98 @@
../isMinimized}}data-tooltip="{{countdown.name}}" {{/if}}> ../isMinimized}}data-tooltip="{{countdown.name}}" {{/if}}>
<img src="{{countdown.img}}" class="countdown-icon" /> <img src="{{countdown.img}}" class="countdown-icon" />
{{#if ../showNumbers}} {{#if ../showVisuals}}
<div class="value-overlay number">
{{countdown.progress.current}}{{#unless ../isMinimized}}<span
class="max-value">/{{countdown.progress.start}}</span>{{/unless}}
</div>
{{else}}
<div class="value-overlay visual"> <div class="value-overlay visual">
{{#if ../enableVisualOverlay}} {{#if ../enableVisualOverlay}}
{{#if (eq ../fillType "grayscale")}}
{{!-- Grayscale Filter Mode --}}
{{#if (eq ../iconShape "circle")}} {{#if (eq ../iconShape "circle")}}
{{#if ../invertProgress}}
<div class="clock-visual grayscale-filter"
style="-webkit-mask-image: conic-gradient(transparent {{countdown.percentage}}%, black {{countdown.percentage}}%); mask-image: conic-gradient(transparent {{countdown.percentage}}%, black {{countdown.percentage}}%);">
</div>
{{else}}
<div class="clock-visual grayscale-filter"
style="-webkit-mask-image: conic-gradient(black {{countdown.percentage}}%, transparent {{countdown.percentage}}%); mask-image: conic-gradient(black {{countdown.percentage}}%, transparent {{countdown.percentage}}%);">
</div>
{{/if}}
{{else}}
{{#if ../invertProgress}}
<div class="bar-visual {{../barOrientation}} grayscale-filter"
style="{{#if (eq ../barOrientation 'horizontal')}}width: {{countdown.pctRemaining}}%; left: {{countdown.percentage}}%;{{else}}height: {{countdown.pctRemaining}}%; top: 0;{{/if}}">
</div>
{{else}}
<div class="bar-visual {{../barOrientation}} grayscale-filter"
style="{{#if (eq ../barOrientation 'horizontal')}}width: {{countdown.percentage}}%;{{else}}height: {{countdown.percentage}}%;{{/if}}">
</div>
{{/if}}
{{/if}}
{{else}}
{{!-- Color Overlay Mode --}}
{{#if (eq ../iconShape "circle")}}
{{#if ../invertProgress}}
<div class="clock-visual" <div class="clock-visual"
style="background: conic-gradient({{../visualColor}} {{countdown.percentage}}%, transparent {{countdown.percentage}}%); opacity: 0.4;"> style="background: conic-gradient(transparent {{countdown.percentage}}%, {{../fillColor}} {{countdown.percentage}}%); opacity: 0.4;">
</div>
{{else}}
<div class="clock-visual"
style="background: conic-gradient({{../fillColor}} {{countdown.percentage}}%, transparent {{countdown.percentage}}%); opacity: 0.4;">
</div>
{{/if}}
{{else}}
{{#if ../invertProgress}}
<div class="bar-visual {{../barOrientation}}"
style="background-color: {{../fillColor}}; opacity: 0.4; {{#if (eq ../barOrientation 'horizontal')}}width: {{countdown.pctRemaining}}%; left: {{countdown.percentage}}%;{{else}}height: {{countdown.pctRemaining}}%; top: 0;{{/if}}">
</div> </div>
{{else}} {{else}}
<div class="bar-visual {{../barOrientation}}" <div class="bar-visual {{../barOrientation}}"
style="background-color: {{../visualColor}}; opacity: 0.4; {{#if (eq ../barOrientation 'horizontal')}}width: {{countdown.percentage}}%;{{else}}height: {{countdown.percentage}}%;{{/if}}"> style="background-color: {{../fillColor}}; opacity: 0.4; {{#if (eq ../barOrientation 'horizontal')}}width: {{countdown.percentage}}%;{{else}}height: {{countdown.percentage}}%;{{/if}}">
</div> </div>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}}
{{/if}}
{{#if ../enableVisualBorder}} {{#if ../enableVisualBorder}}
<svg class="progress-border-svg" viewBox="0 0 48 48" style="transform: rotate(-90deg);"> <svg class="progress-border-svg" viewBox="0 0 48 48" style="transform: rotate(-90deg);">
{{#if (eq ../iconShape "circle")}} {{#if (eq ../iconShape "circle")}}
<circle cx="24" cy="24" r="23" fill="none" stroke="{{../visualColor}}" stroke-width="2" {{#if ../invertBorder}}
<circle cx="24" cy="24" r="23" fill="none" stroke="{{../borderColor}}" stroke-width="2"
pathLength="100" stroke-dasharray="{{countdown.pctRemaining}} 100"
stroke-dashoffset="-{{countdown.percentage}}"
style="transition: stroke-dasharray 0.3s ease, stroke-dashoffset 0.3s ease; opacity: 1;">
</circle>
{{else}}
<circle cx="24" cy="24" r="23" fill="none" stroke="{{../borderColor}}" stroke-width="2"
pathLength="100" stroke-dasharray="{{countdown.percentage}} 100" pathLength="100" stroke-dasharray="{{countdown.percentage}} 100"
style="transition: stroke-dasharray 0.3s ease; opacity: 1;"></circle> style="transition: stroke-dasharray 0.3s ease; opacity: 1;"></circle>
{{/if}}
{{else}}
{{#if ../invertBorder}}
<rect x="1" y="1" width="46" height="46" rx="8" ry="8" fill="none"
stroke="{{../borderColor}}" stroke-width="2" pathLength="100"
stroke-dasharray="{{countdown.pctRemaining}} 100"
stroke-dashoffset="-{{countdown.percentage}}"
style="transition: stroke-dasharray 0.3s ease, stroke-dashoffset 0.3s ease; opacity: 1;">
</rect>
{{else}} {{else}}
<rect x="1" y="1" width="46" height="46" rx="8" ry="8" fill="none" <rect x="1" y="1" width="46" height="46" rx="8" ry="8" fill="none"
stroke="{{../visualColor}}" stroke-width="2" pathLength="100" stroke="{{../borderColor}}" stroke-width="2" pathLength="100"
stroke-dasharray="{{countdown.percentage}} 100" stroke-dasharray="{{countdown.percentage}} 100"
style="transition: stroke-dasharray 0.3s ease; opacity: 1;"></rect> style="transition: stroke-dasharray 0.3s ease; opacity: 1;"></rect>
{{/if}} {{/if}}
{{/if}}
</svg> </svg>
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}
{{#if ../showNumbers}}
<div class="value-overlay number" style="color: {{../numberColor}};">
{{countdown.progress.current}}{{#unless ../isMinimized}}<span
class="max-value">/{{countdown.progress.start}}</span>{{/unless}}
</div>
{{/if}}
</div> </div>
{{#if countdown.editable}} {{#if countdown.editable}}