@clawlabz/clawskin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,596 @@
1
+ /**
2
+ * AgentSlot.js — One agent in the multi-agent office
3
+ * Handles: unique appearance, workstation binding, smooth idle wandering with POI system, click-to-customize
4
+ */
5
+ class AgentSlot {
6
+ constructor(options = {}) {
7
+ this.agentId = options.agentId || 'main';
8
+ this.sessionKeys = options.sessionKeys || ['main'];
9
+ this.label = options.label || options.agentId || 'Agent';
10
+ this.name = options.name || this.label;
11
+
12
+ // Seeded random config per agent — deterministic per agentId
13
+ const savedConfigs = AgentSlot._loadConfigs();
14
+ let charConfig = savedConfigs[this.agentId];
15
+ if (!charConfig) {
16
+ charConfig = CharacterSprite.randomConfig();
17
+ savedConfigs[this.agentId] = charConfig;
18
+ AgentSlot._saveConfigs(savedConfigs);
19
+ }
20
+
21
+ this.character = new CharacterSprite(charConfig, options.x || 0, options.y || 0);
22
+ this.stateMapper = new AgentStateMapper(this.character, {
23
+ agentId: this.agentId,
24
+ sessionKeys: this.sessionKeys,
25
+ });
26
+
27
+ // Workstation reference (set by ClawSkinApp)
28
+ this.station = null;
29
+ this._scene = null; // reference to current scene (for POI list)
30
+ this.showNameTag = options.showNameTag !== false;
31
+
32
+ // ── Smooth movement system ──
33
+ this.isWandering = false;
34
+ this._isReturning = false; // walking back to desk (smooth)
35
+ this._wanderTimer = Math.random() * 20000 + 15000; // first wander 15-35s
36
+ this._wanderTarget = null;
37
+ this._wanderSpeed = 0.06 + Math.random() * 0.02; // faster than before
38
+ this._wanderReturnTimer = 0;
39
+ this._wobbleTime = Math.random() * 10000;
40
+ this._breatheTime = Math.random() * 10000;
41
+ this._manualY = false;
42
+ this._poiAction = null; // current POI interaction label
43
+ this._poiStayTimer = 0; // time spent at POI
44
+ this._returnWaypoints = null; // waypoint queue for desk return path
45
+ this._stuckTimer = 0; // detect stuck against obstacles
46
+ this._outsideOwnDesk = false; // true once agent has left own desk hitbox
47
+
48
+ // Reference to other agents (set by ClawSkinApp for social wandering)
49
+ this._otherAgents = null;
50
+ }
51
+
52
+ update(dt) {
53
+ this.character.update(dt);
54
+ this._breatheTime += dt;
55
+
56
+ const state = this.stateMapper.currentState;
57
+ const isWorking = ['typing', 'executing', 'browsing', 'thinking', 'error'].includes(state);
58
+
59
+ if (isWorking) {
60
+ // Working — return to desk if wandering, cancel wander
61
+ if (this.isWandering || this._isReturning) this._snapToDesk();
62
+ this._wanderTimer = 8000 + Math.random() * 12000;
63
+ this._manualY = false;
64
+ } else if (state === 'sleeping') {
65
+ if (this.isWandering || this._isReturning) this._snapToDesk();
66
+ this._manualY = false;
67
+ } else {
68
+ // Idle — maybe wander
69
+ this._wanderTimer -= dt;
70
+
71
+ if (this._isReturning) {
72
+ this._updateMovement(dt);
73
+ } else if (this.isWandering) {
74
+ this._updateWander(dt);
75
+ } else if (this._wanderTimer <= 0 && this.station) {
76
+ this._startWander();
77
+ } else if (this.station && !this._isReturning) {
78
+ // Subtle breathing wobble at desk
79
+ const wobble = Math.sin(this._breatheTime * 0.0015) * 0.8;
80
+ this.character.y = this.station.charY + wobble;
81
+ this._manualY = true;
82
+ }
83
+ }
84
+
85
+ // ── Agent-to-agent repulsion — push apart when overlapping ──
86
+ if (this._otherAgents && (this.isWandering || this._isReturning)) {
87
+ const cs = this.character.scale || 2.5;
88
+ const myW = 32 * cs;
89
+ const myCx = this.character.x + myW / 2;
90
+ const myCy = this.character.y + myW / 2;
91
+ const minDist = 50; // minimum distance between agent centers
92
+
93
+ for (const other of this._otherAgents) {
94
+ if (other === this) continue;
95
+ const ocs = other.character.scale || 2.5;
96
+ const oW = 32 * ocs;
97
+ const oCx = other.character.x + oW / 2;
98
+ const oCy = other.character.y + oW / 2;
99
+
100
+ const dx = myCx - oCx;
101
+ const dy = myCy - oCy;
102
+ const dist = Math.sqrt(dx * dx + dy * dy);
103
+
104
+ if (dist < minDist && dist > 0.1) {
105
+ // Push this agent away from the other
106
+ const pushStrength = (minDist - dist) * 0.03;
107
+ this.character.x += (dx / dist) * pushStrength;
108
+ this.character.y += (dy / dist) * pushStrength;
109
+ this._manualY = true;
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // ── Wandering with POI system ──
116
+
117
+ /** Corridor Y — a safe horizontal lane below all desks where agents walk freely */
118
+ _getCorridorY() {
119
+ const h = this._scene?.canvas?.height || 400;
120
+ const floorY = h * 0.40;
121
+ // deskSurfaceY = floorY + 50, deskH = 42, so desk bottom ≈ floorY + 92
122
+ return Math.round(floorY + 110);
123
+ }
124
+
125
+ /**
126
+ * Plan a waypoint path from current position to (toX, toY).
127
+ * Uses a corridor below the desks for horizontal travel.
128
+ *
129
+ * When starting from behind a desk:
130
+ * 1. Sidestep LEFT or RIGHT out of desk range
131
+ * 2. Drop down to corridor
132
+ * 3. Walk horizontally
133
+ * 4. Rise up to target (sidestep in if target is behind a desk)
134
+ */
135
+ _planPath(toX, toY) {
136
+ const cx = this.character.x;
137
+ const cy = this.character.y;
138
+ const corridorY = this._getCorridorY();
139
+ const waypoints = [];
140
+
141
+ const needsHorizontal = Math.abs(toX - cx) > 30;
142
+ const targetAboveCorridor = toY < corridorY - 10;
143
+ const currentAboveCorridor = cy < corridorY - 10;
144
+
145
+ if (needsHorizontal && (targetAboveCorridor || currentAboveCorridor)) {
146
+ // ── Step 1: Leave current desk area (if behind a desk) ──
147
+ let exitX = cx;
148
+ if (currentAboveCorridor && this.station) {
149
+ const s = this.station;
150
+ // Obstacles extend from deskX-5 to deskX+deskW+5
151
+ // Foot hitbox is 24px wide, centered at character.x + 40 (charPxW/2)
152
+ // So character.x must be < obs.x - 52 (left) or > obs.x+obs.w - 28 (right)
153
+ // Use generous margins to fully clear
154
+ const margin = 80; // fully clear 80px-wide character + desk obstacle padding
155
+ const deskL = s.deskX - margin;
156
+ const deskR = s.deskX + s.deskW + margin;
157
+ // If currently inside desk's horizontal range, sidestep out first
158
+ if (cx > deskL && cx < deskR) {
159
+ // Pick the closer side
160
+ exitX = (cx - deskL < deskR - cx) ? deskL : deskR;
161
+ waypoints.push({ x: exitX, y: cy }); // sidestep out
162
+ }
163
+ waypoints.push({ x: exitX, y: corridorY }); // drop to corridor
164
+ } else if (currentAboveCorridor) {
165
+ waypoints.push({ x: cx, y: corridorY }); // drop to corridor
166
+ }
167
+
168
+ // ── Step 2: Horizontal travel in corridor ──
169
+ // Determine entry X for target — if target is behind a desk, approach from the side
170
+ let entryX = toX;
171
+ if (targetAboveCorridor) {
172
+ // Check if target is inside any desk's horizontal range
173
+ const obstacles = this._scene?.obstacles || [];
174
+ for (const obs of obstacles) {
175
+ if (obs.deskIndex == null) continue;
176
+ const obsL = obs.x - 80;
177
+ const obsR = obs.x + obs.w + 80;
178
+ if (toX > obsL && toX < obsR && toY < obs.y + obs.h) {
179
+ // Target is behind this desk — approach from the closer side
180
+ entryX = (toX - obsL < obsR - toX) ? obsL : obsR;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+
186
+ waypoints.push({ x: entryX, y: corridorY }); // walk horizontally
187
+
188
+ // ── Step 3: Rise up to target ──
189
+ if (targetAboveCorridor) {
190
+ if (Math.abs(entryX - toX) > 5) {
191
+ waypoints.push({ x: entryX, y: toY }); // rise up at the side
192
+ waypoints.push({ x: toX, y: toY }); // slide into position
193
+ } else {
194
+ waypoints.push({ x: toX, y: toY }); // rise directly
195
+ }
196
+ }
197
+ } else {
198
+ // Short horizontal or both below corridor — direct path is fine
199
+ waypoints.push({ x: toX, y: toY });
200
+ }
201
+
202
+ return waypoints;
203
+ }
204
+
205
+ _startWander() {
206
+ if (!this.station) return;
207
+ this.isWandering = true;
208
+ this._poiAction = null;
209
+ this._stuckTimer = 0;
210
+ this.character.setState('walking');
211
+
212
+ const roll = Math.random();
213
+ const poiList = this._scene?.poiList;
214
+ const canvasW = this._scene?.canvas?.width || 640;
215
+ const floorY = (this._scene?.canvas?.height || 400) * 0.40;
216
+
217
+ let targetX, targetY;
218
+
219
+ if (roll < 0.25 && poiList?.length) {
220
+ const poi = poiList[Math.floor(Math.random() * poiList.length)];
221
+ targetX = poi.x; targetY = poi.y;
222
+ this._poiAction = poi.label;
223
+ this._wanderReturnTimer = 5000 + Math.random() * 8000;
224
+ } else if (roll < 0.40 && this._otherAgents?.length) {
225
+ const others = this._otherAgents.filter(a => a !== this && !a.isWandering);
226
+ if (others.length > 0) {
227
+ const other = others[Math.floor(Math.random() * others.length)];
228
+ targetX = other.character.x + 40;
229
+ targetY = other.character.y + 10;
230
+ this._poiAction = 'colleague';
231
+ this._wanderReturnTimer = 4000 + Math.random() * 5000;
232
+ } else {
233
+ this._setRandomTarget(canvasW, floorY);
234
+ return;
235
+ }
236
+ } else {
237
+ this._setRandomTarget(canvasW, floorY);
238
+ return;
239
+ }
240
+
241
+ // Plan waypoint path to target
242
+ this._returnWaypoints = this._planPath(targetX, targetY);
243
+ this._wanderTarget = this._returnWaypoints.shift();
244
+ }
245
+
246
+ _setRandomTarget(canvasW, floorY) {
247
+ const canvasH = this._scene?.canvas?.height || 400;
248
+ const floorH = canvasH - floorY;
249
+ const targetX = 40 + Math.random() * (canvasW - 80);
250
+ const targetY = floorY + 10 + Math.random() * (floorH - 50);
251
+ this._returnWaypoints = this._planPath(targetX, targetY);
252
+ this._wanderTarget = this._returnWaypoints.shift();
253
+ this._wanderReturnTimer = 3000 + Math.random() * 4000;
254
+ }
255
+
256
+ _updateWander(dt) {
257
+ if (!this._wanderTarget) { this._returnToDesk(); return; }
258
+
259
+ const arrived = this._moveToward(dt);
260
+
261
+ if (arrived) {
262
+ // Check if there are more waypoints in the path
263
+ if (this._returnWaypoints && this._returnWaypoints.length > 0) {
264
+ this._wanderTarget = this._returnWaypoints.shift();
265
+ return;
266
+ }
267
+ // Fully arrived at final destination — do POI action
268
+ if (this._poiAction) {
269
+ this._doPOIAction();
270
+ }
271
+ // Stay for remaining time, then return
272
+ this._poiStayTimer += dt;
273
+ if (this._poiStayTimer > 2000) {
274
+ this._returnToDesk();
275
+ this._poiStayTimer = 0;
276
+ }
277
+ return;
278
+ }
279
+
280
+ this._wanderReturnTimer -= dt;
281
+ if (this._wanderReturnTimer <= 0) {
282
+ this._returnToDesk();
283
+ }
284
+ }
285
+
286
+ _doPOIAction() {
287
+ switch (this._poiAction) {
288
+ case 'cooler':
289
+ this.character.setState('idle');
290
+ this.character.showBubble('💧', 'speech', 2000);
291
+ break;
292
+ case 'window':
293
+ this.character.setState('idle');
294
+ this.character.showBubble('🌤️', 'speech', 2000);
295
+ break;
296
+ case 'bookshelf':
297
+ this.character.setState('idle');
298
+ this.character.showBubble('📖', 'speech', 2000);
299
+ break;
300
+ case 'colleague':
301
+ this.character.setState('idle');
302
+ this.character.showBubble('👋', 'speech', 1500);
303
+ break;
304
+ case 'plant':
305
+ this.character.setState('idle');
306
+ this.character.showBubble('🌿', 'speech', 1500);
307
+ break;
308
+ case 'sofa':
309
+ this.character.setState('idle');
310
+ this.character.showBubble('😌', 'speech', 2500);
311
+ break;
312
+ case 'bar':
313
+ this.character.setState('idle');
314
+ this.character.showBubble('🍺', 'speech', 2000);
315
+ break;
316
+ case 'coffee_machine':
317
+ this.character.setState('idle');
318
+ this.character.showBubble('☕', 'speech', 2000);
319
+ break;
320
+ case 'fridge':
321
+ this.character.setState('idle');
322
+ this.character.showBubble('🧃', 'speech', 1500);
323
+ break;
324
+ }
325
+ this._poiAction = null; // only trigger once
326
+ }
327
+
328
+ /** Smooth return to desk — use corridor path planning */
329
+ _returnToDesk() {
330
+ if (!this.station) { this._snapToDesk(); return; }
331
+ this.isWandering = false;
332
+ this._isReturning = true;
333
+ this._poiAction = null;
334
+ this._poiStayTimer = 0;
335
+ this._stuckTimer = 0;
336
+ this.character.setState('walking');
337
+
338
+ // Use the same corridor-based path planning
339
+ this._returnWaypoints = this._planPath(this.station.charX, this.station.charY);
340
+ this._wanderTarget = this._returnWaypoints.shift();
341
+ }
342
+
343
+ /** Move smoothly toward _wanderTarget with collision avoidance. Returns true if arrived. */
344
+ _moveToward(dt) {
345
+ if (!this._wanderTarget) return true;
346
+
347
+ const dx = this._wanderTarget.x - this.character.x;
348
+ const dy = this._wanderTarget.y - this.character.y;
349
+ const dist = Math.sqrt(dx * dx + dy * dy);
350
+
351
+ if (dist <= 3) {
352
+ this.character.x = this._wanderTarget.x;
353
+ this.character.y = this._wanderTarget.y;
354
+ this._manualY = true;
355
+ return true;
356
+ }
357
+
358
+ // Walking wobble
359
+ this._wobbleTime += dt;
360
+ const wobbleY = Math.sin(this._wobbleTime * 0.008) * 1.5;
361
+
362
+ const prevX = this.character.x;
363
+ const prevY = this.character.y;
364
+ const moveX = (dx / dist) * this._wanderSpeed * dt;
365
+ const moveY = (dy / dist) * this._wanderSpeed * dt + wobbleY * 0.05;
366
+ let newX = this.character.x + moveX;
367
+ let newY = this.character.y + moveY;
368
+
369
+ // ── Collision detection against scene obstacles ──
370
+ const obstacles = this._scene?.obstacles;
371
+ if (obstacles) {
372
+ const cs = this.character.scale || 2.5;
373
+ const charPxW = 32 * cs;
374
+ const charPxH = 32 * cs;
375
+ // Foot hitbox — small rect at character's feet (bottom-center)
376
+ const footW = 24, footH = 12;
377
+ const getFootRect = (cx, cy) => ({
378
+ x: cx + charPxW / 2 - footW / 2,
379
+ y: cy + charPxH - footH,
380
+ w: footW,
381
+ h: footH,
382
+ });
383
+
384
+ const ownDeskIdx = this.station?.index;
385
+
386
+ for (const obs of obstacles) {
387
+ // Own desk: 3-state logic
388
+ // 1. Sitting → leaving: _outsideOwnDesk=false, allow moving OUT (skip collision)
389
+ // 2. Wandering: _outsideOwnDesk=true, collide normally (can't walk through)
390
+ // 3. Returning to sit: _isReturning, skip collision for entire return journey
391
+ if (obs.deskIndex != null && obs.deskIndex === ownDeskIdx) {
392
+ if (this._isReturning) {
393
+ continue; // returning to own desk — skip own desk for entire path
394
+ }
395
+ if (!this._outsideOwnDesk) {
396
+ continue; // still leaving own desk — let agent walk out
397
+ }
398
+ // otherwise: wandering, collide with own desk normally
399
+ }
400
+
401
+ const foot = getFootRect(newX, newY);
402
+ const hit = foot.x < obs.x + obs.w && foot.x + foot.w > obs.x
403
+ && foot.y < obs.y + obs.h && foot.y + foot.h > obs.y;
404
+
405
+ if (hit) {
406
+ // Try X-only movement (slide vertically along obstacle)
407
+ const footXOnly = getFootRect(newX, this.character.y);
408
+ const xHit = footXOnly.x < obs.x + obs.w && footXOnly.x + footXOnly.w > obs.x
409
+ && footXOnly.y < obs.y + obs.h && footXOnly.y + footXOnly.h > obs.y;
410
+
411
+ // Try Y-only movement (slide horizontally along obstacle)
412
+ const footYOnly = getFootRect(this.character.x, newY);
413
+ const yHit = footYOnly.x < obs.x + obs.w && footYOnly.x + footYOnly.w > obs.x
414
+ && footYOnly.y < obs.y + obs.h && footYOnly.y + footYOnly.h > obs.y;
415
+
416
+ if (xHit && yHit) {
417
+ newX = this.character.x;
418
+ newY = this.character.y;
419
+ } else if (xHit) {
420
+ newX = this.character.x; // blocked in X, slide Y
421
+ } else if (yHit) {
422
+ newY = this.character.y; // blocked in Y, slide X
423
+ }
424
+ }
425
+ }
426
+
427
+ // ── Track whether agent has left own desk area ──
428
+ if (!this._outsideOwnDesk) {
429
+ const ownObs = obstacles.find(o => o.deskIndex != null && o.deskIndex === ownDeskIdx);
430
+ if (ownObs) {
431
+ const foot = getFootRect(newX, newY);
432
+ const stillInside = foot.x < ownObs.x + ownObs.w && foot.x + foot.w > ownObs.x
433
+ && foot.y < ownObs.y + ownObs.h && foot.y + foot.h > ownObs.y;
434
+ if (!stillInside) {
435
+ this._outsideOwnDesk = true; // left the desk — now it becomes a wall
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ this.character.x = newX;
442
+ this.character.y = newY;
443
+ this._manualY = true;
444
+
445
+ // ── Anti-stuck: if position barely changed, agent is stuck ──
446
+ const moved = Math.abs(newX - prevX) + Math.abs(newY - prevY);
447
+ if (moved < 0.1) {
448
+ this._stuckTimer += dt;
449
+ if (this._stuckTimer > 1500) {
450
+ // Stuck for 1.5s — give up on current target and go back to desk
451
+ this._stuckTimer = 0;
452
+ this._returnToDesk();
453
+ }
454
+ } else {
455
+ this._stuckTimer = 0;
456
+ }
457
+
458
+ return false;
459
+ }
460
+
461
+ /** Update during return-to-desk walk (with waypoints) */
462
+ _updateMovement(dt) {
463
+ const arrived = this._moveToward(dt);
464
+ if (arrived) {
465
+ // Check if there are more waypoints
466
+ if (this._returnWaypoints && this._returnWaypoints.length > 0) {
467
+ this._wanderTarget = this._returnWaypoints.shift();
468
+ return; // keep walking to next waypoint
469
+ }
470
+ this._isReturning = false;
471
+ this._manualY = false;
472
+ this._returnWaypoints = null;
473
+ this._outsideOwnDesk = false; // back at desk — reset for next departure
474
+ this.character.setState('idle');
475
+ this._wanderTimer = 15000 + Math.random() * 25000;
476
+ this._wanderTarget = null;
477
+ }
478
+ }
479
+
480
+ /** Emergency snap — for when work starts mid-wander */
481
+ _snapToDesk() {
482
+ this.isWandering = false;
483
+ this._isReturning = false;
484
+ this._manualY = false;
485
+ this._poiAction = null;
486
+ this._poiStayTimer = 0;
487
+ this._wanderTarget = null;
488
+ this._returnWaypoints = null;
489
+ this.character.setState('idle');
490
+ if (this.station) {
491
+ this.character.x = this.station.charX;
492
+ this.character.y = this.station.charY;
493
+ }
494
+ this._wanderTimer = 15000 + Math.random() * 25000;
495
+ }
496
+
497
+ // ── Rendering ──
498
+
499
+ render(ctx) {
500
+ this.character.render(ctx);
501
+ }
502
+
503
+ /** Render name tag — called in a separate top-layer phase */
504
+ renderNameTag(ctx) {
505
+ if (this.showNameTag) this._renderNameTag(ctx);
506
+ }
507
+
508
+ _renderNameTag(ctx) {
509
+ const cs = this.character.scale;
510
+ const charW = 32 * cs;
511
+ const cx = this.character.x + charW / 2;
512
+ // Position tag well above character head — clear of any bubbles
513
+ const tagBottom = this.character.y - 6;
514
+ const displayName = this.name.length > 10 ? this.name.slice(0, 8) + '..' : this.name;
515
+
516
+ ctx.save();
517
+ ctx.font = '7px "Press Start 2P", monospace';
518
+
519
+ const stateColors = {
520
+ idle: '#00ff88', thinking: '#ffcc00', typing: '#00f0ff',
521
+ executing: '#b44aff', browsing: '#00f0ff', error: '#ff4466',
522
+ sleeping: '#555', walking: '#00ff88', waving: '#00ff88'
523
+ };
524
+ const dotColor = stateColors[this.stateMapper.currentState] || '#888';
525
+
526
+ // Measure for accurate centering
527
+ const textW = ctx.measureText(displayName).width;
528
+ const dotR = 2.5;
529
+ const dotGap = 5;
530
+ const padX = 7;
531
+ const ph = 14;
532
+ const pw = padX + dotR * 2 + dotGap + textW + padX;
533
+
534
+ const pillX = cx - pw / 2;
535
+ const pillY = tagBottom - ph;
536
+
537
+ // Pill background
538
+ ctx.fillStyle = 'rgba(0,0,0,0.7)';
539
+ ctx.beginPath();
540
+ if (ctx.roundRect) ctx.roundRect(pillX, pillY, pw, ph, 3);
541
+ else ctx.rect(pillX, pillY, pw, ph);
542
+ ctx.fill();
543
+
544
+ // State dot
545
+ const dotCx = pillX + padX + dotR;
546
+ const dotCy = pillY + ph / 2;
547
+ ctx.fillStyle = dotColor;
548
+ ctx.shadowColor = dotColor;
549
+ ctx.shadowBlur = 4;
550
+ ctx.beginPath();
551
+ ctx.arc(dotCx, dotCy, dotR, 0, Math.PI * 2);
552
+ ctx.fill();
553
+ ctx.shadowBlur = 0;
554
+
555
+ // Name
556
+ ctx.fillStyle = '#e0e0f0';
557
+ ctx.textAlign = 'left';
558
+ ctx.textBaseline = 'middle';
559
+ ctx.fillText(displayName, dotCx + dotR + dotGap, dotCy + 0.5);
560
+ ctx.restore();
561
+ }
562
+
563
+ hitTest(clickX, clickY) {
564
+ const cs = this.character.scale;
565
+ const cw = 32 * cs;
566
+ const ch = 32 * cs;
567
+ // Generous hit area (include name tag space)
568
+ return (
569
+ clickX >= this.character.x - 10 &&
570
+ clickX <= this.character.x + cw + 10 &&
571
+ clickY >= this.character.y - 20 &&
572
+ clickY <= this.character.y + ch + 10
573
+ );
574
+ }
575
+
576
+ updateConfig(cfg) {
577
+ this.character.updateConfig(cfg);
578
+ const saved = AgentSlot._loadConfigs();
579
+ saved[this.agentId] = this.character.config;
580
+ AgentSlot._saveConfigs(saved);
581
+ }
582
+
583
+ handleEvent(event) { this.stateMapper.handleEvent(event); }
584
+ destroy() { this.stateMapper.destroy(); }
585
+
586
+ static _loadConfigs() {
587
+ try { return JSON.parse(localStorage.getItem('clawskin_agent_configs') || '{}'); }
588
+ catch { return {}; }
589
+ }
590
+ static _saveConfigs(configs) {
591
+ try { localStorage.setItem('clawskin_agent_configs', JSON.stringify(configs)); }
592
+ catch {}
593
+ }
594
+ }
595
+
596
+ if (typeof window !== 'undefined') window.AgentSlot = AgentSlot;