@combos-fun/plugin-development-tool 0.0.27 → 0.0.29

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";
@@ -25,6 +25,72 @@ declare const COMBOS_DEVELOPMENT_TOOL_READY: "combos-development-tool:ready";
25
25
  declare const COMBOS_DEVELOPMENT_TOOL_SET_MUTED: "combos-development-tool:set-muted";
26
26
  /** iframe → parent (and `game.emit`): current mute snapshot after a successful change. */
27
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;
28
94
 
29
95
  /** Default inbound postMessage host suffixes (host + all subdomains). */
30
96
  declare const DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES: readonly ["knoffice.tech", "converge.ai"];
@@ -103,8 +169,22 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
103
169
  private lastOutlineBounds;
104
170
  /** Remembered mute when `SoundSystem` is not registered yet. */
105
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;
106
184
  private readonly onWindowSet;
107
185
  private readonly onWindowSetMuted;
186
+ private readonly onWindowSetMarkerOverlay;
187
+ private readonly onGameSetMarkerOverlay;
108
188
  private readonly onWindowMessage;
109
189
  private readonly onGameSet;
110
190
  private readonly onGameRefresh;
@@ -115,6 +195,17 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
115
195
  /** Programmatic toggle (same effect as events). */
116
196
  setEnabled(on: boolean): void;
117
197
  get isEnabled(): 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;
118
209
  /** Master mute when `SoundSystem` exists; otherwise the last requested value. */
119
210
  get isMuted(): boolean;
120
211
  /**
@@ -124,11 +215,12 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
124
215
  setMuted(muted: boolean): void;
125
216
  update(e: UpdateParams): void;
126
217
  componentChanged(changed: ComponentChanged): void;
127
- /** Re-bind pick targets after dynamic scene changes while enabled. */
218
+ /** Re-bind pick targets / markers after dynamic scene changes while active. */
128
219
  requestSceneRescan(): void;
129
220
  private attachEditMode;
130
221
  private collectGameObjects;
131
222
  private isIgnoredPickGo;
223
+ private isMarkerGo;
132
224
  private stripGameEvent;
133
225
  private restoreDisabledGameEvents;
134
226
  private detachAllPicks;
@@ -145,6 +237,12 @@ declare class CombosDevelopmentToolSystem extends System<CombosDevelopmentToolSy
145
237
  private releasePick;
146
238
  private postSetSuccess;
147
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;
148
246
  private getSoundSystem;
149
247
  private ensureOutline;
150
248
  private onSelect;
@@ -201,5 +299,5 @@ declare function buildGameObjectDeselectedPayload(reason: DevelopmentToolDeselec
201
299
  declare function shouldNotifyGameObjectDeselected(hadSelection: boolean): boolean;
202
300
  declare function isClearSelectionMessage(data: unknown): boolean;
203
301
 
204
- 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_SUCCESS, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, applyPropertyValue, applyPropertyWithHooks, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, shouldNotifyGameObjectDeselected };
205
- 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 };
@@ -29,6 +29,18 @@ const COMBOS_DEVELOPMENT_TOOL_READY = 'combos-development-tool:ready';
29
29
  const COMBOS_DEVELOPMENT_TOOL_SET_MUTED = 'combos-development-tool:set-muted';
30
30
  /** iframe → parent (and `game.emit`): current mute snapshot after a successful change. */
31
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';
32
44
 
33
45
  /**
34
46
  * Marks a `GameObject` as selectable in editor pick mode. While the development tool is enabled,
@@ -46,6 +58,73 @@ class CombosDevelopmentToolTarget extends Component {
46
58
  }
47
59
  }
48
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
+
49
128
  /** Default inbound postMessage host suffixes (host + all subdomains). */
50
129
  const DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES = [
51
130
  'knoffice.tech',
@@ -369,6 +448,7 @@ function isClearSelectionMessage(data) {
369
448
  }
370
449
 
371
450
  const OUTLINE_GO_NAME = '__combosDevelopmentToolOutline';
451
+ const MARKER_GO_NAME_PREFIX = '__combosDevelopmentToolMarker:';
372
452
  /**
373
453
  * **Off** at start. While enabled:
374
454
  * - soft-disables game `Event` hit targets (`container.interactive = false`, restored on disable);
@@ -409,6 +489,18 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
409
489
  this.lastOutlineBounds = null;
410
490
  /** Remembered mute when `SoundSystem` is not registered yet. */
411
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();
412
504
  this.onWindowSet = (e) => {
413
505
  const d = e.detail;
414
506
  if (d && typeof d.enabled === 'boolean') {
@@ -421,6 +513,17 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
421
513
  this.setMuted(d.muted);
422
514
  }
423
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
+ };
424
527
  this.onWindowMessage = (e) => {
425
528
  if (!isAllowedMessageOrigin(e.origin, this.allowedMessageOrigins)) {
426
529
  return;
@@ -434,6 +537,10 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
434
537
  else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_MUTED && typeof d.muted === 'boolean') {
435
538
  this.setMuted(d.muted);
436
539
  }
540
+ else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY &&
541
+ typeof d.enabled === 'boolean') {
542
+ this.setMarkerOverlay(d.enabled);
543
+ }
437
544
  else if (d.type === COMBOS_DEVELOPMENT_TOOL_REFRESH) {
438
545
  this.requestSceneRescan();
439
546
  }
@@ -468,11 +575,13 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
468
575
  if (typeof window !== 'undefined') {
469
576
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET, this.onWindowSet);
470
577
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onWindowSetMuted);
578
+ window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
471
579
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onWindowRefresh);
472
580
  window.addEventListener('message', this.onWindowMessage);
473
581
  }
474
582
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET, this.onGameSet);
475
583
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onGameSetMuted);
584
+ this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
476
585
  this.game.on(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
477
586
  if (this.enabled) {
478
587
  this.needsRescan = true;
@@ -482,13 +591,16 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
482
591
  if (typeof window !== 'undefined') {
483
592
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET, this.onWindowSet);
484
593
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onWindowSetMuted);
594
+ window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
485
595
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onWindowRefresh);
486
596
  window.removeEventListener('message', this.onWindowMessage);
487
597
  }
488
598
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET, this.onGameSet);
489
599
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onGameSetMuted);
600
+ this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
490
601
  this.game.off(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
491
602
  this.detachAllPicks();
603
+ this.detachAllMarkers();
492
604
  this.restoreDisabledGameEvents();
493
605
  this.clearSelectionAndNotify('pick-disabled');
494
606
  }
@@ -510,6 +622,35 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
510
622
  get isEnabled() {
511
623
  return this.enabled;
512
624
  }
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;
653
+ }
513
654
  /** Master mute when `SoundSystem` exists; otherwise the last requested value. */
514
655
  get isMuted() {
515
656
  const sound = this.getSoundSystem();
@@ -538,11 +679,26 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
538
679
  this.attachEditMode();
539
680
  this.postSetSuccess(true);
540
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
+ }
541
690
  for (const go of [...this.tapOwners.values()]) {
542
691
  if (go.destroyed) {
543
692
  this.releasePick(go);
544
693
  }
545
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
+ }
546
702
  if (!this.enabled || !this.selected || !this.outlineGo)
547
703
  return;
548
704
  this.updateSelectionOutline();
@@ -564,11 +720,14 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
564
720
  }
565
721
  }
566
722
  }
567
- /** Re-bind pick targets after dynamic scene changes while enabled. */
723
+ /** Re-bind pick targets / markers after dynamic scene changes while active. */
568
724
  requestSceneRescan() {
569
- if (!this.enabled)
570
- return;
571
- this.needsRescan = true;
725
+ if (this.enabled) {
726
+ this.needsRescan = true;
727
+ }
728
+ if (this.markerOverlayEnabled) {
729
+ this.needsMarkerRescan = true;
730
+ }
572
731
  }
573
732
  attachEditMode() {
574
733
  this.detachAllPicks();
@@ -596,7 +755,10 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
596
755
  }
597
756
  }
598
757
  isIgnoredPickGo(go) {
599
- 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);
600
762
  }
601
763
  stripGameEvent(go) {
602
764
  if (this.disabledGameEventGoIds.has(go.id))
@@ -813,6 +975,78 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
813
975
  }
814
976
  this.game.emit(COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, state);
815
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
+ }
816
1050
  getSoundSystem() {
817
1051
  const system = this.game.getSystem('SoundSystem');
818
1052
  if (!system ||
@@ -999,8 +1233,8 @@ var CombosDevelopmentToolSystem$1 = CombosDevelopmentToolSystem;
999
1233
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
1000
1234
  Object.assign(CombosDevelopmentToolSystem$1, {
1001
1235
  packageName: "@combos-fun/plugin-development-tool",
1002
- packageVersion: "0.0.27",
1236
+ packageVersion: "0.0.28",
1003
1237
  });
1004
1238
 
1005
- 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_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 };
1006
1240
  //# sourceMappingURL=plugin-development-tool.esm.js.map