← all games
stealth-fable-max — FRICTION.md
# Friction log — stealth-fable-max
Run: top-down stealth vignette ("MOTH"), YAGE 0.9.0, addons abilities 0.1.0 + inventory 0.1.0.
Logged in the moment, in build order. Categories: API friction / missing primitive / recipe gap / docs ambiguity / asset gap / test-infra gap.
---
## 1. LLM-docs index is hard to find; addon docs invisible from the documented entry points
**Trying to do:** load the YAGE LLM docs before writing code, per the yage-game-developer skill ("the LLM docs published at https://yage.dev/llms/").
**What happened:** `https://yage.dev/llms/` returns a 404 (only concrete files like `llms/quick-start.md` resolve). The site's sitemap does not list the llms markdown files, so there was no way to enumerate them from the web. I fell back to `ls docs/llms/` in the yage monorepo — which is **missing the addons**: `docs/llms/packages/` has 13 engine packages and no `addons/` folder, so I initially concluded the two required addons had no LLM docs at all. The real index is `https://yage.dev/llms.txt`, which lists `llms/addons/abilities.md` + `llms/addons/inventory.md` (sourced from `packages/addons/<name>/docs/llms/`, not from `docs/llms/`). I only found `llms.txt` because the create-yage success banner happens to print it. An agent that reads the skill and the monorepo — the two documented entry points — never sees the addon docs.
**Category:** docs ambiguity
## 2. `npm search` does not return the abilities/inventory addons
**Trying to do:** confirm the two required addons exist on npm and see what else is in the `@yagejs-addons` scope.
**What happened:** `npm search "@yagejs-addons" --json` returns `@yagejs-addons/dialogue` plus unrelated noise — `abilities` and `inventory` are absent from search results (both exist; `npm view` resolves them fine, published at 0.1.0). An agent told "use the abilities addon if it exists" and probing with search would wrongly conclude it doesn't. Likely an npm indexing lag, but discoverability of the addon family currently depends on already knowing the exact package names.
**Category:** docs ambiguity
## 3. No stealth genre checklist; recipes folder is empty
**Trying to do:** follow the skill's first-read map: genre design checklist, then a mechanic recipe.
**What happened:** `references/design/` has RPG and shooter checklists only — nothing for stealth (vision cones, alert FSMs, light/shadow detection, noise propagation are all genre staples with known-good shapes). `references/recipes/` contains only `TEMPLATE.md` — there are no recipes at all yet, so the "recipe matching the mechanic" step is a no-op for every mechanic. I derived the stealth model (detection meter, alert states, hearing radii) from scratch; a checklist would have cut the design time and standardized the numbers.
**Category:** recipe gap
## 4. No procedural/synthesized audio primitive; `@yagejs/audio` is file-only
**Trying to do:** build the required soundscape (room tone, footsteps, stings, muffled distance) with zero asset files, as the run spec demands.
**What happened:** `@yagejs/audio` is a channel wrapper over `@pixi/sound` whose only input is a file path (`sound("assets/coin.wav")`). There is no oscillator/buffer/synthesis surface, no way to register a runtime-generated AudioBuffer under an alias, and therefore no way to route synthesized sound through the engine's channel/volume/mute system. I hand-rolled a WebAudio synth engine (master bus, channel gains, lowpass "muffle" bus, distance/pan model, ~30 procedural SFX) completely outside the engine. Renderer has the exact analogue solved — `registerTexture(key, texture)` for runtime textures — audio has no equivalent (`registerSound(alias, buffer)` would have been enough).
**Category:** missing primitive
## 5. No 2D lighting/darkness primitive
**Trying to do:** light pools and shadow zones that double as stealth mechanics (detection modifier) and mood.
**What happened:** the engine has no lighting concept — no light component, no darkness/occlusion layer, nothing in `@yagejs/effects` (which has 18 screen-filter presets but no light-mask primitive). I built it by hand: a world-sized darkness `GraphicsComponent` with light shapes subtracted via the pixi `cut()` escape hatch, warm radial-gradient sprites layered under it, plus a scene-scoped `isLit(point)` service for the detection math. Workable, but this is the biggest hand-rolled system in a stealth game, and cut() gives hard-edged holes — soft light falloff has to be faked by layering gradients over the hole edges.
**Category:** missing primitive
## 6. Inventory def metadata (`ItemDef.data`) is untyped, unlike per-stack data
**Trying to do:** attach `{ weight, value, tier }` to every item definition and read it back typed (weight drives the speed/noise penalty, value drives the summary).
**What happened:** the addon has a typed path for per-STACK instance data (`instance: instanceData<T>()`, inferred end-to-end) but per-DEF metadata is `data?: Readonly<Record<string, unknown>>`. My `ItemMeta` interface failed to assign into it (missing index signature) and reads need `as unknown as ItemMeta` casts. The asymmetry surprised me: def-level constants (weight, value, rarity) are at least as common as rolled per-stack stats. A `defineItems<TMeta>()` type parameter — or documenting the index-signature requirement — would remove the cast.
**Category:** API friction
## 7. `Inventory.stacks()` return shape is not documented (it is NOT a stack array)
**Trying to do:** iterate carried items for weight totals and the extraction summary.
**What happened:** the LLM doc lists `get(slot) / firstSlot(itemId) / stacks()` with no return types. I assumed `stacks()` returns `ItemStack[]` and wrote `stack.itemId` at six call sites; it actually returns `LocatedStack[]` (`{ slot, stack }` pairs), which the doc only defines in the context of `find/findAll`. All six sites failed the typecheck at once. One line in the doc — `stacks(): LocatedStack[]` — would have prevented it.
**Category:** docs ambiguity
## 8. Inventory panel confirm dies when the game disables its input group
**Trying to do:** the standard menu pattern — disable the "gameplay" input group while the satchel panel is open so E/Space/WASD stop driving the world.
**What happened:** the addon's confirm key IS the `interact` action (its default action set reuses common gameplay names). My `interact` lived in the gameplay group, so opening the panel disabled the panel's own confirm: navigation worked (`move-*` were ungrouped) but E silently did nothing — no menu, no warning, cost a probe cycle to notice. The doc's gotcha list warns that the action names must *exist* and that the dialogue addon *also* binds `interact`, but not that group-disable patterns kill the panel. Workaround: keep `interact` ungrouped and gate world interactions on `isGroupEnabled("gameplay")` in the player controller instead. The doc should state which enable/disable state the addon expects its actions to be in while the panel is open, or namespace its action names (`inv-confirm`).
**Category:** docs ambiguity
## 9. Removing a `ColliderComponent` leaves its Rapier collider alive in the world
**Trying to do:** open a door: `entity.remove(ColliderComponent)` so the doorway stops blocking movement and sight.
**What happened:** the component came off the entity (the Inspector's component list no longer showed it), but the physical collider kept existing — the player still collided at the door line (stopped at exactly door-face minus player-radius, y=1059 against a face at 1070) and a wall-filtered `world.raycast` still reported a hit through the "open" door. Nothing hinted at the failure: no warning, `getErrors()` empty. The core docs say `entity.remove(Class)` calls `onRemove`/`onDestroy`, and `ColliderComponent` declares `onDestroy()`, so either the teardown isn't wired into the remove path or it doesn't detach the collider from the Rapier world. Workaround that works: put the collider on a child entity and `destroy()` the child — entity teardown does remove the collider (verified: sight ray passes and the player walks through afterward). Playtesting caught this, not the Inspector, because component lists lie about the physics world here.
**Category:** API friction