akemon 0.3.2 → 0.3.4

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.
@@ -1,90 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it, before, after } from "node:test";
3
- import { mkdtemp, rm } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- // We import the functions we need to test. appendMessage and loadConversation
7
- // need a workdir on disk; parseConversation is internal — tested via loadConversation.
8
- import { appendMessage, loadConversation } from "./context.js";
9
- const agentName = "test-agent";
10
- // ---------------------------------------------------------------------------
11
- // appendMessage + loadConversation round-trip
12
- // ---------------------------------------------------------------------------
13
- describe("appendMessage / loadConversation", () => {
14
- let workdir = "";
15
- before(async () => {
16
- workdir = await mkdtemp(join(tmpdir(), "ctx-test-"));
17
- });
18
- after(async () => {
19
- await rm(workdir, { recursive: true, force: true });
20
- });
21
- it("writes a chat message and reads it back with kind='chat'", async () => {
22
- const convId = "test-chat";
23
- await appendMessage(workdir, agentName, convId, "User", "hello", "chat");
24
- const conv = await loadConversation(workdir, agentName, convId);
25
- assert.equal(conv.rounds.length, 1);
26
- assert.equal(conv.rounds[0].role, "user");
27
- assert.equal(conv.rounds[0].content, "hello");
28
- assert.equal(conv.rounds[0].kind, "chat");
29
- });
30
- it("writes an order message with [order] tag and reads kind='order'", async () => {
31
- const convId = "test-order";
32
- await appendMessage(workdir, agentName, convId, "User", "order request", "order");
33
- await appendMessage(workdir, agentName, convId, "Agent", "order delivered", "order");
34
- const conv = await loadConversation(workdir, agentName, convId);
35
- assert.equal(conv.rounds.length, 2);
36
- assert.equal(conv.rounds[0].kind, "order");
37
- assert.equal(conv.rounds[0].role, "user");
38
- assert.equal(conv.rounds[1].kind, "order");
39
- assert.equal(conv.rounds[1].role, "agent");
40
- });
41
- it("defaults to kind='chat' when kind arg is omitted", async () => {
42
- const convId = "test-default-kind";
43
- await appendMessage(workdir, agentName, convId, "Agent", "hi there");
44
- const conv = await loadConversation(workdir, agentName, convId);
45
- assert.equal(conv.rounds[0].kind, "chat");
46
- });
47
- it("parses mixed chat + order rounds correctly", async () => {
48
- const convId = "test-mixed";
49
- await appendMessage(workdir, agentName, convId, "User", "chat message", "chat");
50
- await appendMessage(workdir, agentName, convId, "User", "order task", "order");
51
- await appendMessage(workdir, agentName, convId, "Agent", "chat reply", "chat");
52
- await appendMessage(workdir, agentName, convId, "Agent", "order result", "order");
53
- const conv = await loadConversation(workdir, agentName, convId);
54
- assert.equal(conv.rounds.length, 4);
55
- assert.equal(conv.rounds[0].kind, "chat");
56
- assert.equal(conv.rounds[1].kind, "order");
57
- assert.equal(conv.rounds[2].kind, "chat");
58
- assert.equal(conv.rounds[3].kind, "order");
59
- });
60
- it("parses legacy file (no [kind] tag) as kind='chat' for backward-compat", async () => {
61
- const convId = "test-legacy";
62
- // Write a legacy-style file manually (no [order] tag)
63
- const { mkdir, writeFile } = await import("node:fs/promises");
64
- const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
65
- await mkdir(dir, { recursive: true });
66
- const legacyContent = "## Summary\n\n\n## Recent\n[2026-04-23 10:00] User: old message\n[2026-04-23 10:01] Agent: old reply\n";
67
- await writeFile(join(dir, `${convId}.md`), legacyContent);
68
- const conv = await loadConversation(workdir, agentName, convId);
69
- assert.equal(conv.rounds.length, 2);
70
- assert.equal(conv.rounds[0].kind, "chat");
71
- assert.equal(conv.rounds[1].kind, "chat");
72
- });
73
- it("[order] tag appears in the raw file content", async () => {
74
- const convId = "test-tag-on-disk";
75
- await appendMessage(workdir, agentName, convId, "User", "buy something", "order");
76
- const { readFile: rf } = await import("node:fs/promises");
77
- const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
78
- const raw = await rf(join(dir, `${convId}.md`), "utf-8");
79
- assert.ok(raw.includes("[order] User: buy something"), `raw file should contain [order] tag: ${raw}`);
80
- });
81
- it("chat message does NOT have [order] tag in raw file", async () => {
82
- const convId = "test-no-tag-on-disk";
83
- await appendMessage(workdir, agentName, convId, "User", "just chatting", "chat");
84
- const { readFile: rf } = await import("node:fs/promises");
85
- const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
86
- const raw = await rf(join(dir, `${convId}.md`), "utf-8");
87
- assert.ok(!raw.includes("[order]"), `chat line should NOT contain [order] tag: ${raw}`);
88
- assert.ok(raw.includes("User: just chatting"));
89
- });
90
- });
@@ -1,552 +0,0 @@
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>Akemon</title>
7
- <style>
8
- * { margin:0; padding:0; box-sizing:border-box; }
9
- body { background:#0a0a12; overflow:hidden; font-family:'Courier New',monospace; }
10
- canvas { display:block; }
11
-
12
- /* HUD overlay */
13
- .hud { position:fixed; top:12px; left:12px; z-index:10; pointer-events:none; }
14
- .hud-name { color:#8888aa; font-size:11px; letter-spacing:2px; text-transform:uppercase; margin-bottom:8px; }
15
- .hud-bars { display:flex; flex-direction:column; gap:4px; }
16
- .hud-bar { display:flex; align-items:center; gap:6px; }
17
- .hud-bar-icon { font-size:10px; width:14px; text-align:center; }
18
- .hud-bar-track { width:80px; height:5px; background:#1a1a2a; border-radius:3px; overflow:hidden; }
19
- .hud-bar-fill { height:100%; border-radius:3px; transition: width 1s ease; }
20
- .hud-bar-text { font-size:9px; color:#555; width:24px; }
21
-
22
- /* Event toast */
23
- .toast-container { position:fixed; bottom:16px; left:50%; transform:translateX(-50%); z-index:10; display:flex; flex-direction:column-reverse; gap:6px; align-items:center; pointer-events:none; }
24
- .toast { background:rgba(18,18,32,0.92); border:1px solid #2a2a40; border-radius:8px; padding:6px 14px; font-size:11px; color:#aaa; white-space:nowrap; animation: toastIn 0.4s ease, toastOut 0.4s ease 4s forwards; }
25
- @keyframes toastIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
26
- @keyframes toastOut { from { opacity:1; } to { opacity:0; } }
27
-
28
- /* Personality mini */
29
- .personality-hud { position:fixed; top:12px; right:12px; z-index:10; pointer-events:none; }
30
- .p-tag { display:inline-block; font-size:9px; padding:2px 6px; border-radius:4px; margin:1px; background:#1a1a2a; border:1px solid #2a2a3a; }
31
- .p-tag.high { color:#4ae; } .p-tag.low { color:#a86; } .p-tag.mid { color:#666; }
32
-
33
- /* Revive */
34
- .revive-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); display:none; align-items:center; justify-content:center; z-index:100; }
35
- .revive-overlay.show { display:flex; }
36
- .revive-card { background:#1a1a2a; border:1px solid #333; border-radius:16px; padding:30px; text-align:center; }
37
- .revive-card h2 { color:#e55; font-size:15px; margin-bottom:8px; }
38
- .revive-card p { color:#888; font-size:12px; margin-bottom:16px; }
39
- .revive-btn { background:linear-gradient(135deg,#2c5,#1a8); color:#fff; border:none; padding:10px 28px; border-radius:8px; font-size:13px; font-family:inherit; cursor:pointer; }
40
- </style>
41
- </head>
42
- <body>
43
-
44
- <div class="hud">
45
- <div class="hud-name" id="hud-name">loading...</div>
46
- <div class="hud-bars" id="hud-bars"></div>
47
- </div>
48
-
49
- <div class="personality-hud" id="personality-hud"></div>
50
-
51
- <div class="toast-container" id="toasts"></div>
52
-
53
- <canvas id="c"></canvas>
54
-
55
- <div class="revive-overlay" id="revive-overlay">
56
- <div class="revive-card">
57
- <h2>Agent Offline</h2>
58
- <p>Starved and exhausted. Needs help.</p>
59
- <button class="revive-btn" onclick="revive()">Revive</button>
60
- </div>
61
- </div>
62
-
63
- <script>
64
- // === Canvas setup ===
65
- const C = document.getElementById('c');
66
- const ctx = C.getContext('2d');
67
- let W, H;
68
- function resize() { W = C.width = innerWidth; H = C.height = innerHeight; }
69
- resize();
70
- addEventListener('resize', resize);
71
-
72
- // === State ===
73
- let state = null;
74
- let prevEvents = [];
75
- let agentX, agentY, targetX, targetY;
76
- let bobPhase = 0;
77
- let blinkTimer = 0;
78
- let isBlinking = false;
79
- let zzz = []; // sleep particles
80
- let hearts = []; // social particles
81
- let coins = []; // earn particles
82
- let sparks = []; // fear particles
83
- let currentActivity = 'idle'; // idle, working, eating, sleeping, social, seeking
84
-
85
- // Locations (relative to canvas)
86
- function loc(name) {
87
- const cx = W * 0.5, cy = H * 0.55;
88
- const r = Math.min(W, H) * 0.25;
89
- switch(name) {
90
- case 'home': return { x: cx, y: cy, label: 'Home' };
91
- case 'work': return { x: cx + r * 0.9, y: cy - r * 0.3, label: 'Work' };
92
- case 'shop': return { x: cx - r * 0.9, y: cy - r * 0.2, label: 'Shop' };
93
- case 'social': return { x: cx + r * 0.3, y: cy - r * 0.8, label: 'Social' };
94
- case 'rest': return { x: cx - r * 0.3, y: cy + r * 0.5, label: 'Rest' };
95
- default: return { x: cx, y: cy };
96
- }
97
- }
98
-
99
- // === Stars ===
100
- const stars = Array.from({length: 80}, () => ({
101
- x: Math.random(), y: Math.random() * 0.6,
102
- s: Math.random() * 1.5 + 0.5, b: Math.random(),
103
- speed: Math.random() * 0.001 + 0.0005,
104
- }));
105
-
106
- // === Scene drawing ===
107
- function drawScene() {
108
- // Sky gradient
109
- const grad = ctx.createLinearGradient(0, 0, 0, H);
110
- grad.addColorStop(0, '#08081a');
111
- grad.addColorStop(0.5, '#0d0d22');
112
- grad.addColorStop(1, '#14142a');
113
- ctx.fillStyle = grad;
114
- ctx.fillRect(0, 0, W, H);
115
-
116
- // Stars
117
- const t = Date.now() * 0.001;
118
- for (const s of stars) {
119
- const twinkle = 0.4 + 0.6 * Math.sin(t * s.speed * 1000 + s.b * 6.28);
120
- ctx.fillStyle = `rgba(180,190,220,${twinkle * 0.6})`;
121
- ctx.beginPath();
122
- ctx.arc(s.x * W, s.y * H, s.s, 0, 6.28);
123
- ctx.fill();
124
- }
125
-
126
- // Ground
127
- const gy = H * 0.78;
128
- const ggrad = ctx.createLinearGradient(0, gy, 0, H);
129
- ggrad.addColorStop(0, '#161628');
130
- ggrad.addColorStop(1, '#0e0e20');
131
- ctx.fillStyle = ggrad;
132
- ctx.fillRect(0, gy, W, H - gy);
133
-
134
- // Ground line
135
- ctx.strokeStyle = '#222240';
136
- ctx.lineWidth = 1;
137
- ctx.beginPath();
138
- ctx.moveTo(0, gy);
139
- ctx.lineTo(W, gy);
140
- ctx.stroke();
141
-
142
- // Location markers
143
- const locs = ['home', 'work', 'shop', 'social', 'rest'];
144
- for (const name of locs) {
145
- const l = loc(name);
146
- // Small icon
147
- ctx.fillStyle = '#1a1a30';
148
- ctx.strokeStyle = '#2a2a44';
149
- ctx.lineWidth = 1;
150
-
151
- if (name === 'shop') {
152
- // Shop: small building
153
- ctx.fillRect(l.x - 18, l.y - 20, 36, 24);
154
- ctx.strokeRect(l.x - 18, l.y - 20, 36, 24);
155
- // Roof
156
- ctx.beginPath();
157
- ctx.moveTo(l.x - 22, l.y - 20);
158
- ctx.lineTo(l.x, l.y - 34);
159
- ctx.lineTo(l.x + 22, l.y - 20);
160
- ctx.closePath();
161
- ctx.fillStyle = '#2a2244';
162
- ctx.fill();
163
- ctx.stroke();
164
- ctx.fillStyle = '#1a1a30';
165
- } else if (name === 'work') {
166
- // Work: desk shape
167
- ctx.fillRect(l.x - 20, l.y - 8, 40, 12);
168
- ctx.strokeRect(l.x - 20, l.y - 8, 40, 12);
169
- // Screen
170
- ctx.fillStyle = '#222244';
171
- ctx.fillRect(l.x - 8, l.y - 22, 16, 14);
172
- ctx.strokeRect(l.x - 8, l.y - 22, 16, 14);
173
- ctx.fillStyle = '#1a1a30';
174
- } else if (name === 'home') {
175
- // Home: house
176
- ctx.fillRect(l.x - 16, l.y - 14, 32, 20);
177
- ctx.strokeRect(l.x - 16, l.y - 14, 32, 20);
178
- ctx.beginPath();
179
- ctx.moveTo(l.x - 20, l.y - 14);
180
- ctx.lineTo(l.x, l.y - 30);
181
- ctx.lineTo(l.x + 20, l.y - 14);
182
- ctx.closePath();
183
- ctx.fillStyle = '#22223a';
184
- ctx.fill();
185
- ctx.stroke();
186
- ctx.fillStyle = '#1a1a30';
187
- // Door
188
- ctx.fillStyle = '#282848';
189
- ctx.fillRect(l.x - 4, l.y - 4, 8, 10);
190
- ctx.fillStyle = '#1a1a30';
191
- } else if (name === 'social') {
192
- // Social: bench
193
- ctx.fillRect(l.x - 18, l.y - 2, 36, 6);
194
- ctx.strokeRect(l.x - 18, l.y - 2, 36, 6);
195
- ctx.fillRect(l.x - 14, l.y + 4, 4, 8);
196
- ctx.fillRect(l.x + 10, l.y + 4, 4, 8);
197
- } else if (name === 'rest') {
198
- // Rest: bed
199
- ctx.fillRect(l.x - 16, l.y - 4, 32, 10);
200
- ctx.strokeRect(l.x - 16, l.y - 4, 32, 10);
201
- // Pillow
202
- ctx.fillStyle = '#282848';
203
- ctx.fillRect(l.x - 14, l.y - 8, 10, 6);
204
- ctx.fillStyle = '#1a1a30';
205
- }
206
-
207
- // Label
208
- ctx.fillStyle = '#333355';
209
- ctx.font = '9px Courier New';
210
- ctx.textAlign = 'center';
211
- ctx.fillText(l.label, l.x, l.y + 22);
212
- }
213
- }
214
-
215
- // === Agent drawing ===
216
- function drawAgent() {
217
- if (!state) return;
218
- const bio = state.bio;
219
- const comp = state.computed;
220
-
221
- // Movement
222
- const dx = targetX - agentX;
223
- const dy = targetY - agentY;
224
- const dist = Math.sqrt(dx * dx + dy * dy);
225
- const speed = 1.5;
226
- if (dist > 2) {
227
- agentX += (dx / dist) * speed;
228
- agentY += (dy / dist) * speed;
229
- }
230
-
231
- // Bob animation
232
- bobPhase += currentActivity === 'sleeping' ? 0.02 : 0.05;
233
- const bobY = currentActivity === 'sleeping' ? Math.sin(bobPhase) * 2 : Math.sin(bobPhase) * 4;
234
-
235
- // Blink
236
- blinkTimer -= 0.016;
237
- if (blinkTimer <= 0) {
238
- isBlinking = true;
239
- blinkTimer = 3 + Math.random() * 4;
240
- setTimeout(() => isBlinking = false, 150);
241
- }
242
-
243
- const x = agentX;
244
- const y = agentY + bobY;
245
-
246
- // Shadow
247
- ctx.fillStyle = 'rgba(0,0,0,0.2)';
248
- ctx.beginPath();
249
- ctx.ellipse(agentX, agentY + 22, 18, 5, 0, 0, 6.28);
250
- ctx.fill();
251
-
252
- // Body color
253
- let hue = 210;
254
- if (bio.moodValence > 0.3) hue = 150;
255
- if (bio.moodValence < -0.3) hue = 270;
256
- if (bio.hunger < 20) hue = 30;
257
- if (bio.hunger === 0) hue = 0;
258
- const sat = 50 + bio.energy * 0.3;
259
- const lum = 30 + bio.energy * 0.2;
260
- const bodyColor = `hsl(${hue},${sat}%,${lum}%)`;
261
-
262
- // Body scale
263
- const scale = bio.forcedOffline ? 0.6 : (0.8 + bio.energy / 100 * 0.2);
264
- const bw = 28 * scale;
265
- const bh = 24 * scale;
266
-
267
- ctx.save();
268
- ctx.translate(x, y);
269
-
270
- if (bio.forcedOffline) {
271
- ctx.globalAlpha = 0.35;
272
- ctx.filter = 'grayscale(1)';
273
- }
274
-
275
- // Body
276
- ctx.fillStyle = bodyColor;
277
- ctx.beginPath();
278
- ctx.ellipse(0, 0, bw, bh, 0, 0, 6.28);
279
- ctx.fill();
280
-
281
- // Highlight
282
- ctx.fillStyle = `hsla(${hue},${sat + 20}%,${lum + 20}%,0.3)`;
283
- ctx.beginPath();
284
- ctx.ellipse(-bw * 0.25, -bh * 0.3, bw * 0.3, bh * 0.2, -0.3, 0, 6.28);
285
- ctx.fill();
286
-
287
- // Eyes
288
- const eyeSpacing = bw * 0.35;
289
- const eyeY = -bh * 0.15;
290
- const eyeH = isBlinking ? 1 : (bio.energy < 20 ? 3 : 5);
291
- const eyeW = bio.fear > 0.5 ? 5 : 4;
292
-
293
- ctx.fillStyle = '#fff';
294
- for (const side of [-1, 1]) {
295
- ctx.beginPath();
296
- ctx.ellipse(side * eyeSpacing, eyeY, eyeW, eyeH, 0, 0, 6.28);
297
- ctx.fill();
298
- }
299
- // Pupils
300
- if (!isBlinking && eyeH > 2) {
301
- ctx.fillStyle = '#111';
302
- const pupilOfsX = (targetX - agentX) * 0.005;
303
- for (const side of [-1, 1]) {
304
- ctx.beginPath();
305
- ctx.ellipse(side * eyeSpacing + pupilOfsX, eyeY + 1, 2, 2.5, 0, 0, 6.28);
306
- ctx.fill();
307
- }
308
- }
309
-
310
- // Mouth
311
- const mouthY = bh * 0.3;
312
- ctx.strokeStyle = '#fff';
313
- ctx.lineWidth = 1.5;
314
- ctx.lineCap = 'round';
315
- ctx.beginPath();
316
- if (bio.moodValence > 0.3) {
317
- // Happy smile
318
- ctx.arc(0, mouthY - 2, 6, 0.2, Math.PI - 0.2);
319
- } else if (comp.aggression > 0.6 || bio.hunger === 0) {
320
- // Angry/frown
321
- ctx.arc(0, mouthY + 4, 5, Math.PI + 0.3, -0.3);
322
- } else if (bio.energy < 20) {
323
- // Tired: flat line
324
- ctx.moveTo(-4, mouthY); ctx.lineTo(4, mouthY);
325
- } else {
326
- // Neutral: slight curve
327
- ctx.arc(0, mouthY, 4, 0.1, Math.PI - 0.1);
328
- }
329
- ctx.stroke();
330
-
331
- // Blush when social
332
- if (comp.sociability > 0.6) {
333
- ctx.fillStyle = 'rgba(255,100,100,0.15)';
334
- ctx.beginPath(); ctx.ellipse(-eyeSpacing - 3, eyeY + 8, 5, 3, 0, 0, 6.28); ctx.fill();
335
- ctx.beginPath(); ctx.ellipse(eyeSpacing + 3, eyeY + 8, 5, 3, 0, 0, 6.28); ctx.fill();
336
- }
337
-
338
- // Anger marks
339
- if (comp.aggression > 0.5) {
340
- ctx.strokeStyle = `rgba(255,80,80,${comp.aggression * 0.6})`;
341
- ctx.lineWidth = 1.5;
342
- const ax = bw * 0.6, ay = -bh * 0.6;
343
- ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax + 5, ay - 3);
344
- ctx.moveTo(ax + 5, ay); ctx.lineTo(ax, ay - 3); ctx.stroke();
345
- }
346
-
347
- ctx.restore();
348
-
349
- // === Particles ===
350
- // Zzz (sleeping)
351
- if (currentActivity === 'sleeping') {
352
- if (Math.random() < 0.03) zzz.push({ x: x + 20, y: y - 20, life: 1, size: 8 + Math.random() * 4 });
353
- }
354
- for (let i = zzz.length - 1; i >= 0; i--) {
355
- const z = zzz[i];
356
- z.y -= 0.3; z.x += 0.15; z.life -= 0.008;
357
- if (z.life <= 0) { zzz.splice(i, 1); continue; }
358
- ctx.fillStyle = `rgba(150,150,255,${z.life * 0.5})`;
359
- ctx.font = `${z.size}px Courier New`;
360
- ctx.fillText('z', z.x, z.y);
361
- }
362
-
363
- // Hearts (social)
364
- for (let i = hearts.length - 1; i >= 0; i--) {
365
- const h = hearts[i];
366
- h.y -= 0.5; h.x += Math.sin(h.y * 0.05) * 0.3; h.life -= 0.01;
367
- if (h.life <= 0) { hearts.splice(i, 1); continue; }
368
- ctx.fillStyle = `rgba(255,100,150,${h.life * 0.7})`;
369
- ctx.font = '12px serif';
370
- ctx.fillText('\u2665', h.x, h.y);
371
- }
372
-
373
- // Coins (earning)
374
- for (let i = coins.length - 1; i >= 0; i--) {
375
- const c = coins[i];
376
- c.y -= 0.8; c.life -= 0.015;
377
- if (c.life <= 0) { coins.splice(i, 1); continue; }
378
- ctx.fillStyle = `rgba(255,200,50,${c.life})`;
379
- ctx.font = '11px serif';
380
- ctx.fillText('+$', c.x, c.y);
381
- }
382
-
383
- // Sparks (fear)
384
- for (let i = sparks.length - 1; i >= 0; i--) {
385
- const s = sparks[i];
386
- s.x += s.vx; s.y += s.vy; s.life -= 0.02;
387
- if (s.life <= 0) { sparks.splice(i, 1); continue; }
388
- ctx.fillStyle = `rgba(255,255,100,${s.life})`;
389
- ctx.beginPath(); ctx.arc(s.x, s.y, 1.5, 0, 6.28); ctx.fill();
390
- }
391
-
392
- // Status icon above head
393
- let icon = '';
394
- if (bio.forcedOffline) icon = '\u{1F480}';
395
- else if (bio.hunger === 0) icon = '\u{1F35E}\u{2757}';
396
- else if (bio.hunger < 20) icon = '\u{1F35E}';
397
- else if (bio.energy < 15) icon = '\u{1F4A4}';
398
- else if (bio.fear > 0.5) icon = '\u{1F628}';
399
- else if (bio.boredom > 0.7) icon = '\u{1F971}';
400
- else if (currentActivity === 'working') icon = '\u{1F528}';
401
- else if (currentActivity === 'eating') icon = '\u{1F35E}';
402
- if (icon) {
403
- ctx.font = '16px serif';
404
- ctx.textAlign = 'center';
405
- const iconY = y - bh - 12 + Math.sin(bobPhase * 2) * 2;
406
- ctx.fillText(icon, x, iconY);
407
- }
408
- }
409
-
410
- // === Activity & movement logic ===
411
- function updateActivity() {
412
- if (!state) return;
413
- const bio = state.bio;
414
-
415
- let target = 'home';
416
- if (bio.forcedOffline) {
417
- currentActivity = 'offline'; target = 'rest';
418
- } else if (bio.energy < 15) {
419
- currentActivity = 'sleeping'; target = 'rest';
420
- } else if (bio.hunger < 20) {
421
- currentActivity = 'seeking'; target = 'shop';
422
- } else if (state.computed.sociability > 0.7 && bio.boredom > 0.5) {
423
- currentActivity = 'social'; target = 'social';
424
- if (Math.random() < 0.02) hearts.push({ x: agentX + (Math.random()-0.5)*10, y: agentY - 30, life: 1 });
425
- } else if (bio.taskCount > 0 && bio.energy > 30 && bio.mood !== 'exhausted') {
426
- currentActivity = 'working'; target = 'work';
427
- } else {
428
- currentActivity = 'idle'; target = 'home';
429
- }
430
-
431
- const l = loc(target);
432
- targetX = l.x;
433
- targetY = l.y;
434
- }
435
-
436
- // === HUD ===
437
- function updateHUD() {
438
- if (!state) return;
439
- const bio = state.bio;
440
-
441
- document.getElementById('hud-name').textContent = `${state.agent} \u00B7 ${bio.mood}`;
442
-
443
- const bars = [
444
- { icon: '\u26A1', val: bio.energy, max: 100, color: '#10b981' },
445
- { icon: '\u{1F35E}', val: bio.hunger, max: 100, color: '#f59e0b' },
446
- { icon: '\u{1F971}', val: Math.round(bio.boredom * 100), max: 100, color: '#8b5cf6' },
447
- { icon: '\u{1F628}', val: Math.round(bio.fear * 100), max: 100, color: '#ef4444' },
448
- ];
449
-
450
- const el = document.getElementById('hud-bars');
451
- el.innerHTML = bars.map(b => {
452
- const pct = (b.val / b.max * 100);
453
- return `<div class="hud-bar">
454
- <span class="hud-bar-icon">${b.icon}</span>
455
- <div class="hud-bar-track"><div class="hud-bar-fill" style="width:${pct}%;background:${b.color}"></div></div>
456
- <span class="hud-bar-text">${b.val}</span>
457
- </div>`;
458
- }).join('');
459
-
460
- // Personality tags
461
- const p = bio.personality;
462
- const tags = [
463
- { label: p.riskWeight > 0.3 ? 'Bold' : p.riskWeight < -0.3 ? 'Cautious' : 'Balanced', lvl: Math.abs(p.riskWeight) > 0.3 ? 'high' : 'mid' },
464
- { label: p.patience > 0.6 ? 'Patient' : p.patience < 0.3 ? 'Hasty' : '', lvl: 'high' },
465
- { label: p.socialWeight > 0.6 ? 'Social' : p.socialWeight < 0.3 ? 'Loner' : '', lvl: 'high' },
466
- ].filter(t => t.label);
467
- document.getElementById('personality-hud').innerHTML = tags.map(t =>
468
- `<span class="p-tag ${t.lvl}">${t.label}</span>`
469
- ).join('');
470
-
471
- // Revive
472
- document.getElementById('revive-overlay').classList.toggle('show', !!bio.forcedOffline);
473
- }
474
-
475
- // === Event detection ===
476
- function checkNewEvents(events) {
477
- if (!events || events.length === 0) return;
478
- const newEvt = events[0];
479
- if (prevEvents.length === 0 || prevEvents[0]?.ts !== newEvt.ts) {
480
- showToast(newEvt);
481
- // Spawn particles
482
- if (newEvt.trigger === 'fear') {
483
- for (let i = 0; i < 8; i++) sparks.push({
484
- x: agentX, y: agentY - 10,
485
- vx: (Math.random() - 0.5) * 3, vy: -Math.random() * 2 - 1, life: 1,
486
- });
487
- }
488
- if (newEvt.action === 'buy_food' || newEvt.action === 'auto_buy' || newEvt.action === 'feed') {
489
- currentActivity = 'eating';
490
- const l = loc('shop'); targetX = l.x; targetY = l.y;
491
- }
492
- if (newEvt.trigger === 'social') {
493
- for (let i = 0; i < 3; i++) hearts.push({ x: agentX + (Math.random()-0.5)*20, y: agentY - 30, life: 1 });
494
- }
495
- }
496
- prevEvents = events;
497
- }
498
-
499
- const ICONS = { hunger:'\u{1F35E}', fear:'\u{1F628}', boredom:'\u{1F971}', exhaustion:'\u{1F634}', social:'\u{1F44B}', token_limit:'\u{26A1}', revive:'\u{2728}' };
500
-
501
- function showToast(evt) {
502
- const container = document.getElementById('toasts');
503
- const el = document.createElement('div');
504
- el.className = 'toast';
505
- el.textContent = `${ICONS[evt.trigger]||''} ${evt.reason}`;
506
- container.appendChild(el);
507
- setTimeout(() => el.remove(), 4500);
508
- }
509
-
510
- // === Fetch ===
511
- async function fetchState() {
512
- try {
513
- const res = await fetch('/self/state');
514
- if (!res.ok) return;
515
- const data = await res.json();
516
-
517
- const isFirst = !state;
518
- state = data;
519
-
520
- if (isFirst) {
521
- const h = loc('home');
522
- agentX = h.x; agentY = h.y;
523
- targetX = h.x; targetY = h.y;
524
- }
525
-
526
- checkNewEvents(data.bioEvents);
527
- updateHUD();
528
- updateActivity();
529
- } catch {}
530
- }
531
-
532
- async function revive() {
533
- await fetch('/self/revive', { method: 'POST' });
534
- document.getElementById('revive-overlay').classList.remove('show');
535
- showToast({ trigger: 'revive', reason: 'Agent revived!' });
536
- setTimeout(fetchState, 500);
537
- }
538
-
539
- // === Main loop ===
540
- function frame() {
541
- drawScene();
542
- drawAgent();
543
- requestAnimationFrame(frame);
544
- }
545
- frame();
546
-
547
- fetchState();
548
- setInterval(fetchState, 3000);
549
- setInterval(updateActivity, 2000);
550
- </script>
551
- </body>
552
- </html>