feat: add new improved, draggable countdown tracker module for Daggerheart.
This commit is contained in:
commit
08e2745b3a
6 changed files with 527 additions and 0 deletions
27
README.md
Normal file
27
README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Improved Countdowns
|
||||
|
||||
A modern, draggable countdown tracker for the Daggerheart system in Foundry VTT (v13+).
|
||||
|
||||
## Features
|
||||
|
||||
- **Modern UI**: A sleek, draggable window replacing the default system tracker.
|
||||
- **Minimized View**: Toggle between a full view with names and icons, or a compact minimized view to save space.
|
||||
- **Interactive Controls**:
|
||||
- **Increase/Decrease**: Hover over countdowns to see `+` and `-` buttons for quick updates.
|
||||
- **Add New**: GMs can add new countdowns directly from the tracker.
|
||||
- **Lock/Unlock**: Lock the tracker's position to prevent accidental drags.
|
||||
- **System Integration**: Fully compatible with the Daggerheart system's built-in countdown logic.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Draggable Handle**: Use the handle at the top of the tracker to reposition it on your canvas.
|
||||
2. **Locking**: Click the lock icon to freeze the tracker in place.
|
||||
3. **Minimizing**: Click the collapse icon to switch to the compact view.
|
||||
4. **Modifying Countdowns**: Hover over any countdown icon to reveal the increment/decrement controls.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the Manifest URL: `https://github.com/your-username/dh-improved-countdowns/releases/latest/download/module.json` (Update with actual URL).
|
||||
2. In Foundry VTT, go to the **Add-on Modules** tab.
|
||||
3. Click **Install Module** and paste the URL.
|
||||
|
||||
31
module.json
Normal file
31
module.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "dh-improved-countdowns",
|
||||
"title": "Improved Countdowns",
|
||||
"description": "A modern, draggable countdown tracker for the Daggerheart system.",
|
||||
"version": "1.0.0",
|
||||
"library": false,
|
||||
"url": "https://github.com/cptn-cosmo/dh-improved-countdowns",
|
||||
"manifest": "https://github.com/cptn-cosmo/dh-improved-countdowns/releases/latest/download/module.json",
|
||||
"download": "https://github.com/cptn-cosmo/dh-improved-countdowns/releases/download/1.0.0/dh-improved-countdowns.zip",
|
||||
"authors": [
|
||||
{
|
||||
"name": "CPTN Cosmo",
|
||||
"email": "cptncosmo@gmail.com",
|
||||
"url": "https://github.com/cptn-cosmo",
|
||||
"discord": "cptn_cosmo"
|
||||
}
|
||||
],
|
||||
"systems": [
|
||||
"daggerheart"
|
||||
],
|
||||
"esmodules": [
|
||||
"scripts/module.js"
|
||||
],
|
||||
"styles": [
|
||||
"styles/countdown.css"
|
||||
],
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "13"
|
||||
}
|
||||
}
|
||||
183
scripts/countdown-app.js
Normal file
183
scripts/countdown-app.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
export class CountdownTrackerApp 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-improved-countdowns-app",
|
||||
tag: "aside",
|
||||
classes: ["dh-improved-countdowns"],
|
||||
window: {
|
||||
frame: false,
|
||||
positioned: true,
|
||||
},
|
||||
position: {
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
},
|
||||
actions: {
|
||||
increaseCountdown: CountdownTrackerApp.#onIncrease,
|
||||
decreaseCountdown: CountdownTrackerApp.#onDecrease,
|
||||
addCountdown: CountdownTrackerApp.#onAdd,
|
||||
toggleViewMode: CountdownTrackerApp.#onToggleView,
|
||||
toggleLock: CountdownTrackerApp.#onToggleLock
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
content: {
|
||||
template: "modules/dh-improved-countdowns/templates/countdown-tracker.hbs",
|
||||
},
|
||||
};
|
||||
|
||||
static initialize() {
|
||||
this.instance = new CountdownTrackerApp();
|
||||
const pos = game.settings.get("dh-improved-countdowns", "position");
|
||||
this.instance.render(true, { position: pos });
|
||||
}
|
||||
|
||||
async _prepareContext(options) {
|
||||
const isGM = game.user.isGM;
|
||||
const isMinimized = game.settings.get("dh-improved-countdowns", "minimized");
|
||||
const isLocked = game.settings.get("dh-improved-countdowns", "locked");
|
||||
|
||||
// Fetch countdowns from system settings
|
||||
const systemCountdownSetting = game.settings.get("daggerheart", "countdowns");
|
||||
const countdowns = {};
|
||||
|
||||
if (systemCountdownSetting && systemCountdownSetting.countdowns) {
|
||||
for (const [id, countdown] of Object.entries(systemCountdownSetting.countdowns)) {
|
||||
const ownership = this.#getPlayerOwnership(game.user, systemCountdownSetting, countdown);
|
||||
if (ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
|
||||
countdowns[id] = {
|
||||
...countdown,
|
||||
editable: isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
countdowns,
|
||||
isGM,
|
||||
isMinimized,
|
||||
isLocked
|
||||
};
|
||||
}
|
||||
|
||||
#getPlayerOwnership(user, setting, countdown) {
|
||||
const playerOwnership = countdown.ownership[user.id];
|
||||
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
|
||||
? setting.defaultOwnership
|
||||
: playerOwnership;
|
||||
}
|
||||
|
||||
static async #onIncrease(event, target) {
|
||||
const id = target.dataset.id;
|
||||
Hooks.call("editCountdown", true, { id });
|
||||
// The Daggerheart system has a static method for this, let's try to use it if available
|
||||
if (typeof game.system.api.applications.ui.DhCountdowns?.editCountdown === "function") {
|
||||
await game.system.api.applications.ui.DhCountdowns.editCountdown(true, { id });
|
||||
}
|
||||
}
|
||||
|
||||
static async #onDecrease(event, target) {
|
||||
const id = target.dataset.id;
|
||||
if (typeof game.system.api.applications.ui.DhCountdowns?.editCountdown === "function") {
|
||||
await game.system.api.applications.ui.DhCountdowns.editCountdown(false, { id });
|
||||
}
|
||||
}
|
||||
|
||||
static async #onAdd(event, target) {
|
||||
if (!game.user.isGM) return;
|
||||
if (game.system.api.applications.ui.CountdownEdit) {
|
||||
new game.system.api.applications.ui.CountdownEdit().render(true);
|
||||
}
|
||||
}
|
||||
|
||||
static async #onToggleView(event, target) {
|
||||
const current = game.settings.get("dh-improved-countdowns", "minimized");
|
||||
await game.settings.set("dh-improved-countdowns", "minimized", !current);
|
||||
this.instance.render();
|
||||
}
|
||||
|
||||
static async #onToggleLock(event, target) {
|
||||
const current = game.settings.get("dh-improved-countdowns", "locked");
|
||||
await game.settings.set("dh-improved-countdowns", "locked", !current);
|
||||
this.instance.render();
|
||||
}
|
||||
|
||||
_onRender(context, options) {
|
||||
this.#setupDragging();
|
||||
}
|
||||
|
||||
#setupDragging() {
|
||||
if (game.settings.get("dh-improved-countdowns", "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';
|
||||
|
||||
window.addEventListener('mousemove', this.#onDragging.bind(this));
|
||||
window.addEventListener('mouseup', this.#onDragEnd.bind(this));
|
||||
}
|
||||
|
||||
#onDragging(e) {
|
||||
if (!this._dragData.isDragging) return;
|
||||
|
||||
const dx = e.clientX - this._dragData.startX;
|
||||
const dy = e.clientY - this._dragData.startY;
|
||||
|
||||
const newLeft = this._dragData.startLeft + dx;
|
||||
const newTop = this._dragData.startTop + dy;
|
||||
|
||||
this.element.style.left = `${newLeft}px`;
|
||||
this.element.style.top = `${newTop}px`;
|
||||
}
|
||||
|
||||
#onDragEnd() {
|
||||
if (!this._dragData.isDragging) return;
|
||||
this._dragData.isDragging = false;
|
||||
this.element.style.cursor = '';
|
||||
|
||||
window.removeEventListener('mousemove', this.#onDragging.bind(this));
|
||||
window.removeEventListener('mouseup', this.#onDragEnd.bind(this));
|
||||
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const pos = {
|
||||
top: rect.top,
|
||||
left: rect.left
|
||||
};
|
||||
|
||||
this.position.top = pos.top;
|
||||
this.position.left = pos.left;
|
||||
|
||||
game.settings.set("dh-improved-countdowns", "position", pos);
|
||||
}
|
||||
}
|
||||
52
scripts/module.js
Normal file
52
scripts/module.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { CountdownTrackerApp } from './countdown-app.js';
|
||||
|
||||
Hooks.once('init', () => {
|
||||
// Register settings for position, locked state, and minimized state
|
||||
game.settings.register("dh-improved-countdowns", "position", {
|
||||
name: "Tracker Position",
|
||||
scope: "client",
|
||||
config: false,
|
||||
type: Object,
|
||||
default: { top: 100, left: 100 }
|
||||
});
|
||||
|
||||
game.settings.register("dh-improved-countdowns", "locked", {
|
||||
name: "Lock Tracker Position",
|
||||
hint: "Prevents the countdown tracker from being dragged.",
|
||||
scope: "client",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
onChange: () => CountdownTrackerApp.instance?.render()
|
||||
});
|
||||
|
||||
game.settings.register("dh-improved-countdowns", "minimized", {
|
||||
name: "Minimized View",
|
||||
scope: "client",
|
||||
config: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
onChange: () => CountdownTrackerApp.instance?.render()
|
||||
});
|
||||
});
|
||||
|
||||
Hooks.once('ready', () => {
|
||||
// Hide default countdown tracker via CSS (handled in countdown.css)
|
||||
|
||||
// Initialize our improved tracker
|
||||
CountdownTrackerApp.initialize();
|
||||
});
|
||||
|
||||
// Re-render when countdowns change in the system
|
||||
Hooks.on('daggerheart.refresh', (data) => {
|
||||
if (data.refreshType === "countdown" || data.refreshType === 4) { // 4 is RefreshType.Countdown in Daggerheart
|
||||
CountdownTrackerApp.instance?.render();
|
||||
}
|
||||
});
|
||||
|
||||
// Generic socket refresh if system refresh doesn't catch everything
|
||||
Hooks.on('refresh', (data) => {
|
||||
if (data.refreshType === "countdown") {
|
||||
CountdownTrackerApp.instance?.render();
|
||||
}
|
||||
});
|
||||
183
styles/countdown.css
Normal file
183
styles/countdown.css
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/* Hide default Daggerheart countdown tracker */
|
||||
.daggerheart.countdowns {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Modern Countdown Tracker Application */
|
||||
.dh-improved-countdowns {
|
||||
pointer-events: none;
|
||||
/* Let clicks pass through to child elements */
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.countdown-tracker-window {
|
||||
pointer-events: all;
|
||||
background: rgba(20, 20, 25, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
color: #eee;
|
||||
font-family: 'Inter', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.countdown-tracker-window.minimized {
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.countdown-tracker-window:hover .tracker-header {
|
||||
opacity: 1;
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.countdown-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);
|
||||
}
|
||||
|
||||
/* Countdowns List */
|
||||
.countdowns-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.countdown-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.countdown-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-align: center;
|
||||
max-width: 150px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.countdown-visual {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.countdown-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.value-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.9), 0 0 8px rgba(0, 0, 0, 0.9);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* +/- Controls */
|
||||
.value-control {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
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;
|
||||
}
|
||||
|
||||
.countdown-item: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;
|
||||
}
|
||||
|
||||
/* No Countdowns State */
|
||||
.no-countdowns {
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
51
templates/countdown-tracker.hbs
Normal file
51
templates/countdown-tracker.hbs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<div class="countdown-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>
|
||||
<div class="header-controls">
|
||||
{{#if isGM}}
|
||||
<a class="control-btn" data-action="addCountdown" data-tooltip="Add New Countdown">
|
||||
<i class="fa-solid fa-plus-circle"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
<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>
|
||||
<a class="control-btn" data-action="toggleViewMode" data-tooltip="{{#if isMinimized}}Maximize{{else}}Minimize{{/if}}">
|
||||
<i class="fa-solid {{#if isMinimized}}fa-expand-alt{{else}}fa-compress-alt{{/if}}"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="countdowns-list">
|
||||
{{#each countdowns as | countdown id |}}
|
||||
<div class="countdown-item" data-id="{{id}}">
|
||||
{{#unless ../isMinimized}}
|
||||
<div class="countdown-name">{{countdown.name}}</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="countdown-visual">
|
||||
{{#if countdown.editable}}
|
||||
<a class="value-control minus" data-action="decreaseCountdown" data-id="{{id}}">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
<div class="icon-container" {{#if ../isMinimized}}data-tooltip="{{countdown.name}}"{{/if}}>
|
||||
<img src="{{countdown.img}}" class="countdown-icon" />
|
||||
<div class="value-overlay">{{countdown.progress.current}}</div>
|
||||
</div>
|
||||
|
||||
{{#if countdown.editable}}
|
||||
<a class="value-control plus" data-action="increaseCountdown" data-id="{{id}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="no-countdowns">No Active Countdowns</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue