clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / openspec / changes / archive / 2026-09-24-add-world-index-navigation / design.md
design.md 6.7K source

World model

Create one world record for each catalog pack, including the core pack:

{
  id: 'tidewater',
  number: 4,
  packId: 'tidewater',
  name: 'Tidewater',
  author: '...',
  glyph: '๐ŸŒŠ',
  tagline: 'Everything floats until it does not.',
  accent: 'sink',
  origin: 'bundled',
  registrationIndex: 3,
  sheets: ['tidewater:sheet-1'],
  levelCount: 9,
  completedCount: 2,
}

number is one-based in display and follows the final world order. The world record is derived from the catalog, not duplicated in level data. A pack may have multiple sheet records, but every sheet has exactly one worldId/packId. The core pack is world 1 and owns its own sheet range. No bundled pack is implicitly folded into the core world.

The layout API should expose both worlds and sheets, plus lookup helpers for world by pack ID, sheet by ID, and node by global level ID. Each sheet keeps the existing fifteen-cell serpentine auto layout and links to the previous and next sheet in the same world. A world boundary is represented separately from an intra-world sheet boundary so callers can animate or announce it later.

Ordering and registration

The catalog order is the source of truth:

  1. core;
  2. bundled packs in registration order;
  3. local and URL/imported packs in install order.

The currently approved default bundle is core, Crossword, Crate Works, then Tidewater. Clockwork joins only after W12 custom-property presentation review; Garden Basics and Word Lab remain importable examples outside the default INDEX. This is a registration choice, not an INDEX hard-coded ID rule.

Replacing a pack preserves its position. Forking an imported pack appends the fork as a new imported world. Removing an imported pack removes its world and its sheets while preserving the relative order of all remaining worlds.

The layout must not infer world identity from origin, a hard-coded ID set, or the active level. This prevents a newly bundled or imported pack from silently joining core.

Manifest world metadata

manifest.world is optional. When present it is an object with these fields:

  • glyph: exactly one grapheme cluster, counted with Intl.Segmenter using granularity: 'grapheme', and no more than 16 UTF-16 code units. This permits visible emoji such as โš™๏ธ, ZWJ sequences, and flags while restricting the card to one glyph;
  • tagline: at most 80 characters, one source line with no newline, sentence case, and no ยท (U+00B7). The diagnostic for the middle dot is explicit: Tagline can't use ยท; use a comma or a dash.;
  • accent: an optional token from you, push, stop, win, sink, defeat, or tick.

World identity is presentation metadata, so an invalid optional field produces a nonblocking W11 note and does not reject the pack. The world falls back to the first palette glyph, no tagline, and the default --fg path. Missing manifest.world uses the same defaults. Invalid gameplay fields continue to reject the complete pack atomically through the existing pack validator. Normalized metadata and notes are available through catalog pack records and pack-service views without changing share identity or level IDs.

Progress and chapter unlocking

Solved state remains keyed by { packId, levelId }. World completion is the count of solved levels whose packId matches that world, over that world's level count. Unlocking is evaluated in world order within each world: its opener is always available and each later proof requires the previous proof in the same world. Solving or selecting a proof in one world cannot unlock a proof in another world.

The active INDEX state must carry worldId, worldIndex, sheetId, and nodeId. World switching selects the first available node in the destination world, preserving that world's current cursor when one exists. If no proof in a world is available, its opener remains the selectable boundary target and the existing locked-node indication applies.

Q and E switch one world left or right. Gamepad LB/RB map to the same actions. Touch uses a two-finger horizontal swipe and must resolve one switch per gesture. Switching at the first/last world is a no-op with the existing blocked-navigation feedback and does not alter progress or the active proof.

Walking from the terminal node of a world's final sheet in its path-forward direction enters the first node of the next world. Walking backward from the first node enters the final node of the previous world. There is no wraparound. Intra-world sheet crossings continue to use the current path boundary behavior and must not be mistaken for a world change.

Presentation seam

The INDEX scene receives a pure view model containing:

  • worldNumber, worldCount, worldName, worldGlyph, worldTagline, worldAccent;
  • completedCount, levelCount, and the current world-strip position;
  • enteredWorldBefore, worldCardRequested, and cardOrigin (index or workshop).

The first entry into a world and Workshop PLAY request a card once per session and then settle on the world sheet. The card displays the glyph, name, tagline, author, and an ENTER action. The follow-up presentation change owns its exact geometry, copy fitting, and 280ms camera ease; the engine must only emit stable world transition and card-request state.

Compatibility and migration

Existing progress keys remain valid. Catalog callers that only ask for flat levels continue to receive the same ordered level records. The layout migration updates callers that assumed built-ins shared a core sheet range. Imported packs without manifest.world remain valid and receive normalized default metadata.

Verification and designer handoff

Physical keyboard Q/E is handled by the stage action router; gamepad LB/RB and two-finger gestures enter through the shared input intent path. The local and public world-index browser fixtures exercised Q/E, first/last boundary feedback, Core โ†’ Crossword โ†’ Crate Works โ†’ Tidewater order, Workshop PLAY, mobile touch, and reduced motion without page errors. Focused map/input tests cover world and sheet edges, cursor restoration after removal, independent opener locks and completion counts, and stable pack/local progress keys.

The designer accepted the header, strip, world cards, and four-world bundle on 2026-09-24 in docs/design-inbox.md. The separate presentation change has been archived. The complete 30-proof sequential world gate, 373-test Node suite, production build, and local Gift/Workshop/share browser flow passed before public revision 13 at https://fcbc.n.zip/. The live game-presentation and level-packs specs contain no obsolete built-in sheet-1 grouping rule; this change's merged spec now owns per-pack worlds.