diff --git a/apps/event-91-night-crawler/index.html b/apps/event-91-night-crawler/index.html index bbedce6..a031ec1 100644 --- a/apps/event-91-night-crawler/index.html +++ b/apps/event-91-night-crawler/index.html @@ -1509,6 +1509,364 @@ function toggleCard(head) { // ============================================================ const ACT_DATA = JSON.parse(document.getElementById('act-data-json').textContent); const REF_DATA = JSON.parse(document.getElementById('ref-data-json').textContent); +function switchAct(id, btn) { + document.querySelectorAll('#act-tabs .act-tab').forEach(t => t.classList.remove('active')); + if (btn) btn.classList.add('active'); + renderActContent(id); +} + +function switchRef(id, btn) { + document.querySelectorAll('#screen-ref .act-tab').forEach(t => t.classList.remove('active')); + if (btn) btn.classList.add('active'); + renderRefContent(id); +} + +function renderActContent(id) { + const data = ACT_DATA[id]; + const container = document.getElementById('act-content'); + let html = `
${data.title}
`; + if (data.goal) html += `
Goal: ${data.goal}
`; + + // Locations with visited state + if (data.locations) { + data.locations.forEach(loc => { + const visited = locationState[loc.id] || false; + html += `
+
${visited ? '✓' : ''}
+
+
${loc.name}
+
${loc.sub}
+
+ +
+ `; + }); + } + + // Sections (for intro/epilogue) + if (data.sections) { + data.sections.forEach(sec => { + html += `
+
${sec.title}
+ ▼ +
${sec.content}
`; + }); + } + + // Optional encounter (Act II) + if (data.optional) { + html += `
+
${data.optional.title}
Optional — skip if running behind
+ ▼ +
${data.optional.content}
`; + } + + // Moral choice (Act III) + if (data.moral) { + html += `
+
+
${data.moral.title}
+ ▼ +
+
`; + data.moral.options.forEach(opt => { + const col = opt.color === 'red' ? 'var(--red)' : opt.color === 'amber' ? 'var(--amber)' : 'var(--text-sub)'; + html += `
+
${opt.name}
+
${opt.desc}
+
`; + }); + html += '
If players try all three: the maths don\'t work. 10 min to rig + Reth + firefight upstairs exceeds any safe timeline. Two of three. Not all three.
'; + html += '
'; + } + + // Notes + const noteKey = id === 'act1' ? 'act1' : id === 'act2' ? 'act2' : id === 'act3' ? 'act3' : 'intro'; + html += `
+
GM Notes — ${data.title}
+ +
`; + + // Pacing + if (data.pacing) { + html += `
⏱ PACING: ${data.pacing}
`; + } + + container.innerHTML = html; +} + +function toggleLocation(id) { + locationState[id] = !locationState[id]; + saveState(); + renderActContent(document.querySelector('#act-tabs .act-tab.active').textContent.toLowerCase().replace('act ', 'act').replace(' ', '')); +} + +function toggleLocDetail(id) { + const el = document.getElementById(id); + if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none'; +} + +// ============================================================ +// NPCs +// ============================================================ +function buildNPCs() { + const container = document.getElementById('npc-list'); + container.innerHTML = NPCS.map(npc => { + const srcMatch = npc.portrait ? npc.portrait.match(/src="([^"]+)"/) : null; + const imgSrc = srcMatch ? srcMatch[1] : ''; + const state = npcState[npc.name] || {}; + const introduced = state.introduced || false; + const pinned = pinnedItems.some(p => p.id === npc.name); + + return `
+
+ ${imgSrc ? `
+ ${npc.portrait} +
⛶ Show
+
` : ''} +
+
${npc.name}
+
${npc.role} · ${npc.act}
+
+ ${npc.dispLabel} + ✓ Introduced + ${pinned ? '📌 Pinned' : ''} +
+
+ ▼ +
+ +
`; + }).join(''); +} + +function toggleNpcCard(head) { + const body = head.nextElementSibling; + const ch = head.querySelector('.chevron'); + body.style.display = body.style.display === 'none' ? 'block' : 'none'; + if (ch) ch.classList.toggle('open', body.style.display !== 'none'); +} + +function markIntroduced(name) { + npcState[name] = npcState[name] || {}; + npcState[name].introduced = !npcState[name].introduced; + saveState(); + buildNPCs(); +} + +// ============================================================ +// PORTRAITS +// ============================================================ +let currentPortraitName = ''; + +function openPortrait(src, name) { + currentPortraitName = name; + document.getElementById('portrait-overlay-img').src = src; + document.getElementById('portrait-overlay-name').textContent = name; + const state = npcState[name] || {}; + const btn = document.getElementById('portrait-shown-btn'); + btn.textContent = state.shownPortrait ? '✓ Shown' : 'Mark Shown to Players'; + btn.className = 'overlay-action-btn' + (state.shownPortrait ? ' shown' : ''); + document.getElementById('portrait-overlay').classList.add('active'); +} + +function markPortraitShown() { + npcState[currentPortraitName] = npcState[currentPortraitName] || {}; + npcState[currentPortraitName].shownPortrait = true; + const btn = document.getElementById('portrait-shown-btn'); + btn.textContent = '✓ Shown'; + btn.className = 'overlay-action-btn shown'; + saveState(); +} + +function closeOverlay(type) { + document.getElementById(type + '-overlay').classList.remove('active'); +} + +// ============================================================ +// CLUES +// ============================================================ +function buildClues() { + document.getElementById('clue-essential-list').innerHTML = CLUES_ESSENTIAL.map(c => clueHTML(c, 'essential')).join(''); + document.getElementById('clue-optional-list').innerHTML = CLUES_OPTIONAL.map(c => clueHTML(c, 'optional')).join(''); + updateClueBadge(); +} + +function clueHTML(c, type) { + const state = clueState[c.id] || 'none'; + const icon = state === 'found' ? '✓' : state === 'missed' ? '✕' : ''; + return `
+
${icon}
+
+
${c.name} ${type === 'essential' ? '⚠ ESSENTIAL' : ''}
+
📍 ${c.location} · ${c.how}
+
${c.reveals}
+
+
`; +} + +function cycleClue(id) { + const states = ['none', 'found', 'missed']; + clueState[id] = states[(states.indexOf(clueState[id] || 'none') + 1) % 3]; + saveState(); + buildClues(); +} + +function updateClueBadge() { + const found = Object.values(clueState).filter(v => v === 'found').length; + const total = CLUES_ESSENTIAL.length; + const badge = document.getElementById('badge-clues'); + if (badge) { + badge.textContent = `${found}/${total}`; + badge.className = 'nav-badge' + (found === total ? ' ' : found > 0 ? ' amber' : ' red'); + } +} + +// ============================================================ +// HANDOUTS +// ============================================================ +let currentHandoutId = ''; + +function buildHandouts() { + const container = document.getElementById('handout-list'); + container.innerHTML = HANDOUTS.map(h => { + const shown = handoutState[h.id] && handoutState[h.id].shown; + return `
+
+
+
${h.id} · ${h.when}
+
${h.title}
+
+
✓ Shown
+ ▼ +
+
+
${h.content}
+
+ + +
+
+
`; + }).join(''); +} + +function toggleHandout(bodyId, head) { + const body = document.getElementById(bodyId); + const ch = head.querySelector('.chevron'); + body.classList.toggle('open'); + if (ch) ch.classList.toggle('open'); +} + +function openHandout(id) { + const h = HANDOUTS.find(h => h.id === id); + if (!h) return; + currentHandoutId = id; + document.getElementById('handout-overlay-title').textContent = h.title; + const textEl = document.getElementById('handout-overlay-text'); + textEl.textContent = h.content; + textEl.className = h.style === 'terminal' ? 'terminal' : ''; + const shown = handoutState[id] && handoutState[id].shown; + const btn = document.getElementById('handout-shown-btn'); + btn.textContent = shown ? '✓ Shown' : 'Mark Shown to Players'; + btn.className = 'overlay-action-btn' + (shown ? ' shown' : ''); + document.getElementById('handout-overlay').classList.add('active'); +} + +function markHandoutShown() { + markHandoutShownById(currentHandoutId); + const btn = document.getElementById('handout-shown-btn'); + btn.textContent = '✓ Shown'; + btn.className = 'overlay-action-btn shown'; +} + +function markHandoutShownById(id) { + handoutState[id] = handoutState[id] || {}; + handoutState[id].shown = true; + saveState(); + buildHandouts(); +} + +// ============================================================ +// PLAYERS +// ============================================================ +function buildPlayers() { + const grid = document.getElementById('pc-grid'); + grid.innerHTML = pcState.map((pc, i) => ` +
+
${pc.name}
+
${pc.player}
+
+
+
HP
+
${pc.hp}
+
+
+
SAN
+
${pc.san}
+
+
+
PP
+
${pc.pp}
+
+
+ ${['hp','san','pp'].map(stat => ` +
+ ${stat.toUpperCase()} + + ${pc[stat]} + +
`).join('')} +
+
Injured
+
Insane
+
Dead
+
+
`).join(''); +} + +function hpCol(v,m) { const p=v/m; return p>.5?'var(--green)':p>.25?'var(--amber)':'var(--red)'; } +function sanCol(v,m){ const p=v/m; return p>.6?'var(--text)':p>.3?'var(--amber)':'var(--red)'; } + +function adj(i, stat, d) { + const maxKey = 'max'+stat.charAt(0).toUpperCase()+stat.slice(1); + pcState[i][stat] = Math.max(0, Math.min(pcState[i][maxKey]||99, pcState[i][stat]+d)); + document.getElementById(stat+'-n-'+i).textContent = pcState[i][stat]; + document.getElementById(stat+'-d-'+i).textContent = pcState[i][stat]; + if (stat==='hp') document.getElementById(stat+'-d-'+i).style.color = hpCol(pcState[i].hp, pcState[i].maxHp); + if (stat==='san') document.getElementById(stat+'-d-'+i).style.color = sanCol(pcState[i].san, pcState[i].maxSan); + saveState(); +} + +function toggleStatus(i, status) { + pcState[i].status[status] = !pcState[i].status[status]; + saveState(); + buildPlayers(); +} + +// ============================================================ +// REFERENCE +// ============================================================ function renderRefContent(id) { document.getElementById('ref-content').innerHTML = REF_DATA[id] || ''; }