@combos-fun/plugin-development-tool 0.0.33 → 0.0.34
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 +16 -50
- package/dist/plugin-development-tool.cjs.js +178 -38
- package/dist/plugin-development-tool.cjs.js.map +1 -1
- package/dist/plugin-development-tool.cjs.prod.js +1 -1
- package/dist/plugin-development-tool.d.ts +27 -1
- package/dist/plugin-development-tool.esm.js +178 -38
- package/dist/plugin-development-tool.esm.js.map +1 -1
- package/package.json +6 -5
package/agent-skill.md
CHANGED
|
@@ -29,30 +29,9 @@ import {
|
|
|
29
29
|
outline, and posts `COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED`
|
|
30
30
|
messages to the parent window.
|
|
31
31
|
|
|
32
|
-
### `CombosDevelopmentToolTargetParams`
|
|
33
|
-
|
|
34
|
-
| Field | Type | Notes |
|
|
35
|
-
|---|---|---|
|
|
36
|
-
| `payload` | `Record<string, unknown>?` | Extra JSON-serializable data included in the selected-object snapshot. |
|
|
37
|
-
|
|
38
|
-
Creator source persistence uses the optional `payload.source` convention:
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
go.addComponent(new CombosDevelopmentToolTarget({
|
|
42
|
-
payload: {
|
|
43
|
-
source: {
|
|
44
|
-
file: 'src/scenes/main-scene.ts', // project-relative
|
|
45
|
-
anchor: 'hero', // stable source key; defaults to go.name in snapshots
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Use a stable `file` + `anchor` pair when property edits must resolve the same GameObject after reload. The Target accepts arbitrary JSON-safe payload fields, but only `payload.source.file` and `payload.source.anchor` participate in source lookup.
|
|
52
|
-
|
|
53
32
|
## Required setup
|
|
54
33
|
|
|
55
|
-
This plugin depends on `@combos-fun/plugin-renderer-event` (for taps) and `@combos-fun/plugin-renderer-graphics` (for the outline).
|
|
34
|
+
This plugin depends on `@combos-fun/plugin-renderer-event` (for taps) and `@combos-fun/plugin-renderer-graphics` (for the outline). Register all of them after `RendererSystem`:
|
|
56
35
|
|
|
57
36
|
```ts
|
|
58
37
|
new Game({
|
|
@@ -65,26 +44,6 @@ new Game({
|
|
|
65
44
|
});
|
|
66
45
|
```
|
|
67
46
|
|
|
68
|
-
## Pick-mode behavior
|
|
69
|
-
|
|
70
|
-
- The System starts disabled.
|
|
71
|
-
- While enabled, only GameObjects carrying `CombosDevelopmentToolTarget` receive editor pick bindings.
|
|
72
|
-
- Existing game `Event` hit targets are soft-disabled by setting their renderer containers non-interactive; they are restored when pick mode is disabled.
|
|
73
|
-
- Adding a Target while enabled automatically soft-disables that object's game Event and installs its pick binding through the Component observer.
|
|
74
|
-
- Removing a Target automatically releases its pick binding and clears selection when necessary.
|
|
75
|
-
- Dynamic Target additions and removals do not require `requestSceneRescan()`. A full rescan exists for parent/tooling-driven structural refreshes.
|
|
76
|
-
|
|
77
|
-
## Pick bounds
|
|
78
|
-
|
|
79
|
-
The generated pick Event resolves local bounds in this order:
|
|
80
|
-
|
|
81
|
-
1. A GameObject with `Graphics` uses its own Pixi `Graphics.getLocalBounds()` first.
|
|
82
|
-
2. Other renderers use positive `transform.size` as a `(0, 0, width, height)` box.
|
|
83
|
-
3. The renderer container's local bounds are used as a fallback.
|
|
84
|
-
4. A 1×1 box is the final fallback.
|
|
85
|
-
|
|
86
|
-
Set `transform.size` for `Img`, `Text`, `Sprite`, and `SpriteAnimation` GameObjects so picking matches their displayed box. Graphics GameObjects should draw their actual geometry; the System deliberately prefers rendered Graphics bounds over the Transform size box, including circles and polygons drawn around a local origin.
|
|
87
|
-
|
|
88
47
|
## Common pitfalls
|
|
89
48
|
|
|
90
49
|
| Symptom | Fix |
|
|
@@ -92,7 +51,7 @@ Set `transform.size` for `Img`, `Text`, `Sprite`, and `SpriteAnimation` GameObje
|
|
|
92
51
|
| Selection box never appears | Ensure both `EventSystem` and `GraphicsSystem` are registered |
|
|
93
52
|
| Parent frame never receives messages | The page must be in an iframe / embedded context; `window.parent !== window` |
|
|
94
53
|
| Parent postMessage ignored | Check `allowedMessageOrigins` — defaults allow `knoffice.tech` and `converge.ai` (+ subdomains). Pass `['localhost']` to merge local dev hosts |
|
|
95
|
-
| Selection stuck
|
|
54
|
+
| Selection stuck on stale node | Listen for `COMBOS_DEVELOPMENT_TOOL_REFRESH` to clear |
|
|
96
55
|
|
|
97
56
|
## Inbound postMessage security
|
|
98
57
|
|
|
@@ -134,19 +93,26 @@ triggers / spawn points later) but nothing else is registered yet.
|
|
|
134
93
|
- Independent toggle, orthogonal to pick mode and mute: `setMarkerOverlay(true)` or
|
|
135
94
|
`postMessage({ type: 'combos-development-tool:set-marker-overlay', enabled: true })`
|
|
136
95
|
(also `window` CustomEvent / `game.emit`).
|
|
137
|
-
- Draws
|
|
96
|
+
- Draws the icon **in-engine** (a `Graphics` child GO pinned to the owner, so it
|
|
138
97
|
follows the object). The layer is **purely visual** — it does not wire up its own
|
|
139
98
|
selection. Because the icon adds to the owner's rendered bounds, the owner's own
|
|
140
99
|
`CombosDevelopmentToolTarget` pick gets a clickable hit area, so selecting and
|
|
141
|
-
editing (e.g. `Sound.
|
|
142
|
-
selection → snapshot → `apply-property` flow and **persist to source** like any
|
|
100
|
+
editing (e.g. `Sound.volume`) go through the normal
|
|
101
|
+
selection → snapshot (`fieldDescriptors`) → `apply-property` flow and **persist to source** like any
|
|
143
102
|
other scene edit. **The marked object must therefore carry a
|
|
144
|
-
`CombosDevelopmentToolTarget`** added by game code
|
|
145
|
-
|
|
146
|
-
render nothing.
|
|
147
|
-
marker badges do not overlap.
|
|
103
|
+
`CombosDevelopmentToolTarget`** (added by the game code — see the game template's
|
|
104
|
+
Scene Edit rules; objects with a `Sound` component must be given a Target even
|
|
105
|
+
when they render nothing).
|
|
148
106
|
- On (re)build it posts `combos-development-tool:marker-overlay-success` with per-component counts.
|
|
149
107
|
|
|
108
|
+
## Scene Edit fieldDescriptors
|
|
109
|
+
|
|
110
|
+
Selection snapshots no longer dump raw component `fields`. The host receives only
|
|
111
|
+
schema-backed `fieldDescriptors` derived from `@Field` metadata (`label`,
|
|
112
|
+
`description`, editor hints, min/max/step, etc.). Fields without a non-empty
|
|
113
|
+
`description` are omitted. Custom game Components that should appear in Creator
|
|
114
|
+
must declare `@Field({ label, description, ... })` in the user's language.
|
|
115
|
+
|
|
150
116
|
## Verification
|
|
151
117
|
|
|
152
118
|
`pnpm --filter @combos-fun/plugin-development-tool run build`.
|
|
@@ -5,6 +5,7 @@ var engine = require('@combos-fun/engine');
|
|
|
5
5
|
var pluginRenderer = require('@combos-fun/plugin-renderer');
|
|
6
6
|
var pluginRendererGraphics = require('@combos-fun/plugin-renderer-graphics');
|
|
7
7
|
var pluginRendererEvent = require('@combos-fun/plugin-renderer-event');
|
|
8
|
+
var inspectorDecorator = require('@combos-fun/inspector-decorator');
|
|
8
9
|
|
|
9
10
|
/** Toggle development-tool selection / parent postMessage. Payload: `{ enabled: boolean }`. */
|
|
10
11
|
const COMBOS_DEVELOPMENT_TOOL_SET = 'combos-development-tool:set';
|
|
@@ -201,30 +202,17 @@ function isSceneSourceAnchor(v) {
|
|
|
201
202
|
typeof v.file === 'string');
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
'
|
|
206
|
-
|
|
207
|
-
'
|
|
208
|
-
|
|
209
|
-
'
|
|
210
|
-
|
|
211
|
-
'
|
|
212
|
-
|
|
213
|
-
'
|
|
214
|
-
|
|
215
|
-
const COMPONENT_SKIP_KEYS = {
|
|
216
|
-
Graphics: new Set(['graphics']),
|
|
217
|
-
Physics: new Set([
|
|
218
|
-
'body',
|
|
219
|
-
'Body',
|
|
220
|
-
'PhysicsEngine',
|
|
221
|
-
'World',
|
|
222
|
-
'Constraint',
|
|
223
|
-
'mouseConstraint',
|
|
224
|
-
'bodyParams',
|
|
225
|
-
]),
|
|
226
|
-
Event: new Set(['hitArea']),
|
|
227
|
-
};
|
|
205
|
+
function resolveValueType(value) {
|
|
206
|
+
if (typeof value === 'string')
|
|
207
|
+
return 'string';
|
|
208
|
+
if (typeof value === 'number')
|
|
209
|
+
return 'number';
|
|
210
|
+
if (typeof value === 'boolean')
|
|
211
|
+
return 'boolean';
|
|
212
|
+
if (value !== null && typeof value === 'object')
|
|
213
|
+
return 'object';
|
|
214
|
+
return 'unsupported';
|
|
215
|
+
}
|
|
228
216
|
function cloneJsonSafe(value, depth = 0) {
|
|
229
217
|
if (value === null || value === undefined)
|
|
230
218
|
return value;
|
|
@@ -249,20 +237,171 @@ function cloneJsonSafe(value, depth = 0) {
|
|
|
249
237
|
}
|
|
250
238
|
return undefined;
|
|
251
239
|
}
|
|
240
|
+
function readFieldReflectSchema(ctor) {
|
|
241
|
+
const properties = Reflect.getMetadata(inspectorDecorator.IDE_PROPERTY_METADATA, ctor) ||
|
|
242
|
+
{};
|
|
243
|
+
return Object.keys(properties).map(key => ({
|
|
244
|
+
key,
|
|
245
|
+
...properties[key],
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
/** Legacy `@type` / `@step` write `constructor.IDEProps` as an object keyed by property name. */
|
|
249
|
+
function readLegacyIdePropsSchema(ctor) {
|
|
250
|
+
const raw = ctor.IDEProps;
|
|
251
|
+
if (!raw || Array.isArray(raw) || typeof raw !== 'object') {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
return Object.keys(raw).map(key => {
|
|
255
|
+
const entry = raw[key] || {};
|
|
256
|
+
return {
|
|
257
|
+
key,
|
|
258
|
+
type: typeof entry.type === 'string' ? entry.type : undefined,
|
|
259
|
+
step: typeof entry.step === 'number' ? entry.step : undefined,
|
|
260
|
+
min: typeof entry.min === 'number' ? entry.min : undefined,
|
|
261
|
+
max: typeof entry.max === 'number' ? entry.max : undefined,
|
|
262
|
+
label: typeof entry.label === 'string' ? entry.label : undefined,
|
|
263
|
+
description: typeof entry.description === 'string' ? entry.description : undefined,
|
|
264
|
+
group: typeof entry.group === 'string' ? entry.group : undefined,
|
|
265
|
+
editor: entry.editor,
|
|
266
|
+
enumOptions: Array.isArray(entry.enumOptions) ? entry.enumOptions : undefined,
|
|
267
|
+
unit: typeof entry.unit === 'string' ? entry.unit : undefined,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function collectSchemaProps(comp) {
|
|
272
|
+
const ctor = comp.constructor;
|
|
273
|
+
const fromField = readFieldReflectSchema(ctor);
|
|
274
|
+
if (fromField.length > 0) {
|
|
275
|
+
return fromField;
|
|
276
|
+
}
|
|
277
|
+
return readLegacyIdePropsSchema(ctor);
|
|
278
|
+
}
|
|
279
|
+
function mapEditorKind(options, valueType) {
|
|
280
|
+
if (options.editor) {
|
|
281
|
+
return options.editor;
|
|
282
|
+
}
|
|
283
|
+
if (valueType === 'boolean')
|
|
284
|
+
return 'toggle';
|
|
285
|
+
if (options.enumOptions?.length && valueType === 'string')
|
|
286
|
+
return 'enum';
|
|
287
|
+
if (valueType === 'number') {
|
|
288
|
+
if (options.min !== undefined && options.max !== undefined) {
|
|
289
|
+
return 'number-slider';
|
|
290
|
+
}
|
|
291
|
+
return 'number-stepper';
|
|
292
|
+
}
|
|
293
|
+
if (valueType === 'string')
|
|
294
|
+
return 'text';
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
function buildNumberMeta(options) {
|
|
298
|
+
if (options.min === undefined &&
|
|
299
|
+
options.max === undefined &&
|
|
300
|
+
options.step === undefined &&
|
|
301
|
+
options.unit === undefined) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
min: options.min,
|
|
306
|
+
max: options.max,
|
|
307
|
+
step: options.step,
|
|
308
|
+
unit: options.unit,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function titleCaseLeaf(leaf) {
|
|
312
|
+
return leaf
|
|
313
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
314
|
+
.replace(/_/g, ' ')
|
|
315
|
+
.replace(/^./, char => char.toUpperCase());
|
|
316
|
+
}
|
|
317
|
+
function resolveLabel(options, path) {
|
|
318
|
+
if (options.label?.trim()) {
|
|
319
|
+
return options.label.trim();
|
|
320
|
+
}
|
|
321
|
+
const leaf = path[path.length - 1];
|
|
322
|
+
return leaf ? titleCaseLeaf(leaf) : undefined;
|
|
323
|
+
}
|
|
324
|
+
function leafDescriptorsFromValue(componentName, options, path, value) {
|
|
325
|
+
const description = options.description?.trim();
|
|
326
|
+
if (!description) {
|
|
327
|
+
// Scene Edit only exposes fields that include a human description.
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
const valueType = resolveValueType(value);
|
|
331
|
+
if (valueType === 'unsupported') {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
const typeHint = options.type;
|
|
335
|
+
const isVector = typeHint === 'vector2' ||
|
|
336
|
+
(valueType === 'object' &&
|
|
337
|
+
value !== null &&
|
|
338
|
+
'x' in value &&
|
|
339
|
+
'y' in value);
|
|
340
|
+
const isSize = typeHint === 'size' ||
|
|
341
|
+
(valueType === 'object' &&
|
|
342
|
+
value !== null &&
|
|
343
|
+
'width' in value &&
|
|
344
|
+
'height' in value);
|
|
345
|
+
if (isVector || isSize) {
|
|
346
|
+
const record = value;
|
|
347
|
+
const axes = isSize ? ['width', 'height'] : ['x', 'y'];
|
|
348
|
+
const baseLabel = resolveLabel(options, path) ?? componentName;
|
|
349
|
+
const out = [];
|
|
350
|
+
for (const axis of axes) {
|
|
351
|
+
const axisValue = record[axis];
|
|
352
|
+
const axisType = resolveValueType(axisValue);
|
|
353
|
+
if (axisType === 'unsupported' || axisType === 'object')
|
|
354
|
+
continue;
|
|
355
|
+
const axisPath = [...path, axis];
|
|
356
|
+
out.push({
|
|
357
|
+
path: axisPath,
|
|
358
|
+
value: axisValue,
|
|
359
|
+
valueType: axisType,
|
|
360
|
+
label: `${baseLabel} ${axis.toUpperCase()}`,
|
|
361
|
+
description,
|
|
362
|
+
group: options.group ?? componentName,
|
|
363
|
+
editorKind: mapEditorKind(options, axisType),
|
|
364
|
+
numberMeta: buildNumberMeta(options),
|
|
365
|
+
persistTarget: componentName === 'Transform' ? 'gameObjectConstructor' : 'component',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
if (valueType === 'object') {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
const label = resolveLabel(options, path);
|
|
374
|
+
if (!label) {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
return [
|
|
378
|
+
{
|
|
379
|
+
path,
|
|
380
|
+
value: cloneJsonSafe(value),
|
|
381
|
+
valueType,
|
|
382
|
+
label,
|
|
383
|
+
description,
|
|
384
|
+
group: options.group ?? componentName,
|
|
385
|
+
editorKind: mapEditorKind(options, valueType),
|
|
386
|
+
enumOptions: options.enumOptions,
|
|
387
|
+
numberMeta: buildNumberMeta(options),
|
|
388
|
+
persistTarget: componentName === 'Transform' ? 'gameObjectConstructor' : 'component',
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
}
|
|
252
392
|
function serializeComponent(comp) {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
393
|
+
const componentName = comp.name;
|
|
394
|
+
const schema = collectSchemaProps(comp);
|
|
395
|
+
if (schema.length === 0) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const out = [];
|
|
399
|
+
const record = comp;
|
|
400
|
+
for (const prop of schema) {
|
|
401
|
+
const value = record[prop.key];
|
|
261
402
|
if (typeof value === 'function')
|
|
262
403
|
continue;
|
|
263
|
-
|
|
264
|
-
if (cloned !== undefined)
|
|
265
|
-
out[key] = cloned;
|
|
404
|
+
out.push(...leafDescriptorsFromValue(componentName, prop, [prop.key], value));
|
|
266
405
|
}
|
|
267
406
|
return out;
|
|
268
407
|
}
|
|
@@ -281,8 +420,9 @@ function buildGameObjectSnapshot(go) {
|
|
|
281
420
|
.filter(c => c.name !== 'CombosDevelopmentToolTarget')
|
|
282
421
|
.map(c => ({
|
|
283
422
|
componentName: c.name,
|
|
284
|
-
|
|
285
|
-
}))
|
|
423
|
+
fieldDescriptors: serializeComponent(c),
|
|
424
|
+
}))
|
|
425
|
+
.filter(c => c.fieldDescriptors.length > 0);
|
|
286
426
|
return {
|
|
287
427
|
id: go.id,
|
|
288
428
|
name: go.name,
|
|
@@ -1235,7 +1375,7 @@ var CombosDevelopmentToolSystem = CombosDevelopmentToolSystem$1;
|
|
|
1235
1375
|
/** Auto-generated by scripts/build-package.mjs — do not edit. */
|
|
1236
1376
|
Object.assign(CombosDevelopmentToolSystem, {
|
|
1237
1377
|
packageName: "@combos-fun/plugin-development-tool",
|
|
1238
|
-
packageVersion: "0.0.
|
|
1378
|
+
packageVersion: "0.0.34",
|
|
1239
1379
|
});
|
|
1240
1380
|
|
|
1241
1381
|
exports.COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY = COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY;
|