← all games
arena-shooter-fable — FRICTION.md
# Friction log — arena-shooter-fable
Build: top-down arena shooter, YAGE 0.9.0, scaffolded with `create-yage --template minimal`.
Categories: API friction / missing primitive / recipe gap / docs ambiguity / asset gap / test-infra gap.
Entries are appended in the moment, in build order.
---
## 1. No way to get sound without shipping audio files
- **Trying to do:** add shoot / hit / death / wave-start sounds. The project has no audio assets and none are bundled with the engine or templates.
- **What happened:** `@yagejs/audio` is a channel wrapper over `@pixi/sound` — it plays loaded files only. There is no procedural/synth source (oscillator, noise burst) and no starter SFX pack, so I wrote a hand-rolled WebAudio synth module (`src/sfx.ts`, ~100 lines) outside the engine. That module bypasses the engine's channel volumes, mute, and blur auto-pause.
- **Category:** asset gap (with a missing-primitive edge: a tiny "beep/noise burst" synth in `@yagejs/audio` would cover every prototype).
## 2. `setIntensity` is documented as the canonical dial but is not on `EffectHandle`
- **Trying to do:** a chromatic-aberration pulse on player damage — set the effect's strength to 1, tween it back to 0.
- **What happened:** the effects doc (`docs/llms/packages/effects.md`) says "`setIntensity` is the canonical 'how strong is this effect right now' dial", so I wrote `handle.setIntensity(...)`. It does not compile: `setIntensity` lives on the internal `Effect` factory interface, not on the public `EffectHandle` (which only has `fadeIn`/`fadeOut`/`setEnabled`/`run`). Preset extras expose rebase setters (`setSeparation`) that preserve the intensity ratio — there is no public 0..1 dial. Workaround: choreograph `fadeIn(0.05)` + delayed `fadeOut(0.3)` through `handle.run`.
- **Category:** docs ambiguity (secondary: API friction — a public `setIntensity` on the handle is the obvious primitive).
## 3. Quick-start implies `inspector.time` works with `debug: true` alone
- **Trying to do:** verify the game deterministically: `window.__yage__.inspector.time.freeze()` + `step(n)`, as the quick-start's Testing & Debugging section shows right after `new Engine({ debug: true })`.
- **What happened:** at runtime it throws `Inspector.time requires DebugPlugin to be active`. The quick-start example never installs `DebugPlugin`, and the sentence introducing the snippet says only "when the engine is constructed with `debug: true`". Fixed by adding `engine.use(new DebugPlugin())`.
- **Category:** docs ambiguity.
## 4. `collider.getOverlapping()` never fires for resolved contacts (player-vs-enemy contact damage)
- **Trying to do:** contact damage — each frame the player is vulnerable, poll `collider.getOverlapping()` and take damage if any entity tagged `enemy` is in the list.
- **What happened:** silent failure. Both bodies are non-sensor dynamics, so the physics solver resolves the contact before penetration; enemies press against the player at exactly the sum of the two radii (verified via Inspector: distances 27–34 px for radii 12+15) and the overlap query returns nothing, because nothing actually overlaps. No warning anywhere; the game just never dealt damage. The physics doc lists `getOverlapping` right under the collision-events section with no note that it only reports true penetration/sensor overlap. Workaround: per-frame distance check against enemy radii (`onCollision` alone also misses the "enemy already touching when invulnerability ends" case).
- **Category:** docs ambiguity (secondary: recipe gap — "contact damage with i-frames" is a stock top-down pattern that deserves a recipe).
## 5. Inspector synthetic input: `tap()` and `clearAll()` swallow press edges
- **Trying to do:** drive the R-to-restart flow from the Inspector during verification: `inspector.input.tap("KeyR")`, and later `clearAll()` followed by `keyDown("KeyR")`.
- **What happened:** both silently failed to produce an `isJustPressed` edge. `tap()` applies press and release synchronously in one batch, so no stepped frame ever observes the pressed state. `clearAll()` immediately followed by `keyDown` in the same batch also loses the edge; inserting `time.step(1)` between them fixes it. The working pattern is `keyDown` → `step(n)` → `keyUp`, but nothing in the debug docs says `tap` is incompatible with frame stepping.
- **Category:** test-infra gap.