@moku-labs/game 0.0.2 → 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 +107 -31
- package/dist/assets.d.mts +58 -6
- package/dist/assets.mjs +1630 -234
- package/dist/commands-C0BljsM5.d.mts +99 -0
- package/dist/control.d.mts +3 -92
- package/dist/control.mjs +3 -757
- package/dist/define-CjP_4H0G.d.mts +51 -0
- package/dist/{registry-DlpRCibU.mjs → dev-CcwFrwok.mjs} +30 -1
- package/dist/index.d.mts +362 -303
- package/dist/index.mjs +12717 -6654
- package/dist/inspect.d.mts +10 -8
- package/dist/inspect.mjs +2 -518
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-8H4AJuhK.mjs} +1 -0
- package/dist/run-tlcn1w7y.mjs +944 -0
- package/dist/{runtime-DiOTkDZz.d.mts → runtime-Bn5TXNxP.d.mts} +1 -1
- package/dist/sources-BooL2MsX.mjs +545 -0
- package/dist/testing.d.mts +328 -3
- package/dist/testing.mjs +2126 -2
- package/dist/types-BZ1OLf24.d.mts +5999 -0
- package/dist/{types-DWILGrPn.d.mts → types-C47fKNcn.d.mts} +93 -20
- package/dist/types-CqkrEY06.d.mts +1734 -0
- package/dist/types-CxZy7Pi4.d.mts +1210 -0
- package/dist/{define-sFoO3y6X.d.mts → types-DmhTHRVW.d.mts} +180 -51
- package/dist/{types-yg_ywtT-.d.mts → types-X9jXjka3.d.mts} +230 -31
- package/dist/{visual-BDUHSRvf.mjs → visual-NPrDkvUk.mjs} +19 -13
- package/package.json +22 -2
- package/dist/headless-CvamCUcR.mjs +0 -186
- package/dist/headless-KaSWcd0s.d.mts +0 -181
- package/dist/types-BxkNNYul.d.mts +0 -2840
- package/dist/types-CTPS9GBu.d.mts +0 -742
- package/dist/types-DD-QrG_z.d.mts +0 -588
- package/dist/types-DYLnSgMI.d.mts +0 -570
- package/dist/types-JNc_UQBo.d.mts +0 -2896
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.
|
|
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
|
-
[](#status)
|
|
10
10
|
[](#requirements)
|
|
11
11
|
[](#requirements)
|
|
12
12
|
[](#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.
|
|
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
|
|---|---|---|---|
|
|
@@ -38,8 +38,8 @@ V1, V2 and V3 are built. V4 has its engine side: the two doors described in [Doo
|
|
|
38
38
|
| V2 | built | `world`, `renderer`, `input`, `assets`, `scenes`, the `@moku-labs/game/assets` entry | A board is visible and items merge by drag; the same game still plays to the end headless |
|
|
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
|
-
| V5 |
|
|
42
|
-
| V6 |
|
|
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 | 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.
|
|
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`
|
|
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
|
|
|
@@ -237,15 +237,17 @@ Five logic plugins are on every app; the nine screen plugins are the list `scree
|
|
|
237
237
|
| [`clock`](./src/plugins/clock/README.md) | Standard | Trusted time as an input: monotonic `now()` and one `elapsed` signal at the next due moment | `now()`, `scheduleAt(moment)`, `onElapsed(listener)`, `poke()`, `dueAt()` |
|
|
238
238
|
| [`flow`](./src/plugins/flow/README.md) | Very Complex | The graph: runner, gate, inbox, effects gateway, features registry | `run()`, `onEnter(stage, callback)`, `walk(route, options?)`, `bookmark()`, `restore(bookmark)`, `describe()`, `state()`, `history()`, `setMode(mode)`, `gate.answer(answer)`, `gate.pointer(active)`, `gate.state()`, `inbox.post(event)`, `fx.handle(kind, handler, options?)`, `fx.dispatch(descriptor)`, `features.register(name, description)`, `features.all()`, `features.contributions(slotName)` |
|
|
239
239
|
| [`world`](./src/plugins/world/README.md) | Very Complex | A zero-dependency ECS (`ecs`) and the projection from committed state to entities (`projection`): keyed reconcile of one `view(item)` function, retarget motions, a despawn queue, named layers | `ecs.spawn(owner, components)`, `ecs.query(...Components)`, `ecs.system(def)`, `ecs.set(entity, Component, patch)`, `ecs.changed(Component)`, `ecs.snapshot()`, `ecs.mode()`, `projection.mount(names, owner)`, `projection.setLayers(list)`, `projection.settle(entity)`, `projection.keyOf(entity)`, `projection.entityOf(projection, key)` |
|
|
240
|
-
| [`renderer`](./src/plugins/renderer/README.md) | Very Complex | The Pixi v8 host loaded lazily (`host`), one `sync` system that owns every display object, the reference viewport fitted to `referenceSide` and `referenceLong` (`viewport`) | `host.ready()`, `host.kind()`, `host.canvas()`, `sync.hitTest(x, y, accept)`, `sync.textures.provide(fn)`, `sync.displayOf(entity)`, `viewport.toReference(x, y)`, `viewport.size()` |
|
|
240
|
+
| [`renderer`](./src/plugins/renderer/README.md) | Very Complex | The Pixi v8 host loaded lazily (`host`), one `sync` system that owns every display object, the reference viewport fitted to `referenceSide` and `referenceLong` (`viewport`), the frame numbers: render passes in every build, draw calls in a dev build | `host.ready()`, `host.kind()`, `host.canvas()`, `host.device()`, `sync.hitTest(x, y, accept)`, `sync.textures.provide(fn)`, `sync.filters.set(entity, slots)`, `sync.displayOf(entity)`, `viewport.toReference(x, y)`, `viewport.size()`, `stats()` |
|
|
241
241
|
| [`input`](./src/plugins/input/README.md) | Standard | Gestures as data components: `Tappable`, `Pressable`, `Draggable`, `DropTarget`, `Swipeable`; the drop target names the intent that reaches `flow.gate` | `tap(target)`, `press(target)`, `drag(from, to)`, `swipe(target, direction)` |
|
|
242
242
|
| [`assets`](./src/plugins/assets/README.md) | Complex | The manifest, five load tiers, graph-driven preload, a texture budget with LRU unload; typed keys from the `assets` entry | `load(bundle)`, `unload(bundle)`, `isLoaded(bundle)`, `texture(key)`, `usage()` |
|
|
243
243
|
| [`scenes`](./src/plugins/scenes/README.md) | Standard | A scene as a declaration: bundle, layers, projections, `music`. A node names its scene; the runner switches through `flow.onEnter` | `current()` |
|
|
244
|
-
| [`anim`](./src/plugins/anim/README.md) | Complex | The one tween core, installed into `world.projection` as the `TweenDriver`; timelines as frozen data built from typed slots; `defineMotion` sugar for the enter, exit and change hooks | `play(animation, slots)`, `finishAll()`, `active()`, `onMark(fn)` |
|
|
244
|
+
| [`anim`](./src/plugins/anim/README.md) | Complex | The one tween core, installed into `world.projection` as the `TweenDriver`; timelines as frozen data built from typed slots; `defineMotion` sugar for the enter, exit and change hooks; the `Frames` component, a frame loop that walks a sprite's textures for its whole life | `play(animation, slots)`, `finishAll()`, `active()`, `onMark(fn)` |
|
|
245
245
|
| [`i18n`](./src/plugins/i18n/README.md) | Complex | Strings as data: `tr(key, params)` is a `Message`, ICU MessageFormat compiled to plain functions by `compileStrings` on the `assets` door, `Part[]` at run time, never a joined string | `locale()`, `setLocale(locale)`, `format(message, locale?)`, `plain(message)`, `has(key)`, `locales()` |
|
|
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
|
-
| [`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` | `tree()`, `find(key)`, `lint()` |
|
|
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 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()` |
|
|
249
251
|
|
|
250
252
|
```mermaid
|
|
251
253
|
flowchart LR
|
|
@@ -285,6 +287,14 @@ flowchart LR
|
|
|
285
287
|
AU --> F
|
|
286
288
|
AU --> A
|
|
287
289
|
AU --> S
|
|
290
|
+
EF["effects"] --> F
|
|
291
|
+
EF --> W
|
|
292
|
+
EF --> R
|
|
293
|
+
EF --> A
|
|
294
|
+
EF --> AN
|
|
295
|
+
PL["platform"] --> L
|
|
296
|
+
PL --> F
|
|
297
|
+
PL --> I
|
|
288
298
|
classDef u fill:#0b7285,stroke:#08525f,color:#fff;
|
|
289
299
|
classDef m fill:#1864ab,stroke:#0d3d6e,color:#fff;
|
|
290
300
|
classDef s fill:#5c940d,stroke:#3d6208,color:#fff;
|
|
@@ -292,19 +302,14 @@ flowchart LR
|
|
|
292
302
|
class G u
|
|
293
303
|
class F,T,L,M,C m
|
|
294
304
|
class W,R,I,A,S s
|
|
295
|
-
class AN,N,X,U,AU v
|
|
305
|
+
class AN,N,X,U,AU,EF,PL v
|
|
296
306
|
```
|
|
297
307
|
|
|
298
|
-
An arrow means "depends on";
|
|
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.
|
|
299
309
|
|
|
300
310
|
### Planned
|
|
301
311
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
| Plugin | Milestone | Tier | Depends on | Will own |
|
|
305
|
-
|---|---|---|---|---|
|
|
306
|
-
| `effects` | V5 | Complex | `flow`, `world`, `renderer` | Particles, filters, frames |
|
|
307
|
-
| `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.
|
|
308
313
|
|
|
309
314
|
### Root exports
|
|
310
315
|
|
|
@@ -312,8 +317,8 @@ Not built. Names are reserved: `defineFeature` refuses them as feature names. Sc
|
|
|
312
317
|
|---|---|---|
|
|
313
318
|
| `createApp` | function | Creates a game application |
|
|
314
319
|
| `createPlugin` | function | Creates a game plugin bound to the engine's config and events |
|
|
315
|
-
| `defineGame` | function | Returns the authoring helpers typed with the game's `player`, `session`, `assets`, `bundles`, `strings` and `
|
|
316
|
-
| `defineFeature` | function | Turns a feature description into a plugin. V3 keys: `projections`, `animations`, `ui`, `strings`, `textStyles` |
|
|
320
|
+
| `defineGame` | function | Returns the authoring helpers typed with the game's `player`, `session`, `assets`, `bundles`, `strings`, `textStyles` and `emitters`: `defineNode`, `defineFlow`, `defineFeature`, `projection`, `sprite`, `Sprite`, `NineSlice`, `defineBundles`, `load`, `defineScene`, from V3 `tr`, `label`, `defineTextStyles`, `defineComponent`, `defineStyle`, `defineTokens`, `popup`, `defineAnimation`, `frames`, `sfx`, `play`, `music`, and from V5 `Frames`, `defineEmitter`, `Emitter`, `Displacement`: a texture key the game does not have or an effect id it did not declare does not compile |
|
|
321
|
+
| `defineFeature` | function | Turns a feature description into a plugin. V3 keys: `projections`, `animations`, `ui`, `strings`, `textStyles`. V5 keys: `emitters`, `filters` |
|
|
317
322
|
| `type`, `exit`, `to`, `slot` | functions | Type tag of a payload, and the three graph helpers for edge targets and slots |
|
|
318
323
|
| `schedule`, `guide`, `hint` | functions | Effect descriptors: next due moment, tutorial narrowing of the gate, cosmetic hint |
|
|
319
324
|
| `SaveUnreadableError` | class | Thrown by `model.store.load()` when the save cannot be read |
|
|
@@ -321,27 +326,29 @@ Not built. Names are reserved: `defineFeature` refuses them as feature names. Sc
|
|
|
321
326
|
| `Transform`, `Sprite`, `NineSlice`, `Shape`, `Parent`, `Display`, `sprite` | components | The display components of `renderer` |
|
|
322
327
|
| `Tappable`, `Pressable`, `Draggable`, `DropTarget`, `Swipeable`, `Touchable`, `Held`, `Hovered`, `PointerOver`, `Pressed`, `Pointer` | components | Gestures as data, from `input`; `PointerOver` marks the view under an idle mouse or pen (the `hover` style state) |
|
|
323
328
|
| `defineBundles`, `load`, `defineScene` | functions | Bundle and scene declarations |
|
|
324
|
-
| `defineAnimation`, `sequence`, `parallel`, `stagger`, `tween`, `set`, `wait`, `mark`, `frames`, `sfx`, `haptic`, `use`, `spawn`, `spawned`, `play`, `external`, `defineMotion`, `Animation` | functions and
|
|
329
|
+
| `defineAnimation`, `sequence`, `parallel`, `stagger`, `tween`, `set`, `wait`, `mark`, `frames`, `sfx`, `haptic`, `use`, `spawn`, `spawned`, `play`, `external`, `defineMotion`, `Animation`, `Frames` | functions and components | Choreography as frozen data. `play(animation, slots)` is the effect a node awaits; `spawn` makes a temporary entity (flying coins, a toast sign) that the timeline despawns when it ends; `sfx` and `haptic` are descriptors `audio` and `platform` handle; `external` throws until Spine arrives. `Frames({ keys, fps, loop, playing })` walks a sprite through its textures for the entity's whole life, a spinning coin |
|
|
325
330
|
| `tr` | function | `tr(key, params?)` builds a frozen `Message`; no locale is read at the call site |
|
|
326
331
|
| `Text`, `label`, `defineTextStyles` | component and functions | Words on the screen and the text styles a feature registers |
|
|
327
332
|
| `defineComponent`, `popup`, `defineStyle`, `defineTokens`, `resolve`, `Box`, `LocalWrite` | functions and components | Interface components, the popup effect, the style vocabulary, the rect of an element |
|
|
328
333
|
| `music` | function | `music(key \| null, { fadeMs? })`, the awaited effect that switches the music track |
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
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 |
|
|
331
338
|
|
|
332
339
|
### Other entries
|
|
333
340
|
|
|
334
341
|
| Entry | Runs in | Exports |
|
|
335
342
|
|---|---|---|
|
|
336
|
-
| `@moku-labs/game/testing` |
|
|
337
|
-
| `@moku-labs/game/assets` | node and bun only | `scanAssets`, `emitKeys`, `emitManifest`, `compileStrings`, `checkStrings`, `runCli`. A game runs it as `bun run assets:keys`: it writes the manifest, the typed asset keys and, next to them, `generated/strings.ts` with one `strings.<locale>.ts` per locale; `--check` fails when any of them is out of date |
|
|
343
|
+
| `@moku-labs/game/testing` | node and bun only | The headless helpers and the visual tests, see below. The visual runner reads and writes baseline files |
|
|
344
|
+
| `@moku-labs/game/assets` | node and bun only | `scanAssets`, `emitKeys`, `emitManifest`, `compileStrings`, `checkStrings`, `packAssets`, `runCli`. A game runs it as `bun run assets:keys`: it writes the manifest, the typed asset keys and, next to them, `generated/strings.ts` with one `strings.<locale>.ts` per locale; `--check` fails when any of them is out of date; `--pack <dir>` writes the production build: WebP atlas pages, content-hashed names and a v2 manifest |
|
|
338
345
|
| `@moku-labs/game/inspect` | anywhere, production included | `read`, `watch`, `defineSource`, the catalogue `sources` and the types `Source`, `InputSchema`, `InputOf`. See [Doors for the editor](#doors-for-the-editor) |
|
|
339
346
|
| `@moku-labs/game/control` | dev builds only | `run`, `defineCommand`, `controlRefused`, the catalogue `commands` and the types `Command`, `Ran`. See [Doors for the editor](#doors-for-the-editor) |
|
|
340
347
|
| `@moku-labs/game/jsx-runtime`, `@moku-labs/game/jsx-dev-runtime` | anywhere | `jsx`, `jsxs`, `jsxDEV`, `Fragment` and the `JSX` namespace that `"jsxImportSource": "@moku-labs/game"` resolves to. A game never imports them by hand |
|
|
341
348
|
|
|
342
349
|
### Testing entry
|
|
343
350
|
|
|
344
|
-
`@moku-labs/game/testing` re-exports the headless helpers.
|
|
351
|
+
`@moku-labs/game/testing` re-exports the headless helpers and the visual tests.
|
|
345
352
|
|
|
346
353
|
| Export | Signature | Purpose |
|
|
347
354
|
|---|---|---|
|
|
@@ -351,9 +358,64 @@ Not built. Names are reserved: `defineFeature` refuses them as feature names. Sc
|
|
|
351
358
|
| `fakeClock` | `(start = 0) => FakeClock` | A `ClockSource` with `advance(ms)` and `set(moment)` |
|
|
352
359
|
| `memory` | `(fixture?: { state: SaveDoc; version: number }) => PlayerStateProvider & { calls: ProviderCall[] }` | In-memory save provider. It keeps what it was committed and records every call |
|
|
353
360
|
| `saveOf` | `(player: Json, seed?: number) => SaveDoc` | Builds a save document for a fixture |
|
|
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 |
|
|
362
|
+
| `runVisualTests` | `(setup: VisualSetup, tests: readonly VisualTest[], options?: VisualOptions) => Promise<VisualReport>` | Plays the tests and compares every checkpoint with its baseline files |
|
|
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 |
|
|
354
364
|
|
|
355
365
|
A `HeadlessGame` has `walk(route)`, `answer(answer)`, `state()`, `history()` and `stop()`.
|
|
356
366
|
|
|
367
|
+
### Visual tests
|
|
368
|
+
|
|
369
|
+
A visual test is data: where the game starts, then steps. A step is a `/control` command by its short name (`answer`, `tap`, `drag`, `key`, `fill`, `walk`, `restore`, `step`, `pause`, `resume`, `reducedMotion`) with that command's input, or a checkpoint.
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
// tests/visual/reward-popup.visual.ts
|
|
373
|
+
export const rewardPopup = defineVisualTest("reward-popup", {
|
|
374
|
+
start: { player: fixtures.ready, checkpoint: "home" },
|
|
375
|
+
steps: [
|
|
376
|
+
{ tap: { key: "play" } },
|
|
377
|
+
{ answer: { intent: "deliver", payload: { orderId: "o1" } } },
|
|
378
|
+
{ checkpoint: "open" },
|
|
379
|
+
{ tap: { key: "claim" } },
|
|
380
|
+
{ checkpoint: "claimed" }
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// tests/visual/run.ts, `bun tests/visual/run.ts --update`
|
|
385
|
+
const report = await runVisualTests({ app: () => createScreenGame().app, page: { url: "http://localhost:3000/" } }, [rewardPopup]);
|
|
386
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
387
|
+
```
|
|
388
|
+
|
|
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.
|
|
390
|
+
|
|
391
|
+
#### Pixels
|
|
392
|
+
|
|
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.
|
|
394
|
+
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
400
|
+
- **`--update`** rewrites `state.json`, `describe.json` and `screen.webp` of every test it runs. A pixel difference writes `screen.actual.webp` and `screen.diff.webp` beside the baseline, red where a pixel differs. Git ignores both.
|
|
401
|
+
- **Tolerance.** A pixel differs when one of its channels moves by more than 24. A checkpoint differs when more than 0.1% of its pixels do. Both are `tolerance` in the options.
|
|
402
|
+
|
|
403
|
+
The fixture game keeps its visual tests in `tests/visual/`: one `*.visual.ts` per test, the list in `tests.ts`, the script in `run.ts` and the baselines next to them. The script imports the runner from `src/testing`, so a stale `dist/` never writes baselines. `bun run test` runs the headless leg through `tests/integration/visual-headless.test.ts`. Both legs run against the dev page:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
# terminal 1: the dev page of the fixture on a free port
|
|
407
|
+
cd tests/integration/merge-game && bun ./web/serve.ts --port 4173
|
|
408
|
+
|
|
409
|
+
# terminal 2: from the root of the repository
|
|
410
|
+
bun run fixture:visual --url http://localhost:4173/ # compare with the baselines
|
|
411
|
+
bun run fixture:visual --url http://localhost:4173/ --update # write the baselines again
|
|
412
|
+
bun run fixture:visual --only gift-popup --no-pixels # one test, headless only
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Without `--url` the page is `http://localhost:3000/`. The exit code is 1 when a checkpoint differs.
|
|
416
|
+
|
|
417
|
+
The test of the browser leg itself opens a real Chrome only on request: `MOKU_VISUAL_BROWSER=1 bunx vitest run tests/unit/visual/browser.test.ts`. It also proves the WebP is lossless: random opaque pixels come back byte for byte. Without the variable it skips with its reason, so `bun run test` opens no browser.
|
|
418
|
+
|
|
357
419
|
## Interface in JSX
|
|
358
420
|
|
|
359
421
|
The interface is one more projection. A screen is a projection whose `view` returns JSX; `ui` reconciles the tree by identity into entities it owns and lays them out with one Yoga solve per change. There is no DOM and no React: the runtime builds plain description nodes, and `"jsxImportSource": "@moku-labs/game"` is the only setup.
|
|
@@ -363,7 +425,7 @@ The interface is one more projection. A screen is a projection whose `view` retu
|
|
|
363
425
|
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@moku-labs/game" } }
|
|
364
426
|
```
|
|
365
427
|
|
|
366
|
-
The tags are `screen`, `layer`, `row`, `column`, `stack`, `spacer`, `panel`, `image`, `icon`, `text`, `button` and `
|
|
428
|
+
The tags are `screen`, `layer`, `row`, `column`, `stack`, `spacer`, `panel`, `image`, `icon`, `text`, `button`, `scroll` and `input`. A `button` either names an `intent` for the gate or writes `local` state of its nearest component, never both. An `input` is a text field: its text lives in the `local` field it names, every keystroke writes it, and Enter answers its `submit` intent with `{ [local]: value }`, the only thing the gate hears: `<input key="nameField" local="name" maxLength={16} submit="save" placeholder={tr("rename.hint")} />`. A `text` takes a string or a `Message` from `tr`; its size comes from a text style key, not from the layout style. Every tag takes `components`, a list of extra component values for the element's entity: `<button key="claim" intent="claim" components={[Glow({ strength: 2 })]} />` glows, and the glow covers the button and its label.
|
|
367
429
|
|
|
368
430
|
```tsx
|
|
369
431
|
// features/hud/view.tsx — the HUD, a reward popup and the choreography they share
|
|
@@ -528,7 +590,8 @@ From `@moku-labs/game/inspect`. `Changes` is when `watch` reads the source again
|
|
|
528
590
|
| `projections` | `game.projections` | none | commit | Projection name to key to entity, through `world.projection.keyOf` |
|
|
529
591
|
| `ui` | `game.ui` | none | frame | `ui.tree()`: the live screen as plain data |
|
|
530
592
|
| `rect` | `game.rect` | `{ key: "string" }` | frame | Where the element with that `key` is on the page, in CSS px; in reference units while the renderer is inert. `undefined` when it is not on screen |
|
|
531
|
-
| `render` | `game.render` | none | frame | `renderer.stats()`: `{ fps, frameMs, textures, textureMb, views, pooled }` |
|
|
593
|
+
| `render` | `game.render` | none | frame | `renderer.stats()`: `{ fps, frameMs, textures, textureMb, views, pooled, renderPasses }`, and `drawCalls` in a dev build. `renderPasses` is 1 for the frame plus, for every view with an enabled filter, 1 for its content and the passes of its filters |
|
|
594
|
+
| `effects` | `game.effects` | none | frame | `effects.stats()`: `{ particles, emitters, filters, renderPasses }`. Only in a game that composes `effectsPlugin` |
|
|
532
595
|
| `sounds` | `game.sounds` | `{ last: "number?" }` | frame | `audio.journal()`: all, or the last `last`. Empty unless `pluginConfigs.audio.journal` is above 0 |
|
|
533
596
|
| `assets` | `game.assets` | none | frame | `assets.usage()`: `{ textureMb, budgetMb, bundles }` |
|
|
534
597
|
| `log` | `game.log` | `{ level: "string?" }` | frame | `log.trace()`: every entry, or the entries at `level` and above. A level other than `debug`, `info`, `warn`, `error` throws |
|
|
@@ -543,6 +606,7 @@ From `@moku-labs/game/control`. Every command runs in dev builds only and leaves
|
|
|
543
606
|
| `tap` | `game.tap` | `{ key: "string?", target: "json?" }`, exactly one | route | `input.tap` on the ui element with that `key`, or on the view `target: { projection, key }`. Value: whether the gate took the answer |
|
|
544
607
|
| `drag` | `game.drag` | `{ from: "json", to: "json" }` | route | `input.drag(from, to)`, both `{ projection, key }`. Value: whether the gate took the answer |
|
|
545
608
|
| `key` | `game.key` | `{ key: "string", shift: "boolean?" }` | route | `input.pressKey(key, { shift })`. Value: whether a listener handled the key |
|
|
609
|
+
| `fill` | `game.fill` | `{ key: "string", value: "string" }` | route | `ui.fill(key, value)`: types into the `input` with that `key`, cut to its `maxLength`. Value: whether the field took the text |
|
|
546
610
|
| `walk` | `game.walk` | `{ route: "json" }` | route | `flow.walk(route)`. Value: the flow state after the walk |
|
|
547
611
|
| `bookmark` | `game.bookmark` | none | read | `flow.bookmark()`. Value: the bookmark, plain JSON |
|
|
548
612
|
| `restore` | `game.restore` | `{ bookmark: "json?", repro: "json?" }`, exactly one | raw | `flow.restore(bookmark)`, or a `/testing` repro: its state at its checkpoint, then `flow.walk(repro.route)`. Value: the flow state |
|
|
@@ -660,7 +724,7 @@ Global events are empty: every event belongs to a plugin. `time` and `clock` emi
|
|
|
660
724
|
| `anim:finished` | `anim` | `{ animation }` | A timeline ended or was finished. Never on `cancel()` |
|
|
661
725
|
| `i18n:locale-changed` | `i18n` | `{ locale }` | The module of the new locale is loaded; `text` re-resolves. Never at start |
|
|
662
726
|
|
|
663
|
-
`text`, `ui` and `
|
|
727
|
+
`text`, `ui`, `audio`, `effects` and `platform` emit nothing; the `effects` budgets are log warnings.
|
|
664
728
|
|
|
665
729
|
```ts
|
|
666
730
|
import { createPlugin, flowPlugin } from "@moku-labs/game";
|
|
@@ -729,13 +793,18 @@ Set with `createApp({ pluginConfigs: { <plugin>: { ... } } })`.
|
|
|
729
793
|
| `i18n` | `fallback` | `string` | `"en"` | The locale a missing key is read from before it is reported missing |
|
|
730
794
|
| `i18n` | `locales` | `Record<string, module \| loader>` | `{}` | Compiled modules outside features, per locale |
|
|
731
795
|
| `text` | `fonts` | `{ body, digits }` | `{ body: "ui.font-body", digits: "ui.font-digits" }` | The two boot fonts behind the built-in styles `body` and `digits` |
|
|
732
|
-
| `text` | `missingGlyph` | `string` | `"□"` | Drawn for a glyph the font lacks |
|
|
733
796
|
| `ui` | `tapTargetPt` | `number` | `44` | The smallest tap target `lint()` accepts |
|
|
734
797
|
| `ui` | `breakpoints` | `{ tall, wide }` | `{ tall: 2, wide: 1.5 }` | Aspect thresholds of the `when` style variants |
|
|
735
798
|
| `audio` | `buses` | `{ master, music, sfx }` | `{ master: 1, music: 0.6, sfx: 1 }` | Start gain of each bus, 0..1 |
|
|
736
799
|
| `audio` | `musicFadeMs` | `number` | `600` | Cross-fade of a music switch, in real milliseconds |
|
|
737
800
|
| `audio` | `volumes` | `(player) => Partial<Record<Bus, number>> \| undefined` | `undefined` | Reads the player's choice from the committed player on every `model:committed`. Absent: the buses stay at `buses` |
|
|
738
801
|
| `audio` | `context` | `() => AudioContext \| undefined` | `undefined` | The context factory, a test seam. Absent: `new AudioContext()` where the global exists |
|
|
802
|
+
| `effects` | `maxParticles` | `number` | `3000` | Live particles over every instance above which `effects:particle-budget` warns once per crossing |
|
|
803
|
+
| `effects` | `maxPasses` | `number` | `24` | Render passes per frame above which `effects:pass-budget` warns once per crossing |
|
|
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 |
|
|
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 |
|
|
739
808
|
|
|
740
809
|
## Development
|
|
741
810
|
|
|
@@ -752,6 +821,9 @@ bun run test:unit # vitest project "unit"
|
|
|
752
821
|
bun run test:integration # vitest project "integration"
|
|
753
822
|
bun run test:coverage # both projects with coverage, 90% thresholds
|
|
754
823
|
bun run validate # publint and attw with the esm-only profile
|
|
824
|
+
bun run fixture:pack # pack the fixture game into tests/integration/merge-game/dist/assets
|
|
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>]
|
|
755
827
|
bun run release:setup # moku-release setup
|
|
756
828
|
bun run release:doctor # moku-release doctor
|
|
757
829
|
bun run release # moku-release
|
|
@@ -764,13 +836,14 @@ bun run release # moku-release
|
|
|
764
836
|
| `tests/unit/` | Framework-level unit tests: root index, setup |
|
|
765
837
|
| `tests/integration/` | Framework-level scenarios across plugins |
|
|
766
838
|
| `tests/integration/merge-game/` | The fixture game, written on the public API only. Not published |
|
|
839
|
+
| `tests/visual/` | The visual tests of the fixture game and their baselines: `<test>/<checkpoint>/state.json`, `describe.json`, `screen.webp` |
|
|
767
840
|
| `src/plugins/<name>/__tests__/unit/` | Unit tests of one plugin |
|
|
768
841
|
| `src/plugins/<name>/__tests__/integration/` | Integration tests of one plugin |
|
|
769
842
|
| `src/plugins/flow/__tests__/types/` | Type-level tests of the graph typing |
|
|
770
843
|
|
|
771
844
|
Plugin tests never go into the root `tests/` folder. Coverage thresholds are 90% for lines, functions, branches and statements.
|
|
772
845
|
|
|
773
|
-
### Lint rules
|
|
846
|
+
### Lint rules
|
|
774
847
|
|
|
775
848
|
The project rules live in [`eslint.config.ts`](./eslint.config.ts).
|
|
776
849
|
|
|
@@ -785,6 +858,7 @@ The project rules live in [`eslint.config.ts`](./eslint.config.ts).
|
|
|
785
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` |
|
|
786
859
|
| L8 | No signature echo: an `@example` whose whole body is one call with bare identifiers is an error | `src/**` |
|
|
787
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 |
|
|
788
862
|
|
|
789
863
|
## Requirements
|
|
790
864
|
|
|
@@ -807,6 +881,8 @@ The project rules live in [`eslint.config.ts`](./eslint.config.ts).
|
|
|
807
881
|
- [`text`](./src/plugins/text/README.md): styles, tags, measurement, fonts
|
|
808
882
|
- [`ui`](./src/plugins/ui/README.md): the JSX runtime, components, styles, layout, popups
|
|
809
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
|
|
810
886
|
- [`llms.txt`](./llms.txt): overview for an LLM that writes a game on this engine
|
|
811
887
|
- [Moku Core specification](https://github.com/moku-labs/core/tree/main/specification)
|
|
812
888
|
|
package/dist/assets.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as Manifest } from "./types-
|
|
1
|
+
import { l as Manifest } from "./types-C47fKNcn.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/plugins/assets/scan/cli.d.ts
|
|
4
4
|
/**
|
|
@@ -26,17 +26,22 @@ type StringsCompiler = (root: string, out: string, options: {
|
|
|
26
26
|
}) => Promise<StringsReport>;
|
|
27
27
|
/**
|
|
28
28
|
* Runs the asset key scanner: walk the features, write the manifest and the key module, then
|
|
29
|
-
* compile the feature strings next to the key module, or check that all of it is current.
|
|
29
|
+
* compile the feature strings next to the key module, or check that all of it is current. With
|
|
30
|
+
* `--pack <dir>` it packs the art for production on the same scan (`--no-cache` for a cold run).
|
|
30
31
|
* Nothing here calls `process.exit`; the caller does.
|
|
31
32
|
*
|
|
32
33
|
* @param argv - The arguments after the script name.
|
|
33
34
|
* @param compile - The strings compiler, `compileStrings` of i18n.
|
|
34
35
|
* @param ui - Where the lines go. The branded console by default.
|
|
35
|
-
* @returns The exit code: `1` when the scan or the
|
|
36
|
+
* @returns The exit code: `1` when the scan, the compile or the pack failed or `--check` found a
|
|
37
|
+
* difference, else `0`.
|
|
36
38
|
* @example
|
|
37
39
|
* ```ts
|
|
38
40
|
* await runCli(["--root", "src", "--manifest", "public/assets/manifest.json", "--check"], compileStrings);
|
|
39
41
|
* // 1 when public/assets/manifest.json is older than the files in src/features
|
|
42
|
+
*
|
|
43
|
+
* await runCli(["--root", "src", "--keys", "src/generated/assets.ts", "--pack", "dist/assets"], compileStrings);
|
|
44
|
+
* // 0, and dist/assets holds the pages, the loose files and the packed manifest.json
|
|
40
45
|
* ```
|
|
41
46
|
*/
|
|
42
47
|
declare function runCli(argv: string[], compile: StringsCompiler, ui?: ScanUi): Promise<number>;
|
|
@@ -92,8 +97,9 @@ declare function checkStrings(root: string, out: string): Promise<boolean>;
|
|
|
92
97
|
//#endregion
|
|
93
98
|
//#region src/plugins/assets/scan/emit.d.ts
|
|
94
99
|
/**
|
|
95
|
-
* Writes `manifest.json`: two-space JSON with a trailing newline, bundles sorted by name
|
|
96
|
-
* files
|
|
100
|
+
* Writes `manifest.json`: two-space JSON with a trailing newline, bundles sorted by name, pages
|
|
101
|
+
* by id and files by key. The version is written as given: `1` from the scanner, `2` from the
|
|
102
|
+
* packer.
|
|
97
103
|
*
|
|
98
104
|
* @param manifest - What the scan produced.
|
|
99
105
|
* @returns The text of the file.
|
|
@@ -118,6 +124,52 @@ declare function emitManifest(manifest: Manifest): string;
|
|
|
118
124
|
*/
|
|
119
125
|
declare function emitKeys(manifest: Manifest): string;
|
|
120
126
|
//#endregion
|
|
127
|
+
//#region src/plugins/assets/scan/pack/pack.d.ts
|
|
128
|
+
/**
|
|
129
|
+
* What one pack is told: where the sources are, what the scan found and where everything goes.
|
|
130
|
+
*/
|
|
131
|
+
type PackOptions = {
|
|
132
|
+
/** Path of the game source root: the `path` of every file of `manifest` is relative to it. */root: string; /** The manifest the scan produced (version 1, loose files). */
|
|
133
|
+
manifest: Manifest; /** The pack folder. It belongs to the command: created, written and pruned. */
|
|
134
|
+
out: string; /** Where the packed manifest (version 2) goes. */
|
|
135
|
+
manifestFile: string; /** The cache folder, or `false` for a run that neither reads nor writes the cache. */
|
|
136
|
+
cache: string | false;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* What a pack wrote.
|
|
140
|
+
*/
|
|
141
|
+
type PackResult = {
|
|
142
|
+
/** The packed manifest, version 2, as written. */manifest: Manifest; /** How many atlas pages the pack folder holds. */
|
|
143
|
+
pages: number; /** How many textures stayed files of their own. */
|
|
144
|
+
loose: number; /** The bytes of every file the manifest references. */
|
|
145
|
+
bytes: number; /** How many of the pages were replayed from the cache. */
|
|
146
|
+
cacheHits: number;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Packs the art of a game for production: per bundle, the textures of the `fx` group and of the
|
|
150
|
+
* `main` group go onto WebP atlas pages (2048 px at most, 2 px padding and border, never trimmed
|
|
151
|
+
* or rotated), a texture with a side above 512 px and a group of one stay loose files, fonts and
|
|
152
|
+
* sounds are copied, every file name carries the hash of its bytes, and the packed manifest
|
|
153
|
+
* (version 2) names it all. Nothing is written while a problem is open; afterwards the pack folder
|
|
154
|
+
* holds exactly what the manifest references, plus `manifest.json`. Two runs on the same tree and
|
|
155
|
+
* the same `sharp` write the same bytes.
|
|
156
|
+
*
|
|
157
|
+
* @param options - The sources, the scanned manifest, the pack folder, the manifest file, the cache.
|
|
158
|
+
* @returns The packed manifest and what was written.
|
|
159
|
+
* @throws {Error} One error that lists every problem, when `sharp` is missing, or when the pack
|
|
160
|
+
* folder holds the sources.
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* // After `scanAssets` read the features of the game. `cache: false` skips the cache.
|
|
164
|
+
* const { manifest, pages, cacheHits } = await packAssets({
|
|
165
|
+
* root: "src", manifest: scanned, cache: "node_modules/.cache/moku-game-pack",
|
|
166
|
+
* out: "dist/assets", manifestFile: "dist/assets/manifest.json"
|
|
167
|
+
* });
|
|
168
|
+
* // manifest.version: 2; on a second run with the same art, cacheHits equals pages
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare function packAssets(options: PackOptions): Promise<PackResult>;
|
|
172
|
+
//#endregion
|
|
121
173
|
//#region src/plugins/assets/scan/scan.d.ts
|
|
122
174
|
/**
|
|
123
175
|
* What one scan is told: where the features are and where the two generated files go.
|
|
@@ -158,4 +210,4 @@ type ScanResult = {
|
|
|
158
210
|
*/
|
|
159
211
|
declare function scanAssets(options: ScanOptions): Promise<ScanResult>;
|
|
160
212
|
//#endregion
|
|
161
|
-
export { type CompileOptions, type CompileReport, type ScanOptions, type ScanResult, type ScanUi, type StringsCompiler, type StringsReport, checkStrings, compileStrings, emitKeys, emitManifest, runCli, scanAssets };
|
|
213
|
+
export { type CompileOptions, type CompileReport, type PackOptions, type PackResult, type ScanOptions, type ScanResult, type ScanUi, type StringsCompiler, type StringsReport, checkStrings, compileStrings, emitKeys, emitManifest, packAssets, runCli, scanAssets };
|