console.log("DH Stream Overlay | Module Loaded (Rewrite v1.0)"); // ============================================================================= // CRASH PROTECTION & STUBS // ============================================================================= // The Daggerheart system crashes on the /stream view because it expects // standard UI elements (ui.combat, canvas) to verify existence. if (window.location.pathname.includes("/stream")) { console.log("DH Stream Overlay | Stream View Detected - Applying Stubs"); if (typeof window.ui === "undefined") window.ui = {}; // Stub ui.combat if (!window.ui.combat) { window.ui.combat = { render: () => { }, apps: [], viewed: null, active: false }; } // Stub ui.notifications if (!window.ui.notifications) { window.ui.notifications = { active: [], warn: (m) => console.warn(m), error: (m) => console.error(m), info: (m) => console.log(m) }; } } // Fallback Canvas Stub if (typeof canvas === "undefined" || !canvas) { window.canvas = { tokens: { placeables: [], get: () => null }, grid: { size: 100 }, ready: true, // DSN requires ready=true dimensions: { width: 1920, height: 1080 } }; } // ============================================================================= // INITIALIZATION // ============================================================================= Hooks.once("init", () => { // Register Settings game.settings.register("dh-stream-overlay", "playerNameDisplay", { name: "DH_STREAM_OVERLAY.Settings.PlayerNameDisplay", hint: "DH_STREAM_OVERLAY.Settings.PlayerNameDisplayHint", scope: "world", config: true, type: String, choices: { "char-top": "DH_STREAM_OVERLAY.Settings.Choices.CharTop", "player-top": "DH_STREAM_OVERLAY.Settings.Choices.PlayerTop", "no-player": "DH_STREAM_OVERLAY.Settings.Choices.NoPlayer" }, default: "char-top", onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showResourceLabels", { name: "DH_STREAM_OVERLAY.Settings.ShowResourceLabels", hint: "DH_STREAM_OVERLAY.Settings.ShowResourceLabelsHint", scope: "world", config: true, type: Boolean, default: false, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showFullAttrNames", { name: "DH_STREAM_OVERLAY.Settings.ShowFullAttrNames", hint: "DH_STREAM_OVERLAY.Settings.ShowFullAttrNamesHint", scope: "world", config: true, type: Boolean, default: false, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "resourceDisplayMode", { name: "DH_STREAM_OVERLAY.Settings.ResourceDisplayMode", hint: "DH_STREAM_OVERLAY.Settings.ResourceDisplayModeHint", scope: "world", config: true, type: String, choices: { "numeric": "DH_STREAM_OVERLAY.ModeNumeric", "pips": "DH_STREAM_OVERLAY.ModePips" }, default: "numeric", onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showResourceIcons", { name: "DH_STREAM_OVERLAY.Settings.ShowResourceIcons", hint: "DH_STREAM_OVERLAY.Settings.ShowResourceIconsHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showResources", { name: "DH_STREAM_OVERLAY.Settings.ShowResources", hint: "DH_STREAM_OVERLAY.Settings.ShowResourcesHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showAttributes", { name: "DH_STREAM_OVERLAY.Settings.ShowAttributes", hint: "DH_STREAM_OVERLAY.Settings.ShowAttributesHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showClass", { name: "DH_STREAM_OVERLAY.Settings.ShowClass", hint: "DH_STREAM_OVERLAY.Settings.ShowClassHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showSubclass", { name: "DH_STREAM_OVERLAY.Settings.ShowSubclass", hint: "DH_STREAM_OVERLAY.Settings.ShowSubclassHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "useGreenScreen", { name: "DH_STREAM_OVERLAY.Settings.UseGreenScreen", hint: "DH_STREAM_OVERLAY.Settings.UseGreenScreenHint", scope: "world", config: true, type: Boolean, default: false, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "hideFearLabel", { name: "DH_STREAM_OVERLAY.Settings.HideFearLabel", hint: "DH_STREAM_OVERLAY.Settings.HideFearLabelHint", scope: "world", config: true, type: Boolean, default: false, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showAncestry", { name: "DH_STREAM_OVERLAY.Settings.ShowAncestry", hint: "DH_STREAM_OVERLAY.Settings.ShowAncestryHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "showCommunity", { name: "DH_STREAM_OVERLAY.Settings.ShowCommunity", hint: "DH_STREAM_OVERLAY.Settings.ShowCommunityHint", scope: "world", config: true, type: Boolean, default: true, onChange: () => { if (document.body.classList.contains("stream")) location.reload(); } }); game.settings.register("dh-stream-overlay", "partyActorUuid", { scope: "world", config: false, type: String, default: "" }); game.settings.register("dh-stream-overlay", "enableDSN", { scope: "world", config: false, type: Boolean, default: true }); }); // ... // ... inside renderPartyOverlay function ... // ============================================================================= // RENDERER FUNCTION // ============================================================================= // ============================================================================= // RENDERER FUNCTION // ============================================================================= // ============================================================================= // RENDERER FUNCTION // ============================================================================= function renderFearTracker(container) { // If container is not provided, look for it. If not found, do nothing (should be created in init) if (!container) container = document.getElementById("stream-fear-container"); if (!container) return; // --------------------------------------------------------- // 1. GATHER DATA // --------------------------------------------------------- let currentFear = 0; let maxFear = 6; try { currentFear = game.settings.get("daggerheart", "ResourcesFear") || 0; const homebrew = game.settings.get("daggerheart", "Homebrew"); if (homebrew?.maxFear) maxFear = homebrew.maxFear; } catch (e) { } // --------------------------------------------------------- // 2. GATHER SETTINGS & THEMES // --------------------------------------------------------- const settings = { iconShape: "circle", iconType: "preset", presetIcon: "fas fa-skull", customIcon: "fas fa-skull", customSvgPath: "icons/svg/mystery-man.svg", iconColor: "#ffffff", colorTheme: "foundryborne", // Updated Default fullColor: "#cf0000" }; // Attempt to read from module if active if (game.modules.get("dh-feartrackerplus")?.active) { try { settings.iconShape = game.settings.get("dh-feartrackerplus", "iconShape"); settings.iconType = game.settings.get("dh-feartrackerplus", "iconType"); settings.presetIcon = game.settings.get("dh-feartrackerplus", "presetIcon"); settings.customIcon = game.settings.get("dh-feartrackerplus", "customIcon"); settings.customSvgPath = game.settings.get("dh-feartrackerplus", "customSvgPath"); settings.iconColor = game.settings.get("dh-feartrackerplus", "iconColor"); settings.colorTheme = game.settings.get("dh-feartrackerplus", "colorTheme"); settings.fullColor = game.settings.get("dh-feartrackerplus", "fullColor"); } catch (e) { console.warn("Stream Overlay | Error reading FearTrackerPlus settings", e); } } // --------------------------------------------------------- // 3. HELPER FUNCTIONS (adapted from dh-feartrackerplus) // --------------------------------------------------------- const getIconHtml = (index) => { let iconClass = 'fas fa-skull'; if (settings.iconType === 'preset') iconClass = settings.presetIcon; else if (settings.iconType === 'custom') iconClass = settings.customIcon; else if (settings.iconType === 'custom-svg') { return ``; } if (iconClass.includes('.svg')) { return ``; } let style = ""; // Default to white if fallback let iColor = settings.iconColor || '#ffffff'; if (iColor !== '#ffffff') { if (iColor.includes('gradient')) { style = `background: ${iColor}; -webkit-background-clip: text; background-clip: text; color: transparent;`; } else { style = `color: ${iColor};`; } } return ``; }; const getTokenStyle = (index, total, isActive) => { if (!isActive) return `background: #252529; border-color: rgba(255,255,255,0.05); box-shadow: inset 0 0 5px rgba(0,0,0,0.8);`; let background = '#cf0000'; // Fallback if (settings.colorTheme === 'custom') { if (settings.fullColor.includes('gradient')) { const posPercent = total > 1 ? (index / (total - 1)) * 100 : 0; background = `${settings.fullColor} no-repeat; background-size: ${total * 100}% 100%; background-position: ${posPercent}% 0%; background-attachment: scroll; background-origin: border-box;`; } else { background = settings.fullColor; } } else { const themes = { 'hope-fear': { start: '#FFC107', end: '#512DA8' }, 'blood-moon': { start: '#5c0000', end: '#ff0000' }, 'ethereal': { start: '#00FFFF', end: '#0000FF' }, 'toxic': { start: '#00FF00', end: '#FFFF00' }, 'foundryborne': { start: '#0a388c', end: '#791d7d' } }; const t = themes[settings.colorTheme] || themes['blood-moon']; // Gradient Logic const gradient = `linear-gradient(90deg, ${t.start}, ${t.end})`; const posPercent = total > 1 ? (index / (total - 1)) * 100 : 0; // Note: We use fixed attachment or careful sizing to simulate the continuous gradient across tokens // Simplified approach: separate gradient for each or huge gradient // The method from the module uses manual calculation background = `${gradient} no-repeat; background-size: ${total * 100}% 100%; background-position: ${posPercent}% 0%;`; } return `background: ${background}; border-color: rgba(255,255,255,0.4); box-shadow: 0 0 10px rgba(0,0,0,0.5);`; }; // --------------------------------------------------------- // 4. GENERATE HTML // --------------------------------------------------------- // Check if label should be hidden const hideLabel = game.settings.get("dh-stream-overlay", "hideFearLabel"); let html = ` ${hideLabel ? '' : '
Fear
'}
`; for (let i = 0; i < maxFear; i++) { const isActive = i < currentFear; const style = getTokenStyle(i, maxFear, isActive); const icon = getIconHtml(i); // Classes let classes = `fear-token ${settings.iconShape}`; if (isActive) classes += " active"; html += `
${icon}
`; } html += `
`; container.innerHTML = html; } function renderPartyOverlay(container) { if (!container) return; // Find Players & GM const players = game.users.filter(u => u.hasPlayerOwner && u.character); const gms = game.users.filter(u => u.isGM); if (players.length === 0 && gms.length === 0) { container.innerHTML = `
No Active Players or GM Found
`; return; } let html = `
`; // 1. Render GM(s) gms.forEach(gm => { const img = gm.avatar || "icons/svg/mystery-man.svg"; const name = gm.name; // GM just uses their user name usually html += `
Game Master
${name}
`; }); // 2. Render Players // Check setting availability let nameMode = "char-top"; let showResLabels = false; let showFullAttr = false; let resMode = "numeric"; let showIcons = true; let showRes = true; let showAttr = true; let showClass = true; let showSubclass = true; let showAncestry = true; let showCommunity = true; try { nameMode = game.settings.get("dh-stream-overlay", "playerNameDisplay"); showResLabels = game.settings.get("dh-stream-overlay", "showResourceLabels"); showFullAttr = game.settings.get("dh-stream-overlay", "showFullAttrNames"); resMode = game.settings.get("dh-stream-overlay", "resourceDisplayMode"); showIcons = game.settings.get("dh-stream-overlay", "showResourceIcons"); showRes = game.settings.get("dh-stream-overlay", "showResources"); showAttr = game.settings.get("dh-stream-overlay", "showAttributes"); showClass = game.settings.get("dh-stream-overlay", "showClass"); showSubclass = game.settings.get("dh-stream-overlay", "showSubclass"); showAncestry = game.settings.get("dh-stream-overlay", "showAncestry"); showCommunity = game.settings.get("dh-stream-overlay", "showCommunity"); } catch (e) { console.warn("DH OVERLAY: Settings not ready yet, using default"); } const renderPips = (val, max, type) => { let h = `
`; for (let i = 1; i <= max; i++) { h += `
`; } h += `
`; return h; }; players.forEach(user => { const actor = user.character; if (!actor) return; const system = actor.system || {}; const items = actor.items; // --- DATA MAPPING --- const name = actor.name; const img = actor.img || "icons/svg/mystery-man.svg"; // Find Items const findItem = (type) => items.find(i => i.type.toLowerCase() === type.toLowerCase()); const classItem = findItem("class"); const subclassItem = findItem("subclass"); const ancestryItem = findItem("ancestry"); const communityItem = findItem("community"); const className = classItem ? classItem.name : "No Class"; const subclassName = subclassItem ? subclassItem.name : ""; const ancestryName = ancestryItem ? ancestryItem.name : "No Ancestry"; const communityName = communityItem ? communityItem.name : ""; // Construct Meta Strings // Ancestry Line: "Community Ancestry" let metaAncestry = ""; if (showCommunity && communityName) metaAncestry += communityName; if (showCommunity && communityName && showAncestry && ancestryName) metaAncestry += " "; if (showAncestry && ancestryName) metaAncestry += ancestryName; // Class Line: "Subclass Class" let metaClass = ""; if (showSubclass && subclassName) metaClass += subclassName; if (showSubclass && subclassName && showClass && className) metaClass += " "; if (showClass && className) metaClass += className; // Name Display Logic let nameHtml = ""; const charNameStr = `
${name}
`; const playerNameStr = `
${user.name}
`; if (nameMode === "player-top") nameHtml = playerNameStr + charNameStr; else if (nameMode === "no-player") nameHtml = charNameStr; else nameHtml = charNameStr + playerNameStr; // Header Styling Logic const headerClass = (!showRes && !showAttr) ? "dh-card-header no-border" : "dh-card-header"; // Resources const res = system.resources || {}; const hp = res.hitPoints || system.health || { value: "?", max: "?" }; const stress = res.stress || system.stress || { value: "?", max: "?" }; const hope = res.hope || system.hope || { value: "?", max: "?" }; // Attributes (Traits) const traits = system.traits || {}; const getTrait = (key) => traits[key]?.value ?? 0; const agi = getTrait("agility"); const str = getTrait("strength"); const fin = getTrait("finesse"); const inst = getTrait("instinct"); const pre = getTrait("presence"); const kno = getTrait("knowledge"); // Attribute Labels const lbl = showFullAttr ? { agi: "Agility", str: "Strength", fin: "Finesse", ins: "Instinct", pre: "Presence", kno: "Knowledge" } : { agi: "AGI", str: "STR", fin: "FIN", ins: "INS", pre: "PRE", kno: "KNO" }; const attrClass = showFullAttr ? "dh-attr-grid full-names" : "dh-attr-grid"; // Resource Icons const iconHp = showIcons ? `` : ""; const iconStress = showIcons ? `` : ""; const iconHope = showIcons ? `` : ""; // Resource Values (Numeric vs Pips) const renderRes = (val, max, type) => { if (resMode === "pips") return renderPips(val, max, type); return `
${val} / ${max}
`; }; // Combat Context let isSpotlightRequesting = false; let turnClass = ""; // Determine Combat State explicitly const combat = game.combat || game.combats.active || game.combats.contents.find(c => c.active); if (combat) { // Log once per actor loop to diagnose // console.log(`DH Overlay | Checking Actor: ${actor.name} (${actor.id}) in Combat: ${combat.id}`); let combatant = combat.combatants.find(c => c.actorId === actor.id); if (combatant) { // Check Active Turn via multiple heuristics let isActiveTurn = false; // 1. Direct Actor ID match on combatant if (combat.combatant?.actorId === actor.id) { isActiveTurn = true; } // 2. Turn Index match else if (combat.turns && Number.isFinite(combat.turn)) { const turnCombatant = combat.turns[combat.turn]; if (turnCombatant && turnCombatant.actorId === actor.id) { isActiveTurn = true; } } // Final Check Logic if (isActiveTurn) { turnClass = "active-turn"; } // Check Spotlight Request if (combatant.system?.spotlight?.requesting) { isSpotlightRequesting = true; } } else { // console.log(`DH Overlay | Actor ${actor.name} is NOT in the active combat.`); } } else { // console.log("DH Overlay | No Active Combat Found."); } // Spotlight Indicator HTML const spotlightHtml = isSpotlightRequesting ? `
` : ""; html += `
${spotlightHtml}
${nameHtml}
${metaAncestry ? `${metaAncestry}` : ""}
${metaClass ? `${metaClass}` : ""}
${showRes ? `
${(showIcons || showResLabels) ? `
${iconHp} ${showResLabels ? 'HP' : ''}
` : ''} ${renderRes(hp.value, hp.max, "hp")}
${(showIcons || showResLabels) ? `
${iconStress} ${showResLabels ? 'Stress' : ''}
` : ''} ${renderRes(stress.value, stress.max, "stress")}
${(showIcons || showResLabels) ? `
${iconHope} ${showResLabels ? 'Hope' : ''}
` : ''} ${renderRes(hope.value, hope.max, "hope")}
` : ''} ${showAttr ? `
${lbl.agi} ${agi}
${lbl.str} ${str}
${lbl.fin} ${fin}
${lbl.ins} ${inst}
${lbl.pre} ${pre}
${lbl.kno} ${kno}
` : ''}
`; }); html += `
`; container.innerHTML = html; } // ============================================================================= // GLOBAL INIT & TRIGGERS // ============================================================================= let overlayInitialized = false; Hooks.once("ready", async () => { if (document.body.classList.contains("stream") && !overlayInitialized) { overlayInitialized = true; await initStreamOverlay(); } }); // Fallback Init Trigger document.addEventListener("DOMContentLoaded", () => { if (window.location.pathname.includes("/stream")) { setTimeout(async () => { if (!overlayInitialized && document.body.classList.contains("stream")) { console.warn("DH Stream Overlay | Fallback Init Triggered"); overlayInitialized = true; await initStreamOverlay(); } }, 1000); } }); // Menu Button Hook Hooks.on("renderDaggerheartMenu", (_app, html) => { const container = (html instanceof HTMLElement ? html : html[0]).querySelector("div") || html[0]; const section = document.createElement("fieldset"); section.innerHTML = `${game.i18n.localize("DH_STREAM_OVERLAY.Settings.MenuLabel")}`; section.classList.add("dh-stream-overlay-menu-section"); const button = document.createElement("button"); button.innerHTML = ` ${game.i18n.localize("DH_STREAM_OVERLAY.OpenStream")}`; button.style.width = "100%"; button.onclick = () => window.open("/stream", "streamOverlay", "width=1280,height=720,menubar=no,toolbar=no,status=no"); section.appendChild(button); container.appendChild(section); }); // ============================================================================= // CORE SETUP FUNCTION // ============================================================================= async function initStreamOverlay() { console.log("DH Stream Overlay | Initializing Layout..."); // 0. WAIT FOR GAME DATA // The stream view might load faster than Foundry data. let dataRetries = 0; while ((!game.users || !game.users.size) && dataRetries < 20) { console.log(`DH Stream Overlay | Waiting for game.users... (${dataRetries}/20)`); await new Promise(r => setTimeout(r, 500)); dataRetries++; } if (!game.users) { console.error("DH Stream Overlay | CRITICAL: game.users never initialized. Aborting."); return; } // 1. Inject CSS injectStyles(); // 2. Build Layout Structure const body = document.body; const wrapper = document.createElement("div"); wrapper.id = "stream-overlay-wrapper"; // Chat Container const chatContainer = document.createElement("div"); chatContainer.id = "stream-chat-container"; wrapper.appendChild(chatContainer); // Right Column Wrapper (Flex Column for Fear + Party) const rightCol = document.createElement("div"); rightCol.id = "stream-right-col"; wrapper.appendChild(rightCol); // Actor/Party Container (First, so it takes top space) const actorContainer = document.createElement("div"); actorContainer.id = "stream-actor-container"; rightCol.appendChild(actorContainer); // Fear Tracker Container (Second, so it sits at bottom) const fearContainer = document.createElement("div"); fearContainer.id = "stream-fear-container"; rightCol.appendChild(fearContainer); // Combat Container (Hidden by default) const combatContainer = document.createElement("div"); combatContainer.id = "stream-combat-container"; wrapper.appendChild(combatContainer); // Prepend to body so it sits behind/integrated (though fixed positioning handles it) // We insert before scripts to be safe, or just append. // Appending is safer for overlay z-index usually. body.appendChild(wrapper); // 3. Move Chat Log const retryChat = async () => { let attempts = 0; while (attempts < 50) { // Try for longer (approx 10s) // Try Standard DOM let chatLog = document.getElementById("chat-log"); // Try UI Object if (!chatLog && ui.chat && ui.chat.element) { chatLog = ui.chat.element[0]; } if (chatLog) { // Determine if we need to extract the list or the whole app // Usually #chat-log is the
    if (chatLog.tagName === "OL" || chatLog.id === "chat-log") { chatContainer.appendChild(chatLog); } else { // If we got the app window, find the list const list = chatLog.querySelector("#chat-log"); if (list) chatContainer.appendChild(list); } // Force scroll to bottom chatLog.scrollTop = chatLog.scrollHeight; console.log("DH Stream Overlay | Chat Log Moved Successfully"); return; } await new Promise(r => setTimeout(r, 200)); attempts++; } console.warn("DH Stream Overlay | Chat Log NOT FOUND after retries"); }; retryChat(); // 4. Initial Render of Party Overlay renderPartyOverlay(actorContainer); // 5. Reactivity Hooks Hooks.on("updateActor", (actor) => { // If the actor is a player character, re-render if (actor.hasPlayerOwner) { renderPartyOverlay(actorContainer); } }); Hooks.on("userUpdated", () => { // If assignments change renderPartyOverlay(actorContainer); }); // Scroll chat to bottom on new message Hooks.on("renderChatMessageHTML", () => { const log = document.getElementById("chat-log"); if (log) log.scrollTop = log.scrollHeight; }); // 6. Combat Tracker Handling const updateCombat = async () => { const combat = game.combat; if (combat?.started) { combatContainer.style.display = "flex"; // Try explicit render for sidebar app compatibility let tracker = ui.combat; if (!tracker) tracker = new CombatTracker({ popOut: false }); // Force it to render if not already in our container const el = document.getElementById("combat") || (tracker.element ? tracker.element[0] : null); if (!el || !combatContainer.contains(el)) { // Clone or Move? Move is safer for events. if (tracker.render) await tracker.render(true); const newEl = document.getElementById("combat"); if (newEl) combatContainer.appendChild(newEl); } } else { combatContainer.style.display = "none"; } // Force refresh of party overlay to update turn indicator renderPartyOverlay(actorContainer); }; Hooks.on("renderCombatTracker", (app, html) => { if (combatContainer && html) { combatContainer.appendChild(html[0]); combatContainer.style.display = "flex"; } }); Hooks.on("updateCombat", updateCombat); Hooks.on("deleteCombat", updateCombat); Hooks.on("createCombat", updateCombat); updateCombat(); // Initial check if (game.modules.get("dice-so-nice")?.active && game.settings.get("dh-stream-overlay", "enableDSN")) { if (!document.getElementById("board")) { const board = document.createElement("div"); board.id = "board"; board.style.position = "fixed"; board.style.top = 0; board.style.left = 0; board.style.width = "100%"; board.style.height = "100%"; board.style.pointerEvents = "none"; board.style.zIndex = 500; document.body.appendChild(board); } if (game.dice3d) game.dice3d.resize(window.innerWidth, window.innerHeight); } // 8. Fear Tracker Init renderFearTracker(fearContainer); Hooks.on("updateSetting", (setting) => { if (setting.key === "daggerheart.ResourcesFear" || setting.key === "daggerheart.Homebrew") { renderFearTracker(fearContainer); } }); } // ============================================================================= // CSS INJECTION // ============================================================================= function injectStyles() { const useGreen = game.settings.get("dh-stream-overlay", "useGreenScreen"); const bgColor = useGreen ? "#00ff00" : "rgba(0, 0, 0, 0)"; const style = document.createElement("style"); style.innerHTML = ` html, body { background-color: ${bgColor} !important; margin: 0px auto; overflow: hidden !important; padding: 0; width: 100vw; height: 100vh; border: none !important; } /* Force removed borders on everything high-level */ #stream-overlay-wrapper, #stream-chat-container, #stream-actor-container { border: none !important; outline: none !important; box-shadow: none !important; background: transparent !important; /* Context containers transparent */ } #stream-overlay-wrapper { display: grid !important; grid-template-columns: 320px 1fr; width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 1000; z-index: 1000; pointer-events: none; } /* Fear Tracker */ #stream-fear-container { position: fixed; top: 20px; left: 360px; /* Offset from chat */ z-index: 1100; display: flex; align-items: center; gap: 15px; background: rgba(0, 0, 0, 0.6); padding: 10px 20px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(5px); } .fear-label { font-size: 1.2em; font-weight: 800; color: rgba(255, 255, 255, 0.8); letter-spacing: 2px; text-transform: uppercase; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .fear-tokens { display: flex; gap: 8px; } .fear-token { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: #333; border: 2px solid rgba(255,255,255,0.1); border-radius: 50%; box-shadow: inset 0 0 10px rgba(0,0,0,0.5); color: rgba(255,255,255,0.3); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .fear-token.highlighted { background: linear-gradient(135deg, #a00, #500); border-color: rgba(255, 100, 100, 0.6); box-shadow: 0 0 10px rgba(255, 0, 0, 0.4); color: #fff; transform: scale(1.1); } .fear-token.highlighted i { text-shadow: 0 0 5px rgba(255, 255, 255, 0.8); } /* Shape variants */ .fear-token.square { border-radius: 4px; } .fear-token.rounded { border-radius: 8px; } .fear-token.circle { border-radius: 50%; } /* Spotlight / Turn Glow */ .dh-active-glow { border-color: #ffd700 !important; box-shadow: 0 0 20px rgba(255, 215, 0, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6) !important; position: relative; } .dh-spotlight-indicator { position: absolute; top: -10px; right: -10px; background: #ffd700; color: #000; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; box-shadow: 0 0 10px rgba(255, 215, 0, 0.8); animation: spotlightPulse 1.5s infinite alternate; font-size: 16px; } @keyframes spotlightPulse { from { transform: scale(1); box-shadow: 0 0 5px rgba(255, 215, 0, 0.5); } to { transform: scale(1.15); box-shadow: 0 0 15px rgba(255, 215, 0, 0.9); } } #stream-chat-container { grid-column: 1; height: 100vh; background: transparent !important; /* Fully transparent for Chroma Key */ /* border-right: 2px solid rgba(255, 255, 255, 0.1) !important; Optional separator */ pointer-events: auto; position: relative; display: flex; flex-direction: column; overflow: hidden; } #stream-right-col { grid-column: 2; height: 100vh; overflow: hidden; /* Parent doesn't scroll */ display: flex; flex-direction: column; padding: 20px; gap: 0; /* Gaps handled by children margins/padding */ } #stream-actor-container { width: 100%; flex: 1; /* Takes all available space */ overflow-y: auto; /* Scrolls internally */ display: flex; flex-direction: column; gap: 15px; padding-bottom: 20px; /* Spacing before footer */ } /* Fear Tracker */ #stream-fear-container { /* Footer Layout - Force Reset */ position: relative !important; top: auto !important; bottom: auto !important; left: auto !important; right: auto !important; flex: 0 0 auto; display: flex; align-items: center; gap: 15px; background: rgba(20, 20, 25, 0.9); padding: 10px 20px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(5px); box-shadow: 0 -4px 15px rgba(0,0,0,0.5); margin-top: 10px; align-self: flex-end; width: fit-content; margin-left: auto; } .fear-label { font-size: 1.2em; font-weight: 800; color: rgba(255, 255, 255, 0.8); letter-spacing: 2px; text-transform: uppercase; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .fear-tokens { display: flex; gap: 8px; } .fear-token { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: #333; border: 2px solid rgba(255,255,255,0.1); border-radius: 50%; box-shadow: inset 0 0 10px rgba(0,0,0,0.5); color: rgba(255,255,255,0.3); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .fear-token img.fear-icon-img { width: 70%; height: 70%; object-fit: contain; filter: brightness(0) invert(1) drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)); } .fear-token.active { /* Highlights handled by inline styles now mostly, but keep some defaults */ color: #fff; transform: scale(1.1); } .fear-token.active i { text-shadow: 0 0 5px rgba(255, 255, 255, 0.8); } /* Force Chat Log Visibility */ #chat-log { display: block !important; height: 100% !important; width: 100% !important; overflow-y: auto !important; background: transparent !important; padding: 10px !important; margin: 0 !important; box-sizing: border-box !important; border: none !important; list-style: none !important; /* Remove bullets */ font-family: var(--font-primary) !important; } .chat-message { background-color: #18181b !important; background-image: none !important; border: 1px solid rgba(255, 255, 255, 0.4) !important; color: #f4f4f5 !important; margin-bottom: 6px !important; padding: 8px !important; border-radius: 4px !important; font-size: 0.9em !important; text-shadow: none !important; box-shadow: 0 2px 5px rgba(0,0,0,0.9) !important; display: block !important; opacity: 1 !important; transform: translateZ(0); /* Force composition layer */ } .chat-message .message-header { color: #ccc !important; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 4px; padding-bottom: 2px; font-weight: bold; } .chat-message .message-content { color: #fff !important; } /* Hide standard timestamps to save space if needed, or style them */ .message-timestamp { color: #888; font-size: 0.8em; } #stream-combat-container { position: absolute; top: 20px; right: 20px; width: 250px; background: rgba(0, 0, 0, 0.9); border: none !important; display: none; /* Hidden by default */ flex-direction: column; pointer-events: auto; border-radius: 8px; overflow: hidden; padding: 5px; } /* Party Overlay Cards - GRID RESTORED */ .dh-party-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); align-items: start; /* Prevents cards from stretching vertically to match tallest */ gap: 15px; width: 100%; padding-right: 10px; } .dh-party-card { background: rgba(20, 20, 25, 0.9); border: 1px solid rgba(255, 255, 255, 0.1) !important; border-radius: 8px; color: white; display: flex; flex-direction: row; overflow: hidden; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6); min-height: 90px; transition: box-shadow 0.3s ease, border-color 0.3s ease; } .dh-party-card.active-turn { border-color: #ffd700 !important; box-shadow: 0 0 20px rgba(255, 215, 0, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6) !important; position: relative; /* Context for spotlight indicator */ } .dh-card-img-wrapper { width: 140px; position: relative; border-right: 1px solid rgba(255,255,255,0.1); background: #111; flex-shrink: 0; } .dh-card-img { width: 100%; height: 100%; position: absolute; top: 0; left: 0; object-fit: cover; object-position: top center; } .dh-card-content { flex: 1; display: flex; flex-direction: column; padding: 12px; gap: 8px; justify-content: center; } .dh-card-header { display: flex; flex-direction: column; gap: 2px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 8px; margin-bottom: auto; } .dh-card-header.no-border { border-bottom: none !important; padding-bottom: 0 !important; margin-bottom: 0 !important; } .dh-card-name { font-size: 1.5em; font-weight: 700; color: #fff; text-transform: uppercase; line-height: 1.1; letter-spacing: 0.5px; } .dh-card-meta { font-size: 0.85em; color: #bbb; display: flex; flex-wrap: wrap; gap: 6px; } .dh-stat-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; background: rgba(0, 0, 0, 0.4); padding: 8px; border-radius: 6px; margin-top: 5px; } .dh-stat { display: flex; flex-direction: column; align-items: center; justify-content: center; } .dh-stat i { font-size: 0.9em; margin-bottom: 3px; opacity: 0.7; } .stat-val { font-size: 1.3em; font-weight: 700; line-height: 1; } .stat-max { font-size: 0.85em; opacity: 0.5; } .stat-hp { color: #ff8787; } .stat-stress { color: #ffd43b; } .stat-hope { color: #74c0fc; } .dh-attr-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 2px; margin-top: 5px; background: rgba(0,0,0,0.3); padding: 4px; border-radius: 4px; } .dh-attr-grid.full-names { grid-template-columns: repeat(3, 1fr); gap: 4px; } .dh-attr { display: flex; flex-direction: column; align-items: center; } .dh-attr-label { font-size: 0.7em; color: #777; font-weight: 600; text-transform: uppercase; } .dh-attr-val { font-size: 1.1em; font-weight: bold; color: #ddd; } /* Stat Labels */ .dh-stat-icon { display: flex; align-items: center; justify-content: center; margin-bottom: 2px; } .stat-label { font-size: 0.75em; text-transform: uppercase; font-weight: 600; opacity: 0.8; margin-left: 5px; color: rgba(255,255,255,0.7); } /* Pips */ .dh-pips-container { display: flex; gap: 3px; flex-wrap: wrap; justify-content: center; max-width: 100%; margin-top: 5px; } .dh-pip { width: 12px; height: 12px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.4); border-radius: 2px; position: relative; } /* Colored Background for ALL pips per type */ .dh-pips-container.hp .dh-pip { background: rgba(255, 107, 107, 0.2); border-color: #ff6b6b; } .dh-pips-container.stress .dh-pip { background: rgba(254, 202, 87, 0.2); border-color: #feca57; } .dh-pips-container.hope .dh-pip { background: rgba(72, 219, 251, 0.2); border-color: #48dbfb; } /* Marked (X'd out) Style */ .dh-pip.marked::after { content: "✕"; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 10px; font-weight: bold; color: white; line-height: 1; } .dh-pips-container.hp .dh-pip.marked { background: #ff6b6b; color: white; } .dh-pips-container.stress .dh-pip.marked { background: #feca57; color: black; } .dh-pips-container.hope .dh-pip.marked { background: #48dbfb; color: black; } /* Ensure X contrasts */ .dh-pips-container.stress .dh-pip.marked::after, .dh-pips-container.hope .dh-pip.marked::after { color: #333; } `; document.head.appendChild(style); }