Add choice-based events with a resolution modal

Standard/kaos events can now present 2 options instead of a fixed
effect, pausing new event triggers until the player answers. Adds
4 choice events as a first batch; resources keep accruing while a
choice is pending.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-12 23:17:48 +02:00
parent 0efbe9d55b
commit b9561b05a5
5 changed files with 189 additions and 9 deletions

View File

@@ -1,4 +1,9 @@
import { STANDARD_EVENTS, KAOS_EVENTS, type GameEvent } from './events.ts';
export interface PendingEvent {
event: GameEvent;
kind: 'event' | 'kaos';
}
import { UPGRADES } from './upgrades.ts';
export interface State {
@@ -24,6 +29,7 @@ export interface State {
usedEventIds: Set<string>;
nextEventTick: number;
nextKaosTick: number;
pendingEvent: PendingEvent | null;
}
export interface LogEntry {
@@ -66,6 +72,7 @@ export function createState(): State {
usedEventIds: new Set(),
nextEventTick: EVENT_INTERVAL_TICKS,
nextKaosTick: EVENT_INTERVAL_TICKS * 3,
pendingEvent: null,
};
}
@@ -107,6 +114,12 @@ export function tick(state: State): State {
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
s.tick += 1;
// A choice event is awaiting the player's answer — freeze new events, keep resources flowing.
if (s.pendingEvent) {
s.nextEventTick += 1;
s.nextKaosTick += 1;
}
// Resource accumulation
s.andakt += s.andaktRate * s.troende * DT;
s.inflytande += INFLYTANDE_BASE_RATE * s.inflytandeRate * Math.pow(s.troende, 1.2) * DT;
@@ -124,24 +137,34 @@ export function tick(state: State): State {
s.hemlighet = Math.max(0, s.hemlighet - (troendeDrain + dreadDrain + ritualDrain) * DT);
// Standard event
if (s.tick >= s.nextEventTick) {
if (!s.pendingEvent && s.tick >= s.nextEventTick) {
const ev = pickEvent(STANDARD_EVENTS, s);
if (ev) {
s.log.unshift({ tick: s.tick, text: ev.text, kind: 'event' });
applyEffect(s, ev.effect);
s.usedEventIds.add(ev.id);
if (ev.choices?.length) {
s.log.unshift({ tick: s.tick, text: ev.text, kind: 'event' });
s.pendingEvent = { event: ev, kind: 'event' };
} else {
s.log.unshift({ tick: s.tick, text: ev.text, kind: 'event' });
applyEffect(s, ev.effect);
}
}
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
if (s.kaos >= KAOS_THRESHOLD || s.tick >= s.nextKaosTick) {
if (!s.pendingEvent && (s.kaos >= KAOS_THRESHOLD || s.tick >= s.nextKaosTick)) {
const ev = pickEvent(KAOS_EVENTS, s);
if (ev) {
s.log.unshift({ tick: s.tick, text: `${ev.text}`, kind: 'kaos' });
applyEffect(s, ev.effect);
s.usedEventIds.add(ev.id);
if (ev.choices?.length) {
s.log.unshift({ tick: s.tick, text: `${ev.text}`, kind: 'kaos' });
s.pendingEvent = { event: ev, kind: 'kaos' };
} else {
s.log.unshift({ tick: s.tick, text: `${ev.text}`, kind: 'kaos' });
applyEffect(s, ev.effect);
}
}
s.kaos = Math.max(0, s.kaos - 80);
s.nextKaosTick = s.tick + EVENT_INTERVAL_TICKS * 5 + Math.floor(Math.random() * 120);
@@ -161,6 +184,29 @@ export function tick(state: State): State {
return s;
}
export function resolveEventChoice(state: State, choiceIndex: number): State {
if (!state.pendingEvent) return state;
const choice = state.pendingEvent.event.choices?.[choiceIndex];
if (!choice) return state;
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
applyEffect(s, choice.effect);
s.log.unshift({ 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;
if (s.hemlighet <= 0) {
s.phase = 'lost';
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' });
} else if (s.ritual >= 100) {
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' });
}
return s;
}
interface LocationVisit {
text: string;
effect: Partial<{ andakt: number; dread: number; kaos: number; ritual: number; hemlighet: number }>;