Files
clockwork-hero/docs/DESIGN.md
Mystica Venatus 55480b3fc6 feat(face): integrate Idle Task Protocol v1.0 end-to-end
Watchface now syncs with the Nine Hazes idle task system via its own
pkjs client — the three-artifact architecture is operational.

Wire side:
- pkjs fetches GET /v1/queue on launch, applies clock offset, converts
  epoch-ms to seconds for AppMessage, resolves task name from the
  cached dictionary (refetch on version drift)
- GameSnapshot persisted at key 0x30 (protocol-specified); task name
  at 0x32 (face-local nicety)
- AppMessage inbox widened to 256 bytes to carry the full snapshot
  dictionary in a single logical message

Display side:
- Task strip renders two lines: countdown-to-completion + used/36 +
  task count, then task name with "(+)" pending-rewards cue
- Status states render distinctly: FROZEN / ABORTED(code) / LOCKED /
  No hero yet (EMPTY)
- serverDown sentinel key: oxblood "!" icon on failed check; absence
  on successful sync clears it; volatile — never persisted
- Countdown math is server-clock corrected (clock_off), computed
  locally from bound_sec — zero radio to stay fresh

Also:
- ES5-normalized pkjs for SDK webpack 1.15/acorn parser
- Replaced face_packet.{h,c} with snapshot.{h,c} (PACKET_KEY 101
  retired; old blob orphaned harmlessly)
- Mock Task API server seeded with 3-task queue (in-memory)

Verified: seeded queue renders "0:xx 16/36 seg 3 tasks / Gather (+)";
server kill → cached strip + offline icon, no error surface.
2026-09-13 10:18:34 -04:00

28 KiB
Raw Permalink Blame History

Project Record: "Nine Hazes" (working title) — Celtic Idle RPG for Pebble Time 2

v2.0 — post-architecture-revamp. The design was rebuilt on verified PebbleOS platform constraints (see §4.4). Game mechanics (§§69) carry over unchanged.

1. Concept

A Celtic-themed idle RPG ecosystem for Pebble Time 2 (emery), built as:

  1. A fully customizable watchface — the flagship and first artifact. Ten toggleable, movable, stylable widgets incl. a Current Task strip. Complete and useful standalone, before any game exists. 2. A companion game watchapp — the idle RPG itself. Canonical save lives on the watch. Entry via Quick Launch. 3. A self-hostable bridge server — lightweight mirror of game state so the watchface can display the hero's current task. The developer's instance is default; players may self-host. Later doubles as multiplayer backend.

            The game is played by queuing 15-minute-chunk tasks and returning to collect
            rewards. Battery impact of the watchface must be indistinguishable from a
            normal watchface.
    
            ## 2. Reference Projects (inspiration only — clean-room rules)
    
            | Reference | What we take | Boundary |
            |---|---|---|
            | IdleFantasy (tristinbaker) | Task/expedition loop, chunk queue | No code; repo is Android/Kotlin |
            | Doors of Doom (msx80) | Combat structure, Range pattern, weighted loot, Entity/Run serialization, instance-vs-definition | Clean-room |
            | Pixel8 watchface | AppMessage patterns, persist-key discipline | No code reuse; license unknown |
            | My Vampire System | Leveling *system shape* only | No names/lore (IP risk) |
            | **Muninn (C-D-Lewis)** | Mechanic of on-watch battery-days estimation: slow sampling, moving-average drain rate, charge-event recalibration, 3-sample cold-start ramp. Also: Wakeup-not-Worker, rolling-single-wakeup scheduling, wakeup-collision retry | Study only; no code copied (repo has no LICENSE file) |
            | **freakified/pebble-calculator** | Existence proof that TouchService works on emery in the current SDK | API reference only |
    
            ## 3. Platform & Environment
    
            - **Target: emery only** (Pebble Time 2, 200×228 color). `"targetPlatforms": ["emery"]`
            - **Toolchain: Core Devices PebbleOS SDK (Apache-2.0)**, local builds: `pebble build --emery`
            - **Docs live at developer.repebble.com** — the project verified against these
            - Custom fonts: bundled OFL TTFs at compile time (system fonts proprietary)
            - **License for our code: Apache-2.0**
            - **Server: TypeScript, zero dependencies**, pure `node:http` + stdlib. TLS
              terminates at a reverse proxy (documented deployment pattern). State is
                inspectable JSON files, atomic write-temp-rename. No database.
                - Repo topology, donations: unchanged from v1 (Gitea master, GitHub mirror at
                  v0.1.0; Liberapay/Ko-fi + FUNDING.yml)
    
                  ## 4. Architecture — three artifacts, one protocol
    
                  ### 4.1 Topology
    
                  ```
                  ┌──────────────┐  PUT state    ┌─────────────┐   GET packet   ┌──────────────┐
                  │ GAME WATCHAPP │ ───────────▶ │   SERVER     │ ◀──────────── │  WATCHFACE    │
                  │ (truth)       │  (JS, when    │ (mirror,     │  (JS, throttled│ (display only)│
                  │               │   running)    │  never       │   poll)       │              │
                  └──────────────┘               │  authority)  │                └──────────────┘
                                                 └─────────────┘
                                                 ```
    
                                                 - **Game watchapp** owns ALL game truth in its per-app persist storage.
                                                   Full buttons + TouchService. Entered via Quick Launch (can be a single
                                                     tap on current hardware). Presence via App Glance line and timeline pins.
                                                     - **Watchface** is pure display: no game logic, no input handling for game
                                                       purposes, fed by a cached server packet. End-timestamps are fixed at
                                                         scheduling, so the countdown is computed locally each minute from the
                                                           cached packet — zero radio needed to stay fresh.
                                                           - **Server** mirrors the small "face packet" (task name, end timestamp,
                                                             queue fill, sync_version). Game watch = source of truth, so last-write-
                                                               wins is correct, not a bug. Token in auth header, never query string.
    
                                                               ### 4.2 Server (self-hostable by design)
    
                                                               - Endpoints: `GET /v1/state/:player_id`, `PUT /v1/state/:player_id`.
                                                                 Token auth, body-size cap (~2 KB), 401 on bad token, silent throttle.
                                                                 - `sync_version` in every packet, forward/backward tolerant — strangers run
                                                                   servers, coordinated upgrades are impossible.
                                                                   - Contract lives in-repo as `/docs/PROTOCOL.md` (public spec for self-hosters).
                                                                   - Server-down semantics: watchface renders cached data or hides the strip;
                                                                     game is fully playable. **Fail-dead-silent, always.**
                                                                     - Self-hosting = the privacy pitch for this audience. Developer instance is
                                                                       default; donor-funded upkeep.
    
                                                                       ### 4.3 Wake cycle (game app only)
    
                                                                       - **No worker, no background process — ever.** Game logic is dormant until
                                                                         invited (user opens app) or a user-enabled semantic wakeup fires.
                                                                         - Wakeups launch the app to the FOREGROUND (visible blip ~12 s), it computes,
                                                                           updates App Glance + pushes packet/pins via its JS, and exits cleanly
                                                                             (AppExitReason). No invisible execution exists on PebbleOS.
                                                                             - **Semantic events only** — whole-task completion, milestones worth pinning;
                                                                               typically a handful per day. Never per-chunk. Never per-minute. The
                                                                                 API *permits* waking every minute via rebooking; the design forbids it
                                                                                   (violates Laws 1 and 3).
                                                                                   - **Rolling single wakeup**: exactly one pending event (next semantic event),
                                                                                     rebooked each cycle. Handle `E_OUT_OF_RESOURCES` and collision retry
                                                                                       gracefully (Muninn pattern). Max 8 pending / 1-minute spacing per docs.
                                                                                       - Wakeups are an *optimization over* lazy check-in, never a dependency.
                                                                                         Must tolerate "no wakeups ever fired."
                                                                                         - Default OFF or "pins only"; user opt-in. Missed-sample tolerance required.
    
                                                                                         ### 4.4 Platform constraint ledger (verified — do not re-litigate)
    
                                                                                         1. Watchfaces receive NO button or touch input. UP/DOWN/BACK/SELECT are
                                                                                            OS-owned on the face (timeline/health/menu); watchfaces cannot push
                                                                                               interactive secondary screens into usability. Double-tap-accel entry was
                                                                                                  considered and rejected as a user nightmare.
                                                                                                  2. Persist storage is per-app sandboxed. No app↔app sharing on watch, in the
                                                                                                     Pebble phone app, or anywhere else. The only sanctioned cross-binary
                                                                                                        bridges: timeline pins (with openWatchApp launch args), AppMessage
                                                                                                           (phone↔own binary only).
                                                                                                           3. Timeline is write-only/push-only to apps; watchfaces cannot read pins.
                                                                                                           4. Quick Launch assigns apps (and OS functions) only — not watchfaces.
                                                                                                           5. PebbleKit JS lifecycle is tied to its watch binary: spawns at launch,
                                                                                                              killed at exit. No persistent phone-side service via the Pebble app.
                                                                                                              6. Wakeup ≠ Worker. (Muninn demonstrates both coexisting; we use neither
                                                                                                                 worker nor any background process.)
                                                                                                                 7. AppExitReason (SDK 4.0+) lets a wakeup-launched app exit back to the
                                                                                                                    watchface cleanly.
    
                                                                                                                    ### 4.5 Input model (final)
    
                                                                                                                    - **Watchface**: no input. All interaction is customization via the phone
                                                                                                                      settings editor (drag-drop on a virtual 200×228 screen, inspector,
                                                                                                                        restore-defaults). Touch-to-wake backlight is a user system setting we
                                                                                                                          never fight.
                                                                                                                          - **Game app**: standard buttons; TouchService edge zones (upper-right/mid/
                                                                                                                            lower-right = UP/SELECT/DOWN analogues) plus the system gesture bridge on
                                                                                                                              MenuLayer/ScrollLayer where suitable; swipe-dismiss = BACK. Central input
                                                                                                                                dispatch; every screen declares its actions once.
                                                                                                                                - **Launch**: Quick Launch binds a button to the game app (single tap on
                                                                                                                                  current hardware). `launch_reason()` detects QUICK_LAUNCH; timeline pin
                                                                                                                                    taps deep-link via `launch_get_args()`.
    
                                                                                                                                    ## 5. Watchface Widgets (all toggleable, movable, font/color stylable)
    
                                                                                                                                    Ten widgets: Time, Date, Watch Battery, Phone Battery, Steps, Distance,
                                                                                                                                    Weather composite (temp + hi/lo, sub-elements hideable), Weather Advisory
                                                                                                                                    (omens), Sleep Time, Current Task strip (game).
    
                                                                                                                                    - Update frequencies user-configurable (health 1/5/15/30/60 min; weather
                                                                                                                                      15/30/60 min) — battery lever. Clock ticks per-minute only. Redraw on
                                                                                                                                        change only (e-paper).
                                                                                                                                        - Metric/imperial toggle; conversion at render time only.
                                                                                                                                        - **Current Task strip**: renders from cached server packet; countdown
                                                                                                                                          computed locally from end timestamp per-minute; staleness is detectable
                                                                                                                                            and shown honestly ("Return for your reward"). With no packet: hidden or
                                                                                                                                              "No hero yet".
                                                                                                                                              - **Watch Battery**: modes `percent | days | both` (default `both`).
                                                                                                                                                Days-remaining computed ON-WATCH (Muninn-inspired mechanics, own code):
                                                                                                                                                  battery samples piggyback on wake-ups the face already performs;
                                                                                                                                                    moving-average drain rate; charge events recalibrate; honest 3-sample
                                                                                                                                                      cold start (percentage-only until trend exists). Gradient moss→brass→
                                                                                                                                                        oxblood applies to both display modes. **Muninn web ingestion rejected**
                                                                                                                                                          (unstable third-party surface; more code + radio than the math).
                                                                                                                                                          - Weather/phone-battery arrive via face's own pkjs; JS lifecycle = face
                                                                                                                                                            foreground (poll on JS wake, not fixed timers). Cache last-good in persist.
    
                                                                                                                                                            ## 6.9. Game Mechanics, Settlement, Theme
    
                                                                                                                                                            **Carried over verbatim from v1 — FINAL, locked.** Idle pillar (15-min
                                                                                                                                                            chunks, 36-chunk budget, lazy resolution, deterministic seeds, items-as-XP,
                                                                                                                                                            skills-only), combat pillar (barrows, gear-defined builds, agility-as-armor,
                                                                                                                                                            blackout/permadeath), settlement chain, Celtic theme. Unchanged.
    
                                                                                                                                                            ## 10. Multiplayer & Server Evolution (deferred)
    
                                                                                                                                                            Solo-first. The bridge server's `player_id`+token becomes the account seed.
                                                                                                                                                            Phased: trade codes → shared-storage rivals → async competition /
                                                                                                                                                            leaderboards — all served by the same self-hostable TS server, which is
                                                                                                                                                            also the donation-sustainability story. No P2P on Pebble (established).
    
                                                                                                                                                            ## 11. Design Laws (north stars)
    
                                                                                                                                                            1. **Casual glance game — benefit, not chore.** Nothing demands a session;
                                                                                                                                                               no interruptions; the watchface never buzzes for game reasons.
                                                                                                                                                               2. **Game logic is dormant until invited.** Math only at check-in / enqueue /
                                                                                                                                                                  cancel / collect / opt-in semantic wakeups. **No background process.**
                                                                                                                                                                  3. Fully freeform face; "restore default" escape hatch.
                                                                                                                                                                  4. **Never load-bearing externals.** Server down, Muninn gone, phone away —
                                                                                                                                                                     the game plays, the face degrades gracefully and silently.
                                                                                                                                                                     5. Verified-platform-only features: if the docs don't permit it, we don't
                                                                                                                                                                        ship it (see §4.4 ledger).
    
                                                                                                                                                                        ## 12. Status & Next Steps
    
                                                                                                                                                                        **Done:** full design spec; architecture verified against current SDK docs;
                                                                                                                                                                        server language (zero-dep TS) and self-hosting model locked; battery-days
                                                                                                                                                                        decision (self-compute) locked.
    
                                                                                                                                                                        **Build order:**
                                                                                                                                                                        1. **Watchface skeleton** — `main.c`: widget table (10 types), renderer,
                                                                                                                                                                           layout persist (own key, own version byte), placeholder task strip,
                                                                                                                                                                              clock/date/battery(percent) live, stubs for the rest
                                                                                                                                                                              2. **`/docs/PROTOCOL.md`** — packet/API contract (face-packet schema,
                                                                                                                                                                                 sync_version semantics, failure ladder)
                                                                                                                                                                                 3. **`server.ts`** — zero-dep TS mirror; curl-testable before any client
                                                                                                                                                                                 4. **`widgets.c` + font table** — health/weather wiring; battery days-
                                                                                                                                                                                    remaining estimator
                                                                                                                                                                                    5. **Settings editor** (phase 2, drag-drop virtual face)
                                                                                                                                                                                    6. **Game watchapp** — `game.c` in full (chunk resolver, enqueue validator,
                                                                                                                                                                                       XP routing, prorating, persist version byte); Play Mode menus; wake
                                                                                                                                                                                          cycle; App Glance + pins
                                                                                                                                                                                          7. **Content tables** — TASKS/RECIPES/ITEMS (xp_value), founding chain,
                                                                                                                                                                                             MONSTERS[]
                                                                                                                                                                                             8. PDC sprites, theme skinning, polish
    
                                                                                                                                                                                             **First build verification checklist:** `pebble build --emery` + emulator;
                                                                                                                                                                                             TouchService delivery + subscription lifecycle; AppMessage plumbing against
                                                                                                                                                                                             stub server endpoint.