4.0 KiB
Architecture
Idlegame is split into four layers.
Engine
src/engine/ contains deterministic gameplay logic:
tickLoop.ts: fixed-timestep accumulator. Large elapsed deltas, including offline catch-up, drain through the same tick path as active play.game.ts: core game state, action queue, costs/unlocks on start, completion-driven queue advancement (actions do not auto-repeat when the queue is empty).save.ts: versioned save schema, serialized export/import strings, and offline elapsed calculation.story.ts: story graph traversal, triggers (boot, actionComplete, minResources), choices, outcomes.num.ts: branded numeric boundary and human-readable formatting.
The engine must stay pure. It does not import React, Zustand, browser APIs, storage, requestAnimationFrame, or Date scheduling.
Content
src/content/ contains authored definitions validated by Zod. M0 includes one
resource and one timed action. M1 expands this into real resources, actions,
story nodes, automation unlocks, and prestige definitions.
Each action carries a behavior kind (instant, loop, timed, story,
context) and a group ({ id, label }) used for UI layout. The schema
enforces kind-specific invariants: timed/loop require durationMs, timed
requires at least one yield, and story requires a storyChoiceId linking the
action to a choice on the current story node.
State
src/state/ owns environment coupling:
- persistence backend selection: IndexedDB via
idb-keyval, with localStorage fallback - load-on-boot and autosave orchestration
- requestAnimationFrame loop and lifecycle hooks
- mapping engine state to view models
- Zustand store updates for React
storyOrchestration.ts: evaluates triggers after boot/publish/action completion; maps prefs to panel auto-open.- Player prefs (
prefs.ts) in localStorage — not in save v1.
UI
src/ui/ renders the view model and calls runtime commands. Components should
not implement gameplay rules.
Shell layout
PR3 replaces the M0/PR2 single-column overlay with a three-region shell
(AppShell.tsx): a left nav rail (Play / Story / Settings / About), a
center panel for the active tab, and a right rail shown on Play
(resources, an inventory placeholder, and an optional event log gated by the
showEventLog pref). store.activePanel selects the center panel; there is no
modal overlay. New story beats raise a nav badge (storyHasUnread), and
storyOpenMode: auto switches to the Story tab instead of opening an overlay.
Action kinds
Play organizes actions into columns by behavior kind, ordered
instant → loop → timed → story → context, with collapsible theme groups
inside each column (collapse state persists in prefs.collapsedActionGroups).
The pure engine dispatches each kind through performAction (game.ts):
instant applies costs/yields immediately, timed enqueues, loop toggles an
entry in enabledLoopActionIds (an idle runner starts the highest-priority
affordable loop only when the queue is empty and only during live ticks, never
offline), and story applies the linked choice. Story forks are taken only
through Story-kind actions; the Story tab itself is read-only.
Story tab
StoryView.tsx is a read-only 60/40 split: a branching StoryTree built from
the story graph's choice/trigger edges (seen paths emphasized, unseen dimmed) and
a scrollable StoryProseLog with no choice buttons. The boot intro shows a
single Continue affordance that advances boot_intro → fork_choice; thereafter
progression is driven by Story-kind actions.
The full design lives in docs/superpowers/specs/2026-06-11-m1-pr3-shell-ui-design.md.
Verification
Run the full local gate before pushing:
pnpm typecheck
pnpm lint
pnpm test:coverage
pnpm build
Rendered checks for the walking skeleton:
- app loads without framework overlay or console errors
- starting the timed action increments Gold after completion
- reload preserves saved state
- reopening after time away credits offline progress