@combos-fun/engine 0.0.12 → 0.0.14

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/agent-skill.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@combos-fun/engine` — Agent notes
2
2
 
3
- ECS microkernel for Combos Fun. This file is the canonical knowledge source for **using** the engine. For **authoring** a new `@combos-fun/plugin-*`, read `@combos-fun/engine/plugin-authoring` instead.
3
+ ECS microkernel for Combos Fun. This file is the canonical knowledge source for **using** the engine. For **authoring** a new `@combos-fun/plugin-`*, read `@combos-fun/engine/plugin-authoring` instead.
4
4
 
5
5
  ## When to read
6
6
 
@@ -25,14 +25,31 @@ Read this every time before working on a Combos Fun project. The entry skill loa
25
25
 
26
26
  ### `GameParams`
27
27
 
28
- | Field | Type | Default | Notes |
29
- |-------|------|---------|-------|
30
- | `systems` | `System[]` | `[]` | Bootstrapped async in registration order |
31
- | `frameRate` | `number` | `60` | |
32
- | `autoStart` | `boolean` | `true` | |
33
- | `needScene` | `boolean` | `true` | Auto-creates `Scene('scene')` |
34
- | `onSystemsBootstrapComplete` | `(game, error?) => void` | | After all systems init |
35
- | `pluginInitNotifyTargetOrigin` | `string` | `'*'` | postMessage target |
28
+
29
+ | Field | Type | Default | Notes |
30
+ | ------------------------------ | ------------------------ | ------- | ---------------------------------------- |
31
+ | `systems` | `System[]` | `[]` | Bootstrapped async in registration order |
32
+ | `frameRate` | `number` | `60` | |
33
+ | `autoStart` | `boolean` | `true` | |
34
+ | `needScene` | `boolean` | `true` | Auto-creates `Scene('scene')` |
35
+ | `onSystemsBootstrapComplete` | `(game, error?) => void` | — | After all systems init |
36
+ | `pluginInitNotifyTargetOrigin` | `string` | `'*'` | postMessage target |
37
+
38
+
39
+ When the game runs inside an iframe (`window.parent !== window`), each `Game.addSystem` call posts to the parent after that system's `init` completes:
40
+
41
+ ```typescript
42
+ {
43
+ type: 'combos-game:plugin-init-success',
44
+ systemName: string, // System.systemName
45
+ engineVersion: string, // @combos-fun/engine build version
46
+ packageName?: string, // npm name, injected at plugin build
47
+ packageVersion?: string, // semver from plugin package.json, injected at plugin build
48
+ }
49
+ ```
50
+
51
+ Official `@combos-fun/plugin-*` packages get `packageName` / `packageVersion` automatically via `scripts/build-package.mjs` (no hand-written static fields). Host pages can gate tooling on specific systems or versions using this payload. Types: `CombosGamePluginInitSuccessMessage`, helper `postParentPluginInitSuccess` in `bootstrapMessages.ts`.
52
+
36
53
 
37
54
  ### `Game` methods
38
55
 
@@ -40,19 +57,23 @@ Read this every time before working on a Combos Fun project. The entry skill loa
40
57
 
41
58
  ### `resource` singleton
42
59
 
43
- | Method | Notes |
44
- |--------|-------|
45
- | `addResource(resources[])` | Register (no load) |
46
- | `preload()` | Load all `preload: true` |
47
- | `loadConfig(resources[])` | `addResource` + `preload` |
48
- | `loadSingle(resource): Promise` | Add + load one |
49
- | `getResource(name): Promise` | Get loaded |
50
- | `destroy(name): Promise` | Destroy one |
51
- | `registerResourceType(type, value?)` | Custom type |
52
- | `registerInstance(type, cb)` / `registerDestroy(type, cb)` | Factory / destructor |
60
+
61
+ | Method | Notes |
62
+ | ---------------------------------------------------------- | ------------------------- |
63
+ | `addResource(resources[])` | Register (no load) |
64
+ | `preload()` | Load all `preload: true` |
65
+ | `loadConfig(resources[])` | `addResource` + `preload` |
66
+ | `loadSingle(resource): Promise` | Add + load one |
67
+ | `getResource(name): Promise` | Get loaded |
68
+ | `destroy(name): Promise` | Destroy one |
69
+ | `registerResourceType(type, value?)` | Custom type |
70
+ | `registerInstance(type, cb)` / `registerDestroy(type, cb)` | Factory / destructor |
71
+
53
72
 
54
73
  Fields: `timeout` (6000ms), `resourcesMap`, `progress`.
55
74
 
75
+ > ⚠️ `addResource` only registers; it does **not** kick off network load. Components that reference an unloaded `resource` (e.g. `Img({ resource: 'logo' })`) silently render nothing — the canvas stays blank with no console error. Always pair it with `preload()` (and gate `new Game(...)` behind `resource.once(LOAD_EVENT.COMPLETE, ...)`), or skip the pair entirely and use `loadConfig(resources[])` which does both in one call.
76
+
56
77
  ## ECS hard rules (mandatory)
57
78
 
58
79
  These rules apply to every code change. Violations must be fixed before the change is considered complete.
@@ -63,7 +84,7 @@ Any data belonging to a game entity must be a field on a `Component` attached to
63
84
 
64
85
  ### Rule 2: All behaviour logic lives in Component / System hooks
65
86
 
66
- Code that runs per-frame, reacts to events, or mutates entity state must live in `Component` hooks (`init` / `awake` / `start` / `update` / `lateUpdate` / `onPause` / `onResume` / `onDestroy`) or `System` hooks (same set + `componentObserver` in `update`). No event callbacks in bootstrap, no `setInterval` outside Components, no `game.ticker.add` for game logic.
87
+ Code that runs per-frame, reacts to events, or mutates entity state must live in `Component` hooks (`init` / `awake` / `start` / `update` / `lateUpdate` / `onPause` / `onResume` / `onDestroy`) or `System` hooks (same set + `componentObserver` in `update`). No event callbacks in bootstrap, no `setInterval` outside Components, no `game.ticker.add` for game logic. Every `.on(...)` / `addEventListener(...)` subscription set up in `start` / `awake` must be torn down in `onDestroy`.
67
88
 
68
89
  ### Rule 3: Bootstrap code only wires, never implements
69
90
 
@@ -71,11 +92,11 @@ The entry file may: register resources, create `Game` with `systems`, create `Ga
71
92
 
72
93
  ### Rule 4: Single concern per Component, cross-entity logic in Systems
73
94
 
74
- A Component holds data + self-contained behaviour for one concern on one entity. A System holds cross-entity logic reacting to component changes. Do not put multi-entity coordination inside a Component.
95
+ A Component holds data + self-contained behaviour for one concern on one entity. A System holds cross-entity logic reacting to component changes. Do not put multi-entity coordination inside a Component. Every custom Component subclass must set `static componentName`; every custom System reacts via `@decorators.componentObserver({ Name: ['prop'] })` and drains observations in `update()` — never via ad-hoc scans of the scene graph.
75
96
 
76
97
  ### Rule 5: No direct Pixi / Three.js / DOM manipulation outside Renderer pattern
77
98
 
78
- 2D display objects must come through `plugin-renderer-*` systems or a custom `Renderer` subclass registered via `rendererManager.register(this)`. 3D objects must come through `plugin-renderer-3d-*` systems or a custom `Renderer3D` subclass. No raw `new PIXI.Sprite(...)` / `new THREE.Mesh(...)` in bootstrap or plain Components.
99
+ 2D display objects must come through `plugin-renderer-`* systems or a custom `Renderer` subclass registered via `rendererManager.register(this)`. 3D objects must come through `plugin-renderer-3d-`* systems or a custom `Renderer3D` subclass. No raw `new PIXI.Sprite(...)` / `new THREE.Mesh(...)` in bootstrap or plain Components.
79
100
 
80
101
  ### Rule 6: Component configuration via params, not imperative calls
81
102
 
@@ -94,99 +115,154 @@ Runs only once at startup to wire things?
94
115
  └── Rendering integration → Renderer (2D) / Renderer3D (3D) subclass
95
116
  ```
96
117
 
97
- ## ECS compliance checklist
98
-
99
- Run after every change:
100
-
101
- - [ ] No game logic in entry file — only resource registration, `Game` / `System` creation, `GameObject` + `Component` wiring, scene setup
102
- - [ ] No retained component references — entry file does not store `addComponent()` returns for later use
103
- - [ ] No anonymous event handlers — all `.on(...)` / `addEventListener(...)` inside Component lifecycle hooks with cleanup in `onDestroy`
104
- - [ ] No module-scope mutable state holding entity data
105
- - [ ] All entity state accessible via `gameObject.getComponent(...)`
106
- - [ ] All per-frame logic in Component / System `update` / `lateUpdate`
107
- - [ ] Custom behaviour = named Component class with `static componentName`
108
- - [ ] Every event subscription has cleanup in `onDestroy`
109
- - [ ] Systems use `@decorators.componentObserver` and drain in `update()`
110
- - [ ] No direct Pixi / Three.js manipulation outside `Renderer` / `Renderer3D` subclasses
111
-
112
118
  ## Inventory (mandatory after every task)
113
119
 
114
120
  After every development task, write or update three inventory tables in the **project memory**:
115
121
 
116
122
  **Component inventory** — for each custom Component:
117
123
 
118
- | Field | Description |
119
- |-------|-------------|
120
- | `componentName` | Static name string |
121
- | Purpose | One-sentence description |
122
- | Key params | Constructor params that affect behaviour |
124
+
125
+ | Field | Description |
126
+ | --------------- | ----------------------------------------------------------------------------------------- |
127
+ | `componentName` | Static name string |
128
+ | Purpose | One-sentence description |
129
+ | Key params | Constructor params that affect behaviour |
123
130
  | Lifecycle hooks | Which of `init` / `awake` / `start` / `update` / `lateUpdate` / `onDestroy` it implements |
124
131
 
132
+
125
133
  **GameObject inventory** — for each GameObject:
126
134
 
127
- | Field | Description |
128
- |-------|-------------|
129
- | Name | Identifier or variable name |
130
- | Parent | Parent GameObject or scene root |
131
- | Components | Attached Components |
132
- | Purpose | Role in the game |
135
+
136
+ | Field | Description |
137
+ | ---------- | ------------------------------- |
138
+ | Name | Identifier or variable name |
139
+ | Parent | Parent GameObject or scene root |
140
+ | Components | Attached Components |
141
+ | Purpose | Role in the game |
142
+
133
143
 
134
144
  **System inventory** — for each System:
135
145
 
136
- | Field | Description |
137
- |-------|-------------|
138
- | Class name | System class |
139
- | Order | Registration order in `systems` array |
140
- | What it owns | Components observed / coordinated |
141
- | Purpose | One-sentence description |
146
+
147
+ | Field | Description |
148
+ | ------------ | ------------------------------------- |
149
+ | Class name | System class |
150
+ | Order | Registration order in `systems` array |
151
+ | What it owns | Components observed / coordinated |
152
+ | Purpose | One-sentence description |
153
+
142
154
 
143
155
  Skipping inventory is treated with the same weight as the ECS hard rules.
144
156
 
145
157
  ## Common pitfalls
146
158
 
147
- | Symptom | Fix |
148
- |---------|-----|
149
- | Blank canvas | Add the right base renderer system (`RendererSystem` for 2D, `Renderer3DSystem` for 3D); ensure `autoStart: true` or call `game.start()` |
150
- | Nothing draws | Add the matching sub-system (e.g. `ImgSystem`, `Graphics3DSystem`) before adding components |
151
- | `getSystem` returns undefined | Use class reference, not string: `game.getSystem(RendererSystem)` |
152
- | "Component already added" error | Use a new `Component` instance per `GameObject` |
153
- | Destroyed `GameObject` causes errors | Retained reference in a closure — move logic into a Component |
154
- | Cannot pause / resume behaviour | Logic in `setInterval` / `ticker.add` — move into `Component.update()` |
155
- | Resource name not found | Match `resource.addResource` `name` to component `resource` field |
156
- | TS: `onError` missing on `SoundSystem` | `SoundSystem` requires `onError` callback |
157
- | Stale render after async load | 3D: use `increaseAsyncId` / `validateAsyncId` to drop stale work |
158
159
 
159
- ## Minimal 2D bootstrap
160
+ | Symptom | Fix |
161
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | Blank canvas | Add the right base renderer system (`RendererSystem` for 2D, `Renderer3DSystem` for 3D); ensure `autoStart: true` or call `game.start()` |
163
+ | Nothing draws | Add the matching sub-system (e.g. `ImgSystem`, `Graphics3DSystem`) before adding components |
164
+ | `getSystem` returns undefined | Use class reference, not string: `game.getSystem(RendererSystem)` |
165
+ | "Component already added" error | Use a new `Component` instance per `GameObject` |
166
+ | Resource name not found | Match `resource.addResource` `name` to component `resource` field |
167
+ | TS: `onError` missing on `SoundSystem` | `SoundSystem` requires `onError` callback |
168
+ | Stale render after async load | 3D: use `increaseAsyncId` / `validateAsyncId` to drop stale work |
160
169
 
161
- ```ts
162
- import { Game, resource } from '@combos-fun/engine';
163
- import { RendererSystem } from '@combos-fun/plugin-renderer';
164
- import { RenderSystem } from '@combos-fun/plugin-renderer-render';
165
- import { ImgSystem } from '@combos-fun/plugin-renderer-img';
166
170
 
167
- resource.addResource([{ name: 'logo', src: 'logo.png', preload: true }]);
171
+ ## Minimal 2D bootstrap
168
172
 
169
- new Game({
170
- systems: [
171
- new RendererSystem({ canvas: document.querySelector('#canvas')!, width: 750, height: 1334 }),
172
- new RenderSystem(),
173
- new ImgSystem(),
174
- ],
173
+ ```ts
174
+ import {
175
+ Game,
176
+ GameObject,
177
+ resource,
178
+ RESOURCE_TYPE,
179
+ LOAD_EVENT,
180
+ } from "@combos-fun/engine";
181
+ import { RendererSystem } from "@combos-fun/plugin-renderer";
182
+ import { Render, RenderSystem } from "@combos-fun/plugin-renderer-render";
183
+ import { Img, ImgSystem } from "@combos-fun/plugin-renderer-img";
184
+
185
+ resource.once(LOAD_EVENT.COMPLETE, () => {
186
+ new Game({
187
+ systems: [
188
+ new RendererSystem({
189
+ canvas: document.querySelector("#canvas")!,
190
+ width: 750,
191
+ height: 1334,
192
+ }),
193
+ new RenderSystem(),
194
+ new ImgSystem(),
195
+ ],
196
+ // Safe place to wire the scene graph: every system has finished init.
197
+ onSystemsBootstrapComplete: (g) => {
198
+ // Transform values go through constructor params — never set them imperatively.
199
+ const logo = new GameObject("logo", {
200
+ position: { x: 100, y: 100 },
201
+ size: { width: 200, height: 200 },
202
+ origin: { x: 0.5, y: 0.5 },
203
+ });
204
+ logo.addComponent(new Img({ resource: "logo" }));
205
+ // Render is only needed when you want to hide / fade / reorder; without it the
206
+ // object is still drawn at alpha 1, zIndex 0. Requires RenderSystem registered.
207
+ logo.addComponent(new Render({ zIndex: 5 }));
208
+ // Use addChild — not addGameObject — so transform parent + scene are both wired.
209
+ g.scene.addChild(logo);
210
+ },
211
+ });
175
212
  });
213
+
214
+ resource.loadConfig([
215
+ {
216
+ name: "logo",
217
+ type: RESOURCE_TYPE.IMAGE,
218
+ src: { image: { type: "png", url: "logo.png" } },
219
+ preload: true,
220
+ },
221
+ ]);
176
222
  ```
177
223
 
224
+ `ResourceBase` requires both `type: RESOURCE_TYPE` and a nested `src` object keyed by media slot (`image` / `json` / `audio` / `video` / `tex` / `ske` / …) where each slot is `{ type, url }`. A flat `src: 'logo.png'` is invalid.
225
+
178
226
  ## Minimal 3D bootstrap
179
227
 
180
228
  ```ts
181
- import { Game } from '@combos-fun/engine';
182
- import { Renderer3DSystem } from '@combos-fun/plugin-renderer-3d';
183
- import { Graphics3DSystem } from '@combos-fun/plugin-renderer-3d-graphics';
229
+ import { Game, GameObject } from "@combos-fun/engine";
230
+ import { Renderer3DSystem } from "@combos-fun/plugin-renderer-3d";
231
+ import {
232
+ Graphics3D,
233
+ Graphics3DSystem,
234
+ } from "@combos-fun/plugin-renderer-3d-graphics";
184
235
 
185
236
  new Game({
186
237
  systems: [
187
- new Renderer3DSystem({ canvas: document.querySelector('#canvas')!, width: 750, height: 1000 }),
238
+ new Renderer3DSystem({
239
+ canvas: document.querySelector("#canvas")!,
240
+ width: 750,
241
+ height: 1000,
242
+ }),
188
243
  new Graphics3DSystem(),
189
244
  ],
245
+ // Safe place to wire the scene graph: every system has finished init.
246
+ onSystemsBootstrapComplete: (g) => {
247
+ // 3D position / rotation / scale go through the component's own params
248
+ // (positionX/Y/Z, rotationX/Y/Z, scaleX/Y/Z) — GameObject's TransformParams
249
+ // is 2D-only (Vector2 + Size2) and is not read by 3D renderers.
250
+ const box = new GameObject("box");
251
+ box.addComponent(
252
+ new Graphics3D({
253
+ shape: "box",
254
+ width: 1,
255
+ height: 1,
256
+ depth: 1,
257
+ color: 0xff0000,
258
+ positionX: 0,
259
+ positionY: 1,
260
+ positionZ: 0,
261
+ }),
262
+ );
263
+ // Use addChild — not addGameObject — so transform parent + scene are both wired.
264
+ g.scene.addChild(box);
265
+ },
190
266
  });
191
267
  ```
192
268
 
@@ -202,3 +278,4 @@ After modifying engine code:
202
278
  ## See also
203
279
 
204
280
  - `@combos-fun/engine/plugin-authoring` — full spec for writing a new `@combos-fun/plugin-*` package, including Component / System / Renderer / Renderer3D subclassing, manifest format, and publish requirements.
281
+
@@ -963,6 +963,9 @@ class Scene extends GameObject {
963
963
  }
964
964
  }
965
965
 
966
+ /** Generated at build from package.json */
967
+ const version = "0.0.14";
968
+
966
969
  /**
967
970
  * Sent to `window.parent` after each `System.init` completes during `Game.addSystem`
968
971
  * (when running inside an iframe and parent differs from self).
@@ -971,14 +974,17 @@ const COMBOS_GAME_PLUGIN_INIT_SUCCESS = "combos-game:plugin-init-success";
971
974
  /**
972
975
  * Notifies the embedding page that a system (plugin) finished async/sync `init`.
973
976
  */
974
- function postParentPluginInitSuccess(systemName, targetOrigin = "*") {
977
+ function postParentPluginInitSuccess(input, targetOrigin = "*") {
975
978
  if (typeof window === "undefined")
976
979
  return;
977
980
  if (!window.parent || window.parent === window)
978
981
  return;
979
982
  const payload = {
980
983
  type: COMBOS_GAME_PLUGIN_INIT_SUCCESS,
981
- systemName,
984
+ systemName: input.systemName,
985
+ engineVersion: version,
986
+ ...(input.packageName ? { packageName: input.packageName } : {}),
987
+ ...(input.packageVersion ? { packageVersion: input.packageVersion } : {}),
982
988
  };
983
989
  try {
984
990
  window.parent.postMessage(payload, targetOrigin);
@@ -996,6 +1002,13 @@ function systemClassName(ctor) {
996
1002
  }
997
1003
  return "UnknownSystem";
998
1004
  }
1005
+ function systemPackageMeta(ctor) {
1006
+ const meta = ctor;
1007
+ return {
1008
+ packageName: typeof meta.packageName === "string" ? meta.packageName : undefined,
1009
+ packageVersion: typeof meta.packageVersion === "string" ? meta.packageVersion : undefined,
1010
+ };
1011
+ }
999
1012
  function componentClassName(ctor) {
1000
1013
  if ("componentName" in ctor) {
1001
1014
  const cn = ctor.componentName;
@@ -1173,7 +1186,10 @@ class Game extends EventEmitter__default.default {
1173
1186
  if (system.init) {
1174
1187
  await Promise.resolve(system.init(system.__systemDefaultParams));
1175
1188
  }
1176
- postParentPluginInitSuccess(systemClassName(system.constructor), this.pluginInitNotifyTargetOrigin);
1189
+ postParentPluginInitSuccess({
1190
+ systemName: systemClassName(system.constructor),
1191
+ ...systemPackageMeta(system.constructor),
1192
+ }, this.pluginInitNotifyTargetOrigin);
1177
1193
  setSystemObserver(system, system.constructor);
1178
1194
  initObserver(system.constructor);
1179
1195
  try {
@@ -1709,7 +1725,6 @@ const decorators = {
1709
1725
  IDEProp,
1710
1726
  componentObserver,
1711
1727
  };
1712
- const version = '__VERSION__';
1713
1728
  console.log(`@combos-fun/engine version: ${version}`);
1714
1729
 
1715
1730
  exports.COMBOS_GAME_PLUGIN_INIT_SUCCESS = COMBOS_GAME_PLUGIN_INIT_SUCCESS;