← 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).