From 8e3b991518717eb2ce99f5ae28df1d7206cc8f9a Mon Sep 17 00:00:00 2001 From: cosmo Date: Fri, 10 Jul 2026 20:38:16 +0200 Subject: [PATCH] initial commit --- README.md | 20 ++++ lang/en.json | 12 ++ module.json | 39 +++++++ scripts/cosmos-dh-tweaks.mjs | 208 +++++++++++++++++++++++++++++++++++ styles/cosmos-dh-tweaks.css | 71 ++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 README.md create mode 100644 lang/en.json create mode 100644 module.json create mode 100644 scripts/cosmos-dh-tweaks.mjs create mode 100644 styles/cosmos-dh-tweaks.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbf7e51 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Cosmo's Daggerheart Tweaks + +A Foundry VTT v14 module that adds quality-of-life tweaks for the **Daggerheart** system. + +## Features + +### 1. Token HUD Spotlight (On by default) +Adds a new Spotlight button directly to the default Token HUD menu: +* **For Players**: Allows requesting or cancelling a spotlight request for their character. +* **For GMs**: Allows granting spotlight to or removing spotlight from any token. +* **Real-time Updates**: The HUD button automatically reflects status changes in real-time if updated via the combat tracker or other canvas actions. + +### 2. Minimal Countdown UI (On by default) +Reduces screen clutter by optimizing the Countdown Tracker widget: +* **For Players**: Hides the countdown widget completely if there are no active/visible countdowns. +* **For GMs**: Minimizes the countdown widget to a tiny square featuring a `+` icon when empty. Clicking the `+` icon opens the system's native Countdown Edit dialog. + +## Configuration + +Both features can be toggled on or off independently under **Configure Settings** > **Cosmo's Daggerheart Tweaks**. diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..5034839 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,12 @@ +{ + "COSMOS_DH_TWEAKS.Settings.TokenHudSpotlight.Name": "Token HUD Spotlight Button", + "COSMOS_DH_TWEAKS.Settings.TokenHudSpotlight.Hint": "Add a Spotlight button to the Token HUD. Players can request/cancel spotlight, while GMs can grant/take it.", + "COSMOS_DH_TWEAKS.Settings.MinimalCountdownUI.Name": "Minimal Countdown UI", + "COSMOS_DH_TWEAKS.Settings.MinimalCountdownUI.Hint": "When no countdowns are active, hides the UI completely for players and minimizes it to a '+' button for GMs.", + "COSMOS_DH_TWEAKS.Spotlight.request": "Request Spotlight", + "COSMOS_DH_TWEAKS.Spotlight.cancel": "Cancel Spotlight Request", + "COSMOS_DH_TWEAKS.Spotlight.grant": "Grant Spotlight", + "COSMOS_DH_TWEAKS.Spotlight.take": "Take Spotlight", + "COSMOS_DH_TWEAKS.Spotlight.warnNoCombatant": "This token is not in combat. You can only request spotlight in combat.", + "COSMOS_DH_TWEAKS.Countdown.add": "Add Countdown" +} diff --git a/module.json b/module.json new file mode 100644 index 0000000..1f1cf55 --- /dev/null +++ b/module.json @@ -0,0 +1,39 @@ +{ + "id": "cosmos-dh-tweaks", + "title": "Cosmo's Daggerheart Tweaks", + "description": "A selection of quality of life tweaks for the Daggerheart system on Foundry VTT.", + "version": "1.0.0", + "url": "https://git.geeks.gay/cosmo/cosmos-dh-tweaks", + "manifest": "https://git.geeks.gay/cosmo/cosmos-dh-tweaks/raw/branch/main/module.json", + "download": "https://git.geeks.gay/cosmo/cosmos-dh-tweaks/releases/download/1.0.0/cosmos-dh-tweaks.zip", + "compatibility": { + "minimum": "14.000", + "verified": "14.364" + }, + "authors": [ + { + "name": "Cosmo" + } + ], + "relationships": { + "systems": [ + { + "id": "daggerheart", + "type": "system" + } + ] + }, + "esmodules": [ + "scripts/cosmos-dh-tweaks.mjs" + ], + "styles": [ + "styles/cosmos-dh-tweaks.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ] +} diff --git a/scripts/cosmos-dh-tweaks.mjs b/scripts/cosmos-dh-tweaks.mjs new file mode 100644 index 0000000..3d110ce --- /dev/null +++ b/scripts/cosmos-dh-tweaks.mjs @@ -0,0 +1,208 @@ +Hooks.once("init", () => { + // Register settings + game.settings.register("cosmos-dh-tweaks", "tokenHudSpotlight", { + name: "COSMOS_DH_TWEAKS.Settings.TokenHudSpotlight.Name", + hint: "COSMOS_DH_TWEAKS.Settings.TokenHudSpotlight.Hint", + scope: "world", + config: true, + type: Boolean, + default: true + }); + + game.settings.register("cosmos-dh-tweaks", "minimalCountdownUI", { + name: "COSMOS_DH_TWEAKS.Settings.MinimalCountdownUI.Name", + hint: "COSMOS_DH_TWEAKS.Settings.MinimalCountdownUI.Hint", + scope: "world", + config: true, + type: Boolean, + default: true, + onChange: () => { + if (ui.countdowns) ui.countdowns.render(); + } + }); + + // Override the Countdown class in CONFIG + const OriginalCountdowns = CONFIG.ui.countdowns; + if (OriginalCountdowns) { + class CosmoDhTweaksCountdowns extends OriginalCountdowns { + async _onRender(context, options) { + await super._onRender(context, options); + + if (!game.settings.get("cosmos-dh-tweaks", "minimalCountdownUI")) { + this.element.classList.remove("cosmo-minimal-countdown"); + const addBtn = this.element.querySelector(".cosmo-add-countdown"); + if (addBtn) addBtn.remove(); + return; + } + + // Check if there are active countdowns using public API + const countdownData = this._getCountdownData(); + let count = 0; + for (const key of Object.keys(countdownData)) { + count += Object.keys(countdownData[key]).length; + } + const hasCountdowns = count > 0; + + if (game.user.isGM) { + if (!hasCountdowns) { + this.element.classList.add("cosmo-minimal-countdown"); + let addBtn = this.element.querySelector(".cosmo-add-countdown"); + if (!addBtn) { + const header = this.element.querySelector("header.countdowns-header"); + if (header) { + addBtn = document.createElement("a"); + addBtn.className = "cosmo-add-countdown"; + addBtn.setAttribute("data-tooltip", game.i18n.localize("COSMOS_DH_TWEAKS.Countdown.add")); + addBtn.innerHTML = ''; + addBtn.addEventListener("click", (e) => { + e.preventDefault(); + new game.system.api.applications.ui.CountdownEdit().render(true); + }); + header.appendChild(addBtn); + } + } + } else { + this.element.classList.remove("cosmo-minimal-countdown"); + const addBtn = this.element.querySelector(".cosmo-add-countdown"); + if (addBtn) addBtn.remove(); + } + } else { + // For players: if there are no countdowns, hide the UI completely. + if (!hasCountdowns) { + this.element.hidden = true; + } + } + } + } + CONFIG.ui.countdowns = CosmoDhTweaksCountdowns; + } +}); + +Hooks.on("renderTokenHUD", (tokenHUD, html, tokenData) => { + if (!game.settings.get("cosmos-dh-tweaks", "tokenHudSpotlight")) return; + + const token = tokenHUD.object; + if (!token) return; + + const isGM = game.user.isGM; + let active = false; + + if (isGM) { + // Determine if currently spotlighted + const isSpotlighted = game.combat + ? (game.combat.combatant?.tokenId === token.id) + : game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker)?.spotlightedTokens.has(token.document.uuid); + active = isSpotlighted; + } else { + // Players: only proceed if they own the token + if (!token.document.isOwner) return; + const combatant = token.combatant; + active = combatant?.system.spotlight?.requesting || false; + } + + const title = isGM + ? (active ? game.i18n.localize("COSMOS_DH_TWEAKS.Spotlight.take") : game.i18n.localize("COSMOS_DH_TWEAKS.Spotlight.grant")) + : (active ? game.i18n.localize("COSMOS_DH_TWEAKS.Spotlight.cancel") : game.i18n.localize("COSMOS_DH_TWEAKS.Spotlight.request")); + + const activeClass = active ? "active" : ""; + const iconClass = active ? "fa-hand-sparkles" : "fa-hand"; + + // Build the button element + const button = document.createElement("div"); + button.className = `control-icon cosmo-spotlight-hud-btn ${activeClass}`; + button.setAttribute("title", title); + button.innerHTML = ``; + + button.addEventListener("click", async (event) => { + event.preventDefault(); + + if (isGM) { + // Trigger GM spotlight toggle + if (game.system.api?.macros?.spotlightCombatant) { + await game.system.api.macros.spotlightCombatant(token); + } else { + // Fallback manual update if macro is missing + const combatantCombat = token.combatant + ? game.combat + : game.combats.find(combat => combat.combatants.some(x => x.token && x.token.id === token.document.id)); + if (combatantCombat) { + const combatant = combatantCombat.combatants.find(x => x.token.id === token.document.id); + if (!combatantCombat.active) { + await combatantCombat.activate(); + if (combatantCombat.combatant?.id !== combatant.id) ui.combat.setCombatantSpotlight(combatant.id); + } else { + ui.combat.setCombatantSpotlight(combatant.id); + } + } else { + if (game.combat) await ui.combat.clearTurn(); + const spotlightTracker = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker); + const isSpotlighted = spotlightTracker.spotlightedTokens.has(token.document.uuid); + if (!isSpotlighted) { + // Clear previous spotlight manually + const previouslySpotlightedUuid = spotlightTracker.spotlightedTokens.size > 0 ? spotlightTracker.spotlightedTokens.first() : null; + if (previouslySpotlightedUuid) { + spotlightTracker.updateSource({ spotlightedTokens: [] }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker, spotlightTracker); + const previousToken = await foundry.utils.fromUuid(previouslySpotlightedUuid); + if (previousToken?.object) previousToken.object.renderFlags.set({ refreshTurnMarker: true }); + } + } + spotlightTracker.updateSource({ + spotlightedTokens: isSpotlighted ? [] : [token.document.uuid] + }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker, spotlightTracker); + token.renderFlags.set({ refreshTurnMarker: true }); + } + } + } else { + // Player requesting / cancelling spotlight + const combatant = token.combatant; + if (!combatant) { + ui.notifications.warn(game.i18n.localize("COSMOS_DH_TWEAKS.Spotlight.warnNoCombatant")); + return; + } + const isRequesting = combatant.system.spotlight.requesting; + const characters = game.combat.turns?.filter(x => !x.isNPC) ?? []; + const orderValues = characters.map(character => character.system.spotlight.requestOrderIndex || 0); + const maxRequestIndex = Math.max(...orderValues, 0); + + await combatant.update({ + 'system.spotlight': { + requesting: !isRequesting, + requestOrderIndex: !isRequesting ? maxRequestIndex + 1 : 0 + } + }); + } + + // Rerender HUD to update icon state + tokenHUD.render(true); + }); + + // In Foundry, HUD HTML can be jQuery. Find col.left and append. + // Supports both jQuery and HTMLElement. + const leftCol = html.find ? html.find('.col.left') : html.querySelector('.col.left'); + if (leftCol && leftCol.append) { + leftCol.append(button); + } else if (leftCol) { + leftCol.appendChild(button); + } +}); + +// Re-render Token HUD if open and its token is updated in combatant spotlight +Hooks.on("updateCombatant", (combatant, change, options, userId) => { + if (!game.settings.get("cosmos-dh-tweaks", "tokenHudSpotlight")) return; + if (canvas.tokens.hud.rendered && canvas.tokens.hud.object?.id === combatant.tokenId) { + canvas.tokens.hud.render(true); + } +}); + +// Re-render Token HUD if out-of-combat spotlight settings change +Hooks.on("updateSetting", (setting, change, options, userId) => { + if (!game.settings.get("cosmos-dh-tweaks", "tokenHudSpotlight")) return; + const key = setting.key || setting.id; + if (key === "daggerheart.SpotlightTracker" || key === `${CONFIG.DH.id}.SpotlightTracker`) { + if (canvas.tokens.hud.rendered) { + canvas.tokens.hud.render(true); + } + } +}); diff --git a/styles/cosmos-dh-tweaks.css b/styles/cosmos-dh-tweaks.css new file mode 100644 index 0000000..39486a5 --- /dev/null +++ b/styles/cosmos-dh-tweaks.css @@ -0,0 +1,71 @@ +/* Minimal Countdown UI styles */ +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown { + width: 36px !important; + height: 36px !important; + min-width: 36px !important; + min-height: 36px !important; + padding: 0 !important; + margin: 0 !important; + border-radius: 4px; + align-self: flex-end; + overflow: hidden; +} + +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown::before { + opacity: 1 !important; +} + +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown .countdowns-header { + display: flex !important; + width: 36px !important; + height: 36px !important; + flex: 0 0 36px !important; + padding: 0 !important; + margin: 0 !important; + align-items: center; + justify-content: center; + border: none !important; + background: transparent !important; +} + +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown .countdowns-header > *:not(.cosmo-add-countdown) { + display: none !important; +} + +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown .countdowns-container { + display: none !important; +} + +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown .cosmo-add-countdown { + display: flex !important; + align-items: center; + justify-content: center; + width: 36px !important; + height: 36px !important; + font-size: 16px !important; + color: var(--golden, #c5a45e) !important; + cursor: pointer; + transition: color 0.2s ease, transform 0.2s ease, text-shadow 0.2s ease; +} + +.daggerheart.dh-style.countdowns.cosmo-minimal-countdown .cosmo-add-countdown:hover { + color: #ffffff !important; + transform: scale(1.15); + text-shadow: 0 0 5px var(--golden, #c5a45e) !important; +} + +/* Token HUD Spotlight Button styling */ +.control-icon.cosmo-spotlight-hud-btn { + transition: color 0.2s ease, text-shadow 0.2s ease, background-color 0.2s ease; +} + +.control-icon.cosmo-spotlight-hud-btn:hover { + color: #ffffff; +} + +.control-icon.cosmo-spotlight-hud-btn.active { + color: var(--golden, #c5a45e) !important; + text-shadow: 0 0 8px var(--golden, #c5a45e) !important; + border: 1px solid var(--golden, #c5a45e) !important; + box-shadow: 0 0 5px rgba(197, 164, 94, 0.5) !important; +}