bee-ladybug 0.1.0
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/README.md +207 -0
- package/package.json +23 -0
- package/src/adapters/CanvasAdapter.js +348 -0
- package/src/adapters/WebDOMAdapter.js +506 -0
- package/src/adapters/python/PythonBridge.js +40 -0
- package/src/core/BeeLadybugCore.js +524 -0
- package/src/core/UIOverlay.js +490 -0
- package/src/index.js +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# BeeLadybug
|
|
2
|
+
|
|
3
|
+
Universal telemetry and visual inspection overlay.
|
|
4
|
+
|
|
5
|
+
BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
|
|
6
|
+
core that accepts raw packets from any source — Canvas 2D games, DOM pages,
|
|
7
|
+
headless bots, Python processes, AI pipelines — and renders them in one
|
|
8
|
+
dark console.
|
|
9
|
+
|
|
10
|
+
> Il Core non sa cosa sta monitorando. Riceve solo dati grezzi.
|
|
11
|
+
|
|
12
|
+
## Perché questa struttura
|
|
13
|
+
|
|
14
|
+
Il file storico `BeeLadybug` era accoppiato a un engine di gioco (entità,
|
|
15
|
+
hitbox, `ctx.drawOverlay`). Quello non scala a un sito HTML o a un modello
|
|
16
|
+
Python. Per questo il progetto riparte da zero con due strati:
|
|
17
|
+
|
|
18
|
+
| Strato | Responsabilità |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `src/core/` | Orologio fittizio, buffer, grafici, overlay cyberpunk |
|
|
21
|
+
| `src/adapters/` | Traduttori. Convertono un mondo specifico in `sendData()` |
|
|
22
|
+
|
|
23
|
+
L'overlay **non** è disegnato sul Canvas. È un pannello HTML in Shadow DOM:
|
|
24
|
+
funziona sopra un gioco, sopra una pagina, o da solo in una sandbox.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
bee-ladybug/
|
|
28
|
+
├── src/
|
|
29
|
+
│ ├── index.js # API pubblica
|
|
30
|
+
│ ├── core/
|
|
31
|
+
│ │ ├── BeeLadybugCore.js # stato, clock, sendData
|
|
32
|
+
│ │ └── UIOverlay.js # console scura + sparkline
|
|
33
|
+
│ └── adapters/
|
|
34
|
+
│ ├── CanvasAdapter.js # hitbox / FPS / collisioni Canvas 2D
|
|
35
|
+
│ ├── WebDOMAdapter.js # box model / hover / mutazioni HTML
|
|
36
|
+
│ └── python/
|
|
37
|
+
│ └── PythonBridge.js # futuro: WebSocket → sendData
|
|
38
|
+
├── examples/
|
|
39
|
+
│ ├── core-sandbox.js # demo senza adapter
|
|
40
|
+
│ ├── core.html
|
|
41
|
+
│ ├── canvas-sandbox.js # demo CanvasAdapter
|
|
42
|
+
│ ├── dom-sandbox.js
|
|
43
|
+
│ └── dom.html
|
|
44
|
+
├── index.html
|
|
45
|
+
├── package.json
|
|
46
|
+
└── README.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Servire la cartella con un server statico (i moduli ES non partono da `file://`):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m http.server 8080
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Aprire `http://localhost:8080`. Premere `F2` / `F3` / `F4`. In console:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
bee.sendData('log', { source: 'human', message: 'hello core' })
|
|
61
|
+
bee.sendData('fps', { value: 61 })
|
|
62
|
+
bee.sendData('error', { source: 'ai', message: 'timeout' })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Uso minimo in un'app:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { BeeLadybugCore } from './src/index.js';
|
|
69
|
+
|
|
70
|
+
const bee = new BeeLadybugCore();
|
|
71
|
+
bee.sendData('log', { message: 'boot', source: 'app' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Core headless (bot, test, Node con DOM assente):
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const bee = new BeeLadybugCore({ ui: false, mount: false, autoAttach: false });
|
|
78
|
+
bee.sendData('metric', { name: 'loss', value: 0.12 });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Contratto `sendData(type, payload)`
|
|
82
|
+
|
|
83
|
+
Gli adapter parlano **solo** così. Uno scalare viene avvolto in `{ value }`.
|
|
84
|
+
|
|
85
|
+
| `type` | `payload` | Effetto |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `fps` | `{ value }` | HUD + grafico storico |
|
|
88
|
+
| `metric` | `{ name, value, unit? }` | HUD + serie storica |
|
|
89
|
+
| `coord` | `{ label?, x, y }` | HUD |
|
|
90
|
+
| `state` | `{ key, value }` | HUD |
|
|
91
|
+
| `log` | `{ message, level?, source? }` | console |
|
|
92
|
+
| `warn` | `{ message, source? }` | console ambra |
|
|
93
|
+
| `error` | `{ message, source? }` | console rossa |
|
|
94
|
+
| `clear` | `{ channel?: 'logs'\|'history'\|'hud' }` | reset buffer |
|
|
95
|
+
| `sys` | emesso dal Core | freeze / visibilità / scala |
|
|
96
|
+
|
|
97
|
+
Ogni pacchetto viene stampato con:
|
|
98
|
+
|
|
99
|
+
- `wallTime` — orologio reale
|
|
100
|
+
- `simTime` — tempo fittizio del Core
|
|
101
|
+
- `frame` — frame-count indipendente dall'host
|
|
102
|
+
|
|
103
|
+
Il freeze **non** ferma l'overlay. Ferma solo il clock fittizio. Gli adapter
|
|
104
|
+
possono ascoltare `type: 'sys'` e mettere in pausa il loro mondo.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
bee.subscribe((packet) => {
|
|
108
|
+
if (packet.type === 'sys' && packet.payload.op === 'freeze') {
|
|
109
|
+
host.paused = packet.payload.frozen;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Tasti
|
|
115
|
+
|
|
116
|
+
| Tasto | Azione |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `F2` | mostra / nascondi overlay (e hitbox canvas) |
|
|
119
|
+
| `F3` | slow-motion del tempo fittizio (`0.25x` / `1x`) |
|
|
120
|
+
| `F4` | freeze / run del tempo fittizio |
|
|
121
|
+
|
|
122
|
+
Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
|
|
123
|
+
|
|
124
|
+
## CanvasAdapter
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { BeeLadybugCore, CanvasAdapter } from './src/index.js';
|
|
128
|
+
|
|
129
|
+
const bee = new BeeLadybugCore();
|
|
130
|
+
const adapter = new CanvasAdapter(bee, {
|
|
131
|
+
canvas, // HTMLCanvasElement del gioco
|
|
132
|
+
entities, // array di AABB, tenuto per riferimento
|
|
133
|
+
overlay: true, // hitbox su canvas stacked (default)
|
|
134
|
+
computeCollisions: true
|
|
135
|
+
});
|
|
136
|
+
adapter.attach();
|
|
137
|
+
|
|
138
|
+
function loop() {
|
|
139
|
+
if (!adapter.frozen) updateGame(adapter.timeScale);
|
|
140
|
+
drawGame();
|
|
141
|
+
adapter.pump(); // una volta per frame host
|
|
142
|
+
requestAnimationFrame(loop);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
L'adapter accetta oggetti qualsiasi con `x/y/width/height` (o `worldX/worldY`).
|
|
147
|
+
Disegna le hitbox su un canvas trasparente sopra il gioco: verde = ok, rosso = collide.
|
|
148
|
+
Espone `frozen` e `timeScale` così l'host può mettere in pausa la simulazione.
|
|
149
|
+
Pacchetti: `fps`, `metric entities`, `metric hits`, `coord` del player, `warn` all'ingresso collisione.
|
|
150
|
+
|
|
151
|
+
## WebDOMAdapter
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import { BeeLadybugCore, WebDOMAdapter } from './src/index.js';
|
|
155
|
+
|
|
156
|
+
const bee = new BeeLadybugCore();
|
|
157
|
+
const adapter = new WebDOMAdapter(bee, {
|
|
158
|
+
root: document.body, // sottoalbero da ispezionare
|
|
159
|
+
watch: ['.card', '#cta'],
|
|
160
|
+
overlay: true,
|
|
161
|
+
autoTick: true // non serve un game loop
|
|
162
|
+
});
|
|
163
|
+
adapter.attach();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
|
|
167
|
+
|
|
168
|
+
## API Core
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const bee = new BeeLadybugCore({
|
|
172
|
+
toggleKey: 'F2',
|
|
173
|
+
slowKey: 'F3',
|
|
174
|
+
freezeKey: 'F4',
|
|
175
|
+
slowScale: 0.25,
|
|
176
|
+
maxLogs: 250,
|
|
177
|
+
historySize: 120,
|
|
178
|
+
mount: 'auto', // document.body, HTMLElement, o false
|
|
179
|
+
ui: true,
|
|
180
|
+
autoAttach: true,
|
|
181
|
+
autoStart: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
bee.sendData(type, payload);
|
|
185
|
+
bee.subscribe(handler); // ritorna unsubscribe
|
|
186
|
+
bee.getState();
|
|
187
|
+
bee.getLogs();
|
|
188
|
+
bee.getHistory('fps');
|
|
189
|
+
bee.getHud();
|
|
190
|
+
bee.show(); bee.hide(); bee.toggle();
|
|
191
|
+
bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
|
|
192
|
+
bee.applySlowMo(); bee.restoreRealtime();
|
|
193
|
+
bee.destroy();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Roadmap
|
|
197
|
+
|
|
198
|
+
1. **Ora** — Core + overlay + CanvasAdapter + WebDOMAdapter. Fatto.
|
|
199
|
+
2. **PythonBridge** — WebSocket JSON `{ type, payload }` verso `sendData`.
|
|
200
|
+
3. **Pacchetto npm** — `import { BeeLadybugCore } from 'bee-ladybug'`.
|
|
201
|
+
|
|
202
|
+
## Come contribuire
|
|
203
|
+
|
|
204
|
+
Il Core resta cieco. Se una feature ha bisogno di conoscere Canvas, DOM o
|
|
205
|
+
Python, vive in un adapter. Patch al Core solo per clock, buffer, protocollo
|
|
206
|
+
o UI condivisa.
|
|
207
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bee-ladybug",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal telemetry and visual inspection overlay for Canvas, DOM, bots, Python and AI flows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"debug",
|
|
17
|
+
"telemetry",
|
|
18
|
+
"overlay",
|
|
19
|
+
"observability",
|
|
20
|
+
"canvas",
|
|
21
|
+
"devtools"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasAdapter — Canvas 2D / game translator.
|
|
3
|
+
*
|
|
4
|
+
* Reads generic AABB entities, measures host FPS, computes collisions,
|
|
5
|
+
* and pushes packets into the core. Hitboxes are drawn on a stacked
|
|
6
|
+
* overlay canvas so the game pixels stay untouched.
|
|
7
|
+
*
|
|
8
|
+
* The core must never import this file.
|
|
9
|
+
*
|
|
10
|
+
* Entity contract (any extra fields are ignored):
|
|
11
|
+
* { name?, id?, x|worldX, y|worldY, width|w, height|h, active?, colliding? }
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const adapter = new CanvasAdapter(core, { canvas, entities });
|
|
15
|
+
* adapter.attach();
|
|
16
|
+
* // after updating entity positions each host frame:
|
|
17
|
+
* adapter.pump();
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const CANVAS_ADAPTER_DEFAULTS = Object.freeze({
|
|
21
|
+
overlay: true,
|
|
22
|
+
computeCollisions: true,
|
|
23
|
+
colorActive: '#3dff6a',
|
|
24
|
+
colorColliding: '#ff3b3b',
|
|
25
|
+
colorInactive: '#8a8a8a',
|
|
26
|
+
source: 'canvas'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export class CanvasAdapter {
|
|
30
|
+
/**
|
|
31
|
+
* @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
32
|
+
* @param {Partial<typeof CANVAS_ADAPTER_DEFAULTS> & {
|
|
33
|
+
* canvas?: HTMLCanvasElement,
|
|
34
|
+
* entities?: object[]
|
|
35
|
+
* }} [options]
|
|
36
|
+
*/
|
|
37
|
+
constructor(core, options = {}) {
|
|
38
|
+
const cfg = { ...CANVAS_ADAPTER_DEFAULTS, ...options };
|
|
39
|
+
this.core = core;
|
|
40
|
+
this.canvas = options.canvas ?? null;
|
|
41
|
+
this.entities = options.entities ?? [];
|
|
42
|
+
this.overlayEnabled = cfg.overlay;
|
|
43
|
+
this.computeCollisions = cfg.computeCollisions;
|
|
44
|
+
this.colorActive = cfg.colorActive;
|
|
45
|
+
this.colorColliding = cfg.colorColliding;
|
|
46
|
+
this.colorInactive = cfg.colorInactive;
|
|
47
|
+
this.source = cfg.source;
|
|
48
|
+
|
|
49
|
+
/** Host should pause simulation when the core freezes. */
|
|
50
|
+
this.frozen = false;
|
|
51
|
+
/** Host should scale its dt by this factor (F3). */
|
|
52
|
+
this.timeScale = 1;
|
|
53
|
+
|
|
54
|
+
this.#attached = false;
|
|
55
|
+
this.#overlay = null;
|
|
56
|
+
this.#unsubscribe = null;
|
|
57
|
+
this.#lastPump = 0;
|
|
58
|
+
this.#fps = 0;
|
|
59
|
+
this.#wasColliding = new Set();
|
|
60
|
+
this.#onResize = () => this.#syncOverlay();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#attached;
|
|
64
|
+
#overlay;
|
|
65
|
+
#unsubscribe;
|
|
66
|
+
#lastPump;
|
|
67
|
+
#fps;
|
|
68
|
+
#wasColliding;
|
|
69
|
+
#onResize;
|
|
70
|
+
|
|
71
|
+
attach() {
|
|
72
|
+
if (this.#attached) return this;
|
|
73
|
+
this.#attached = true;
|
|
74
|
+
this.frozen = Boolean(this.core?.frozen);
|
|
75
|
+
this.timeScale = this.core?.timeScale ?? 1;
|
|
76
|
+
|
|
77
|
+
if (this.core && typeof this.core.subscribe === 'function') {
|
|
78
|
+
this.#unsubscribe = this.core.subscribe((packet) => this.#onCore(packet));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.overlayEnabled) this.#mountOverlay();
|
|
82
|
+
if (typeof window !== 'undefined') {
|
|
83
|
+
window.addEventListener('resize', this.#onResize);
|
|
84
|
+
window.addEventListener('scroll', this.#onResize, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.core?.sendData('log', {
|
|
88
|
+
source: this.source,
|
|
89
|
+
message: `CanvasAdapter attached (${this.entities.length} entities).`
|
|
90
|
+
});
|
|
91
|
+
this.core?.sendData('state', {
|
|
92
|
+
source: this.source,
|
|
93
|
+
key: 'adapter',
|
|
94
|
+
value: 'canvas'
|
|
95
|
+
});
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
detach() {
|
|
100
|
+
if (!this.#attached) return this;
|
|
101
|
+
this.#attached = false;
|
|
102
|
+
if (this.#unsubscribe) this.#unsubscribe();
|
|
103
|
+
this.#unsubscribe = null;
|
|
104
|
+
this.#unmountOverlay();
|
|
105
|
+
if (typeof window !== 'undefined') {
|
|
106
|
+
window.removeEventListener('resize', this.#onResize);
|
|
107
|
+
window.removeEventListener('scroll', this.#onResize, true);
|
|
108
|
+
}
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {HTMLCanvasElement | null} canvas */
|
|
113
|
+
setCanvas(canvas) {
|
|
114
|
+
this.canvas = canvas;
|
|
115
|
+
this.#syncOverlay();
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @param {object[]} entities */
|
|
120
|
+
setEntities(entities) {
|
|
121
|
+
this.entities = entities ?? [];
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Call once per host frame, after entity positions are updated.
|
|
127
|
+
* Pass a 2D context only if overlay is disabled and you want hitboxes
|
|
128
|
+
* drawn onto the game canvas itself.
|
|
129
|
+
* @param {CanvasRenderingContext2D} [ctx]
|
|
130
|
+
*/
|
|
131
|
+
pump(ctx) {
|
|
132
|
+
if (!this.#attached || !this.core) return this;
|
|
133
|
+
|
|
134
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
135
|
+
if (this.#lastPump) {
|
|
136
|
+
const dt = Math.min(Math.max(now - this.#lastPump, 0), 100);
|
|
137
|
+
const instant = dt > 0 ? 1000 / dt : 0;
|
|
138
|
+
this.#fps = this.#fps ? this.#fps * 0.85 + instant * 0.15 : instant;
|
|
139
|
+
}
|
|
140
|
+
this.#lastPump = now;
|
|
141
|
+
|
|
142
|
+
const boxes = this.#collectBoxes();
|
|
143
|
+
this.#markCollisions(boxes);
|
|
144
|
+
this.#emitTelemetry(boxes);
|
|
145
|
+
this.#emitCollisionEdges(boxes);
|
|
146
|
+
this.#drawHitboxes(boxes, ctx);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#onCore(packet) {
|
|
151
|
+
if (packet.type !== 'sys') return;
|
|
152
|
+
const op = packet.payload.op;
|
|
153
|
+
if (op === 'freeze') this.frozen = Boolean(packet.payload.frozen);
|
|
154
|
+
if (op === 'timescale') {
|
|
155
|
+
const scale = Number(packet.payload.scale);
|
|
156
|
+
if (Number.isFinite(scale)) this.timeScale = scale;
|
|
157
|
+
}
|
|
158
|
+
if (op === 'visibility') this.#syncOverlay();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#collectBoxes() {
|
|
162
|
+
const list = this.entities;
|
|
163
|
+
const boxes = [];
|
|
164
|
+
for (let i = 0; i < list.length; i++) {
|
|
165
|
+
const box = aabbOf(list[i], i);
|
|
166
|
+
if (box) boxes.push(box);
|
|
167
|
+
}
|
|
168
|
+
return boxes;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#markCollisions(boxes) {
|
|
172
|
+
for (let i = 0; i < boxes.length; i++) boxes[i].hit = Boolean(boxes[i].entity.colliding);
|
|
173
|
+
|
|
174
|
+
if (!this.computeCollisions) return;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
177
|
+
const a = boxes[i];
|
|
178
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
179
|
+
const b = boxes[j];
|
|
180
|
+
if (!overlaps(a, b)) continue;
|
|
181
|
+
a.hit = true;
|
|
182
|
+
b.hit = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#emitTelemetry(boxes) {
|
|
188
|
+
let hits = 0;
|
|
189
|
+
let tracked = null;
|
|
190
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
191
|
+
if (boxes[i].hit) hits += 1;
|
|
192
|
+
if (!tracked && isPrimary(boxes[i].entity)) tracked = boxes[i];
|
|
193
|
+
}
|
|
194
|
+
if (!tracked && boxes[0]) tracked = boxes[0];
|
|
195
|
+
|
|
196
|
+
if (this.#fps > 0) {
|
|
197
|
+
this.core.sendData('fps', { value: this.#fps, source: this.source });
|
|
198
|
+
}
|
|
199
|
+
this.core.sendData('metric', {
|
|
200
|
+
name: 'entities',
|
|
201
|
+
value: boxes.length,
|
|
202
|
+
source: this.source
|
|
203
|
+
});
|
|
204
|
+
this.core.sendData('metric', {
|
|
205
|
+
name: 'hits',
|
|
206
|
+
value: hits,
|
|
207
|
+
source: this.source
|
|
208
|
+
});
|
|
209
|
+
this.core.sendData('state', {
|
|
210
|
+
source: this.source,
|
|
211
|
+
key: 'hits',
|
|
212
|
+
value: `${hits} / ${boxes.length}`
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (tracked) {
|
|
216
|
+
this.core.sendData('coord', {
|
|
217
|
+
source: this.source,
|
|
218
|
+
label: tracked.name,
|
|
219
|
+
x: tracked.x,
|
|
220
|
+
y: tracked.y
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#emitCollisionEdges(boxes) {
|
|
226
|
+
const now = new Set();
|
|
227
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
228
|
+
if (!boxes[i].hit) continue;
|
|
229
|
+
now.add(boxes[i].id);
|
|
230
|
+
if (!this.#wasColliding.has(boxes[i].id)) {
|
|
231
|
+
this.core.sendData('warn', {
|
|
232
|
+
source: this.source,
|
|
233
|
+
message: `${boxes[i].name} entered collision`
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const id of this.#wasColliding) {
|
|
238
|
+
if (now.has(id)) continue;
|
|
239
|
+
const name = idFromKey(id);
|
|
240
|
+
this.core.sendData('log', {
|
|
241
|
+
source: this.source,
|
|
242
|
+
message: `${name} left collision`
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
this.#wasColliding = now;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#drawHitboxes(boxes, hostCtx) {
|
|
249
|
+
const overlayCtx = this.#overlay ? this.#overlay.getContext('2d') : null;
|
|
250
|
+
if (overlayCtx) this.#syncOverlay();
|
|
251
|
+
|
|
252
|
+
const ctx = overlayCtx || hostCtx;
|
|
253
|
+
if (!ctx) return;
|
|
254
|
+
|
|
255
|
+
const w = ctx.canvas.width;
|
|
256
|
+
const h = ctx.canvas.height;
|
|
257
|
+
if (overlayCtx) ctx.clearRect(0, 0, w, h);
|
|
258
|
+
|
|
259
|
+
if (this.core && this.core.visible === false) return;
|
|
260
|
+
|
|
261
|
+
ctx.save();
|
|
262
|
+
ctx.lineWidth = 1.5;
|
|
263
|
+
ctx.font = '11px ui-monospace, Consolas, monospace';
|
|
264
|
+
ctx.textBaseline = 'bottom';
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
267
|
+
const box = boxes[i];
|
|
268
|
+
const active = box.entity.active !== false;
|
|
269
|
+
const color = box.hit
|
|
270
|
+
? this.colorColliding
|
|
271
|
+
: (active ? this.colorActive : this.colorInactive);
|
|
272
|
+
|
|
273
|
+
ctx.strokeStyle = color;
|
|
274
|
+
ctx.fillStyle = box.hit ? 'rgba(255, 59, 59, 0.16)' : 'rgba(61, 255, 106, 0.08)';
|
|
275
|
+
ctx.fillRect(box.x, box.y, box.w, box.h);
|
|
276
|
+
ctx.strokeRect(box.x + 0.5, box.y + 0.5, box.w, box.h);
|
|
277
|
+
|
|
278
|
+
ctx.fillStyle = color;
|
|
279
|
+
ctx.fillText(box.name, box.x + 3, box.y - 3);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
ctx.restore();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#mountOverlay() {
|
|
286
|
+
if (typeof document === 'undefined' || this.#overlay) return;
|
|
287
|
+
const canvas = document.createElement('canvas');
|
|
288
|
+
canvas.setAttribute('data-bee-ladybug-canvas', '');
|
|
289
|
+
canvas.style.pointerEvents = 'none';
|
|
290
|
+
canvas.style.position = 'fixed';
|
|
291
|
+
canvas.style.zIndex = '2147483645';
|
|
292
|
+
canvas.style.margin = '0';
|
|
293
|
+
document.body.appendChild(canvas);
|
|
294
|
+
this.#overlay = canvas;
|
|
295
|
+
this.#syncOverlay();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#unmountOverlay() {
|
|
299
|
+
if (this.#overlay?.parentNode) this.#overlay.parentNode.removeChild(this.#overlay);
|
|
300
|
+
this.#overlay = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#syncOverlay() {
|
|
304
|
+
const host = this.canvas;
|
|
305
|
+
const overlay = this.#overlay;
|
|
306
|
+
if (!host || !overlay) return;
|
|
307
|
+
const rect = host.getBoundingClientRect();
|
|
308
|
+
overlay.width = host.width;
|
|
309
|
+
overlay.height = host.height;
|
|
310
|
+
overlay.style.left = `${rect.left}px`;
|
|
311
|
+
overlay.style.top = `${rect.top}px`;
|
|
312
|
+
overlay.style.width = `${rect.width}px`;
|
|
313
|
+
overlay.style.height = `${rect.height}px`;
|
|
314
|
+
overlay.style.display = this.core && this.core.visible === false ? 'none' : 'block';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function aabbOf(entity, index) {
|
|
319
|
+
if (!entity) return null;
|
|
320
|
+
const x = numberOr(entity.worldX, entity.x);
|
|
321
|
+
const y = numberOr(entity.worldY, entity.y);
|
|
322
|
+
const w = numberOr(entity.width, entity.w);
|
|
323
|
+
const h = numberOr(entity.height, entity.h);
|
|
324
|
+
if (x == null || y == null || !w || !h || w <= 0 || h <= 0) return null;
|
|
325
|
+
const name = String(entity.name ?? entity.label ?? entity.id ?? `#${index}`);
|
|
326
|
+
const id = entity.id != null ? `id:${entity.id}` : `idx:${index}:${name}`;
|
|
327
|
+
return { entity, x, y, w, h, name, id, hit: false };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function numberOr(a, b) {
|
|
331
|
+
if (typeof a === 'number' && Number.isFinite(a)) return a;
|
|
332
|
+
if (typeof b === 'number' && Number.isFinite(b)) return b;
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function overlaps(a, b) {
|
|
337
|
+
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isPrimary(entity) {
|
|
341
|
+
const key = String(entity.name ?? entity.label ?? entity.role ?? '').toLowerCase();
|
|
342
|
+
return key === 'player' || key === 'hero' || entity.primary === true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function idFromKey(id) {
|
|
346
|
+
const parts = String(id).split(':');
|
|
347
|
+
return parts[parts.length - 1] || id;
|
|
348
|
+
}
|