// Choosing what happens this turn. // // There is deliberately no such thing as "a scripted turn" versus "a random // event" in here. Every turn, the whole event pool is filtered by stage and // conditions, then drawn from by weight. A routine turn is an event with a // high weight and loose conditions; a rare interruption is one with a low // weight and tight conditions; the "hard times" branch is one whose // conditions include a money threshold. The engine branches on none of it. import { check } from './conditions.js'; import { getPath } from './paths.js'; import { presentSlots } from './cast.js'; import { bindConditions } from './text.js'; /** * Pick, for each role an event `involves`, one person present in this run. An * event about "a co-worker" is about one particular co-worker for the turn. */ export function bindRoles(pack, state, event, rng) { const bindings = {}; for (const role of event.involves ?? []) { const slots = presentSlots(pack, state, role); if (slots.length) bindings[role] = slots.length === 1 ? slots[0] : rng.pick(slots); } return bindings; } /** Events that could fire right now, with the reason excluded ones could not. */ export function eligibleEvents(pack, state) { const eligible = []; const excluded = []; for (const event of pack.events ?? []) { if (event.stage !== undefined && event.stage !== state.stage) { excluded.push({ event, reason: 'stage' }); continue; } const seenCount = getPath(state, `seen.${event.id}`) ?? 0; if (event.once && seenCount > 0) { excluded.push({ event, reason: 'once' }); continue; } if (event.cooldown) { const lastTurn = getPath(state, `lastSeen.${event.id}`); if (lastTurn !== undefined && state.turn - lastTurn < event.cooldown) { excluded.push({ event, reason: 'cooldown' }); continue; } } const gate = check(state, event.requires); if (!gate.passed) { excluded.push({ event, reason: 'requires', failed: gate.failed }); continue; } // An event about a co-worker cannot fire in a run that drew none. if ((event.involves ?? []).some((role) => !presentSlots(pack, state, role).length)) { excluded.push({ event, reason: 'involves' }); continue; } eligible.push(event); } return { eligible, excluded }; } /** * Draw this turn's event. Returns undefined when nothing is eligible, which is * a content bug rather than a game state — the validator checks that at least * one unconditional event exists per stage so this cannot happen in practice. */ export function selectEvent(pack, state, rng) { const { eligible } = eligibleEvents(pack, state); return rng.weighted(eligible, (event) => event.weight ?? 1); } /** * Every option on an event, in authored order, marked available or locked and * carrying the conditions that locked it. The UI greys locked options out; the * feedback log records them, because what a player *could not* choose is the * strongest signal that the choice set is too narrow. */ export function resolveOptions(state, event) { const bindings = state?.meta?.bindings ?? {}; return (event.options ?? []).map((option) => { const gate = check(state, bindConditions(option.requires, bindings)); return { option, available: gate.passed, failed: gate.failed, hidden: !gate.passed && option.whenLocked === 'hide', }; }); } /** Look up one option on an event by id. */ export function findOption(event, optionId) { return (event.options ?? []).find((option) => option.id === optionId); }