@moku-labs/game 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  **A 2D puzzle game engine where the game is a deterministic graph of business logic.**
4
4
 
5
- `@moku-labs/game` is a Layer-2 framework on [`@moku-labs/core`](https://github.com/moku-labs/core), written in TypeScript, with PixiJS v8 as a peer dependency. You write small nodes and edge tables. The engine runs them, commits state on the edges, saves at rest points and replays the same game without a screen. It is not a general-purpose engine and it ships no genre rules: no match-3, no merge, no physics. V1 is the logic half; V2 adds the screen: an own small ECS, projections from committed state, a Pixi v8 renderer loaded lazily, gestures as data components, typed asset keys and scenes as declarations. V3 adds the interface: choreographies as data, strings as data, text from MSDF fonts, screens written in JSX and laid out by Yoga, and sound as an effect a node awaits. V4 adds two doors for the editor: sources that read a running game and dev-only commands that drive it. V5 adds cosmetic effects as data: particles and WGSL filters as components, a frame loop for sprites, and the render numbers that say what they cost; then the production pack of the art, icons that wrap with their words, a text field, and visual tests.
5
+ `@moku-labs/game` is a Layer-2 framework on [`@moku-labs/core`](https://github.com/moku-labs/core), written in TypeScript, with PixiJS v8 as a peer dependency. You write small nodes and edge tables. The engine runs them, commits state on the edges, saves at rest points and replays the same game without a screen. It is not a general-purpose engine and it ships no genre rules: no match-3, no merge, no physics. V1 is the logic half; V2 adds the screen: an own small ECS, projections from committed state, a Pixi v8 renderer loaded lazily, gestures as data components, typed asset keys and scenes as declarations. V3 adds the interface: choreographies as data, strings as data, text from MSDF fonts, screens written in JSX and laid out by Yoga, and sound as an effect a node awaits. V4 adds two doors for the editor: sources that read a running game and dev-only commands that drive it. V5 adds cosmetic effects as data: particles and WGSL filters as components, a frame loop for sprites, and the render numbers that say what they cost; then the production pack of the art, icons that wrap with their words, a text field, and visual tests. V6 adds the platform seam: the Back chain, haptics and keep-awake, filters that also draw on WebGL, and the game as a Tauri app on iOS and Android.
6
6
 
7
7
  <br/>
8
8
 
9
- [![version](https://img.shields.io/badge/version-0.0.0-1864ab)](#status)
9
+ [![version](https://img.shields.io/badge/version-0.1.0-1864ab)](#status)
10
10
  [![types](https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white)](#requirements)
11
11
  [![node](https://img.shields.io/badge/node-%3E%3D24-339933?logo=node.js&logoColor=white)](#requirements)
12
12
  [![bun](https://img.shields.io/badge/bun-%3E%3D1.3.14-2da44e?logo=bun&logoColor=white)](#requirements)
@@ -30,7 +30,7 @@
30
30
 
31
31
  ## Status
32
32
 
33
- V1, V2 and V3 are built. V4 has its engine side: the two doors described in [Doors for the editor](#doors-for-the-editor). The editor that uses them is a separate package. V5 is built: the `effects` plugin, the `Frames` component, the `components` prop of the JSX tags and the render numbers; the production pack of `assets` (`--pack`, a v2 manifest with WebP atlas pages); icons inside wrapped text; the `input` tag, a text field; and visual tests in `@moku-labs/game/testing`. Everything after that is a plan and may change.
33
+ V1, V2 and V3 are built. V4 has its engine side: the two doors described in [Doors for the editor](#doors-for-the-editor). The editor that uses them is a separate package. V5 is built: the `effects` plugin, the `Frames` component, the `components` prop of the JSX tags and the render numbers; the production pack of `assets` (`--pack`, a v2 manifest with WebP atlas pages); icons inside wrapped text; the `input` tag, a text field; and visual tests in `@moku-labs/game/testing`. V6 is built: the `platform` plugin with its provider seam, the Back chain, haptics and keep-awake; GLSL twins for custom filters, so they draw on WebGL; the Tauri fallback in `assets`; and a WebGL leg of the visual tests. `0.1.0` is the first working release. The API may still change before `1.0`.
34
34
 
35
35
  | Milestone | State | Scope | Exit criterion |
36
36
  |---|---|---|---|
@@ -39,7 +39,7 @@ V1, V2 and V3 are built. V4 has its engine side: the two doors described in [Doo
39
39
  | V3 | built | `anim`, `i18n`, `text`, `ui`, `audio`, the `@moku-labs/game/jsx-runtime` entry | Popup, HUD and buttons with sound; the same game still plays to the end headless |
40
40
  | V4 | doors built | `/inspect` and `/control` entries | External tools can read and drive a game |
41
41
  | V5 | built | `effects`, `Frames`, render passes and draw calls; the production pack of `assets`, icons in wrapped text, the `input` tag, visual tests in `/testing` | Not defined yet |
42
- | V6 | planned | `platform` | A template game runs on a phone |
42
+ | V6 | built | `platform` (provider seam, Back chain, haptics, keep-awake), custom filters on WebGL, the Tauri fallback in `assets`, a WebGL leg of the visual tests | The fixture game runs as a Tauri app on the iOS simulator and an Android emulator; real phones are not checked yet |
43
43
 
44
44
  Rendering: WebGPU is preferred, with Pixi's WebGL fallback and an honest "unsupported device" screen when neither exists.
45
45
 
@@ -50,7 +50,7 @@ bun add @moku-labs/game pixi.js
50
50
  ```
51
51
 
52
52
  > [!NOTE]
53
- > **Status: `0.0.0`, not published yet.** The package is not on npm. The command above is the intended install line. `pixi.js` `^8.0.0` is a peer dependency. No V1 code imports it.
53
+ > **Status: `0.1.0` on npm.** The first working release; the API may still change before `1.0`. `pixi.js` `^8.0.0` is a peer dependency.
54
54
 
55
55
  > [!IMPORTANT]
56
56
  > Bun only. ESM only. `"sideEffects": false`. There is no CJS build. `yoga-layout` is a dependency the `ui` plugin loads lazily; nothing imports it before `onStart`.
@@ -225,7 +225,7 @@ flowchart LR
225
225
 
226
226
  ## Plugins
227
227
 
228
- Five logic plugins are on every app; the nine screen plugins are the list `screen` a game spreads in; `audio` and `effects` are opt-in, `[...screen, audioPlugin, effectsPlugin]`. `log` and `env` come from [`@moku-labs/common`](https://github.com/moku-labs/common) and sit on every plugin context as `ctx.log` and `ctx.env`.
228
+ Five logic plugins are on every app; the nine screen plugins are the list `screen` a game spreads in; `effects`, `audio` and `platform` are opt-in, `[...screen, effectsPlugin, audioPlugin, platformPlugin]`. `log` and `env` come from [`@moku-labs/common`](https://github.com/moku-labs/common) and sit on every plugin context as `ctx.log` and `ctx.env`.
229
229
 
230
230
  ### Built
231
231
 
@@ -246,7 +246,8 @@ Five logic plugins are on every app; the nine screen plugins are the list `scree
246
246
  | [`text`](./src/plugins/text/README.md) | Complex | The `Text` component, `label()`, `defineTextStyles()`, the tags `<b> <i> <color=#hex> <icon=key>`, measurement from the font's advance table, BitmapText from the MSDF fonts of a bundle | `measure(content, style)`, `styles()` |
247
247
  | [`ui`](./src/plugins/ui/README.md) | Very Complex | A screen is a projection whose `view` returns JSX; the tree is reconciled by identity into entities, laid out by one Yoga solve per change, `Box` is the rest pose; `defineComponent` with `local` and `outcomes`, `popup` as an effect, `defineStyle`, `defineTokens`; every tag takes `components`, extra component values such as a filter; the `input` tag is a text field whose text lives in `local` | `tree()`, `find(key)`, `lint()`, `fill(key, value)` |
248
248
  | [`audio`](./src/plugins/audio/README.md) | Standard | Opt-in. Buses `master`, `music`, `sfx`; `sfx()` descriptors of `anim` and `music()` descriptors handled here; the scene's `music`; volumes read from the committed player through `volumes` | `setVolume(bus, value)`, `volume(bus)`, `mute(bus, on)`, `unlocked()` |
249
- | [`effects`](./src/plugins/effects/README.md) | Complex | Opt-in. Particles: `defineEmitter` and the `Emitter` component, one Pixi `ParticleContainer` per instance stepped by the engine clock. Filters: `defineFilter` turns a WGSL fragment body into a flat component that `tween` drives; `Glow`, `Outline`, `Blur`, `ColorMatrix`, `Noise`, `Displacement`, `Alpha` ship built in. Budgets warn once per crossing; a dev build compiles each WGSL kind before its first instance. Headless it draws nothing | `stats()` |
249
+ | [`effects`](./src/plugins/effects/README.md) | Complex | Opt-in. Particles: `defineEmitter` and the `Emitter` component, one Pixi `ParticleContainer` per instance stepped by the engine clock. Filters: `defineFilter` turns a WGSL fragment body and its GLSL twin into a flat component that `tween` drives, so a custom filter draws on WebGPU and on WebGL; `Glow`, `Outline`, `Blur`, `ColorMatrix`, `Noise`, `Displacement`, `Alpha` ship built in. Budgets warn once per crossing; a dev build compiles each kind before its first instance, WGSL on WebGPU and GLSL on WebGL. Headless it draws nothing | `stats()` |
250
+ | [`platform`](./src/plugins/platform/README.md) | Standard | Opt-in, last in the array. The phone as a provider: the game passes a `PlatformProvider`; its pause and resume become the `"background"` reason of `lifecycle`, its Back press runs the Back chain (Escape, then the intent `back`, then `exit()`), the `haptic` effect reaches `provider.haptic`, and `keepAwake` keeps the screen on while the game runs. Inert without a provider | `back()` |
250
251
 
251
252
  ```mermaid
252
253
  flowchart LR
@@ -290,6 +291,10 @@ flowchart LR
290
291
  EF --> W
291
292
  EF --> R
292
293
  EF --> A
294
+ EF --> AN
295
+ PL["platform"] --> L
296
+ PL --> F
297
+ PL --> I
293
298
  classDef u fill:#0b7285,stroke:#08525f,color:#fff;
294
299
  classDef m fill:#1864ab,stroke:#0d3d6e,color:#fff;
295
300
  classDef s fill:#5c940d,stroke:#3d6208,color:#fff;
@@ -297,18 +302,14 @@ flowchart LR
297
302
  class G u
298
303
  class F,T,L,M,C m
299
304
  class W,R,I,A,S s
300
- class AN,N,X,U,AU,EF v
305
+ class AN,N,X,U,AU,EF,PL v
301
306
  ```
302
307
 
303
- An arrow means "depends on"; for the V3 plugins the edges to `time` and the edges a nearer plugin already implies are left out for space, the plugin READMEs list them in full. `time`, `model` and `clock` depend on nothing. The logic plugins are registered in this order: `time`, `lifecycle`, `model`, `clock`, `flow`; the screen set `screen` follows as `world`, `renderer`, `input`, `assets`, `scenes`, `anim`, `i18n`, `text`, `ui`; a game that wants sound appends `audioPlugin`, and one that wants particles and filters appends `effectsPlugin`. A game without `effectsPlugin` carries none of its systems, filters or WGSL in its bundle. Without a document the screen plugins are inert: the same app starts in plain Bun, Yoga included.
308
+ An arrow means "depends on"; from `renderer` on, most edges to `time` and `clock` and most edges a nearer plugin already implies are left out for space, the plugin READMEs list them in full. `time`, `model` and `clock` depend on nothing. The logic plugins are registered in this order: `time`, `lifecycle`, `model`, `clock`, `flow`; the screen set `screen` follows as `world`, `renderer`, `input`, `assets`, `scenes`, `anim`, `i18n`, `text`, `ui`; a game that wants particles and filters appends `effectsPlugin`, one that wants sound appends `audioPlugin`, and one in a native shell appends `platformPlugin` last. A game without `effectsPlugin` carries none of its systems, filters or WGSL in its bundle. Without a document the screen plugins are inert: the same app starts in plain Bun, Yoga included.
304
309
 
305
310
  ### Planned
306
311
 
307
- Not built. Names are reserved: `defineFeature` refuses them as feature names. Scope and tiers come from the plan and may change.
308
-
309
- | Plugin | Milestone | Tier | Depends on | Will own |
310
- |---|---|---|---|---|
311
- | `platform` | V6 | Standard | `lifecycle`, `flow` | The native provider: background, system dialogs |
312
+ None at the moment: `platform`, the last planned plugin, is built. `defineFeature` refuses every engine plugin name as a feature name.
312
313
 
313
314
  ### Root exports
314
315
 
@@ -330,9 +331,10 @@ Not built. Names are reserved: `defineFeature` refuses them as feature names. Sc
330
331
  | `Text`, `label`, `defineTextStyles` | component and functions | Words on the screen and the text styles a feature registers |
331
332
  | `defineComponent`, `popup`, `defineStyle`, `defineTokens`, `resolve`, `Box`, `LocalWrite` | functions and components | Interface components, the popup effect, the style vocabulary, the rect of an element |
332
333
  | `music` | function | `music(key \| null, { fadeMs? })`, the awaited effect that switches the music track |
333
- | `defineEmitter`, `Emitter`, `defineFilter`, `Glow`, `Outline`, `Blur`, `ColorMatrix`, `Noise`, `Displacement`, `Alpha` | functions and components | Effects as data, drawn by `effectsPlugin`. `defineEmitter(id, config)` describes a particle effect that a feature lists under `emitters`; `Emitter({ effect, active })` runs it on its entity. `defineFilter(id, { wgsl, uniforms, passes?, padding? })` returns a filter component type that a feature lists under `filters`; a filter on a ui element goes in its `components` prop |
334
- | `timePlugin`, `lifecyclePlugin`, `modelPlugin`, `clockPlugin`, `flowPlugin`, `worldPlugin`, `rendererPlugin`, `inputPlugin`, `assetsPlugin`, `scenesPlugin`, `animPlugin`, `i18nPlugin`, `textPlugin`, `uiPlugin`, `audioPlugin`, `effectsPlugin` | plugin instances | For `depends` and `ctx.require` in game plugins; `screen` is the list of the nine screen plugins |
335
- | `Time`, `Lifecycle`, `Model`, `Clock`, `Flow`, `World`, `Renderer`, `Input`, `Assets`, `Scenes`, `Anim`, `I18n`, `TextTypes`, `Ui`, `Audio`, `Effects` | type namespaces | All public types of one plugin |
334
+ | `defineEmitter`, `Emitter`, `defineFilter`, `Glow`, `Outline`, `Blur`, `ColorMatrix`, `Noise`, `Displacement`, `Alpha` | functions and components | Effects as data, drawn by `effectsPlugin`. `defineEmitter(id, config)` describes a particle effect that a feature lists under `emitters`; `Emitter({ effect, active })` runs it on its entity. `defineFilter(id, { wgsl, glsl, uniforms, passes?, padding? })` returns a filter component type that a feature lists under `filters`; a filter on a ui element goes in its `components` prop |
335
+ | `PlatformProvider`, `BackResult`, `HapticKind`, `HAPTIC_KINDS` | types and a constant | The seam a game fills for `platformPlugin`, what one Back press ends in, and the seven haptic kinds as a type and as a frozen list |
336
+ | `timePlugin`, `lifecyclePlugin`, `modelPlugin`, `clockPlugin`, `flowPlugin`, `worldPlugin`, `rendererPlugin`, `inputPlugin`, `assetsPlugin`, `scenesPlugin`, `animPlugin`, `i18nPlugin`, `textPlugin`, `uiPlugin`, `audioPlugin`, `effectsPlugin`, `platformPlugin` | plugin instances | For `depends` and `ctx.require` in game plugins; `screen` is the list of the nine screen plugins |
337
+ | `Time`, `Lifecycle`, `Model`, `Clock`, `Flow`, `World`, `Renderer`, `Input`, `Assets`, `Scenes`, `Anim`, `I18n`, `TextTypes`, `Ui`, `Audio`, `Effects`, `Platform` | type namespaces | All public types of one plugin |
336
338
 
337
339
  ### Other entries
338
340
 
@@ -356,9 +358,9 @@ Not built. Names are reserved: `defineFeature` refuses them as feature names. Sc
356
358
  | `fakeClock` | `(start = 0) => FakeClock` | A `ClockSource` with `advance(ms)` and `set(moment)` |
357
359
  | `memory` | `(fixture?: { state: SaveDoc; version: number }) => PlayerStateProvider & { calls: ProviderCall[] }` | In-memory save provider. It keeps what it was committed and records every call |
358
360
  | `saveOf` | `(player: Json, seed?: number) => SaveDoc` | Builds a save document for a fixture |
359
- | `defineVisualTest` | `(name: string, test: { start: VisualStart; steps: readonly VisualStep[] }) => VisualTest` | A visual test as frozen data: where it starts and its steps |
361
+ | `defineVisualTest` | `(name: string, test: { start: VisualStart; steps: readonly VisualStep[]; webgl?: boolean }) => VisualTest` | A visual test as frozen data: where it starts, its steps, and whether it also runs in the WebGL leg |
360
362
  | `runVisualTests` | `(setup: VisualSetup, tests: readonly VisualTest[], options?: VisualOptions) => Promise<VisualReport>` | Plays the tests and compares every checkpoint with its baseline files |
361
- | `parseVisualArgv` | `(argv: readonly string[]) => { update?; pixels?; only?; dir? }` | Reads `--update`, `--no-pixels`, `--only <name>` and `--dir <path>` from a command line |
363
+ | `parseVisualArgv` | `(argv: readonly string[]) => { update?; pixels?; only?; dir?; renderer? }` | Reads `--update`, `--no-pixels`, `--webgl`, `--only <name>` and `--dir <path>` from a command line. `--webgl` runs only the tests with `webgl: true`, on the page with `?renderer=webgl`: such a test writes its picture to `screen.webgl.webp` and shares `state.json` and `describe.json` with the WebGPU leg |
362
364
 
363
365
  A `HeadlessGame` has `walk(route)`, `answer(answer)`, `state()`, `history()` and `stop()`.
364
366
 
@@ -384,14 +386,14 @@ const report = await runVisualTests({ app: () => createScreenGame().app, page: {
384
386
  process.exitCode = report.ok ? 0 : 1;
385
387
  ```
386
388
 
387
- A checkpoint settles the motions and saves three baseline files next to the test: `<dir>/<test>/<checkpoint>/state.json`, `describe.json` and `screen.webp`. A missing file is written; `--update` rewrites them; any other file is compared. The headless leg plays every test in plain Bun and compares `state.json` and `describe.json` exactly, so it runs in `bun run test`. The pixel leg plays the same steps on the dev page in Chrome with WebGPU, on a Mac only, and compares `screen.webp` with a tolerance; a pixel difference with the same state and describe is reported as a rendering regression. The page is the contract: a dev build that sets `globalThis.game` to the app and `globalThis.doors` to `{ read, watch, sources, run, commands }`. No CI job runs pixels.
389
+ A checkpoint settles the motions and saves three baseline files next to the test: `<dir>/<test>/<checkpoint>/state.json`, `describe.json` and `screen.webp`. A missing file is written; `--update` rewrites them; any other file is compared. The headless leg plays every test in plain Bun and compares `state.json` and `describe.json` exactly, so it runs in `bun run test`. The pixel leg plays the same steps on the dev page in Chrome with WebGPU, or with WebGL under `--webgl`, on a Mac only, and compares `screen.webp` with a tolerance; a pixel difference with the same state and describe is reported as a rendering regression. The page is the contract: a dev build that sets `globalThis.game` to the app and `globalThis.doors` to `{ read, watch, sources, run, commands }`. No CI job runs pixels.
388
390
 
389
391
  #### Pixels
390
392
 
391
393
  The pixel leg runs on a Mac only. `pixels` is on by default when the setup has a `page` and the platform is `darwin`, and off everywhere else. No CI job runs it: a Linux runner has no WebGPU worth trusting.
392
394
 
393
395
  - **`playwright-core`** is an optional peer dependency. The leg loads it with a dynamic import, so a game that never runs pixels never installs it. Add it with `bun add -d playwright-core`. Without it the run stops with "The pixel leg needs playwright-core"; `--no-pixels` runs the headless leg alone.
394
- - **The browser.** `page.browser` when the setup names one (`{ channel }` or `{ executablePath }`). Otherwise Playwright's own Chromium, whose version `playwright-core` pins, so a Chrome update never moves the baselines: install it with `bunx playwright-core install chromium`. Otherwise the system Google Chrome. Chrome starts with WebGPU on Metal; a page that draws with anything else fails the leg.
396
+ - **The browser.** `page.browser` when the setup names one (`{ channel }` or `{ executablePath }`). Otherwise Playwright's own Chromium, whose version `playwright-core` pins, so a Chrome update never moves the baselines: install it with `bunx playwright-core install chromium`. Otherwise the system Google Chrome. Chrome starts with WebGPU on Metal; a page that draws with a renderer other than the requested one fails the leg.
395
397
  - **The page.** 390 CSS px wide at a device scale of 2, as tall as the game's inert frame: 520 for a 4:3 portrait game, so `screen.webp` is 780 × 1040.
396
398
  - **Game time.** The leg drives the clock itself, so two runs draw the same pixels: a dash phase, a pulse or a particle reads the same time. It waits until the page has loaded what it asked for (no request for half a second), runs `pause`, then steps the game to 5 s of game time, the last frame shorter so it lands exactly. From there every frame of the restore, of every settle and of a checkpoint is a `step` of 1000/60 ms, the frame of the headless leg; the browser's own frames move no game time. The picture is the stage after the last stepped frame. A `resume` step hands the clock back to the browser.
397
399
  - **The picture.** Lossless WebP: the page encodes it with `convertToBlob({ type: "image/webp", quality: 1 })`, which Chrome encodes without loss, so the baseline holds the captured pixels exactly.
@@ -722,7 +724,7 @@ Global events are empty: every event belongs to a plugin. `time` and `clock` emi
722
724
  | `anim:finished` | `anim` | `{ animation }` | A timeline ended or was finished. Never on `cancel()` |
723
725
  | `i18n:locale-changed` | `i18n` | `{ locale }` | The module of the new locale is loaded; `text` re-resolves. Never at start |
724
726
 
725
- `text`, `ui` and `audio` emit nothing.
727
+ `text`, `ui`, `audio`, `effects` and `platform` emit nothing; the `effects` budgets are log warnings.
726
728
 
727
729
  ```ts
728
730
  import { createPlugin, flowPlugin } from "@moku-labs/game";
@@ -801,6 +803,8 @@ Set with `createApp({ pluginConfigs: { <plugin>: { ... } } })`.
801
803
  | `effects` | `maxPasses` | `number` | `24` | Render passes per frame above which `effects:pass-budget` warns once per crossing |
802
804
  | `effects` | `phone` | `boolean \| "auto"` | `"auto"` | Whether the device is a phone. `"auto"`: a coarse pointer and a short side of at most 820 CSS px, read once at start |
803
805
  | `effects` | `blur` | `{ quality, phoneResolution }` | `{ quality: 2, phoneResolution: 0.5 }` | What a `Blur` with `quality: 0` and `resolution: 0` resolves to |
806
+ | `platform` | `provider` | `PlatformProvider \| undefined` | `undefined` | The provider the game builds, usually from `@moku-labs/system`. Absent: the plugin is inert and `back()` answers `"none"` |
807
+ | `platform` | `keepAwake` | `boolean` | `false` | Keep the screen on while the game runs; released while it is paused and on stop |
804
808
 
805
809
  ## Development
806
810
 
@@ -818,7 +822,8 @@ bun run test:integration # vitest project "integration"
818
822
  bun run test:coverage # both projects with coverage, 90% thresholds
819
823
  bun run validate # publint and attw with the esm-only profile
820
824
  bun run fixture:pack # pack the fixture game into tests/integration/merge-game/dist/assets
821
- bun run fixture:visual # the fixture's visual tests, both legs: --url <dev page>, --update, --only <name>, --no-pixels
825
+ bun run fixture:visual # the fixture's visual tests, both legs: --url <dev page>, --update, --only <name>, --no-pixels, --webgl
826
+ bun run fixture:native # the fixture as a native app on @moku-labs/native: <target> [--simulator] [--page <html>]
822
827
  bun run release:setup # moku-release setup
823
828
  bun run release:doctor # moku-release doctor
824
829
  bun run release # moku-release
@@ -838,7 +843,7 @@ bun run release # moku-release
838
843
 
839
844
  Plugin tests never go into the root `tests/` folder. Coverage thresholds are 90% for lines, functions, branches and statements.
840
845
 
841
- ### Lint rules L1 to L9
846
+ ### Lint rules
842
847
 
843
848
  The project rules live in [`eslint.config.ts`](./eslint.config.ts).
844
849
 
@@ -853,6 +858,7 @@ The project rules live in [`eslint.config.ts`](./eslint.config.ts).
853
858
  | L7 | The public contract carries the docs: every member of a `…Api` type in `types.ts` has JSDoc and a scenario `@example` (when it is called, literal arguments, the result). A member another plugin calls is shown from that plugin's point of view; there is no private tier and no exemption. The implementation of an API method has no JSDoc. Elsewhere an example is allowed, never required | `src/plugins/**/types.ts` |
854
859
  | L8 | No signature echo: an `@example` whose whole body is one call with bare identifiers is an error | `src/**` |
855
860
  | L9 | The JSX runtime module is reached only through `src/jsx-runtime.ts` and `src/jsx-dev-runtime.ts`, and those two import nothing else | `src/**` outside `ui` |
861
+ | L13 | No import of `@moku-labs/system`, `@moku-labs/native` or `@tauri-apps/*`, type imports included. The game builds the `PlatformProvider` in its own layer | every file under `src/`, tests included |
856
862
 
857
863
  ## Requirements
858
864
 
@@ -875,6 +881,8 @@ The project rules live in [`eslint.config.ts`](./eslint.config.ts).
875
881
  - [`text`](./src/plugins/text/README.md): styles, tags, measurement, fonts
876
882
  - [`ui`](./src/plugins/ui/README.md): the JSX runtime, components, styles, layout, popups
877
883
  - [`audio`](./src/plugins/audio/README.md): buses, the unlock, pause, memory
884
+ - [`effects`](./src/plugins/effects/README.md): particles, filters, WGSL and GLSL twins, budgets
885
+ - [`platform`](./src/plugins/platform/README.md): the provider seam, the Back chain, haptics, keep-awake
878
886
  - [`llms.txt`](./llms.txt): overview for an LLM that writes a game on this engine
879
887
  - [Moku Core specification](https://github.com/moku-labs/core/tree/main/specification)
880
888
 
package/dist/assets.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { l as Manifest } from "./types-DgwbzDLP.mjs";
1
+ import { l as Manifest } from "./types-C47fKNcn.mjs";
2
2
 
3
3
  //#region src/plugins/assets/scan/cli.d.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { Tt as Time, Ut as Api, ft as FlowState, lt as Bookmark, t as Api$1, u as DebugSwitches } from "./types-Cjf3ywai.mjs";
2
- import { d as HeadlessApp, n as Command, r as ControlApp } from "./types-D0uYTTC3.mjs";
3
- import { H as InputApi } from "./types-C0SLQsx0.mjs";
4
- import { b as UiApi, t as AnimApi } from "./types-KlE_GL1z.mjs";
1
+ import { Tt as Time, Ut as Api, ft as FlowState, lt as Bookmark, t as Api$1, u as DebugSwitches } from "./types-BZ1OLf24.mjs";
2
+ import { d as HeadlessApp, n as Command, r as ControlApp } from "./types-DmhTHRVW.mjs";
3
+ import { H as InputApi } from "./types-X9jXjka3.mjs";
4
+ import { A as UiApi, t as AnimApi } from "./types-CqkrEY06.mjs";
5
5
 
6
6
  //#region src/plugins/flow/doors/commands.d.ts
7
7
  /**
@@ -1,6 +1,6 @@
1
- import { d as HeadlessApp, i as InputArguments, n as Command, o as InputSchema, s as Ran } from "./types-D0uYTTC3.mjs";
2
- import { t as commands } from "./commands-BOxqlv7w.mjs";
3
- import { t as defineCommand } from "./define-BMo0pHe_.mjs";
1
+ import { d as HeadlessApp, i as InputArguments, n as Command, o as InputSchema, s as Ran } from "./types-DmhTHRVW.mjs";
2
+ import { t as commands } from "./commands-C0BljsM5.mjs";
3
+ import { t as defineCommand } from "./define-CjP_4H0G.mjs";
4
4
 
5
5
  //#region src/plugins/flow/doors/dev.d.ts
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { c as Source, d as HeadlessApp, n as Command, o as InputSchema } from "./types-D0uYTTC3.mjs";
1
+ import { c as Source, d as HeadlessApp, n as Command, o as InputSchema } from "./types-DmhTHRVW.mjs";
2
2
 
3
3
  //#region src/plugins/flow/doors/define.d.ts
4
4
  /**