@clawlabz/clawskin 1.0.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.
@@ -0,0 +1,595 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>ClawSkin</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='28' font-size='28'>🐾</text></svg>">
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
10
+
11
+ /* Fallback if Google Fonts is unreachable (offline/air-gapped) */
12
+ @font-face {
13
+ font-family: 'Press Start 2P Fallback';
14
+ src: local('Courier New'), local('Courier'), local('monospace');
15
+ }
16
+
17
+ * { margin: 0; padding: 0; box-sizing: border-box; }
18
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a1a; font-family: 'Press Start 2P', 'Press Start 2P Fallback', monospace; }
19
+
20
+ /* ── Fullscreen Canvas ── */
21
+ #app-canvas {
22
+ display: block;
23
+ width: 100vw;
24
+ height: 100vh;
25
+ image-rendering: pixelated;
26
+ image-rendering: crisp-edges;
27
+ cursor: pointer;
28
+ }
29
+
30
+ /* ── Pixel HUD (KOF style) ── */
31
+ .hud { position: fixed; font-family: 'Press Start 2P', monospace; z-index: 10; pointer-events: none; }
32
+ .hud * { pointer-events: auto; }
33
+
34
+ /* Top-left: logo + mode */
35
+ .hud-top-left {
36
+ top: 12px; left: 16px;
37
+ display: flex; align-items: center; gap: 10px;
38
+ }
39
+ .hud-logo {
40
+ font-size: 10px;
41
+ color: #00f0ff;
42
+ text-shadow: 0 0 8px rgba(0,240,255,0.5), 2px 2px 0 #0a0a1a;
43
+ letter-spacing: 2px;
44
+ }
45
+ .hud-mode {
46
+ font-size: 7px;
47
+ padding: 2px 8px;
48
+ border: 1px solid;
49
+ border-radius: 2px;
50
+ }
51
+ .hud-mode.live { color: #00ff88; border-color: #00ff88; text-shadow: 0 0 6px rgba(0,255,136,0.5); }
52
+ .hud-mode.demo { color: #b44aff; border-color: #b44aff; }
53
+
54
+ /* Top-right: connection + settings */
55
+ .hud-top-right {
56
+ top: 12px; right: 16px;
57
+ display: flex; gap: 8px; align-items: center;
58
+ }
59
+ .hud-btn {
60
+ font-family: 'Press Start 2P', monospace;
61
+ font-size: 7px;
62
+ padding: 4px 10px;
63
+ background: rgba(0,0,0,0.6);
64
+ border: 1px solid rgba(255,255,255,0.15);
65
+ color: #aaa;
66
+ cursor: pointer;
67
+ transition: all 0.2s;
68
+ }
69
+ .hud-btn:hover { border-color: #00f0ff; color: #00f0ff; }
70
+
71
+ /* Bottom-center: agent roster */
72
+ .hud-bottom {
73
+ bottom: 12px; left: 50%; transform: translateX(-50%);
74
+ display: flex; gap: 6px; align-items: center;
75
+ }
76
+ .hud-agent {
77
+ font-size: 6px;
78
+ padding: 3px 8px;
79
+ background: rgba(0,0,0,0.6);
80
+ border: 1px solid rgba(255,255,255,0.1);
81
+ color: #ccc;
82
+ display: flex; align-items: center; gap: 5px;
83
+ cursor: pointer;
84
+ transition: all 0.2s;
85
+ }
86
+ .hud-agent:hover { border-color: #00f0ff; }
87
+ .hud-agent.active { border-color: #ffcc00; color: #ffcc00; }
88
+ .hud-dot {
89
+ width: 5px; height: 5px;
90
+ border-radius: 50%;
91
+ display: inline-block;
92
+ }
93
+ .hud-dot.idle { background: #00ff88; }
94
+ .hud-dot.typing { background: #00f0ff; animation: blink 0.5s infinite; }
95
+ .hud-dot.thinking { background: #ffcc00; animation: blink 1s infinite; }
96
+ .hud-dot.executing { background: #b44aff; animation: blink 0.3s infinite; }
97
+ .hud-dot.browsing { background: #00f0ff; }
98
+ .hud-dot.error { background: #ff4466; }
99
+ .hud-dot.sleeping { background: #555; }
100
+
101
+ @keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
102
+
103
+ /* Bottom-left: pets button */
104
+ .hud-bottom-left {
105
+ bottom: 12px; left: 16px;
106
+ }
107
+
108
+ /* ── Connection Panel (overlay) ── */
109
+ .conn-overlay {
110
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
111
+ background: rgba(0,0,0,0.85);
112
+ display: none; justify-content: center; align-items: center;
113
+ z-index: 100;
114
+ font-family: 'Press Start 2P', monospace;
115
+ }
116
+ .conn-overlay.show { display: flex; }
117
+ .conn-box {
118
+ background: #12122a;
119
+ border: 2px solid #00f0ff;
120
+ box-shadow: 0 0 30px rgba(0,240,255,0.2);
121
+ padding: 24px;
122
+ width: 380px;
123
+ max-width: 90vw;
124
+ }
125
+ .conn-box h2 {
126
+ font-size: 10px; color: #00f0ff; margin-bottom: 16px;
127
+ text-shadow: 0 0 8px rgba(0,240,255,0.4);
128
+ }
129
+ .conn-box .field { margin-bottom: 12px; }
130
+ .conn-box label { display: block; font-size: 7px; color: #888; margin-bottom: 4px; }
131
+ .conn-box input {
132
+ width: 100%; padding: 8px;
133
+ background: #0a0a1a; border: 1px solid #333;
134
+ color: #e0e0f0; font-family: 'Press Start 2P', monospace; font-size: 7px;
135
+ outline: none;
136
+ }
137
+ .conn-box input:focus { border-color: #00f0ff; }
138
+ .conn-box .hint { font-size: 5px; color: #555; margin-top: 3px; }
139
+ .conn-box .actions { display: flex; gap: 8px; margin-top: 16px; }
140
+ .conn-box .btn-connect {
141
+ flex: 1; padding: 8px; font-family: 'Press Start 2P', monospace; font-size: 8px;
142
+ background: rgba(0,240,255,0.1); border: 1px solid #00f0ff; color: #00f0ff;
143
+ cursor: pointer; transition: all 0.2s;
144
+ }
145
+ .conn-box .btn-connect:hover { background: rgba(0,240,255,0.2); }
146
+ .conn-box .btn-cancel {
147
+ padding: 8px 12px; font-family: 'Press Start 2P', monospace; font-size: 8px;
148
+ background: none; border: 1px solid #444; color: #888;
149
+ cursor: pointer;
150
+ }
151
+ .conn-error { font-size: 6px; color: #ff4466; margin-top: 8px; }
152
+
153
+ /* ── Character Editor (overlay) ── */
154
+ .editor-overlay {
155
+ position: fixed; top: 0; right: 0; bottom: 0;
156
+ width: 280px; max-width: 90vw;
157
+ background: rgba(10,10,26,0.95);
158
+ border-left: 2px solid rgba(0,240,255,0.3);
159
+ z-index: 50;
160
+ display: none;
161
+ flex-direction: column;
162
+ font-family: 'Press Start 2P', monospace;
163
+ overflow-y: auto;
164
+ transform: translateX(100%);
165
+ transition: transform 0.3s ease;
166
+ }
167
+ .editor-overlay.show { display: flex; transform: translateX(0); }
168
+ .editor-overlay .ed-header {
169
+ padding: 12px; font-size: 8px; color: #ffcc00;
170
+ border-bottom: 1px solid rgba(255,255,255,0.1);
171
+ display: flex; justify-content: space-between; align-items: center;
172
+ text-shadow: 0 0 6px rgba(255,204,0,0.4);
173
+ }
174
+ .editor-overlay .ed-close {
175
+ background: none; border: none; color: #666; font-size: 12px; cursor: pointer;
176
+ }
177
+ .editor-overlay .ed-body { padding: 12px; flex: 1; }
178
+ .editor-overlay .ed-row { margin-bottom: 10px; }
179
+ .editor-overlay .ed-row label { display: block; font-size: 6px; color: #888; margin-bottom: 4px; }
180
+ .editor-overlay .swatches { display: flex; gap: 4px; flex-wrap: wrap; }
181
+ .editor-overlay .sw {
182
+ width: 22px; height: 22px; border: 2px solid transparent; cursor: pointer;
183
+ transition: all 0.15s;
184
+ }
185
+ .editor-overlay .sw:hover { transform: scale(1.2); }
186
+ .editor-overlay .sw.on { border-color: #00f0ff; box-shadow: 0 0 6px rgba(0,240,255,0.4); }
187
+ .editor-overlay .btns { display: flex; gap: 3px; flex-wrap: wrap; }
188
+ .editor-overlay .eb {
189
+ padding: 3px 6px; font-family: 'Press Start 2P', monospace; font-size: 5px;
190
+ background: #0a0a1a; border: 1px solid #333; color: #aaa; cursor: pointer;
191
+ }
192
+ .editor-overlay .eb:hover { border-color: #b44aff; color: #fff; }
193
+ .editor-overlay .eb.on { border-color: #00f0ff; color: #00f0ff; background: rgba(0,240,255,0.1); }
194
+ .editor-overlay .ed-actions {
195
+ padding: 12px; border-top: 1px solid rgba(255,255,255,0.1);
196
+ display: flex; gap: 6px;
197
+ }
198
+ .editor-overlay .ed-act {
199
+ flex: 1; padding: 6px; font-family: 'Press Start 2P', monospace; font-size: 6px;
200
+ background: #0a0a1a; border: 1px solid #333; color: #aaa; cursor: pointer;
201
+ }
202
+ .editor-overlay .ed-act:hover { border-color: #00ff88; color: #00ff88; }
203
+
204
+ /* ── Scene Picker (bottom-right) ── */
205
+ .hud-scenes {
206
+ bottom: 12px; right: 16px;
207
+ display: flex; gap: 4px;
208
+ }
209
+ .hud-scene-btn {
210
+ font-family: 'Press Start 2P', monospace;
211
+ font-size: 6px; padding: 3px 8px;
212
+ background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.1);
213
+ color: #888; cursor: pointer;
214
+ }
215
+ .hud-scene-btn:hover { border-color: #b44aff; color: #b44aff; }
216
+ .hud-scene-btn.on { border-color: #b44aff; color: #b44aff; background: rgba(180,74,255,0.1); }
217
+ </style>
218
+ </head>
219
+ <body>
220
+ <!-- ══ Fullscreen Canvas ══ -->
221
+ <canvas id="app-canvas"></canvas>
222
+
223
+ <!-- ══ HUD: Top-Left ══ -->
224
+ <div class="hud hud-top-left">
225
+ <div class="hud-logo">CLAWSKIN</div>
226
+ <div class="hud-mode demo" id="hud-mode">DEMO</div>
227
+ </div>
228
+
229
+ <!-- ══ HUD: Top-Right ══ -->
230
+ <div class="hud hud-top-right">
231
+ <button class="hud-btn" id="btn-connect" onclick="toggleConnPanel()">⚡ CONNECT</button>
232
+ </div>
233
+
234
+ <!-- ══ HUD: Bottom-Left (pets button) ══ -->
235
+ <div class="hud hud-bottom-left">
236
+ <button class="hud-btn" id="btn-pets" onclick="togglePetPanel()">🐾 PETS</button>
237
+ </div>
238
+
239
+ <!-- ══ HUD: Bottom-Center (agent roster) ══ -->
240
+ <div class="hud hud-bottom" id="hud-agents"></div>
241
+
242
+ <!-- ══ HUD: Bottom-Right (scenes) ══ -->
243
+ <div class="hud hud-scenes" id="hud-scenes"></div>
244
+
245
+ <!-- ══ Connection Panel Overlay ══ -->
246
+ <div class="conn-overlay" id="conn-overlay">
247
+ <div class="conn-box">
248
+ <h2>⚡ CONNECT TO GATEWAY</h2>
249
+ <div class="field">
250
+ <label>GATEWAY URL</label>
251
+ <input type="text" id="conn-url" placeholder="wss://your-gateway.example.com" />
252
+ <div class="hint">LOCAL: ws://localhost:18789 · REMOTE: wss://your-gateway.example.com</div>
253
+ </div>
254
+ <div class="field">
255
+ <label>TOKEN</label>
256
+ <input type="password" id="conn-token" placeholder="(if auth enabled)" />
257
+ </div>
258
+ <div class="conn-error" id="conn-error"></div>
259
+ <div class="actions">
260
+ <button class="btn-connect" onclick="doConnect()">CONNECT</button>
261
+ <button class="btn-cancel" onclick="toggleConnPanel()">CANCEL</button>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ <!-- ══ Character Editor Overlay ══ -->
267
+ <div class="editor-overlay" id="editor-overlay">
268
+ <div class="ed-header">
269
+ <span id="ed-title">🎨 CUSTOMIZE</span>
270
+ <button class="ed-close" onclick="closeEditor()">✕</button>
271
+ </div>
272
+ <div class="ed-body" id="ed-body"></div>
273
+ <div class="ed-actions">
274
+ <button class="ed-act" onclick="edRandom()">🎲 RANDOM</button>
275
+ <button class="ed-act" onclick="edSave()">💾 SAVE</button>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- ══ Pet Manager Overlay ══ -->
280
+ <div class="editor-overlay" id="pet-overlay">
281
+ <div class="ed-header">
282
+ <span>🐾 PETS</span>
283
+ <button class="ed-close" onclick="closePetPanel()">✕</button>
284
+ </div>
285
+ <div class="ed-body" id="pet-body"></div>
286
+ <div class="ed-actions">
287
+ <button class="ed-act" onclick="clearAllPets()">🗑 CLEAR ALL</button>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- ══ Scripts ══ -->
292
+ <script src="js/sprites/SpriteGenerator.js"></script>
293
+ <script src="js/character/AnimationManager.js"></script>
294
+ <script src="js/character/BubbleManager.js"></script>
295
+ <script src="js/character/CharacterSprite.js"></script>
296
+ <script src="js/state/DemoMode.js"></script>
297
+ <script src="js/scenes/OfficeScene.js"></script>
298
+ <script src="js/scenes/HackerScene.js"></script>
299
+ <script src="js/scenes/CafeScene.js"></script>
300
+ <script src="js/ui/CharacterEditor.js"></script>
301
+ <script src="js/ui/ScenePicker.js"></script>
302
+ <script src="js/app/SettingsManager.js"></script>
303
+ <script src="js/app/DeviceIdentity.js"></script>
304
+ <script src="js/app/GatewayClient.js"></script>
305
+ <script src="js/app/AgentStateMapper.js"></script>
306
+ <script src="js/app/AgentSlot.js"></script>
307
+ <script src="js/pets/Pet.js"></script>
308
+ <script src="js/pets/PetManager.js"></script>
309
+ <script src="js/app/ClawSkinApp.js"></script>
310
+
311
+ <script>
312
+ // HTML escape helper to prevent XSS from untrusted Gateway data
313
+ function esc(str) {
314
+ if (!str) return '';
315
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
316
+ }
317
+
318
+ // ── Globals ──
319
+ let app, editingSlot = null;
320
+
321
+ // ── Fullscreen canvas sizing ──
322
+ function resizeCanvas() {
323
+ const c = document.getElementById('app-canvas');
324
+ c.width = window.innerWidth;
325
+ c.height = window.innerHeight;
326
+ if (app) { app.width = c.width; app.height = c.height; }
327
+ }
328
+ window.addEventListener('resize', resizeCanvas);
329
+ resizeCanvas();
330
+
331
+ // ── Connection Panel ──
332
+ function toggleConnPanel() {
333
+ const el = document.getElementById('conn-overlay');
334
+ el.classList.toggle('show');
335
+ if (el.classList.contains('show')) {
336
+ const s = app?.settings?.load() || {};
337
+ const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
338
+ document.getElementById('conn-url').value = s.gatewayUrl || (isLocal ? 'ws://localhost:18789' : '');
339
+ document.getElementById('conn-token').value = s.token || '';
340
+ document.getElementById('conn-error').textContent = '';
341
+ }
342
+ }
343
+ function doConnect() {
344
+ let url = document.getElementById('conn-url').value.trim();
345
+ const token = document.getElementById('conn-token').value.trim();
346
+ if (!url) { document.getElementById('conn-error').textContent = 'ENTER A URL'; return; }
347
+ // Auto-fix: upgrade ws:// to wss:// when page is served over HTTPS
348
+ if (window.location.protocol === 'https:' && url.startsWith('ws://') && !url.includes('localhost') && !url.includes('127.0.0.1')) {
349
+ url = url.replace('ws://', 'wss://');
350
+ document.getElementById('conn-url').value = url;
351
+ }
352
+ app.settings.update({ gatewayUrl: url, token, autoConnect: true });
353
+ app.connectToGateway(url, token);
354
+ document.getElementById('conn-overlay').classList.remove('show');
355
+ }
356
+
357
+ // ── Character Editor ──
358
+ function openEditor(slot) {
359
+ editingSlot = slot;
360
+ document.getElementById('ed-title').textContent = '🎨 ' + slot.name;
361
+ renderEditorBody(slot.character.config);
362
+ document.getElementById('editor-overlay').classList.add('show');
363
+ }
364
+ function closeEditor() {
365
+ document.getElementById('editor-overlay').classList.remove('show');
366
+ editingSlot = null;
367
+ }
368
+ function renderEditorBody(cfg) {
369
+ const skins = SpriteGenerator.SKIN_TONES;
370
+ const hairs = SpriteGenerator.HAIR_COLORS;
371
+ const hairNames = ['MESSY', 'SPIKY', 'LONG', 'CURLY', 'BUZZ'];
372
+ const outfitTypes = Object.keys(SpriteGenerator.OUTFIT_COLORS);
373
+ const outfitNames = { hoodie:'HOODIE', shirt:'SHIRT', suit:'SUIT', labcoat:'LABCOAT', tshirt:'TEE' };
374
+ const accNames = { '':'NONE', glasses:'GLASSES', hat:'HAT', headphones:'PHONES', cap:'CAP' };
375
+
376
+ document.getElementById('ed-body').innerHTML = `
377
+ <div class="ed-row"><label>SKIN</label><div class="swatches">
378
+ ${skins.map(c => `<div class="sw ${cfg.skinColor===c?'on':''}" style="background:${c}" onclick="edSet('skinColor','${c}')"></div>`).join('')}
379
+ </div></div>
380
+ <div class="ed-row"><label>HAIR</label><div class="btns">
381
+ ${hairNames.map((n,i) => `<button class="eb ${cfg.hairType===i?'on':''}" onclick="edSet('hairType',${i})">${n}</button>`).join('')}
382
+ </div></div>
383
+ <div class="ed-row"><label>HAIR COLOR</label><div class="swatches">
384
+ ${hairs.map(c => `<div class="sw ${cfg.hairColor===c?'on':''}" style="background:${c}" onclick="edSet('hairColor','${c}')"></div>`).join('')}
385
+ </div></div>
386
+ <div class="ed-row"><label>OUTFIT</label><div class="btns">
387
+ ${outfitTypes.map(t => `<button class="eb ${cfg.outfitType===t?'on':''}" onclick="edSet('outfitType','${t}')">${outfitNames[t]}</button>`).join('')}
388
+ </div></div>
389
+ <div class="ed-row"><label>OUTFIT COLOR</label><div class="swatches">
390
+ ${[0,1,2,3,4].map(i => {
391
+ const c = (SpriteGenerator.OUTFIT_COLORS[cfg.outfitType||'hoodie']||SpriteGenerator.OUTFIT_COLORS.hoodie)[i];
392
+ return `<div class="sw ${cfg.outfitColorIdx===i?'on':''}" style="background:${c}" onclick="edSet('outfitColorIdx',${i})"></div>`;
393
+ }).join('')}
394
+ </div></div>
395
+ <div class="ed-row"><label>ACCESSORY</label><div class="btns">
396
+ ${Object.entries(accNames).map(([k,v]) => `<button class="eb ${(cfg.accessory||'')=== k?'on':''}" onclick="edSet('accessory','${k}')">${v}</button>`).join('')}
397
+ </div></div>
398
+ `;
399
+ }
400
+ function edSet(key, val) {
401
+ if (!editingSlot) return;
402
+ if ((key === 'accessory' || key === 'pet') && val === '') val = null;
403
+ editingSlot.character.updateConfig({ [key]: val });
404
+ editingSlot.updateConfig(editingSlot.character.config);
405
+ renderEditorBody(editingSlot.character.config);
406
+ }
407
+ function edRandom() {
408
+ if (!editingSlot) return;
409
+ const cfg = CharacterSprite.randomConfig();
410
+ editingSlot.character.updateConfig(cfg);
411
+ editingSlot.updateConfig(cfg);
412
+ renderEditorBody(cfg);
413
+ }
414
+ function edSave() { closeEditor(); }
415
+
416
+ // ── Pet Manager ──
417
+ function togglePetPanel() {
418
+ const el = document.getElementById('pet-overlay');
419
+ if (el.classList.contains('show')) {
420
+ closePetPanel();
421
+ } else {
422
+ closeEditor(); // close character editor if open
423
+ renderPetPanel();
424
+ el.classList.add('show');
425
+ }
426
+ }
427
+ function closePetPanel() {
428
+ document.getElementById('pet-overlay').classList.remove('show');
429
+ }
430
+ function renderPetPanel() {
431
+ if (!app?.petManager) return;
432
+ const pm = app.petManager;
433
+ const types = PetManager.getAvailableTypes();
434
+ const typeEmoji = { cat:'🐱', dog:'🐕', robot:'🤖', bird:'🐦', hamster:'🐹' };
435
+ const typeLabel = { cat:'CAT', dog:'DOG', robot:'ROBOT', bird:'BIRD', hamster:'HAMSTER' };
436
+
437
+ const petList = pm.pets.map(p => {
438
+ const colors = Pet.COLORS[p.type] || [];
439
+ const colorSwatches = colors.map(c =>
440
+ `<div class="sw ${p.color===c?'on':''}" style="background:${c};width:16px;height:16px;" onclick="setPetColor('${p.id}','${c}')"></div>`
441
+ ).join('');
442
+ return `<div class="ed-row" style="margin-bottom:8px;border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:6px;">
443
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
444
+ <span style="font-size:7px;color:#ccc;">${typeEmoji[p.type]||''} ${typeLabel[p.type]||p.type}</span>
445
+ <button class="eb" style="color:#ff4466;border-color:#ff4466;" onclick="removePet('${p.id}')">✕</button>
446
+ </div>
447
+ <div class="swatches" style="gap:3px;">${colorSwatches}</div>
448
+ </div>`;
449
+ }).join('');
450
+
451
+ const addBtns = types.map(t =>
452
+ `<button class="eb" onclick="addPet('${t}')">${typeEmoji[t]||''} ${typeLabel[t]||t}</button>`
453
+ ).join('');
454
+
455
+ document.getElementById('pet-body').innerHTML = `
456
+ <div class="ed-row"><label>CURRENT PETS (${pm.pets.length})</label></div>
457
+ ${petList || '<div style="font-size:6px;color:#555;margin-bottom:8px;">No pets yet</div>'}
458
+ <div class="ed-row" style="margin-top:12px;"><label>ADD PET</label><div class="btns">${addBtns}</div></div>
459
+ `;
460
+ }
461
+ function addPet(type) {
462
+ if (!app?.petManager) return;
463
+ app.petManager.addPet(type);
464
+ renderPetPanel();
465
+ }
466
+ function removePet(id) {
467
+ if (!app?.petManager) return;
468
+ app.petManager.removePet(id);
469
+ renderPetPanel();
470
+ }
471
+ function setPetColor(id, color) {
472
+ if (!app?.petManager) return;
473
+ app.petManager.setPetColor(id, color);
474
+ // Clear sprite cache so new color takes effect
475
+ app.spriteGen.cache.clear();
476
+ renderPetPanel();
477
+ }
478
+ function clearAllPets() {
479
+ if (!app?.petManager) return;
480
+ app.petManager.clearPets();
481
+ renderPetPanel();
482
+ }
483
+
484
+ // ── HUD Updates ──
485
+ function updateHUD() {
486
+ if (!app) return;
487
+ // Mode badge
488
+ const modeEl = document.getElementById('hud-mode');
489
+ if (app.mode === 'live') {
490
+ modeEl.textContent = 'LIVE';
491
+ modeEl.className = 'hud-mode live';
492
+ document.getElementById('btn-connect').textContent = '⚡ CONNECTED';
493
+ } else {
494
+ modeEl.textContent = 'DEMO';
495
+ modeEl.className = 'hud-mode demo';
496
+ document.getElementById('btn-connect').textContent = '⚡ CONNECT';
497
+ }
498
+
499
+ // Agent roster
500
+ const agentsEl = document.getElementById('hud-agents');
501
+ if (app.agents.length > 0) {
502
+ agentsEl.innerHTML = app.agents.map((a, i) => {
503
+ const st = a.stateMapper?.currentState || 'idle';
504
+ const sel = editingSlot === a ? ' active' : '';
505
+ return `<div class="hud-agent${sel}" onclick="clickAgentBadge(${i})"><span class="hud-dot ${esc(st)}"></span>${esc(a.name)}</div>`;
506
+ }).join('');
507
+ } else {
508
+ agentsEl.innerHTML = '';
509
+ }
510
+ }
511
+
512
+ function clickAgentBadge(idx) {
513
+ const slot = app.agents[idx];
514
+ if (slot) openEditor(slot);
515
+ }
516
+
517
+ // ── Scene Picker ──
518
+ function initScenePicker() {
519
+ const scenes = app.scenes;
520
+ const el = document.getElementById('hud-scenes');
521
+ el.innerHTML = scenes.map((s, i) =>
522
+ `<button class="hud-scene-btn ${i===0?'on':''}" onclick="setScene(${i})">${s.label}</button>`
523
+ ).join('');
524
+ }
525
+ function setScene(idx) {
526
+ app.setScene(idx);
527
+ document.querySelectorAll('.hud-scene-btn').forEach((b, i) => {
528
+ b.classList.toggle('on', i === idx);
529
+ });
530
+ }
531
+
532
+ // ── Init ──
533
+ document.addEventListener('DOMContentLoaded', () => {
534
+ try {
535
+ app = new ClawSkinApp('app-canvas', {
536
+ width: window.innerWidth,
537
+ height: window.innerHeight,
538
+ });
539
+ app.init();
540
+ window._app = app;
541
+
542
+ // Override connection state callbacks for HUD
543
+ const origStateChange = app._onConnectionStateChange.bind(app);
544
+ app._onConnectionStateChange = (state, detail) => {
545
+ if (state === 'error') {
546
+ document.getElementById('conn-error').textContent = detail || 'CONNECTION FAILED';
547
+ const overlay = document.getElementById('conn-overlay');
548
+ if (!overlay.classList.contains('show')) toggleConnPanel();
549
+ }
550
+ updateHUD();
551
+ };
552
+
553
+ const origConnected = app._onGatewayConnected.bind(app);
554
+ app._onGatewayConnected = async (hello) => {
555
+ await origConnected(hello);
556
+ updateHUD();
557
+ // Keep updating HUD
558
+ setInterval(updateHUD, 2000);
559
+ };
560
+
561
+ const origDisconnected = app._onGatewayDisconnected.bind(app);
562
+ app._onGatewayDisconnected = (info) => {
563
+ origDisconnected(info);
564
+ updateHUD();
565
+ };
566
+
567
+ // Canvas click → open editor for clicked agent
568
+ document.getElementById('app-canvas').addEventListener('click', (e) => {
569
+ if (app.mode !== 'live') return;
570
+ const c = document.getElementById('app-canvas');
571
+ const rect = c.getBoundingClientRect();
572
+ const sx = app.width / rect.width;
573
+ const sy = app.height / rect.height;
574
+ const cx = (e.clientX - rect.left) * sx;
575
+ const cy = (e.clientY - rect.top) * sy;
576
+ for (const slot of app.agents) {
577
+ if (slot.hitTest(cx, cy)) {
578
+ openEditor(slot);
579
+ return;
580
+ }
581
+ }
582
+ // Clicked empty space → close editor
583
+ closeEditor();
584
+ });
585
+
586
+ initScenePicker();
587
+ app.start();
588
+ updateHUD();
589
+ } catch (e) {
590
+ console.error('[ClawSkin] init failed:', e);
591
+ }
592
+ });
593
+ </script>
594
+ </body>
595
+ </html>