26eb77a216
- src/sim/offline/: OfflineEventBlockSchema (Zod) + EMPTY_OFFLINE_EVENTS + aggregateOfflineEvent pure aggregator (D-19); 14 tests green - src/sim/garden/auto-harvest.ts: autoHarvestReadyPlants silent-mode branch (D-10); reuses harvest() pipeline so selector + Pitfall 10 unlocks + STRY-10 Lura gate all run identically; BLOCKER 3 invariant preserved (no lastTickAt writes); 7 tests green - simulateOneTick: ctx.silent triggers auto-harvest sweep before tick increment; active-play path unchanged (silent defaults false) - content/dialogue/season1/letter-from-the-garden.ink: authored skeleton with VAR plants_bloomed / fragment_titles / lura_was_here per D-17/D-18; bible voice, anti-FOMO compliant, 24h cap silent in voice (D-11) - ink-loader: loadInkStory union extended with letter-from-the-garden; separate letterStoryGlob for lazy code-split chunk; INK_VARIABLE_MAP gains plants_bloomed / fragment_titles / lura_was_here slots reading from session.pendingLetterEventBlock - src/ui/letter/letter-renderer.ts: pure buildLetterSlots helper — prefers fragment first-sentence body for tonal weight, slugified-id fallback; 10 tests green - npm run compile:ink emits 5 .ink.json files (was 4); Vite emits the letter as a separate lazy chunk (letter-from-the-garden.ink-*.js) - 295/295 tests green (was 264; +31 new); npm run ci exits 0
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* sim/offline/events — OfflineEventBlock Zod runtime validator + pure
|
|
* aggregator. Per CONTEXT D-19, D-10, D-11.
|
|
*
|
|
* Phase 2 ships the minimum slot vocabulary that the letter Ink template
|
|
* (UX-02) needs: per-plant counts of plants bloomed, the list of
|
|
* auto-harvested fragment ids, and a flag for any newly-unlocked Lura
|
|
* beat queued for first-visit. Phase 4+ may add more if playtest
|
|
* demands.
|
|
*
|
|
* Structurally compatible with the OfflineEventBlock interface declared
|
|
* in src/save/migrations.ts (Plan 02-01) — that one is the type the
|
|
* save layer carries; this file is the runtime validator + aggregator.
|
|
*
|
|
* Pure. Imports only zod. CORE-10 firewall + Phase-2 sim-purity rule
|
|
* still apply: no Date.now, no setInterval, no DOM, no fetch.
|
|
*/
|
|
export const OfflineEventBlockSchema = z.object({
|
|
plantsBloomedCount: z.record(z.string(), z.number().int().nonnegative()),
|
|
harvestedFragmentIds: z.array(z.string().regex(/^season\d+\.[a-z0-9._-]+$/)),
|
|
luraBeatPending: z.enum(['arrival', 'mid', 'farewell']).nullable(),
|
|
});
|
|
|
|
export type OfflineEventBlock = z.infer<typeof OfflineEventBlockSchema>;
|
|
|
|
/**
|
|
* Frozen empty block. The boot path uses this as the seed for the silent
|
|
* catch-up loop's offlineEvents accumulator. Object.freeze prevents
|
|
* accidental mutation across catchup boundaries.
|
|
*/
|
|
export const EMPTY_OFFLINE_EVENTS: OfflineEventBlock = Object.freeze({
|
|
plantsBloomedCount: Object.freeze({}) as Record<string, number>,
|
|
harvestedFragmentIds: Object.freeze([]) as unknown as string[],
|
|
luraBeatPending: null,
|
|
});
|
|
|
|
/**
|
|
* aggregateOfflineEvent — pure combiner for a single auto-harvest event
|
|
* during the silent-mode catchup loop.
|
|
*
|
|
* Returns a NEW OfflineEventBlock with:
|
|
* - plantsBloomedCount[plantTypeId] incremented by 1
|
|
* - fragmentId appended to harvestedFragmentIds
|
|
* - luraBeatPending: prev's value preserved unless the incoming
|
|
* `luraBeatPending` is non-null (in which case the most recent
|
|
* pending beat wins — Phase 2 has at most one pending beat at a
|
|
* time per advanceLuraBeatProgress's invariant in
|
|
* src/sim/narrative/lura-gate.ts).
|
|
*
|
|
* Per CONTEXT D-17/D-19 — this is the slot vocabulary the letter Ink
|
|
* template renders.
|
|
*/
|
|
export function aggregateOfflineEvent(
|
|
prev: OfflineEventBlock,
|
|
plantTypeId: string,
|
|
fragmentId: string,
|
|
luraBeatPending: OfflineEventBlock['luraBeatPending'],
|
|
): OfflineEventBlock {
|
|
const counts = { ...prev.plantsBloomedCount };
|
|
counts[plantTypeId] = (counts[plantTypeId] ?? 0) + 1;
|
|
return {
|
|
plantsBloomedCount: counts,
|
|
harvestedFragmentIds: [...prev.harvestedFragmentIds, fragmentId],
|
|
luraBeatPending: luraBeatPending ?? prev.luraBeatPending,
|
|
};
|
|
}
|