docs(m1): add PR3 shell UI spec and implementation plans
Capture the PR3 UX brainstorm: three-region shell, action columns by kind, story-via-actions, Story tab tree+log, and universal automation with shareable recipes. Includes T3.0/T3.1 implementation plans and M1 vertical slice plan. Also ignore local .worktrees/.
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
# M1 PR2 — Playable Loop Design Spec
|
||||
|
||||
> **Status:** Approved by ginnoir (brainstorm 2026-06-11).
|
||||
> **Branch:** `feat/m1-playable-loop` off `main`.
|
||||
> **Closes:** Gitea #5 (Action queue UI), #6 (Story graph and first branch).
|
||||
> **Parent plan:** `docs/plans/2026-06-11-m1-vertical-slice.md` §PR2.
|
||||
|
||||
## Summary
|
||||
|
||||
PR2 turns the PR1 queue engine into the first **playable loop**: a hybrid story graph (choices, action-complete beats, boot, resource thresholds), a full-window visual-novel story panel with running story log, upgraded action queue UI with narrative context (subtitles/tooltips), and player preferences for story-open and action-detail modes. Stub prose and placeholder names prove branch mechanics; real content replaces them in PR3.
|
||||
|
||||
## What we decided (brainstorm)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Story pacing | **Hybrid** — manual choices, action-complete beats, boot intro, resource thresholds |
|
||||
| Threshold checks | On **action completion** and on **publish** (~10 fps) |
|
||||
| Stub A/B branch | **Flags + resources + route-exclusive action unlocks** |
|
||||
| Action context | `storyHint` subtitle + `storyTooltip` with mechanics and consequences |
|
||||
| Story UI | **Full-window overlay** (VN layout + running story log), not inline strip |
|
||||
| Story open behavior | **Player-configurable**, default **auto on new beats**; modes: auto / choices-only / manual |
|
||||
| Action detail display | **Inline subtitle default**; hover on desktop; configurable (inline / hover / info-button) |
|
||||
| PR2 scope | **Mechanics + minimal Settings drawer**; full mobile layout pass deferred to PR4 (#10) |
|
||||
| Architecture | **Approach 1** — pure engine triggers + thin UI/prefs in state layer |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/engine/story.ts traverse, applyChoice, evaluateTriggers, outcomes
|
||||
src/engine/game.ts + storyFlags, currentStoryNodeId, seenStoryNodeIds
|
||||
src/engine/save.ts persist story fields in SAVE_VERSION=1 (.default())
|
||||
src/content/storySchema.ts Zod story node / choice / trigger / outcome defs
|
||||
src/content/story.ts stub ~6-node graph
|
||||
src/content/schema.ts + action.storyHint, action.storyTooltip
|
||||
src/state/storyOrchestration.ts evaluateTriggers after boot, publish, actionComplete
|
||||
src/state/prefs.ts localStorage: storyOpenMode, actionDetailMode
|
||||
src/state/viewModel.ts + story view, action availability, hints
|
||||
src/state/runtime.ts wire orchestration, applyChoice, cancelQueue
|
||||
src/ui/StoryPanel.tsx full-screen overlay, VN passage, choices, story log
|
||||
src/ui/ActionPanel.tsx queue list, cancel, disabled, subtitles/tooltips
|
||||
src/ui/SettingsDrawer.tsx two preference selects
|
||||
```
|
||||
|
||||
**Boundaries (unchanged from M0):**
|
||||
|
||||
- `src/engine/` — pure TS; no React, DOM, localStorage, wall clock.
|
||||
- `src/state/` — orchestration, prefs, RAF, persistence.
|
||||
- `src/ui/` — renders view model; calls runtime commands only.
|
||||
|
||||
## Story schema
|
||||
|
||||
### StoryNode
|
||||
|
||||
```typescript
|
||||
StoryNode {
|
||||
id: string
|
||||
prose: string
|
||||
choices?: StoryChoice[]
|
||||
triggers?: StoryTrigger[]
|
||||
enterOutcomes?: StoryOutcome[]
|
||||
}
|
||||
```
|
||||
|
||||
### StoryChoice
|
||||
|
||||
```typescript
|
||||
StoryChoice {
|
||||
id: string
|
||||
label: string
|
||||
requirements?: {
|
||||
minResources?: Record<string, number>
|
||||
requireStoryFlags?: string[]
|
||||
excludeStoryFlags?: string[]
|
||||
}
|
||||
outcomes: StoryOutcome[]
|
||||
targetNodeId: string
|
||||
}
|
||||
```
|
||||
|
||||
### StoryTrigger
|
||||
|
||||
```typescript
|
||||
StoryTrigger {
|
||||
type: 'boot' | 'actionComplete' | 'minResources'
|
||||
actionId?: string
|
||||
minResources?: Record<string, number>
|
||||
targetNodeId: string
|
||||
once?: boolean // default true
|
||||
}
|
||||
```
|
||||
|
||||
### StoryOutcome
|
||||
|
||||
| Type | Effect |
|
||||
|---|---|
|
||||
| `setFlag` | `storyFlags[id] = true` |
|
||||
| `clearFlag` | `storyFlags[id] = false` |
|
||||
| `grantResource` | increase resource |
|
||||
| `consumeResource` | decrease resource (validated) |
|
||||
| `log` | narrative line appended to story log (no state change) |
|
||||
|
||||
Outcomes on a choice or `enterOutcomes` apply atomically before advancing.
|
||||
|
||||
### Action narrative fields
|
||||
|
||||
```typescript
|
||||
action.storyHint?: string // one-line subtitle (inline mode)
|
||||
action.storyTooltip?: string // longer detail: yields, costs, consequences
|
||||
```
|
||||
|
||||
Tooltip copy may reference flags/choices textually (authoring); engine does not parse natural language.
|
||||
|
||||
### Content validation
|
||||
|
||||
At `buildContent` / story load:
|
||||
|
||||
- All `targetNodeId` references resolve.
|
||||
- Trigger `actionId` values exist in `actionsById`.
|
||||
- Resource ids in outcomes/triggers exist.
|
||||
- Graph has exactly one boot trigger entry point.
|
||||
- Invalid graphs throw at load (dev-time), matching action schema behavior.
|
||||
|
||||
## GameState extensions
|
||||
|
||||
```typescript
|
||||
interface GameState {
|
||||
// existing: resources, activeActionId, actionElapsedMs, actionQueue
|
||||
storyFlags: Record<string, boolean>
|
||||
currentStoryNodeId: string
|
||||
seenStoryNodeIds: string[]
|
||||
}
|
||||
```
|
||||
|
||||
**Save v1** (no version bump; PR4 ships formal v1→v2 migration):
|
||||
|
||||
```typescript
|
||||
storyFlags: z.record(z.string(), z.boolean()).default({})
|
||||
currentStoryNodeId: z.string().default('') // hydrated to entry on first boot if empty
|
||||
seenStoryNodeIds: z.array(z.string()).default([])
|
||||
```
|
||||
|
||||
## Engine API (`src/engine/story.ts`)
|
||||
|
||||
| Function | Purpose |
|
||||
|---|---|
|
||||
| `initStory(state, content)` | Resolve entry node from boot trigger; set `currentStoryNodeId` |
|
||||
| `evaluateTriggers(state, content, ctx)` | ctx: `{ reason: 'boot' \| 'publish' \| 'actionComplete', actionId? }`; returns `{ enteredNodes: string[], events: StoryEvent[] }` |
|
||||
| `applyChoice(state, content, choiceId)` | Validate requirements, apply outcomes, advance, append to `seenStoryNodeIds` |
|
||||
| `getAvailableChoices(state, content, nodeId)` | Filter choices by requirements |
|
||||
| `getCurrentNode(state, content)` | Lookup helper |
|
||||
|
||||
**Trigger evaluation rules:**
|
||||
|
||||
1. **`boot`** — fires once on `initStory` if target not seen (when `once !== false`).
|
||||
2. **`actionComplete`** — fires when `ctx.reason === 'actionComplete'` and `ctx.actionId` matches.
|
||||
3. **`minResources`** — fires when all thresholds met; checked on `publish` and after `actionComplete`.
|
||||
4. A trigger does not re-fire if `targetNodeId` is in `seenStoryNodeIds` and `once !== false`.
|
||||
5. Entering a node applies `enterOutcomes`, adds id to `seenStoryNodeIds`, emits events.
|
||||
|
||||
**Integration with actions:** `game.ts` `isActionAvailable` receives `state.storyFlags` (remove PR1 default `{}` placeholder at call sites).
|
||||
|
||||
## Stub story graph
|
||||
|
||||
~6 nodes, one A/B fork, placeholder prose:
|
||||
|
||||
```text
|
||||
boot_intro
|
||||
└─ fork_choice ─┬─ route_a_beat → unlocks fortify_camp (requireStoryFlags: route_a)
|
||||
└─ route_b_beat → unlocks push_onward stub action (requireStoryFlags: route_b)
|
||||
threshold_beat (minResources coin ≥ 3) → merchant_flavor
|
||||
actionComplete(scout_path) → scout_aftermath
|
||||
```
|
||||
|
||||
**Route outcomes:**
|
||||
|
||||
- Route A: `setFlag route_a`, grant supplies, unlock `fortify_camp`.
|
||||
- Route B: `setFlag route_b`, grant coin, unlock new stub action `push_onward`.
|
||||
|
||||
Add `push_onward` to `definitions.ts` with `requireStoryFlags: ['route_b']`.
|
||||
|
||||
## Runtime orchestration
|
||||
|
||||
```text
|
||||
boot → loadGame → initStory → evaluateTriggers(boot) → publish
|
||||
|
||||
each frame:
|
||||
tickGame → [if action completed this tick] evaluateTriggers(actionComplete)
|
||||
publish (≥10fps) → evaluateTriggers(publish)
|
||||
if new node:
|
||||
appendStoryLog
|
||||
open StoryPanel per prefs.storyOpenMode
|
||||
optionally mirror one line to event log
|
||||
|
||||
applyChoice → engine → appendStoryLog → publish → close/minimize panel
|
||||
cancelQueuedAction → engine → publish
|
||||
```
|
||||
|
||||
`src/state/storyOrchestration.ts` owns the trigger call sequence; runtime invokes it.
|
||||
|
||||
## Player preferences (`src/state/prefs.ts`)
|
||||
|
||||
Stored in `localStorage` (not save v1):
|
||||
|
||||
| Key | Values | Default |
|
||||
|---|---|---|
|
||||
| `storyOpenMode` | `auto` \| `choices-only` \| `manual` | `auto` |
|
||||
| `actionDetailMode` | `inline` \| `hover` \| `info-button` | `inline` |
|
||||
|
||||
**SettingsDrawer** — gear icon in header; two `<select>` controls; changes apply immediately.
|
||||
|
||||
### Story open modes
|
||||
|
||||
| Mode | Behavior |
|
||||
|---|---|
|
||||
| `auto` | Open overlay on every new node |
|
||||
| `choices-only` | Auto-open only when node has choices; else unread badge |
|
||||
| `manual` | Never auto-open; **Story** button shows unread badge |
|
||||
|
||||
## UI
|
||||
|
||||
### App shell
|
||||
|
||||
Order unchanged: header (title + settings gear + story button) → ResourceBar → ActionPanel → EventLog.
|
||||
|
||||
### StoryPanel (overlay)
|
||||
|
||||
- `fixed inset-0 z-50`, dark backdrop.
|
||||
- **Main:** current prose; choice buttons; "Continue" on passage-only nodes.
|
||||
- **Story log:** collapsible sidebar (desktop) or bottom sheet (mobile) — append-only `{ nodeId, prose, choiceLabel? }`.
|
||||
- Close/minimize respects open mode; choices require selection before dismiss when auto-opened for a choice node.
|
||||
|
||||
### ActionPanel
|
||||
|
||||
- Button per action with progress bar on active (existing).
|
||||
- **Queue list:** ordered names, cancel (✕) per index.
|
||||
- **Disabled** when locked or unaffordable; click logs reason (no enqueue).
|
||||
- **Inline:** `storyHint` under action name.
|
||||
- **Hover (desktop):** tooltip with `storyTooltip`.
|
||||
- **Info-button:** ⓘ opens tooltip popover.
|
||||
|
||||
Availability derived from view model (`available`, `disabledReason`).
|
||||
|
||||
### View model additions
|
||||
|
||||
```typescript
|
||||
actions: {
|
||||
id, name, available, disabledReason,
|
||||
storyHint?, storyTooltip?,
|
||||
costsSummary?, yieldsSummary?
|
||||
}[]
|
||||
|
||||
story: {
|
||||
isOpen, hasUnread,
|
||||
currentProse, choices: { id, label, disabled, disabledReason }[],
|
||||
log: { nodeId, prose, choiceLabel? }[]
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
| Failure | Behavior |
|
||||
|---|---|
|
||||
| Invalid story content at load | Throw during `buildContent` (dev-time) |
|
||||
| Unknown node/choice id in engine API | Throw (dev-time) |
|
||||
| Choice requirements not met | `applyChoice` throws; UI disables button + shows reason |
|
||||
| Unaffordable enqueue | Existing pattern — catch in runtime, log to event log |
|
||||
| Missing prefs in localStorage | Use defaults |
|
||||
| Save missing story fields | `.default()` on schema — backward compatible with PR1 saves |
|
||||
|
||||
## Testing
|
||||
|
||||
### Engine (required, ≥80% coverage on `src/engine/`)
|
||||
|
||||
- Linear node traversal and `seenStoryNodeIds`.
|
||||
- Branch by choice; requirements gate choices.
|
||||
- Each trigger type: boot, actionComplete, minResources.
|
||||
- Threshold fires on resource change via outcomes.
|
||||
- Outcomes: flags, grant/consume resources.
|
||||
- `once: false` vs default once-only triggers.
|
||||
- Invalid graph rejected at content load.
|
||||
|
||||
### Content schema
|
||||
|
||||
- Valid stub graph builds.
|
||||
- Dangling `targetNodeId` throws.
|
||||
- Unknown action id in trigger throws.
|
||||
|
||||
### State
|
||||
|
||||
- Orchestration fires boot trigger on load.
|
||||
- `storyFlags` wired into action availability in view model.
|
||||
- Prefs round-trip localStorage.
|
||||
|
||||
### UI (browser smoke — Antigravity or manual)
|
||||
|
||||
- Queue 2+ actions, cancel one, order respected.
|
||||
- Route A vs B → different flags, resources, unlocked actions.
|
||||
- Story panel opens per default auto mode.
|
||||
- Settings change story-open mode behavior.
|
||||
- Reload preserves story state + queue.
|
||||
|
||||
### Pre-PR verification chain
|
||||
|
||||
```powershell
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
pnpm test:coverage
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Out of scope (PR2)
|
||||
|
||||
- Real opening-arc prose (PR3 / Phase 0 gate).
|
||||
- Automation unlock (PR3 #7).
|
||||
- Prestige reset (PR3 #8).
|
||||
- Save v2 migration and export/import UX (PR4 #9).
|
||||
- Full mobile layout pass (PR4 #10) — basic overlay responsiveness only.
|
||||
- Playwright E2E.
|
||||
- Additional prefs beyond the two PR2 toggles.
|
||||
|
||||
## AI tool routing (from parent plan)
|
||||
|
||||
| Task | Tool |
|
||||
|---|---|
|
||||
| T2.1–T2.2 Story engine + schema | Codex |
|
||||
| T2.3–T2.5 UI + stub graph + browser verify | Antigravity |
|
||||
| Ginnoir review / tweaks | Cursor |
|
||||
|
||||
## Success criteria (PR2 merge)
|
||||
|
||||
1. Player can queue/cancel actions with visible queue and disabled states.
|
||||
2. Full-window story panel shows stub VN passage and choices.
|
||||
3. Completing route A vs B produces different flags, resources, and unlocked actions.
|
||||
4. Action subtitles/tooltips show narrative + mechanical hints.
|
||||
5. Settings drawer toggles story-open and action-detail modes.
|
||||
6. Reload preserves story position, flags, and queue.
|
||||
7. CI green; engine coverage ≥80%.
|
||||
|
||||
---
|
||||
|
||||
*Brainstorm approved 2026-06-11. Next step: invoke `writing-plans` for task-level implementation plan.*
|
||||
@@ -0,0 +1,367 @@
|
||||
# M1 PR3 — Shell UI & Action Column Design Spec
|
||||
|
||||
> **Status:** Approved by ginnoir (brainstorm 2026-06-11).
|
||||
> **Branch:** `feat/m1-progression` off `main` (first chunk before automation/prestige/content).
|
||||
> **Supersedes (partially):** PR2 story overlay UX in `docs/superpowers/specs/2026-06-11-m1-pr2-playable-loop-design.md` §Story UI.
|
||||
> **Parent plan:** `docs/plans/2026-06-11-m1-vertical-slice.md` §PR3.
|
||||
|
||||
## Summary
|
||||
|
||||
PR3 opens with a **shell refactor** that fixes PR2 UX debt before progression/content land: replace the full-screen story overlay with a three-region layout (nav rail / center panel / right rail), reorganize Play into **behavior-type action columns** with collapsible theme groups, move **all story forks to actions** (Story tab is read-only prose + branching tree), and establish **universal automation** with **shareable text recipes** referencing content action ids.
|
||||
|
||||
## What we decided (brainstorm)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Timing | **First chunk of PR3** — shell lands at start of `feat/m1-progression`, then T3.1 automation, T3.2 prestige, T3.3 content |
|
||||
| Story choices | **Actions only** — Story tab has no choice buttons |
|
||||
| Story tab | **Split pane:** branching tree (larger, ~60%) + long-form prose log (~40%); tree left, log right |
|
||||
| App shell | **Left nav** (Play, Story, Settings, About) → **center** active panel → **right rail** on Play (resources + inventory + optional event log) |
|
||||
| Action layout | **Columns by behavior kind**, collapsible **theme/purpose groups** inside each column |
|
||||
| Column order | **Instant → Loop → Timed → Story → Context** (Timed centered as primary column) |
|
||||
| Loop vs automation | **Loop** = idle filler while playing (queue empty, nothing timed active). **Automation** = hands-off repeat queue including offline |
|
||||
| Automation scope | **Universal** — instant, loop, timed, story, context all automatable **after first manual completion** on that action |
|
||||
| Story automation | Same rule — first time at a fork is conscious; after performing a branch once, that story action can be automated on repeat runs |
|
||||
| Automation sharing | Recipes **export/import as text**; steps reference stable **content action ids** |
|
||||
| Event log | **Optional** on Play right rail (collapsible + pref to hide; default visible) |
|
||||
| Mobile | Column stacking + responsive Story split deferred to PR4 (#10) |
|
||||
| Approach | **Shell first, typed columns second** within PR3 opener (T3.0) |
|
||||
|
||||
## Problem statement (PR2 debt)
|
||||
|
||||
1. **Readability** — full-screen story overlay (`bg-black/80`) ghosts underlying UI; hard to read.
|
||||
2. **Redundant choice paths** — story panel choice buttons and Play actions both visible; unclear authority.
|
||||
3. **Structural mismatch** — single centered column does not match intended Chronicle-style shell (nav / main / sidebar).
|
||||
4. **Flat action list** — no distinction between instant, idle loop, timed queue, story decisions, or context switches.
|
||||
|
||||
## App shell
|
||||
|
||||
```
|
||||
┌──────────┬─────────────────────────────┬──────────────┐
|
||||
│ Nav rail │ Center (active panel) │ Right rail │
|
||||
│ │ │ (Play only) │
|
||||
│ ● Play │ Play → action columns │ Resources │
|
||||
│ Story │ Story → tree │ prose log │ Inventory * │
|
||||
│ Settings │ Event log † │
|
||||
│ About │ Settings / About → content │ │
|
||||
└──────────┴─────────────────────────────┴──────────────┘
|
||||
|
||||
* Inventory placeholder = resource list until item system exists
|
||||
† Collapsible; pref to hide (default: visible)
|
||||
```
|
||||
|
||||
### Nav rail
|
||||
|
||||
- Fixed left column; icons + labels on desktop, icons-only acceptable on narrow widths until PR4.
|
||||
- **Story unread badge** when new beats arrive (replaces overlay auto-open as primary signal).
|
||||
- `storyOpenMode` pref remapped: `auto` switches to Story nav or pulses badge instead of opening overlay.
|
||||
|
||||
### Center panel
|
||||
|
||||
Swaps content by active nav item. No full-screen modal for story.
|
||||
|
||||
### Right rail (Play only)
|
||||
|
||||
- **Resources** — always visible.
|
||||
- **Inventory** — resource list placeholder in M1; item grid later.
|
||||
- **Event log** — collapsible section; `showEventLog` pref (default `true`).
|
||||
|
||||
### Removed
|
||||
|
||||
- `StoryPanel` full-screen overlay with choice buttons.
|
||||
- Header Story / Settings buttons replaced by nav rail (Settings may retain drawer or become full center panel — implementer picks cleaner fit).
|
||||
|
||||
## Play panel — action columns
|
||||
|
||||
### Column order (left → right)
|
||||
|
||||
| # | Column | `kind` | Role |
|
||||
|---|---|---|---|
|
||||
| 1 | Instant | `instant` | One-shot interactions: buy, sell, trade |
|
||||
| 2 | Loop | `loop` | Idle upkeep between tasks (rest, passive recovery) |
|
||||
| 3 | **Timed** | `timed` | Primary gameplay; manual queue + progress bars |
|
||||
| 4 | Story | `story` | Narrative forks and hard decisions |
|
||||
| 5 | Context | `context` | Area changes, combat entry, context switches |
|
||||
|
||||
Timed column is visually central. Narrow viewports: horizontal scroll with Timed near viewport center.
|
||||
|
||||
### Groups within columns
|
||||
|
||||
Each action belongs to a **group** (content-defined theme/purpose):
|
||||
|
||||
```typescript
|
||||
group: {
|
||||
id: string // e.g. 'camp', 'tavern', 'buy'
|
||||
label: string // e.g. 'Camp activities', 'Buy'
|
||||
}
|
||||
```
|
||||
|
||||
- Groups render as **collapsible sections** inside their kind column.
|
||||
- Examples:
|
||||
- **Instant:** Buy, Sell, Trade
|
||||
- **Loop:** Camp activities, Travel activities
|
||||
- **Timed:** Library, Tavern, Camp (location-themed)
|
||||
- **Story:** Arc-specific decision groups
|
||||
- **Context:** Region / combat entry groups
|
||||
- Collapse state stored in `GamePrefs.collapsedActionGroups: Record<string, boolean>`.
|
||||
|
||||
### Action card UX (unchanged from PR2 where applicable)
|
||||
|
||||
- `storyHint` inline subtitle (default) / hover / info-button per pref.
|
||||
- `storyTooltip` for mechanics and fork consequences.
|
||||
- **Story-kind actions** get distinct styling (amber fork badge, border) so diverging paths are obvious in Play.
|
||||
|
||||
## Action kinds — engine behavior
|
||||
|
||||
### `instant`
|
||||
|
||||
- Click → validate afford/unlock → apply costs/yields immediately.
|
||||
- No queue slot, no duration.
|
||||
|
||||
### `timed`
|
||||
|
||||
- Current PR1/PR2 behavior: `durationMs`, manual `actionQueue`, progress bar.
|
||||
- Primary manual play column.
|
||||
|
||||
### `loop`
|
||||
|
||||
- Runs only when **`activeActionId` is null** and **manual `actionQueue` is empty**.
|
||||
- Player enables/disables per loop action (toggle); priority order when multiple enabled (content `loopPriority` number, lower first).
|
||||
- On completion while still idle, immediately repeats same loop action.
|
||||
- Respects costs; stops if unaffordable.
|
||||
- **Does not run offline** — idle-present behavior only.
|
||||
|
||||
### `story`
|
||||
|
||||
- Represents one graph choice. Content field `storyChoiceId` links to `StoryChoice.id`.
|
||||
- On execute → `applyChoice(state, content, storyChoiceId)` → sibling story actions hide via flags/requirements.
|
||||
- First visit: only available choices shown; player picks consciously.
|
||||
- After first manual completion of that action id: eligible for automation queue.
|
||||
|
||||
### `context`
|
||||
|
||||
- Switches active game context (location, combat scene, etc.).
|
||||
- M1: stub until opening arc requires it; schema + column chrome ship in T3.0.
|
||||
|
||||
## Automation (T3.1 — universal)
|
||||
|
||||
### Philosophy
|
||||
|
||||
Once the player has **completed an action manually at least once**, they may add it to an **automation queue** that repeats hands-off, **including offline** (within existing offline cap).
|
||||
|
||||
Applies to **all kinds** including `story` (after that specific branch was taken once). New/unseen branches remain manual until first completion.
|
||||
|
||||
Prestige/knowledge catch-up (T3.2) treats previously cleared routes as already completed for automation unlock purposes — same knowledge model as story fast-forward.
|
||||
|
||||
### Queues
|
||||
|
||||
| Queue | Purpose |
|
||||
|---|---|
|
||||
| `actionQueue` | Manual timed queue (player actively planning) |
|
||||
| `automationQueue` | Hands-off repeat pipeline |
|
||||
|
||||
Automation runner executes when manual queue is idle (exact precedence documented in engine; automation fills gaps like a background worker, distinct from loop-kind idle actions — both may need priority rules: **manual > automation > loop**).
|
||||
|
||||
### Automation recipe export/import
|
||||
|
||||
Recipes are **shareable text** referencing stable **content action ids** (not display names).
|
||||
|
||||
**Canonical multi-line format (v1):**
|
||||
|
||||
```text
|
||||
idlegame-recipe/v1
|
||||
# name: Early camp grind
|
||||
gather_supplies
|
||||
scout_path
|
||||
rest_briefly
|
||||
```
|
||||
|
||||
- Line 1: magic header with version (`idlegame-recipe/v1`).
|
||||
- Optional `# name:` comment for human label (ignored by parser if malformed).
|
||||
- One action id per non-empty, non-comment line.
|
||||
- Lines must match `/^[a-z][a-z0-9_]*$/` (same id convention as content defs).
|
||||
|
||||
**Single-line alias (optional parser support):**
|
||||
|
||||
```text
|
||||
idlegame-recipe/v1:gather_supplies,scout_path,rest_briefly
|
||||
```
|
||||
|
||||
**Compressed variant (long recipes):** same payload JSON + `lz-string` URI encoding as saves (`toRecipeExportString` / `fromRecipeExportString`) — optional in T3.1 if recipes exceed ~20 steps; multi-line text remains the default share format.
|
||||
|
||||
**Import validation:**
|
||||
|
||||
1. Parse version; reject unknown versions explicitly.
|
||||
2. Every id must exist in `content.actionsById`.
|
||||
3. Every id must be **automation-unlocked** for the current player state (`manualCompletionCounts[id] >= threshold`).
|
||||
4. On failure: user-visible error listing unknown or locked ids — no partial apply unless ginnoir opts in later.
|
||||
|
||||
**Export:** serializes current `automationQueue` (or named saved recipe slot if added later) to multi-line text; copy-to-clipboard in Settings or Play automation UI.
|
||||
|
||||
### State fields
|
||||
|
||||
```typescript
|
||||
manualCompletionCounts: Record<string, number> // default {}; gates automation
|
||||
automationQueue: string[] // action ids
|
||||
enabledLoopActionIds: Record<string, boolean> // loop toggles
|
||||
```
|
||||
|
||||
Persist in save payload (PR3 adds fields; PR4 may bump `SAVE_VERSION` to 2 with migration).
|
||||
|
||||
## Story tab
|
||||
|
||||
### Layout
|
||||
|
||||
Split pane, resizable on desktop:
|
||||
|
||||
```
|
||||
┌──────────────────────────┬─────────────────────┐
|
||||
│ Branching tree (~60%) │ Prose log (~40%) │
|
||||
│ │ │
|
||||
│ ○ boot_intro │ [Full passage text │
|
||||
│ ├─● route_a │ for selected or │
|
||||
│ │ └─ merchant │ latest node] │
|
||||
│ └─○ route_b (dimmed) │ │
|
||||
│ │ Scrollable history │
|
||||
└──────────────────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
- **Tree (primary surface):** built from story graph + `seenStoryNodeIds` / `storyFlags`. Taken paths emphasized; untaken branches dimmed. Click node → prose log scrolls to that beat. Larger pane because the tree needs manipulation room.
|
||||
- **Prose log (secondary):** full passage text, not truncated. Choice labels inline: `[You chose: Take the high road]`.
|
||||
- **No choice buttons.**
|
||||
|
||||
### Story map data
|
||||
|
||||
- Tree nodes mirror `storyNodesById` edges via choices + triggers.
|
||||
- Visibility: seen nodes solid; future/locked dimmed or hidden per design.
|
||||
- M1 T3.0 may ship minimal tree (indented list) before polish; structure must support real graph.
|
||||
|
||||
### Unread / navigation
|
||||
|
||||
- New story beats → Story nav badge.
|
||||
- `storyOpenMode: auto` → switch to Story tab or highlight badge (no overlay).
|
||||
|
||||
## Content schema changes
|
||||
|
||||
```typescript
|
||||
actionKindSchema = z.enum(['instant', 'loop', 'timed', 'story', 'context'])
|
||||
|
||||
actionGroupSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
})
|
||||
|
||||
actionDefSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
kind: actionKindSchema.default('timed'),
|
||||
group: actionGroupSchema,
|
||||
durationMs: z.number().positive().optional(), // required for timed + loop
|
||||
loopPriority: z.number().int().nonnegative().optional(),
|
||||
costs: ...,
|
||||
yields: ...,
|
||||
unlock: ...,
|
||||
storyHint: ...,
|
||||
storyTooltip: ...,
|
||||
storyChoiceId: z.string().min(1).optional(), // required when kind === 'story'
|
||||
contextId: z.string().min(1).optional(), // required when kind === 'context'
|
||||
automation: z.object({
|
||||
unlockAfterManualCompletions: z.number().int().positive().default(1),
|
||||
}).optional(),
|
||||
})
|
||||
```
|
||||
|
||||
**Validation rules:**
|
||||
|
||||
- `timed` and `loop` require `durationMs`.
|
||||
- `instant`, `story`, `context` must not require duration for execution (engine enforces).
|
||||
- `story` requires valid `storyChoiceId` referencing a choice in the loaded story graph.
|
||||
- Column placement derived from `kind` (not a separate column field).
|
||||
|
||||
### Stub content migration (T3.0)
|
||||
|
||||
| Current action | New kind | Group example |
|
||||
|---|---|---|
|
||||
| gather_supplies | timed | Camp |
|
||||
| scout_path | timed | Travel |
|
||||
| trade_at_camp | timed | Camp |
|
||||
| fortify_camp | timed | Camp (route A) |
|
||||
| push_onward | timed | Travel (route B) |
|
||||
| rest_briefly | loop | Camp activities |
|
||||
| *(new)* pick_high_road | story | Fork |
|
||||
| *(new)* follow_river | story | Fork |
|
||||
|
||||
Remove parallel choice buttons from story graph UI; fork choices exist only as story-kind actions.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/ui/AppShell.tsx nav rail + center + right rail layout
|
||||
src/ui/NavRail.tsx
|
||||
src/ui/PlayPanel.tsx action column grid
|
||||
src/ui/ActionColumn.tsx one kind column
|
||||
src/ui/ActionGroup.tsx collapsible group
|
||||
src/ui/StoryView.tsx split tree + prose log (replaces overlay StoryPanel)
|
||||
src/ui/StoryTree.tsx
|
||||
src/ui/StoryProseLog.tsx
|
||||
src/ui/RightRail.tsx resources, inventory placeholder, event log
|
||||
src/ui/AutomationBar.tsx automation queue UI + export/import (T3.1)
|
||||
|
||||
src/content/schema.ts + kind, group, storyChoiceId, contextId, automation
|
||||
src/engine/game.ts instant execute, loop idle runner, completion counts
|
||||
src/engine/automation.ts automation queue runner, recipe parse/serialize (new)
|
||||
src/engine/recipe.ts recipe export/import validation (new)
|
||||
src/state/viewModel.ts column/group projection, automation eligibility
|
||||
src/state/runtime.ts nav commands, remove overlay; story action → applyChoice
|
||||
src/state/prefs.ts + collapsedActionGroups, showEventLog
|
||||
```
|
||||
|
||||
**Boundaries unchanged:** engine pure TS; UI calls runtime only.
|
||||
|
||||
## PR3 task order (revised opener)
|
||||
|
||||
| Task | Deliverable | Gitea |
|
||||
|---|---|---|
|
||||
| **T3.0** | App shell, action columns, story-via-actions, Story tab split, remove overlay | UI gate |
|
||||
| **T3.1** | Universal automation + automation queue + recipe export/import | #7 |
|
||||
| **T3.2** | Prestige + knowledge catch-up for automation/story | #8 |
|
||||
| **T3.3** | Opening arc content on new shell | #11 |
|
||||
|
||||
## Testing
|
||||
|
||||
### Engine
|
||||
|
||||
- Instant action applies costs/yields without queue.
|
||||
- Loop runs when manual queue idle; stops when timed work queued.
|
||||
- Priority: manual timed > automation > loop (document exact rules in tests).
|
||||
- Story action executes `applyChoice`; siblings become unavailable.
|
||||
- `manualCompletionCounts` gates automation; story actions unlock after first perform.
|
||||
- Recipe round-trip: export → import → identical queue; reject unknown ids; reject locked ids.
|
||||
|
||||
### UI / view model
|
||||
|
||||
- Actions projected into correct column and group.
|
||||
- Group collapse prefs persist.
|
||||
- Story tab: tree click selects prose; no choice buttons rendered.
|
||||
- Nav badge on unread story.
|
||||
|
||||
### Regression
|
||||
|
||||
- Remove/update PR2 overlay tests and story choice button tests.
|
||||
- Existing queue/tick/story graph tests adapted for action-kind story forks.
|
||||
|
||||
## Out of scope (T3.0)
|
||||
|
||||
- Polished graph visualization (minimal tree OK).
|
||||
- Item inventory (resource list placeholder).
|
||||
- Combat mechanics (context column stub).
|
||||
- Mobile responsive column stack (PR4 #10).
|
||||
- Save v2 migration (PR4 #9) — PR3 adds new state fields with defaults in v1 payload until bump.
|
||||
|
||||
## PR2 supersession note
|
||||
|
||||
PR2 spec locked full-window overlay with in-panel choices. This spec **intentionally replaces** that UX. Engine story graph (`applyChoice`, triggers, flags) remains; only presentation and choice surface move to Play actions + Story read-only tab.
|
||||
|
||||
## Open questions (none blocking)
|
||||
|
||||
All brainstorm decisions resolved 2026-06-11.
|
||||
Reference in New Issue
Block a user