@clawlabz/clawskin 1.0.4 → 1.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/CHANGELOG.md +145 -85
- package/README.md +46 -90
- package/docs/TECHNICAL.md +134 -0
- package/docs/images/screenshot.png +0 -0
- package/package.json +1 -1
- package/public/app.html +413 -17
- package/public/js/app/GatewayClient.js +3 -0
- package/public/js/pets/Pet.js +105 -6
- package/public/js/pets/PetManager.js +66 -0
- package/public/js/scenes/CafeScene.js +102 -32
- package/public/js/scenes/HackerScene.js +29 -2
- package/public/js/scenes/OfficeScene.js +125 -8
package/public/app.html
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/* ── Pixel HUD (KOF style) ── */
|
|
31
|
-
.hud { position: fixed; font-family: 'Press Start 2P', monospace; z-index: 10; pointer-events: none; }
|
|
31
|
+
.hud { position: fixed; font-family: 'Press Start 2P', monospace; z-index: 10; pointer-events: none; zoom: var(--ui-scale, 1); }
|
|
32
32
|
.hud * { pointer-events: auto; }
|
|
33
33
|
|
|
34
34
|
/* Top-left: logo + mode */
|
|
@@ -121,6 +121,7 @@
|
|
|
121
121
|
padding: 24px;
|
|
122
122
|
width: 380px;
|
|
123
123
|
max-width: 90vw;
|
|
124
|
+
zoom: var(--ui-scale, 1);
|
|
124
125
|
}
|
|
125
126
|
.conn-box h2 {
|
|
126
127
|
font-size: 10px; color: #00f0ff; margin-bottom: 16px;
|
|
@@ -153,28 +154,39 @@
|
|
|
153
154
|
/* ── Character Editor (overlay) ── */
|
|
154
155
|
.editor-overlay {
|
|
155
156
|
position: fixed; top: 0; right: 0; bottom: 0;
|
|
156
|
-
width:
|
|
157
|
+
width: 360px; max-width: 90vw;
|
|
157
158
|
background: rgba(10,10,26,0.95);
|
|
158
159
|
border-left: 2px solid rgba(0,240,255,0.3);
|
|
159
160
|
z-index: 50;
|
|
160
161
|
display: none;
|
|
161
162
|
flex-direction: column;
|
|
162
163
|
font-family: 'Press Start 2P', monospace;
|
|
163
|
-
overflow
|
|
164
|
+
overflow: hidden;
|
|
164
165
|
transform: translateX(100%);
|
|
165
166
|
transition: transform 0.3s ease;
|
|
167
|
+
zoom: var(--ui-scale, 1);
|
|
166
168
|
}
|
|
167
169
|
.editor-overlay.show { display: flex; transform: translateX(0); }
|
|
168
170
|
.editor-overlay .ed-header {
|
|
169
|
-
padding: 12px; font-size: 8px; color: #ffcc00;
|
|
171
|
+
padding: 8px 12px; font-size: 8px; color: #ffcc00;
|
|
170
172
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
171
173
|
display: flex; justify-content: space-between; align-items: center;
|
|
172
174
|
text-shadow: 0 0 6px rgba(255,204,0,0.4);
|
|
175
|
+
flex-shrink: 0;
|
|
173
176
|
}
|
|
177
|
+
.editor-overlay .ed-header-btns {
|
|
178
|
+
display: flex; gap: 6px; align-items: center;
|
|
179
|
+
}
|
|
180
|
+
.editor-overlay .ed-header-btn {
|
|
181
|
+
font-family: 'Press Start 2P', monospace; font-size: 6px;
|
|
182
|
+
padding: 3px 8px; background: #0a0a1a; border: 1px solid #333;
|
|
183
|
+
color: #aaa; cursor: pointer; transition: all 0.2s;
|
|
184
|
+
}
|
|
185
|
+
.editor-overlay .ed-header-btn:hover { border-color: #00ff88; color: #00ff88; }
|
|
174
186
|
.editor-overlay .ed-close {
|
|
175
187
|
background: none; border: none; color: #666; font-size: 12px; cursor: pointer;
|
|
176
188
|
}
|
|
177
|
-
.editor-overlay .ed-body { padding: 12px; flex:
|
|
189
|
+
.editor-overlay .ed-body { padding: 12px; flex-shrink: 0; overflow-y: auto; max-height: 45%; }
|
|
178
190
|
.editor-overlay .ed-row { margin-bottom: 10px; }
|
|
179
191
|
.editor-overlay .ed-row label { display: block; font-size: 6px; color: #888; margin-bottom: 4px; }
|
|
180
192
|
.editor-overlay .swatches { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
@@ -201,6 +213,70 @@
|
|
|
201
213
|
}
|
|
202
214
|
.editor-overlay .ed-act:hover { border-color: #00ff88; color: #00ff88; }
|
|
203
215
|
|
|
216
|
+
/* ── Chat Panel (inside editor overlay) ── */
|
|
217
|
+
.ed-chat {
|
|
218
|
+
border-top: 1px solid rgba(0,240,255,0.2);
|
|
219
|
+
display: flex; flex-direction: column;
|
|
220
|
+
flex: 1; min-height: 0; overflow: hidden;
|
|
221
|
+
}
|
|
222
|
+
.ed-chat-header {
|
|
223
|
+
padding: 8px 12px; font-size: 7px; color: #00f0ff;
|
|
224
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
225
|
+
}
|
|
226
|
+
.ed-chat-messages {
|
|
227
|
+
flex: 1; overflow-y: auto; padding: 10px;
|
|
228
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
229
|
+
}
|
|
230
|
+
.ed-chat-msg {
|
|
231
|
+
font-size: 8px; line-height: 1.8; padding: 8px 10px;
|
|
232
|
+
max-width: 90%; word-break: break-word;
|
|
233
|
+
}
|
|
234
|
+
.ed-chat-msg.agent {
|
|
235
|
+
align-self: flex-start;
|
|
236
|
+
background: rgba(180,74,255,0.1); border: 1px solid rgba(180,74,255,0.3);
|
|
237
|
+
color: #ccc;
|
|
238
|
+
}
|
|
239
|
+
.ed-chat-msg.user {
|
|
240
|
+
align-self: flex-end;
|
|
241
|
+
background: rgba(0,240,255,0.1); border: 1px solid rgba(0,240,255,0.3);
|
|
242
|
+
color: #e0e0f0;
|
|
243
|
+
}
|
|
244
|
+
.ed-chat-msg .msg-role {
|
|
245
|
+
font-size: 6px; color: #666; margin-bottom: 3px;
|
|
246
|
+
}
|
|
247
|
+
.ed-chat-msg.agent .msg-role { color: #b44aff; }
|
|
248
|
+
.ed-chat-msg.user .msg-role { color: #00f0ff; }
|
|
249
|
+
.ed-chat-typing {
|
|
250
|
+
align-self: flex-start; font-size: 8px; color: #b44aff;
|
|
251
|
+
padding: 8px 10px; animation: blink 1s infinite;
|
|
252
|
+
}
|
|
253
|
+
.ed-chat-empty {
|
|
254
|
+
font-size: 8px; color: #444; text-align: center; padding: 20px 8px;
|
|
255
|
+
}
|
|
256
|
+
.ed-chat-input-row {
|
|
257
|
+
display: flex; padding: 0;
|
|
258
|
+
border-top: 1px solid rgba(0,240,255,0.2);
|
|
259
|
+
flex-shrink: 0;
|
|
260
|
+
}
|
|
261
|
+
.ed-chat-input {
|
|
262
|
+
flex: 1; min-width: 0; width: 100%; padding: 10px 12px;
|
|
263
|
+
background: rgba(0,0,0,0.4); border: none; border-right: 1px solid rgba(0,240,255,0.15);
|
|
264
|
+
color: #e0e0f0; font-family: 'Press Start 2P', monospace; font-size: 8px;
|
|
265
|
+
outline: none; box-shadow: none; -webkit-appearance: none; border-radius: 0;
|
|
266
|
+
}
|
|
267
|
+
.ed-chat-input::placeholder { color: #444; }
|
|
268
|
+
.ed-chat-input:focus { background: rgba(0,240,255,0.05); outline: none; box-shadow: none; }
|
|
269
|
+
.ed-chat-send {
|
|
270
|
+
padding: 10px 14px; background: rgba(0,240,255,0.1);
|
|
271
|
+
border: none; color: #00f0ff;
|
|
272
|
+
font-family: 'Press Start 2P', monospace; font-size: 8px;
|
|
273
|
+
cursor: pointer; transition: all 0.2s; flex-shrink: 0;
|
|
274
|
+
}
|
|
275
|
+
.ed-chat-send:hover { background: rgba(0,240,255,0.25); color: #fff; }
|
|
276
|
+
cursor: pointer; transition: all 0.2s;
|
|
277
|
+
}
|
|
278
|
+
.ed-chat-send:hover { background: rgba(0,240,255,0.2); }
|
|
279
|
+
|
|
204
280
|
/* ── Scene Picker (bottom-right) ── */
|
|
205
281
|
.hud-scenes {
|
|
206
282
|
bottom: 12px; right: 16px;
|
|
@@ -267,12 +343,19 @@
|
|
|
267
343
|
<div class="editor-overlay" id="editor-overlay">
|
|
268
344
|
<div class="ed-header">
|
|
269
345
|
<span id="ed-title">🎨 CUSTOMIZE</span>
|
|
270
|
-
<
|
|
346
|
+
<div class="ed-header-btns">
|
|
347
|
+
<button class="ed-header-btn" onclick="edRandom()">🎲 RANDOM</button>
|
|
348
|
+
<button class="ed-close" onclick="closeEditor()">✕</button>
|
|
349
|
+
</div>
|
|
271
350
|
</div>
|
|
272
351
|
<div class="ed-body" id="ed-body"></div>
|
|
273
|
-
<div class="ed-
|
|
274
|
-
<
|
|
275
|
-
<
|
|
352
|
+
<div class="ed-chat" id="ed-chat" style="display:none;">
|
|
353
|
+
<div class="ed-chat-header">💬 CHAT</div>
|
|
354
|
+
<div class="ed-chat-messages" id="ed-chat-messages"></div>
|
|
355
|
+
<div class="ed-chat-input-row">
|
|
356
|
+
<input type="text" class="ed-chat-input" id="ed-chat-input" placeholder="Message..." onkeydown="if(event.key==='Enter')sendChatMsg()" />
|
|
357
|
+
<button class="ed-chat-send" onclick="sendChatMsg()">▶</button>
|
|
358
|
+
</div>
|
|
276
359
|
</div>
|
|
277
360
|
</div>
|
|
278
361
|
|
|
@@ -317,6 +400,7 @@
|
|
|
317
400
|
|
|
318
401
|
// ── Globals ──
|
|
319
402
|
let app, editingSlot = null;
|
|
403
|
+
let chatSessionKey = null, chatStreamingEl = null;
|
|
320
404
|
|
|
321
405
|
// ── Fullscreen canvas sizing ──
|
|
322
406
|
// Fixed logical resolution — CSS scales it up with pixelated rendering.
|
|
@@ -324,6 +408,16 @@
|
|
|
324
408
|
const LOGICAL_W = 960;
|
|
325
409
|
const LOGICAL_H = 600;
|
|
326
410
|
|
|
411
|
+
// ── UI scaling for HUD/panels on large screens ──
|
|
412
|
+
function updateUIScale() {
|
|
413
|
+
// Only scale UI on screens wider than 2K (2560px).
|
|
414
|
+
// Below 2K, keep native size (scale=1) to avoid bloated HUD/panels.
|
|
415
|
+
const scale = window.innerWidth > 2560 ? window.innerWidth / 2560 : 1;
|
|
416
|
+
document.documentElement.style.setProperty('--ui-scale', scale.toFixed(2));
|
|
417
|
+
}
|
|
418
|
+
updateUIScale();
|
|
419
|
+
window.addEventListener('resize', updateUIScale);
|
|
420
|
+
|
|
327
421
|
function resizeCanvas() {
|
|
328
422
|
const c = document.getElementById('app-canvas');
|
|
329
423
|
c.width = LOGICAL_W;
|
|
@@ -365,10 +459,15 @@
|
|
|
365
459
|
document.getElementById('ed-title').textContent = '🎨 ' + slot.name;
|
|
366
460
|
renderEditorBody(slot.character.config);
|
|
367
461
|
document.getElementById('editor-overlay').classList.add('show');
|
|
462
|
+
loadChat(slot);
|
|
368
463
|
}
|
|
369
464
|
function closeEditor() {
|
|
370
465
|
document.getElementById('editor-overlay').classList.remove('show');
|
|
371
466
|
editingSlot = null;
|
|
467
|
+
chatSessionKey = null;
|
|
468
|
+
chatStreamingEl = null;
|
|
469
|
+
const chatEl = document.getElementById('ed-chat');
|
|
470
|
+
if (chatEl) chatEl.style.display = 'none';
|
|
372
471
|
}
|
|
373
472
|
function renderEditorBody(cfg) {
|
|
374
473
|
const skins = SpriteGenerator.SKIN_TONES;
|
|
@@ -416,7 +515,134 @@
|
|
|
416
515
|
editingSlot.updateConfig(cfg);
|
|
417
516
|
renderEditorBody(cfg);
|
|
418
517
|
}
|
|
419
|
-
|
|
518
|
+
// ── Agent Chat ──
|
|
519
|
+
function loadChat(slot) {
|
|
520
|
+
const chatEl = document.getElementById('ed-chat');
|
|
521
|
+
const msgsEl = document.getElementById('ed-chat-messages');
|
|
522
|
+
chatSessionKey = null;
|
|
523
|
+
chatStreamingEl = null;
|
|
524
|
+
|
|
525
|
+
// Only show chat in live mode with valid sessionKeys
|
|
526
|
+
if (!app || app.mode !== 'live' || !app.gateway?.connected) {
|
|
527
|
+
chatEl.style.display = 'none';
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const keys = slot.stateMapper ? [...slot.stateMapper.sessionKeys] : [];
|
|
531
|
+
if (keys.length === 0 || keys[0] === 'main') {
|
|
532
|
+
chatEl.style.display = 'none';
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
chatSessionKey = keys[0];
|
|
537
|
+
chatEl.style.display = 'flex';
|
|
538
|
+
msgsEl.innerHTML = '<div class="ed-chat-empty">Loading...</div>';
|
|
539
|
+
|
|
540
|
+
app.gateway.getChatHistory(chatSessionKey, 50).then(result => {
|
|
541
|
+
if (editingSlot !== slot) return; // user switched agents
|
|
542
|
+
const messages = result?.messages || result || [];
|
|
543
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
544
|
+
msgsEl.innerHTML = '<div class="ed-chat-empty">No messages yet</div>';
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
msgsEl.innerHTML = '';
|
|
548
|
+
for (const msg of messages) {
|
|
549
|
+
const role = msg.role === 'user' ? 'user' : 'agent';
|
|
550
|
+
const text = extractMsgText(msg);
|
|
551
|
+
if (text) appendChatMessage(role, text);
|
|
552
|
+
}
|
|
553
|
+
}).catch(() => {
|
|
554
|
+
if (editingSlot !== slot) return;
|
|
555
|
+
msgsEl.innerHTML = '<div class="ed-chat-empty">Could not load history</div>';
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function extractMsgText(msg) {
|
|
560
|
+
if (!msg) return null;
|
|
561
|
+
if (typeof msg.text === 'string') return msg.text;
|
|
562
|
+
if (typeof msg.content === 'string') return msg.content;
|
|
563
|
+
if (Array.isArray(msg.content)) {
|
|
564
|
+
for (const block of msg.content) {
|
|
565
|
+
if (block.type === 'text' && block.text) return block.text;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (typeof msg.message === 'object') return extractMsgText(msg.message);
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function appendChatMessage(role, text) {
|
|
573
|
+
const msgsEl = document.getElementById('ed-chat-messages');
|
|
574
|
+
// Remove empty placeholder if present
|
|
575
|
+
const empty = msgsEl.querySelector('.ed-chat-empty');
|
|
576
|
+
if (empty) empty.remove();
|
|
577
|
+
|
|
578
|
+
const div = document.createElement('div');
|
|
579
|
+
div.className = 'ed-chat-msg ' + role;
|
|
580
|
+
const roleLabel = role === 'user' ? 'YOU' : (editingSlot?.name || 'AGENT');
|
|
581
|
+
div.innerHTML = '<div class="msg-role">' + esc(roleLabel) + '</div>' + esc(text);
|
|
582
|
+
msgsEl.appendChild(div);
|
|
583
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function sendChatMsg() {
|
|
587
|
+
const input = document.getElementById('ed-chat-input');
|
|
588
|
+
const text = (input.value || '').trim();
|
|
589
|
+
if (!text || !chatSessionKey || !app?.gateway?.connected) return;
|
|
590
|
+
input.value = '';
|
|
591
|
+
|
|
592
|
+
appendChatMessage('user', text);
|
|
593
|
+
app.gateway.sendChat(chatSessionKey, text).catch(err => {
|
|
594
|
+
appendChatMessage('agent', 'Error: ' + (err.message || 'Send failed'));
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function handleEditorChatEvent(agentId, payload) {
|
|
599
|
+
if (!editingSlot || !payload) return;
|
|
600
|
+
// Only process events for the agent we're currently editing
|
|
601
|
+
if (editingSlot.agentId !== agentId) return;
|
|
602
|
+
|
|
603
|
+
const msgsEl = document.getElementById('ed-chat-messages');
|
|
604
|
+
if (!msgsEl) return;
|
|
605
|
+
|
|
606
|
+
switch (payload.state) {
|
|
607
|
+
case 'delta': {
|
|
608
|
+
if (!chatStreamingEl) {
|
|
609
|
+
// Remove empty placeholder
|
|
610
|
+
const empty = msgsEl.querySelector('.ed-chat-empty');
|
|
611
|
+
if (empty) empty.remove();
|
|
612
|
+
chatStreamingEl = document.createElement('div');
|
|
613
|
+
chatStreamingEl.className = 'ed-chat-typing';
|
|
614
|
+
chatStreamingEl.textContent = '...';
|
|
615
|
+
msgsEl.appendChild(chatStreamingEl);
|
|
616
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case 'final': {
|
|
621
|
+
if (chatStreamingEl) {
|
|
622
|
+
chatStreamingEl.remove();
|
|
623
|
+
chatStreamingEl = null;
|
|
624
|
+
}
|
|
625
|
+
const text = extractMsgText(payload.message || payload);
|
|
626
|
+
if (text) appendChatMessage('agent', text);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'error': {
|
|
630
|
+
if (chatStreamingEl) {
|
|
631
|
+
chatStreamingEl.remove();
|
|
632
|
+
chatStreamingEl = null;
|
|
633
|
+
}
|
|
634
|
+
appendChatMessage('agent', 'Something went wrong...');
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case 'aborted': {
|
|
638
|
+
if (chatStreamingEl) {
|
|
639
|
+
chatStreamingEl.remove();
|
|
640
|
+
chatStreamingEl = null;
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
420
646
|
|
|
421
647
|
// ── Pet Manager ──
|
|
422
648
|
function togglePetPanel() {
|
|
@@ -519,6 +745,99 @@
|
|
|
519
745
|
if (slot) openEditor(slot);
|
|
520
746
|
}
|
|
521
747
|
|
|
748
|
+
// ── Easter Egg Helpers ──
|
|
749
|
+
function showWeatherToast(weather) {
|
|
750
|
+
const labels = { sunny:'☀️ Sunny', night:'🌙 Night', rain:'🌧️ Rain', snow:'❄️ Snow',
|
|
751
|
+
purple:'💜 Neon', red_alert:'🔴 Alert', matrix:'💚 Matrix', blackout:'🌑 Blackout',
|
|
752
|
+
fog:'🌫️ Fog' };
|
|
753
|
+
showDecoToast(labels[weather] || weather);
|
|
754
|
+
}
|
|
755
|
+
function showDecoToast(msg) {
|
|
756
|
+
const toast = document.createElement('div');
|
|
757
|
+
toast.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:200;' +
|
|
758
|
+
'font-family:"Press Start 2P",monospace;font-size:10px;color:#fff;background:rgba(10,10,26,0.9);' +
|
|
759
|
+
'border:1px solid rgba(0,240,255,0.4);padding:10px 18px;pointer-events:none;text-align:center;' +
|
|
760
|
+
'text-shadow:0 0 6px rgba(0,240,255,0.4);opacity:0;transition:opacity 0.3s;';
|
|
761
|
+
toast.textContent = msg;
|
|
762
|
+
document.body.appendChild(toast);
|
|
763
|
+
requestAnimationFrame(() => toast.style.opacity = '1');
|
|
764
|
+
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 1800);
|
|
765
|
+
}
|
|
766
|
+
function handleClockClick(scene, cx, cy, w, wallSafe) {
|
|
767
|
+
let clockX, clockY;
|
|
768
|
+
if (scene.name === 'office') { clockX = w - 45; clockY = wallSafe + 8; }
|
|
769
|
+
else if (scene.name === 'hacker') { clockX = w - 30; clockY = wallSafe + 8; }
|
|
770
|
+
else if (scene.name === 'cafe') { clockX = w - 40; clockY = wallSafe + 8; }
|
|
771
|
+
else return false;
|
|
772
|
+
if (Math.hypot(cx - clockX, cy - (clockY + 10)) < 16) {
|
|
773
|
+
const now = new Date();
|
|
774
|
+
const h = now.getHours().toString().padStart(2,'0');
|
|
775
|
+
const m = now.getMinutes().toString().padStart(2,'0');
|
|
776
|
+
const s = now.getSeconds().toString().padStart(2,'0');
|
|
777
|
+
showDecoToast('🕐 ' + h + ':' + m + ':' + s);
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
function handlePlantClick(cx, cy, w, floorY, h) {
|
|
783
|
+
const floorH = h - floorY;
|
|
784
|
+
// Left plant areas
|
|
785
|
+
if (cx < 30 && cy > floorY + floorH * 0.25 && cy < floorY + floorH * 0.40) {
|
|
786
|
+
showDecoToast(['🌱 *wiggle*', '🌿 Water me!', '🪴 I\'m growing!'][Math.floor(Math.random()*3)]);
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
// Right plant areas
|
|
790
|
+
if (cx > w - 30 && cy > floorY + floorH * 0.60 && cy < floorY + floorH * 0.80) {
|
|
791
|
+
showDecoToast(['🌵 Don\'t touch!', '🌻 Hello!', '🍀 Lucky!'][Math.floor(Math.random()*3)]);
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
function getRandomQuote() {
|
|
797
|
+
const quotes = [
|
|
798
|
+
'📚 "Code is poetry"',
|
|
799
|
+
'📖 "Ship it!"',
|
|
800
|
+
'📚 "RTFM" - Ancient proverb',
|
|
801
|
+
'📖 "It works on my machine"',
|
|
802
|
+
'📚 "Hello, World!"',
|
|
803
|
+
'📖 "There are only 2 hard things..."',
|
|
804
|
+
];
|
|
805
|
+
return quotes[Math.floor(Math.random() * quotes.length)];
|
|
806
|
+
}
|
|
807
|
+
function getRandomIdea() {
|
|
808
|
+
const ideas = [
|
|
809
|
+
'💡 TODO: Fix everything',
|
|
810
|
+
'📝 Sprint #42: Ship it',
|
|
811
|
+
'💡 Refactor the refactor',
|
|
812
|
+
'📝 Q3 Goal: ????→Profit',
|
|
813
|
+
'💡 "Just one more feature"',
|
|
814
|
+
'📝 Bug ≠ Feature ...or is it?',
|
|
815
|
+
];
|
|
816
|
+
return ideas[Math.floor(Math.random() * ideas.length)];
|
|
817
|
+
}
|
|
818
|
+
function getRandomArcade() {
|
|
819
|
+
const msgs = [
|
|
820
|
+
'🕹️ INSERT COIN',
|
|
821
|
+
'👾 HIGH SCORE: 99999',
|
|
822
|
+
'🎮 GAME OVER',
|
|
823
|
+
'🕹️ PLAYER 1 READY',
|
|
824
|
+
'👾 LEVEL UP!',
|
|
825
|
+
'🎮 CONTINUE? 9...8...7...',
|
|
826
|
+
];
|
|
827
|
+
return msgs[Math.floor(Math.random() * msgs.length)];
|
|
828
|
+
}
|
|
829
|
+
function getRandomMenu() {
|
|
830
|
+
const items = [
|
|
831
|
+
'☕ Espresso ....... $4',
|
|
832
|
+
'🧋 Matcha Latte ... $6',
|
|
833
|
+
'🍰 Cheesecake ..... $7',
|
|
834
|
+
'🥐 Croissant ...... $3',
|
|
835
|
+
'🫖 Earl Grey ...... $3',
|
|
836
|
+
'🍪 Cookie ......... $2',
|
|
837
|
+
];
|
|
838
|
+
return items[Math.floor(Math.random() * items.length)];
|
|
839
|
+
}
|
|
840
|
+
|
|
522
841
|
// ── Scene Picker ──
|
|
523
842
|
function initScenePicker() {
|
|
524
843
|
const scenes = app.scenes;
|
|
@@ -569,23 +888,100 @@
|
|
|
569
888
|
updateHUD();
|
|
570
889
|
};
|
|
571
890
|
|
|
572
|
-
//
|
|
891
|
+
// Hook Gateway events to also feed the editor chat panel
|
|
892
|
+
const origGatewayEvent = app._onGatewayEvent.bind(app);
|
|
893
|
+
app._onGatewayEvent = (event) => {
|
|
894
|
+
origGatewayEvent(event);
|
|
895
|
+
// Route chat events to the editor chat panel
|
|
896
|
+
if (event?.event === 'chat' && editingSlot) {
|
|
897
|
+
const sessionKey = event.payload?.sessionKey;
|
|
898
|
+
if (sessionKey) {
|
|
899
|
+
const match = sessionKey.match(/^agent:([^:]+):/);
|
|
900
|
+
const agentId = match ? match[1] : 'main';
|
|
901
|
+
handleEditorChatEvent(agentId, event.payload);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
// Canvas click → agents, pets, weather, treats, decorations
|
|
573
907
|
document.getElementById('app-canvas').addEventListener('click', (e) => {
|
|
574
|
-
if (app.mode !== 'live') return;
|
|
575
908
|
const c = document.getElementById('app-canvas');
|
|
576
909
|
const rect = c.getBoundingClientRect();
|
|
577
910
|
const sx = app.width / rect.width;
|
|
578
911
|
const sy = app.height / rect.height;
|
|
579
912
|
const cx = (e.clientX - rect.left) * sx;
|
|
580
913
|
const cy = (e.clientY - rect.top) * sy;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
914
|
+
|
|
915
|
+
// Live mode: agent click → open editor
|
|
916
|
+
if (app.mode === 'live') {
|
|
917
|
+
for (const slot of app.agents) {
|
|
918
|
+
if (slot.hitTest(cx, cy)) {
|
|
919
|
+
openEditor(slot);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Pet click → reaction bubble
|
|
926
|
+
if (app.petManager && app.petManager.handleClick(cx, cy)) return;
|
|
927
|
+
|
|
928
|
+
// Window/weather click — cycle weather
|
|
929
|
+
if (app.currentScene && app.currentScene.getWindowRect) {
|
|
930
|
+
const wr = app.currentScene.getWindowRect();
|
|
931
|
+
if (cx >= wr.x && cx <= wr.x + wr.w && cy >= wr.y && cy <= wr.y + wr.h) {
|
|
932
|
+
const newWeather = app.currentScene.cycleWeather();
|
|
933
|
+
showWeatherToast(newWeather);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Decoration clicks
|
|
939
|
+
if (app.currentScene) {
|
|
940
|
+
const scene = app.currentScene;
|
|
941
|
+
const h = app.height, w = app.width;
|
|
942
|
+
const floorY = Math.round(h * 0.40);
|
|
943
|
+
const wallSafe = Math.round(h * 0.12);
|
|
944
|
+
|
|
945
|
+
// Clock click → show time
|
|
946
|
+
if (handleClockClick(scene, cx, cy, w, wallSafe)) return;
|
|
947
|
+
|
|
948
|
+
// Plant click → wiggle emoji
|
|
949
|
+
if (handlePlantClick(cx, cy, w, floorY, h)) return;
|
|
950
|
+
|
|
951
|
+
// Bookshelf click → random quote
|
|
952
|
+
if (scene.name === 'office' && cx < 65 && cy >= wallSafe && cy <= wallSafe + 64) {
|
|
953
|
+
showDecoToast(getRandomQuote());
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Whiteboard click — office
|
|
958
|
+
if (scene.name === 'office' && cx >= w*0.15 && cx <= w*0.15+70 && cy >= wallSafe+2 && cy <= wallSafe+46) {
|
|
959
|
+
showDecoToast(getRandomIdea());
|
|
584
960
|
return;
|
|
585
961
|
}
|
|
962
|
+
|
|
963
|
+
// Arcade click — hacker
|
|
964
|
+
if (scene.name === 'hacker' && cx >= w - 28 && cy >= floorY + (h-floorY)*0.30 && cy <= floorY + (h-floorY)*0.30 + 68) {
|
|
965
|
+
showDecoToast(getRandomArcade());
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Menu board click — cafe
|
|
970
|
+
if (scene.name === 'cafe' && cx >= 25 && cx <= 90 && cy >= wallSafe && cy <= wallSafe + 44) {
|
|
971
|
+
showDecoToast(getRandomMenu());
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Floor click (below floorY) → drop treat
|
|
977
|
+
const floorY = Math.round(app.height * 0.40);
|
|
978
|
+
if (cy > floorY + 20 && app.petManager && app.petManager.pets.length > 0) {
|
|
979
|
+
app.petManager.dropTreat(cx, cy);
|
|
980
|
+
return;
|
|
586
981
|
}
|
|
587
|
-
|
|
588
|
-
|
|
982
|
+
|
|
983
|
+
// Empty space → close editor
|
|
984
|
+
if (app.mode === 'live') closeEditor();
|
|
589
985
|
});
|
|
590
986
|
|
|
591
987
|
initScenePicker();
|
|
@@ -282,6 +282,9 @@ class GatewayClient {
|
|
|
282
282
|
async getChatHistory(sessionKey, limit = 50) {
|
|
283
283
|
return this.request('chat.history', { sessionKey, limit });
|
|
284
284
|
}
|
|
285
|
+
async sendChat(sessionKey, message) {
|
|
286
|
+
return this.request('chat.send', { sessionKey, message, idempotencyKey: this._uuid() });
|
|
287
|
+
}
|
|
285
288
|
async getSessionsList(opts = {}) {
|
|
286
289
|
return this.request('sessions.list', { activeMinutes: 120, ...opts });
|
|
287
290
|
}
|
package/public/js/pets/Pet.js
CHANGED
|
@@ -39,8 +39,28 @@ class Pet {
|
|
|
39
39
|
|
|
40
40
|
// Sprite generator (shared)
|
|
41
41
|
this.generator = config.generator || new SpriteGenerator();
|
|
42
|
+
|
|
43
|
+
// Interaction bubble
|
|
44
|
+
this.bubble = null; // { text, timer, duration }
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/** Reaction phrases per type */
|
|
48
|
+
static REACTIONS = {
|
|
49
|
+
cat: ['Purrr~', 'Mrrrow!', '*kneads*', 'Meow~', '💕', '(=^・ω・^=)', '*purr purr*', '🐟?'],
|
|
50
|
+
dog: ['Woof!', '*tail wag*', 'Bork!', '🐾', '*pant pant*', '💖', 'Play?!', 'Yip!'],
|
|
51
|
+
robot: ['BOOP', '01001000!', '*whirrs*', '⚡', 'BEEP', '*click*', 'HELLO'],
|
|
52
|
+
bird: ['Tweet!', '*flutter*', 'Chirp~', '🎵', 'Squawk!', '*head tilt*', '🌾?'],
|
|
53
|
+
hamster: ['Squeak!', '*stuffs cheeks*', '🥜', '*wiggle*', 'Eep!', '*zoom*', '💤→💨'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
static TREAT_REACTIONS = {
|
|
57
|
+
cat: ['Yummy fish!', '🐟 nom!', '*purrs loudly*', 'More pls~'],
|
|
58
|
+
dog: ['TREAT!! 🦴', '*chomp*', 'YESSS!', '*tail 360*'],
|
|
59
|
+
robot: ['ENERGY+1 ⚡', 'FUEL OK', '*charging*'],
|
|
60
|
+
bird: ['Seeds! 🌾', '*peck peck*', 'Chirp! 🎵'],
|
|
61
|
+
hamster: ['*stuff stuff* 🥜', 'Nom nom!', '*cheeks full*'],
|
|
62
|
+
};
|
|
63
|
+
|
|
44
64
|
/** Pet type metadata */
|
|
45
65
|
static TYPES = {
|
|
46
66
|
cat: { moveType: 'walk', speed: 0.04, scale: 2.5, sleepChance: 0.20 },
|
|
@@ -86,12 +106,20 @@ class Pet {
|
|
|
86
106
|
this._flyTime += dt;
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
//
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
// Bubble timer
|
|
110
|
+
if (this.bubble) {
|
|
111
|
+
this.bubble.timer += dt;
|
|
112
|
+
if (this.bubble.timer > this.bubble.duration) this.bubble = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// AI decision timer (pause during interaction)
|
|
116
|
+
if (this.state !== 'interacting') {
|
|
117
|
+
this.actionTimer += dt;
|
|
118
|
+
if (this.actionTimer > this.nextActionTime) {
|
|
119
|
+
this._decideAction(agents, otherPets, sceneBounds);
|
|
120
|
+
this.actionTimer = 0;
|
|
121
|
+
this.nextActionTime = 3000 + Math.random() * 8000;
|
|
122
|
+
}
|
|
95
123
|
}
|
|
96
124
|
|
|
97
125
|
// Movement
|
|
@@ -292,6 +320,77 @@ class Pet {
|
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
ctx.restore();
|
|
323
|
+
|
|
324
|
+
// Interaction bubble (drawn outside ctx.save/restore so no flip)
|
|
325
|
+
if (this.bubble) {
|
|
326
|
+
const bx = this.x + drawW / 2;
|
|
327
|
+
const by = drawY - 6;
|
|
328
|
+
const text = this.bubble.text;
|
|
329
|
+
const pad = 5;
|
|
330
|
+
ctx.font = '7px "Press Start 2P", monospace';
|
|
331
|
+
const tw = ctx.measureText(text).width;
|
|
332
|
+
const bw = tw + pad * 2;
|
|
333
|
+
const bh = 14;
|
|
334
|
+
const rx = bx - bw / 2;
|
|
335
|
+
const ry = by - bh;
|
|
336
|
+
// Bubble fade out in last 500ms
|
|
337
|
+
const remaining = this.bubble.duration - this.bubble.timer;
|
|
338
|
+
ctx.globalAlpha = remaining < 500 ? remaining / 500 : 1;
|
|
339
|
+
// Background
|
|
340
|
+
ctx.fillStyle = 'rgba(10,10,26,0.85)';
|
|
341
|
+
ctx.fillRect(rx, ry, bw, bh);
|
|
342
|
+
ctx.strokeStyle = 'rgba(0,240,255,0.5)';
|
|
343
|
+
ctx.lineWidth = 1;
|
|
344
|
+
ctx.strokeRect(rx, ry, bw, bh);
|
|
345
|
+
// Tail
|
|
346
|
+
ctx.fillStyle = 'rgba(10,10,26,0.85)';
|
|
347
|
+
ctx.beginPath(); ctx.moveTo(bx - 3, by); ctx.lineTo(bx, by + 4); ctx.lineTo(bx + 3, by); ctx.fill();
|
|
348
|
+
// Text
|
|
349
|
+
ctx.fillStyle = '#FFF';
|
|
350
|
+
ctx.fillText(text, rx + pad, ry + 10);
|
|
351
|
+
ctx.globalAlpha = 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Hit test for click detection */
|
|
356
|
+
hitTest(mx, my) {
|
|
357
|
+
const drawW = 16 * this.scale;
|
|
358
|
+
const drawH = 16 * this.scale;
|
|
359
|
+
let drawY = this.y;
|
|
360
|
+
if (this.moveType === 'fly') {
|
|
361
|
+
drawY = this.y - 20 + Math.sin(this._flyTime * 0.003) * 8;
|
|
362
|
+
}
|
|
363
|
+
return mx >= this.x && mx <= this.x + drawW
|
|
364
|
+
&& my >= drawY && my <= drawY + drawH;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** React to being clicked */
|
|
368
|
+
react() {
|
|
369
|
+
const pool = Pet.REACTIONS[this.type] || ['!'];
|
|
370
|
+
const text = pool[Math.floor(Math.random() * pool.length)];
|
|
371
|
+
this.bubble = { text, timer: 0, duration: 2500 };
|
|
372
|
+
this.state = 'interacting';
|
|
373
|
+
this.target = null;
|
|
374
|
+
setTimeout(() => { if (this.state === 'interacting') this.state = 'idle'; }, 2500);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Move toward a treat and eat it */
|
|
378
|
+
goToTreat(tx, ty) {
|
|
379
|
+
this.target = { x: tx, y: ty };
|
|
380
|
+
this.state = 'moving';
|
|
381
|
+
this.interactLabel = 'treat';
|
|
382
|
+
this._treatTarget = { x: tx, y: ty };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Called when pet reaches the treat */
|
|
386
|
+
eatTreat() {
|
|
387
|
+
const pool = Pet.TREAT_REACTIONS[this.type] || ['Yum!'];
|
|
388
|
+
const text = pool[Math.floor(Math.random() * pool.length)];
|
|
389
|
+
this.bubble = { text, timer: 0, duration: 3000 };
|
|
390
|
+
this.state = 'interacting';
|
|
391
|
+
this.target = null;
|
|
392
|
+
this._treatTarget = null;
|
|
393
|
+
setTimeout(() => { if (this.state === 'interacting') this.state = 'idle'; }, 3000);
|
|
295
394
|
}
|
|
296
395
|
|
|
297
396
|
/** Y value for depth sorting */
|