@agent-os-lab/agent-game-sdk 0.1.1

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.
Files changed (68) hide show
  1. package/README.md +99 -0
  2. package/package.json +38 -0
  3. package/src/core/agent-game-store.ts +110 -0
  4. package/src/core/agent-service-event-adapter.ts +20 -0
  5. package/src/core/assets.ts +119 -0
  6. package/src/core/commands.ts +42 -0
  7. package/src/core/errors.ts +19 -0
  8. package/src/core/event-adapter.ts +40 -0
  9. package/src/core/index.ts +23 -0
  10. package/src/core/life-presets.ts +54 -0
  11. package/src/core/movement.ts +50 -0
  12. package/src/core/office-building-layout.ts +376 -0
  13. package/src/core/office-layout.ts +152 -0
  14. package/src/core/pixel-character-avatar.ts +87 -0
  15. package/src/core/pixel-character.ts +684 -0
  16. package/src/core/realtime-events.ts +44 -0
  17. package/src/core/realtime-transport.ts +39 -0
  18. package/src/core/reducer.ts +105 -0
  19. package/src/core/scene.ts +144 -0
  20. package/src/core/schedule.ts +20 -0
  21. package/src/core/sequence.ts +48 -0
  22. package/src/core/state.ts +26 -0
  23. package/src/core/svg-pixel-avatar.ts +372 -0
  24. package/src/core/town-office-assets.ts +109 -0
  25. package/src/core/town-office-room-presets.ts +455 -0
  26. package/src/core/town-office-seat-layout.ts +238 -0
  27. package/src/graph.ts +112 -0
  28. package/src/index.ts +2 -0
  29. package/src/office/core/projection.ts +89 -0
  30. package/src/office/core/source.ts +46 -0
  31. package/src/office/core/types.ts +110 -0
  32. package/src/office/index.ts +4 -0
  33. package/src/office/mount.ts +104 -0
  34. package/src/office/react/AgentGameOfficeView.ts +58 -0
  35. package/src/office/react/index.ts +1 -0
  36. package/src/office/renderers/three/agent-activity-effects.ts +161 -0
  37. package/src/office/renderers/three/agent-animation.ts +205 -0
  38. package/src/office/renderers/three/agent-body-instancing.ts +119 -0
  39. package/src/office/renderers/three/agent-label.ts +82 -0
  40. package/src/office/renderers/three/agent-layout.ts +72 -0
  41. package/src/office/renderers/three/agent-mesh.ts +145 -0
  42. package/src/office/renderers/three/mount.ts +253 -0
  43. package/src/office/renderers/three/scene.ts +790 -0
  44. package/src/phaser/agent-game-scene.ts +87 -0
  45. package/src/phaser/anchor-debug.ts +22 -0
  46. package/src/phaser/avatar-registry.ts +46 -0
  47. package/src/phaser/camera-controls.ts +419 -0
  48. package/src/phaser/camera-model.ts +81 -0
  49. package/src/phaser/create-agent-game.ts +242 -0
  50. package/src/phaser/debug-overlay.ts +21 -0
  51. package/src/phaser/index.ts +13 -0
  52. package/src/phaser/movement-tween.ts +59 -0
  53. package/src/phaser/office-background.ts +48 -0
  54. package/src/phaser/office-building-renderer.ts +87 -0
  55. package/src/phaser/office-layout-renderer.ts +58 -0
  56. package/src/phaser/render-layers.ts +30 -0
  57. package/src/phaser/scene-reconciler.ts +614 -0
  58. package/src/phaser/scene-renderer.ts +138 -0
  59. package/src/phaser/text-style.ts +8 -0
  60. package/src/phaser/town-office-business-props.ts +256 -0
  61. package/src/phaser/town-office-environment.ts +89 -0
  62. package/src/phaser/town-office-furniture.ts +182 -0
  63. package/src/phaser/town-office-primitives.ts +53 -0
  64. package/src/phaser/town-office-renderer.ts +429 -0
  65. package/src/phaser/types.ts +67 -0
  66. package/src/phaser/viewport.ts +88 -0
  67. package/src/runtime-client.ts +435 -0
  68. package/src/types.ts +80 -0
@@ -0,0 +1,205 @@
1
+ import * as THREE from "three";
2
+
3
+ import type { AgentGameOfficeAgent } from "../../core/types";
4
+ import { updateAgentActivityEffects } from "./agent-activity-effects";
5
+ import type { AgentMeshParts } from "./agent-mesh";
6
+
7
+ export type AgentAnimationContext = {
8
+ mesh: AgentMeshParts;
9
+ agent: AgentGameOfficeAgent;
10
+ target: THREE.Vector3;
11
+ facingTarget?: THREE.Vector3;
12
+ elapsedSeconds: number;
13
+ deltaSeconds: number;
14
+ speedUnitsPerSecond?: number;
15
+ };
16
+
17
+ export type AgentAnimationMode =
18
+ | "walking"
19
+ | "working"
20
+ | "thinking"
21
+ | "meeting"
22
+ | "resting"
23
+ | "idle"
24
+ | "entertaining"
25
+ | "offline";
26
+
27
+ export function resolveAgentAnimationMode(
28
+ agent: AgentGameOfficeAgent,
29
+ moving: boolean,
30
+ ): AgentAnimationMode {
31
+ if (agent.sceneState === "offline") {
32
+ return "offline";
33
+ }
34
+ if (moving) {
35
+ return "walking";
36
+ }
37
+ return agent.sceneState;
38
+ }
39
+
40
+ export function isSeatedAnimationMode(mode: AgentAnimationMode): boolean {
41
+ return mode === "working" || mode === "meeting" || mode === "resting";
42
+ }
43
+
44
+ export function hasVisibleActivity(agent: Pick<AgentGameOfficeAgent, "activityLabel">): boolean {
45
+ return Boolean(agent.activityLabel?.trim());
46
+ }
47
+
48
+ export function isAgentMoving(current: THREE.Vector3, target: THREE.Vector3, epsilon = 0.05): boolean {
49
+ return current.distanceTo(target) > epsilon;
50
+ }
51
+
52
+ export function stepToward(current: THREE.Vector3, target: THREE.Vector3, maxStep: number): THREE.Vector3 {
53
+ const delta = target.clone().sub(current);
54
+ const distance = delta.length();
55
+ if (distance <= maxStep) {
56
+ return target.clone();
57
+ }
58
+ return current.clone().add(delta.normalize().multiplyScalar(maxStep));
59
+ }
60
+
61
+ export function calculateMoveStep(
62
+ distance: number,
63
+ deltaSeconds: number,
64
+ speedUnitsPerSecond: number,
65
+ ): number {
66
+ const maxStep = speedUnitsPerSecond * deltaSeconds;
67
+ if (distance <= maxStep) {
68
+ return distance;
69
+ }
70
+ const easing = Math.max(0.35, Math.min(1, distance / 1.4));
71
+ return maxStep * easing;
72
+ }
73
+
74
+ export function updateAgentMotion(context: AgentAnimationContext): boolean {
75
+ const speed = context.speedUnitsPerSecond ?? 4.2;
76
+ const current = context.mesh.group.position;
77
+ const distance = current.distanceTo(context.target);
78
+ const next = stepToward(current, context.target, calculateMoveStep(distance, context.deltaSeconds, speed));
79
+ const direction = next.clone().sub(current);
80
+
81
+ if (direction.lengthSq() > 0.0001) {
82
+ faceToward(context.mesh.group, next);
83
+ }
84
+
85
+ context.mesh.group.position.copy(next);
86
+ const moving = isAgentMoving(next, context.target);
87
+ if (!moving) {
88
+ faceToward(context.mesh.group, context.facingTarget ?? context.target);
89
+ }
90
+ return moving;
91
+ }
92
+
93
+ export function applyAgentPose(
94
+ mesh: AgentMeshParts,
95
+ agent: AgentGameOfficeAgent,
96
+ moving: boolean,
97
+ elapsedSeconds: number,
98
+ ): void {
99
+ resetPose(mesh);
100
+ applyPoseMode(mesh, resolveAgentAnimationMode(agent, moving), elapsedSeconds);
101
+
102
+ const hasActivity = hasVisibleActivity(agent) && agent.sceneState !== "offline";
103
+ mesh.activityGlow.visible = hasActivity;
104
+ if (hasActivity) {
105
+ const scale = 1 + Math.sin(elapsedSeconds * 4) * 0.08;
106
+ mesh.activityGlow.scale.set(scale, scale, scale);
107
+ }
108
+ updateAgentActivityEffects(mesh.activityEffects, agent, elapsedSeconds);
109
+ }
110
+
111
+ function resetPose(mesh: AgentMeshParts): void {
112
+ mesh.visualRoot.position.y = 0;
113
+ mesh.body.rotation.set(0, 0, 0);
114
+ mesh.head.rotation.set(0, 0, 0);
115
+ mesh.leftArm.rotation.set(0, 0, 0);
116
+ mesh.rightArm.rotation.set(0, 0, 0);
117
+ mesh.leftLeg.rotation.set(0, 0, 0);
118
+ mesh.rightLeg.rotation.set(0, 0, 0);
119
+ mesh.leftFoot.rotation.set(0, 0, 0);
120
+ mesh.rightFoot.rotation.set(0, 0, 0);
121
+ mesh.body.scale.set(1, 1, 1);
122
+ mesh.activityGlow.scale.set(1, 1, 1);
123
+ }
124
+
125
+ function applyPoseMode(
126
+ mesh: AgentMeshParts,
127
+ mode: AgentAnimationMode,
128
+ elapsedSeconds: number,
129
+ ): void {
130
+ switch (mode) {
131
+ case "walking": {
132
+ const phase = elapsedSeconds * 8;
133
+ const swing = Math.sin(phase);
134
+ mesh.visualRoot.position.y = Math.abs(swing) * 0.075;
135
+ mesh.leftLeg.rotation.x = swing * 0.62;
136
+ mesh.rightLeg.rotation.x = -swing * 0.62;
137
+ mesh.leftFoot.rotation.x = Math.max(0, swing) * 0.22;
138
+ mesh.rightFoot.rotation.x = Math.max(0, -swing) * 0.22;
139
+ mesh.leftArm.rotation.x = -swing * 0.5;
140
+ mesh.rightArm.rotation.x = swing * 0.5;
141
+ mesh.body.rotation.z = swing * 0.04;
142
+ return;
143
+ }
144
+ case "working": {
145
+ const phase = elapsedSeconds * 7;
146
+ applySeatedLowerBody(mesh);
147
+ mesh.visualRoot.position.y = -0.08;
148
+ mesh.body.rotation.x = -0.1;
149
+ mesh.head.rotation.x = -0.06;
150
+ mesh.leftArm.rotation.x = 0.58 + Math.sin(phase) * 0.18;
151
+ mesh.rightArm.rotation.x = 0.58 - Math.sin(phase) * 0.18;
152
+ mesh.leftArm.rotation.z = -0.08;
153
+ mesh.rightArm.rotation.z = 0.08;
154
+ return;
155
+ }
156
+ case "thinking":
157
+ mesh.head.rotation.y = Math.sin(elapsedSeconds * 1.2) * 0.35;
158
+ mesh.head.rotation.x = Math.sin(elapsedSeconds * 0.9) * 0.08;
159
+ mesh.body.rotation.z = Math.sin(elapsedSeconds * 1.6) * 0.07;
160
+ return;
161
+ case "meeting":
162
+ applySeatedLowerBody(mesh);
163
+ mesh.visualRoot.position.y = -0.08;
164
+ mesh.head.rotation.x = Math.sin(elapsedSeconds * 2) * 0.08;
165
+ mesh.leftArm.rotation.x = 0.08;
166
+ mesh.rightArm.rotation.x = -0.08;
167
+ return;
168
+ case "resting": {
169
+ const breath = 1 + Math.sin(elapsedSeconds * 1.2) * 0.025;
170
+ applySeatedLowerBody(mesh);
171
+ mesh.visualRoot.position.y = -0.12;
172
+ mesh.body.scale.set(1, breath, 1);
173
+ mesh.body.rotation.x = 0.08;
174
+ mesh.head.rotation.z = Math.sin(elapsedSeconds * 0.8) * 0.05;
175
+ return;
176
+ }
177
+ case "idle": {
178
+ const breath = 1 + Math.sin(elapsedSeconds * 0.9) * 0.012;
179
+ mesh.body.scale.set(1, breath, 1);
180
+ return;
181
+ }
182
+ case "entertaining":
183
+ mesh.visualRoot.position.y = Math.abs(Math.sin(elapsedSeconds * 4)) * 0.12;
184
+ mesh.body.rotation.z = Math.sin(elapsedSeconds * 5) * 0.1;
185
+ mesh.leftArm.rotation.z = -0.22 + Math.sin(elapsedSeconds * 5) * 0.38;
186
+ mesh.rightArm.rotation.z = 0.22 - Math.sin(elapsedSeconds * 5) * 0.38;
187
+ return;
188
+ case "offline":
189
+ return;
190
+ }
191
+ }
192
+
193
+ function applySeatedLowerBody(mesh: AgentMeshParts): void {
194
+ mesh.leftLeg.rotation.x = -1.15;
195
+ mesh.rightLeg.rotation.x = -1.15;
196
+ mesh.leftFoot.rotation.x = 0.8;
197
+ mesh.rightFoot.rotation.x = 0.8;
198
+ }
199
+
200
+ function faceToward(group: THREE.Group, target: THREE.Vector3): void {
201
+ const direction = target.clone().sub(group.position);
202
+ if (direction.lengthSq() > 0.0001) {
203
+ group.rotation.y = Math.atan2(direction.x, direction.z);
204
+ }
205
+ }
@@ -0,0 +1,119 @@
1
+ import * as THREE from "three";
2
+
3
+ import type { AgentMeshParts } from "./agent-mesh";
4
+ import {
5
+ resolveAgentBodyPartRenderSpecs,
6
+ } from "./agent-mesh";
7
+
8
+ export type AgentBodyInstanceRecord = {
9
+ mesh: AgentMeshParts;
10
+ renderIndex: number;
11
+ visible: boolean;
12
+ };
13
+
14
+ type AgentBodyBatch = {
15
+ mesh: THREE.InstancedMesh;
16
+ capacity: number;
17
+ count: number;
18
+ };
19
+
20
+ const geometryCache = new Map<string, THREE.BoxGeometry>();
21
+ const materialCache = new Map<number, THREE.MeshLambertMaterial>();
22
+ const matrixHelper = new THREE.Matrix4();
23
+
24
+ export class AgentBodyInstancedLayer {
25
+ private readonly scene: THREE.Scene;
26
+ private readonly batches = new Map<string, AgentBodyBatch>();
27
+
28
+ constructor(scene: THREE.Scene) {
29
+ this.scene = scene;
30
+ }
31
+
32
+ update(records: AgentBodyInstanceRecord[]): void {
33
+ this.batches.forEach((batch) => {
34
+ batch.count = 0;
35
+ batch.mesh.count = 0;
36
+ });
37
+
38
+ records.forEach((record) => {
39
+ if (!record.visible) {
40
+ return;
41
+ }
42
+ record.mesh.group.updateMatrixWorld(true);
43
+ resolveAgentBodyPartRenderSpecs(record.renderIndex).forEach((part) => {
44
+ const batch = this.ensureBatch(part, records.length);
45
+ const object = record.mesh[part.key];
46
+ object.updateMatrixWorld(true);
47
+ matrixHelper.copy(object.matrixWorld);
48
+ batch.mesh.setMatrixAt(batch.count, matrixHelper);
49
+ batch.count += 1;
50
+ });
51
+ });
52
+
53
+ this.batches.forEach((batch) => {
54
+ batch.mesh.count = batch.count;
55
+ batch.mesh.visible = batch.count > 0;
56
+ batch.mesh.instanceMatrix.needsUpdate = true;
57
+ });
58
+ }
59
+
60
+ dispose(): void {
61
+ this.batches.forEach((batch) => {
62
+ this.scene.remove(batch.mesh);
63
+ });
64
+ this.batches.clear();
65
+ }
66
+
67
+ private ensureBatch(
68
+ part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number],
69
+ requiredCapacity: number,
70
+ ): AgentBodyBatch {
71
+ const key = `${part.key}:${part.width}:${part.height}:${part.depth}:${part.color}`;
72
+ const existing = this.batches.get(key);
73
+ if (existing && existing.capacity >= requiredCapacity) {
74
+ return existing;
75
+ }
76
+ if (existing) {
77
+ this.scene.remove(existing.mesh);
78
+ }
79
+
80
+ const mesh = new THREE.InstancedMesh(
81
+ getGeometry(part.width, part.height, part.depth),
82
+ getMaterial(part.color),
83
+ Math.max(requiredCapacity, 1),
84
+ );
85
+ mesh.castShadow = false;
86
+ mesh.receiveShadow = false;
87
+ mesh.name = `agentBody:${key}`;
88
+ this.scene.add(mesh);
89
+
90
+ const batch = { mesh, capacity: Math.max(requiredCapacity, 1), count: 0 };
91
+ this.batches.set(key, batch);
92
+ return batch;
93
+ }
94
+ }
95
+
96
+ export function createAgentBodyInstancedLayer(scene: THREE.Scene): AgentBodyInstancedLayer {
97
+ return new AgentBodyInstancedLayer(scene);
98
+ }
99
+
100
+ function getGeometry(width: number, height: number, depth: number): THREE.BoxGeometry {
101
+ const key = `${width}:${height}:${depth}`;
102
+ const cached = geometryCache.get(key);
103
+ if (cached) {
104
+ return cached;
105
+ }
106
+ const geometry = new THREE.BoxGeometry(width, height, depth);
107
+ geometryCache.set(key, geometry);
108
+ return geometry;
109
+ }
110
+
111
+ function getMaterial(color: number): THREE.MeshLambertMaterial {
112
+ const cached = materialCache.get(color);
113
+ if (cached) {
114
+ return cached;
115
+ }
116
+ const material = new THREE.MeshLambertMaterial({ color });
117
+ materialCache.set(color, material);
118
+ return material;
119
+ }
@@ -0,0 +1,82 @@
1
+ import * as THREE from "three";
2
+
3
+ import type { AgentGameOfficeAgent } from "../../core/types";
4
+
5
+ export type AgentLabelRecord = {
6
+ root: HTMLDivElement;
7
+ title: HTMLDivElement;
8
+ activity: HTMLDivElement;
9
+ };
10
+
11
+ export function formatAgentLabel(
12
+ agent: Pick<AgentGameOfficeAgent, "name" | "statusLabel" | "activityLabel">,
13
+ ): { title: string; activity?: string } {
14
+ const activity = agent.activityLabel?.trim();
15
+ return {
16
+ title: `${agent.name} · ${agent.statusLabel}`,
17
+ ...(activity ? { activity } : {}),
18
+ };
19
+ }
20
+
21
+ export function createAgentLabel(overlay: HTMLElement): AgentLabelRecord {
22
+ const root = document.createElement("div");
23
+ root.style.position = "absolute";
24
+ root.style.transform = "translate(-50%, -100%)";
25
+ root.style.padding = "4px 7px";
26
+ root.style.borderRadius = "6px";
27
+ root.style.background = "rgba(17, 24, 39, 0.76)";
28
+ root.style.color = "white";
29
+ root.style.font = "12px system-ui, sans-serif";
30
+ root.style.lineHeight = "1.25";
31
+ root.style.maxWidth = "180px";
32
+ root.style.textAlign = "center";
33
+ root.style.boxSizing = "border-box";
34
+
35
+ const title = document.createElement("div");
36
+ title.style.fontWeight = "600";
37
+ title.style.whiteSpace = "nowrap";
38
+ title.style.overflow = "hidden";
39
+ title.style.textOverflow = "ellipsis";
40
+ root.appendChild(title);
41
+
42
+ const activity = document.createElement("div");
43
+ activity.style.marginTop = "2px";
44
+ activity.style.fontSize = "11px";
45
+ activity.style.opacity = "0.88";
46
+ activity.style.whiteSpace = "nowrap";
47
+ activity.style.overflow = "hidden";
48
+ activity.style.textOverflow = "ellipsis";
49
+ activity.style.display = "none";
50
+ root.appendChild(activity);
51
+
52
+ overlay.appendChild(root);
53
+
54
+ return { root, title, activity };
55
+ }
56
+
57
+ export function updateAgentLabel(record: AgentLabelRecord, agent: AgentGameOfficeAgent): void {
58
+ const label = formatAgentLabel(agent);
59
+ record.title.textContent = label.title;
60
+ record.activity.textContent = label.activity ?? "";
61
+ record.activity.style.display = label.activity ? "block" : "none";
62
+ record.root.style.display = agent.sceneState === "offline" ? "none" : "block";
63
+ }
64
+
65
+ export function updateAgentLabelPosition(
66
+ record: AgentLabelRecord,
67
+ position: THREE.Vector3,
68
+ camera: THREE.Camera,
69
+ viewport: HTMLElement,
70
+ ): void {
71
+ const projected = position.clone();
72
+ projected.y += 2.2;
73
+ projected.project(camera);
74
+ const x = (projected.x * 0.5 + 0.5) * viewport.clientWidth;
75
+ const y = (-projected.y * 0.5 + 0.5) * viewport.clientHeight;
76
+ record.root.style.left = `${x}px`;
77
+ record.root.style.top = `${y}px`;
78
+ }
79
+
80
+ export function removeAgentLabel(record: AgentLabelRecord): void {
81
+ record.root.remove();
82
+ }
@@ -0,0 +1,72 @@
1
+ import * as THREE from "three";
2
+
3
+ import type { AgentGameOfficeAgent } from "../../core/types";
4
+ import {
5
+ DESK_SEATS,
6
+ LARGE_MEETING_X,
7
+ LARGE_MEETING_Z,
8
+ LOUNGE_CENTER,
9
+ MEETING_SEATS,
10
+ OFFICE_LAYOUT_SCALE,
11
+ scaleOfficeX,
12
+ scaleOfficeZ,
13
+ } from "./scene";
14
+
15
+ export function resolveAgentPosition(
16
+ agent: AgentGameOfficeAgent,
17
+ index: number,
18
+ totalAgents: number,
19
+ ): THREE.Vector3 {
20
+ if (agent.targetPosition) {
21
+ return new THREE.Vector3(agent.targetPosition.x, 0, agent.targetPosition.z);
22
+ }
23
+ switch (agent.zoneId) {
24
+ case "desk":
25
+ return new THREE.Vector3(
26
+ DESK_SEATS[index % DESK_SEATS.length]!.x,
27
+ 0,
28
+ DESK_SEATS[index % DESK_SEATS.length]!.z,
29
+ );
30
+ case "meeting-room":
31
+ return new THREE.Vector3(
32
+ MEETING_SEATS[index % MEETING_SEATS.length]!.x,
33
+ 0,
34
+ MEETING_SEATS[index % MEETING_SEATS.length]!.z,
35
+ );
36
+ case "lounge":
37
+ return new THREE.Vector3(
38
+ LOUNGE_CENTER.x - 1.7 * OFFICE_LAYOUT_SCALE + (index % 4) * 1.15 * OFFICE_LAYOUT_SCALE,
39
+ 0,
40
+ LOUNGE_CENTER.z - 0.8 * OFFICE_LAYOUT_SCALE + Math.floor(index / 4) * 0.95 * OFFICE_LAYOUT_SCALE,
41
+ );
42
+ case "pantry":
43
+ return new THREE.Vector3(scaleOfficeX(-9.2), 0, scaleOfficeZ(5.2 + index * 0.4));
44
+ case "review-area": {
45
+ const angle = (index / Math.max(totalAgents, 1)) * Math.PI * 2;
46
+ return new THREE.Vector3(
47
+ scaleOfficeX(0.5) + Math.cos(angle) * 2.2 * OFFICE_LAYOUT_SCALE,
48
+ 0,
49
+ scaleOfficeZ(4) + Math.sin(angle) * 1.4 * OFFICE_LAYOUT_SCALE,
50
+ );
51
+ }
52
+ case "offstage":
53
+ return new THREE.Vector3(scaleOfficeX(-13), 0, scaleOfficeZ(8));
54
+ }
55
+ }
56
+
57
+ export function resolveAgentFacingTarget(agent: AgentGameOfficeAgent): THREE.Vector3 {
58
+ switch (agent.zoneId) {
59
+ case "desk":
60
+ return new THREE.Vector3(scaleOfficeX(-7), 0, scaleOfficeZ(-5));
61
+ case "meeting-room":
62
+ return new THREE.Vector3(LARGE_MEETING_X, 0, LARGE_MEETING_Z);
63
+ case "lounge":
64
+ return new THREE.Vector3(LOUNGE_CENTER.x, 0, LOUNGE_CENTER.z);
65
+ case "review-area":
66
+ return new THREE.Vector3(scaleOfficeX(0.5), 0, scaleOfficeZ(3.05));
67
+ case "pantry":
68
+ return new THREE.Vector3(scaleOfficeX(-9.2), 0, scaleOfficeZ(5.2));
69
+ case "offstage":
70
+ return new THREE.Vector3(scaleOfficeX(-13), 0, scaleOfficeZ(8));
71
+ }
72
+ }
@@ -0,0 +1,145 @@
1
+ import * as THREE from "three";
2
+
3
+ import type { AgentGameOfficeAgent } from "../../core/types";
4
+ import {
5
+ type AgentActivityEffects,
6
+ createAgentActivityEffects,
7
+ } from "./agent-activity-effects";
8
+
9
+ export type AgentMeshParts = {
10
+ group: THREE.Group;
11
+ visualRoot: THREE.Group;
12
+ body: THREE.Object3D;
13
+ head: THREE.Object3D;
14
+ leftEye: THREE.Object3D;
15
+ rightEye: THREE.Object3D;
16
+ hairTop: THREE.Object3D;
17
+ hairBack: THREE.Object3D;
18
+ leftArm: THREE.Object3D;
19
+ rightArm: THREE.Object3D;
20
+ leftLeg: THREE.Object3D;
21
+ rightLeg: THREE.Object3D;
22
+ leftFoot: THREE.Object3D;
23
+ rightFoot: THREE.Object3D;
24
+ activityGlow: THREE.Mesh;
25
+ activityEffects: AgentActivityEffects;
26
+ };
27
+
28
+ const agentColors = [0x2563eb, 0xdc2626, 0x059669, 0xd97706, 0x7c3aed, 0x0891b2];
29
+ const skinColors = [0xffdbac, 0xf5cba7, 0xd4a574, 0xc68642];
30
+ const hairColors = [0x3f2a1d, 0x111827, 0x7c2d12, 0x4b5563];
31
+ export const AGENT_MESH_SCALE = 1.15;
32
+ const activityGlowGeometry = new THREE.RingGeometry(0.56, 0.92, 32);
33
+ const activityGlowMaterial = new THREE.MeshBasicMaterial({
34
+ color: 0x38bdf8,
35
+ transparent: true,
36
+ opacity: 0.35,
37
+ side: THREE.DoubleSide,
38
+ depthWrite: false,
39
+ });
40
+
41
+ export type AgentBodyPartKey =
42
+ | "body"
43
+ | "leftLeg"
44
+ | "rightLeg"
45
+ | "leftFoot"
46
+ | "rightFoot"
47
+ | "head"
48
+ | "leftEye"
49
+ | "rightEye"
50
+ | "hairTop"
51
+ | "hairBack"
52
+ | "leftArm"
53
+ | "rightArm";
54
+
55
+ export type AgentBodyPartRenderSpec = {
56
+ key: AgentBodyPartKey;
57
+ width: number;
58
+ height: number;
59
+ depth: number;
60
+ color: number;
61
+ x: number;
62
+ y: number;
63
+ z: number;
64
+ };
65
+
66
+ export function resolveAgentBodyPartRenderSpecs(index: number): AgentBodyPartRenderSpec[] {
67
+ const shirt = agentColors[index % agentColors.length] ?? agentColors[0];
68
+ const skin = skinColors[index % skinColors.length] ?? skinColors[0];
69
+ const hair = hairColors[index % hairColors.length] ?? hairColors[0];
70
+
71
+ return [
72
+ { key: "body", width: 0.56, height: 0.62, depth: 0.34, color: shirt, x: 0, y: 0.92, z: 0 },
73
+ { key: "leftLeg", width: 0.18, height: 0.48, depth: 0.18, color: 0x1f2937, x: -0.14, y: 0.34, z: 0 },
74
+ { key: "rightLeg", width: 0.18, height: 0.48, depth: 0.18, color: 0x1f2937, x: 0.14, y: 0.34, z: 0 },
75
+ { key: "leftFoot", width: 0.24, height: 0.12, depth: 0.34, color: 0x111827, x: -0.14, y: 0.06, z: 0.08 },
76
+ { key: "rightFoot", width: 0.24, height: 0.12, depth: 0.34, color: 0x111827, x: 0.14, y: 0.06, z: 0.08 },
77
+ { key: "head", width: 0.5, height: 0.48, depth: 0.42, color: skin, x: 0, y: 1.48, z: 0 },
78
+ { key: "leftEye", width: 0.06, height: 0.07, depth: 0.035, color: 0x111827, x: -0.11, y: 1.5, z: 0.225 },
79
+ { key: "rightEye", width: 0.06, height: 0.07, depth: 0.035, color: 0x111827, x: 0.11, y: 1.5, z: 0.225 },
80
+ { key: "hairTop", width: 0.54, height: 0.14, depth: 0.46, color: hair, x: 0, y: 1.78, z: -0.01 },
81
+ { key: "hairBack", width: 0.52, height: 0.28, depth: 0.09, color: hair, x: 0, y: 1.61, z: -0.23 },
82
+ { key: "leftArm", width: 0.15, height: 0.56, depth: 0.17, color: shirt, x: -0.43, y: 0.88, z: 0 },
83
+ { key: "rightArm", width: 0.15, height: 0.56, depth: 0.17, color: shirt, x: 0.43, y: 0.88, z: 0 },
84
+ ];
85
+ }
86
+
87
+ export function createAgentMesh(agent: AgentGameOfficeAgent, index: number): AgentMeshParts {
88
+ const group = new THREE.Group();
89
+ group.scale.setScalar(AGENT_MESH_SCALE);
90
+ const visualRoot = new THREE.Group();
91
+ group.add(visualRoot);
92
+ const parts = Object.fromEntries(
93
+ resolveAgentBodyPartRenderSpecs(index).map((part) => [
94
+ part.key,
95
+ addPart(visualRoot, part.x, part.y, part.z),
96
+ ]),
97
+ ) as Record<AgentBodyPartKey, THREE.Object3D>;
98
+ const activityGlow = createActivityGlow();
99
+ const activityEffects = createAgentActivityEffects();
100
+ group.add(activityGlow);
101
+ group.add(activityEffects.group);
102
+ group.name = `agent_${agent.id}`;
103
+
104
+ return {
105
+ group,
106
+ visualRoot,
107
+ body: parts.body,
108
+ head: parts.head,
109
+ leftEye: parts.leftEye,
110
+ rightEye: parts.rightEye,
111
+ hairTop: parts.hairTop,
112
+ hairBack: parts.hairBack,
113
+ leftArm: parts.leftArm,
114
+ rightArm: parts.rightArm,
115
+ leftLeg: parts.leftLeg,
116
+ rightLeg: parts.rightLeg,
117
+ leftFoot: parts.leftFoot,
118
+ rightFoot: parts.rightFoot,
119
+ activityGlow,
120
+ activityEffects,
121
+ };
122
+ }
123
+
124
+ function addPart(
125
+ group: THREE.Group,
126
+ x: number,
127
+ y: number,
128
+ z: number,
129
+ ): THREE.Object3D {
130
+ const part = new THREE.Object3D();
131
+ part.position.set(x, y, z);
132
+ group.add(part);
133
+ return part;
134
+ }
135
+
136
+ function createActivityGlow(): THREE.Mesh {
137
+ const glow = new THREE.Mesh(
138
+ activityGlowGeometry,
139
+ activityGlowMaterial,
140
+ );
141
+ glow.rotation.x = -Math.PI / 2;
142
+ glow.position.y = 0.03;
143
+ glow.visible = false;
144
+ return glow;
145
+ }