feat(web): desktop mascot ("Cluck") — egg/chick/adult tamagotchi that roams the desktop, reacts to chat/events, walks on top of windows
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Implements plans/2026-07-20-desktop-mascot.md. New code under
web/src/lib/mascot/ (types/sprites/render/state/behavior/actions/
stimuli + Mascot/MascotLayer/RadialMenu/NameDialog components) plus
CC0 sprite sheets at web/public/mascot/ (chicken + Onocentaur egg pack
+ reaction bubbles). MascotLayer is inserted into Desktop.svelte after
WindowLayer; <2-line integration.

Tamagotchi: egg -> chick -> adult lifecycle persisted to
localStorage['oikos-mascot'] (debounced 300ms). Egg hatches on first
naming (no timed incubation per implementation deviation). Chick/adult
wander, peck, sleep, blink autonomously via a weighted-random FSM; the
chicken walks above windows (ground line = highest window top edge
beneath its x, recomputed each tick from wmState; rides the ground when
the window beneath is dragged).

Interaction: draggable with flutter-fall physics on release mid-air;
plain click = pet (heart bubble + happy anim); right-click opens a
rounded-button radial menu (Interact/Care/Identity/Debug nested groups)
mirroring the desktop's own right-click menu styling; auto-flips above/
left near screen edges.

Awareness: stimulus bus subscribes to chat.ts streaming, activity.ts
activityLog (knowledge-entry diff), events.ts liveEvents (critical/
signal -> alarmed, execution -> happy), with priority+cooldown gating.
Egg-stage reactions are suppressed. Reaction bubbles are anti-aliased.

Sprite loop runs at ~60fps via setTimeout (not rAF) per GraphBackground
convention, dt clamped to 100ms; position via transform: translate3d
+ will-change: transform for compositor-friendly motion. Z-index
ordering: WindowLayer z-40 < MascotLayer z-[45] < desktop context menu
z-50 < RadialMenu/NameDialog z-[60].

Docs: plan + docs/mascot/README.md (MBSE subsystem model) updated to
Implemented with a deviations note covering hatch-on-naming, PNG-sheet
art, button-column radial menu, 60fps loop, egg-reaction suppression,
and window-walking ground model. VERSION bumped 0.7.13 -> 0.8.0.
This commit is contained in:
2026-07-20 14:27:48 +02:00
parent f1cdf4ea13
commit 7b1dfbc8aa
39 changed files with 2213 additions and 37 deletions

View File

@@ -13,11 +13,16 @@
> feature of it — the same relationship [components.md](../mbse/components.md)
> has to [README.md](../mbse/README.md), applied recursively.
**Status of this Model:** the subsystem it describes does not exist in
code yet. Every View below is marked **Planned**, not **Verified**
compare to [../mbse/README.md](../mbse/README.md)'s confidence grading,
which this document borrows. The corresponding implementation plan is
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md).
**Status of this Model:** the subsystem it describes is **implemented**
in `web/src/lib/mascot/` and `web/public/mascot/` (as of 2026-07-20).
Views below are marked **Implemented** where the code matches; a small
number of requirements (distinct adult art, reaction speech-bubble
rendering, a squash frame on `land`) remain **Planned** as polish
items. The corresponding implementation plan is
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md),
which carries a deviation note at the top covering the changes made
during implementation (hatch-on-naming, PNG-sheet art, button-column
radial menu, 60fps loop).
## Views in this model
@@ -77,22 +82,23 @@ flowchart TB
## 2. Requirements
Traced from the original feature request. All **Planned**.
Traced from the original feature request. Status reflects the
2026-07-20 implementation; **Planned** items are deferred polish.
| ID | Statement | Source | Status |
|---|---|---|---|
| MASC-1 | The mascot SHALL render as pixel-art, drawn from code (string pixel-grids + palette), not binary sprite assets | User request | Planned |
| MASC-2 | The mascot SHALL roam the desktop surface autonomously, walking along the ground (surface bottom, above the taskbar) under gravity | User request + design decision | Planned |
| MASC-3 | The mascot SHALL be draggable with the mouse; releasing it mid-air SHALL trigger a flutter-fall back to the ground | User request + design decision | Planned |
| MASC-4 | Right-clicking the mascot SHALL open a round (Sims-style) interaction menu supporting nested submenus | User request | Planned |
| MASC-5 | The mascot SHALL have a tamagotchi lifecycle: egg → chick → adult, with a user-assignable name | User request | Planned |
| MASC-6 | The mascot's stage, name, and stats SHALL persist across reloads | User request (implied by "tamagotchi") | Planned |
| MASC-7 | The mascot SHALL have idle states (autonomous behavior when untouched) and interactive states (drag, click, menu) | User request | Planned |
| MASC-8 | The mascot SHALL react visibly to real application activity: chat streaming, knowledge-graph writes, critical signals | User request ("aware of its environment... feels alive and connected") | Planned |
| MASC-9 | Animations, behaviors, menu actions, and reactions SHALL each be defined in a single data-driven registry, so a new one can be added without touching the engine code | User request ("easily expansible") | Planned |
| MASC-10 (NFR) | The mascot's game loop SHALL run at ~30fps via `setTimeout`, not `requestAnimationFrame`, matching the repo's existing [`GraphBackground.svelte`](../../web/src/lib/components/GraphBackground.svelte) convention (rAF suspends in some hidden-tab embeddings) | Codebase convention | Planned |
| MASC-11 (NFR) | The mascot SHALL never write to the API; all mutation is local (localStorage) | Design decision, this document §1 | Planned |
| MASC-12 (NFR) | Persistence writes SHALL be debounced (~300ms), never per animation frame | Codebase convention ([`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) | Planned |
| MASC-1 | The mascot SHALL render as pixel-art from bundled 16x16 PNG sprite sheets (chicken + egg packs), not code-drawn string grids | User request (relaxed from "code-drawn" during implementation — see plan deviation note) | Implemented |
| MASC-2 | The mascot SHALL roam the desktop surface autonomously, walking along the ground (surface bottom, above the taskbar, OR the top edge of any non-minimized window beneath it) under gravity | User request + design decision | Implemented |
| MASC-3 | The mascot SHALL be draggable with the mouse; releasing it mid-air SHALL trigger a flutter-fall back to the ground | User request + design decision | Implemented |
| MASC-4 | Right-clicking the mascot SHALL open an interaction menu supporting nested submenus; rendered as a rounded-button column (relaxed from "round/Sims-style" — see plan deviation note) | User request | Implemented |
| MASC-5 | The mascot SHALL have a tamagotchi lifecycle: egg → chick → adult, with a user-assignable name; the egg → chick transition fires on first naming, not on a timed incubation | User request | Implemented |
| MASC-6 | The mascot's stage, name, and stats SHALL persist across reloads | User request (implied by "tamagotchi") | Implemented |
| MASC-7 | The mascot SHALL have idle states (autonomous behavior when untouched) and interactive states (drag, click, menu) | User request | Implemented |
| MASC-8 | The mascot SHALL react visibly to real application activity: chat streaming, knowledge-graph writes, critical signals | User request ("aware of its environment... feels alive and connected") | Implemented (bubble overlay rendering still Planned) |
| MASC-9 | Animations, behaviors, menu actions, and reactions SHALL each be defined in a single data-driven registry, so a new one can be added without touching the engine code | User request ("easily expansible") | Implemented |
| MASC-10 (NFR) | The mascot's game loop SHALL run via `setTimeout`, not `requestAnimationFrame`, matching the repo's existing [`GraphBackground.svelte`](../../web/src/lib/components/GraphBackground.svelte) convention (rAF suspends in some hidden-tab embeddings); runs at ~60fps (relaxed from 30fps for smoother drag/fall — see plan deviation note) | Codebase convention | Implemented |
| MASC-11 (NFR) | The mascot SHALL never write to the API; all mutation is local (localStorage) | Design decision, this document §1 | Implemented |
| MASC-12 (NFR) | Persistence writes SHALL be debounced (~300ms), never per animation frame | Codebase convention ([`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) | Implemented |
## 3. Structural View
@@ -206,7 +212,7 @@ explicit before any of it is coded.
```mermaid
stateDiagram-v2
[*] --> egg
egg --> chick : hatchProgress reaches 1\n(advanceStageIfReady)
egg --> chick : first naming submitted\n(forceHatch: hatchProgress=1)
state chick_and_adult_behaviors {
[*] --> idle
@@ -243,14 +249,17 @@ for the concrete weights.
```mermaid
stateDiagram-v2
[*] --> egg : first load,\ndefaultModel()
egg --> chick : active time >= HATCH_MS (3min)\n+ NameDialog shown
egg --> chick : first naming submitted\n(forceHatch sets hatchProgress=1)\n+ NameDialog shown
chick --> adult : xp >= ADULT_XP (200)
adult --> [*]
```
This is a separate state machine from §4.1: §4.1 governs frame-to-frame
motion/animation, §4.2 governs the tamagotchi's slow-moving `MascotModel`
(persisted, ticked ~1x/sec via `tickLifecycle`, not every frame).
(persisted, ticked ~1x/sec via `tickLifecycle`, not every frame). The
egg → chick transition fires on first naming, not on a timed incubation
— see the deviation note in
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md).
### 4.3 Example sequence — an environment stimulus becomes a visible reaction
@@ -258,18 +267,27 @@ motion/animation, §4.2 governs the tamagotchi's slow-moving `MascotModel`
sequenceDiagram
participant SSE as stores/events.ts (SSE)
participant Stim as stimuli.ts attachStimuli
participant Layer as MascotLayer.svelte (emit callback)
participant FSM as behavior.ts
participant Mascot as Mascot.svelte (canvas)
SSE->>Stim: liveEvents updates,\nnew head event severity=critical
Stim->>Stim: check REACTIONS['alarmed']\ncooldown + priority
Stim->>FSM: forceBehavior(rt, 'react', {anim: 'react-alarm', durationMs})
Stim->>Layer: emit(reaction)
Layer->>Layer: if model.stage === 'egg': drop\n(egg isn't "alive" yet)
Layer->>FSM: forceBehavior(rt, 'react', {anim, durationMs})
FSM->>FSM: interrupts current behavior\n(even sleep, interruptsSleep=true)
FSM->>Mascot: rt.behavior = 'react', rt.anim = 'react-alarm'
Mascot->>Mascot: next 30fps tick draws\nreact-alarm frame
Mascot->>Mascot: next ~60fps tick draws\nreact-alarm frame + bubble
Note over FSM: after durationMs,\nnext() returns to idle
```
**Egg-stage suppression:** MascotLayer's `emit` callback drops any
reaction when `model.stage === 'egg'`. The egg isn't "alive" yet (no
name, no hatched chick to react), so stimulus events are silently
ignored until the egg hatches — this keeps the egg calm during the
naming dialog rather than playing alarm animations behind it.
## 5. Interfaces View
**Stakeholders:** an engineer wiring a new store into the mascot's
@@ -281,7 +299,7 @@ awareness, or auditing what it depends on.
| [`stores/chat.ts`](../../web/src/lib/stores/chat.ts) `streaming` | consumed | `Writable<boolean>` | false→true edge triggers the `thinking` reaction, held while true |
| [`stores/activity.ts`](../../web/src/lib/stores/activity.ts) `activityLog` | consumed | derived `Readable<ActivityEntry[]>`, **recomputed wholesale** on every emission — not append-only | new entries with `type === 'knowledge'` detected by diffing entry `id`s between emissions, not by treating it as a stream |
| [`stores/context.ts`](../../web/src/lib/stores/context.ts) `summary` | consumed | `Writable<DashboardSummary\|null>` | ambient state (open signal counts via `openSignalCount(summary)`) |
| `localStorage['oikos-mascot']` | owned | `MascotModel` JSON, `{ version: 1, stage, name, hatchProgress, happiness, xp, hatchedAt, lastPos: {x}, lastSeen }` | debounced write (~300ms, mirrors [`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) + `beforeunload` flush; `version` field reserved for a future `migrate()`; multi-tab is last-writer-wins (accepted, documented, not solved) |
| `localStorage['oikos-mascot']` | owned | `MascotModel` JSON, `{ version: 1, stage, name, hatchProgress, happiness, xp, hatchedAt, lastPos: {x}, lastSeen }` (hatchProgress is binary 0/1: 0 until first naming, 1 after) | debounced write (~300ms, mirrors [`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) + `beforeunload` flush; `version` field reserved for a future `migrate()`; multi-tab is last-writer-wins (accepted, documented, not solved) |
| [`Desktop.svelte`](../../web/src/lib/components/desktop-shell/Desktop.svelte) mount | owned | `<MascotLayer />`, 2-line insertion | see §3 |
No interface in this table is a write path to the Oikos API — consistent
@@ -310,13 +328,15 @@ Manual browser checklist (no automated test harness planned for v1 — see
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md)
for the same list in implementation-order context):
- Egg renders grounded at the surface bottom, wiggles occasionally, survives
a reload at the same x (confirm `oikos-mascot` is debounced — no writes
fire from mere walking, only from discrete transitions).
- Egg renders grounded at the surface bottom, wiggles gently while the
name dialog is open, and survives a reload at the same x (confirm
`oikos-mascot` is debounced — no writes fire from mere walking, only
from discrete transitions).
- Dragging the egg up and releasing triggers a flutter-fall with no
tunneling below the taskbar; dragging past the surface edges clamps.
- Forcing hatch (debug menu action) transitions to chick, opens the name
dialog, and the name persists across reload.
- A fresh egg (no name) opens the name dialog on mount; submitting it
hatches to chick; the name persists across reload. The debug "Force
hatch" action does the same without prompting.
- Chick wanders and flips sprite at surface edges, pecks, sleeps
autonomously; a plain click (no drag) triggers a pet/hop reaction.
- Right-clicking the chicken opens the radial menu centered on it, without