← all games
adventure-fable-unknown — FRICTION.md
# Friction log — adventure-fable-unknown
Logged in the moment while building. Categories: API friction / missing primitive /
recipe gap / docs ambiguity / asset gap / test-infra gap.
## 1. Addon docs exist but are invisible from the docs index
- **Trying to:** find consumer docs for `@yagejs-addons/interaction`, `/quests`,
`/steering` before writing NPC/quest code. The genre design checklist maps
directly to these packages.
- **What happened:** `https://yage.dev/llms.txt` lists only three addons
(dialogue, inventory, abilities). The interaction/quests/steering docs ARE
published (`https://yage.dev/llms/addons/interaction.md` etc. return 200) but
nothing links to them — an agent following the index would conclude the
addons are undocumented or don't exist, and hand-roll proximity prompts and a
quest log. I only found them because I could read the monorepo's
`packages/addons/*/docs/llms/` tree directly (via the addon *authoring* guide,
which is aimed at authors, not consumers).
- **Category:** docs ambiguity (stale `llms.txt` index vs published files).
## 2. No procedural/runtime audio path in @yagejs/audio
- **Trying to:** ship fully synthesized audio (bell strikes with inharmonic
partials, wind/wave ambience, UI blips) with zero asset files, as this run
requires.
- **What happened:** `@yagejs/audio` is alias-based playback over `@pixi/sound`:
the only documented way in is the `sound("file.ext")` asset factory +
preload. There is no way to register a runtime-generated `AudioBuffer` under
an alias — no analogue of the renderer's `registerTexture(key, tex)`, which
solves exactly this problem for visuals (that symmetry gap is what makes it
feel like a hole rather than a scope choice). Consequence: the whole audio
layer for this game bypasses `@yagejs/audio` — hand-rolled WebAudio synth +
mixer, re-implementing channel volumes, unlock-on-gesture, and mute-on-blur
that the engine package already has.
- **Category:** missing primitive (`registerSound(alias, buffer)` or any
synthesis-friendly entry point).
## 3. patterns.md example calls `Transform.translate` with a Vec2; the API takes (dx, dy)
- **Trying to:** move the wandering cat with `tr.translate(new Vec2(dx, dy))`,
copied from the docs.
- **What happened:** compile error — `translate(dx: number, dy: number)` per
`@yagejs/core`'s d.ts. The LLM docs' `patterns.md` (Testing Patterns →
"Unit testing a component") shows `t.translate(new Vec2(0, 9.8 * dt))`, and
core-concepts.md says "Transform has mutating methods (`setPosition`,
`translate`)" without signatures, so the doc example is the only signature
an agent sees — and it's wrong. One-minute fix, but only because tsc caught
it; in a JS project it would have been a silent runtime `NaN`.
- **Category:** docs ambiguity (doc example contradicts the typed API).
## 4. Entities have `.scene` but no `.context` (components have both)
- **Trying to:** resolve services (dialogue, HUD) from inside `Entity`
subclass methods (`GreatBell.interacted`, an `Interactable.onInteract`
closure created in `setup()`).
- **What happened:** `this.context` doesn't exist on `Entity` — components get
`this.context`/`this.service()`, entities only `this.scene`. The fix is
`this.scene.context.tryResolve(...)`, easy once seen, but the docs never
state which accessors exist at the entity level, and the asymmetry with
Component (which documents `this.service`/`this.use`/`this.context`
prominently) invites exactly this mistake. Six call sites hit it in one file.
- **Category:** docs ambiguity (entity-level service access is undocumented).
## 5. quick-start presents Inspector time/input control as debug:true-only; it needs DebugPlugin
- **Trying to:** verify the game the way the docs (and this repo's skill)
prescribe: freeze the clock with `window.__yage__.inspector.time.freeze()` +
`step(n)` and drive synthetic input, because the game does not advance in a
backgrounded tab (rAF never fires — exactly the failure mode the skill's
gotcha list predicts).
- **What happened:** `Error: Inspector.time requires DebugPlugin to be
active.` quick-start.md's "Inspector (runtime queries)" section shows
`time.freeze()`, `time.step(1)`, and `input.keyDown(...)` right after
"engine constructed with `debug: true` installs an introspection API" —
nothing marks the time/input namespaces as DebugPlugin-gated (the section
explicitly frames extensions like `debug` as the plugin-gated part).
Snapshot/query calls worked, so the failure only shows up at the exact
moment an agent starts driving frames.
- **Category:** docs ambiguity (which Inspector namespaces need DebugPlugin).
## 6. Frame-stepped verification can't drive dialogue: advance is async inside
- **Trying to:** drive a full conversation (reveal → advance → choice) with the
engine's own recommended agent-verification loop — `inspector.time.freeze()`
+ `step(n)` + synthetic input — inside one synchronous script.
- **What happened:** only the first advance per script registered. The
dialogue session's `advance()` refuses re-entry while `advancing` is held,
and `advanceLine()` is async — its continuation (which clears the latch and
presents the next line) sits in the microtask queue, which never drains
while a synchronous `step()` loop is running. Symptom from the outside:
taps look randomly swallowed, conversations look stuck, and nothing errors.
Burned well over an hour on wrong theories (reveal speed, rate limits,
disabled components) before reading the addon source. Workaround: yield a
macrotask (`await setTimeout(0)`) between taps so the runner's continuation
can land — after which one script can drive whole conversations.
- **Category:** test-infra gap (step-driven verification vs async runner
internals; nothing in the docs warns about it, and the skill's prescribed
freeze+step pattern walks straight into it).
## 7. A second engine copy fails as a cryptic pixi crash, with no duplicate-instance guard
- **Trying to:** add `@yagejs-addons/virtual-controls`. My `npm install` ran in
the wrong directory (repo root instead of the game folder — my mistake), so
Node resolution found the addon up-tree, where it pulled its own
`@yagejs/renderer` + `pixi.js` copies next to the game's.
- **What happened:** the game crashed on the first rendered frame with
`TypeError: Failed to execute 'createPattern' on 'CanvasRenderingContext2D'`
deep inside pixi's canvas-text generator — no mention of the actual problem
(two engine instances exchanging objects that fail each other's instanceof
checks). The addons authoring guide names exactly this hazard as the reason
engine deps are peers, but nothing at runtime detects it: a dev-mode "two
@yagejs/renderer instances loaded" warning would have turned a 25-minute
cache-clearing goose chase into a one-line fix.
- **Category:** API friction (missing duplicate-instance diagnostic; the
failure surfaces as an unrelated pixi error).
## 8. Synthetic touch pointers press at (0,0) unless you know the undocumented move-with-id form
- **Trying to:** drive the touch overlay in verification:
`firePointerDown(0, { id: 9, type: "touch" })` on a fresh pointer id, as
the input docs' own multi-touch example shows.
- **What happened:** the press landed at (0,0) — a pointer that has never
moved has no position, and `firePointerDown` silently inherits
`screenPos (0,0)`, so the overlay's zone test failed and nothing engaged.
The fix is `firePointerMove(x, y, { id, type })` *before* the down — but the
docs only show `firePointerMove(120, 80)` with no options argument, so the
id-carrying form (which the implementation supports) is invisible; I found
it by reading `@yagejs/input`'s dist. The doc example as written can never
position a secondary touch.
- **Category:** docs ambiguity (`firePointerMove` opts undocumented; the
multi-touch example presses at 0,0).