Files
clockwork-hero/DESIGN.md
2026-09-10 17:27:16 +00:00

208 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
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 ~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.