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.
208 lines
28 KiB
Markdown
208 lines
28 KiB
Markdown
# 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. |