@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/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: 280px; max-width: 90vw;
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-y: auto;
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: 1; }
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
- <button class="ed-close" onclick="closeEditor()">✕</button>
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-actions">
274
- <button class="ed-act" onclick="edRandom()">🎲 RANDOM</button>
275
- <button class="ed-act" onclick="edSave()">💾 SAVE</button>
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
- function edSave() { closeEditor(); }
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
- // Canvas click open editor for clicked agent
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
- for (const slot of app.agents) {
582
- if (slot.hitTest(cx, cy)) {
583
- openEditor(slot);
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
- // Clicked empty space → close editor
588
- closeEditor();
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
  }
@@ -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
- // AI decision timer
90
- this.actionTimer += dt;
91
- if (this.actionTimer > this.nextActionTime) {
92
- this._decideAction(agents, otherPets, sceneBounds);
93
- this.actionTimer = 0;
94
- this.nextActionTime = 3000 + Math.random() * 8000;
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 */