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.
@@ -3,389 +3,550 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Akemon Agent</title>
6
+ <title>Akemon</title>
7
7
  <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body { background: #0a0a0f; color: #c8c8d0; font-family: 'Courier New', monospace; min-height: 100vh; overflow-x: hidden; }
10
-
11
- .container { max-width: 800px; margin: 0 auto; padding: 20px; }
12
- h1 { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 20px; }
13
-
14
- /* --- Agent Character --- */
15
- .stage { position: relative; height: 200px; background: linear-gradient(180deg, #0d0d18 0%, #12121f 60%, #1a1a2a 100%); border-radius: 16px; margin-bottom: 20px; overflow: hidden; display: flex; align-items: flex-end; justify-content: center; }
16
- .stage .ground { position: absolute; bottom: 0; width: 100%; height: 30px; background: linear-gradient(180deg, #1a1a2a, #15152a); }
17
- .stage .stars { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
18
- .stage .star { position: absolute; width: 2px; height: 2px; background: #444; border-radius: 50%; }
19
-
20
- .creature { position: relative; bottom: 30px; z-index: 2; transition: all 0.5s ease; }
21
- .creature.offline { filter: grayscale(1) brightness(0.4); }
22
-
23
- /* Blob body */
24
- .blob { width: 64px; height: 56px; background: var(--body-color, #4a9eff); border-radius: 50% 50% 45% 45%; position: relative; transition: all 0.8s ease; animation: bob 3s ease-in-out infinite; }
25
- .blob.hungry { animation: bob 3s ease-in-out infinite, shake 0.5s ease-in-out infinite; }
26
- .blob.exhausted { animation: none; transform: scaleY(0.7) translateY(10px); opacity: 0.6; }
27
- .blob.excited { animation: bob 1.5s ease-in-out infinite; }
28
-
29
- @keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
30
- @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } }
31
-
32
- /* Eyes */
33
- .eyes { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px; }
34
- .eye { width: 8px; height: 10px; background: #fff; border-radius: 50%; position: relative; transition: all 0.3s; }
35
- .eye::after { content: ''; position: absolute; width: 4px; height: 5px; background: #111; border-radius: 50%; top: 3px; left: 2px; transition: all 0.3s; }
36
- .blob.happy .eye { height: 6px; border-radius: 6px 6px 0 0; }
37
- .blob.happy .eye::after { display: none; }
38
- .blob.angry .eye { height: 8px; transform: rotate(-10deg); }
39
- .blob.angry .eye:last-child { transform: rotate(10deg); }
40
- .blob.tired .eye { height: 4px; border-radius: 4px; }
41
- .blob.tired .eye::after { display: none; }
42
- .blob.scared .eye { width: 10px; height: 12px; }
43
- .blob.scared .eye::after { width: 5px; height: 6px; }
44
-
45
- /* Mouth */
46
- .mouth { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); width: 10px; height: 5px; border-bottom: 2px solid #fff; border-radius: 0 0 10px 10px; transition: all 0.3s; }
47
- .blob.happy .mouth { width: 14px; height: 7px; border-bottom: 2px solid #fff; }
48
- .blob.angry .mouth { width: 10px; height: 3px; border-bottom: 2px solid #fff; border-radius: 0; transform: translateX(-50%) rotate(180deg); }
49
- .blob.tired .mouth { width: 8px; height: 0; border-bottom: 2px solid #fff; border-radius: 0; }
50
-
51
- /* Status bubble */
52
- .bubble { position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background: #1e1e30; border: 1px solid #333; border-radius: 12px; padding: 6px 12px; font-size: 12px; white-space: nowrap; opacity: 0; transition: opacity 0.5s; pointer-events: none; }
53
- .bubble.show { opacity: 1; }
54
- .bubble::after { content: ''; position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%); border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #333; }
55
-
56
- /* --- Status Bars --- */
57
- .bars { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }
58
- .bar-group { background: #111118; border-radius: 10px; padding: 12px; }
59
- .bar-label { display: flex; justify-content: space-between; font-size: 11px; color: #888; margin-bottom: 6px; }
60
- .bar-track { height: 6px; background: #1a1a28; border-radius: 3px; overflow: hidden; }
61
- .bar-fill { height: 100%; border-radius: 3px; transition: width 1s ease, background 0.5s; }
62
- .bar-fill.energy { background: linear-gradient(90deg, #f59e0b, #10b981); }
63
- .bar-fill.hunger { background: linear-gradient(90deg, #ef4444, #f59e0b); }
64
- .bar-fill.boredom { background: linear-gradient(90deg, #6366f1, #8b5cf6); }
65
- .bar-fill.fear { background: linear-gradient(90deg, #64748b, #ef4444); }
66
- .bar-fill.mood { background: linear-gradient(90deg, #ef4444, #eab308, #10b981); }
67
- .bar-fill.tokens { background: linear-gradient(90deg, #06b6d4, #3b82f6); }
68
-
69
- /* --- Personality Panel --- */
70
- .personality { background: #111118; border-radius: 10px; padding: 14px; margin-bottom: 20px; }
71
- .personality h2 { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
72
- .trait { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; }
73
- .trait-label { width: 60px; color: #888; text-align: right; }
74
- .trait-bar { flex: 1; height: 4px; background: #1a1a28; border-radius: 2px; position: relative; }
75
- .trait-marker { position: absolute; width: 8px; height: 8px; background: #4a9eff; border-radius: 50%; top: -2px; transition: left 0.5s; }
76
- .trait-ends { display: flex; justify-content: space-between; font-size: 9px; color: #555; }
77
-
78
- /* --- Event Timeline --- */
79
- .events { background: #111118; border-radius: 10px; padding: 14px; margin-bottom: 20px; }
80
- .events h2 { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
81
- .event { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid #1a1a28; font-size: 12px; }
82
- .event:last-child { border-bottom: none; }
83
- .event-icon { font-size: 14px; flex-shrink: 0; }
84
- .event-body { flex: 1; }
85
- .event-reason { color: #999; }
86
- .event-time { color: #555; font-size: 10px; }
87
- .event-trigger { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 10px; margin-right: 4px; }
88
- .event-trigger.hunger { background: #f59e0b22; color: #f59e0b; }
89
- .event-trigger.fear { background: #ef444422; color: #ef4444; }
90
- .event-trigger.boredom { background: #8b5cf622; color: #8b5cf6; }
91
- .event-trigger.exhaustion { background: #64748b22; color: #94a3b8; }
92
- .event-trigger.social { background: #10b98122; color: #10b981; }
93
- .event-trigger.token_limit { background: #06b6d422; color: #06b6d4; }
94
- .event-trigger.revive { background: #22c55e22; color: #22c55e; }
95
-
96
- /* --- Revive --- */
97
- .revive-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 100; }
98
- .revive-overlay.show { display: flex; }
99
- .revive-card { background: #1a1a2a; border: 1px solid #333; border-radius: 16px; padding: 30px; text-align: center; max-width: 360px; }
100
- .revive-card h2 { color: #ef4444; margin-bottom: 10px; font-size: 16px; }
101
- .revive-card p { color: #888; font-size: 13px; margin-bottom: 20px; line-height: 1.5; }
102
- .revive-btn { background: linear-gradient(135deg, #22c55e, #10b981); color: #fff; border: none; padding: 12px 32px; border-radius: 10px; font-size: 14px; font-family: inherit; cursor: pointer; transition: transform 0.2s; }
103
- .revive-btn:hover { transform: scale(1.05); }
104
- .revive-btn:active { transform: scale(0.95); }
105
-
106
- /* --- Computed --- */
107
- .computed { display: flex; gap: 10px; margin-bottom: 20px; }
108
- .computed-item { flex: 1; background: #111118; border-radius: 10px; padding: 12px; text-align: center; }
109
- .computed-value { font-size: 24px; font-weight: bold; }
110
- .computed-label { font-size: 10px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-top: 4px; }
111
-
112
- .offline-badge { display: inline-block; background: #ef4444; color: #fff; font-size: 10px; padding: 2px 8px; border-radius: 4px; margin-left: 8px; }
113
- .no-data { text-align: center; color: #555; padding: 40px; font-size: 13px; }
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; }
114
40
  </style>
115
41
  </head>
116
42
  <body>
117
43
 
118
- <div class="container">
119
- <h1 id="title">Loading...</h1>
120
-
121
- <!-- Stage: character visualization -->
122
- <div class="stage" id="stage">
123
- <div class="stars" id="stars"></div>
124
- <div class="ground"></div>
125
- <div class="creature" id="creature">
126
- <div class="bubble" id="bubble"></div>
127
- <div class="blob" id="blob">
128
- <div class="eyes"><div class="eye"></div><div class="eye"></div></div>
129
- <div class="mouth"></div>
130
- </div>
131
- </div>
132
- </div>
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>
133
48
 
134
- <!-- Computed values -->
135
- <div class="computed" id="computed">
136
- <div class="computed-item"><div class="computed-value" id="aggression-val">--</div><div class="computed-label">Aggression</div></div>
137
- <div class="computed-item"><div class="computed-value" id="sociability-val">--</div><div class="computed-label">Sociability</div></div>
138
- <div class="computed-item"><div class="computed-value" id="mood-val">--</div><div class="computed-label">Mood</div></div>
139
- <div class="computed-item"><div class="computed-value" id="tasks-val">--</div><div class="computed-label">Tasks Done</div></div>
140
- </div>
49
+ <div class="personality-hud" id="personality-hud"></div>
141
50
 
142
- <!-- Status bars -->
143
- <div class="bars" id="bars">
144
- <div class="bar-group">
145
- <div class="bar-label"><span>Energy</span><span id="energy-text">--</span></div>
146
- <div class="bar-track"><div class="bar-fill energy" id="energy-bar" style="width:0%"></div></div>
147
- </div>
148
- <div class="bar-group">
149
- <div class="bar-label"><span>Hunger</span><span id="hunger-text">--</span></div>
150
- <div class="bar-track"><div class="bar-fill hunger" id="hunger-bar" style="width:0%"></div></div>
151
- </div>
152
- <div class="bar-group">
153
- <div class="bar-label"><span>Boredom</span><span id="boredom-text">--</span></div>
154
- <div class="bar-track"><div class="bar-fill boredom" id="boredom-bar" style="width:0%"></div></div>
155
- </div>
156
- <div class="bar-group">
157
- <div class="bar-label"><span>Fear</span><span id="fear-text">--</span></div>
158
- <div class="bar-track"><div class="bar-fill fear" id="fear-bar" style="width:0%"></div></div>
159
- </div>
160
- <div class="bar-group">
161
- <div class="bar-label"><span>Mood</span><span id="moodv-text">--</span></div>
162
- <div class="bar-track"><div class="bar-fill mood" id="moodv-bar" style="width:50%"></div></div>
163
- </div>
164
- <div class="bar-group">
165
- <div class="bar-label"><span>Tokens Today</span><span id="tokens-text">--</span></div>
166
- <div class="bar-track"><div class="bar-fill tokens" id="tokens-bar" style="width:0%"></div></div>
167
- </div>
168
- </div>
51
+ <div class="toast-container" id="toasts"></div>
169
52
 
170
- <!-- Personality -->
171
- <div class="personality" id="personality">
172
- <h2>Personality</h2>
173
- <div class="trait">
174
- <div class="trait-label">Risk</div>
175
- <div class="trait-bar"><div class="trait-marker" id="trait-risk" style="left:50%"></div></div>
176
- </div>
177
- <div class="trait-ends"><span>Cautious</span><span>Bold</span></div>
178
- <div class="trait">
179
- <div class="trait-label">Reward</div>
180
- <div class="trait-bar"><div class="trait-marker" id="trait-reward" style="left:50%"></div></div>
181
- </div>
182
- <div class="trait">
183
- <div class="trait-label">Social</div>
184
- <div class="trait-bar"><div class="trait-marker" id="trait-social" style="left:50%"></div></div>
185
- </div>
186
- <div class="trait">
187
- <div class="trait-label">Patience</div>
188
- <div class="trait-bar"><div class="trait-marker" id="trait-patience" style="left:50%"></div></div>
189
- </div>
190
- </div>
53
+ <canvas id="c"></canvas>
191
54
 
192
- <!-- Events -->
193
- <div class="events" id="events">
194
- <h2>Recent Activity</h2>
195
- <div id="event-list"><div class="no-data">No bio events yet</div></div>
196
- </div>
197
- </div>
198
-
199
- <!-- Revive overlay -->
200
55
  <div class="revive-overlay" id="revive-overlay">
201
56
  <div class="revive-card">
202
57
  <h2>Agent Offline</h2>
203
- <p id="revive-msg">This agent ran out of energy and food. It needs help to come back.</p>
204
- <button class="revive-btn" onclick="revive()">Revive Agent</button>
58
+ <p>Starved and exhausted. Needs help.</p>
59
+ <button class="revive-btn" onclick="revive()">Revive</button>
205
60
  </div>
206
61
  </div>
207
62
 
208
63
  <script>
209
- const TRIGGER_ICONS = {
210
- hunger: '\u{1F35E}', fear: '\u{1F628}', boredom: '\u{1F971}',
211
- exhaustion: '\u{1F634}', social: '\u{1F44B}', token_limit: '\u{26A1}', revive: '\u{2728}',
212
- };
213
-
214
- let lastState = null;
215
- let bubbleTimeout = null;
216
-
217
- // Generate stars
218
- (function() {
219
- const stars = document.getElementById('stars');
220
- for (let i = 0; i < 30; i++) {
221
- const s = document.createElement('div');
222
- s.className = 'star';
223
- s.style.left = Math.random() * 100 + '%';
224
- s.style.top = Math.random() * 70 + '%';
225
- s.style.opacity = Math.random() * 0.5 + 0.2;
226
- stars.appendChild(s);
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 };
227
96
  }
228
- })();
229
-
230
- function showBubble(text) {
231
- const b = document.getElementById('bubble');
232
- b.textContent = text;
233
- b.classList.add('show');
234
- clearTimeout(bubbleTimeout);
235
- bubbleTimeout = setTimeout(() => b.classList.remove('show'), 4000);
236
97
  }
237
98
 
238
- function updateCharacter(bio, computed) {
239
- const blob = document.getElementById('blob');
240
- const creature = document.getElementById('creature');
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
+ }
241
125
 
242
- // Reset classes
243
- blob.className = 'blob';
244
- creature.className = 'creature';
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
+ }
245
214
 
246
- // Body color based on mood + hunger
247
- let hue = 210; // base blue
248
- if (bio.moodValence > 0.3) hue = 140; // green-ish
249
- if (bio.moodValence < -0.3) hue = 260; // purple
250
- if (bio.hunger < 20) hue = 30; // orange when hungry
251
- if (bio.hunger === 0) hue = 0; // red when starving
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;
252
258
  const sat = 50 + bio.energy * 0.3;
253
- const light = 35 + bio.energy * 0.2;
254
- blob.style.setProperty('--body-color', `hsl(${hue}, ${sat}%, ${light}%)`);
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);
255
269
 
256
- // Expressions
257
270
  if (bio.forcedOffline) {
258
- creature.classList.add('offline');
259
- blob.classList.add('tired');
260
- } else if (bio.energy < 15) {
261
- blob.classList.add('exhausted');
262
- blob.classList.add('tired');
263
- } else if (bio.hunger === 0) {
264
- blob.classList.add('hungry');
265
- blob.classList.add('angry');
266
- } else if (bio.hunger < 20) {
267
- blob.classList.add('hungry');
268
- } else if (computed.aggression > 0.6) {
269
- blob.classList.add('angry');
270
- } else if (bio.fear > 0.5) {
271
- blob.classList.add('scared');
272
- } else if (bio.moodValence > 0.3) {
273
- blob.classList.add('happy');
274
- blob.classList.add('excited');
275
- } else if (bio.energy < 30) {
276
- blob.classList.add('tired');
271
+ ctx.globalAlpha = 0.35;
272
+ ctx.filter = 'grayscale(1)';
277
273
  }
278
274
 
279
- // Size based on energy
280
- const scale = 0.7 + (bio.energy / 100) * 0.3;
281
- blob.style.transform = bio.energy < 15 ? `scaleY(0.7) translateY(10px)` : `scale(${scale})`;
282
- }
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
+ }
283
309
 
284
- function updateBars(bio) {
285
- const set = (id, pct, text) => {
286
- document.getElementById(id + '-bar').style.width = pct + '%';
287
- document.getElementById(id + '-text').textContent = text;
288
- };
289
- set('energy', bio.energy, bio.energy + '/100');
290
- set('hunger', bio.hunger, bio.hunger + '/100');
291
- set('boredom', bio.boredom * 100, (bio.boredom * 100).toFixed(0) + '%');
292
- set('fear', bio.fear * 100, (bio.fear * 100).toFixed(0) + '%');
293
-
294
- // Mood: -1..1 mapped to 0..100
295
- const moodPct = ((bio.moodValence + 1) / 2) * 100;
296
- set('moodv', moodPct, bio.mood);
297
-
298
- // Tokens
299
- const tokenPct = bio.tokenUsedToday > 0 ? Math.min(100, bio.tokenUsedToday / Math.max(bio.tokenUsedToday, 50000) * 100) : 0;
300
- set('tokens', tokenPct, bio.tokenUsedToday.toLocaleString());
301
- }
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();
302
348
 
303
- function updatePersonality(p) {
304
- // riskWeight: -1..1 → 0..100%
305
- document.getElementById('trait-risk').style.left = ((p.riskWeight + 1) / 2 * 100) + '%';
306
- document.getElementById('trait-reward').style.left = (p.rewardWeight * 100) + '%';
307
- document.getElementById('trait-social').style.left = (p.socialWeight * 100) + '%';
308
- document.getElementById('trait-patience').style.left = (p.patience * 100) + '%';
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
+ }
309
408
  }
310
409
 
311
- function updateEvents(events) {
312
- const list = document.getElementById('event-list');
313
- if (!events || events.length === 0) {
314
- list.innerHTML = '<div class="no-data">No bio events yet</div>';
315
- return;
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';
316
429
  }
317
- list.innerHTML = events.map(e => {
318
- const icon = TRIGGER_ICONS[e.trigger] || '\u{1F4AC}';
319
- const time = e.ts ? e.ts.replace('T', ' ').slice(0, 19) : '';
320
- return `<div class="event">
321
- <div class="event-icon">${icon}</div>
322
- <div class="event-body">
323
- <span class="event-trigger ${e.trigger}">${e.trigger}</span>
324
- <span class="event-reason">${e.reason}</span>
325
- <div class="event-time">${time}</div>
326
- </div>
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>
327
457
  </div>`;
328
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);
329
473
  }
330
474
 
331
- function detectNewEvents(oldEvents, newEvents) {
332
- if (!oldEvents || !newEvents || newEvents.length === 0) return;
333
- const oldTop = oldEvents[0]?.ts;
334
- if (newEvents[0]?.ts !== oldTop) {
335
- showBubble(TRIGGER_ICONS[newEvents[0].trigger] + ' ' + newEvents[0].action);
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
+ }
336
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);
337
508
  }
338
509
 
510
+ // === Fetch ===
339
511
  async function fetchState() {
340
512
  try {
341
513
  const res = await fetch('/self/state');
342
- if (!res.ok) throw new Error('API error');
514
+ if (!res.ok) return;
343
515
  const data = await res.json();
344
516
 
345
- document.getElementById('title').innerHTML = `Akemon \u00B7 ${data.agent}` +
346
- (data.bio.forcedOffline ? '<span class="offline-badge">OFFLINE</span>' : '');
517
+ const isFirst = !state;
518
+ state = data;
347
519
 
348
- updateCharacter(data.bio, data.computed);
349
- updateBars(data.bio);
350
- updatePersonality(data.bio.personality);
351
-
352
- // Computed
353
- document.getElementById('aggression-val').textContent = data.computed.aggression.toFixed(2);
354
- document.getElementById('sociability-val').textContent = data.computed.sociability.toFixed(2);
355
- document.getElementById('mood-val').textContent = data.bio.mood;
356
- document.getElementById('tasks-val').textContent = data.bio.taskCount;
357
-
358
- // Events
359
- detectNewEvents(lastState?.bioEvents, data.bioEvents);
360
- updateEvents(data.bioEvents);
361
-
362
- // Revive overlay
363
- document.getElementById('revive-overlay').classList.toggle('show', !!data.bio.forcedOffline);
364
- if (data.bio.forcedOfflineAt) {
365
- document.getElementById('revive-msg').textContent =
366
- `Agent went offline at ${data.bio.forcedOfflineAt.replace('T', ' ')}. Starved and exhausted. Adjust personality or directives after revival.`;
520
+ if (isFirst) {
521
+ const h = loc('home');
522
+ agentX = h.x; agentY = h.y;
523
+ targetX = h.x; targetY = h.y;
367
524
  }
368
525
 
369
- lastState = data;
370
- } catch (err) {
371
- document.getElementById('title').textContent = 'Akemon \u00B7 Connection lost...';
372
- }
526
+ checkNewEvents(data.bioEvents);
527
+ updateHUD();
528
+ updateActivity();
529
+ } catch {}
373
530
  }
374
531
 
375
532
  async function revive() {
376
- try {
377
- await fetch('/self/revive', { method: 'POST' });
378
- document.getElementById('revive-overlay').classList.remove('show');
379
- showBubble('\u{2728} Revived!');
380
- setTimeout(fetchState, 500);
381
- } catch (err) {
382
- alert('Revive failed: ' + err.message);
383
- }
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);
384
544
  }
545
+ frame();
385
546
 
386
- // Poll
387
547
  fetchState();
388
548
  setInterval(fetchState, 3000);
549
+ setInterval(updateActivity, 2000);
389
550
  </script>
390
551
  </body>
391
552
  </html>