28 KiB
28 KiB
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 (§§6–9) carry over unchanged.
1. Concept
A Celtic-themed idle RPG ecosystem for Pebble Time 2 (emery), built as:
-
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 ~1–2 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.