let-them-talk 3.5.1 → 3.6.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.
- package/CHANGELOG.md +57 -0
- package/README.md +62 -21
- package/cli.js +16 -9
- package/conversation-templates/managed-team.json +12 -0
- package/dashboard.html +7389 -5720
- package/dashboard.js +2017 -1766
- package/mods/built-in-accessories.json +122 -0
- package/mods/registry.json +4 -0
- package/office/accessories.js +265 -0
- package/office/agents.js +376 -0
- package/office/animation.js +337 -0
- package/office/appearance.js +56 -0
- package/office/character.js +208 -0
- package/office/constants.js +62 -0
- package/office/environment.js +805 -0
- package/office/face.js +258 -0
- package/office/hair.js +183 -0
- package/office/index.js +337 -0
- package/office/mod-loader.js +257 -0
- package/office/monitors.js +113 -0
- package/office/outfits.js +212 -0
- package/office/scene.js +75 -0
- package/office/spectator-camera.js +177 -0
- package/office/state.js +25 -0
- package/package.json +58 -56
- package/server.js +2704 -2196
- package/templates/managed.json +26 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { S } from './state.js';
|
|
2
|
+
import { updateMonitorScreen, setMonitorDim } from './monitors.js';
|
|
3
|
+
|
|
4
|
+
export function easeInOutQuad(t) {
|
|
5
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function updateAgent(agent, dt, time) {
|
|
9
|
+
var isWalking = agent.target !== null;
|
|
10
|
+
var isSleeping = agent.state === 'sleeping';
|
|
11
|
+
var isDead = agent.state === 'dead';
|
|
12
|
+
|
|
13
|
+
// Death removal
|
|
14
|
+
if (agent.dying) {
|
|
15
|
+
agent.deathOpacity = Math.max(0, agent.deathOpacity - dt * 2);
|
|
16
|
+
var s = Math.max(0.01, agent.deathOpacity);
|
|
17
|
+
agent.parts.group.scale.set(s, s, s);
|
|
18
|
+
if (agent.deathOpacity <= 0) {
|
|
19
|
+
S.scene.remove(agent.parts.group);
|
|
20
|
+
disposeAgent(agent);
|
|
21
|
+
delete S.agents3d[agent.name];
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Walk
|
|
27
|
+
if (agent.target && agent.walkDuration > 0) {
|
|
28
|
+
var speedMult = S.conversationVelocity === 1 ? 1.5 : (S.conversationVelocity === -1 ? 0.8 : 1);
|
|
29
|
+
agent.walkProgress += (dt / agent.walkDuration) * speedMult;
|
|
30
|
+
if (agent.walkProgress >= 1) {
|
|
31
|
+
agent.pos.x = agent.target.x;
|
|
32
|
+
agent.pos.z = agent.target.z;
|
|
33
|
+
var cb = agent.target.cb;
|
|
34
|
+
agent.target = null;
|
|
35
|
+
agent.walkProgress = 0;
|
|
36
|
+
if (agent.walkQueue && agent.walkQueue.length > 0) {
|
|
37
|
+
var next = agent.walkQueue.shift();
|
|
38
|
+
walkTo(agent, next.x, next.z, next.cb);
|
|
39
|
+
} else if (cb) {
|
|
40
|
+
cb();
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
var t = easeInOutQuad(agent.walkProgress);
|
|
44
|
+
agent.pos.x = agent.walkStart.x + (agent.target.x - agent.walkStart.x) * t;
|
|
45
|
+
agent.pos.z = agent.walkStart.z + (agent.target.z - agent.walkStart.z) * t;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
agent.parts.group.position.x = agent.pos.x;
|
|
50
|
+
agent.parts.group.position.z = agent.pos.z;
|
|
51
|
+
|
|
52
|
+
if (isDead && !agent.dying) {
|
|
53
|
+
agent.parts.group.visible = false;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
agent.parts.group.visible = true;
|
|
57
|
+
|
|
58
|
+
// Hand-raise gesture
|
|
59
|
+
if (agent.handRaiseTimer > 0) {
|
|
60
|
+
agent.handRaiseTimer -= dt;
|
|
61
|
+
var raiseT = agent.handRaiseTimer / 0.4;
|
|
62
|
+
agent.parts.rightArm.rotation.z = -Math.sin(raiseT * Math.PI) * 1.2;
|
|
63
|
+
agent.parts.rightArm.rotation.x = -Math.sin(raiseT * Math.PI) * 0.3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Wave gesture (both arms up, friendly wave)
|
|
67
|
+
if (agent.waveTimer > 0) {
|
|
68
|
+
agent.waveTimer -= dt;
|
|
69
|
+
var wT = agent.waveTimer / 0.8;
|
|
70
|
+
agent.parts.rightArm.rotation.z = -Math.sin(wT * Math.PI) * 1.4;
|
|
71
|
+
agent.parts.rightArm.rotation.x = Math.sin(time * 12) * 0.3 * wT;
|
|
72
|
+
agent.parts.leftArm.rotation.z = Math.sin(wT * Math.PI) * 0.3;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Thinking gesture (hand on chin, head tilted)
|
|
76
|
+
if (agent.thinkTimer > 0) {
|
|
77
|
+
agent.thinkTimer -= dt;
|
|
78
|
+
var thT = Math.min(1, agent.thinkTimer / 1.5);
|
|
79
|
+
agent.parts.rightArm.rotation.x = -1.0 * thT;
|
|
80
|
+
agent.parts.rightForearm.rotation.x = -1.5 * thT;
|
|
81
|
+
agent.parts.head.rotation.z = 0.1 * thT;
|
|
82
|
+
agent.parts.head.rotation.x = -0.08 * thT;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Pointing gesture (right arm extended forward)
|
|
86
|
+
if (agent.pointTimer > 0) {
|
|
87
|
+
agent.pointTimer -= dt;
|
|
88
|
+
var ptT = agent.pointTimer / 0.6;
|
|
89
|
+
agent.parts.rightArm.rotation.x = -Math.sin(ptT * Math.PI) * 1.4;
|
|
90
|
+
agent.parts.rightForearm.rotation.x = -0.1 * Math.sin(ptT * Math.PI);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Celebrate gesture (both arms up, bounce)
|
|
94
|
+
if (agent.celebrateTimer > 0) {
|
|
95
|
+
agent.celebrateTimer -= dt;
|
|
96
|
+
var celT = agent.celebrateTimer / 1.5;
|
|
97
|
+
agent.parts.leftArm.rotation.z = Math.sin(celT * Math.PI) * 1.6;
|
|
98
|
+
agent.parts.rightArm.rotation.z = -Math.sin(celT * Math.PI) * 1.6;
|
|
99
|
+
agent.parts.leftArm.rotation.x = -0.2 * celT;
|
|
100
|
+
agent.parts.rightArm.rotation.x = -0.2 * celT;
|
|
101
|
+
agent.parts.group.position.y += Math.abs(Math.sin(time * 10)) * 0.04 * celT;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Stretch gesture (arms wide, body arches back)
|
|
105
|
+
if (agent.stretchTimer > 0) {
|
|
106
|
+
agent.stretchTimer -= dt;
|
|
107
|
+
var stT = agent.stretchTimer / 2;
|
|
108
|
+
var stPhase = Math.sin(stT * Math.PI);
|
|
109
|
+
agent.parts.leftArm.rotation.z = stPhase * 1.3;
|
|
110
|
+
agent.parts.rightArm.rotation.z = -stPhase * 1.3;
|
|
111
|
+
agent.parts.leftArm.rotation.x = -stPhase * 0.5;
|
|
112
|
+
agent.parts.rightArm.rotation.x = -stPhase * 0.5;
|
|
113
|
+
agent.parts.body.rotation.x = -stPhase * 0.15;
|
|
114
|
+
agent.parts.head.rotation.x = -stPhase * 0.2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Idle gesture system — random gestures when sitting and idle
|
|
118
|
+
if (!agent.idleGestureTimer) agent.idleGestureTimer = 5 + Math.random() * 10;
|
|
119
|
+
if (agent.isSitting && agent.state === 'active' && !isWalking && !agent.isListening) {
|
|
120
|
+
agent.idleGestureTimer -= dt;
|
|
121
|
+
if (agent.idleGestureTimer <= 0) {
|
|
122
|
+
agent.idleGestureTimer = 8 + Math.random() * 15;
|
|
123
|
+
var gestures = ['stretch', 'think', 'none', 'none', 'none'];
|
|
124
|
+
var gesture = gestures[Math.floor(Math.random() * gestures.length)];
|
|
125
|
+
if (gesture === 'stretch') agent.stretchTimer = 2;
|
|
126
|
+
else if (gesture === 'think') agent.thinkTimer = 1.5;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Face walk direction
|
|
131
|
+
if (isWalking && agent.target) {
|
|
132
|
+
var dx = agent.target.x - agent.pos.x;
|
|
133
|
+
var dz = agent.target.z - agent.pos.z;
|
|
134
|
+
if (Math.abs(dx) > 0.01 || Math.abs(dz) > 0.01) {
|
|
135
|
+
agent.facingTarget = Math.atan2(dx, dz);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sitting logic
|
|
140
|
+
var atDesk = !agent.location || agent.location === 'desk';
|
|
141
|
+
var shouldSit = !isWalking && agent.registered && !isSleeping && !isDead && agent.handRaiseTimer <= 0 && atDesk;
|
|
142
|
+
if (shouldSit && !agent.isSitting) {
|
|
143
|
+
agent.isSitting = true;
|
|
144
|
+
} else if (!shouldSit && agent.isSitting) {
|
|
145
|
+
agent.isSitting = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var sittingTarget = agent.isSitting ? 1 : 0;
|
|
149
|
+
agent.sittingLerp += (sittingTarget - agent.sittingLerp) * Math.min(1, dt * 5);
|
|
150
|
+
|
|
151
|
+
agent.parts.group.position.y = agent.sittingLerp * 0.06;
|
|
152
|
+
var sitHip = -1.5 * agent.sittingLerp;
|
|
153
|
+
agent.parts.leftLeg.rotation.x = agent.parts.leftLeg.rotation.x * (1 - agent.sittingLerp) + sitHip * agent.sittingLerp;
|
|
154
|
+
agent.parts.rightLeg.rotation.x = agent.parts.rightLeg.rotation.x * (1 - agent.sittingLerp) + sitHip * agent.sittingLerp;
|
|
155
|
+
var sitKnee = 1.5 * agent.sittingLerp;
|
|
156
|
+
agent.parts.leftLowerLeg.rotation.x = sitKnee;
|
|
157
|
+
agent.parts.rightLowerLeg.rotation.x = sitKnee;
|
|
158
|
+
agent.parts.leftForearm.rotation.x = -0.4 * agent.sittingLerp;
|
|
159
|
+
agent.parts.rightForearm.rotation.x = -0.4 * agent.sittingLerp;
|
|
160
|
+
|
|
161
|
+
// Facing at desk
|
|
162
|
+
if (agent.isSitting && agent.sittingLerp > 0.5) {
|
|
163
|
+
agent.facingTarget = Math.PI;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Idle look-around
|
|
167
|
+
if (!isWalking && !agent.isSitting && !isSleeping && agent.registered) {
|
|
168
|
+
agent.facingTarget = Math.sin(time * 0.3 + agent.name.length) * 0.4;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Smooth rotation
|
|
172
|
+
var currentRot = agent.parts.group.rotation.y;
|
|
173
|
+
var diff = agent.facingTarget - currentRot;
|
|
174
|
+
while (diff > Math.PI) diff -= Math.PI * 2;
|
|
175
|
+
while (diff < -Math.PI) diff += Math.PI * 2;
|
|
176
|
+
agent.parts.group.rotation.y += diff * Math.min(1, dt * 4);
|
|
177
|
+
|
|
178
|
+
// Leg/arm swing
|
|
179
|
+
if (isWalking) {
|
|
180
|
+
var swingSpeed = S.conversationVelocity === 1 ? 14 : (S.conversationVelocity === -1 ? 7 : 10);
|
|
181
|
+
var swingAmplitude = S.conversationVelocity === 1 ? 0.7 : (S.conversationVelocity === -1 ? 0.35 : 0.5);
|
|
182
|
+
var swing = Math.sin(time * swingSpeed) * swingAmplitude;
|
|
183
|
+
agent.parts.leftLeg.rotation.x = swing;
|
|
184
|
+
agent.parts.rightLeg.rotation.x = -swing;
|
|
185
|
+
agent.parts.leftLowerLeg.rotation.x = Math.max(0, -swing) * 0.8;
|
|
186
|
+
agent.parts.rightLowerLeg.rotation.x = Math.max(0, swing) * 0.8;
|
|
187
|
+
agent.parts.leftArm.rotation.x = -swing * 0.7;
|
|
188
|
+
agent.parts.rightArm.rotation.x = swing * 0.7;
|
|
189
|
+
agent.parts.leftForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
|
|
190
|
+
agent.parts.rightForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
|
|
191
|
+
} else if (!agent.isSitting) {
|
|
192
|
+
agent.parts.leftLeg.rotation.x *= 0.9;
|
|
193
|
+
agent.parts.rightLeg.rotation.x *= 0.9;
|
|
194
|
+
agent.parts.leftArm.rotation.x *= 0.9;
|
|
195
|
+
agent.parts.rightArm.rotation.x *= 0.9;
|
|
196
|
+
agent.parts.leftLowerLeg.rotation.x *= 0.9;
|
|
197
|
+
agent.parts.rightLowerLeg.rotation.x *= 0.9;
|
|
198
|
+
agent.parts.leftForearm.rotation.x *= 0.9;
|
|
199
|
+
agent.parts.rightForearm.rotation.x *= 0.9;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Idle breathing
|
|
203
|
+
if (!isWalking && !isSleeping) {
|
|
204
|
+
var breatheSpeed = S.conversationVelocity === -1 ? 1.2 : 2;
|
|
205
|
+
var breathe = 1 + Math.sin(time * breatheSpeed) * 0.02;
|
|
206
|
+
agent.parts.body.scale.y = breathe;
|
|
207
|
+
if (!agent.isSitting) {
|
|
208
|
+
agent.parts.head.rotation.z = Math.sin(time * 0.5) * 0.03;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Sleep transition
|
|
213
|
+
var sleepTarget = isSleeping ? 1 : 0;
|
|
214
|
+
agent.sleepTransition += (sleepTarget - agent.sleepTransition) * Math.min(1, dt * (isSleeping ? 1 : 4));
|
|
215
|
+
agent.parts.head.rotation.x = agent.sleepTransition * 0.35;
|
|
216
|
+
agent.parts.body.rotation.x = agent.sleepTransition * 0.18;
|
|
217
|
+
|
|
218
|
+
// Wake-up bounce
|
|
219
|
+
if (agent.prevState === 'sleeping' && agent.state === 'active') {
|
|
220
|
+
if (agent.sleepTransition < 0.05) {
|
|
221
|
+
agent.prevState = null;
|
|
222
|
+
agent.parts.group.position.y = 0.08;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ZZZ floating sprites
|
|
227
|
+
if (isSleeping && agent.sleepTransition > 0.5) {
|
|
228
|
+
if (!agent.zzzActive) {
|
|
229
|
+
agent.zzzActive = true;
|
|
230
|
+
agent.parts.zzzObjects.forEach(function(z) { z.obj.visible = true; });
|
|
231
|
+
}
|
|
232
|
+
agent.parts.zzzObjects.forEach(function(z) {
|
|
233
|
+
var phase = time * 1.5 + z.index * 1.2;
|
|
234
|
+
var cycleT = (phase % 3) / 3;
|
|
235
|
+
var yOff = cycleT * 0.5;
|
|
236
|
+
var xOff = Math.sin(phase * 2) * 0.08;
|
|
237
|
+
z.obj.position.set(0.2 + z.index * 0.1 + xOff, z.baseY + yOff, 0);
|
|
238
|
+
var opacity = cycleT < 0.2 ? cycleT / 0.2 : (cycleT > 0.7 ? (1 - cycleT) / 0.3 : 1);
|
|
239
|
+
z.div.style.opacity = String(Math.max(0, opacity));
|
|
240
|
+
});
|
|
241
|
+
} else if (agent.zzzActive) {
|
|
242
|
+
agent.zzzActive = false;
|
|
243
|
+
agent.parts.zzzObjects.forEach(function(z) {
|
|
244
|
+
z.obj.visible = false;
|
|
245
|
+
z.div.style.opacity = '0';
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Listening head-tilt
|
|
250
|
+
if (agent.isListening && !isWalking && !isSleeping) {
|
|
251
|
+
agent.parts.head.rotation.z = Math.sin(time * 1.5) * 0.08;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Typing dots
|
|
255
|
+
var showTyping = agent.state === 'active' && !agent.isListening && !isWalking && !isSleeping && agent.registered && agent.isSitting;
|
|
256
|
+
agent.parts.typingLabel.visible = showTyping;
|
|
257
|
+
|
|
258
|
+
// Task indicator
|
|
259
|
+
var task = agent.currentTask;
|
|
260
|
+
if (agent.taskCelebration > 0) {
|
|
261
|
+
agent.taskCelebration -= dt;
|
|
262
|
+
agent.parts.taskLabel.visible = true;
|
|
263
|
+
agent.parts.taskDiv.className = 'office3d-task-indicator done';
|
|
264
|
+
agent.parts.taskDiv.textContent = '\u2714 Done!';
|
|
265
|
+
var bounceT = agent.taskCelebration / 2;
|
|
266
|
+
agent.parts.group.position.y += Math.abs(Math.sin(bounceT * Math.PI * 4)) * 0.05;
|
|
267
|
+
if (agent.taskCelebration <= 0) {
|
|
268
|
+
agent.parts.taskLabel.visible = false;
|
|
269
|
+
agent.taskCelebration = 0;
|
|
270
|
+
}
|
|
271
|
+
} else if (task) {
|
|
272
|
+
var taskStatus = task.status || '';
|
|
273
|
+
if (taskStatus === 'in_progress' || taskStatus === 'in-progress') {
|
|
274
|
+
agent.parts.taskLabel.visible = true;
|
|
275
|
+
agent.parts.taskDiv.className = 'office3d-task-indicator working';
|
|
276
|
+
agent.parts.taskDiv.textContent = '\u2699 Working';
|
|
277
|
+
} else if (taskStatus === 'blocked') {
|
|
278
|
+
agent.parts.taskLabel.visible = true;
|
|
279
|
+
agent.parts.taskDiv.className = 'office3d-task-indicator blocked';
|
|
280
|
+
agent.parts.taskDiv.textContent = '\u2757 Blocked';
|
|
281
|
+
} else {
|
|
282
|
+
agent.parts.taskLabel.visible = false;
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
agent.parts.taskLabel.visible = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Monitor screen content
|
|
289
|
+
if (agent.registered && agent.isSitting && agent.state === 'active') {
|
|
290
|
+
agent.monitorTimer += dt;
|
|
291
|
+
if (agent.monitorTimer >= 0.5) {
|
|
292
|
+
agent.monitorTimer = 0;
|
|
293
|
+
updateMonitorScreen(agent.deskIdx, agent.name, time);
|
|
294
|
+
}
|
|
295
|
+
} else if (agent.registered && isSleeping) {
|
|
296
|
+
setMonitorDim(agent.deskIdx);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Bubble timer
|
|
300
|
+
if (agent.bubbleTimer > 0) {
|
|
301
|
+
agent.bubbleTimer -= dt;
|
|
302
|
+
if (agent.bubbleTimer <= 1) {
|
|
303
|
+
agent.parts.bubbleDiv.style.opacity = String(Math.max(0, agent.bubbleTimer));
|
|
304
|
+
}
|
|
305
|
+
if (agent.bubbleTimer <= 0) {
|
|
306
|
+
agent.parts.bubbleDiv.style.display = 'none';
|
|
307
|
+
agent.bubbleTimer = 0;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function walkTo(agent, tx, tz, callback) {
|
|
313
|
+
var dx = tx - agent.pos.x;
|
|
314
|
+
var dz = tz - agent.pos.z;
|
|
315
|
+
var dist = Math.sqrt(dx * dx + dz * dz);
|
|
316
|
+
agent.walkStart = { x: agent.pos.x, z: agent.pos.z };
|
|
317
|
+
agent.target = { x: tx, z: tz, cb: callback || null };
|
|
318
|
+
agent.walkProgress = 0;
|
|
319
|
+
agent.walkDuration = Math.max(dist * 0.4, 0.3);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function disposeAgent(agent) {
|
|
323
|
+
agent.parts.group.traverse(function(child) {
|
|
324
|
+
if (child.geometry) child.geometry.dispose();
|
|
325
|
+
if (child.material) {
|
|
326
|
+
if (child.material.map) child.material.map.dispose();
|
|
327
|
+
child.material.dispose();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
if (agent.parts.labelDiv.parentElement) agent.parts.labelDiv.remove();
|
|
331
|
+
if (agent.parts.bubbleDiv.parentElement) agent.parts.bubbleDiv.remove();
|
|
332
|
+
if (agent.parts.taskDiv && agent.parts.taskDiv.parentElement) agent.parts.taskDiv.remove();
|
|
333
|
+
if (agent.parts.typingDiv && agent.parts.typingDiv.parentElement) agent.parts.typingDiv.remove();
|
|
334
|
+
if (agent.parts.zzzObjects) {
|
|
335
|
+
agent.parts.zzzObjects.forEach(function(z) { if (z.div.parentElement) z.div.remove(); });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { DEFAULT_APPEARANCE, AGENT_PALETTES } from './constants.js';
|
|
3
|
+
|
|
4
|
+
// Deterministic appearance resolution from agent name + stored appearance
|
|
5
|
+
export function resolveAppearance(name, appearance) {
|
|
6
|
+
var app = appearance || {};
|
|
7
|
+
var h = 0;
|
|
8
|
+
for (var i = 0; i < name.length; i++) h = ((h << 5) - h + name.charCodeAt(i)) | 0;
|
|
9
|
+
h = Math.abs(h);
|
|
10
|
+
var palette = AGENT_PALETTES[h % AGENT_PALETTES.length];
|
|
11
|
+
|
|
12
|
+
// Accessory pools — deterministic based on name hash
|
|
13
|
+
var glassesPool = [null, null, null, 'round', 'square', 'sunglasses', null, null];
|
|
14
|
+
var headwearPool = [null, null, null, null, 'headphones', 'beanie', 'cap', null, null, 'headband'];
|
|
15
|
+
var neckwearPool = [null, null, null, 'tie', 'bowtie', 'lanyard', null, null];
|
|
16
|
+
var accColorPool = ['#555555', '#8B4513', '#333333', '#c0392b', '#1a5276', '#7d3c98', '#2e4053'];
|
|
17
|
+
|
|
18
|
+
// Outfit and body type pools
|
|
19
|
+
var outfitPool = [null, null, null, null, 'hoodie', 'suit', 'jacket', 'vest', null, null, 'labcoat', null];
|
|
20
|
+
var bodyTypePool = ['default', 'default', 'default', 'stocky', 'slim', 'default', 'default'];
|
|
21
|
+
var eyePool = ['dots', 'dots', 'anime', 'confident', 'happy', 'dots', 'wink', 'dots'];
|
|
22
|
+
var mouthPool = ['smile', 'smile', 'neutral', 'grin', 'smile', 'smirk', 'smile'];
|
|
23
|
+
|
|
24
|
+
var resolved = {
|
|
25
|
+
head_color: app.head_color || DEFAULT_APPEARANCE.head_color,
|
|
26
|
+
hair_style: app.hair_style || ['short', 'spiky', 'bob', 'ponytail', 'curly', 'afro', 'bun', 'braids', 'mohawk', 'wavy', 'long'][h % 11],
|
|
27
|
+
hair_color: app.hair_color || palette.hair_color,
|
|
28
|
+
eye_style: app.eye_style || eyePool[h % eyePool.length],
|
|
29
|
+
mouth_style: app.mouth_style || mouthPool[(h >> 2) % mouthPool.length],
|
|
30
|
+
shirt_color: app.shirt_color || palette.shirt_color,
|
|
31
|
+
pants_color: app.pants_color || palette.pants_color,
|
|
32
|
+
shoe_color: app.shoe_color || DEFAULT_APPEARANCE.shoe_color,
|
|
33
|
+
glasses: app.glasses !== undefined ? app.glasses : glassesPool[(h >> 3) % glassesPool.length],
|
|
34
|
+
glasses_color: app.glasses_color || accColorPool[(h >> 4) % accColorPool.length],
|
|
35
|
+
headwear: app.headwear !== undefined ? app.headwear : headwearPool[(h >> 5) % headwearPool.length],
|
|
36
|
+
headwear_color: app.headwear_color || accColorPool[(h >> 6) % accColorPool.length],
|
|
37
|
+
neckwear: app.neckwear !== undefined ? app.neckwear : neckwearPool[(h >> 7) % neckwearPool.length],
|
|
38
|
+
neckwear_color: app.neckwear_color || accColorPool[(h >> 8) % accColorPool.length],
|
|
39
|
+
outfit: app.outfit !== undefined ? app.outfit : outfitPool[(h >> 9) % outfitPool.length],
|
|
40
|
+
body_type: app.body_type || bodyTypePool[(h >> 10) % bodyTypePool.length],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Add Three.js hex values
|
|
44
|
+
resolved.head_hex = new THREE.Color(resolved.head_color).getHex();
|
|
45
|
+
resolved.shirt_hex = new THREE.Color(resolved.shirt_color).getHex();
|
|
46
|
+
resolved.pants_hex = new THREE.Color(resolved.pants_color).getHex();
|
|
47
|
+
resolved.shoe_hex = new THREE.Color(resolved.shoe_color).getHex();
|
|
48
|
+
resolved.hair_hex = new THREE.Color(resolved.hair_color).getHex();
|
|
49
|
+
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Expose for legacy callers (profile editor, 2D compat)
|
|
54
|
+
window.officeGetAppearance = function(agent) {
|
|
55
|
+
return resolveAppearance(agent.displayName || 'agent', agent.appearance || {});
|
|
56
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
|
|
3
|
+
import { resolveAppearance } from './appearance.js';
|
|
4
|
+
import { buildHair } from './hair.js';
|
|
5
|
+
import { buildFaceSprite } from './face.js';
|
|
6
|
+
import { buildGlasses, buildHeadwear, buildNeckwear } from './accessories.js';
|
|
7
|
+
import { buildOutfit } from './outfits.js';
|
|
8
|
+
|
|
9
|
+
// Body type scale multipliers (all keep the chibi oversized head)
|
|
10
|
+
var BODY_TYPES = {
|
|
11
|
+
default: { torsoW: 1, torsoH: 1, torsoD: 1, legW: 1, legH: 1, armW: 1, armH: 1, legSpread: 1, armSpread: 1, headY: 0 },
|
|
12
|
+
stocky: { torsoW: 1.3, torsoH: 0.95, torsoD: 1.25, legW: 1.25, legH: 0.9, armW: 1.2, armH: 0.95, legSpread: 1.2, armSpread: 1.15, headY: -0.02 },
|
|
13
|
+
slim: { torsoW: 0.82, torsoH: 1.1, torsoD: 0.85, legW: 0.8, legH: 1.08, armW: 0.8, armH: 1.05, legSpread: 0.85, armSpread: 0.9, headY: 0.04 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createCharacter(name, appearance) {
|
|
17
|
+
var a = resolveAppearance(name, appearance);
|
|
18
|
+
var bt = BODY_TYPES[a.body_type] || BODY_TYPES.default;
|
|
19
|
+
var group = new THREE.Group();
|
|
20
|
+
group.userData.agentName = name;
|
|
21
|
+
|
|
22
|
+
// Shadow
|
|
23
|
+
var shadowGeo = new THREE.PlaneGeometry(0.5, 0.5);
|
|
24
|
+
var shadowMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.25 });
|
|
25
|
+
var shadow = new THREE.Mesh(shadowGeo, shadowMat);
|
|
26
|
+
shadow.rotation.x = -Math.PI / 2;
|
|
27
|
+
shadow.position.y = 0.01;
|
|
28
|
+
shadow.userData.isShadow = true;
|
|
29
|
+
group.add(shadow);
|
|
30
|
+
|
|
31
|
+
// Materials
|
|
32
|
+
var bodyMat = new THREE.MeshStandardMaterial({ color: a.shirt_hex, roughness: 0.7 });
|
|
33
|
+
var legMat = new THREE.MeshStandardMaterial({ color: a.pants_hex, roughness: 0.7 });
|
|
34
|
+
var shoeMat = new THREE.MeshStandardMaterial({ color: a.shoe_hex, roughness: 0.6 });
|
|
35
|
+
var armMat = new THREE.MeshStandardMaterial({ color: a.shirt_hex, roughness: 0.7 });
|
|
36
|
+
var handMat = new THREE.MeshStandardMaterial({ color: a.head_hex, roughness: 0.7 });
|
|
37
|
+
|
|
38
|
+
// Torso (scaled by body type)
|
|
39
|
+
var body = new THREE.Mesh(new THREE.BoxGeometry(0.3 * bt.torsoW, 0.32 * bt.torsoH, 0.18 * bt.torsoD), bodyMat);
|
|
40
|
+
body.position.y = 0.58; body.castShadow = true;
|
|
41
|
+
group.add(body);
|
|
42
|
+
|
|
43
|
+
// Left Leg
|
|
44
|
+
var legXOffset = 0.08 * bt.legSpread;
|
|
45
|
+
var leftLeg = new THREE.Group();
|
|
46
|
+
leftLeg.position.set(-legXOffset, 0.42, 0);
|
|
47
|
+
group.add(leftLeg);
|
|
48
|
+
var leftUpperLeg = new THREE.Mesh(new THREE.BoxGeometry(0.1 * bt.legW, 0.18 * bt.legH, 0.1 * bt.legW), legMat);
|
|
49
|
+
leftUpperLeg.position.y = -0.09 * bt.legH; leftUpperLeg.castShadow = true;
|
|
50
|
+
leftLeg.add(leftUpperLeg);
|
|
51
|
+
var leftLowerLeg = new THREE.Group();
|
|
52
|
+
leftLowerLeg.position.set(0, -0.18 * bt.legH, 0);
|
|
53
|
+
leftLeg.add(leftLowerLeg);
|
|
54
|
+
var leftShin = new THREE.Mesh(new THREE.BoxGeometry(0.09 * bt.legW, 0.16 * bt.legH, 0.09 * bt.legW), legMat);
|
|
55
|
+
leftShin.position.y = -0.08 * bt.legH; leftShin.castShadow = true;
|
|
56
|
+
leftLowerLeg.add(leftShin);
|
|
57
|
+
var leftShoe = new THREE.Mesh(new THREE.BoxGeometry(0.1 * bt.legW, 0.05, 0.14), shoeMat);
|
|
58
|
+
leftShoe.position.set(0, -0.18 * bt.legH, 0.02); leftShoe.castShadow = true;
|
|
59
|
+
leftLowerLeg.add(leftShoe);
|
|
60
|
+
|
|
61
|
+
// Right Leg
|
|
62
|
+
var rightLeg = new THREE.Group();
|
|
63
|
+
rightLeg.position.set(legXOffset, 0.42, 0);
|
|
64
|
+
group.add(rightLeg);
|
|
65
|
+
var rightUpperLeg = new THREE.Mesh(new THREE.BoxGeometry(0.1 * bt.legW, 0.18 * bt.legH, 0.1 * bt.legW), legMat);
|
|
66
|
+
rightUpperLeg.position.y = -0.09 * bt.legH; rightUpperLeg.castShadow = true;
|
|
67
|
+
rightLeg.add(rightUpperLeg);
|
|
68
|
+
var rightLowerLeg = new THREE.Group();
|
|
69
|
+
rightLowerLeg.position.set(0, -0.18 * bt.legH, 0);
|
|
70
|
+
rightLeg.add(rightLowerLeg);
|
|
71
|
+
var rightShin = new THREE.Mesh(new THREE.BoxGeometry(0.09 * bt.legW, 0.16 * bt.legH, 0.09 * bt.legW), legMat);
|
|
72
|
+
rightShin.position.y = -0.08 * bt.legH; rightShin.castShadow = true;
|
|
73
|
+
rightLowerLeg.add(rightShin);
|
|
74
|
+
var rightShoe = new THREE.Mesh(new THREE.BoxGeometry(0.1 * bt.legW, 0.05, 0.14), shoeMat);
|
|
75
|
+
rightShoe.position.set(0, -0.18 * bt.legH, 0.02); rightShoe.castShadow = true;
|
|
76
|
+
rightLowerLeg.add(rightShoe);
|
|
77
|
+
|
|
78
|
+
// Left Arm
|
|
79
|
+
var armXOffset = 0.21 * bt.armSpread;
|
|
80
|
+
var leftArm = new THREE.Group();
|
|
81
|
+
leftArm.position.set(-armXOffset, 0.7, 0);
|
|
82
|
+
group.add(leftArm);
|
|
83
|
+
var leftUpperArm = new THREE.Mesh(new THREE.BoxGeometry(0.08 * bt.armW, 0.16 * bt.armH, 0.08 * bt.armW), armMat);
|
|
84
|
+
leftUpperArm.position.y = -0.08 * bt.armH; leftUpperArm.castShadow = true;
|
|
85
|
+
leftArm.add(leftUpperArm);
|
|
86
|
+
var leftForearm = new THREE.Group();
|
|
87
|
+
leftForearm.position.set(0, -0.16 * bt.armH, 0);
|
|
88
|
+
leftArm.add(leftForearm);
|
|
89
|
+
var leftForearmMesh = new THREE.Mesh(new THREE.BoxGeometry(0.07 * bt.armW, 0.14 * bt.armH, 0.07 * bt.armW), armMat);
|
|
90
|
+
leftForearmMesh.position.y = -0.07 * bt.armH; leftForearmMesh.castShadow = true;
|
|
91
|
+
leftForearm.add(leftForearmMesh);
|
|
92
|
+
var leftHand = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 6), handMat);
|
|
93
|
+
leftHand.position.y = -0.16 * bt.armH;
|
|
94
|
+
leftForearm.add(leftHand);
|
|
95
|
+
|
|
96
|
+
// Right Arm
|
|
97
|
+
var rightArm = new THREE.Group();
|
|
98
|
+
rightArm.position.set(armXOffset, 0.7, 0);
|
|
99
|
+
group.add(rightArm);
|
|
100
|
+
var rightUpperArm = new THREE.Mesh(new THREE.BoxGeometry(0.08 * bt.armW, 0.16 * bt.armH, 0.08 * bt.armW), armMat);
|
|
101
|
+
rightUpperArm.position.y = -0.08 * bt.armH; rightUpperArm.castShadow = true;
|
|
102
|
+
rightArm.add(rightUpperArm);
|
|
103
|
+
var rightForearm = new THREE.Group();
|
|
104
|
+
rightForearm.position.set(0, -0.16 * bt.armH, 0);
|
|
105
|
+
rightArm.add(rightForearm);
|
|
106
|
+
var rightForearmMesh = new THREE.Mesh(new THREE.BoxGeometry(0.07 * bt.armW, 0.14 * bt.armH, 0.07 * bt.armW), armMat);
|
|
107
|
+
rightForearmMesh.position.y = -0.07 * bt.armH; rightForearmMesh.castShadow = true;
|
|
108
|
+
rightForearm.add(rightForearmMesh);
|
|
109
|
+
var rightHand = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 6), handMat);
|
|
110
|
+
rightHand.position.y = -0.16 * bt.armH;
|
|
111
|
+
rightForearm.add(rightHand);
|
|
112
|
+
|
|
113
|
+
// Head (always same chibi size regardless of body type)
|
|
114
|
+
var headGeo = new THREE.SphereGeometry(0.25, 20, 16);
|
|
115
|
+
var headMat = new THREE.MeshStandardMaterial({ color: a.head_hex, roughness: 0.6 });
|
|
116
|
+
var head = new THREE.Mesh(headGeo, headMat);
|
|
117
|
+
head.position.y = 1.05 + bt.headY; head.castShadow = true;
|
|
118
|
+
group.add(head);
|
|
119
|
+
|
|
120
|
+
// Hair
|
|
121
|
+
var hairGroup = buildHair(a.hair_style, a.hair_hex);
|
|
122
|
+
hairGroup.position.y = 1.05 + bt.headY;
|
|
123
|
+
group.add(hairGroup);
|
|
124
|
+
|
|
125
|
+
// Face
|
|
126
|
+
var faceSprite = buildFaceSprite(a.eye_style, a.mouth_style, false);
|
|
127
|
+
faceSprite.position.set(0, 0, 0.251);
|
|
128
|
+
head.add(faceSprite);
|
|
129
|
+
|
|
130
|
+
// Outfit (layered on top of body)
|
|
131
|
+
var outfitGroup = null;
|
|
132
|
+
if (a.outfit) {
|
|
133
|
+
outfitGroup = buildOutfit(a.outfit, { shirt_color: a.shirt_color, pants_color: a.pants_color }, group);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Accessories
|
|
137
|
+
if (a.glasses) buildGlasses(a.glasses, a.glasses_color || '#555555', head);
|
|
138
|
+
if (a.headwear) buildHeadwear(a.headwear, a.headwear_color || '#333333', head);
|
|
139
|
+
if (a.neckwear && !a.outfit) buildNeckwear(a.neckwear, a.neckwear_color || '#c0392b', group);
|
|
140
|
+
|
|
141
|
+
// Name label
|
|
142
|
+
var labelDiv = document.createElement('div');
|
|
143
|
+
labelDiv.className = 'office3d-label';
|
|
144
|
+
labelDiv.innerHTML = '<span class="office3d-label-name"></span><span class="office3d-label-dot"></span>';
|
|
145
|
+
var label = new CSS2DObject(labelDiv);
|
|
146
|
+
label.position.set(0, 1.55, 0);
|
|
147
|
+
group.add(label);
|
|
148
|
+
|
|
149
|
+
// Bubble
|
|
150
|
+
var bubbleDiv = document.createElement('div');
|
|
151
|
+
bubbleDiv.className = 'office3d-bubble';
|
|
152
|
+
bubbleDiv.style.display = 'none';
|
|
153
|
+
var bubble = new CSS2DObject(bubbleDiv);
|
|
154
|
+
bubble.position.set(0, 1.8, 0);
|
|
155
|
+
group.add(bubble);
|
|
156
|
+
|
|
157
|
+
// ZZZ sprites
|
|
158
|
+
var zzzObjects = [];
|
|
159
|
+
['z', 'Z', 'Z'].forEach(function(letter, i) {
|
|
160
|
+
var zDiv = document.createElement('div');
|
|
161
|
+
zDiv.className = 'office3d-zzz';
|
|
162
|
+
zDiv.textContent = letter;
|
|
163
|
+
zDiv.style.fontSize = (10 + i * 4) + 'px';
|
|
164
|
+
var zObj = new CSS2DObject(zDiv);
|
|
165
|
+
zObj.position.set(0.15 + i * 0.1, 1.4 + i * 0.15, 0);
|
|
166
|
+
zObj.visible = false;
|
|
167
|
+
group.add(zObj);
|
|
168
|
+
zzzObjects.push({ obj: zObj, div: zDiv, baseY: 1.4 + i * 0.15, index: i });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Task indicator
|
|
172
|
+
var taskDiv = document.createElement('div');
|
|
173
|
+
taskDiv.className = 'office3d-task-indicator working';
|
|
174
|
+
var taskLabel = new CSS2DObject(taskDiv);
|
|
175
|
+
taskLabel.position.set(0, 1.7, 0);
|
|
176
|
+
taskLabel.visible = false;
|
|
177
|
+
group.add(taskLabel);
|
|
178
|
+
|
|
179
|
+
// Typing dots
|
|
180
|
+
var typingDiv = document.createElement('div');
|
|
181
|
+
typingDiv.className = 'office3d-typing';
|
|
182
|
+
typingDiv.innerHTML = '<span class="office3d-typing-dot"></span><span class="office3d-typing-dot"></span><span class="office3d-typing-dot"></span>';
|
|
183
|
+
var typingLabel = new CSS2DObject(typingDiv);
|
|
184
|
+
typingLabel.position.set(0, 1.65, 0);
|
|
185
|
+
typingLabel.visible = false;
|
|
186
|
+
group.add(typingLabel);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
group: group,
|
|
190
|
+
body: body, head: head,
|
|
191
|
+
leftLeg: leftLeg, rightLeg: rightLeg,
|
|
192
|
+
leftLowerLeg: leftLowerLeg, rightLowerLeg: rightLowerLeg,
|
|
193
|
+
leftArm: leftArm, rightArm: rightArm,
|
|
194
|
+
leftForearm: leftForearm, rightForearm: rightForearm,
|
|
195
|
+
leftHand: leftHand, rightHand: rightHand,
|
|
196
|
+
leftShoe: leftShoe, rightShoe: rightShoe,
|
|
197
|
+
faceSprite: faceSprite, hairGroup: hairGroup,
|
|
198
|
+
outfitGroup: outfitGroup,
|
|
199
|
+
label: label, labelDiv: labelDiv,
|
|
200
|
+
bubble: bubble, bubbleDiv: bubbleDiv,
|
|
201
|
+
shadow: shadow,
|
|
202
|
+
bodyMat: bodyMat, legMat: legMat, headMat: headMat,
|
|
203
|
+
armMat: armMat, handMat: handMat, shoeMat: shoeMat,
|
|
204
|
+
zzzObjects: zzzObjects,
|
|
205
|
+
taskDiv: taskDiv, taskLabel: taskLabel,
|
|
206
|
+
typingDiv: typingDiv, typingLabel: typingLabel
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const FLOOR_W = 28;
|
|
2
|
+
export const FLOOR_D = 16;
|
|
3
|
+
|
|
4
|
+
export const DESK_POSITIONS = [
|
|
5
|
+
{ x: -4.5, z: 1.5 }, { x: -1.5, z: 1.5 }, { x: 1.5, z: 1.5 }, { x: 4.5, z: 1.5 },
|
|
6
|
+
{ x: -4.5, z: -1 }, { x: -1.5, z: -1 }, { x: 1.5, z: -1 }, { x: 4.5, z: -1 },
|
|
7
|
+
{ x: -4.5, z: -3.5 },{ x: -1.5, z: -3.5 },{ x: 1.5, z: -3.5 },{ x: 4.5, z: -3.5 },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const RECEPTION_POS = { x: 0, z: 6 };
|
|
11
|
+
export const SPAWN_POS = { x: 0, z: 7.5 };
|
|
12
|
+
|
|
13
|
+
export const ENVS = {
|
|
14
|
+
modern: {
|
|
15
|
+
floor1: 0x2a2d35, floor2: 0x323640,
|
|
16
|
+
wall: 0x1e2028,
|
|
17
|
+
desk: 0x5a6a80, deskLegs: 0x4a5568,
|
|
18
|
+
chair: 0x374151, chairSeat: 0x2d3748,
|
|
19
|
+
accent: 0x58a6ff,
|
|
20
|
+
},
|
|
21
|
+
startup: {
|
|
22
|
+
floor1: 0x2c2520, floor2: 0x362f28,
|
|
23
|
+
wall: 0x1a1512,
|
|
24
|
+
desk: 0xA67B1D, deskLegs: 0x8B6914,
|
|
25
|
+
chair: 0x5a3e28, chairSeat: 0x3d2b1f,
|
|
26
|
+
accent: 0xf97316,
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const HEAD_R = 0.25;
|
|
31
|
+
|
|
32
|
+
// Dressing room — right wing
|
|
33
|
+
export const DRESSING_ROOM_POS = { x: 10, z: -1.5 }; // platform center
|
|
34
|
+
export const DRESSING_ROOM_ENTRANCE = { x: 7.5, z: -1.5 }; // walk target
|
|
35
|
+
|
|
36
|
+
// Rest area — right wing, further back
|
|
37
|
+
export const REST_AREA_POS = { x: 10, z: -5.5 }; // beanbag center
|
|
38
|
+
export const REST_AREA_ENTRANCE = { x: 7.5, z: -5.5 }; // walk target
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_APPEARANCE = {
|
|
41
|
+
head_color: '#FFD5B8',
|
|
42
|
+
hair_style: 'short',
|
|
43
|
+
hair_color: '#4A3728',
|
|
44
|
+
eye_style: 'dots',
|
|
45
|
+
mouth_style: 'smile',
|
|
46
|
+
shirt_color: '#58a6ff',
|
|
47
|
+
pants_color: '#2d3748',
|
|
48
|
+
shoe_color: '#1a1a2e',
|
|
49
|
+
outfit: null,
|
|
50
|
+
body_type: 'default',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const AGENT_PALETTES = [
|
|
54
|
+
{ shirt_color: '#58a6ff', pants_color: '#2d3748', hair_color: '#4A3728' },
|
|
55
|
+
{ shirt_color: '#f97316', pants_color: '#3d2b1f', hair_color: '#1a1a1a' },
|
|
56
|
+
{ shirt_color: '#a855f7', pants_color: '#1e1b2e', hair_color: '#8B4513' },
|
|
57
|
+
{ shirt_color: '#22c55e', pants_color: '#1a2e1a', hair_color: '#D4A574' },
|
|
58
|
+
{ shirt_color: '#ef4444', pants_color: '#2e1a1a', hair_color: '#333' },
|
|
59
|
+
{ shirt_color: '#eab308', pants_color: '#2e2a1a', hair_color: '#8B0000' },
|
|
60
|
+
{ shirt_color: '#06b6d4', pants_color: '#1a2e2e', hair_color: '#F5DEB3' },
|
|
61
|
+
{ shirt_color: '#ec4899', pants_color: '#2e1a28', hair_color: '#FFD700' },
|
|
62
|
+
];
|