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;
}
});