Add flag-gated NPC event chains for Broder Lindqvist and Syster Hassan

Events can now depend on a previously-set flag and a completed event
id, letting a choice made early in the game (e.g. whether Lindqvist's
whisper was heeded) shape which follow-up events surface later. Adds
two 3-step chains as a first example of persistent NPC arcs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-12 23:53:43 +02:00
parent b9561b05a5
commit 80fdebf102
2 changed files with 109 additions and 5 deletions

View File

@@ -27,6 +27,7 @@ export interface State {
phase: 'playing' | 'won' | 'lost';
lostReason?: string;
usedEventIds: Set<string>;
flags: Set<string>;
nextEventTick: number;
nextKaosTick: number;
pendingEvent: PendingEvent | null;
@@ -70,6 +71,7 @@ export function createState(): State {
log: [{ tick: 0, text: 'Mötet började i en fuktig källare i Rosengård. Nio stolar. En av dem är alltid lite för varm.', kind: 'system' }],
phase: 'playing',
usedEventIds: new Set(),
flags: new Set(),
nextEventTick: EVENT_INTERVAL_TICKS,
nextKaosTick: EVENT_INTERVAL_TICKS * 3,
pendingEvent: null,
@@ -85,6 +87,8 @@ function pickEvent(pool: GameEvent[], state: State): GameEvent | null {
if (r.minDread && state.dread < r.minDread) return false;
if (r.minRitual && state.ritual < r.minRitual) return false;
if (r.unlockedArea && !state.areas[r.unlockedArea]) return false;
if (r.flag && !state.flags.has(r.flag)) return false;
if (r.completedEventId && !state.usedEventIds.has(r.completedEventId)) return false;
return true;
});
if (!available.length) return null;
@@ -111,7 +115,7 @@ const DT = 1 / 30; // 30fps
export function tick(state: State): State {
if (state.phase !== 'playing') return state;
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
s.tick += 1;
// A choice event is awaiting the player's answer — freeze new events, keep resources flowing.
@@ -189,8 +193,9 @@ export function resolveEventChoice(state: State, choiceIndex: number): 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) };
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);
if (choice.flag) s.flags.add(choice.flag);
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;
@@ -264,7 +269,7 @@ export function visitLocation(state: State, id: string, cooldowns: Map<string, n
const now = state.tick;
if ((cooldowns.get(id) ?? 0) > now) return { state, cooldowns };
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
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 areaLocked = visit.requiresArea && !s.areas[visit.requiresArea];
const text = areaLocked ? visit.lockedText : visit.text;
@@ -292,6 +297,7 @@ export function saveState(state: State): void {
...state,
upgrades: [...state.upgrades],
usedEventIds: [...state.usedEventIds],
flags: [...state.flags],
log: state.log.slice(0, 20),
};
localStorage.setItem(SAVE_KEY, JSON.stringify(serial));
@@ -307,6 +313,7 @@ export function loadState(): State | null {
...d,
upgrades: new Set<string>(d.upgrades ?? []),
usedEventIds: new Set<string>(d.usedEventIds ?? []),
flags: new Set<string>(d.flags ?? []),
} as State;
} catch (_) {
return null;
@@ -326,7 +333,7 @@ export function buyUpgrade(state: State, id: string): State {
if (cost.dread && state.dread < cost.dread) return state;
if (cost.inflytande && state.inflytande < cost.inflytande) return state;
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
if (cost.andakt) s.andakt -= cost.andakt;
if (cost.dread) s.dread -= cost.dread;
if (cost.inflytande) s.inflytande -= cost.inflytande;