135 lines
5.0 KiB
Markdown
135 lines
5.0 KiB
Markdown
# 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`.
|
|
|
|
### Automation
|
|
|
|
Automation is universal across action kinds once an action has at least its
|
|
configured number of successful completions (`automation.unlockAfterManualCompletions`,
|
|
default `1`). The engine stores those counts in
|
|
`GameState.manualCompletionCounts` and stores configured automation in
|
|
`GameState.automationQueue`.
|
|
|
|
`src/engine/automation.ts` owns unlock checks, queue mutation, and the runner.
|
|
The runner preserves precedence: manual active/queued actions first,
|
|
automation second, loop-idle actions last. `tickGame` can start automation when
|
|
the manual queue exhausts, including during offline catch-up; loop actions still
|
|
start only from the live runtime path.
|
|
|
|
`src/engine/recipe.ts` serializes automation queues as shareable text using
|
|
content action ids. The v1 multiline format is:
|
|
|
|
```text
|
|
idlegame-recipe/v1
|
|
# name: Camp loop
|
|
gather_supplies
|
|
rest
|
|
```
|
|
|
|
The single-line alias is:
|
|
|
|
```text
|
|
idlegame-recipe/v1:gather_supplies,rest
|
|
```
|
|
|
|
Import rejects unknown action ids and actions that are not automation-unlocked
|
|
for the current save.
|
|
|
|
## Verification
|
|
|
|
Run the full local gate before pushing:
|
|
|
|
```powershell
|
|
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
|