@combos-fun/engine 0.0.35 → 0.0.37
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 +21 -273
- package/dist/engine.cjs.js +1 -1
- package/dist/engine.cjs.js.map +1 -1
- package/dist/engine.cjs.prod.js +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.esm.js +1 -1
- package/dist/engine.esm.js.map +1 -1
- package/package.json +7 -5
- package/references/bootstrap-examples.md +111 -0
- package/references/public-api.md +87 -0
package/agent-skill.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# `@combos-fun/engine` — Agent
|
|
1
|
+
# `@combos-fun/engine` — Agent short card
|
|
2
2
|
|
|
3
|
-
ECS microkernel for Combos Fun.
|
|
3
|
+
ECS microkernel for Combos Fun. **Short card only** — do not re-cat after the first read. For authoring a new `@combos-fun/plugin-*`, read `@combos-fun/engine/plugin-authoring`.
|
|
4
4
|
|
|
5
5
|
## When to read
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Open this card **only when** an existing-project hard gate allows a plugin skill read (unfamiliar compile symbol / new plugin / real API conflict), or when starting from zero after mode detection. **Do not** load it every turn, and **do not** bulk-read sibling plugin skills with it.
|
|
8
|
+
|
|
9
|
+
Public API tables, host `postMessage` protocol, and long bootstrap samples: [`references/public-api.md`](references/public-api.md), [`references/bootstrap-examples.md`](references/bootstrap-examples.md).
|
|
8
10
|
|
|
9
11
|
## Microkernel model
|
|
10
12
|
|
|
@@ -13,120 +15,14 @@ Read this every time before working on a Combos Fun project. The entry skill loa
|
|
|
13
15
|
- **Observer**: `@decorators.componentObserver({ Name: ['prop'] })` on a System; drain `this.componentObserver.clear()` in `update()` and switch on `OBSERVER_TYPE.ADD | CHANGE | REMOVE`.
|
|
14
16
|
- **Frame order**: `Component.update` → `Component.lateUpdate` → `System.update` → `System.lateUpdate`. `start()` fires inline on the first `update` tick.
|
|
15
17
|
|
|
16
|
-
## Public API
|
|
17
|
-
|
|
18
|
-
### Values
|
|
19
|
-
|
|
20
|
-
`Game`, `Scene`, `GameObject`, `Component`, `System`, `Transform`, `resource`, `resourceLoader`, `decorators`, `IDEProp`, `componentObserver`, `LOAD_EVENT`, `RESOURCE_TYPE`, `OBSERVER_TYPE`, `LOAD_SCENE_MODE`, `RESOURCE_TYPE_STRATEGY`, `version`, `COMBOS_GAME_PLUGIN_INIT_SUCCESS`, `COMBOS_GAME_READY`, `COMBOS_GAME_SET_PLAYING`, `COMBOS_GAME_STATE_CHANGED`, `postParentPluginInitSuccess`, `postParentGameReady`, `postParentGameState`, `parseSetPlayingMessage`, `DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES`, `isAllowedMessageOrigin`, `mergeAllowedMessageOrigins`.
|
|
21
|
-
|
|
22
|
-
### Types
|
|
23
|
-
|
|
24
|
-
`GameParams`, `PluginStruct`, `TransformParams`, `ComponentChanged`, `UpdateParams`, `ComponentParams`, `ObserverInfo`, `PureObserverInfo`, `ResourceBase`, `SystemConstructor`, `CombosGamePluginInitSuccessMessage`, `CombosGameReadyMessage`, `CombosGameStateChangedMessage`, `CombosGameSetPlayingMessage`.
|
|
25
|
-
|
|
26
|
-
### `GameParams`
|
|
27
|
-
|
|
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` | `'*'` | Outbound postMessage target (init / ready / state) |
|
|
37
|
-
| `allowedMessageOrigins` | `string[]` | defaults | Inbound origins allowed to send `set-playing` (merged with defaults; `['*']` = any) |
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
When the game runs inside an iframe (`window.parent !== window`), each `Game.addSystem` call posts to the parent after that system's `init` completes:
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
{
|
|
44
|
-
type: 'combos-game:plugin-init-success',
|
|
45
|
-
systemName: string, // System.systemName
|
|
46
|
-
engineVersion: string, // @combos-fun/engine build version
|
|
47
|
-
packageName?: string, // npm name, injected at plugin build
|
|
48
|
-
packageVersion?: string, // semver from plugin package.json, injected at plugin build
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
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`.
|
|
53
|
-
|
|
54
|
-
### Host lifecycle protocol (preload → hold → play)
|
|
55
|
-
|
|
56
|
-
Core `Game` speaks a `postMessage` protocol with the embedding page so a host APP can preload, hold, then start the game on user intent. All outbound messages go to `pluginInitNotifyTargetOrigin`; the inbound command is origin-checked against `allowedMessageOrigins`.
|
|
57
|
-
|
|
58
|
-
**Outbound (iframe → parent):**
|
|
59
|
-
|
|
60
|
-
| Type | When | Payload |
|
|
61
|
-
| ---- | ---- | ------- |
|
|
62
|
-
| `combos-game:ready` | Bootstrap finished (systems `init`/`awake`, optional scene load & start). Sent once even with `autoStart:false`. | `{ engineVersion, error? }` |
|
|
63
|
-
| `combos-game:state-changed` | After `start` / `pause` / `resume` | `{ playing, started }` |
|
|
64
|
-
|
|
65
|
-
**Inbound (parent → iframe):**
|
|
66
|
-
|
|
67
|
-
| Type | Effect | Payload |
|
|
68
|
-
| ---- | ------ | ------- |
|
|
69
|
-
| `combos-game:set-playing` | `true` → cold `start()` on first play, else `resume()`; `false` → `pause()` | `{ playing: boolean }` |
|
|
70
|
-
|
|
71
|
-
Recommended flow: create the game with `autoStart:false`, wait for `combos-game:ready`, keep the game held at frame 0 (host shows a cover/loading overlay), then post `{ type: 'combos-game:set-playing', playing: true }` when the user taps play. `game.setPlaying(playing)` is the programmatic equivalent. Helpers: `postParentGameReady`, `postParentGameState`, `parseSetPlayingMessage`.
|
|
72
|
-
|
|
73
|
-
> Note: play/pause is owned by the engine core here, **not** by `plugin-development-tool` (which now only handles pick mode + mute).
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
### `Game` methods
|
|
77
|
-
|
|
78
|
-
`addSystem(system)`, `removeSystem(system | class | string)` (calls `system.destroy()` internally), `getSystem(class | string)`, `loadScene({ scene, mode?, params? })` (`LOAD_SCENE_MODE.SINGLE | MULTI_CANVAS`), `start()`, `pause()`, `resume()`, `setPlaying(playing)` (host bridge: cold-start-or-resume / pause), `destroy()`.
|
|
79
|
-
|
|
80
|
-
### `resource` singleton
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
| Method | Notes |
|
|
84
|
-
| ---------------------------------------------------------- | ------------------------- |
|
|
85
|
-
| `addResource(resources[])` | Register (no load) |
|
|
86
|
-
| `preload()` | Load all `preload: true` |
|
|
87
|
-
| `loadConfig(resources[])` | `addResource` + `preload` |
|
|
88
|
-
| `loadSingle(resource): Promise` | Add + load one |
|
|
89
|
-
| `getResource(name): Promise` | Get loaded |
|
|
90
|
-
| `destroy(name): Promise` | Destroy one |
|
|
91
|
-
| `registerResourceType(type, value?)` | Custom type |
|
|
92
|
-
| `registerInstance(type, cb)` / `registerDestroy(type, cb)` | Factory / destructor |
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Fields: `timeout` (6000ms), `resourcesMap`, `progress`.
|
|
96
|
-
|
|
97
|
-
> ⚠️ `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.
|
|
98
|
-
|
|
99
18
|
## ECS hard rules (mandatory)
|
|
100
19
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
### Rule 2: All behaviour logic lives in Component / System hooks
|
|
108
|
-
|
|
109
|
-
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`.
|
|
110
|
-
|
|
111
|
-
### Rule 3: Bootstrap code only wires, never implements
|
|
112
|
-
|
|
113
|
-
The entry file may: register resources, create `Game` with `systems`, create `GameObject`s + attach `Component`s, add to scene, call `game.start()`. The entry file must **not**: define event handlers, call component methods (`spriteAnim.play()`), hold component references for later use, or contain `if` / `switch` / loop game logic. When you need interaction logic, create a custom Component whose `start()` wires it and whose `onDestroy()` cleans it up.
|
|
114
|
-
|
|
115
|
-
### Rule 4: Single concern per Component, cross-entity logic in Systems
|
|
116
|
-
|
|
117
|
-
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.
|
|
118
|
-
|
|
119
|
-
### Rule 5: No direct Pixi / Three.js / DOM manipulation outside Renderer pattern
|
|
120
|
-
|
|
121
|
-
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.
|
|
122
|
-
|
|
123
|
-
### Rule 6: One render Component per GameObject
|
|
124
|
-
|
|
125
|
-
A GameObject may carry at most one visible render Component. Do not attach multiple render Components such as `Img` + `Text`, `Graphics` + `Sprite`, or multiple sprite renderers to the same GameObject. Create child or sibling GameObjects for additional visible elements and use the Transform hierarchy to compose them. This keeps renderer ownership, bounds, picking, ordering, and destruction deterministic.
|
|
126
|
-
|
|
127
|
-
### Rule 7: Component configuration via params, not imperative calls
|
|
128
|
-
|
|
129
|
-
Configure Components through constructor params (e.g. `autoPlay: true`). If behaviour must be triggered at runtime, do it from another Component's lifecycle hook, not from bootstrap code.
|
|
20
|
+
1. **Entity state in Components** — no module-scope / closure entity stores. Destroying a `GameObject` must drop its state.
|
|
21
|
+
2. **Behaviour in Component / System hooks** — no bootstrap event handlers, no `setInterval` / `game.ticker.add` for game logic; tear down listeners in `onDestroy`.
|
|
22
|
+
3. **Bootstrap only wires** — create Game / Systems / GameObjects / Components; no game `if` / loops in entry files.
|
|
23
|
+
4. **One concern per Component; cross-entity logic in Systems** — custom types need `static componentName` / observer drain, not ad-hoc scene scans.
|
|
24
|
+
5. **No raw Pixi / Three / DOM outside Renderer pattern** — use `plugin-renderer-*` / `plugin-renderer-3d-*` or registered `Renderer` / `Renderer3D`.
|
|
25
|
+
6. **Configure via constructor params** — runtime triggers from other Component hooks, not bootstrap.
|
|
130
26
|
|
|
131
27
|
## Decision flowchart
|
|
132
28
|
|
|
@@ -141,166 +37,18 @@ Runs only once at startup to wire things?
|
|
|
141
37
|
└── Rendering integration → Renderer (2D) / Renderer3D (3D) subclass
|
|
142
38
|
```
|
|
143
39
|
|
|
144
|
-
## Inventory (mandatory after every task)
|
|
145
|
-
|
|
146
|
-
After every development task, write or update three inventory tables in the **project memory**:
|
|
147
|
-
|
|
148
|
-
**Component inventory** — for each custom Component:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
| Field | Description |
|
|
152
|
-
| --------------- | ----------------------------------------------------------------------------------------- |
|
|
153
|
-
| `componentName` | Static name string |
|
|
154
|
-
| Purpose | One-sentence description |
|
|
155
|
-
| Key params | Constructor params that affect behaviour |
|
|
156
|
-
| Lifecycle hooks | Which of `init` / `awake` / `start` / `update` / `lateUpdate` / `onDestroy` it implements |
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
**GameObject inventory** — for each GameObject:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
| Field | Description |
|
|
163
|
-
| ---------- | ------------------------------- |
|
|
164
|
-
| Name | Identifier or variable name |
|
|
165
|
-
| Parent | Parent GameObject or scene root |
|
|
166
|
-
| Components | Attached Components |
|
|
167
|
-
| Purpose | Role in the game |
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
**System inventory** — for each System:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
| Field | Description |
|
|
174
|
-
| ------------ | ------------------------------------- |
|
|
175
|
-
| Class name | System class |
|
|
176
|
-
| Order | Registration order in `systems` array |
|
|
177
|
-
| What it owns | Components observed / coordinated |
|
|
178
|
-
| Purpose | One-sentence description |
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Skipping inventory is treated with the same weight as the ECS hard rules.
|
|
182
|
-
|
|
183
40
|
## Common pitfalls
|
|
184
41
|
|
|
185
|
-
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
| Resource name not found | Match `resource.addResource` `name` to component `resource` field |
|
|
193
|
-
| TS: `onError` missing on `SoundSystem` | `SoundSystem` requires `onError` callback |
|
|
194
|
-
| Stale render after async load | 3D: use `increaseAsyncId` / `validateAsyncId` to drop stale work |
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
## Minimal 2D bootstrap
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
import {
|
|
201
|
-
Game,
|
|
202
|
-
GameObject,
|
|
203
|
-
resource,
|
|
204
|
-
RESOURCE_TYPE,
|
|
205
|
-
LOAD_EVENT,
|
|
206
|
-
} from "@combos-fun/engine";
|
|
207
|
-
import { RendererSystem } from "@combos-fun/plugin-renderer";
|
|
208
|
-
import { Render, RenderSystem } from "@combos-fun/plugin-renderer-render";
|
|
209
|
-
import { Img, ImgSystem } from "@combos-fun/plugin-renderer-img";
|
|
210
|
-
|
|
211
|
-
resource.once(LOAD_EVENT.COMPLETE, () => {
|
|
212
|
-
new Game({
|
|
213
|
-
systems: [
|
|
214
|
-
new RendererSystem({
|
|
215
|
-
canvas: document.querySelector("#canvas")!,
|
|
216
|
-
width: 750,
|
|
217
|
-
height: 1334,
|
|
218
|
-
}),
|
|
219
|
-
new RenderSystem(),
|
|
220
|
-
new ImgSystem(),
|
|
221
|
-
],
|
|
222
|
-
// Safe place to wire the scene graph: every system has finished init.
|
|
223
|
-
onSystemsBootstrapComplete: (g) => {
|
|
224
|
-
// Transform values go through constructor params — never set them imperatively.
|
|
225
|
-
const logo = new GameObject("logo", {
|
|
226
|
-
position: { x: 100, y: 100 },
|
|
227
|
-
size: { width: 200, height: 200 },
|
|
228
|
-
origin: { x: 0.5, y: 0.5 },
|
|
229
|
-
});
|
|
230
|
-
logo.addComponent(new Img({ resource: "logo" }));
|
|
231
|
-
// Render is only needed when you want to hide / fade / reorder; without it the
|
|
232
|
-
// object is still drawn at alpha 1, zIndex 0. Requires RenderSystem registered.
|
|
233
|
-
logo.addComponent(new Render({ zIndex: 5 }));
|
|
234
|
-
// Use addChild — not addGameObject — so transform parent + scene are both wired.
|
|
235
|
-
g.scene.addChild(logo);
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
resource.loadConfig([
|
|
241
|
-
{
|
|
242
|
-
name: "logo",
|
|
243
|
-
type: RESOURCE_TYPE.IMAGE,
|
|
244
|
-
src: { image: { type: "png", url: "logo.png" } },
|
|
245
|
-
preload: true,
|
|
246
|
-
},
|
|
247
|
-
]);
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
`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.
|
|
251
|
-
|
|
252
|
-
## Minimal 3D bootstrap
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
import { Game, GameObject } from "@combos-fun/engine";
|
|
256
|
-
import { Renderer3DSystem } from "@combos-fun/plugin-renderer-3d";
|
|
257
|
-
import {
|
|
258
|
-
Graphics3D,
|
|
259
|
-
Graphics3DSystem,
|
|
260
|
-
} from "@combos-fun/plugin-renderer-3d-graphics";
|
|
261
|
-
|
|
262
|
-
new Game({
|
|
263
|
-
systems: [
|
|
264
|
-
new Renderer3DSystem({
|
|
265
|
-
canvas: document.querySelector("#canvas")!,
|
|
266
|
-
width: 750,
|
|
267
|
-
height: 1000,
|
|
268
|
-
}),
|
|
269
|
-
new Graphics3DSystem(),
|
|
270
|
-
],
|
|
271
|
-
// Safe place to wire the scene graph: every system has finished init.
|
|
272
|
-
onSystemsBootstrapComplete: (g) => {
|
|
273
|
-
// 3D position / rotation / scale go through the component's own params
|
|
274
|
-
// (positionX/Y/Z, rotationX/Y/Z, scaleX/Y/Z) — GameObject's TransformParams
|
|
275
|
-
// is 2D-only (Vector2 + Size2) and is not read by 3D renderers.
|
|
276
|
-
const box = new GameObject("box");
|
|
277
|
-
box.addComponent(
|
|
278
|
-
new Graphics3D({
|
|
279
|
-
shape: "box",
|
|
280
|
-
width: 1,
|
|
281
|
-
height: 1,
|
|
282
|
-
depth: 1,
|
|
283
|
-
color: 0xff0000,
|
|
284
|
-
positionX: 0,
|
|
285
|
-
positionY: 1,
|
|
286
|
-
positionZ: 0,
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
289
|
-
// Use addChild — not addGameObject — so transform parent + scene are both wired.
|
|
290
|
-
g.scene.addChild(box);
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Verification
|
|
296
|
-
|
|
297
|
-
After modifying engine code:
|
|
298
|
-
|
|
299
|
-
1. `pnpm typecheck`
|
|
300
|
-
2. `pnpm run build` (rebuild all packages in dependency order)
|
|
301
|
-
3. `pnpm validate-plugin-manifests:strict`
|
|
302
|
-
4. Run an example app from `examples/` and verify no console errors
|
|
42
|
+
| Symptom | Fix |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Blank canvas | Base renderer system registered; `autoStart` or `game.start()` |
|
|
45
|
+
| Nothing draws | Matching sub-system (e.g. `ImgSystem`) before components |
|
|
46
|
+
| `getSystem` undefined | Pass class, not string |
|
|
47
|
+
| Resource missing / blank sprite | `loadConfig` or `addResource` + `preload` + `LOAD_EVENT.COMPLETE` before use |
|
|
48
|
+
| TS / runtime API guess wrong | Prefer `$combos-engine-development` → `references/api-guessing-cases.md`, not `node_modules` digs |
|
|
303
49
|
|
|
304
50
|
## See also
|
|
305
51
|
|
|
306
|
-
-
|
|
52
|
+
- [`references/public-api.md`](references/public-api.md) — exports, `GameParams`, host lifecycle, `resource` API
|
|
53
|
+
- [`references/bootstrap-examples.md`](references/bootstrap-examples.md) — minimal 2D / 3D samples
|
|
54
|
+
- `@combos-fun/engine/plugin-authoring` — new plugin package spec
|
package/dist/engine.cjs.js
CHANGED
|
@@ -1001,7 +1001,7 @@ class Scene extends GameObject {
|
|
|
1001
1001
|
}
|
|
1002
1002
|
|
|
1003
1003
|
/** Generated at build from package.json */
|
|
1004
|
-
const version = "0.0.
|
|
1004
|
+
const version = "0.0.37";
|
|
1005
1005
|
|
|
1006
1006
|
/**
|
|
1007
1007
|
* Sent to `window.parent` after each `System.init` completes during `Game.addSystem`
|