diff --git a/README.md b/README.md index cbf7e51..0121ba8 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,21 @@ A Foundry VTT v14 module that adds quality-of-life tweaks for the **Daggerheart* ### 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. +* **Works Everywhere**: Spotlight can be requested, granted, and taken both in and out of combat (an active encounter is not required). +* **For Players**: Allows requesting or cancelling a spotlight request for their character. Out-of-combat requests are saved securely on the character actor. +* **For GMs**: Allows granting spotlight to or removing spotlight from any token. Toggling spotlight on a token will automatically clear its active spotlight request. +* **Real-time Updates**: The HUD button automatically reflects status changes in real-time if updated via the combat tracker, canvas settings, or other players. -### 2. Minimal Countdown UI (On by default) +### 2. Spotlight Request Canvas Indicator (On by default) +Displays a visual indicator on the canvas to make managing spotlight requests easier for the GM: +* **For GMs**: Shows a small, gold hand-sparkles badge overlay in the top-right corner of any token that is currently requesting spotlight. +* **Canvas Synchronization**: Instantly appears or disappears as players toggle their spotlight requests. + +### 3. 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**. +All features can be toggled on or off independently under **Configure Settings** > **Cosmo's Daggerheart Tweaks**. The *Spotlight Request Canvas Indicator* is grouped together with the HUD button option. diff --git a/lang/en.json b/lang/en.json index 5034839..2f7b71d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,12 +1,13 @@ { "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.SpotlightRequestIndicator.Name": "Spotlight Request Canvas Indicator", + "COSMOS_DH_TWEAKS.Settings.SpotlightRequestIndicator.Hint": "For GMs, display a small hand icon overlay on tokens that are requesting spotlight.", "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 index d42c109..b86b62e 100644 --- a/module.json +++ b/module.json @@ -2,10 +2,10 @@ "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.3", + "version": "1.1.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.3/cosmos-dh-tweaks.zip", + "download": "https://git.geeks.gay/cosmo/cosmos-dh-tweaks/releases/download/1.1.0/cosmos-dh-tweaks.zip", "compatibility": { "minimum": "14.000", "verified": "14.364" diff --git a/scripts/cosmos-dh-tweaks.mjs b/scripts/cosmos-dh-tweaks.mjs index 658f467..385c295 100644 --- a/scripts/cosmos-dh-tweaks.mjs +++ b/scripts/cosmos-dh-tweaks.mjs @@ -9,6 +9,23 @@ Hooks.once("init", () => { default: true }); + game.settings.register("cosmos-dh-tweaks", "spotlightRequestIndicator", { + name: "COSMOS_DH_TWEAKS.Settings.SpotlightRequestIndicator.Name", + hint: "COSMOS_DH_TWEAKS.Settings.SpotlightRequestIndicator.Hint", + scope: "world", + config: true, + type: Boolean, + default: true, + onChange: () => { + if (canvas.ready) { + for (const token of canvas.tokens.placeables) { + if (token.refresh) token.refresh(); + else if (token.renderFlags) token.renderFlags.set({ refreshState: true }); + } + } + } + }); + game.settings.register("cosmos-dh-tweaks", "minimalCountdownUI", { name: "COSMOS_DH_TWEAKS.Settings.MinimalCountdownUI.Name", hint: "COSMOS_DH_TWEAKS.Settings.MinimalCountdownUI.Hint", @@ -97,7 +114,9 @@ Hooks.on("renderTokenHUD", (tokenHUD, html, tokenData) => { // Players: only proceed if they own the token if (!token.document.isOwner) return; const combatant = token.combatant; - active = combatant?.system.spotlight?.requesting || false; + active = combatant + ? (combatant.system.spotlight?.requesting || false) + : (token.actor?.getFlag("cosmos-dh-tweaks", "requestingSpotlight") || false); } const title = isGM @@ -154,24 +173,31 @@ Hooks.on("renderTokenHUD", (tokenHUD, html, tokenData) => { token.renderFlags.set({ refreshTurnMarker: true }); } } + + // Clear spotlight request indicator if GM grants/takes spotlight + if (token.actor?.getFlag("cosmos-dh-tweaks", "requestingSpotlight")) { + await token.actor.setFlag("cosmos-dh-tweaks", "requestingSpotlight", false); + } } 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); + if (combatant) { + 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 - } - }); + await combatant.update({ + 'system.spotlight': { + requesting: !isRequesting, + requestOrderIndex: !isRequesting ? maxRequestIndex + 1 : 0 + } + }); + } else { + // Out of combat request + const isRequesting = !!token.actor?.getFlag("cosmos-dh-tweaks", "requestingSpotlight"); + await token.actor?.setFlag("cosmos-dh-tweaks", "requestingSpotlight", !isRequesting); + } } // Rerender HUD to update icon state @@ -206,3 +232,96 @@ Hooks.on("updateSetting", (setting, change, options, userId) => { } } }); + +// Trigger token refresh when Actor changes their requestingSpotlight flag +Hooks.on("updateActor", (actor, change, options, userId) => { + if (!game.ready) return; + const tokens = actor.getActiveTokens(); + for (const token of tokens) { + if (token.refresh) token.refresh(); + else if (token.renderFlags) token.renderFlags.set({ refreshState: true }); + } + if (canvas.tokens.hud.rendered && canvas.tokens.hud.object?.actor?.id === actor.id) { + canvas.tokens.hud.render(true); + } +}); + +// Trigger token refresh when Token changes +Hooks.on("updateToken", (tokenDoc, change, options, userId) => { + if (!game.ready) return; + const token = tokenDoc.object; + if (token) { + if (token.refresh) token.refresh(); + else if (token.renderFlags) token.renderFlags.set({ refreshState: true }); + } + if (canvas.tokens.hud.rendered && canvas.tokens.hud.object?.id === tokenDoc.id) { + canvas.tokens.hud.render(true); + } +}); + +// Render the GM's spotlight request indicator on the token canvas +Hooks.on("refreshToken", (token, options) => { + if (!game.ready) return; + if (!game.user?.isGM) return; + if (!game.settings.get("cosmos-dh-tweaks", "tokenHudSpotlight")) return; + if (!game.settings.get("cosmos-dh-tweaks", "spotlightRequestIndicator")) { + if (token.cosmoSpotlightIndicator) { + token.cosmoSpotlightIndicator.visible = false; + } + return; + } + + const inCombat = !!token.combatant; + const requestingInCombat = token.combatant?.system.spotlight?.requesting; + const requestingOutOfCombat = token.actor?.getFlag("cosmos-dh-tweaks", "requestingSpotlight"); + const isRequesting = inCombat ? requestingInCombat : requestingOutOfCombat; + + if (isRequesting) { + // Safe check to reconstruct if it was destroyed or detached + const isDestroyed = token.cosmoSpotlightIndicator && (token.cosmoSpotlightIndicator.destroyed || !token.children.includes(token.cosmoSpotlightIndicator)); + if (isDestroyed) { + token.cosmoSpotlightIndicator = null; + } + + if (!token.cosmoSpotlightIndicator) { + const container = new PIXI.Container(); + container.name = "cosmoSpotlightIndicator"; + + // Draw a background circle (gold) + const bg = new PIXI.Graphics(); + bg.beginFill(0xc5a45e, 0.9); + bg.lineStyle(1.5, 0xffffff, 1); + bg.drawCircle(0, 0, 11); + bg.endFill(); + container.addChild(bg); + + // Draw FontAwesome solid e05d hand sparkles icon + const style = new PIXI.TextStyle({ + fontFamily: '"Font Awesome 6 Free"', + fontWeight: "900", + fontSize: 13, + fill: "#ffffff" + }); + const text = new PIXI.Text("\ue05d", style); + text.anchor.set(0.5); + container.addChild(text); + + token.cosmoSpotlightIndicator = token.addChild(container); + } + + // Position the indicator at the top-right of the token container + const size = token.w || 50; + token.cosmoSpotlightIndicator.position.set(size - 10, 10); + token.cosmoSpotlightIndicator.visible = true; + } else { + if (token.cosmoSpotlightIndicator) { + token.cosmoSpotlightIndicator.visible = false; + } + } +}); + +Hooks.on("destroyToken", (token) => { + if (token.cosmoSpotlightIndicator) { + token.cosmoSpotlightIndicator = null; + } +});