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.
- package/README.md +6 -1
- package/dist/cli.js +209 -19
- package/dist/server.js +236 -2
- package/dist/software-agent-memory.js +141 -0
- package/dist/software-agent-peripheral.js +297 -24
- package/package.json +4 -4
- package/dist/context.test.js +0 -90
- package/dist/dashboard.html +0 -552
- package/dist/engine-queue.test.js +0 -99
- package/dist/engine-routing.test.js +0 -122
- package/dist/engine-stream.test.js +0 -103
- package/dist/event-bus.test.js +0 -51
- package/dist/orphan-scan.test.js +0 -81
- package/dist/reflection-module.integration.test.js +0 -180
- package/dist/reflection-module.test.js +0 -66
- package/dist/role-module.test.js +0 -208
- package/dist/software-agent-http.test.js +0 -108
- package/dist/software-agent-peripheral.test.js +0 -187
- package/dist/task-helpers.test.js +0 -88
package/dist/context.test.js
DELETED
|
@@ -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
|
-
});
|
package/dist/dashboard.html
DELETED
|
@@ -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>
|