Files
djupet/src/ui.ts
T

290 lines
12 KiB
TypeScript
Raw Normal View History

2026-07-07 23:05:22 +02:00
import type { State } from './engine.ts';
import { UPGRADES } from './upgrades.ts';
import { VERSION, CHANGELOG } from './data/version.ts';
2026-07-07 23:05:22 +02:00
export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRestart: () => void): void {
app.innerHTML = `
<div id="header">
<div class="brand">DJUPET</div>
<div class="brand-sub">Malmö, Sverige · <span id="phase-tag" class="phase-tag early">Tidigt skede</span></div>
<button id="help-btn" class="help-btn" title="Hjälp &amp; Ändringslogg">? v${VERSION}</button>
</div>
<div id="help-modal" class="help-modal">
<div class="help-modal-inner" id="help-modal-inner">
<div class="help-modal-header">
<span class="help-modal-title">DJUPET — Hjälp &amp; Ändringslogg</span>
<button id="help-close" class="help-close">✕</button>
</div>
<div class="help-modal-body">
<section class="help-section">
<div class="help-section-title">Hur man spelar</div>
<ul class="help-list">
<li><strong>Andakt</strong> ackumuleras passivt. Använd den för att köpa åtgärder i vänsterpanelen.</li>
<li><strong>Dread</strong> ökar när ritualen fördjupas. Hög dread låser upp nya platser och händelser.</li>
<li><strong>Hemlighet</strong> sjunker när ni blir för uppenbara. Når den noll är det slut.</li>
<li><strong>Kaos</strong> byggs upp naturligt och utlöser kaotiska händelser när det når 100.</li>
<li><strong>Ritual</strong> låses upp via Skriften. Nå 100 för att fullborda ceremonin.</li>
<li>Klicka på <em>platser på kartan</em> för att besöka dem. Varje besök lämnar spår.</li>
<li>Händelser inträffar automatiskt. De flesta är dåliga. En del är värre.</li>
</ul>
</section>
<section class="help-section">
<div class="help-section-title">Om spelet</div>
<p class="help-flavor">
Djupet är ett idle-spel om en kult i Malmö, Sverige.<br>
Inspirerat av Anders Fager, Kult och Call of Cthulhu.<br>
Den Sovande är inte vaken ännu. Jobba på det.
</p>
</section>
<section class="help-section">
<div class="help-section-title">Ändringslogg</div>
${CHANGELOG.map(e => `
<div class="help-changelog-entry">
<div class="help-changelog-version">v${e.version} <span class="help-changelog-date">${e.date}</span></div>
<ul class="help-list">${e.changes.map(c => `<li>${c}</li>`).join('')}</ul>
</div>
`).join('')}
</section>
</div>
</div>
2026-07-07 23:05:22 +02:00
</div>
<div class="panel" id="panel-left">
<div class="panel-title">Resurser</div>
<div class="resource">
<div class="resource-label">Troende</div>
<div class="resource-value" id="r-troende">1</div>
</div>
<div class="resource">
<div class="resource-label">Andakt <span id="r-andakt-rate" style="color:var(--dim);font-size:10px;"></span></div>
<div class="resource-value" id="r-andakt">0</div>
</div>
<div class="resource">
<div class="resource-label">Inflytande</div>
<div class="resource-value" id="r-inflytande">0</div>
</div>
2026-07-07 23:05:22 +02:00
<div class="resource">
<div class="resource-label">Dread <span id="r-dread-rate" style="color:var(--dread);font-size:10px;"></span></div>
<div class="resource-value" style="color:var(--dread);" id="r-dread">0</div>
</div>
<div class="resource bar-hemlighet" style="margin-top:16px;">
<div class="resource-label">Hemlighet</div>
<div class="resource-value" id="r-hemlighet">100</div>
<div class="resource-bar"><div class="resource-bar-fill" id="bar-hemlighet" style="width:100%"></div></div>
</div>
<div class="resource bar-kaos">
<div class="resource-label">Kaos</div>
<div class="resource-value" id="r-kaos">0</div>
<div class="resource-bar"><div class="resource-bar-fill" id="bar-kaos" style="width:0%"></div></div>
</div>
<div class="resource bar-ritual" id="ritual-section" style="display:none;">
<div class="resource-label">Ritual</div>
<div class="resource-value" style="color:var(--ritual);" id="r-ritual">0</div>
<div class="resource-bar"><div class="resource-bar-fill" id="bar-ritual" style="width:0%"></div></div>
</div>
<div style="margin-top:20px;">
<div class="panel-title">Åtgärder</div>
<div id="upgrades"></div>
</div>
</div>
<div id="map-container">
<div id="map"></div>
<div id="log"></div>
</div>
<div class="panel" id="panel-right">
<div class="panel-title">Aktiva områden</div>
<div id="areas" style="font-size:11px;color:var(--dim);line-height:2;">
<div id="area-rosengard">○ Rosengård</div>
<div id="area-varnhem">○ Värnhemstorget</div>
<div id="area-torso">○ Turning Torso</div>
</div>
<div style="margin-top:20px;" id="investigator-section">
<div class="panel-title">Utredarna</div>
<div id="investigator-status" style="font-size:11px;color:var(--safe);">Inaktiva</div>
</div>
</div>
<div id="overlay">
<div class="overlay-title" id="overlay-title"></div>
<div class="overlay-body" id="overlay-body"></div>
<button class="overlay-btn" id="overlay-btn">Börja om</button>
</div>
`;
document.getElementById('overlay-btn')!.addEventListener('click', onRestart);
const helpModal = document.getElementById('help-modal')!;
const helpInner = document.getElementById('help-modal-inner')!;
document.getElementById('help-btn')!.addEventListener('click', () => helpModal.classList.toggle('visible'));
document.getElementById('help-close')!.addEventListener('click', () => helpModal.classList.remove('visible'));
helpModal.addEventListener('click', (e) => { if (!helpInner.contains(e.target as Node)) helpModal.classList.remove('visible'); });
document.addEventListener('keydown', (e) => { if (e.key === '?' || (e.key === 'Escape' && helpModal.classList.contains('visible'))) helpModal.classList.toggle('visible'); });
2026-07-07 23:05:22 +02:00
// Split the map container: map on top, log below
const mapContainer = document.getElementById('map-container')!;
mapContainer.style.cssText = 'display:flex;flex-direction:column;overflow:hidden;height:100%;';
2026-07-07 23:05:22 +02:00
const mapEl = document.getElementById('map')!;
mapEl.style.cssText = 'flex:0 0 45%;min-height:0;max-height:45%;';
2026-07-07 23:05:22 +02:00
const logEl = document.getElementById('log')!;
logEl.style.cssText = 'flex:1 1 0;overflow-y:auto;border-top:1px solid var(--border);min-height:0;';
2026-07-07 23:05:22 +02:00
// Upgrade click delegation
document.getElementById('upgrades')!.addEventListener('click', e => {
const btn = (e.target as Element).closest('[data-upgrade]') as HTMLElement | null;
if (btn?.dataset['upgrade']) onUpgrade(btn.dataset['upgrade']);
});
}
export function renderState(state: State): void {
setText('r-troende', String(state.troende));
setText('r-andakt', fmt(state.andakt));
setText('r-andakt-rate', `+${(state.andaktRate * state.troende).toFixed(1)}/s`);
setText('r-inflytande', fmt(state.inflytande));
2026-07-07 23:05:22 +02:00
setText('r-dread', fmt(state.dread));
setText('r-dread-rate', state.dreadRate > 0 ? `+${state.dreadRate.toFixed(1)}/s` : '');
setText('r-hemlighet', Math.floor(state.hemlighet).toString());
setText('r-kaos', Math.floor(state.kaos).toString());
setBar('bar-hemlighet', state.hemlighet);
setBar('bar-kaos', state.kaos);
const ritualSection = document.getElementById('ritual-section')!;
if (state.ritualUnlocked) {
ritualSection.style.display = '';
setText('r-ritual', Math.floor(state.ritual).toString());
setBar('bar-ritual', state.ritual);
}
// Phase tag
const phaseEl = document.getElementById('phase-tag')!;
if (state.ritual >= 50) {
phaseEl.textContent = 'Sent skede'; phaseEl.className = 'phase-tag late';
} else if (state.dread >= 30) {
phaseEl.textContent = 'Mitt skede'; phaseEl.className = 'phase-tag mid';
} else {
phaseEl.textContent = 'Tidigt skede'; phaseEl.className = 'phase-tag early';
}
// Areas
setArea('area-rosengard', state.areas.rosengard, 'Rosengård');
setArea('area-varnhem', state.areas.varnhem, 'Värnhemstorget');
setArea('area-torso', state.areas.torso, 'Turning Torso');
// Investigator status
const inv = document.getElementById('investigator-status')!;
if (state.hemlighet < 20) {
inv.textContent = 'Nära — väldigt nära'; inv.style.color = 'var(--danger)';
} else if (state.hemlighet < 50) {
inv.textContent = 'Misstänksamma'; inv.style.color = 'var(--kaos)';
2026-07-07 23:05:22 +02:00
} else {
inv.textContent = 'Inaktiva'; inv.style.color = 'var(--safe)';
}
// Upgrades
renderUpgrades(state);
// Log
renderLog(state);
// Overlay
if (state.phase !== 'playing') showOverlay(state);
}
let lastUpgradeKey = '';
2026-07-07 23:05:22 +02:00
function renderUpgrades(state: State): void {
const container = document.getElementById('upgrades')!;
const available = UPGRADES.filter(u => u.available(state) && (u.once ? !state.upgrades.has(u.id) : true));
// Only rebuild DOM when availability or affordability changes
const key = available.map(u => {
const cost = u.cost(state);
const canAfford = (!cost.andakt || state.andakt >= cost.andakt) && (!cost.dread || state.dread >= cost.dread) && (!cost.inflytande || state.inflytande >= cost.inflytande);
return `${u.id}:${canAfford}:${cost.andakt ?? ''}`;
}).join('|');
if (key === lastUpgradeKey) return;
lastUpgradeKey = key;
2026-07-07 23:05:22 +02:00
if (!available.length) {
container.innerHTML = '<div style="font-size:11px;color:var(--dim);">Inga tillgängliga åtgärder.</div>';
return;
}
container.innerHTML = available.map(u => {
const cost = u.cost(state);
const canAfford = (!cost.andakt || state.andakt >= cost.andakt) && (!cost.dread || state.dread >= cost.dread) && (!cost.inflytande || state.inflytande >= cost.inflytande);
2026-07-07 23:05:22 +02:00
const costs = [
cost.andakt ? `${cost.andakt} andakt` : '',
cost.dread ? `${cost.dread} dread` : '',
cost.inflytande ? `${cost.inflytande} inflytande` : '',
2026-07-07 23:05:22 +02:00
].filter(Boolean).join(', ');
return `<button class="upgrade-btn" data-upgrade="${u.id}" ${canAfford ? '' : 'disabled'}>
<span class="up-name">${u.name}</span>
<span class="up-desc">${u.desc}</span>
<span class="up-cost">Kostnad: ${costs || '—'}</span>
</button>`;
}).join('');
}
let lastLogLength = 0;
2026-07-07 23:05:22 +02:00
function renderLog(state: State): void {
const log = document.getElementById('log')!;
if (state.log.length === lastLogLength) return;
lastLogLength = state.log.length;
// state.log is newest-first (unshift); render as-is, scroll to top
2026-07-07 23:05:22 +02:00
log.innerHTML = state.log.map(e =>
`<div class="log-entry ${e.kind}">${e.text}</div>`
).join('');
log.scrollTop = 0;
2026-07-07 23:05:22 +02:00
}
function showOverlay(state: State): void {
const ov = document.getElementById('overlay')!;
ov.classList.add('visible');
const title = document.getElementById('overlay-title')!;
const body = document.getElementById('overlay-body')!;
if (state.phase === 'won') {
title.textContent = 'FULLBORDAT';
title.className = 'overlay-title won';
body.textContent = 'Det är gjort. Turning Torso pekar rakt ned. Havet är stilla — alltför stilla. Malmö fortsätter som vanligt. Det är det värsta.';
} else {
title.textContent = 'AVSLÖJAD';
title.className = 'overlay-title lost';
body.textContent = state.lostReason ?? 'Utredarna kom.';
}
}
function setText(id: string, val: string): void {
const el = document.getElementById(id);
if (el) el.textContent = val;
}
function setBar(id: string, pct: number): void {
const el = document.getElementById(id) as HTMLElement | null;
if (el) el.style.width = `${Math.min(100, Math.max(0, pct))}%`;
}
function setArea(id: string, active: boolean, label: string): void {
const el = document.getElementById(id);
if (el) {
el.textContent = `${active ? '●' : '○'} ${label}`;
el.style.color = active ? 'var(--dread)' : 'var(--dim)';
}
}
function fmt(n: number): string {
return Math.floor(n).toLocaleString('sv-SE');
}