@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.
package/agent-skill.md CHANGED
@@ -78,8 +78,32 @@ Outbound `postMessageOrigin` is separate — it limits which parent origin recei
78
78
  | parent → iframe | `combos-development-tool:set` | `{ enabled: boolean }` | Enable/disable pick mode |
79
79
  | parent → iframe | `combos-development-tool:clear-selection` | `{}` | Clear in-canvas outline |
80
80
  | parent → iframe | `combos-development-tool:apply-property` | component field mutation | Live preview |
81
+ | parent → iframe | `combos-development-tool:set-marker-overlay` | `{ enabled: boolean }` | Toggle the marker overlay (independent of pick mode) |
81
82
  | iframe → parent | `combos-development-tool:gameobject-selected` | `{ snapshot, pointer }` | Object picked |
82
83
  | iframe → parent | `combos-development-tool:gameobject-deselected` | `{ reason }` | Selection cleared (`parent-request`, `pick-disabled`, `target-removed`) |
84
+ | iframe → parent | `combos-development-tool:marker-overlay-success` | `{ enabled, total, markers: { componentName, count }[] }` | Marker overlay finished (re)building |
85
+
86
+ ## Marker overlay (surface invisible components)
87
+
88
+ A reusable "pin a persistent icon on every object owning an otherwise invisible
89
+ component" layer. Registry-driven (`markerLayer.ts` → `MARKER_COMPONENTS`).
90
+ **For now it is `Sound` only** — the mechanism is generic (append `Physics` / `Camera` /
91
+ triggers / spawn points later) but nothing else is registered yet.
92
+
93
+ - Independent toggle, orthogonal to pick mode and mute: `setMarkerOverlay(true)` or
94
+ `postMessage({ type: 'combos-development-tool:set-marker-overlay', enabled: true })`
95
+ (also `window` CustomEvent / `game.emit`).
96
+ - Draws the icon **in-engine** (a `Graphics` child GO pinned to the owner, so it
97
+ follows the object). The layer is **purely visual** — it does not wire up its own
98
+ selection. Because the icon adds to the owner's rendered bounds, the owner's own
99
+ `CombosDevelopmentToolTarget` pick gets a clickable hit area, so selecting and
100
+ editing (e.g. `Sound.config.volume`) go through the normal
101
+ selection → snapshot → `apply-property` flow and **persist to source** like any
102
+ other scene edit. **The marked object must therefore carry a
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).
106
+ - On (re)build it posts `combos-development-tool:marker-overlay-success` with per-component counts.
83
107
 
84
108
  ## Verification
85
109
 
@@ -22,12 +22,27 @@ const COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED = 'combos-development-tool:g
22
22
  const COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY = 'combos-development-tool:apply-property';
23
23
  /** iframe → parent: game scene bootstrap finished; parent may enable pick mode. */
24
24
  const COMBOS_DEVELOPMENT_TOOL_READY = 'combos-development-tool:ready';
25
- /** Parent → iframe: pause or resume the game loop. Payload: `{ paused: boolean }`. */
26
- const COMBOS_DEVELOPMENT_TOOL_SET_PAUSED = 'combos-development-tool:set-paused';
27
- /** Parent → iframe: mute or unmute audio when `SoundSystem` is registered. Payload: `{ muted: boolean }`. */
25
+ /**
26
+ * Parent → iframe: mute or unmute audio when `SoundSystem` is registered. Payload: `{ muted: boolean }`.
27
+ *
28
+ * Note: play / pause / resume are owned by the engine core lifecycle protocol
29
+ * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
30
+ */
28
31
  const COMBOS_DEVELOPMENT_TOOL_SET_MUTED = 'combos-development-tool:set-muted';
29
- /** iframe → parent (and `game.emit`): current pause/mute snapshot after a successful change. */
32
+ /** iframe → parent (and `game.emit`): current mute snapshot after a successful change. */
30
33
  const COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED = 'combos-development-tool:state-changed';
34
+ /**
35
+ * Parent → iframe (also `window` CustomEvent / `game.emit`): toggle the marker
36
+ * overlay — a persistent, clickable icon on every object owning an otherwise
37
+ * invisible registered component (Sound first). Independent of pick mode and
38
+ * mute. Payload: `{ enabled: boolean }`.
39
+ */
40
+ const COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY = 'combos-development-tool:set-marker-overlay';
41
+ /**
42
+ * iframe → parent (and `game.emit`): marker overlay finished (re)building.
43
+ * Payload: `{ enabled: boolean, total: number, markers: { componentName, count }[] }`.
44
+ */
45
+ const COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS = 'combos-development-tool:marker-overlay-success';
31
46
 
32
47
  /**
33
48
  * Marks a `GameObject` as selectable in editor pick mode. While the development tool is enabled,
@@ -45,6 +60,73 @@ class CombosDevelopmentToolTarget extends engine.Component {
45
60
  }
46
61
  }
47
62
 
63
+ /**
64
+ * Registered marker kinds, in priority order: a GameObject is marked by the
65
+ * first registered component it owns. For now this is **`Sound` only**; the
66
+ * mechanism is generic (append Physics / Camera / triggers / spawn points later
67
+ * to reuse the same layer) but only audio is wanted right now.
68
+ */
69
+ const MARKER_COMPONENTS = [
70
+ { componentName: 'Sound', badgeColor: 0x8b5cf6, glyph: 'speaker' },
71
+ ];
72
+ /** Fixed badge size in the owner's local space. */
73
+ const MARKER_ICON_SIZE = 28;
74
+ /**
75
+ * First registered marker def whose component is present on `go`, else `null`.
76
+ * Priority follows {@link MARKER_COMPONENTS} order.
77
+ */
78
+ function resolveMarkerDef(go, registry = MARKER_COMPONENTS) {
79
+ for (const def of registry) {
80
+ if (go.getComponent(def.componentName)) {
81
+ return def;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ const GLYPH_COLOR = 0xffffff;
87
+ /** Draw the rounded badge background shared by every glyph. */
88
+ function drawBadge(g, def, size) {
89
+ g.clear();
90
+ g.roundRect(0, 0, size, size, size * 0.22);
91
+ g.fill({ color: def.badgeColor, alpha: 0.92 });
92
+ g.roundRect(0, 0, size, size, size * 0.22);
93
+ g.stroke({ width: Math.max(1, size * 0.05), color: 0xffffff, alpha: 0.6 });
94
+ }
95
+ /** A speaker cone + two volume bars, normalized to the `size` box. */
96
+ function drawSpeakerGlyph(g, s) {
97
+ g.poly([
98
+ s * 0.28, s * 0.4,
99
+ s * 0.42, s * 0.4,
100
+ s * 0.55, s * 0.26,
101
+ s * 0.55, s * 0.74,
102
+ s * 0.42, s * 0.6,
103
+ s * 0.28, s * 0.6,
104
+ ]);
105
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
106
+ g.rect(s * 0.62, s * 0.44, s * 0.045, s * 0.12);
107
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
108
+ g.rect(s * 0.71, s * 0.39, s * 0.045, s * 0.22);
109
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
110
+ }
111
+ /** Generic fallback glyph for component kinds without a dedicated icon. */
112
+ function drawDotGlyph(g, s) {
113
+ g.circle(s * 0.5, s * 0.5, s * 0.18);
114
+ g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
115
+ }
116
+ /** Render a full marker (badge + glyph) into `g`, sized to `size` (local units). */
117
+ function drawMarkerIcon(g, def, size = MARKER_ICON_SIZE) {
118
+ drawBadge(g, def, size);
119
+ switch (def.glyph) {
120
+ case 'speaker':
121
+ drawSpeakerGlyph(g, size);
122
+ break;
123
+ case 'dot':
124
+ default:
125
+ drawDotGlyph(g, size);
126
+ break;
127
+ }
128
+ }
129
+
48
130
  /** Default inbound postMessage host suffixes (host + all subdomains). */
49
131
  const DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES = [
50
132
  'knoffice.tech',
@@ -368,6 +450,7 @@ function isClearSelectionMessage(data) {
368
450
  }
369
451
 
370
452
  const OUTLINE_GO_NAME = '__combosDevelopmentToolOutline';
453
+ const MARKER_GO_NAME_PREFIX = '__combosDevelopmentToolMarker:';
371
454
  /**
372
455
  * **Off** at start. While enabled:
373
456
  * - soft-disables game `Event` hit targets (`container.interactive = false`, restored on disable);
@@ -378,12 +461,14 @@ const OUTLINE_GO_NAME = '__combosDevelopmentToolOutline';
378
461
  * - `window.postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET, enabled: true }, targetOrigin)` (e.g. from parent iframe)
379
462
  * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET, { enabled: true })` or `getSystem(CombosDevelopmentToolSystem).setEnabled(true)`
380
463
  *
381
- * Pause / mute (independent of pick mode; parent toolbar or in-game debug UI):
382
- * - `postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, paused: true })`
464
+ * Mute (independent of pick mode; parent toolbar or in-game debug UI):
383
465
  * - `postMessage({ type: COMBOS_DEVELOPMENT_TOOL_SET_MUTED, muted: true })`
384
- * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, { paused: true })` or `.setPaused(true)`
466
+ * - `game.emit(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, { muted: true })` or `.setMuted(true)`
385
467
  * Mute applies when `SoundSystem` is registered; otherwise the desired value is remembered until it appears.
386
- * Both emit / post `COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED` with `{ paused, muted }`.
468
+ * Emits / posts `COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED` with `{ muted }`.
469
+ *
470
+ * Play / pause / resume are handled by the engine core lifecycle protocol
471
+ * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
387
472
  *
388
473
  * While enabled: `game.emit(COMBOS_DEVELOPMENT_TOOL_REFRESH)`, `window` event `combos-development-tool:refresh`,
389
474
  * or `postMessage({ type: 'combos-development-tool:refresh' })` to re-bind after adding/removing objects.
@@ -406,24 +491,41 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
406
491
  this.lastOutlineBounds = null;
407
492
  /** Remembered mute when `SoundSystem` is not registered yet. */
408
493
  this.mutedWithoutSoundSystem = false;
494
+ /**
495
+ * Marker overlay (see {@link markerLayer}): a visual-only icon layer, toggled
496
+ * independently of pick mode + mute. Selection/persistence are NOT handled here
497
+ * — a marked object (e.g. Sound) carries its own `CombosDevelopmentToolTarget`
498
+ * (added by the game code), so it is picked and serialized through the normal
499
+ * Target path. The icon just makes the invisible object visible and, by adding
500
+ * to the owner's rendered bounds, gives its Target pick a clickable hit area.
501
+ */
502
+ this.markerOverlayEnabled = false;
503
+ this.needsMarkerRescan = false;
504
+ /** ownerGoId → its marker GO (icon child, Graphics only). */
505
+ this.markerGoByOwner = new Map();
409
506
  this.onWindowSet = (e) => {
410
507
  const d = e.detail;
411
508
  if (d && typeof d.enabled === 'boolean') {
412
509
  this.setEnabled(d.enabled);
413
510
  }
414
511
  };
415
- this.onWindowSetPaused = (e) => {
416
- const d = e.detail;
417
- if (d && typeof d.paused === 'boolean') {
418
- this.setPaused(d.paused);
419
- }
420
- };
421
512
  this.onWindowSetMuted = (e) => {
422
513
  const d = e.detail;
423
514
  if (d && typeof d.muted === 'boolean') {
424
515
  this.setMuted(d.muted);
425
516
  }
426
517
  };
518
+ this.onWindowSetMarkerOverlay = (e) => {
519
+ const d = e.detail;
520
+ if (d && typeof d.enabled === 'boolean') {
521
+ this.setMarkerOverlay(d.enabled);
522
+ }
523
+ };
524
+ this.onGameSetMarkerOverlay = (payload) => {
525
+ if (payload && typeof payload.enabled === 'boolean') {
526
+ this.setMarkerOverlay(payload.enabled);
527
+ }
528
+ };
427
529
  this.onWindowMessage = (e) => {
428
530
  if (!isAllowedMessageOrigin(e.origin, this.allowedMessageOrigins)) {
429
531
  return;
@@ -434,12 +536,13 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
434
536
  if (d.type === COMBOS_DEVELOPMENT_TOOL_SET && typeof d.enabled === 'boolean') {
435
537
  this.setEnabled(d.enabled);
436
538
  }
437
- else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_PAUSED && typeof d.paused === 'boolean') {
438
- this.setPaused(d.paused);
439
- }
440
539
  else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_MUTED && typeof d.muted === 'boolean') {
441
540
  this.setMuted(d.muted);
442
541
  }
542
+ else if (d.type === COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY &&
543
+ typeof d.enabled === 'boolean') {
544
+ this.setMarkerOverlay(d.enabled);
545
+ }
443
546
  else if (d.type === COMBOS_DEVELOPMENT_TOOL_REFRESH) {
444
547
  this.requestSceneRescan();
445
548
  }
@@ -458,11 +561,6 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
458
561
  this.onGameRefresh = () => {
459
562
  this.requestSceneRescan();
460
563
  };
461
- this.onGameSetPaused = (payload) => {
462
- if (payload && typeof payload.paused === 'boolean') {
463
- this.setPaused(payload.paused);
464
- }
465
- };
466
564
  this.onGameSetMuted = (payload) => {
467
565
  if (payload && typeof payload.muted === 'boolean') {
468
566
  this.setMuted(payload.muted);
@@ -478,14 +576,14 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
478
576
  this.allowedMessageOrigins = mergeAllowedMessageOrigins(params?.allowedMessageOrigins);
479
577
  if (typeof window !== 'undefined') {
480
578
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET, this.onWindowSet);
481
- window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onWindowSetPaused);
482
579
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onWindowSetMuted);
580
+ window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
483
581
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onWindowRefresh);
484
582
  window.addEventListener('message', this.onWindowMessage);
485
583
  }
486
584
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET, this.onGameSet);
487
- this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onGameSetPaused);
488
585
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onGameSetMuted);
586
+ this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
489
587
  this.game.on(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
490
588
  if (this.enabled) {
491
589
  this.needsRescan = true;
@@ -494,16 +592,17 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
494
592
  onDestroy() {
495
593
  if (typeof window !== 'undefined') {
496
594
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET, this.onWindowSet);
497
- window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onWindowSetPaused);
498
595
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onWindowSetMuted);
596
+ window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
499
597
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onWindowRefresh);
500
598
  window.removeEventListener('message', this.onWindowMessage);
501
599
  }
502
600
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET, this.onGameSet);
503
- this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_PAUSED, this.onGameSetPaused);
504
601
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MUTED, this.onGameSetMuted);
602
+ this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
505
603
  this.game.off(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
506
604
  this.detachAllPicks();
605
+ this.detachAllMarkers();
507
606
  this.restoreDisabledGameEvents();
508
607
  this.clearSelectionAndNotify('pick-disabled');
509
608
  }
@@ -525,9 +624,34 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
525
624
  get isEnabled() {
526
625
  return this.enabled;
527
626
  }
528
- /** Whether the game loop is paused (`!game.playing`). */
529
- get isPaused() {
530
- return !this.game.playing;
627
+ /**
628
+ * Toggle the marker overlay: pin a persistent icon on every object that owns a
629
+ * registered invisible component ({@link markerLayer}, Sound first). Independent
630
+ * of pick mode / mute. This layer is purely visual — the icon makes the object
631
+ * visible and adds to its rendered bounds so the owner's own
632
+ * `CombosDevelopmentToolTarget` pick gets a clickable hit area. Selecting and
633
+ * persisting edits (e.g. volume) still go through that Target, exactly like any
634
+ * other scene edit, so the object must carry a Target (added by the game code).
635
+ */
636
+ setMarkerOverlay(on) {
637
+ if (this.markerOverlayEnabled === on)
638
+ return;
639
+ this.markerOverlayEnabled = on;
640
+ if (on) {
641
+ // `attachMarkers` requests the pick rescan itself, once the icons exist.
642
+ this.needsMarkerRescan = true;
643
+ }
644
+ else {
645
+ this.detachAllMarkers();
646
+ this.postMarkerOverlaySuccess(false, []);
647
+ // Shrink owners' pick hit areas back now that the icons are gone.
648
+ if (this.enabled) {
649
+ this.needsRescan = true;
650
+ }
651
+ }
652
+ }
653
+ get isMarkerOverlayEnabled() {
654
+ return this.markerOverlayEnabled;
531
655
  }
532
656
  /** Master mute when `SoundSystem` exists; otherwise the last requested value. */
533
657
  get isMuted() {
@@ -537,18 +661,6 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
537
661
  }
538
662
  return this.mutedWithoutSoundSystem;
539
663
  }
540
- /** Pause or resume the game loop (same as `game.pause()` / `game.resume()`). */
541
- setPaused(paused) {
542
- if (paused) {
543
- if (this.game.playing) {
544
- this.game.pause();
545
- }
546
- }
547
- else if (!this.game.playing) {
548
- this.game.resume();
549
- }
550
- this.postStateChanged();
551
- }
552
664
  /**
553
665
  * Mute or unmute audio via `SoundSystem` when registered.
554
666
  * No-op on the audio graph if `SoundSystem` is missing; value is remembered for `isMuted`.
@@ -569,11 +681,26 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
569
681
  this.attachEditMode();
570
682
  this.postSetSuccess(true);
571
683
  }
684
+ // Attaching markers is deferred to run *after* the pick rescan above so that a
685
+ // newly drawn icon's rebuild lands on a later frame (see `attachMarkers`): the
686
+ // icon child needs a rendered container before it contributes to the owner's
687
+ // bounds, and only then does the owner's Target pick get a hit area over it.
688
+ if (this.markerOverlayEnabled && this.needsMarkerRescan) {
689
+ this.needsMarkerRescan = false;
690
+ this.attachMarkers();
691
+ }
572
692
  for (const go of [...this.tapOwners.values()]) {
573
693
  if (go.destroyed) {
574
694
  this.releasePick(go);
575
695
  }
576
696
  }
697
+ // Drop markers whose owner disappeared while the overlay is on.
698
+ for (const [ownerId, marker] of [...this.markerGoByOwner.entries()]) {
699
+ const owner = this.findGameObjectById(ownerId);
700
+ if (!owner || owner.destroyed || marker.destroyed) {
701
+ this.removeMarker(ownerId);
702
+ }
703
+ }
577
704
  if (!this.enabled || !this.selected || !this.outlineGo)
578
705
  return;
579
706
  this.updateSelectionOutline();
@@ -595,11 +722,14 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
595
722
  }
596
723
  }
597
724
  }
598
- /** Re-bind pick targets after dynamic scene changes while enabled. */
725
+ /** Re-bind pick targets / markers after dynamic scene changes while active. */
599
726
  requestSceneRescan() {
600
- if (!this.enabled)
601
- return;
602
- this.needsRescan = true;
727
+ if (this.enabled) {
728
+ this.needsRescan = true;
729
+ }
730
+ if (this.markerOverlayEnabled) {
731
+ this.needsMarkerRescan = true;
732
+ }
603
733
  }
604
734
  attachEditMode() {
605
735
  this.detachAllPicks();
@@ -627,7 +757,10 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
627
757
  }
628
758
  }
629
759
  isIgnoredPickGo(go) {
630
- return go.name === OUTLINE_GO_NAME || go === this.outlineGo;
760
+ return go.name === OUTLINE_GO_NAME || go === this.outlineGo || this.isMarkerGo(go);
761
+ }
762
+ isMarkerGo(go) {
763
+ return typeof go.name === 'string' && go.name.startsWith(MARKER_GO_NAME_PREFIX);
631
764
  }
632
765
  stripGameEvent(go) {
633
766
  if (this.disabledGameEventGoIds.has(go.id))
@@ -833,7 +966,6 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
833
966
  }
834
967
  postStateChanged() {
835
968
  const state = {
836
- paused: this.isPaused,
837
969
  muted: this.isMuted,
838
970
  };
839
971
  const payload = {
@@ -845,6 +977,78 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
845
977
  }
846
978
  this.game.emit(COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, state);
847
979
  }
980
+ /** (Re)build markers for every object owning a registered marker component. */
981
+ attachMarkers() {
982
+ this.detachAllMarkers();
983
+ const list = [];
984
+ for (const tr of this.game.scene.transform.children) {
985
+ this.collectGameObjects(tr.gameObject, list);
986
+ }
987
+ const counts = new Map();
988
+ for (const go of list) {
989
+ if (this.isIgnoredPickGo(go))
990
+ continue;
991
+ const def = resolveMarkerDef(go);
992
+ if (!def)
993
+ continue;
994
+ this.createMarker(go, def);
995
+ counts.set(def.componentName, (counts.get(def.componentName) ?? 0) + 1);
996
+ }
997
+ const markers = [...counts.entries()].map(([componentName, count]) => ({
998
+ componentName,
999
+ count,
1000
+ }));
1001
+ this.postMarkerOverlaySuccess(true, markers);
1002
+ // Rebuild picks next frame: the icons just added need a rendered container
1003
+ // before they enlarge their owners' bounds into a clickable hit area. Runs on
1004
+ // a later frame because the pick rescan block already executed above.
1005
+ if (this.enabled) {
1006
+ this.needsRescan = true;
1007
+ }
1008
+ }
1009
+ createMarker(owner, def) {
1010
+ if (this.markerGoByOwner.has(owner.id))
1011
+ return;
1012
+ const bounds = this.resolvePickBounds(owner);
1013
+ const marker = new engine.GameObject(MARKER_GO_NAME_PREFIX + owner.id, {
1014
+ size: { width: MARKER_ICON_SIZE, height: MARKER_ICON_SIZE },
1015
+ position: { x: bounds.x, y: bounds.y },
1016
+ origin: { x: 0, y: 0 },
1017
+ });
1018
+ const gfx = marker.addComponent(new pluginRendererGraphics.Graphics());
1019
+ if (gfx?.graphics) {
1020
+ drawMarkerIcon(gfx.graphics, def, MARKER_ICON_SIZE);
1021
+ }
1022
+ owner.addChild(marker);
1023
+ this.markerGoByOwner.set(owner.id, marker);
1024
+ }
1025
+ removeMarker(ownerId) {
1026
+ const marker = this.markerGoByOwner.get(ownerId);
1027
+ if (!marker)
1028
+ return;
1029
+ this.markerGoByOwner.delete(ownerId);
1030
+ if (!marker.destroyed) {
1031
+ marker.destroy();
1032
+ }
1033
+ }
1034
+ detachAllMarkers() {
1035
+ for (const ownerId of [...this.markerGoByOwner.keys()]) {
1036
+ this.removeMarker(ownerId);
1037
+ }
1038
+ this.markerGoByOwner.clear();
1039
+ }
1040
+ postMarkerOverlaySuccess(enabled, markers) {
1041
+ const total = markers.reduce((sum, m) => sum + m.count, 0);
1042
+ const state = { enabled, total, markers };
1043
+ const payload = {
1044
+ type: COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS,
1045
+ ...state,
1046
+ };
1047
+ if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
1048
+ window.parent.postMessage(payload, this.postMessageOrigin);
1049
+ }
1050
+ this.game.emit(COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS, state);
1051
+ }
848
1052
  getSoundSystem() {
849
1053
  const system = this.game.getSystem('SoundSystem');
850
1054
  if (!system ||
@@ -1031,31 +1235,36 @@ var CombosDevelopmentToolSystem = CombosDevelopmentToolSystem$1;
1031
1235
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
1032
1236
  Object.assign(CombosDevelopmentToolSystem, {
1033
1237
  packageName: "@combos-fun/plugin-development-tool",
1034
- packageVersion: "0.0.26",
1238
+ packageVersion: "0.0.27",
1035
1239
  });
1036
1240
 
1037
1241
  exports.COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY = COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY;
1038
1242
  exports.COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION = COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION;
1039
1243
  exports.COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED = COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED;
1040
1244
  exports.COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED = COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED;
1245
+ exports.COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS = COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS;
1041
1246
  exports.COMBOS_DEVELOPMENT_TOOL_READY = COMBOS_DEVELOPMENT_TOOL_READY;
1042
1247
  exports.COMBOS_DEVELOPMENT_TOOL_REFRESH = COMBOS_DEVELOPMENT_TOOL_REFRESH;
1043
1248
  exports.COMBOS_DEVELOPMENT_TOOL_SET = COMBOS_DEVELOPMENT_TOOL_SET;
1249
+ exports.COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY = COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY;
1044
1250
  exports.COMBOS_DEVELOPMENT_TOOL_SET_MUTED = COMBOS_DEVELOPMENT_TOOL_SET_MUTED;
1045
- exports.COMBOS_DEVELOPMENT_TOOL_SET_PAUSED = COMBOS_DEVELOPMENT_TOOL_SET_PAUSED;
1046
1251
  exports.COMBOS_DEVELOPMENT_TOOL_SET_SUCCESS = COMBOS_DEVELOPMENT_TOOL_SET_SUCCESS;
1047
1252
  exports.COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED = COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED;
1048
1253
  exports.CombosDevelopmentToolSystem = CombosDevelopmentToolSystem;
1049
1254
  exports.CombosDevelopmentToolTarget = CombosDevelopmentToolTarget;
1050
1255
  exports.DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES = DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES;
1256
+ exports.MARKER_COMPONENTS = MARKER_COMPONENTS;
1257
+ exports.MARKER_ICON_SIZE = MARKER_ICON_SIZE;
1051
1258
  exports.applyPropertyValue = applyPropertyValue;
1052
1259
  exports.applyPropertyWithHooks = applyPropertyWithHooks;
1053
1260
  exports.buildGameObjectDeselectedPayload = buildGameObjectDeselectedPayload;
1054
1261
  exports.buildGameObjectSnapshot = buildGameObjectSnapshot;
1262
+ exports.drawMarkerIcon = drawMarkerIcon;
1055
1263
  exports.isAllowedMessageOrigin = isAllowedMessageOrigin;
1056
1264
  exports.isClearSelectionMessage = isClearSelectionMessage;
1057
1265
  exports.mergeAllowedMessageOrigins = mergeAllowedMessageOrigins;
1058
1266
  exports.readPropertyValue = readPropertyValue;
1059
1267
  exports.readSourceAnchor = readSourceAnchor;
1268
+ exports.resolveMarkerDef = resolveMarkerDef;
1060
1269
  exports.shouldNotifyGameObjectDeselected = shouldNotifyGameObjectDeselected;
1061
1270
  //# sourceMappingURL=plugin-development-tool.cjs.js.map