added additional layouts and carousel mode

This commit is contained in:
CPTN Cosmo 2026-01-24 12:54:36 +01:00
parent d516de52c0
commit 4a550233c0
No known key found for this signature in database
4 changed files with 519 additions and 46 deletions

View file

@ -19,14 +19,34 @@
"ShowCommunityHint": "Sichtbarkeit der Charaktergemeinschaft.", "ShowCommunityHint": "Sichtbarkeit der Charaktergemeinschaft.",
"Choices": { "Choices": {
"CharTop": "Charakter-Name Oben (Spieler Unten)", "CharTop": "Charakter-Name Oben (Spieler Unten)",
"PlayerTop": "Spieler-Name Oben (Charakter Unten)",
"NoPlayer": "Spieler-Name ausblenden",
"MenuLabel": "Stream-Overlay", "MenuLabel": "Stream-Overlay",
"PlayerNameDisplay": "Spielernamen-Anzeige" "PlayerNameDisplay": "Spielernamen-Anzeige",
"Standard": "Standard (Breitbild)",
"Vertical": "Vertikal (Sammelkarte)",
"RefFade": "Ausblenden (Fade)",
"RefSlide": "Gleiten (Slide)",
"RefZoom": "Zoomen (Zoom)",
"RefCycle": "Zyklus (In Rotation)",
"RefStatic": "Statisch (Immer sichtbar)",
"RefHidden": "Versteckt (Im Karussell)"
}, },
"PlayerNameDisplayHint": "Wo der Spielername relativ zum Charakternamen angezeigt werden soll.", "PlayerNameDisplayHint": "Wo der Spielername relativ zum Charakternamen angezeigt werden soll.",
"HideFearLabel": "Fear-Label verbergen", "HideFearLabel": "Fear-Label verbergen",
"HideFearLabelHint": "Verbirgt die Textbeschriftung 'FEAR' neben den Fear-Tokens.", "HideFearLabelHint": "Verbirgt die Textbeschriftung 'FEAR' neben den Fear-Tokens.",
"ShowResourceLabels": "Ressourcen-Bezeichnungen anzeigen", "ShowResourceLabels": "Ressourcen-Bezeichnungen anzeigen",
"ShowResourceLabelsHint": "Text-Bezeichnungen (HP, Stress, Hoffnung) neben Icons anzeigen." "ShowResourceLabelsHint": "Text-Bezeichnungen (HP, Stress, Hoffnung) neben Icons anzeigen.",
"CardLayout": "Karten-Layout Modus",
"CardLayoutHint": "Wähle zwischen der Standard-Breitbild-Karte oder einem vertikalen Sammelkarten-Layout.",
"CarouselEnabled": "Karussell-Modus aktivieren",
"CarouselEnabledHint": "Wechsle automatisch nacheinander durch Spieler.",
"CarouselSpeed": "Karussell-Geschwindigkeit (s)",
"CarouselSpeedHint": "Sekunden, die jede Karte angezeigt wird, bevor gewechselt wird.",
"CarouselEffect": "Übergangseffekt",
"CarouselEffectHint": "Animationseffekt beim Wechseln der Karten.",
"CarouselGMMode": "GM Karussell-Verhalten",
"CarouselGMModeHint": "Wie der GM im Karussell behandelt werden soll."
}, },
"OpenStream": "Stream-Ansicht öffnen", "OpenStream": "Stream-Ansicht öffnen",
"ModeNumeric": "Numerisch (3/6)", "ModeNumeric": "Numerisch (3/6)",

View file

@ -24,7 +24,15 @@
"Choices": { "Choices": {
"CharTop": "Character Name Top (Player Bottom)", "CharTop": "Character Name Top (Player Bottom)",
"PlayerTop": "Player Name Top (Character Bottom)", "PlayerTop": "Player Name Top (Character Bottom)",
"NoPlayer": "Hide Player Name" "NoPlayer": "Hide Player Name",
"Standard": "Standard (Horizontal Card)",
"Vertical": "Vertical (Trading Card)",
"RefFade": "Fade",
"RefSlide": "Slide",
"RefZoom": "Zoom",
"RefCycle": "Cycle (In Rotation)",
"RefStatic": "Static (Always Visible)",
"RefHidden": "Hidden (In Carousel)"
}, },
"MenuLabel": "Stream Overlay", "MenuLabel": "Stream Overlay",
"PlayerNameDisplay": "Player Name Display", "PlayerNameDisplay": "Player Name Display",
@ -32,7 +40,21 @@
"HideFearLabel": "Hide Fear Label", "HideFearLabel": "Hide Fear Label",
"HideFearLabelHint": "Hide the text label 'FEAR' next to the fear tokens.", "HideFearLabelHint": "Hide the text label 'FEAR' next to the fear tokens.",
"ShowResourceLabels": "Show Resource Labels", "ShowResourceLabels": "Show Resource Labels",
"ShowResourceLabelsHint": "Show text labels (HP, Stress, Hope) next to icons." "ShowResourceLabelsHint": "Show text labels (HP, Stress, Hope) next to icons.",
"AutoExpandChat": "Auto-Expand Chat Messages",
"AutoExpandChatHint": "Automatically expand all chat messages to show full content (rolls, descriptions) without needing to click.",
"HideChatActions": "Hide Chat Action Buttons",
"HideChatActionsHint": "Hides buttons like 'Apply Damage', 'Place Template', or other interactive elements from chat cards to keep the view clean.",
"CardLayout": "Card Layout Mode",
"CardLayoutHint": "Choose between the standard wide card or a vertical trading-card style.",
"CarouselEnabled": "Enable Carousel Mode",
"CarouselEnabledHint": "Cycle through players one by one automatically.",
"CarouselSpeed": "Carousel Speed (s)",
"CarouselSpeedHint": "Seconds to show each player card before switching.",
"CarouselEffect": "Transition Effect",
"CarouselEffectHint": "Animation effect when switching cards.",
"CarouselGMMode": "GM Carousel Behavior",
"CarouselGMModeHint": "How the GM should constitute in the carousel."
}, },
"OpenStream": "Open Stream View", "OpenStream": "Open Stream View",
"ModeNumeric": "Numeric (3/6)", "ModeNumeric": "Numeric (3/6)",

View file

@ -1,7 +1,7 @@
{ {
"id": "dh-stream-overlay", "id": "dh-stream-overlay",
"title": "Daggerheart Stream Overlay", "title": "Daggerheart Stream Overlay",
"version": "1.0.0", "version": "2.0.0",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13" "verified": "13"
@ -44,7 +44,7 @@
], ],
"packFolders": [], "packFolders": [],
"url": "https://github.com/cptn-cosmo/dh-stream-overlay", "url": "https://github.com/cptn-cosmo/dh-stream-overlay",
"manifest": "https://github.com/cptn-cosmo/dh-stream-overlay/releases/latest/download/module.json", "manifest": "https://git.geeks.gay/cosmo/dh-stream-overlay/raw/branch/main/module.json",
"download": "https://github.com/cptn-cosmo/dh-stream-overlay/releases/download/1.0.0/dh-stream-overlay.zip", "download": "https://git.geeks.gay/cosmo/dh-stream-overlay/releases/download/2.0.0/dh-stream-overlay.zip",
"description": "A stream overlay module for Daggerheart that displays chat and a linked party actor sheet." "description": "A stream overlay module for Daggerheart that displays chat and a linked party actor sheet."
} }

View file

@ -191,7 +191,149 @@ Hooks.once("init", () => {
scope: "world", config: false, type: Boolean, default: true scope: "world", config: false, type: Boolean, default: true
}); });
game.settings.register("dh-stream-overlay", "autoExpandChat", {
name: "DH_STREAM_OVERLAY.Settings.AutoExpandChat",
hint: "DH_STREAM_OVERLAY.Settings.AutoExpandChatHint",
scope: "world",
config: true,
type: Boolean,
default: true,
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
}); });
game.settings.register("dh-stream-overlay", "hideChatActions", {
name: "DH_STREAM_OVERLAY.Settings.HideChatActions",
hint: "DH_STREAM_OVERLAY.Settings.HideChatActionsHint",
scope: "world",
config: true,
type: Boolean,
default: true,
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
});
game.settings.register("dh-stream-overlay", "cardLayout", {
name: "DH_STREAM_OVERLAY.Settings.CardLayout",
hint: "DH_STREAM_OVERLAY.Settings.CardLayoutHint",
scope: "world",
config: true,
type: String,
choices: {
"standard": "DH_STREAM_OVERLAY.Settings.Choices.Standard",
"vertical": "DH_STREAM_OVERLAY.Settings.Choices.Vertical"
},
default: "standard",
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
});
game.settings.register("dh-stream-overlay", "carouselEnabled", {
name: "DH_STREAM_OVERLAY.Settings.CarouselEnabled",
hint: "DH_STREAM_OVERLAY.Settings.CarouselEnabledHint",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
});
game.settings.register("dh-stream-overlay", "carouselSpeed", {
name: "DH_STREAM_OVERLAY.Settings.CarouselSpeed",
hint: "DH_STREAM_OVERLAY.Settings.CarouselSpeedHint",
scope: "world",
config: true,
type: Number,
default: 5,
range: { min: 2, max: 60, step: 1 },
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
});
game.settings.register("dh-stream-overlay", "carouselEffect", {
name: "DH_STREAM_OVERLAY.Settings.CarouselEffect",
hint: "DH_STREAM_OVERLAY.Settings.CarouselEffectHint",
scope: "world",
config: true,
type: String,
choices: {
"fade": "DH_STREAM_OVERLAY.Settings.Choices.RefFade",
"slide": "DH_STREAM_OVERLAY.Settings.Choices.RefSlide",
"zoom": "DH_STREAM_OVERLAY.Settings.Choices.RefZoom"
},
default: "fade",
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
});
game.settings.register("dh-stream-overlay", "carouselGMMode", {
name: "DH_STREAM_OVERLAY.Settings.CarouselGMMode",
hint: "DH_STREAM_OVERLAY.Settings.CarouselGMModeHint",
scope: "world",
config: true,
type: String,
choices: {
"cycle": "DH_STREAM_OVERLAY.Settings.Choices.RefCycle",
"static": "DH_STREAM_OVERLAY.Settings.Choices.RefStatic",
"hidden": "DH_STREAM_OVERLAY.Settings.Choices.RefHidden"
},
default: "cycle",
onChange: () => { if (document.body.classList.contains("stream")) location.reload(); }
});
});
// =============================================================================
// CAROUSEL LOGIC
// =============================================================================
let carouselIndex = 0;
let carouselTimer = null;
function startCarousel() {
if (carouselTimer) clearInterval(carouselTimer);
const speed = game.settings.get("dh-stream-overlay", "carouselSpeed") || 5;
carouselTimer = setInterval(() => {
cycleCarousel();
}, speed * 1000);
}
function stopCarousel() {
if (carouselTimer) {
clearInterval(carouselTimer);
carouselTimer = null;
}
}
function cycleCarousel() {
const container = document.querySelector(".dh-party-grid.carousel-mode");
if (!container) return;
const cards = container.querySelectorAll(".dh-party-card");
if (cards.length === 0) return;
// Increment
carouselIndex++;
if (carouselIndex >= cards.length) carouselIndex = 0;
updateCarouselClasses(container);
}
function updateCarouselClasses(container) {
if (!container) return;
const cards = container.querySelectorAll(".dh-party-card");
// Safety: Ensure index is valid
if (carouselIndex >= cards.length) carouselIndex = 0;
cards.forEach((card, idx) => {
// Reset classes
card.classList.remove("active-slide", "exit-slide");
if (idx === carouselIndex) {
card.classList.add("active-slide");
} else {
// Optional: Mark previous slide for exit animation if needed
// For simple fade, just lacking 'active-slide' hides it
}
});
}
// ... // ...
// ... inside renderPartyOverlay function ... // ... inside renderPartyOverlay function ...
@ -353,24 +495,45 @@ function renderPartyOverlay(container) {
const gms = game.users.filter(u => u.isGM); const gms = game.users.filter(u => u.isGM);
if (players.length === 0 && gms.length === 0) { if (players.length === 0 && gms.length === 0) {
container.innerHTML = `<div style="color:white; opacity:0.6; padding:10px;">No Active Players or GM Found</div>`;
return; return;
} }
let html = `<div class="dh-party-grid">`; let layoutMode = "standard";
try { layoutMode = game.settings.get("dh-stream-overlay", "cardLayout"); } catch (e) { }
// 1. Render GM(s) let carouselEnabled = false;
let carouselEffect = "fade";
let carouselGMMode = "cycle";
try {
carouselEnabled = game.settings.get("dh-stream-overlay", "carouselEnabled");
carouselEffect = game.settings.get("dh-stream-overlay", "carouselEffect");
carouselGMMode = game.settings.get("dh-stream-overlay", "carouselGMMode");
} catch (e) { }
const renderPips = (val, max, type) => {
let h = `<div class="dh-pips-container ${type}">`;
for (let i = 1; i <= max; i++) {
h += `<div class="dh-pip ${i <= val ? 'marked' : 'open'}"></div>`;
}
h += `</div>`;
return h;
};
// --- HTML GENERATORS ---
// GM HTML Generator
let gmHtml = "";
gms.forEach(gm => { gms.forEach(gm => {
const img = gm.avatar || "icons/svg/mystery-man.svg"; const img = gm.avatar || "icons/svg/mystery-man.svg";
const name = gm.name; // GM just uses their user name usually const name = gm.name;
html += ` gmHtml += `
<div class="dh-party-card" style="min-height: 100px;"> <div class="dh-party-card">
<div class="dh-card-img-wrapper" style="width: 100px;"> <div class="dh-card-img-wrapper">
<img class="dh-card-img" src="${img}" /> <img class="dh-card-img" src="${img}" />
</div> </div>
<div class="dh-card-content"> <div class="dh-card-content">
<div class="dh-card-header" style="border-bottom:none; padding-bottom:0;"> <div class="dh-card-header no-border">
<div class="dh-card-name" style="color: #fca5a5;">Game Master</div> <div class="dh-card-name" style="color: #fca5a5;">Game Master</div>
<div class="dh-card-meta" style="font-size:1.1em; color:white; margin-top:5px;">${name}</div> <div class="dh-card-meta" style="font-size:1.1em; color:white; margin-top:5px;">${name}</div>
</div> </div>
@ -379,6 +542,9 @@ function renderPartyOverlay(container) {
`; `;
}); });
// Player HTML Generator
let playerHtml = "";
// 2. Render Players // 2. Render Players
// Check setting availability // Check setting availability
let nameMode = "char-top"; let nameMode = "char-top";
@ -407,14 +573,7 @@ function renderPartyOverlay(container) {
showCommunity = game.settings.get("dh-stream-overlay", "showCommunity"); showCommunity = game.settings.get("dh-stream-overlay", "showCommunity");
} catch (e) { console.warn("DH OVERLAY: Settings not ready yet, using default"); } } catch (e) { console.warn("DH OVERLAY: Settings not ready yet, using default"); }
const renderPips = (val, max, type) => {
let h = `<div class="dh-pips-container ${type}">`;
for (let i = 1; i <= max; i++) {
h += `<div class="dh-pip ${i <= val ? 'marked' : 'open'}"></div>`;
}
h += `</div>`;
return h;
};
players.forEach(user => { players.forEach(user => {
const actor = user.character; const actor = user.character;
@ -546,7 +705,7 @@ function renderPartyOverlay(container) {
const spotlightHtml = isSpotlightRequesting ? const spotlightHtml = isSpotlightRequesting ?
`<div class="dh-spotlight-indicator"><i class="fas fa-hand-sparkles"></i></div>` : ""; `<div class="dh-spotlight-indicator"><i class="fas fa-hand-sparkles"></i></div>` : "";
html += ` playerHtml += `
<div class="dh-party-card ${turnClass}"> <div class="dh-party-card ${turnClass}">
${spotlightHtml} ${spotlightHtml}
<div class="dh-card-img-wrapper"> <div class="dh-card-img-wrapper">
@ -594,8 +753,45 @@ function renderPartyOverlay(container) {
}); });
html += `</div>`; // --- ASSEMBLY LOGIC ---
container.innerHTML = html; if (carouselEnabled) {
let finalHtml = "";
let carouselClass = `carousel-mode effect-${carouselEffect}`;
// GM Logic
if (carouselGMMode === "static") {
// Static grid for GM, Carousel for Players
// Wrap in a flex container
container.innerHTML = `
<div class="dh-carousel-wrapper">
${gms.length > 0 ? `<div class="dh-party-grid layout-${layoutMode} dh-static-gm">${gmHtml}</div>` : ""}
${players.length > 0 ? `<div class="dh-party-grid layout-${layoutMode} ${carouselClass}">${playerHtml}</div>` : ""}
</div>`;
} else if (carouselGMMode === "hidden") {
// Only Players in Carousel
container.innerHTML = `<div class="dh-party-grid layout-${layoutMode} ${carouselClass}">${playerHtml}</div>`;
} else {
// "cycle" or default - Mixed
container.innerHTML = `<div class="dh-party-grid layout-${layoutMode} ${carouselClass}">${gmHtml}${playerHtml}</div>`;
}
} else {
// Standard Mode
container.innerHTML = `<div class="dh-party-grid layout-${layoutMode}">${gmHtml}${playerHtml}</div>`;
}
// Post-Render: Apply Carousel State immediately (if applicable)
if (carouselEnabled) {
// Find the carousel grid (could be the main one or secondary)
const grid = container.querySelector(".dh-party-grid.carousel-mode");
if (grid) {
updateCarouselClasses(grid);
if (!carouselTimer) startCarousel();
}
} else {
stopCarousel();
}
} }
// ============================================================================= // =============================================================================
@ -612,17 +808,27 @@ Hooks.once("ready", async () => {
}); });
// Fallback Init Trigger // Fallback Init Trigger
document.addEventListener("DOMContentLoaded", () => { // Fallback Init Trigger
if (window.location.pathname.includes("/stream")) { if (window.location.pathname.includes("/stream")) {
setTimeout(async () => { const attemptInit = async (retries = 0) => {
if (!overlayInitialized && document.body.classList.contains("stream")) { if (overlayInitialized) return;
console.warn("DH Stream Overlay | Fallback Init Triggered");
// If game is ready, or purely maximizing retries (10 seconds), force init
if (game.ready || retries > 20) {
if (!overlayInitialized) {
console.warn(`DH Stream Overlay | Fallback Init Triggered (Ready: ${game.ready}, Retries: ${retries})`);
overlayInitialized = true; overlayInitialized = true;
await initStreamOverlay(); await initStreamOverlay();
} }
}, 1000); } else {
// Check again in 500ms
setTimeout(() => attemptInit(retries + 1), 500);
}
};
// Start the fallback poll
setTimeout(() => attemptInit(), 1000);
} }
});
// Menu Button Hook // Menu Button Hook
Hooks.on("renderDaggerheartMenu", (_app, html) => { Hooks.on("renderDaggerheartMenu", (_app, html) => {
@ -633,7 +839,10 @@ Hooks.on("renderDaggerheartMenu", (_app, html) => {
const button = document.createElement("button"); const button = document.createElement("button");
button.innerHTML = `<i class="fas fa-video"></i> ${game.i18n.localize("DH_STREAM_OVERLAY.OpenStream")}`; button.innerHTML = `<i class="fas fa-video"></i> ${game.i18n.localize("DH_STREAM_OVERLAY.OpenStream")}`;
button.style.width = "100%"; button.style.width = "100%";
button.onclick = () => window.open("/stream", "streamOverlay", "width=1280,height=720,menubar=no,toolbar=no,status=no"); button.onclick = () => {
const path = window.location.pathname.replace(/\/game\/?$/, "/stream");
window.open(path, "streamOverlay", "width=1280,height=720,menubar=no,toolbar=no,status=no");
};
section.appendChild(button); section.appendChild(button);
container.appendChild(section); container.appendChild(section);
}); });
@ -645,12 +854,20 @@ Hooks.on("renderDaggerheartMenu", (_app, html) => {
async function initStreamOverlay() { async function initStreamOverlay() {
console.log("DH Stream Overlay | Initializing Layout..."); console.log("DH Stream Overlay | Initializing Layout...");
// 0a. WAIT FOR SETTINGS
// Ensure "init" hook has run so settings are registered.
// If not, we wait a moment.
if (!game.settings.settings.has("dh-stream-overlay.autoExpandChat")) {
console.warn("DH Stream Overlay | Settings not yet registered, waiting for init...");
await new Promise(r => setTimeout(r, 500));
}
// 0. WAIT FOR GAME DATA // 0. WAIT FOR GAME DATA
// The stream view might load faster than Foundry data. // The stream view might load faster than Foundry data.
let dataRetries = 0; let dataRetries = 0;
while ((!game.users || !game.users.size) && dataRetries < 20) { while ((!game.users || !game.users.size) && dataRetries < 10) {
console.log(`DH Stream Overlay | Waiting for game.users... (${dataRetries}/20)`); console.log(`DH Stream Overlay | Waiting for game.users... (${dataRetries}/10)`);
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 200));
dataRetries++; dataRetries++;
} }
@ -697,10 +914,11 @@ async function initStreamOverlay() {
// Appending is safer for overlay z-index usually. // Appending is safer for overlay z-index usually.
body.appendChild(wrapper); body.appendChild(wrapper);
// 3. Move Chat Log
// 3. Move Chat Log // 3. Move Chat Log
const retryChat = async () => { const retryChat = async () => {
let attempts = 0; let attempts = 0;
while (attempts < 50) { // Try for longer (approx 10s) while (attempts < 20) { // Reduced retries to avoid long load times
// Try Standard DOM // Try Standard DOM
let chatLog = document.getElementById("chat-log"); let chatLog = document.getElementById("chat-log");
@ -723,9 +941,22 @@ async function initStreamOverlay() {
// Force scroll to bottom // Force scroll to bottom
chatLog.scrollTop = chatLog.scrollHeight; chatLog.scrollTop = chatLog.scrollHeight;
console.log("DH Stream Overlay | Chat Log Moved Successfully"); console.log("DH Stream Overlay | Chat Log Moved Successfully");
// --- FIX: EXISTING MESSAGES ---
// Manually strip inline styles from existing messages if auto-expand is OFF
const autoExpand = game.settings.get("dh-stream-overlay", "autoExpandChat");
if (!autoExpand) {
const $log = $(chatLog);
// Remove .expanded class first
$log.find(".dice-roll.expanded, .damage-section.expanded, .target-section.expanded").removeClass("expanded");
// Strip inline styles
$log.find(".dice-tooltip, .dice-formula, .daggerheart-target-list, .evaluation-selection").css("display", "");
$log.find(".dice-roll .dice-tooltip").css("display", ""); // Specificity check
}
return; return;
} }
await new Promise(r => setTimeout(r, 200)); await new Promise(r => setTimeout(r, 250));
attempts++; attempts++;
} }
console.warn("DH Stream Overlay | Chat Log NOT FOUND after retries"); console.warn("DH Stream Overlay | Chat Log NOT FOUND after retries");
@ -749,9 +980,37 @@ async function initStreamOverlay() {
}); });
// Scroll chat to bottom on new message // Scroll chat to bottom on new message
Hooks.on("renderChatMessageHTML", () => { // Scroll chat to bottom on new message & Handle Auto-Expand Logic
Hooks.on("renderChatMessage", (message, html) => {
// Scroll
const log = document.getElementById("chat-log"); const log = document.getElementById("chat-log");
if (log) log.scrollTop = log.scrollHeight; if (log) log.scrollTop = log.scrollHeight;
// Auto-Expand Logic (Override System Defaults if necessary)
// If Auto-Expand is DISABLED, we want to ensure tooltips don't have inline 'display: block'
// which the system might be adding by default.
try {
const autoExpand = game.settings.get("dh-stream-overlay", "autoExpandChat");
if (!autoExpand) {
// WRAPPED IN TIMEOUT to beat system race conditions
setTimeout(() => {
// 1. Remove .expanded class to force collapse state, allowing manual toggle primarily
html.find(".dice-roll.expanded, .damage-section.expanded, .target-section.expanded").removeClass("expanded");
// 2. Remove inline display styles that force expansion (failsafe)
html.find(".dice-tooltip, .dice-formula, .daggerheart-target-list, .evaluation-selection").css("display", "");
// Also legacy support for non-jquery if appropriate (but standard hook passes jQuery)
if (html instanceof HTMLElement) {
html.querySelectorAll(".expanded").forEach(el => el.classList.remove("expanded"));
const nodes = html.querySelectorAll(".dice-tooltip, .dice-formula, .daggerheart-target-list, .evaluation-selection");
nodes.forEach(n => n.style.display = "");
}
}, 10);
}
} catch (e) {
console.warn("DH Stream Overlay | Error in renderChatMessage hook", e);
}
}); });
// 6. Combat Tracker Handling // 6. Combat Tracker Handling
@ -817,6 +1076,8 @@ async function initStreamOverlay() {
function injectStyles() { function injectStyles() {
const useGreen = game.settings.get("dh-stream-overlay", "useGreenScreen"); const useGreen = game.settings.get("dh-stream-overlay", "useGreenScreen");
const autoExpand = game.settings.get("dh-stream-overlay", "autoExpandChat");
const hideActions = game.settings.get("dh-stream-overlay", "hideChatActions");
const bgColor = useGreen ? "#00ff00" : "rgba(0, 0, 0, 0)"; const bgColor = useGreen ? "#00ff00" : "rgba(0, 0, 0, 0)";
const style = document.createElement("style"); const style = document.createElement("style");
@ -938,12 +1199,22 @@ function injectStyles() {
height: 100vh; height: 100vh;
background: transparent !important; /* Fully transparent for Chroma Key */ background: transparent !important; /* Fully transparent for Chroma Key */
/* border-right: 2px solid rgba(255, 255, 255, 0.1) !important; Optional separator */ /* border-right: 2px solid rgba(255, 255, 255, 0.1) !important; Optional separator */
pointer-events: auto; pointer-events: auto !important;
z-index: 10000 !important; /* Force on top of everything */
position: relative; position: relative;
display: flex; flex-direction: column; display: flex; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
/* GLOBAL INTERACTIVITY FORCE - ensuring chat is always clickable */
#stream-chat-container #chat-log,
#stream-chat-container .chat-message,
#stream-chat-container .message-content,
#stream-chat-container .dice-roll,
#stream-chat-container button {
pointer-events: auto !important;
}
#stream-right-col { #stream-right-col {
grid-column: 2; grid-column: 2;
height: 100vh; height: 100vh;
@ -1077,6 +1348,51 @@ function injectStyles() {
/* Hide standard timestamps to save space if needed, or style them */ /* Hide standard timestamps to save space if needed, or style them */
.message-timestamp { color: #888; font-size: 0.8em; } .message-timestamp { color: #888; font-size: 0.8em; }
/* Auto-Expand Chat Logic */
${autoExpand ? `
.chat-message .message-content,
.chat-message .dice-roll,
.chat-message .dice-result,
.chat-message .dice-tooltip,
.chat-message .dice-formula {
display: block !important;
visibility: visible !important;
height: auto !important;
opacity: 1 !important;
}
.dice-tooltip { display: block !important; }
` : `
/* Force collapse default state (allow JS toggling) via HIGH SPECIFICITY */
#stream-chat-container .chat-message .dice-tooltip,
#stream-chat-container .chat-message .dice-formula,
#stream-chat-container .chat-message .daggerheart-target-list,
#stream-chat-container .chat-message .evaluation-selection,
#stream-chat-container .chat-message .dice-roll .dice-tooltip,
#stream-chat-container .chat-message .dice-result .dice-tooltip {
display: none !important;
}
/* Allow manual interaction if the .expanded class is present */
#stream-chat-container .chat-message .dice-roll.expanded .dice-tooltip,
#stream-chat-container .chat-message .dice-roll.expanded .dice-formula,
#stream-chat-container .chat-message .target-section.expanded .daggerheart-target-list,
#stream-chat-container .chat-message .damage-section.expanded .dice-formula,
#stream-chat-container .chat-message .dice-roll.expanded .dice-result .dice-tooltip {
display: block !important;
}
/* FORCE INTERACTIVITY removed from here to be global */
`}
${hideActions ? `
.chat-message button,
.chat-message .chat-card-buttons,
.chat-message .card-buttons {
display: none !important;
}
` : ''}
#stream-combat-container { #stream-combat-container {
position: absolute; position: absolute;
top: 20px; right: 20px; top: 20px; right: 20px;
@ -1091,14 +1407,16 @@ function injectStyles() {
padding: 5px; padding: 5px;
} }
/* Party Overlay Cards - GRID RESTORED */ /* Party Overlay Cards - GRID RESTORED */
.dh-party-grid { .dh-party-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));
align-items: start; /* Prevents cards from stretching vertically to match tallest */ align-items: start; /* Prevents cards from stretching vertically to match tallest */
gap: 15px; gap: 40px;
width: 100%; width: 100%;
padding-right: 10px; padding: 20px;
padding-right: 30px;
} }
.dh-party-card { .dh-party-card {
@ -1260,7 +1578,120 @@ function injectStyles() {
color: #333; color: #333;
} }
`; `;
document.head.appendChild(style);
style.innerHTML += `
/* Vertical Layout */
.dh-party-grid.layout-vertical {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
align-items: start;
} }
.dh-party-grid.layout-vertical .dh-party-card {
flex-direction: column;
min-height: 400px;
}
.dh-party-grid.layout-vertical .dh-card-img-wrapper {
width: 100%;
height: 250px;
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.dh-party-grid.layout-vertical .dh-card-img {
object-position: top center;
}
.dh-party-grid.layout-vertical .dh-card-content {
padding: 15px;
gap: 10px;
}
`;
/* Carousel Mode */
style.innerHTML += `
/* Carousel Mode */
.dh-party-grid.carousel-mode {
display: grid !important;
grid-template-columns: 1fr !important;
grid-template-rows: 1fr;
justify-items: center;
align-items: center;
}
.dh-party-grid.carousel-mode .dh-party-card {
grid-area: 1 / 1 / -1 / -1; /* Stack on top of each other */
opacity: 0;
visibility: hidden;
/* TRANSITION FIX: Delay visibility hide so it fades out first */
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out, visibility 0s linear 0.8s;
pointer-events: none;
width: 100%;
max-width: 600px; /* Constrain width in carousel */
min-height: 120px;
}
/* Active Slide */
.dh-party-grid.carousel-mode .dh-party-card.active-slide {
opacity: 1;
visibility: visible;
/* Show immediately */
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out, visibility 0s linear;
pointer-events: auto;
z-index: 10;
transform: none;
}
/* Effects */
/* Fade (Default behavior above) */
/* Slide */
.dh-party-grid.carousel-mode.effect-slide .dh-party-card {
transform: translateX(50px);
}
.dh-party-grid.carousel-mode.effect-slide .dh-party-card.active-slide {
transform: translateX(0);
}
/* Zoom */
.dh-party-grid.carousel-mode.effect-zoom .dh-party-card {
transform: scale(0.9);
}
.dh-party-grid.carousel-mode.effect-zoom .dh-party-card.active-slide {
transform: scale(1);
}
`;
style.innerHTML += `
/* Width Constraint for Vertical Carousel */
.dh-party-grid.carousel-mode.layout-vertical .dh-party-card {
max-width: 350px !important;
}
/* Static GM Wrapper */
.dh-carousel-wrapper {
display: flex;
align-items: center;
width: 100%;
gap: 20px;
justify-content: center;
}
.dh-static-gm {
flex: 0 0 auto;
/* Matches vertical card layout styling */
width: 350px;
}
.dh-static-gm .dh-party-card {
height: 100%;
max-width: 350px;
}
`;
document.head.appendChild(style);
}