1266 lines
46 KiB
JavaScript
1266 lines
46 KiB
JavaScript
|
|
|
|
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 `<img src="${settings.customSvgPath}" class="fear-icon-img" />`;
|
|
}
|
|
|
|
if (iconClass.includes('.svg')) {
|
|
return `<img src="${iconClass}" class="fear-icon-img" />`;
|
|
}
|
|
|
|
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 `<i class="${iconClass}" style="${style}"></i>`;
|
|
};
|
|
|
|
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 ? '' : '<div class="fear-label">Fear</div>'}
|
|
<div class="fear-tokens">
|
|
`;
|
|
|
|
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 += `<div class="${classes}" style="${style}">${icon}</div>`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
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 = `<div style="color:white; opacity:0.6; padding:10px;">No Active Players or GM Found</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = `<div class="dh-party-grid">`;
|
|
|
|
// 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 += `
|
|
<div class="dh-party-card" style="min-height: 100px;">
|
|
<div class="dh-card-img-wrapper" style="width: 100px;">
|
|
<img class="dh-card-img" src="${img}" />
|
|
</div>
|
|
<div class="dh-card-content">
|
|
<div class="dh-card-header" style="border-bottom:none; padding-bottom:0;">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
// 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 = `<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 => {
|
|
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 = `<div class="dh-card-name">${name}</div>`;
|
|
const playerNameStr = `<div class="dh-card-player-name" style="font-size:0.9em; color:#aaa; margin-bottom:2px;">${user.name}</div>`;
|
|
|
|
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 ? `<i class="fas fa-heart"></i>` : "";
|
|
const iconStress = showIcons ? `<i class="fas fa-bolt"></i>` : "";
|
|
const iconHope = showIcons ? `<i class="fas fa-star"></i>` : "";
|
|
|
|
// Resource Values (Numeric vs Pips)
|
|
const renderRes = (val, max, type) => {
|
|
if (resMode === "pips") return renderPips(val, max, type);
|
|
return `<div><span class="stat-val">${val}</span> <span class="stat-max">/ ${max}</span></div>`;
|
|
};
|
|
|
|
// 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 ?
|
|
`<div class="dh-spotlight-indicator"><i class="fas fa-hand-sparkles"></i></div>` : "";
|
|
|
|
html += `
|
|
<div class="dh-party-card ${turnClass}">
|
|
${spotlightHtml}
|
|
<div class="dh-card-img-wrapper">
|
|
<img class="dh-card-img" src="${img}" />
|
|
</div>
|
|
<div class="dh-card-content">
|
|
<div class="${headerClass}">
|
|
${nameHtml}
|
|
<div class="dh-card-meta">
|
|
${metaAncestry ? `<span class="dh-meta-ancestry">${metaAncestry}</span>` : ""}
|
|
</div>
|
|
<div class="dh-card-meta">
|
|
${metaClass ? `<span class="dh-meta-class">${metaClass}</span>` : ""}
|
|
</div>
|
|
</div>
|
|
|
|
${showRes ? `
|
|
<div class="dh-stat-row ${showResLabels ? 'with-labels' : ''}">
|
|
<div class="dh-stat stat-hp" title="HP">
|
|
${(showIcons || showResLabels) ? `<div class="dh-stat-icon">${iconHp} ${showResLabels ? '<span class="stat-label">HP</span>' : ''}</div>` : ''}
|
|
${renderRes(hp.value, hp.max, "hp")}
|
|
</div>
|
|
<div class="dh-stat stat-stress" title="Stress">
|
|
${(showIcons || showResLabels) ? `<div class="dh-stat-icon">${iconStress} ${showResLabels ? '<span class="stat-label">Stress</span>' : ''}</div>` : ''}
|
|
${renderRes(stress.value, stress.max, "stress")}
|
|
</div>
|
|
<div class="dh-stat stat-hope" title="Hope">
|
|
${(showIcons || showResLabels) ? `<div class="dh-stat-icon">${iconHope} ${showResLabels ? '<span class="stat-label">Hope</span>' : ''}</div>` : ''}
|
|
${renderRes(hope.value, hope.max, "hope")}
|
|
</div>
|
|
</div>` : ''}
|
|
|
|
${showAttr ? `
|
|
<div class="${attrClass}">
|
|
<div class="dh-attr"><span class="dh-attr-label">${lbl.agi}</span> <span class="dh-attr-val">${agi}</span></div>
|
|
<div class="dh-attr"><span class="dh-attr-label">${lbl.str}</span> <span class="dh-attr-val">${str}</span></div>
|
|
<div class="dh-attr"><span class="dh-attr-label">${lbl.fin}</span> <span class="dh-attr-val">${fin}</span></div>
|
|
<div class="dh-attr"><span class="dh-attr-label">${lbl.ins}</span> <span class="dh-attr-val">${inst}</span></div>
|
|
<div class="dh-attr"><span class="dh-attr-label">${lbl.pre}</span> <span class="dh-attr-val">${pre}</span></div>
|
|
<div class="dh-attr"><span class="dh-attr-label">${lbl.kno}</span> <span class="dh-attr-val">${kno}</span></div>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
});
|
|
|
|
html += `</div>`;
|
|
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 = `<legend>${game.i18n.localize("DH_STREAM_OVERLAY.Settings.MenuLabel")}</legend>`;
|
|
section.classList.add("dh-stream-overlay-menu-section");
|
|
const button = document.createElement("button");
|
|
button.innerHTML = `<i class="fas fa-video"></i> ${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 <ol>
|
|
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);
|
|
}
|
|
|
|
|