Fix silently-dropped log entries past the 40-line cap, add pause, clarify locations panel
All checks were successful
Docker / build-and-push (push) Successful in 42s
All checks were successful
Docker / build-and-push (push) Successful in 42s
- The log view only re-rendered when state.log.length changed, but the log is capped at 40 entries — so once a run had logged 40+ things (easy in a normal session), every entry after that point, including choice-event outcomes, upgrade purchases, and location visits, stopped appearing even though the underlying state updated. Fixed by tracking a monotonic logSeq counter that increments on every push regardless of the cap, and rendering off that instead of array length. Consolidated the repeated unshift+cap logic into one pushLog() helper so future call sites can't reintroduce the bug. - Added a pause button (and "PAUSAD" indicator) that freezes the tick loop without touching game state, so a hectic run can be paused to think without it counting as a save-scummy state mutation. Manual actions (buying upgrades, visiting locations) still work while paused, since that's usually the point of pausing. - The "Aktiva områden" sidebar only ever listed the 3 story-gated core areas, while the map has 7 clickable locations — making it look like 4 of the map pins didn't really exist. Split the panel into "Ritualens kärnplatser" (the 3 gated ones, unlock status shown as before) and "Övriga platser" (the 4 always-open flavor locations), so the sidebar and map agree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ export interface State {
|
|||||||
nextEventTick: number;
|
nextEventTick: number;
|
||||||
nextKaosTick: number;
|
nextKaosTick: number;
|
||||||
pendingEvent: PendingEvent | null;
|
pendingEvent: PendingEvent | null;
|
||||||
|
logSeq: number; // increments on every log push — log.length alone can't detect changes once capped at 40
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
@@ -75,6 +76,7 @@ export function createState(): State {
|
|||||||
nextEventTick: EVENT_INTERVAL_TICKS,
|
nextEventTick: EVENT_INTERVAL_TICKS,
|
||||||
nextKaosTick: EVENT_INTERVAL_TICKS * 3,
|
nextKaosTick: EVENT_INTERVAL_TICKS * 3,
|
||||||
pendingEvent: null,
|
pendingEvent: null,
|
||||||
|
logSeq: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +86,10 @@ export function startGame(state: State): State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function abandonGame(state: State): State {
|
export function abandonGame(state: State): State {
|
||||||
return {
|
const s = { ...state, log: [...state.log] };
|
||||||
...state,
|
s.phase = 'abandoned';
|
||||||
phase: 'abandoned',
|
pushLog(s, { tick: s.tick, text: 'Ni lämnar källaren. Cirkeln bryts. Den Sovande sover vidare, ovetande, ofärdig.', kind: 'system' });
|
||||||
log: [{ tick: state.tick, text: 'Ni lämnar källaren. Cirkeln bryts. Den Sovande sover vidare, ovetande, ofärdig.', kind: 'system' }, ...state.log],
|
return s;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickEvent(pool: GameEvent[], state: State): GameEvent | null {
|
function pickEvent(pool: GameEvent[], state: State): GameEvent | null {
|
||||||
@@ -114,6 +115,12 @@ function pickEvent(pool: GameEvent[], state: State): GameEvent | null {
|
|||||||
return available[available.length - 1];
|
return available[available.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pushLog(state: State, entry: LogEntry): void {
|
||||||
|
state.log.unshift(entry);
|
||||||
|
state.logSeq += 1;
|
||||||
|
if (state.log.length > 40) state.log = state.log.slice(0, 40);
|
||||||
|
}
|
||||||
|
|
||||||
function applyEffect(state: State, effect: GameEvent['effect']): void {
|
function applyEffect(state: State, effect: GameEvent['effect']): void {
|
||||||
if (effect.andakt) state.andakt = Math.max(0, state.andakt + effect.andakt);
|
if (effect.andakt) state.andakt = Math.max(0, state.andakt + effect.andakt);
|
||||||
if (effect.hemlighet) state.hemlighet = Math.max(0, Math.min(100, state.hemlighet + effect.hemlighet));
|
if (effect.hemlighet) state.hemlighet = Math.max(0, Math.min(100, state.hemlighet + effect.hemlighet));
|
||||||
@@ -159,15 +166,14 @@ export function tick(state: State): State {
|
|||||||
if (ev) {
|
if (ev) {
|
||||||
s.usedEventIds.add(ev.id);
|
s.usedEventIds.add(ev.id);
|
||||||
if (ev.choices?.length) {
|
if (ev.choices?.length) {
|
||||||
s.log.unshift({ tick: s.tick, text: ev.text, kind: 'event' });
|
pushLog(s, { tick: s.tick, text: ev.text, kind: 'event' });
|
||||||
s.pendingEvent = { event: ev, kind: 'event' };
|
s.pendingEvent = { event: ev, kind: 'event' };
|
||||||
} else {
|
} else {
|
||||||
s.log.unshift({ tick: s.tick, text: ev.text, kind: 'event' });
|
pushLog(s, { tick: s.tick, text: ev.text, kind: 'event' });
|
||||||
applyEffect(s, ev.effect);
|
applyEffect(s, ev.effect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.nextEventTick = s.tick + EVENT_INTERVAL_TICKS + Math.floor(Math.random() * 60);
|
s.nextEventTick = s.tick + EVENT_INTERVAL_TICKS + Math.floor(Math.random() * 60);
|
||||||
if (s.log.length > 40) s.log = s.log.slice(0, 40);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kaos event
|
// Kaos event
|
||||||
@@ -176,26 +182,25 @@ export function tick(state: State): State {
|
|||||||
if (ev) {
|
if (ev) {
|
||||||
s.usedEventIds.add(ev.id);
|
s.usedEventIds.add(ev.id);
|
||||||
if (ev.choices?.length) {
|
if (ev.choices?.length) {
|
||||||
s.log.unshift({ tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' });
|
pushLog(s, { tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' });
|
||||||
s.pendingEvent = { event: ev, kind: 'kaos' };
|
s.pendingEvent = { event: ev, kind: 'kaos' };
|
||||||
} else {
|
} else {
|
||||||
s.log.unshift({ tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' });
|
pushLog(s, { tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' });
|
||||||
applyEffect(s, ev.effect);
|
applyEffect(s, ev.effect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.kaos = Math.max(0, s.kaos - 80);
|
s.kaos = Math.max(0, s.kaos - 80);
|
||||||
s.nextKaosTick = s.tick + EVENT_INTERVAL_TICKS * 5 + Math.floor(Math.random() * 120);
|
s.nextKaosTick = s.tick + EVENT_INTERVAL_TICKS * 5 + Math.floor(Math.random() * 120);
|
||||||
if (s.log.length > 40) s.log = s.log.slice(0, 40);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Win / lose checks
|
// Win / lose checks
|
||||||
if (s.ritual >= 100) {
|
if (s.ritual >= 100) {
|
||||||
s.phase = 'won';
|
s.phase = 'won';
|
||||||
s.log.unshift({ tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
|
pushLog(s, { tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
|
||||||
} else if (s.hemlighet <= 0) {
|
} else if (s.hemlighet <= 0) {
|
||||||
s.phase = 'lost';
|
s.phase = 'lost';
|
||||||
s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
|
s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
|
||||||
s.log.unshift({ tick: s.tick, text: s.lostReason, kind: 'system' });
|
pushLog(s, { tick: s.tick, text: s.lostReason, kind: 'system' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
@@ -209,17 +214,16 @@ export function resolveEventChoice(state: State, choiceIndex: number): State {
|
|||||||
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
|
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
|
||||||
applyEffect(s, choice.effect);
|
applyEffect(s, choice.effect);
|
||||||
if (choice.flag) s.flags.add(choice.flag);
|
if (choice.flag) s.flags.add(choice.flag);
|
||||||
s.log.unshift({ tick: s.tick, text: `↳ ${choice.resultText}`, kind: s.pendingEvent!.kind });
|
pushLog(s, { tick: s.tick, text: `↳ ${choice.resultText}`, kind: s.pendingEvent!.kind });
|
||||||
if (s.log.length > 40) s.log = s.log.slice(0, 40);
|
|
||||||
s.pendingEvent = null;
|
s.pendingEvent = null;
|
||||||
|
|
||||||
if (s.hemlighet <= 0) {
|
if (s.hemlighet <= 0) {
|
||||||
s.phase = 'lost';
|
s.phase = 'lost';
|
||||||
s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
|
s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
|
||||||
s.log.unshift({ tick: s.tick, text: s.lostReason, kind: 'system' });
|
pushLog(s, { tick: s.tick, text: s.lostReason, kind: 'system' });
|
||||||
} else if (s.ritual >= 100) {
|
} else if (s.ritual >= 100) {
|
||||||
s.phase = 'won';
|
s.phase = 'won';
|
||||||
s.log.unshift({ tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
|
pushLog(s, { tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
@@ -323,8 +327,7 @@ export function visitLocation(state: State, id: string, cooldowns: Map<string, n
|
|||||||
if (visit.effect.hemlighet) s.hemlighet = Math.max(0, Math.min(100, s.hemlighet + visit.effect.hemlighet));
|
if (visit.effect.hemlighet) s.hemlighet = Math.max(0, Math.min(100, s.hemlighet + visit.effect.hemlighet));
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.unshift({ tick: now, text: `⬡ ${text}`, kind: 'system' });
|
pushLog(s, { tick: now, text: `⬡ ${text}`, kind: 'system' });
|
||||||
if (s.log.length > 40) s.log = s.log.slice(0, 40);
|
|
||||||
|
|
||||||
const newCooldowns = new Map(cooldowns);
|
const newCooldowns = new Map(cooldowns);
|
||||||
newCooldowns.set(id, now + VISIT_COOLDOWN_TICKS);
|
newCooldowns.set(id, now + VISIT_COOLDOWN_TICKS);
|
||||||
@@ -355,6 +358,7 @@ export function loadState(): State | null {
|
|||||||
upgrades: new Set<string>(d.upgrades ?? []),
|
upgrades: new Set<string>(d.upgrades ?? []),
|
||||||
usedEventIds: new Set<string>(d.usedEventIds ?? []),
|
usedEventIds: new Set<string>(d.usedEventIds ?? []),
|
||||||
flags: new Set<string>(d.flags ?? []),
|
flags: new Set<string>(d.flags ?? []),
|
||||||
|
logSeq: d.logSeq ?? 0,
|
||||||
} as State;
|
} as State;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
@@ -380,7 +384,6 @@ export function buyUpgrade(state: State, id: string): State {
|
|||||||
if (cost.inflytande) s.inflytande -= cost.inflytande;
|
if (cost.inflytande) s.inflytande -= cost.inflytande;
|
||||||
upgrade.effect(s);
|
upgrade.effect(s);
|
||||||
if (upgrade.once) s.upgrades.add(id);
|
if (upgrade.once) s.upgrades.add(id);
|
||||||
s.log.unshift({ tick: s.tick, text: `↳ ${upgrade.name}: ${upgrade.desc}`, kind: 'upgrade' });
|
pushLog(s, { tick: s.tick, text: `↳ ${upgrade.name}: ${upgrade.desc}`, kind: 'upgrade' });
|
||||||
if (s.log.length > 40) s.log = s.log.slice(0, 40);
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ export const STANDARD_EVENTS: GameEvent[] = [
|
|||||||
export const KAOS_EVENTS: GameEvent[] = [
|
export const KAOS_EVENTS: GameEvent[] = [
|
||||||
{
|
{
|
||||||
id: 'k_goran_dog',
|
id: 'k_goran_dog',
|
||||||
text: 'Göran (från Discord-incidenten) har tagit med sig sin hund "Bamse" till ritualen. Bamse verkar inte störd av chantingen. Tvärtom. Bamse leder nu det andra kvartalets böner.',
|
text: 'Göran (från Discord-incidenten) har tagit med sig sin hund "Bamse" till ritualen. Bamse verkar inte störd av chantandet. Tvärtom. Bamse leder nu det andra kvartalets böner.',
|
||||||
effect: { kaos: -40, andakt: 20, troende: 1 },
|
effect: { kaos: -40, andakt: 20, troende: 1 },
|
||||||
weight: 10,
|
weight: 10,
|
||||||
},
|
},
|
||||||
@@ -746,7 +746,7 @@ export const KAOS_EVENTS: GameEvent[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'k_malmofestivalen',
|
id: 'k_malmofestivalen',
|
||||||
text: 'Malmöfestivalen har av misstag bokat oss för en scen vid Gustav Adolfs Torg. Chantingen tolkas som experimentell körmusik. Vi vinner publikens pris. Ingen av juryn minns att de röstade.',
|
text: 'Malmöfestivalen har av misstag bokat oss för en scen vid Gustav Adolfs Torg. Chantandet tolkas som experimentell körmusik. Vi vinner publikens pris. Ingen av juryn minns att de röstade.',
|
||||||
effect: { kaos: -35, andakt: 40, troende: 2, hemlighet: -12 },
|
effect: { kaos: -35, andakt: 40, troende: 2, hemlighet: -12 },
|
||||||
weight: 7,
|
weight: 7,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import { createState, tick, buyUpgrade, visitLocation, resolveEventChoice, startGame, abandonGame, saveState, loadState, clearSave } from './engine.ts';
|
import { createState, tick, buyUpgrade, visitLocation, resolveEventChoice, startGame, abandonGame, saveState, loadState, clearSave } from './engine.ts';
|
||||||
import { initUI, renderState } from './ui.ts';
|
import { initUI, renderState, setPausedUI } from './ui.ts';
|
||||||
import { initMap, updateMap } from './map.ts';
|
import { initMap, updateMap } from './map.ts';
|
||||||
|
|
||||||
let state = loadState() ?? createState();
|
let state = loadState() ?? createState();
|
||||||
let visitCooldowns = new Map<string, number>();
|
let visitCooldowns = new Map<string, number>();
|
||||||
|
let paused = false;
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
const TARGET_FPS = 30;
|
const TARGET_FPS = 30;
|
||||||
const FRAME_MS = 1000 / TARGET_FPS;
|
const FRAME_MS = 1000 / TARGET_FPS;
|
||||||
@@ -25,7 +26,8 @@ function start(): void {
|
|||||||
},
|
},
|
||||||
(index) => { state = resolveEventChoice(state, index); renderState(state); saveState(state); },
|
(index) => { state = resolveEventChoice(state, index); renderState(state); saveState(state); },
|
||||||
() => { state = startGame(state); renderState(state); saveState(state); },
|
() => { state = startGame(state); renderState(state); saveState(state); },
|
||||||
() => { state = abandonGame(state); renderState(state); saveState(state); }
|
() => { state = abandonGame(state); renderState(state); saveState(state); },
|
||||||
|
() => { paused = !paused; setPausedUI(paused); }
|
||||||
);
|
);
|
||||||
|
|
||||||
initMap('map', (locationId) => {
|
initMap('map', (locationId) => {
|
||||||
@@ -42,7 +44,7 @@ function start(): void {
|
|||||||
function loop(now: number): void {
|
function loop(now: number): void {
|
||||||
if (now - lastTime >= FRAME_MS) {
|
if (now - lastTime >= FRAME_MS) {
|
||||||
lastTime = now;
|
lastTime = now;
|
||||||
if (state.phase === 'playing') {
|
if (state.phase === 'playing' && !paused) {
|
||||||
state = tick(state);
|
state = tick(state);
|
||||||
renderState(state);
|
renderState(state);
|
||||||
updateMap(state.dread, state.areas);
|
updateMap(state.dread, state.areas);
|
||||||
|
|||||||
@@ -355,6 +355,22 @@ body {
|
|||||||
.phase-tag.mid { color: var(--accent); border-color: var(--accent); }
|
.phase-tag.mid { color: var(--accent); border-color: var(--accent); }
|
||||||
.phase-tag.late { color: var(--dread); border-color: var(--dread); }
|
.phase-tag.late { color: var(--dread); border-color: var(--dread); }
|
||||||
|
|
||||||
|
.pause-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--kaos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dim the world a touch while paused, so it reads as "held" rather than "broken" */
|
||||||
|
#app.paused #map-container,
|
||||||
|
#app.paused #panel-left,
|
||||||
|
#app.paused #panel-right {
|
||||||
|
filter: grayscale(0.5) brightness(0.75);
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Help / header buttons ── */
|
/* ── Help / header buttons ── */
|
||||||
.header-actions {
|
.header-actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|||||||
29
src/ui.ts
29
src/ui.ts
@@ -9,12 +9,14 @@ export function initUI(
|
|||||||
onEventChoice: (index: number) => void,
|
onEventChoice: (index: number) => void,
|
||||||
onStart: () => void,
|
onStart: () => void,
|
||||||
onAbandon: () => void,
|
onAbandon: () => void,
|
||||||
|
onTogglePause: () => void,
|
||||||
): void {
|
): void {
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div class="brand">DJUPET</div>
|
<div class="brand">DJUPET</div>
|
||||||
<div class="brand-sub">Malmö, Sverige · <span id="phase-tag" class="phase-tag early">Tidigt skede</span></div>
|
<div class="brand-sub">Malmö, Sverige · <span id="phase-tag" class="phase-tag early">Tidigt skede</span><span id="pause-tag" class="pause-tag"></span></div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<button id="pause-btn" class="help-btn" title="Pausa spelet">Pausa</button>
|
||||||
<button id="abandon-btn" class="help-btn" title="Överge kulten">Avbryt</button>
|
<button id="abandon-btn" class="help-btn" title="Överge kulten">Avbryt</button>
|
||||||
<button id="help-btn" class="help-btn" title="Hjälp & Ändringslogg">? v${VERSION}</button>
|
<button id="help-btn" class="help-btn" title="Hjälp & Ändringslogg">? v${VERSION}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,12 +128,21 @@ export function initUI(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" id="panel-right">
|
<div class="panel" id="panel-right">
|
||||||
<div class="panel-title">Aktiva områden</div>
|
<div class="panel-title">Ritualens kärnplatser</div>
|
||||||
<div id="areas" style="font-size:11px;color:var(--dim);line-height:2;">
|
<div id="areas" style="font-size:11px;color:var(--dim);line-height:2;">
|
||||||
<div id="area-rosengard">○ Rosengård</div>
|
<div id="area-rosengard">○ Rosengård</div>
|
||||||
<div id="area-varnhem">○ Värnhemstorget</div>
|
<div id="area-varnhem">○ Värnhemstorget</div>
|
||||||
<div id="area-torso">○ Turning Torso</div>
|
<div id="area-torso">○ Turning Torso</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:16px;">
|
||||||
|
<div class="panel-title">Övriga platser</div>
|
||||||
|
<div style="font-size:11px;color:var(--dim);line-height:2;">
|
||||||
|
<div>Øresundsbron</div>
|
||||||
|
<div>Ribersborgsstranden</div>
|
||||||
|
<div>Davidshallsgatan</div>
|
||||||
|
<div>Stortorget</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style="margin-top:20px;" id="investigator-section">
|
<div style="margin-top:20px;" id="investigator-section">
|
||||||
<div class="panel-title">Utredarna</div>
|
<div class="panel-title">Utredarna</div>
|
||||||
<div id="investigator-status" style="font-size:11px;color:var(--safe);">Inaktiva</div>
|
<div id="investigator-status" style="font-size:11px;color:var(--safe);">Inaktiva</div>
|
||||||
@@ -163,6 +174,7 @@ export function initUI(
|
|||||||
document.getElementById('abandon-btn')!.addEventListener('click', () => {
|
document.getElementById('abandon-btn')!.addEventListener('click', () => {
|
||||||
if (window.confirm('Överge kulten? Cirkeln bryts och ni går skilda vägar.')) onAbandon();
|
if (window.confirm('Överge kulten? Cirkeln bryts och ni går skilda vägar.')) onAbandon();
|
||||||
});
|
});
|
||||||
|
document.getElementById('pause-btn')!.addEventListener('click', onTogglePause);
|
||||||
|
|
||||||
document.getElementById('event-modal-choices')!.addEventListener('click', e => {
|
document.getElementById('event-modal-choices')!.addEventListener('click', e => {
|
||||||
const btn = (e.target as Element).closest('[data-choice]') as HTMLElement | null;
|
const btn = (e.target as Element).closest('[data-choice]') as HTMLElement | null;
|
||||||
@@ -320,12 +332,12 @@ function renderUpgrades(state: State): void {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastLogLength = 0;
|
let lastLogSeq = -1;
|
||||||
|
|
||||||
function renderLog(state: State): void {
|
function renderLog(state: State): void {
|
||||||
const log = document.getElementById('log')!;
|
const log = document.getElementById('log')!;
|
||||||
if (state.log.length === lastLogLength) return;
|
if (state.logSeq === lastLogSeq) return;
|
||||||
lastLogLength = state.log.length;
|
lastLogSeq = state.logSeq;
|
||||||
// state.log is newest-first (unshift); render as-is, scroll to top
|
// state.log is newest-first (unshift); render as-is, scroll to top
|
||||||
log.innerHTML = state.log.map(e =>
|
log.innerHTML = state.log.map(e =>
|
||||||
`<div class="log-entry ${e.kind}">${e.text}</div>`
|
`<div class="log-entry ${e.kind}">${e.text}</div>`
|
||||||
@@ -363,6 +375,13 @@ function showOverlay(state: State): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setPausedUI(paused: boolean): void {
|
||||||
|
const btn = document.getElementById('pause-btn')!;
|
||||||
|
btn.textContent = paused ? 'Fortsätt' : 'Pausa';
|
||||||
|
document.getElementById('app')!.classList.toggle('paused', paused);
|
||||||
|
setText('pause-tag', paused ? '⏸ Pausad' : '');
|
||||||
|
}
|
||||||
|
|
||||||
function setText(id: string, val: string): void {
|
function setText(id: string, val: string): void {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.textContent = val;
|
if (el) el.textContent = val;
|
||||||
|
|||||||
Reference in New Issue
Block a user