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", "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", 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 = '' + game.i18n.localize("COSMOS_DH_TWEAKS.Countdown.add") + ''; 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 ? (combatant.system.spotlight?.requesting || false) : (token.actor?.getFlag("cosmos-dh-tweaks", "requestingSpotlight") || 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 }); } } // 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) { 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 } }); } 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 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); } } }); // 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; } });