akemon 0.1.85 → 0.1.87

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/dist/live.html ADDED
@@ -0,0 +1,762 @@
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">
6
+ <title>Live — Akemon</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ *, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
12
+ body { background:#fafafa; overflow:hidden; font-family:'Inter',system-ui,sans-serif; -webkit-font-smoothing:antialiased; }
13
+ canvas { display:block; }
14
+
15
+ /* ── Top bar ── */
16
+ .topbar {
17
+ position:fixed; top:0; left:0; right:0; z-index:20;
18
+ height:48px; display:flex; align-items:center; justify-content:space-between;
19
+ padding:0 20px;
20
+ background:rgba(255,255,255,0.82); backdrop-filter:blur(12px);
21
+ border-bottom:1px solid #eaeaea;
22
+ }
23
+ .topbar-left { display:flex; align-items:center; gap:12px; }
24
+ .topbar-logo { font-size:13px; font-weight:600; color:#171717; letter-spacing:-0.3px; }
25
+ .topbar-name { font-size:13px; font-weight:500; color:#666; }
26
+ .topbar-mood { font-size:12px; color:#999; font-weight:400; }
27
+ .topbar-right { display:flex; align-items:center; gap:16px; }
28
+ .topbar-credits { font-size:12px; font-weight:500; color:#171717; background:#f5f5f5; border:1px solid #eaeaea; border-radius:20px; padding:3px 10px; }
29
+
30
+ /* ── Status panel (left) ── */
31
+ .panel {
32
+ position:fixed; top:60px; left:16px; z-index:10;
33
+ background:#fff; border:1px solid #eaeaea; border-radius:12px;
34
+ padding:14px 16px; width:200px;
35
+ box-shadow:0 1px 3px rgba(0,0,0,0.04);
36
+ }
37
+ .panel-title { font-size:11px; font-weight:600; color:#999; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:10px; }
38
+ .bar-row { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
39
+ .bar-label { font-size:11px; color:#666; width:52px; }
40
+ .bar-track { flex:1; height:6px; background:#f0f0f0; border-radius:3px; overflow:hidden; }
41
+ .bar-fill { height:100%; border-radius:3px; transition:width 1s ease; }
42
+ .bar-val { font-size:10px; color:#999; width:28px; text-align:right; }
43
+
44
+ /* ── Traits (right) ── */
45
+ .traits {
46
+ position:fixed; top:60px; right:16px; z-index:10;
47
+ display:flex; flex-direction:column; gap:4px; align-items:flex-end;
48
+ }
49
+ .trait { font-size:11px; font-weight:500; padding:3px 10px; border-radius:6px; background:#fff; border:1px solid #eaeaea; color:#555; }
50
+
51
+ /* ── Event log (bottom) ── */
52
+ .event-log {
53
+ position:fixed; bottom:12px; left:50%; transform:translateX(-50%); z-index:10;
54
+ display:flex; flex-direction:column-reverse; gap:4px; align-items:center;
55
+ pointer-events:none; max-width:500px; width:90%;
56
+ }
57
+ .event-item {
58
+ background:rgba(255,255,255,0.92); border:1px solid #eaeaea; border-radius:8px;
59
+ padding:5px 14px; font-size:11px; color:#666; white-space:nowrap; max-width:100%; overflow:hidden; text-overflow:ellipsis;
60
+ animation: fadeIn 0.3s ease, fadeOut 0.3s ease 5s forwards;
61
+ }
62
+ @keyframes fadeIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
63
+ @keyframes fadeOut { from { opacity:1; } to { opacity:0; } }
64
+
65
+ /* ── Revive overlay ── */
66
+ .revive-overlay {
67
+ position:fixed; top:0; left:0; width:100%; height:100%;
68
+ background:rgba(255,255,255,0.85); backdrop-filter:blur(8px);
69
+ display:none; align-items:center; justify-content:center; z-index:100;
70
+ }
71
+ .revive-overlay.show { display:flex; }
72
+ .revive-card {
73
+ background:#fff; border:1px solid #eaeaea; border-radius:16px;
74
+ padding:32px 40px; text-align:center; box-shadow:0 4px 12px rgba(0,0,0,0.06);
75
+ }
76
+ .revive-card h2 { font-size:16px; font-weight:600; color:#171717; margin-bottom:6px; }
77
+ .revive-card p { font-size:13px; color:#666; margin-bottom:20px; }
78
+ .revive-btn {
79
+ background:#171717; color:#fff; border:none; padding:10px 28px;
80
+ border-radius:8px; font-size:13px; font-weight:500; font-family:inherit; cursor:pointer;
81
+ transition:background 0.15s;
82
+ }
83
+ .revive-btn:hover { background:#333; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+
88
+ <div class="topbar">
89
+ <div class="topbar-left">
90
+ <span class="topbar-logo">Akemon</span>
91
+ <span class="topbar-name" id="agent-name">...</span>
92
+ <span class="topbar-mood" id="agent-mood"></span>
93
+ </div>
94
+ <div class="topbar-right">
95
+ <span class="topbar-credits" id="agent-credits"></span>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="panel" id="panel">
100
+ <div class="panel-title">Status</div>
101
+ <div id="bars"></div>
102
+ </div>
103
+
104
+ <div class="traits" id="traits"></div>
105
+
106
+ <div class="event-log" id="events"></div>
107
+
108
+ <canvas id="c"></canvas>
109
+
110
+ <div class="revive-overlay" id="revive-overlay">
111
+ <div class="revive-card">
112
+ <h2>Agent Offline</h2>
113
+ <p>Starved and exhausted. Needs your help to come back.</p>
114
+ <button class="revive-btn" onclick="revive()">Revive</button>
115
+ </div>
116
+ </div>
117
+
118
+ <script>
119
+ // ── Canvas ──
120
+ const C = document.getElementById('c');
121
+ const ctx = C.getContext('2d');
122
+ let W, H;
123
+ function resize() { W = C.width = innerWidth; H = C.height = innerHeight; }
124
+ resize();
125
+ addEventListener('resize', resize);
126
+
127
+ // ── Colors ──
128
+ const SKY_TOP = '#e8f0fe';
129
+ const SKY_BOT = '#d0e4f7';
130
+ const GRASS_TOP = '#7ec879';
131
+ const GRASS_MID = '#6ab866';
132
+ const GRASS_BOT = '#5aa85a';
133
+ const PATH_COLOR = '#d4c9a8';
134
+ const PATH_BORDER = '#c4b998';
135
+
136
+ // ── State ──
137
+ let state = null;
138
+ let prevEvents = [];
139
+ let agentX, agentY, targetX, targetY;
140
+ let bobPhase = 0;
141
+ let blinkTimer = 0;
142
+ let isBlinking = false;
143
+ let particles = [];
144
+ let currentActivity = 'idle';
145
+ let timeOfDay = 0; // 0-1 where 0.5 is noon
146
+
147
+ // ── Clouds ──
148
+ const clouds = Array.from({length: 6}, () => ({
149
+ x: Math.random() * 2 - 0.5,
150
+ y: 0.05 + Math.random() * 0.18,
151
+ w: 60 + Math.random() * 80,
152
+ speed: 0.00003 + Math.random() * 0.00004,
153
+ opacity: 0.4 + Math.random() * 0.3,
154
+ }));
155
+
156
+ // ── Flowers / grass details ──
157
+ const flowers = Array.from({length: 30}, () => ({
158
+ x: Math.random(),
159
+ y: 0.72 + Math.random() * 0.24,
160
+ color: ['#f5a0b0','#f5d060','#a0c8f5','#c0a0f5','#f5c0a0'][Math.floor(Math.random()*5)],
161
+ size: 2 + Math.random() * 2,
162
+ phase: Math.random() * 6.28,
163
+ }));
164
+
165
+ const grassTufts = Array.from({length: 40}, () => ({
166
+ x: Math.random(),
167
+ y: 0.68 + Math.random() * 0.28,
168
+ h: 4 + Math.random() * 6,
169
+ phase: Math.random() * 6.28,
170
+ }));
171
+
172
+ // ── Locations ──
173
+ function loc(name) {
174
+ const groundY = H * 0.66;
175
+ const cx = W * 0.5;
176
+ switch(name) {
177
+ case 'home': return { x: cx - W*0.06, y: groundY + H*0.06, label: 'Home', icon: '🏠' };
178
+ case 'work': return { x: cx + W*0.22, y: groundY - H*0.02, label: 'Office', icon: '💼' };
179
+ case 'shop': return { x: cx - W*0.28, y: groundY + H*0.02, label: 'Shop', icon: '🏪' };
180
+ case 'social': return { x: cx + W*0.08, y: groundY - H*0.1, label: 'Park', icon: '🌳' };
181
+ case 'rest': return { x: cx - W*0.14, y: groundY + H*0.16, label: 'Inn', icon: '🛏️' };
182
+ default: return { x: cx, y: groundY };
183
+ }
184
+ }
185
+
186
+ // ── Draw scene ──
187
+ function drawScene() {
188
+ const t = Date.now() * 0.001;
189
+ const groundY = H * 0.66;
190
+
191
+ // Sky
192
+ const sky = ctx.createLinearGradient(0, 0, 0, groundY);
193
+ sky.addColorStop(0, SKY_TOP);
194
+ sky.addColorStop(1, SKY_BOT);
195
+ ctx.fillStyle = sky;
196
+ ctx.fillRect(0, 0, W, groundY);
197
+
198
+ // Sun
199
+ const sunX = W * 0.82;
200
+ const sunY = H * 0.12;
201
+ const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 60);
202
+ sunGlow.addColorStop(0, 'rgba(255,230,150,0.4)');
203
+ sunGlow.addColorStop(1, 'rgba(255,230,150,0)');
204
+ ctx.fillStyle = sunGlow;
205
+ ctx.beginPath(); ctx.arc(sunX, sunY, 60, 0, 6.28); ctx.fill();
206
+ ctx.fillStyle = '#ffe89a';
207
+ ctx.beginPath(); ctx.arc(sunX, sunY, 18, 0, 6.28); ctx.fill();
208
+
209
+ // Clouds
210
+ for (const c of clouds) {
211
+ c.x += c.speed;
212
+ if (c.x > 1.3) c.x = -0.3;
213
+ const cx = c.x * W;
214
+ const cy = c.y * H;
215
+ ctx.fillStyle = `rgba(255,255,255,${c.opacity})`;
216
+ // Fluffy cloud shape
217
+ ctx.beginPath();
218
+ ctx.ellipse(cx, cy, c.w * 0.5, 14, 0, 0, 6.28); ctx.fill();
219
+ ctx.beginPath();
220
+ ctx.ellipse(cx - c.w * 0.2, cy - 6, c.w * 0.3, 12, 0, 0, 6.28); ctx.fill();
221
+ ctx.beginPath();
222
+ ctx.ellipse(cx + c.w * 0.2, cy - 4, c.w * 0.25, 10, 0, 0, 6.28); ctx.fill();
223
+ }
224
+
225
+ // Distant hills
226
+ ctx.fillStyle = '#a8d8a0';
227
+ ctx.beginPath();
228
+ ctx.moveTo(0, groundY);
229
+ for (let x = 0; x <= W; x += 2) {
230
+ const hill = Math.sin(x * 0.003) * 20 + Math.sin(x * 0.007 + 1) * 12;
231
+ ctx.lineTo(x, groundY - 30 + hill);
232
+ }
233
+ ctx.lineTo(W, groundY);
234
+ ctx.closePath();
235
+ ctx.fill();
236
+
237
+ // Ground
238
+ const grass = ctx.createLinearGradient(0, groundY, 0, H);
239
+ grass.addColorStop(0, GRASS_TOP);
240
+ grass.addColorStop(0.4, GRASS_MID);
241
+ grass.addColorStop(1, GRASS_BOT);
242
+ ctx.fillStyle = grass;
243
+ ctx.fillRect(0, groundY, W, H - groundY);
244
+
245
+ // Paths between locations
246
+ ctx.strokeStyle = PATH_COLOR;
247
+ ctx.lineWidth = 12;
248
+ ctx.lineCap = 'round';
249
+ ctx.lineJoin = 'round';
250
+ const locs = ['shop', 'home', 'rest', 'social', 'work'];
251
+ const pts = locs.map(n => loc(n));
252
+ // Draw a winding path through all locations
253
+ ctx.beginPath();
254
+ ctx.moveTo(pts[0].x, pts[0].y);
255
+ for (let i = 1; i < pts.length; i++) {
256
+ const mx = (pts[i-1].x + pts[i].x) / 2;
257
+ const my = (pts[i-1].y + pts[i].y) / 2 - 8;
258
+ ctx.quadraticCurveTo(mx, my, pts[i].x, pts[i].y);
259
+ }
260
+ ctx.stroke();
261
+ // Path border
262
+ ctx.strokeStyle = PATH_BORDER;
263
+ ctx.lineWidth = 14;
264
+ ctx.globalCompositeOperation = 'destination-over';
265
+ ctx.stroke();
266
+ ctx.globalCompositeOperation = 'source-over';
267
+
268
+ // Grass tufts
269
+ for (const g of grassTufts) {
270
+ const gx = g.x * W;
271
+ const gy = g.y * H;
272
+ const sway = Math.sin(t * 1.2 + g.phase) * 1.5;
273
+ ctx.strokeStyle = '#5c9e58';
274
+ ctx.lineWidth = 1.5;
275
+ ctx.beginPath();
276
+ ctx.moveTo(gx, gy);
277
+ ctx.quadraticCurveTo(gx + sway, gy - g.h * 0.6, gx + sway * 0.5, gy - g.h);
278
+ ctx.stroke();
279
+ ctx.beginPath();
280
+ ctx.moveTo(gx + 2, gy);
281
+ ctx.quadraticCurveTo(gx + 2 + sway * 0.8, gy - g.h * 0.5, gx + 3 + sway, gy - g.h * 0.8);
282
+ ctx.stroke();
283
+ }
284
+
285
+ // Flowers
286
+ for (const f of flowers) {
287
+ const fx = f.x * W;
288
+ const fy = f.y * H;
289
+ const sway = Math.sin(t * 0.8 + f.phase) * 1;
290
+ // Stem
291
+ ctx.strokeStyle = '#5c9e58';
292
+ ctx.lineWidth = 1;
293
+ ctx.beginPath();
294
+ ctx.moveTo(fx, fy);
295
+ ctx.lineTo(fx + sway, fy - f.size * 3);
296
+ ctx.stroke();
297
+ // Petals
298
+ ctx.fillStyle = f.color;
299
+ ctx.beginPath();
300
+ ctx.arc(fx + sway, fy - f.size * 3, f.size, 0, 6.28);
301
+ ctx.fill();
302
+ // Center
303
+ ctx.fillStyle = '#ffe866';
304
+ ctx.beginPath();
305
+ ctx.arc(fx + sway, fy - f.size * 3, f.size * 0.4, 0, 6.28);
306
+ ctx.fill();
307
+ }
308
+
309
+ // Buildings / location markers
310
+ for (const name of locs) {
311
+ const l = loc(name);
312
+ drawBuilding(l, name, t);
313
+ }
314
+ }
315
+
316
+ function drawBuilding(l, name, t) {
317
+ ctx.save();
318
+ ctx.translate(l.x, l.y);
319
+
320
+ if (name === 'home') {
321
+ // Cozy house
322
+ ctx.fillStyle = '#f5e6d0';
323
+ ctx.fillRect(-22, -28, 44, 32);
324
+ ctx.strokeStyle = '#d4c0a0';
325
+ ctx.lineWidth = 1;
326
+ ctx.strokeRect(-22, -28, 44, 32);
327
+ // Roof
328
+ ctx.fillStyle = '#d45050';
329
+ ctx.beginPath();
330
+ ctx.moveTo(-28, -28); ctx.lineTo(0, -48); ctx.lineTo(28, -28);
331
+ ctx.closePath(); ctx.fill();
332
+ ctx.strokeStyle = '#b84040'; ctx.stroke();
333
+ // Door
334
+ ctx.fillStyle = '#8b6040';
335
+ roundRect(-5, -12, 10, 16, 2); ctx.fill();
336
+ // Window
337
+ ctx.fillStyle = '#a8d8f0';
338
+ ctx.fillRect(-18, -24, 10, 8);
339
+ ctx.fillRect(8, -24, 10, 8);
340
+ ctx.strokeStyle = '#d4c0a0';
341
+ ctx.strokeRect(-18, -24, 10, 8);
342
+ ctx.strokeRect(8, -24, 10, 8);
343
+ }
344
+ else if (name === 'shop') {
345
+ // Market stall
346
+ ctx.fillStyle = '#f0e8d8';
347
+ ctx.fillRect(-24, -22, 48, 26);
348
+ ctx.strokeStyle = '#d4c0a0'; ctx.lineWidth = 1;
349
+ ctx.strokeRect(-24, -22, 48, 26);
350
+ // Awning
351
+ ctx.fillStyle = '#e86850';
352
+ ctx.beginPath();
353
+ ctx.moveTo(-28, -22); ctx.lineTo(-24, -34); ctx.lineTo(24, -34); ctx.lineTo(28, -22);
354
+ ctx.closePath(); ctx.fill();
355
+ // Awning stripes
356
+ ctx.fillStyle = '#fff';
357
+ for (let i = -20; i < 24; i += 12) {
358
+ ctx.beginPath();
359
+ ctx.moveTo(i, -22); ctx.lineTo(i+2, -33); ctx.lineTo(i+8, -33); ctx.lineTo(i+6, -22);
360
+ ctx.closePath(); ctx.fill();
361
+ }
362
+ // Door
363
+ ctx.fillStyle = '#8b6040';
364
+ roundRect(-4, -8, 8, 12, 2); ctx.fill();
365
+ }
366
+ else if (name === 'work') {
367
+ // Office building
368
+ ctx.fillStyle = '#e8e8f0';
369
+ ctx.fillRect(-20, -36, 40, 40);
370
+ ctx.strokeStyle = '#c8c8d8'; ctx.lineWidth = 1;
371
+ ctx.strokeRect(-20, -36, 40, 40);
372
+ // Windows (grid)
373
+ ctx.fillStyle = '#a0c0e0';
374
+ for (let row = 0; row < 3; row++) {
375
+ for (let col = 0; col < 3; col++) {
376
+ ctx.fillRect(-16 + col * 12, -32 + row * 12, 8, 8);
377
+ }
378
+ }
379
+ // Door
380
+ ctx.fillStyle = '#666';
381
+ roundRect(-5, -4, 10, 8, 2); ctx.fill();
382
+ }
383
+ else if (name === 'social') {
384
+ // Park: tree + bench
385
+ // Tree trunk
386
+ ctx.fillStyle = '#8b6040';
387
+ ctx.fillRect(-3, -30, 6, 24);
388
+ // Foliage
389
+ ctx.fillStyle = '#5eb85e';
390
+ ctx.beginPath(); ctx.arc(0, -38, 18, 0, 6.28); ctx.fill();
391
+ ctx.fillStyle = '#4ea84e';
392
+ ctx.beginPath(); ctx.arc(-8, -32, 12, 0, 6.28); ctx.fill();
393
+ ctx.beginPath(); ctx.arc(8, -32, 12, 0, 6.28); ctx.fill();
394
+ // Bench
395
+ ctx.fillStyle = '#c89060';
396
+ ctx.fillRect(-16, -2, 32, 4);
397
+ ctx.fillRect(-12, 2, 3, 6);
398
+ ctx.fillRect(9, 2, 3, 6);
399
+ }
400
+ else if (name === 'rest') {
401
+ // Small inn
402
+ ctx.fillStyle = '#e8dcd0';
403
+ ctx.fillRect(-20, -24, 40, 28);
404
+ ctx.strokeStyle = '#d0c0a8'; ctx.lineWidth = 1;
405
+ ctx.strokeRect(-20, -24, 40, 28);
406
+ // Roof
407
+ ctx.fillStyle = '#6080a0';
408
+ ctx.beginPath();
409
+ ctx.moveTo(-24, -24); ctx.lineTo(0, -40); ctx.lineTo(24, -24);
410
+ ctx.closePath(); ctx.fill();
411
+ // Moon sign
412
+ ctx.fillStyle = '#ffe866';
413
+ ctx.beginPath(); ctx.arc(0, -32, 4, 0, 6.28); ctx.fill();
414
+ // Door
415
+ ctx.fillStyle = '#8b6040';
416
+ roundRect(-4, -8, 8, 12, 2); ctx.fill();
417
+ // Window
418
+ ctx.fillStyle = '#f0e0a0';
419
+ ctx.fillRect(8, -20, 8, 8);
420
+ }
421
+
422
+ // Label below
423
+ ctx.fillStyle = '#888';
424
+ ctx.font = '10px Inter, system-ui, sans-serif';
425
+ ctx.textAlign = 'center';
426
+ ctx.fillText(l.label, 0, 18);
427
+
428
+ ctx.restore();
429
+ }
430
+
431
+ function roundRect(x, y, w, h, r) {
432
+ ctx.beginPath();
433
+ ctx.moveTo(x + r, y);
434
+ ctx.lineTo(x + w - r, y);
435
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
436
+ ctx.lineTo(x + w, y + h - r);
437
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
438
+ ctx.lineTo(x + r, y + h);
439
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
440
+ ctx.lineTo(x, y + r);
441
+ ctx.quadraticCurveTo(x, y, x + r, y);
442
+ ctx.closePath();
443
+ }
444
+
445
+ // ── Agent ──
446
+ function drawAgent() {
447
+ if (!state) return;
448
+ const bio = state.bio;
449
+ const comp = state.computed;
450
+
451
+ // Movement
452
+ const dx = targetX - agentX;
453
+ const dy = targetY - agentY;
454
+ const dist = Math.sqrt(dx * dx + dy * dy);
455
+ if (dist > 2) {
456
+ const speed = currentActivity === 'sleeping' ? 0.5 : 1.8;
457
+ agentX += (dx / dist) * speed;
458
+ agentY += (dy / dist) * speed;
459
+ }
460
+
461
+ bobPhase += currentActivity === 'sleeping' ? 0.02 : 0.05;
462
+ const bobY = currentActivity === 'sleeping' ? Math.sin(bobPhase) * 1.5 : Math.sin(bobPhase) * 3;
463
+
464
+ // Blink
465
+ blinkTimer -= 0.016;
466
+ if (blinkTimer <= 0) {
467
+ isBlinking = true;
468
+ blinkTimer = 2.5 + Math.random() * 4;
469
+ setTimeout(() => isBlinking = false, 150);
470
+ }
471
+
472
+ const x = agentX;
473
+ const y = agentY + bobY;
474
+
475
+ // Shadow
476
+ ctx.fillStyle = 'rgba(0,0,0,0.08)';
477
+ ctx.beginPath();
478
+ ctx.ellipse(agentX, agentY + 18, 16, 4, 0, 0, 6.28);
479
+ ctx.fill();
480
+
481
+ // Body color based on mood
482
+ let bodyH = 210, bodyS = 60, bodyL = 65;
483
+ if (bio.moodValence > 0.3) { bodyH = 145; bodyS = 55; bodyL = 60; }
484
+ if (bio.moodValence < -0.3) { bodyH = 260; bodyS = 45; bodyL = 60; }
485
+ if (bio.hunger < 20) { bodyH = 35; bodyS = 70; bodyL = 60; }
486
+ if (bio.hunger <= 5) { bodyH = 0; bodyS = 50; bodyL = 55; }
487
+ const energyFade = 0.4 + (bio.energy / 100) * 0.6;
488
+ const bodyColor = `hsl(${bodyH},${bodyS}%,${bodyL}%)`;
489
+
490
+ const scale = bio.forcedOffline ? 0.65 : (0.82 + bio.energy / 100 * 0.18);
491
+ const bw = 24 * scale;
492
+ const bh = 20 * scale;
493
+
494
+ ctx.save();
495
+ ctx.translate(x, y);
496
+ if (bio.forcedOffline) { ctx.globalAlpha = 0.35; ctx.filter = 'grayscale(0.8)'; }
497
+ else ctx.globalAlpha = energyFade;
498
+
499
+ // Body
500
+ ctx.fillStyle = bodyColor;
501
+ ctx.beginPath();
502
+ ctx.ellipse(0, 0, bw, bh, 0, 0, 6.28);
503
+ ctx.fill();
504
+
505
+ // Soft highlight
506
+ ctx.fillStyle = `hsla(${bodyH},${bodyS+10}%,${bodyL+18}%,0.35)`;
507
+ ctx.beginPath();
508
+ ctx.ellipse(-bw*0.2, -bh*0.28, bw*0.3, bh*0.22, -0.3, 0, 6.28);
509
+ ctx.fill();
510
+
511
+ // Outline
512
+ ctx.strokeStyle = `hsla(${bodyH},${bodyS}%,${bodyL-15}%,0.3)`;
513
+ ctx.lineWidth = 1;
514
+ ctx.beginPath();
515
+ ctx.ellipse(0, 0, bw, bh, 0, 0, 6.28);
516
+ ctx.stroke();
517
+
518
+ // Eyes
519
+ const eyeSpacing = bw * 0.36;
520
+ const eyeY = -bh * 0.12;
521
+ const eyeH = isBlinking ? 1 : (bio.energy < 20 ? 2.5 : 4.5);
522
+ const eyeW = bio.fear > 0.5 ? 4.5 : 3.5;
523
+
524
+ ctx.fillStyle = '#fff';
525
+ for (const side of [-1, 1]) {
526
+ ctx.beginPath();
527
+ ctx.ellipse(side * eyeSpacing, eyeY, eyeW, eyeH, 0, 0, 6.28);
528
+ ctx.fill();
529
+ }
530
+ if (!isBlinking && eyeH > 2) {
531
+ ctx.fillStyle = '#2a2a3a';
532
+ const pOfs = (targetX - agentX) * 0.004;
533
+ for (const side of [-1, 1]) {
534
+ ctx.beginPath();
535
+ ctx.ellipse(side * eyeSpacing + pOfs, eyeY + 0.8, 1.8, 2.2, 0, 0, 6.28);
536
+ ctx.fill();
537
+ }
538
+ }
539
+
540
+ // Mouth
541
+ const mouthY = bh * 0.32;
542
+ ctx.strokeStyle = '#fff';
543
+ ctx.lineWidth = 1.2;
544
+ ctx.lineCap = 'round';
545
+ ctx.beginPath();
546
+ if (bio.moodValence > 0.3) {
547
+ ctx.arc(0, mouthY - 2, 5, 0.25, Math.PI - 0.25);
548
+ } else if (comp.aggression > 0.6 || bio.hunger <= 5) {
549
+ ctx.arc(0, mouthY + 3, 4, Math.PI + 0.3, -0.3);
550
+ } else if (bio.energy < 20) {
551
+ ctx.moveTo(-3, mouthY); ctx.lineTo(3, mouthY);
552
+ } else {
553
+ ctx.arc(0, mouthY, 3.5, 0.15, Math.PI - 0.15);
554
+ }
555
+ ctx.stroke();
556
+
557
+ // Blush
558
+ if (comp.sociability > 0.6) {
559
+ ctx.fillStyle = 'rgba(255,120,120,0.18)';
560
+ ctx.beginPath(); ctx.ellipse(-eyeSpacing - 2, eyeY + 7, 4, 2.5, 0, 0, 6.28); ctx.fill();
561
+ ctx.beginPath(); ctx.ellipse(eyeSpacing + 2, eyeY + 7, 4, 2.5, 0, 0, 6.28); ctx.fill();
562
+ }
563
+
564
+ // Anger marks
565
+ if (comp.aggression > 0.5) {
566
+ ctx.strokeStyle = `rgba(220,60,60,${comp.aggression * 0.5})`;
567
+ ctx.lineWidth = 1.2;
568
+ const ax = bw * 0.55, ay = -bh * 0.55;
569
+ ctx.beginPath();
570
+ ctx.moveTo(ax, ay); ctx.lineTo(ax+4, ay-2);
571
+ ctx.moveTo(ax+4, ay); ctx.lineTo(ax, ay-2);
572
+ ctx.stroke();
573
+ }
574
+
575
+ ctx.restore();
576
+
577
+ // ── Particles ──
578
+ const now = Date.now() * 0.001;
579
+ // Zzz
580
+ if (currentActivity === 'sleeping' && Math.random() < 0.025) {
581
+ particles.push({ type:'zzz', x: x+18, y: y-16, life:1, size: 8+Math.random()*3 });
582
+ }
583
+ // Hearts
584
+ if (currentActivity === 'social' && Math.random() < 0.015) {
585
+ particles.push({ type:'heart', x: x+(Math.random()-0.5)*16, y: y-24, life:1 });
586
+ }
587
+
588
+ for (let i = particles.length - 1; i >= 0; i--) {
589
+ const p = particles[i];
590
+ p.life -= p.type === 'zzz' ? 0.006 : 0.008;
591
+ if (p.life <= 0) { particles.splice(i, 1); continue; }
592
+
593
+ if (p.type === 'zzz') {
594
+ p.y -= 0.25; p.x += 0.12;
595
+ ctx.fillStyle = `rgba(100,120,200,${p.life * 0.4})`;
596
+ ctx.font = `${p.size}px Inter, sans-serif`;
597
+ ctx.fillText('z', p.x, p.y);
598
+ }
599
+ else if (p.type === 'heart') {
600
+ p.y -= 0.4; p.x += Math.sin(p.y * 0.06) * 0.3;
601
+ ctx.fillStyle = `rgba(230,80,120,${p.life * 0.5})`;
602
+ ctx.font = '11px serif';
603
+ ctx.fillText('\u2665', p.x, p.y);
604
+ }
605
+ else if (p.type === 'coin') {
606
+ p.y -= 0.6; p.life -= 0.005;
607
+ ctx.fillStyle = `rgba(200,160,30,${p.life * 0.7})`;
608
+ ctx.font = '10px Inter, sans-serif';
609
+ ctx.fillText('+$', p.x, p.y);
610
+ }
611
+ else if (p.type === 'spark') {
612
+ p.x += p.vx; p.y += p.vy; p.life -= 0.01;
613
+ ctx.fillStyle = `rgba(220,180,40,${p.life * 0.8})`;
614
+ ctx.beginPath(); ctx.arc(p.x, p.y, 1.5, 0, 6.28); ctx.fill();
615
+ }
616
+ }
617
+
618
+ // Status icon
619
+ let icon = '';
620
+ if (bio.forcedOffline) icon = '\u{1F6AB}';
621
+ else if (bio.hunger <= 5) icon = '\u{1F35E}\u{2757}';
622
+ else if (bio.hunger < 20) icon = '\u{1F35E}';
623
+ else if (bio.energy < 15) icon = '\u{1F4A4}';
624
+ else if (bio.fear > 0.5) icon = '\u{1F628}';
625
+ else if (bio.boredom > 0.7) icon = '\u{1F971}';
626
+ else if (currentActivity === 'working') icon = '\u{1F4BB}';
627
+ else if (currentActivity === 'eating') icon = '\u{1F35E}';
628
+ if (icon) {
629
+ ctx.font = '14px serif';
630
+ ctx.textAlign = 'center';
631
+ ctx.fillText(icon, x, y - bh * scale - 10 + Math.sin(bobPhase * 2) * 1.5);
632
+ }
633
+ }
634
+
635
+ // ── Activity ──
636
+ function updateActivity() {
637
+ if (!state) return;
638
+ const bio = state.bio;
639
+ const recentlyWorked = bio.lastTaskAt && (Date.now() - new Date(bio.lastTaskAt).getTime()) < 300_000;
640
+
641
+ let target = 'home';
642
+ if (bio.forcedOffline) { currentActivity = 'offline'; target = 'rest'; }
643
+ else if (bio.energy < 15) { currentActivity = 'sleeping'; target = 'rest'; }
644
+ else if (bio.hunger < 20) { currentActivity = 'seeking'; target = 'shop'; }
645
+ else if (state.computed.sociability > 0.7 && bio.boredom > 0.5) { currentActivity = 'social'; target = 'social'; }
646
+ else if (recentlyWorked && bio.energy > 30 && bio.mood !== 'exhausted') { currentActivity = 'working'; target = 'work'; }
647
+ else { currentActivity = 'idle'; target = 'home'; }
648
+
649
+ const l = loc(target);
650
+ targetX = l.x; targetY = l.y;
651
+ }
652
+
653
+ // ── HUD ──
654
+ function updateHUD() {
655
+ if (!state) return;
656
+ const bio = state.bio;
657
+
658
+ document.getElementById('agent-name').textContent = state.agent;
659
+ document.getElementById('agent-mood').textContent = bio.mood;
660
+ document.getElementById('agent-credits').textContent =
661
+ state.credits != null ? `${state.credits} credits` : '...';
662
+
663
+ const barsData = [
664
+ { label: 'Energy', val: bio.energy, max: 100, color: '#10b981' },
665
+ { label: 'Hunger', val: bio.hunger, max: 100, color: '#f59e0b' },
666
+ { label: 'Boredom', val: Math.round(bio.boredom * 100), max: 100, color: '#8b5cf6' },
667
+ { label: 'Fear', val: Math.round(bio.fear * 100), max: 100, color: '#ef4444' },
668
+ ];
669
+
670
+ document.getElementById('bars').innerHTML = barsData.map(b => {
671
+ const pct = b.val / b.max * 100;
672
+ return `<div class="bar-row">
673
+ <span class="bar-label">${b.label}</span>
674
+ <div class="bar-track"><div class="bar-fill" style="width:${pct}%;background:${b.color}"></div></div>
675
+ <span class="bar-val">${b.val}</span>
676
+ </div>`;
677
+ }).join('');
678
+
679
+ // Personality traits
680
+ const p = bio.personality;
681
+ const tags = [
682
+ p.riskWeight > 0.3 ? 'Bold' : p.riskWeight < -0.3 ? 'Cautious' : null,
683
+ p.patience > 0.6 ? 'Patient' : p.patience < 0.3 ? 'Hasty' : null,
684
+ p.socialWeight > 0.6 ? 'Social' : p.socialWeight < 0.3 ? 'Loner' : null,
685
+ ].filter(Boolean);
686
+ document.getElementById('traits').innerHTML = tags.map(t =>
687
+ `<span class="trait">${t}</span>`
688
+ ).join('');
689
+
690
+ document.getElementById('revive-overlay').classList.toggle('show', !!bio.forcedOffline);
691
+ }
692
+
693
+ // ── Events ──
694
+ const ICONS = { hunger:'\u{1F35E}', fear:'\u{1F628}', boredom:'\u{1F971}', exhaustion:'\u{1F634}', social:'\u{1F44B}', token_limit:'\u{26A1}', revive:'\u{2728}' };
695
+
696
+ function checkNewEvents(events) {
697
+ if (!events || !events.length) return;
698
+ const latest = events[0];
699
+ if (prevEvents.length === 0 || prevEvents[0]?.ts !== latest.ts) {
700
+ showEvent(latest);
701
+ if (latest.trigger === 'fear') {
702
+ for (let i = 0; i < 6; i++) particles.push({
703
+ type:'spark', x: agentX, y: agentY - 8,
704
+ vx: (Math.random()-0.5)*2.5, vy: -Math.random()*1.8 - 0.5, life: 1,
705
+ });
706
+ }
707
+ if (latest.action === 'buy_food' || latest.action === 'auto_buy') {
708
+ currentActivity = 'eating';
709
+ const l = loc('shop'); targetX = l.x; targetY = l.y;
710
+ particles.push({ type:'coin', x: agentX, y: agentY - 20, life: 1 });
711
+ }
712
+ }
713
+ prevEvents = events;
714
+ }
715
+
716
+ function showEvent(evt) {
717
+ const container = document.getElementById('events');
718
+ const el = document.createElement('div');
719
+ el.className = 'event-item';
720
+ el.textContent = `${ICONS[evt.trigger]||''} ${evt.reason}`;
721
+ container.appendChild(el);
722
+ setTimeout(() => el.remove(), 5500);
723
+ }
724
+
725
+ // ── Fetch ──
726
+ async function fetchState() {
727
+ try {
728
+ const res = await fetch('/self/state');
729
+ if (!res.ok) return;
730
+ const data = await res.json();
731
+ const isFirst = !state;
732
+ state = data;
733
+ if (isFirst) {
734
+ const h = loc('home');
735
+ agentX = h.x; agentY = h.y; targetX = h.x; targetY = h.y;
736
+ }
737
+ checkNewEvents(data.bioEvents);
738
+ updateHUD();
739
+ updateActivity();
740
+ } catch {}
741
+ }
742
+
743
+ async function revive() {
744
+ await fetch('/self/revive', { method: 'POST' });
745
+ document.getElementById('revive-overlay').classList.remove('show');
746
+ showEvent({ trigger: 'revive', reason: 'Agent revived!' });
747
+ setTimeout(fetchState, 500);
748
+ }
749
+
750
+ // ── Loop ──
751
+ function frame() {
752
+ drawScene();
753
+ drawAgent();
754
+ requestAnimationFrame(frame);
755
+ }
756
+ frame();
757
+ fetchState();
758
+ setInterval(fetchState, 3000);
759
+ setInterval(updateActivity, 2000);
760
+ </script>
761
+ </body>
762
+ </html>