@combos-fun/engine 0.0.7 → 0.0.8
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 +26 -272
- package/package.json +5 -3
- package/plugin-authoring.md +187 -0
package/agent-skill.md
CHANGED
|
@@ -1,43 +1,27 @@
|
|
|
1
1
|
# `@combos-fun/engine` — Agent notes
|
|
2
2
|
|
|
3
|
-
ECS microkernel for Combos Fun. This file is the canonical knowledge source for
|
|
4
|
-
the engine and the standard for authoring any new `@combos-fun/plugin-*`.
|
|
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.
|
|
5
4
|
|
|
6
5
|
## When to read
|
|
7
6
|
|
|
8
|
-
Read this every time before working on a Combos Fun project. The entry skill
|
|
9
|
-
loads this file as part of "Core" packages, regardless of 2D / 3D mode.
|
|
7
|
+
Read this every time before working on a Combos Fun project. The entry skill loads this file as part of "Core" packages, regardless of 2D / 3D mode.
|
|
10
8
|
|
|
11
9
|
## Microkernel model
|
|
12
10
|
|
|
13
|
-
- **Kernel** `@combos-fun/engine`: `Game`, `Scene`, `GameObject`, `Component`,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **
|
|
17
|
-
`gameObject.addComponent(new Xxx(...))`.
|
|
18
|
-
- **Observer**: `@decorators.componentObserver({ Name: ['prop'] })` on a
|
|
19
|
-
System; drain `this.componentObserver.clear()` in `update()` and switch on
|
|
20
|
-
`OBSERVER_TYPE.ADD | CHANGE | REMOVE`.
|
|
21
|
-
- **Frame order**:
|
|
22
|
-
`Component.update` → `Component.lateUpdate` → `System.update` →
|
|
23
|
-
`System.lateUpdate`. `start()` fires inline on the first `update` tick.
|
|
11
|
+
- **Kernel** `@combos-fun/engine`: `Game`, `Scene`, `GameObject`, `Component`, `System`, `Transform`, `resource`, `decorators`. **No** built-in rendering / physics / audio.
|
|
12
|
+
- **Extension**: `game.addSystem(new XxxSystem(...))` then `gameObject.addComponent(new Xxx(...))`.
|
|
13
|
+
- **Observer**: `@decorators.componentObserver({ Name: ['prop'] })` on a System; drain `this.componentObserver.clear()` in `update()` and switch on `OBSERVER_TYPE.ADD | CHANGE | REMOVE`.
|
|
14
|
+
- **Frame order**: `Component.update` → `Component.lateUpdate` → `System.update` → `System.lateUpdate`. `start()` fires inline on the first `update` tick.
|
|
24
15
|
|
|
25
16
|
## Public API
|
|
26
17
|
|
|
27
18
|
### Values
|
|
28
19
|
|
|
29
|
-
`Game`, `Scene`, `GameObject`, `Component`, `System`, `Transform`,
|
|
30
|
-
`resource`, `resourceLoader`, `decorators`, `IDEProp`,
|
|
31
|
-
`componentObserver`, `LOAD_EVENT`, `RESOURCE_TYPE`, `OBSERVER_TYPE`,
|
|
32
|
-
`LOAD_SCENE_MODE`, `RESOURCE_TYPE_STRATEGY`, `version`,
|
|
33
|
-
`COMBOS_GAME_PLUGIN_INIT_SUCCESS`, `postParentPluginInitSuccess`.
|
|
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`, `postParentPluginInitSuccess`.
|
|
34
21
|
|
|
35
22
|
### Types
|
|
36
23
|
|
|
37
|
-
`GameParams`, `PluginStruct`, `TransformParams`, `ComponentChanged`,
|
|
38
|
-
`UpdateParams`, `ComponentParams`, `ObserverInfo`, `PureObserverInfo`,
|
|
39
|
-
`ResourceBase`, `SystemConstructor`,
|
|
40
|
-
`CombosGamePluginInitSuccessMessage`.
|
|
24
|
+
`GameParams`, `PluginStruct`, `TransformParams`, `ComponentChanged`, `UpdateParams`, `ComponentParams`, `ObserverInfo`, `PureObserverInfo`, `ResourceBase`, `SystemConstructor`, `CombosGamePluginInitSuccessMessage`.
|
|
41
25
|
|
|
42
26
|
### `GameParams`
|
|
43
27
|
|
|
@@ -52,11 +36,7 @@ loads this file as part of "Core" packages, regardless of 2D / 3D mode.
|
|
|
52
36
|
|
|
53
37
|
### `Game` methods
|
|
54
38
|
|
|
55
|
-
`addSystem(system)`, `removeSystem(system | class | string)` (calls
|
|
56
|
-
`system.destroy()` internally), `getSystem(class | string)`,
|
|
57
|
-
`loadScene({ scene, mode?, params? })`
|
|
58
|
-
(`LOAD_SCENE_MODE.SINGLE | MULTI_CANVAS`), `start()`, `pause()`,
|
|
59
|
-
`resume()`, `destroy()`.
|
|
39
|
+
`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()`, `destroy()`.
|
|
60
40
|
|
|
61
41
|
### `resource` singleton
|
|
62
42
|
|
|
@@ -75,55 +55,31 @@ Fields: `timeout` (6000ms), `resourcesMap`, `progress`.
|
|
|
75
55
|
|
|
76
56
|
## ECS hard rules (mandatory)
|
|
77
57
|
|
|
78
|
-
These rules apply to every code change. Violations must be fixed before the
|
|
79
|
-
change is considered complete.
|
|
58
|
+
These rules apply to every code change. Violations must be fixed before the change is considered complete.
|
|
80
59
|
|
|
81
60
|
### Rule 1: All entity state lives in Components
|
|
82
61
|
|
|
83
|
-
Any data belonging to a game entity must be a field on a `Component`
|
|
84
|
-
attached to a `GameObject`. No module-scope variables, closure captures,
|
|
85
|
-
or shadow stores holding entity state. **Test:** if you `destroy()` a
|
|
86
|
-
`GameObject`, does all its state disappear? If anything survives in a
|
|
87
|
-
closure or module variable, violation.
|
|
62
|
+
Any data belonging to a game entity must be a field on a `Component` attached to a `GameObject`. No module-scope variables, closure captures, or shadow stores holding entity state. **Test:** if you `destroy()` a `GameObject`, does all its state disappear? If anything survives in a closure or module variable, violation.
|
|
88
63
|
|
|
89
64
|
### Rule 2: All behaviour logic lives in Component / System hooks
|
|
90
65
|
|
|
91
|
-
Code that runs per-frame, reacts to events, or mutates entity state must
|
|
92
|
-
live in `Component` hooks (`init` / `awake` / `start` / `update` /
|
|
93
|
-
`lateUpdate` / `onPause` / `onResume` / `onDestroy`) or `System` hooks
|
|
94
|
-
(same set + `componentObserver` in `update`). No event callbacks in
|
|
95
|
-
bootstrap, no `setInterval` outside Components, no `game.ticker.add` for
|
|
96
|
-
game logic.
|
|
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.
|
|
97
67
|
|
|
98
68
|
### Rule 3: Bootstrap code only wires, never implements
|
|
99
69
|
|
|
100
|
-
The entry file may: register resources, create `Game` with `systems`,
|
|
101
|
-
create `GameObject`s + attach `Component`s, add to scene, call
|
|
102
|
-
`game.start()`. The entry file must **not**: define event handlers,
|
|
103
|
-
call component methods (`spriteAnim.play()`), hold component references
|
|
104
|
-
for later use, or contain `if` / `switch` / loop game logic. When you
|
|
105
|
-
need interaction logic, create a custom Component whose `start()` wires
|
|
106
|
-
it and whose `onDestroy()` cleans it up.
|
|
70
|
+
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.
|
|
107
71
|
|
|
108
72
|
### Rule 4: Single concern per Component, cross-entity logic in Systems
|
|
109
73
|
|
|
110
|
-
A Component holds data + self-contained behaviour for one concern on one
|
|
111
|
-
entity. A System holds cross-entity logic reacting to component changes.
|
|
112
|
-
Do not put multi-entity coordination inside a Component.
|
|
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.
|
|
113
75
|
|
|
114
76
|
### Rule 5: No direct Pixi / Three.js / DOM manipulation outside Renderer pattern
|
|
115
77
|
|
|
116
|
-
2D display objects must come through `plugin-renderer-*` systems or a
|
|
117
|
-
custom `Renderer` subclass registered via `rendererManager.register(this)`.
|
|
118
|
-
3D objects must come through `plugin-renderer-3d-*` systems or a custom
|
|
119
|
-
`Renderer3D` subclass. No raw `new PIXI.Sprite(...)` / `new THREE.Mesh(...)`
|
|
120
|
-
in bootstrap or plain Components.
|
|
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.
|
|
121
79
|
|
|
122
80
|
### Rule 6: Component configuration via params, not imperative calls
|
|
123
81
|
|
|
124
|
-
Configure Components through constructor params (e.g. `autoPlay: true`).
|
|
125
|
-
If behaviour must be triggered at runtime, do it from another Component's
|
|
126
|
-
lifecycle hook, not from bootstrap code.
|
|
82
|
+
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.
|
|
127
83
|
|
|
128
84
|
## Decision flowchart
|
|
129
85
|
|
|
@@ -142,26 +98,20 @@ Runs only once at startup to wire things?
|
|
|
142
98
|
|
|
143
99
|
Run after every change:
|
|
144
100
|
|
|
145
|
-
- [ ] No game logic in entry file — only resource registration, `Game` /
|
|
146
|
-
|
|
147
|
-
- [ ] No
|
|
148
|
-
`addComponent()` returns for later use
|
|
149
|
-
- [ ] No anonymous event handlers — all `.on(...)` /
|
|
150
|
-
`addEventListener(...)` inside Component lifecycle hooks with cleanup
|
|
151
|
-
in `onDestroy`
|
|
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`
|
|
152
104
|
- [ ] No module-scope mutable state holding entity data
|
|
153
105
|
- [ ] All entity state accessible via `gameObject.getComponent(...)`
|
|
154
106
|
- [ ] All per-frame logic in Component / System `update` / `lateUpdate`
|
|
155
107
|
- [ ] Custom behaviour = named Component class with `static componentName`
|
|
156
108
|
- [ ] Every event subscription has cleanup in `onDestroy`
|
|
157
109
|
- [ ] Systems use `@decorators.componentObserver` and drain in `update()`
|
|
158
|
-
- [ ] No direct Pixi / Three.js manipulation outside `Renderer` /
|
|
159
|
-
`Renderer3D` subclasses
|
|
110
|
+
- [ ] No direct Pixi / Three.js manipulation outside `Renderer` / `Renderer3D` subclasses
|
|
160
111
|
|
|
161
112
|
## Inventory (mandatory after every task)
|
|
162
113
|
|
|
163
|
-
After every development task, write or update three inventory tables in the
|
|
164
|
-
**project memory**:
|
|
114
|
+
After every development task, write or update three inventory tables in the **project memory**:
|
|
165
115
|
|
|
166
116
|
**Component inventory** — for each custom Component:
|
|
167
117
|
|
|
@@ -192,206 +142,6 @@ After every development task, write or update three inventory tables in the
|
|
|
192
142
|
|
|
193
143
|
Skipping inventory is treated with the same weight as the ECS hard rules.
|
|
194
144
|
|
|
195
|
-
## Plugin authoring (the standard for `@combos-fun/plugin-*`)
|
|
196
|
-
|
|
197
|
-
A plugin is `Component(s)` + optional `System(s)` + optional `Renderer` /
|
|
198
|
-
`Renderer3D` subclass, shipped as an npm package or local package under the
|
|
199
|
-
`@combos-fun/` namespace.
|
|
200
|
-
|
|
201
|
-
### Required package layout
|
|
202
|
-
|
|
203
|
-
```
|
|
204
|
-
packages/<plugin-name>/
|
|
205
|
-
package.json
|
|
206
|
-
combos-plugin.json # machine-readable manifest (see schemas/combos-plugin.schema.json)
|
|
207
|
-
agent-skill.md # per-package agent notes
|
|
208
|
-
README.md # human-facing docs
|
|
209
|
-
index.js # CJS entry
|
|
210
|
-
lib/ # TypeScript sources
|
|
211
|
-
dist/ # built CJS / ESM / .d.ts (created by build-package.mjs)
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Required `package.json` fields
|
|
215
|
-
|
|
216
|
-
```json
|
|
217
|
-
{
|
|
218
|
-
"name": "@combos-fun/plugin-xxx",
|
|
219
|
-
"files": ["dist", "index.js", "agent-skill.md", "combos-plugin.json"],
|
|
220
|
-
"exports": {
|
|
221
|
-
".": "./dist/plugin-xxx.esm.js",
|
|
222
|
-
"./plugin-manifest": "./combos-plugin.json",
|
|
223
|
-
"./agent-skill": "./agent-skill.md"
|
|
224
|
-
},
|
|
225
|
-
"combos": {
|
|
226
|
-
"pluginManifest": "./combos-plugin.json"
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
The `./plugin-manifest` and `./agent-skill` subpaths are part of the public
|
|
232
|
-
contract. External Agents resolve documentation via these stable subpaths.
|
|
233
|
-
Renaming the underlying files without updating `exports` is a breaking
|
|
234
|
-
change.
|
|
235
|
-
|
|
236
|
-
### Component subclass
|
|
237
|
-
|
|
238
|
-
Subclass `Component` from `@combos-fun/engine`. Set
|
|
239
|
-
`static componentName = 'MyFeature'`.
|
|
240
|
-
|
|
241
|
-
Lifecycle (all optional):
|
|
242
|
-
|
|
243
|
-
| Hook | When |
|
|
244
|
-
|------|------|
|
|
245
|
-
| `init(params?)` | During construction |
|
|
246
|
-
| `awake()` | When added to a GameObject |
|
|
247
|
-
| `start()` | Inline on first `update` tick (same frame as first `update`) |
|
|
248
|
-
| `update(frame)` | Every frame (`frame.deltaTime` in ms) |
|
|
249
|
-
| `lateUpdate(frame)` | After all components' `update`, before any `System.update` |
|
|
250
|
-
| `onPause()` / `onResume()` | Game pause / resume |
|
|
251
|
-
| `onDestroy()` | Component or GameObject destroyed |
|
|
252
|
-
|
|
253
|
-
`UpdateParams`: `deltaTime`, `frameCount`, `time`, `currentTime`, `fps`.
|
|
254
|
-
|
|
255
|
-
### System subclass
|
|
256
|
-
|
|
257
|
-
Subclass `System`. Set `static systemName = 'MyFeatureSystem'`.
|
|
258
|
-
|
|
259
|
-
Decorate with `@decorators.componentObserver({ MyFeature: ['power'] })`:
|
|
260
|
-
|
|
261
|
-
- `[]` (empty array) → ADD / REMOVE only
|
|
262
|
-
- `['prop']` → CHANGE on set
|
|
263
|
-
- `{ prop: ['a','b'], deep: true }` → deep change watching
|
|
264
|
-
|
|
265
|
-
In `update()`:
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
const changes = this.componentObserver.clear();
|
|
269
|
-
for (const c of changes) {
|
|
270
|
-
switch (c.type) {
|
|
271
|
-
case OBSERVER_TYPE.ADD: /* c.component, c.gameObject */ break;
|
|
272
|
-
case OBSERVER_TYPE.CHANGE: /* c.prop?: { deep, prop: string[] } */ break;
|
|
273
|
-
case OBSERVER_TYPE.REMOVE: break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
System lifecycle is the same as Component (`init` can be async). `init`
|
|
279
|
-
receives constructor params; `this.game` is available.
|
|
280
|
-
|
|
281
|
-
`System.destroy()` nulls internals and calls `onDestroy()` but **does not**
|
|
282
|
-
remove the system from Game. Always call `game.removeSystem(system)`, which
|
|
283
|
-
calls `destroy()` internally.
|
|
284
|
-
|
|
285
|
-
### `ComponentChanged` shape
|
|
286
|
-
|
|
287
|
-
`type: OBSERVER_TYPE`, `component`, `componentName`, `gameObject`,
|
|
288
|
-
`prop?: { deep, prop: string[] }`.
|
|
289
|
-
|
|
290
|
-
### 2D Pixi `Renderer` subclass (`@combos-fun/plugin-renderer`)
|
|
291
|
-
|
|
292
|
-
Extend `Renderer` from `plugin-renderer` with `@decorators.componentObserver`.
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
class MyRenderer extends Renderer {
|
|
296
|
-
init() {
|
|
297
|
-
this.rendererSystem = this.game.getSystem(RendererSystem);
|
|
298
|
-
this.rendererSystem.rendererManager.register(this);
|
|
299
|
-
}
|
|
300
|
-
componentChanged(changed: ComponentChanged) {
|
|
301
|
-
const container = this.rendererSystem.containerManager.getContainer(
|
|
302
|
-
changed.gameObject.id,
|
|
303
|
-
);
|
|
304
|
-
switch (changed.type) {
|
|
305
|
-
case OBSERVER_TYPE.ADD: /* create pixi object, container.addChild(obj) */ break;
|
|
306
|
-
case OBSERVER_TYPE.CHANGE: /* mutate */ break;
|
|
307
|
-
case OBSERVER_TYPE.REMOVE: /* destroy + container.removeChild */ break;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
rendererUpdate(gameObject) {
|
|
311
|
-
/* per-frame sync */
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
`RendererSystem` must be added before any system calling
|
|
317
|
-
`getSystem(RendererSystem)` in `init`.
|
|
318
|
-
|
|
319
|
-
### 3D Three.js `Renderer3D` subclass (`@combos-fun/plugin-renderer-3d`)
|
|
320
|
-
|
|
321
|
-
Extend `Renderer3D` with `@decorators.componentObserver`.
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
class MyRenderer3D extends Renderer3D {
|
|
325
|
-
init() {
|
|
326
|
-
this.rendererSystem = this.game.getSystem(Renderer3DSystem);
|
|
327
|
-
this.rendererSystem.rendererManager.register(this);
|
|
328
|
-
}
|
|
329
|
-
componentChanged(changed) {
|
|
330
|
-
const scene = this.threeContext.scene;
|
|
331
|
-
/* create / mutate / dispose THREE.Object3D */
|
|
332
|
-
}
|
|
333
|
-
rendererUpdate(gameObject) { /* per-frame */ }
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
`Renderer3DSystem` must be added before any system calling
|
|
338
|
-
`getSystem(Renderer3DSystem)` in `init`.
|
|
339
|
-
|
|
340
|
-
**Async loading pattern** (3D only): use the inherited `increaseAsyncId(id)`
|
|
341
|
-
before async work and `validateAsyncId(id, asyncId)` after each `await` to
|
|
342
|
-
cancel stale operations when the component is removed mid-load.
|
|
343
|
-
|
|
344
|
-
### `combos-plugin.json` (manifest)
|
|
345
|
-
|
|
346
|
-
Validate against `schemas/combos-plugin.schema.json`. Required fields:
|
|
347
|
-
`name`, `pluginId`, `category`, `dimension`, `agentSkill`. For non-core
|
|
348
|
-
packages, `category` ∈ {`rendering`, `physics`, `audio`, `input`, `ui`,
|
|
349
|
-
`a11y`, `animation`, `devtool`, `other`} and `dimension` ∈ {`2d`, `3d`,
|
|
350
|
-
`shared`}.
|
|
351
|
-
|
|
352
|
-
### `agent-skill.md` (per-plugin notes)
|
|
353
|
-
|
|
354
|
-
Use this template:
|
|
355
|
-
|
|
356
|
-
```md
|
|
357
|
-
# <plugin-name> — Agent notes
|
|
358
|
-
|
|
359
|
-
## When to read
|
|
360
|
-
<!-- which tasks should load this -->
|
|
361
|
-
|
|
362
|
-
## Public API
|
|
363
|
-
<!-- exported components / systems / params -->
|
|
364
|
-
|
|
365
|
-
## Required setup
|
|
366
|
-
<!-- system registration order, dependencies -->
|
|
367
|
-
|
|
368
|
-
## Runtime behaviour
|
|
369
|
-
<!-- lifecycle interactions, frame-order specifics -->
|
|
370
|
-
|
|
371
|
-
## Common pitfalls
|
|
372
|
-
<!-- blank canvas, missing systems, async stale, etc -->
|
|
373
|
-
|
|
374
|
-
## Minimal example
|
|
375
|
-
<!-- shortest copy-pasteable snippet -->
|
|
376
|
-
|
|
377
|
-
## Verification
|
|
378
|
-
<!-- how to run / test the plugin after changes -->
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
### Build & publish requirements
|
|
382
|
-
|
|
383
|
-
- Build: CJS / ESM + `.d.ts` to `dist/` via
|
|
384
|
-
`node ../../scripts/build-package.mjs`.
|
|
385
|
-
- Peer / runtime deps: `@combos-fun/engine`, optionally `plugin-renderer` +
|
|
386
|
-
`pixi.js` (for 2D renderer plugins) or `plugin-renderer-3d` + `three` (for
|
|
387
|
-
3D renderer plugins).
|
|
388
|
-
- `package.json`: include `agent-skill.md` and `combos-plugin.json` in
|
|
389
|
-
`files`; expose `./plugin-manifest` and `./agent-skill` in `exports`.
|
|
390
|
-
- Run `pnpm validate-plugin-manifests --strict` and
|
|
391
|
-
`pnpm plugin-index:check` before publishing. The publish script does
|
|
392
|
-
this automatically.
|
|
393
|
-
- Duplicate registration of the same System class is warned and skipped.
|
|
394
|
-
|
|
395
145
|
## Common pitfalls
|
|
396
146
|
|
|
397
147
|
| Symptom | Fix |
|
|
@@ -442,10 +192,14 @@ new Game({
|
|
|
442
192
|
|
|
443
193
|
## Verification
|
|
444
194
|
|
|
445
|
-
After modifying engine code
|
|
195
|
+
After modifying engine code:
|
|
446
196
|
|
|
447
197
|
1. `pnpm typecheck`
|
|
448
198
|
2. `pnpm run build` (rebuild all packages in dependency order)
|
|
449
199
|
3. `pnpm validate-plugin-manifests` (warn during migration, strict at GA)
|
|
450
200
|
4. `pnpm plugin-index:check`
|
|
451
201
|
5. Run an example app from `examples/` and verify no console errors
|
|
202
|
+
|
|
203
|
+
## See also
|
|
204
|
+
|
|
205
|
+
- `@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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@combos-fun/engine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "@combos-fun/engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "dist/engine.esm.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"index.js",
|
|
11
11
|
"dist",
|
|
12
12
|
"agent-skill.md",
|
|
13
|
+
"plugin-authoring.md",
|
|
13
14
|
"combos-plugin.json"
|
|
14
15
|
],
|
|
15
16
|
"exports": {
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
"types": "./dist/engine.d.ts"
|
|
20
21
|
},
|
|
21
22
|
"./plugin-manifest": "./combos-plugin.json",
|
|
22
|
-
"./agent-skill": "./agent-skill.md"
|
|
23
|
+
"./agent-skill": "./agent-skill.md",
|
|
24
|
+
"./plugin-authoring": "./plugin-authoring.md"
|
|
23
25
|
},
|
|
24
26
|
"combos": {
|
|
25
27
|
"pluginManifest": "./combos-plugin.json"
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
"lodash-es": "^4.17.21",
|
|
36
38
|
"resource-loader": "^4.0.0-rc4",
|
|
37
39
|
"sprite-timeline": "^1.10.2",
|
|
38
|
-
"@combos-fun/inspector-decorator": "0.0.
|
|
40
|
+
"@combos-fun/inspector-decorator": "0.0.8"
|
|
39
41
|
},
|
|
40
42
|
"scripts": {
|
|
41
43
|
"build": "node ../../scripts/build-package.mjs"
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# `@combos-fun/engine` — Plugin authoring spec
|
|
2
|
+
|
|
3
|
+
This file is the standard for authoring any new `@combos-fun/plugin-*` package — npm-published or project-local. Read it only when you are **creating** a plugin. Day-to-day engine usage is covered in `@combos-fun/engine/agent-skill`.
|
|
4
|
+
|
|
5
|
+
A plugin is `Component(s)` + optional `System(s)` + optional `Renderer` / `Renderer3D` subclass, shipped under the `@combos-fun/` namespace as an npm package or as a folder inside a user project.
|
|
6
|
+
|
|
7
|
+
## When to read
|
|
8
|
+
|
|
9
|
+
- You are adding a new feature that does not match any existing `@combos-fun/plugin-*` keyword.
|
|
10
|
+
- You are publishing a new plugin to npm under `@combos-fun/*`.
|
|
11
|
+
- You are forking / extending an existing plugin's `Renderer` / `Renderer3D` subclass.
|
|
12
|
+
|
|
13
|
+
For pure consumption of existing plugins, this file is not required — read `@combos-fun/engine/agent-skill` plus each plugin's `agent-skill` instead.
|
|
14
|
+
|
|
15
|
+
## Required package layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
packages/<plugin-name>/
|
|
19
|
+
package.json
|
|
20
|
+
combos-plugin.json # machine-readable manifest (validated against schemas/combos-plugin.schema.json)
|
|
21
|
+
agent-skill.md # per-package agent notes
|
|
22
|
+
README.md # human-facing docs
|
|
23
|
+
index.js # CJS entry
|
|
24
|
+
lib/ # TypeScript sources
|
|
25
|
+
dist/ # built CJS / ESM / .d.ts (created by build-package.mjs)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Project-local plugins (not published to npm) may skip `dist/`, the build step, and `combos-plugin.json` — but adopting both unlocks the same selection logic the entry skill uses for official plugins.
|
|
29
|
+
|
|
30
|
+
## Required `package.json` fields
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"name": "@combos-fun/plugin-xxx",
|
|
35
|
+
"files": ["dist", "index.js", "agent-skill.md", "combos-plugin.json"],
|
|
36
|
+
"exports": {
|
|
37
|
+
".": "./dist/plugin-xxx.esm.js",
|
|
38
|
+
"./plugin-manifest": "./combos-plugin.json",
|
|
39
|
+
"./agent-skill": "./agent-skill.md"
|
|
40
|
+
},
|
|
41
|
+
"combos": {
|
|
42
|
+
"pluginManifest": "./combos-plugin.json"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The `./plugin-manifest` and `./agent-skill` subpaths are part of the public contract. External Agents resolve documentation via these stable subpaths. Renaming the underlying files without updating `exports` is a breaking change.
|
|
48
|
+
|
|
49
|
+
## Component subclass
|
|
50
|
+
|
|
51
|
+
Subclass `Component` from `@combos-fun/engine`. Set `static componentName = 'MyFeature'`.
|
|
52
|
+
|
|
53
|
+
Lifecycle (all optional):
|
|
54
|
+
|
|
55
|
+
| Hook | When |
|
|
56
|
+
|------|------|
|
|
57
|
+
| `init(params?)` | During construction |
|
|
58
|
+
| `awake()` | When added to a GameObject |
|
|
59
|
+
| `start()` | Inline on first `update` tick (same frame as first `update`) |
|
|
60
|
+
| `update(frame)` | Every frame (`frame.deltaTime` in ms) |
|
|
61
|
+
| `lateUpdate(frame)` | After all components' `update`, before any `System.update` |
|
|
62
|
+
| `onPause()` / `onResume()` | Game pause / resume |
|
|
63
|
+
| `onDestroy()` | Component or GameObject destroyed |
|
|
64
|
+
|
|
65
|
+
`UpdateParams`: `deltaTime`, `frameCount`, `time`, `currentTime`, `fps`.
|
|
66
|
+
|
|
67
|
+
## System subclass
|
|
68
|
+
|
|
69
|
+
Subclass `System`. Set `static systemName = 'MyFeatureSystem'`.
|
|
70
|
+
|
|
71
|
+
Decorate with `@decorators.componentObserver({ MyFeature: ['power'] })`:
|
|
72
|
+
|
|
73
|
+
- `[]` (empty array) → ADD / REMOVE only
|
|
74
|
+
- `['prop']` → CHANGE on set
|
|
75
|
+
- `{ prop: ['a','b'], deep: true }` → deep change watching
|
|
76
|
+
|
|
77
|
+
In `update()`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const changes = this.componentObserver.clear();
|
|
81
|
+
for (const c of changes) {
|
|
82
|
+
switch (c.type) {
|
|
83
|
+
case OBSERVER_TYPE.ADD: /* c.component, c.gameObject */ break;
|
|
84
|
+
case OBSERVER_TYPE.CHANGE: /* c.prop?: { deep, prop: string[] } */ break;
|
|
85
|
+
case OBSERVER_TYPE.REMOVE: break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
System lifecycle is the same as Component (`init` can be async). `init` receives constructor params; `this.game` is available.
|
|
91
|
+
|
|
92
|
+
`System.destroy()` nulls internals and calls `onDestroy()` but **does not** remove the system from Game. Always call `game.removeSystem(system)`, which calls `destroy()` internally.
|
|
93
|
+
|
|
94
|
+
### `ComponentChanged` shape
|
|
95
|
+
|
|
96
|
+
`type: OBSERVER_TYPE`, `component`, `componentName`, `gameObject`, `prop?: { deep, prop: string[] }`.
|
|
97
|
+
|
|
98
|
+
## 2D Pixi `Renderer` subclass (`@combos-fun/plugin-renderer`)
|
|
99
|
+
|
|
100
|
+
Extend `Renderer` from `plugin-renderer` with `@decorators.componentObserver`.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
class MyRenderer extends Renderer {
|
|
104
|
+
init() {
|
|
105
|
+
this.rendererSystem = this.game.getSystem(RendererSystem);
|
|
106
|
+
this.rendererSystem.rendererManager.register(this);
|
|
107
|
+
}
|
|
108
|
+
componentChanged(changed: ComponentChanged) {
|
|
109
|
+
const container = this.rendererSystem.containerManager.getContainer(
|
|
110
|
+
changed.gameObject.id,
|
|
111
|
+
);
|
|
112
|
+
switch (changed.type) {
|
|
113
|
+
case OBSERVER_TYPE.ADD: /* create pixi object, container.addChild(obj) */ break;
|
|
114
|
+
case OBSERVER_TYPE.CHANGE: /* mutate */ break;
|
|
115
|
+
case OBSERVER_TYPE.REMOVE: /* destroy + container.removeChild */ break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
rendererUpdate(gameObject) {
|
|
119
|
+
/* per-frame sync */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`RendererSystem` must be added before any system calling `getSystem(RendererSystem)` in `init`.
|
|
125
|
+
|
|
126
|
+
## 3D Three.js `Renderer3D` subclass (`@combos-fun/plugin-renderer-3d`)
|
|
127
|
+
|
|
128
|
+
Extend `Renderer3D` with `@decorators.componentObserver`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
class MyRenderer3D extends Renderer3D {
|
|
132
|
+
init() {
|
|
133
|
+
this.rendererSystem = this.game.getSystem(Renderer3DSystem);
|
|
134
|
+
this.rendererSystem.rendererManager.register(this);
|
|
135
|
+
}
|
|
136
|
+
componentChanged(changed) {
|
|
137
|
+
const scene = this.threeContext.scene;
|
|
138
|
+
/* create / mutate / dispose THREE.Object3D */
|
|
139
|
+
}
|
|
140
|
+
rendererUpdate(gameObject) { /* per-frame */ }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`Renderer3DSystem` must be added before any system calling `getSystem(Renderer3DSystem)` in `init`.
|
|
145
|
+
|
|
146
|
+
**Async loading pattern** (3D only): use the inherited `increaseAsyncId(id)` before async work and `validateAsyncId(id, asyncId)` after each `await` to cancel stale operations when the component is removed mid-load.
|
|
147
|
+
|
|
148
|
+
## `combos-plugin.json` (manifest)
|
|
149
|
+
|
|
150
|
+
Validate against `schemas/combos-plugin.schema.json`. Required fields: `name`, `pluginId`, `category`, `dimension`, `agentSkill`. For non-core packages, `category` ∈ {`rendering`, `physics`, `audio`, `input`, `ui`, `a11y`, `animation`, `devtool`, `other`} and `dimension` ∈ {`2d`, `3d`, `shared`}.
|
|
151
|
+
|
|
152
|
+
## `agent-skill.md` template (per-plugin notes)
|
|
153
|
+
|
|
154
|
+
```md
|
|
155
|
+
# <plugin-name> — Agent notes
|
|
156
|
+
|
|
157
|
+
## When to read
|
|
158
|
+
<!-- which tasks should load this -->
|
|
159
|
+
|
|
160
|
+
## Public API
|
|
161
|
+
<!-- exported components / systems / params -->
|
|
162
|
+
|
|
163
|
+
## Required setup
|
|
164
|
+
<!-- system registration order, dependencies -->
|
|
165
|
+
|
|
166
|
+
## Runtime behaviour
|
|
167
|
+
<!-- lifecycle interactions, frame-order specifics -->
|
|
168
|
+
|
|
169
|
+
## Common pitfalls
|
|
170
|
+
<!-- blank canvas, missing systems, async stale, etc -->
|
|
171
|
+
|
|
172
|
+
## Minimal example
|
|
173
|
+
<!-- shortest copy-pasteable snippet -->
|
|
174
|
+
|
|
175
|
+
## Verification
|
|
176
|
+
<!-- how to run / test the plugin after changes -->
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Build & publish requirements (npm-published plugins only)
|
|
180
|
+
|
|
181
|
+
- Build: CJS / ESM + `.d.ts` to `dist/` via `node ../../scripts/build-package.mjs`.
|
|
182
|
+
- Peer / runtime deps: `@combos-fun/engine`, optionally `plugin-renderer` + `pixi.js` (for 2D renderer plugins) or `plugin-renderer-3d` + `three` (for 3D renderer plugins).
|
|
183
|
+
- `package.json`: include `agent-skill.md` and `combos-plugin.json` in `files`; expose `./plugin-manifest` and `./agent-skill` in `exports`.
|
|
184
|
+
- Run `pnpm validate-plugin-manifests --strict` and `pnpm plugin-index:check` before publishing. The publish script does this automatically.
|
|
185
|
+
- Duplicate registration of the same System class is warned and skipped.
|
|
186
|
+
|
|
187
|
+
Project-local plugins skip all of this: no build, no manifest, no schema validation, no exports, no publish. The Component / System / Renderer code itself is identical.
|