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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user