@combos-fun/plugin-development-tool 0.0.26 → 0.0.28

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.
@@ -1,4 +1,4 @@
1
- import { Component, ComponentParams, System, UpdateParams, ComponentChanged, GameObject } from '@combos-fun/engine';
1
+ import { GameObject, Component, ComponentParams, System, UpdateParams, ComponentChanged } from '@combos-fun/engine';
2
2
 
3
3
  /** Toggle development-tool selection / parent postMessage. Payload: `{ enabled: boolean }`. */
4
4
  declare const COMBOS_DEVELOPMENT_TOOL_SET: "combos-development-tool:set";
@@ -16,12 +16,81 @@ declare const COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED: "combos-development
16
16
  declare const COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY: "combos-development-tool:apply-property";
17
17
  /** iframe → parent: game scene bootstrap finished; parent may enable pick mode. */
18
18
  declare const COMBOS_DEVELOPMENT_TOOL_READY: "combos-development-tool:ready";
19
- /** Parent → iframe: pause or resume the game loop. Payload: `{ paused: boolean }`. */
20
- declare const COMBOS_DEVELOPMENT_TOOL_SET_PAUSED: "combos-development-tool:set-paused";
21
- /** Parent → iframe: mute or unmute audio when `SoundSystem` is registered. Payload: `{ muted: boolean }`. */
19
+ /**
20
+ * Parent → iframe: mute or unmute audio when `SoundSystem` is registered. Payload: `{ muted: boolean }`.
21
+ *
22
+ * Note: play / pause / resume are owned by the engine core lifecycle protocol
23
+ * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
24
+ */
22
25
  declare const COMBOS_DEVELOPMENT_TOOL_SET_MUTED: "combos-development-tool:set-muted";
23
- /** iframe → parent (and `game.emit`): current pause/mute snapshot after a successful change. */
26
+ /** iframe → parent (and `game.emit`): current mute snapshot after a successful change. */
24
27
  declare const COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED: "combos-development-tool:state-changed";
28
+ /**
29
+ * Parent → iframe (also `window` CustomEvent / `game.emit`): toggle the marker
30
+ * overlay — a persistent, clickable icon on every object owning an otherwise
31
+ * invisible registered component (Sound first). Independent of pick mode and
32
+ * mute. Payload: `{ enabled: boolean }`.
33
+ */
34
+ declare const COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY: "combos-development-tool:set-marker-overlay";
35
+ /**
36
+ * iframe → parent (and `game.emit`): marker overlay finished (re)building.
37
+ * Payload: `{ enabled: boolean, total: number, markers: { componentName, count }[] }`.
38
+ */
39
+ declare const COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS: "combos-development-tool:marker-overlay-success";
40
+
41
+ /**
42
+ * Marker overlay = a reusable "pin a persistent, clickable icon on every object
43
+ * that owns an otherwise-invisible component" capability. Audio (`Sound`) is the
44
+ * first consumer; register more component kinds below (Physics, Camera, Trigger,
45
+ * timers, spawn points, …) to reuse the exact same layer.
46
+ */
47
+ /** Glyph drawn inside a marker badge. Extend as new component kinds are added. */
48
+ type MarkerGlyph = 'speaker' | 'dot';
49
+ /** One kind of invisible component the overlay surfaces with a pinned icon. */
50
+ interface MarkerComponentDef {
51
+ /** Engine component name to look for on each GameObject, e.g. `'Sound'`. */
52
+ componentName: string;
53
+ /** Badge fill color, `0xRRGGBB`. */
54
+ badgeColor: number;
55
+ /** Which glyph to draw inside the badge. */
56
+ glyph: MarkerGlyph;
57
+ }
58
+ /**
59
+ * Registered marker kinds, in priority order: a GameObject is marked by the
60
+ * first registered component it owns. For now this is **`Sound` only**; the
61
+ * mechanism is generic (append Physics / Camera / triggers / spawn points later
62
+ * to reuse the same layer) but only audio is wanted right now.
63
+ */
64
+ declare const MARKER_COMPONENTS: readonly MarkerComponentDef[];
65
+ /** Fixed badge size in the owner's local space. */
66
+ declare const MARKER_ICON_SIZE = 28;
67
+ /**
68
+ * First registered marker def whose component is present on `go`, else `null`.
69
+ * Priority follows {@link MARKER_COMPONENTS} order.
70
+ */
71
+ declare function resolveMarkerDef(go: Pick<GameObject, 'getComponent'>, registry?: readonly MarkerComponentDef[]): MarkerComponentDef | null;
72
+ /**
73
+ * Minimal subset of the PixiGraphics v8 surface the marker drawing uses. Keeps
74
+ * this module decoupled from the renderer and unit-testable with a mock.
75
+ */
76
+ interface MarkerGraphicsLike {
77
+ clear(): unknown;
78
+ roundRect(x: number, y: number, width: number, height: number, radius?: number): unknown;
79
+ rect(x: number, y: number, width: number, height: number): unknown;
80
+ circle(x: number, y: number, radius: number): unknown;
81
+ poly(points: number[], close?: boolean): unknown;
82
+ fill(style: {
83
+ color: number;
84
+ alpha?: number;
85
+ }): unknown;
86
+ stroke(style: {
87
+ width: number;
88
+ color: number;
89
+ alpha?: number;
90
+ }): unknown;
91
+ }
92
+ /** Render a full marker (badge + glyph) into `g`, sized to `size` (local units). */
93
+ declare function drawMarkerIcon(g: MarkerGraphicsLike, def: MarkerComponentDef, size?: number): void;
25
94
 
26
95
  /** Default inbound postMessage host suffixes (host + all subdomains). */
27
96
  declare const DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES: readonly ["knoffice.tech", "converge.ai"];
@@ -71,12 +140,14 @@ interface CombosDevelopmentToolSystemParams {
71
140
  * - `window.postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET, enabled: true }, targetOrigin)` (e.g. from parent iframe)
72
141
  * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET, { enabled: true })` or `getSystem(CombosDevelopmentToolSystem).setEnabled(true)`
73
142
  *
74
- * Pause / mute (independent of pick mode; parent toolbar or in-game debug UI):
75
- * - `postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, paused: true })`
143
+ * Mute (independent of pick mode; parent toolbar or in-game debug UI):
76
144
  * - `postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET_MUTED, muted: true })`
77
- * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, { paused: true })` or `.setPaused(true)`
145
+ * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, { muted: true })` or `.setMuted(true)`
78
146
  * Mute applies when `SoundSystem` is registered; otherwise the desired value is remembered until it appears.
79
- * Both emit / post `COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED` with `{ paused, muted }`.
147
+ * Emits / posts `COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED` with `{ muted }`.
148
+ *
149
+ * Play / pause / resume are handled by the engine core lifecycle protocol
150
+ * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
80
151
  *
81
152
  * While enabled: `game.emit(COMBOS_DEVELOPMENT_TOOL_REFRESH)`, `window` event `combos-development-tool:refresh`,
82
153
  * or `postMessage({ type: 'combos-development-tool:refresh' })` to re-bind after adding/removing objects.
@@ -98,13 +169,25 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
98
169
  private lastOutlineBounds;
99
170
  /** Remembered mute when `SoundSystem` is not registered yet. */
100
171
  private mutedWithoutSoundSystem;
172
+ /**
173
+ * Marker overlay (see {@link markerLayer}): a visual-only icon layer, toggled
174
+ * independently of pick mode + mute. Selection/persistence are NOT handled here
175
+ * — a marked object (e.g. Sound) carries its own `CombosDevelopmentToolTarget`
176
+ * (added by the game code), so it is picked and serialized through the normal
177
+ * Target path. The icon just makes the invisible object visible and, by adding
178
+ * to the owner's rendered bounds, gives its Target pick a clickable hit area.
179
+ */
180
+ private markerOverlayEnabled;
181
+ private needsMarkerRescan;
182
+ /** ownerGoId → its marker GO (icon child, Graphics only). */
183
+ private readonly markerGoByOwner;
101
184
  private readonly onWindowSet;
102
- private readonly onWindowSetPaused;
103
185
  private readonly onWindowSetMuted;
186
+ private readonly onWindowSetMarkerOverlay;
187
+ private readonly onGameSetMarkerOverlay;
104
188
  private readonly onWindowMessage;
105
189
  private readonly onGameSet;
106
190
  private readonly onGameRefresh;
107
- private readonly onGameSetPaused;
108
191
  private readonly onGameSetMuted;
109
192
  private readonly onWindowRefresh;
110
193
  init(params?: CombosDevelopmentToolSystemParams): void;
@@ -112,12 +195,19 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
112
195
  /** Programmatic toggle (same effect as events). */
113
196
  setEnabled(on: boolean): void;
114
197
  get isEnabled(): boolean;
115
- /** Whether the game loop is paused (`!game.playing`). */
116
- get isPaused(): boolean;
198
+ /**
199
+ * Toggle the marker overlay: pin a persistent icon on every object that owns a
200
+ * registered invisible component ({@link markerLayer}, Sound first). Independent
201
+ * of pick mode / mute. This layer is purely visual — the icon makes the object
202
+ * visible and adds to its rendered bounds so the owner's own
203
+ * `CombosDevelopmentToolTarget` pick gets a clickable hit area. Selecting and
204
+ * persisting edits (e.g. volume) still go through that Target, exactly like any
205
+ * other scene edit, so the object must carry a Target (added by the game code).
206
+ */
207
+ setMarkerOverlay(on: boolean): void;
208
+ get isMarkerOverlayEnabled(): boolean;
117
209
  /** Master mute when `SoundSystem` exists; otherwise the last requested value. */
118
210
  get isMuted(): boolean;
119
- /** Pause or resume the game loop (same as `game.pause()` / `game.resume()`). */
120
- setPaused(paused: boolean): void;
121
211
  /**
122
212
  * Mute or unmute audio via `SoundSystem` when registered.
123
213
  * No-op on the audio graph if `SoundSystem` is missing; value is remembered for `isMuted`.
@@ -125,11 +215,12 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
125
215
  setMuted(muted: boolean): void;
126
216
  update(e: UpdateParams): void;
127
217
  componentChanged(changed: ComponentChanged): void;
128
- /** Re-bind pick targets after dynamic scene changes while enabled. */
218
+ /** Re-bind pick targets / markers after dynamic scene changes while active. */
129
219
  requestSceneRescan(): void;
130
220
  private attachEditMode;
131
221
  private collectGameObjects;
132
222
  private isIgnoredPickGo;
223
+ private isMarkerGo;
133
224
  private stripGameEvent;
134
225
  private restoreDisabledGameEvents;
135
226
  private detachAllPicks;
@@ -146,6 +237,12 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
146
237
  private releasePick;
147
238
  private postSetSuccess;
148
239
  private postStateChanged;
240
+ /** (Re)build markers for every object owning a registered marker component. */
241
+ private attachMarkers;
242
+ private createMarker;
243
+ private removeMarker;
244
+ private detachAllMarkers;
245
+ private postMarkerOverlaySuccess;
149
246
  private getSoundSystem;
150
247
  private ensureOutline;
151
248
  private onSelect;
@@ -202,5 +299,5 @@ declare function buildGameObjectDeselectedPayload(reason: DevelopmentToolDeselec
202
299
  declare function shouldNotifyGameObjectDeselected(hadSelection: boolean): boolean;
203
300
  declare function isClearSelectionMessage(data: unknown): boolean;
204
301
 
205
- export { COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY, COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED, COMBOS_DEVELOPMENT_TOOL_READY, COMBOS_DEVELOPMENT_TOOL_REFRESH, COMBOS_DEVELOPMENT_TOOL_SET, COMBOS_DEVELOPMENT_TOOL_SET_MUTED, COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, COMBOS_DEVELOPMENT_TOOL_SET_SUCCESS, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, applyPropertyValue, applyPropertyWithHooks, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, shouldNotifyGameObjectDeselected };
206
- export type { CombosDevelopmentToolSystemParams, CombosDevelopmentToolTargetParams, DevelopmentToolDeselectReason, SceneEditComponentSnapshot, SceneEditGameObjectSnapshot, SceneEditPropertyRef, SceneSourceAnchor };
302
+ export { COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY, COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED, COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS, COMBOS_DEVELOPMENT_TOOL_READY, COMBOS_DEVELOPMENT_TOOL_REFRESH, COMBOS_DEVELOPMENT_TOOL_SET, COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, COMBOS_DEVELOPMENT_TOOL_SET_MUTED, COMBOS_DEVELOPMENT_TOOL_SET_SUCCESS, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, MARKER_COMPONENTS, MARKER_ICON_SIZE, applyPropertyValue, applyPropertyWithHooks, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, drawMarkerIcon, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, resolveMarkerDef, shouldNotifyGameObjectDeselected };
303
+ export type { CombosDevelopmentToolSystemParams, CombosDevelopmentToolTargetParams, DevelopmentToolDeselectReason, MarkerComponentDef, MarkerGlyph, MarkerGraphicsLike, SceneEditComponentSnapshot, SceneEditGameObjectSnapshot, SceneEditPropertyRef, SceneSourceAnchor };
@@ -20,12 +20,27 @@ const COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED = 'combos-development-tool:g
20
20
  const COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY = 'combos-development-tool:apply-property';
21
21
  /** iframe → parent: game scene bootstrap finished; parent may enable pick mode. */
22
22
  const COMBOS_DEVELOPMENT_TOOL_READY = 'combos-development-tool:ready';
23
- /** Parent → iframe: pause or resume the game loop. Payload: `{ paused: boolean }`. */
24
- const COMBOS_DEVELOPMENT_TOOL_SET_PAUSED = 'combos-development-tool:set-paused';
25
- /** Parent → iframe: mute or unmute audio when `SoundSystem` is registered. Payload: `{ muted: boolean }`. */
23
+ /**
24
+ * Parent → iframe: mute or unmute audio when `SoundSystem` is registered. Payload: `{ muted: boolean }`.
25
+ *
26
+ * Note: play / pause / resume are owned by the engine core lifecycle protocol
27
+ * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
28
+ */
26
29
  const COMBOS_DEVELOPMENT_TOOL_SET_MUTED = 'combos-development-tool:set-muted';
27
- /** iframe → parent (and `game.emit`): current pause/mute snapshot after a successful change. */
30
+ /** iframe → parent (and `game.emit`): current mute snapshot after a successful change. */
28
31
  const COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED = 'combos-development-tool:state-changed';
32
+ /**
33
+ * Parent → iframe (also `window` CustomEvent / `game.emit`): toggle the marker
34
+ * overlay — a persistent, clickable icon on every object owning an otherwise
35
+ * invisible registered component (Sound first). Independent of pick mode and
36
+ * mute. Payload: `{ enabled: boolean }`.
37
+ */
38
+ const COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY = 'combos-development-tool:set-marker-overlay';
39
+ /**
40
+ * iframe → parent (and `game.emit`): marker overlay finished (re)building.
41
+ * Payload: `{ enabled: boolean, total: number, markers: { componentName, count }[] }`.
42
+ */
43
+ const COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS = 'combos-development-tool:marker-overlay-success';
29
44
 
30
45
  /**
31
46
  * Marks a `GameObject` as selectable in editor pick mode. While the development tool is enabled,
@@ -43,6 +58,73 @@ class CombosDevelopmentToolTarget extends Component {
43
58
  }
44
59
  }
45
60
 
61
+ /**
62
+ * Registered marker kinds, in priority order: a GameObject is marked by the
63
+ * first registered component it owns. For now this is **`Sound` only**; the
64
+ * mechanism is generic (append Physics / Camera / triggers / spawn points later
65
+ * to reuse the same layer) but only audio is wanted right now.
66
+ */
67
+ const MARKER_COMPONENTS = [
68
+ { componentName: 'Sound', badgeColor: 0x8b5cf6, glyph: 'speaker' },
69
+ ];
70
+ /** Fixed badge size in the owner's local space. */
71
+ const MARKER_ICON_SIZE = 28;
72
+ /**
73
+ * First registered marker def whose component is present on `go`, else `null`.
74
+ * Priority follows {@link MARKER_COMPONENTS} order.
75
+ */
76
+ function resolveMarkerDef(go, registry = MARKER_COMPONENTS) {
77
+ for (const def of registry) {
78
+ if (go.getComponent(def.componentName)) {
79
+ return def;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ const GLYPH_COLOR = 0xffffff;
85
+ /** Draw the rounded badge background shared by every glyph. */
86
+ function drawBadge(g, def, size) {
87
+ g.clear();
88
+ g.roundRect(0, 0, size, size, size * 0.22);
89
+ g.fill({ color: def.badgeColor, alpha: 0.92 });
90
+ g.roundRect(0, 0, size, size, size * 0.22);
91
+ g.stroke({ width: Math.max(1, size * 0.05), color: 0xffffff, alpha: 0.6 });
92
+ }
93
+ /** A speaker cone + two volume bars, normalized to the `size` box. */
94
+ function drawSpeakerGlyph(g, s) {
95
+ g.poly([
96
+ s * 0.28, s * 0.4,
97
+ s * 0.42, s * 0.4,
98
+ s * 0.55, s * 0.26,
99
+ s * 0.55, s * 0.74,
100
+ s * 0.42, s * 0.6,
101
+ s * 0.28, s * 0.6,
102
+ ]);
103
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
104
+ g.rect(s * 0.62, s * 0.44, s * 0.045, s * 0.12);
105
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
106
+ g.rect(s * 0.71, s * 0.39, s * 0.045, s * 0.22);
107
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
108
+ }
109
+ /** Generic fallback glyph for component kinds without a dedicated icon. */
110
+ function drawDotGlyph(g, s) {
111
+ g.circle(s * 0.5, s * 0.5, s * 0.18);
112
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
113
+ }
114
+ /** Render a full marker (badge + glyph) into `g`, sized to `size` (local units). */
115
+ function drawMarkerIcon(g, def, size = MARKER_ICON_SIZE) {
116
+ drawBadge(g, def, size);
117
+ switch (def.glyph) {
118
+ case 'speaker':
119
+ drawSpeakerGlyph(g, size);
120
+ break;
121
+ case 'dot':
122
+ default:
123
+ drawDotGlyph(g, size);
124
+ break;
125
+ }
126
+ }
127
+
46
128
  /** Default inbound postMessage host suffixes (host + all subdomains). */
47
129
  const DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES = [
48
130
  'knoffice.tech',
@@ -366,6 +448,7 @@ function isClearSelectionMessage(data) {
366
448
  }
367
449
 
368
450
  const OUTLINE_GO_NAME = '__combosDevelopmentToolOutline';
451
+ const MARKER_GO_NAME_PREFIX = '__combosDevelopmentToolMarker:';
369
452
  /**
370
453
  * **Off** at start. While enabled:
371
454
  * - soft-disables game `Event` hit targets (`container.interactive = false`, restored on disable);
@@ -376,12 +459,14 @@ const OUTLINE_GO_NAME = '__combosDevelopmentToolOutline';
376
459
  * - `window.postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET, enabled: true }, targetOrigin)` (e.g. from parent iframe)
377
460
  * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET, { enabled: true })` or `getSystem(CombosDevelopmentToolSystem).setEnabled(true)`
378
461
  *
379
- * Pause / mute (independent of pick mode; parent toolbar or in-game debug UI):
380
- * - `postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, paused: true })`
462
+ * Mute (independent of pick mode; parent toolbar or in-game debug UI):
381
463
  * - `postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET_MUTED, muted: true })`
382
- * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, { paused: true })` or `.setPaused(true)`
464
+ * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, { muted: true })` or `.setMuted(true)`
383
465
  * Mute applies when `SoundSystem` is registered; otherwise the desired value is remembered until it appears.
384
- * Both emit / post `COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED` with `{ paused, muted }`.
466
+ * Emits / posts `COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED` with `{ muted }`.
467
+ *
468
+ * Play / pause / resume are handled by the engine core lifecycle protocol
469
+ * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
385
470
  *
386
471
  * While enabled: `game.emit(COMBOS_DEVELOPMENT_TOOL_REFRESH)`, `window` event `combos-development-tool:refresh`,
387
472
  * or `postMessage({ type: 'combos-development-tool:refresh' })` to re-bind after adding/removing objects.
@@ -404,24 +489,41 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
404
489
  this.lastOutlineBounds = null;
405
490
  /** Remembered mute when `SoundSystem` is not registered yet. */
406
491
  this.mutedWithoutSoundSystem = false;
492
+ /**
493
+ * Marker overlay (see {@link markerLayer}): a visual-only icon layer, toggled
494
+ * independently of pick mode + mute. Selection/persistence are NOT handled here
495
+ * — a marked object (e.g. Sound) carries its own `CombosDevelopmentToolTarget`
496
+ * (added by the game code), so it is picked and serialized through the normal
497
+ * Target path. The icon just makes the invisible object visible and, by adding
498
+ * to the owner's rendered bounds, gives its Target pick a clickable hit area.
499
+ */
500
+ this.markerOverlayEnabled = false;
501
+ this.needsMarkerRescan = false;
502
+ /** ownerGoId → its marker GO (icon child, Graphics only). */
503
+ this.markerGoByOwner = new Map();
407
504
  this.onWindowSet = (e) => {
408
505
  const d = e.detail;
409
506
  if (d && typeof d.enabled === 'boolean') {
410
507
  this.setEnabled(d.enabled);
411
508
  }
412
509
  };
413
- this.onWindowSetPaused = (e) => {
414
- const d = e.detail;
415
- if (d && typeof d.paused === 'boolean') {
416
- this.setPaused(d.paused);
417
- }
418
- };
419
510
  this.onWindowSetMuted = (e) => {
420
511
  const d = e.detail;
421
512
  if (d && typeof d.muted === 'boolean') {
422
513
  this.setMuted(d.muted);
423
514
  }
424
515
  };
516
+ this.onWindowSetMarkerOverlay = (e) => {
517
+ const d = e.detail;
518
+ if (d && typeof d.enabled === 'boolean') {
519
+ this.setMarkerOverlay(d.enabled);
520
+ }
521
+ };
522
+ this.onGameSetMarkerOverlay = (payload) => {
523
+ if (payload && typeof payload.enabled === 'boolean') {
524
+ this.setMarkerOverlay(payload.enabled);
525
+ }
526
+ };
425
527
  this.onWindowMessage = (e) => {
426
528
  if (!isAllowedMessageOrigin(e.origin, this.allowedMessageOrigins)) {
427
529
  return;
@@ -432,12 +534,13 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
432
534
  if (d.type === COMBOS_DEVELOPMENT_TOOL_SET && typeof d.enabled === 'boolean') {
433
535
  this.setEnabled(d.enabled);
434
536
  }
435
- else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_PAUSED && typeof d.paused === 'boolean') {
436
- this.setPaused(d.paused);
437
- }
438
537
  else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_MUTED && typeof d.muted === 'boolean') {
439
538
  this.setMuted(d.muted);
440
539
  }
540
+ else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY &&
541
+ typeof d.enabled === 'boolean') {
542
+ this.setMarkerOverlay(d.enabled);
543
+ }
441
544
  else if (d.type === COMBOS_DEVELOPMENT_TOOL_REFRESH) {
442
545
  this.requestSceneRescan();
443
546
  }
@@ -456,11 +559,6 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
456
559
  this.onGameRefresh = () => {
457
560
  this.requestSceneRescan();
458
561
  };
459
- this.onGameSetPaused = (payload) => {
460
- if (payload && typeof payload.paused === 'boolean') {
461
- this.setPaused(payload.paused);
462
- }
463
- };
464
562
  this.onGameSetMuted = (payload) => {
465
563
  if (payload && typeof payload.muted === 'boolean') {
466
564
  this.setMuted(payload.muted);
@@ -476,14 +574,14 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
476
574
  this.allowedMessageOrigins = mergeAllowedMessageOrigins(params?.allowedMessageOrigins);
477
575
  if (typeof window !== 'undefined') {
478
576
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET, this.onWindowSet);
479
- window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onWindowSetPaused);
480
577
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onWindowSetMuted);
578
+ window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
481
579
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onWindowRefresh);
482
580
  window.addEventListener('message', this.onWindowMessage);
483
581
  }
484
582
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET, this.onGameSet);
485
- this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onGameSetPaused);
486
583
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onGameSetMuted);
584
+ this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
487
585
  this.game.on(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
488
586
  if (this.enabled) {
489
587
  this.needsRescan = true;
@@ -492,16 +590,17 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
492
590
  onDestroy() {
493
591
  if (typeof window !== 'undefined') {
494
592
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET, this.onWindowSet);
495
- window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onWindowSetPaused);
496
593
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onWindowSetMuted);
594
+ window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
497
595
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onWindowRefresh);
498
596
  window.removeEventListener('message', this.onWindowMessage);
499
597
  }
500
598
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET, this.onGameSet);
501
- this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onGameSetPaused);
502
599
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onGameSetMuted);
600
+ this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
503
601
  this.game.off(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
504
602
  this.detachAllPicks();
603
+ this.detachAllMarkers();
505
604
  this.restoreDisabledGameEvents();
506
605
  this.clearSelectionAndNotify('pick-disabled');
507
606
  }
@@ -523,9 +622,34 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
523
622
  get isEnabled() {
524
623
  return this.enabled;
525
624
  }
526
- /** Whether the game loop is paused (`!game.playing`). */
527
- get isPaused() {
528
- return !this.game.playing;
625
+ /**
626
+ * Toggle the marker overlay: pin a persistent icon on every object that owns a
627
+ * registered invisible component ({@link markerLayer}, Sound first). Independent
628
+ * of pick mode / mute. This layer is purely visual — the icon makes the object
629
+ * visible and adds to its rendered bounds so the owner's own
630
+ * `CombosDevelopmentToolTarget` pick gets a clickable hit area. Selecting and
631
+ * persisting edits (e.g. volume) still go through that Target, exactly like any
632
+ * other scene edit, so the object must carry a Target (added by the game code).
633
+ */
634
+ setMarkerOverlay(on) {
635
+ if (this.markerOverlayEnabled === on)
636
+ return;
637
+ this.markerOverlayEnabled = on;
638
+ if (on) {
639
+ // `attachMarkers` requests the pick rescan itself, once the icons exist.
640
+ this.needsMarkerRescan = true;
641
+ }
642
+ else {
643
+ this.detachAllMarkers();
644
+ this.postMarkerOverlaySuccess(false, []);
645
+ // Shrink owners' pick hit areas back now that the icons are gone.
646
+ if (this.enabled) {
647
+ this.needsRescan = true;
648
+ }
649
+ }
650
+ }
651
+ get isMarkerOverlayEnabled() {
652
+ return this.markerOverlayEnabled;
529
653
  }
530
654
  /** Master mute when `SoundSystem` exists; otherwise the last requested value. */
531
655
  get isMuted() {
@@ -535,18 +659,6 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
535
659
  }
536
660
  return this.mutedWithoutSoundSystem;
537
661
  }
538
- /** Pause or resume the game loop (same as `game.pause()` / `game.resume()`). */
539
- setPaused(paused) {
540
- if (paused) {
541
- if (this.game.playing) {
542
- this.game.pause();
543
- }
544
- }
545
- else if (!this.game.playing) {
546
- this.game.resume();
547
- }
548
- this.postStateChanged();
549
- }
550
662
  /**
551
663
  * Mute or unmute audio via `SoundSystem` when registered.
552
664
  * No-op on the audio graph if `SoundSystem` is missing; value is remembered for `isMuted`.
@@ -567,11 +679,26 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
567
679
  this.attachEditMode();
568
680
  this.postSetSuccess(true);
569
681
  }
682
+ // Attaching markers is deferred to run *after* the pick rescan above so that a
683
+ // newly drawn icon's rebuild lands on a later frame (see `attachMarkers`): the
684
+ // icon child needs a rendered container before it contributes to the owner's
685
+ // bounds, and only then does the owner's Target pick get a hit area over it.
686
+ if (this.markerOverlayEnabled && this.needsMarkerRescan) {
687
+ this.needsMarkerRescan = false;
688
+ this.attachMarkers();
689
+ }
570
690
  for (const go of [...this.tapOwners.values()]) {
571
691
  if (go.destroyed) {
572
692
  this.releasePick(go);
573
693
  }
574
694
  }
695
+ // Drop markers whose owner disappeared while the overlay is on.
696
+ for (const [ownerId, marker] of [...this.markerGoByOwner.entries()]) {
697
+ const owner = this.findGameObjectById(ownerId);
698
+ if (!owner || owner.destroyed || marker.destroyed) {
699
+ this.removeMarker(ownerId);
700
+ }
701
+ }
575
702
  if (!this.enabled || !this.selected || !this.outlineGo)
576
703
  return;
577
704
  this.updateSelectionOutline();
@@ -593,11 +720,14 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
593
720
  }
594
721
  }
595
722
  }
596
- /** Re-bind pick targets after dynamic scene changes while enabled. */
723
+ /** Re-bind pick targets / markers after dynamic scene changes while active. */
597
724
  requestSceneRescan() {
598
- if (!this.enabled)
599
- return;
600
- this.needsRescan = true;
725
+ if (this.enabled) {
726
+ this.needsRescan = true;
727
+ }
728
+ if (this.markerOverlayEnabled) {
729
+ this.needsMarkerRescan = true;
730
+ }
601
731
  }
602
732
  attachEditMode() {
603
733
  this.detachAllPicks();
@@ -625,7 +755,10 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
625
755
  }
626
756
  }
627
757
  isIgnoredPickGo(go) {
628
- return go.name === OUTLINE_GO_NAME || go === this.outlineGo;
758
+ return go.name === OUTLINE_GO_NAME || go === this.outlineGo || this.isMarkerGo(go);
759
+ }
760
+ isMarkerGo(go) {
761
+ return typeof go.name === 'string' && go.name.startsWith(MARKER_GO_NAME_PREFIX);
629
762
  }
630
763
  stripGameEvent(go) {
631
764
  if (this.disabledGameEventGoIds.has(go.id))
@@ -831,7 +964,6 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
831
964
  }
832
965
  postStateChanged() {
833
966
  const state = {
834
- paused: this.isPaused,
835
967
  muted: this.isMuted,
836
968
  };
837
969
  const payload = {
@@ -843,6 +975,78 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
843
975
  }
844
976
  this.game.emit(COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, state);
845
977
  }
978
+ /** (Re)build markers for every object owning a registered marker component. */
979
+ attachMarkers() {
980
+ this.detachAllMarkers();
981
+ const list = [];
982
+ for (const tr of this.game.scene.transform.children) {
983
+ this.collectGameObjects(tr.gameObject, list);
984
+ }
985
+ const counts = new Map();
986
+ for (const go of list) {
987
+ if (this.isIgnoredPickGo(go))
988
+ continue;
989
+ const def = resolveMarkerDef(go);
990
+ if (!def)
991
+ continue;
992
+ this.createMarker(go, def);
993
+ counts.set(def.componentName, (counts.get(def.componentName) ?? 0) + 1);
994
+ }
995
+ const markers = [...counts.entries()].map(([componentName, count]) => ({
996
+ componentName,
997
+ count,
998
+ }));
999
+ this.postMarkerOverlaySuccess(true, markers);
1000
+ // Rebuild picks next frame: the icons just added need a rendered container
1001
+ // before they enlarge their owners' bounds into a clickable hit area. Runs on
1002
+ // a later frame because the pick rescan block already executed above.
1003
+ if (this.enabled) {
1004
+ this.needsRescan = true;
1005
+ }
1006
+ }
1007
+ createMarker(owner, def) {
1008
+ if (this.markerGoByOwner.has(owner.id))
1009
+ return;
1010
+ const bounds = this.resolvePickBounds(owner);
1011
+ const marker = new GameObject(MARKER_GO_NAME_PREFIX + owner.id, {
1012
+ size: { width: MARKER_ICON_SIZE, height: MARKER_ICON_SIZE },
1013
+ position: { x: bounds.x, y: bounds.y },
1014
+ origin: { x: 0, y: 0 },
1015
+ });
1016
+ const gfx = marker.addComponent(new Graphics());
1017
+ if (gfx?.graphics) {
1018
+ drawMarkerIcon(gfx.graphics, def, MARKER_ICON_SIZE);
1019
+ }
1020
+ owner.addChild(marker);
1021
+ this.markerGoByOwner.set(owner.id, marker);
1022
+ }
1023
+ removeMarker(ownerId) {
1024
+ const marker = this.markerGoByOwner.get(ownerId);
1025
+ if (!marker)
1026
+ return;
1027
+ this.markerGoByOwner.delete(ownerId);
1028
+ if (!marker.destroyed) {
1029
+ marker.destroy();
1030
+ }
1031
+ }
1032
+ detachAllMarkers() {
1033
+ for (const ownerId of [...this.markerGoByOwner.keys()]) {
1034
+ this.removeMarker(ownerId);
1035
+ }
1036
+ this.markerGoByOwner.clear();
1037
+ }
1038
+ postMarkerOverlaySuccess(enabled, markers) {
1039
+ const total = markers.reduce((sum, m) => sum + m.count, 0);
1040
+ const state = { enabled, total, markers };
1041
+ const payload = {
1042
+ type: COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS,
1043
+ ...state,
1044
+ };
1045
+ if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
1046
+ window.parent.postMessage(payload, this.postMessageOrigin);
1047
+ }
1048
+ this.game.emit(COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS, state);
1049
+ }
846
1050
  getSoundSystem() {
847
1051
  const system = this.game.getSystem('SoundSystem');
848
1052
  if (!system ||
@@ -1029,8 +1233,8 @@ var CombosDevelopmentToolSystem$1 = CombosDevelopmentToolSystem;
1029
1233
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
1030
1234
  Object.assign(CombosDevelopmentToolSystem$1, {
1031
1235
  packageName: "@combos-fun/plugin-development-tool",
1032
- packageVersion: "0.0.26",
1236
+ packageVersion: "0.0.27",
1033
1237
  });
1034
1238
 
1035
- export { COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY, COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED, COMBOS_DEVELOPMENT_TOOL_READY, COMBOS_DEVELOPMENT_TOOL_REFRESH, COMBOS_DEVELOPMENT_TOOL_SET, COMBOS_DEVELOPMENT_TOOL_SET_MUTED, COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, COMBOS_DEVELOPMENT_TOOL_SET_SUCCESS, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem$1 as CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, applyPropertyValue, applyPropertyWithHooks, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, shouldNotifyGameObjectDeselected };
1239
+ export { COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY, COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED, COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS, COMBOS_DEVELOPMENT_TOOL_READY, COMBOS_DEVELOPMENT_TOOL_REFRESH, COMBOS_DEVELOPMENT_TOOL_SET, COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, COMBOS_DEVELOPMENT_TOOL_SET_MUTED, COMBOS_DEVELOPMENT_TOOL_SET_SUCCESS, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem$1 as CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, MARKER_COMPONENTS, MARKER_ICON_SIZE, applyPropertyValue, applyPropertyWithHooks, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, drawMarkerIcon, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, resolveMarkerDef, shouldNotifyGameObjectDeselected };
1036
1240
  //# sourceMappingURL=plugin-development-tool.esm.js.map