let-them-talk 4.3.0 → 5.2.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 +640 -582
- package/README.md +592 -415
- package/cli.js +1089 -589
- package/conversation-templates/autonomous-feature.json +22 -0
- package/conversation-templates/code-review.json +21 -11
- package/conversation-templates/debug-squad.json +21 -11
- package/conversation-templates/feature-build.json +21 -11
- package/conversation-templates/research-write.json +21 -11
- package/dashboard.html +9250 -7964
- package/dashboard.js +1071 -29
- package/office/building-interior.js +261 -0
- package/office/car-hud.js +368 -0
- package/office/daynight.js +221 -0
- package/office/economy-hud.js +432 -0
- package/office/economy-ui.js +238 -0
- package/office/environment.js +818 -808
- package/office/fast-travel.js +215 -0
- package/office/hq-building.js +295 -0
- package/office/index.js +1095 -1046
- package/office/instancing.js +160 -0
- package/office/lod-manager.js +165 -0
- package/office/multiplayer-hud.js +428 -0
- package/office/net-client.js +299 -0
- package/office/particles.js +172 -0
- package/office/player.js +658 -658
- package/office/post-processing.js +82 -0
- package/office/sky.js +319 -0
- package/office/street-furniture.js +308 -0
- package/office/vehicle.js +455 -0
- package/package.json +59 -59
- package/server.js +7190 -4685
- package/conversation-templates/managed-team.json +0 -12
package/office/player.js
CHANGED
|
@@ -1,658 +1,658 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import { S } from './state.js';
|
|
3
|
-
import { createCharacter } from './character.js';
|
|
4
|
-
import { resolveAppearance } from './appearance.js';
|
|
5
|
-
// ============================================================
|
|
6
|
-
// PLAYER AVATAR — Walk around the 3D world as a character
|
|
7
|
-
// Not an agent — no MCP, no messages — just visual presence
|
|
8
|
-
// ============================================================
|
|
9
|
-
|
|
10
|
-
var PLAYER_SPEED = 3;
|
|
11
|
-
var CAMERA_OFFSET = new THREE.Vector3(0, 3, 5); // behind + above
|
|
12
|
-
var CAMERA_LOOK_OFFSET = new THREE.Vector3(0, 1.2, 0); // look at chest height
|
|
13
|
-
var PLAYER_RADIUS = 0.35; // collision radius
|
|
14
|
-
|
|
15
|
-
// Reusable Vector3s (avoid per-frame allocation)
|
|
16
|
-
var _tmpForward = new THREE.Vector3();
|
|
17
|
-
var _tmpRight = new THREE.Vector3();
|
|
18
|
-
var _tmpDir = new THREE.Vector3();
|
|
19
|
-
var _tmpCamTarget = new THREE.Vector3();
|
|
20
|
-
var _tmpLookAt = new THREE.Vector3();
|
|
21
|
-
var _tmpTargetPos = new THREE.Vector3();
|
|
22
|
-
|
|
23
|
-
// ==================== COLLISION SYSTEM ====================
|
|
24
|
-
// AABB boxes: { minX, maxX, minZ, maxZ }
|
|
25
|
-
// Thin walls as boxes with small thickness
|
|
26
|
-
|
|
27
|
-
function getCampusColliders() {
|
|
28
|
-
var W = 50, D = 35;
|
|
29
|
-
var colliders = [
|
|
30
|
-
// Building walls (0.3 thick)
|
|
31
|
-
{ minX: -W/2 - 0.3, maxX: -W/2, minZ: -D/2, maxZ: D/2 }, // left wall
|
|
32
|
-
{ minX: W/2, maxX: W/2 + 0.3, minZ: -D/2, maxZ: D/2 }, // right wall
|
|
33
|
-
{ minX: -W/2, maxX: W/2, minZ: -D/2 - 0.3, maxZ: -D/2 }, // back wall
|
|
34
|
-
// Front wall with entrance gap (gap at x: -4 to 4)
|
|
35
|
-
{ minX: -W/2, maxX: -4, minZ: D/2, maxZ: D/2 + 0.3 },
|
|
36
|
-
{ minX: 4, maxX: W/2, minZ: D/2, maxZ: D/2 + 0.3 },
|
|
37
|
-
|
|
38
|
-
// Manager office walls: group at (12,5), offW=8, offD=7
|
|
39
|
-
// Left wall (x=8): z from 1.5 to 8.5
|
|
40
|
-
{ minX: 7.85, maxX: 8.15, minZ: 1.5, maxZ: 8.5 },
|
|
41
|
-
// Right wall (x=16): z from 1.5 to 8.5
|
|
42
|
-
{ minX: 15.85, maxX: 16.15, minZ: 1.5, maxZ: 8.5 },
|
|
43
|
-
// Back wall (z=8.5): x from 8 to 16
|
|
44
|
-
{ minX: 8, maxX: 16, minZ: 8.35, maxZ: 8.65 },
|
|
45
|
-
// Front wall left of door (z=1.5, x from 8 to ~11.4)
|
|
46
|
-
{ minX: 8, maxX: 11.4, minZ: 1.35, maxZ: 1.65 },
|
|
47
|
-
// Front wall right of door (z=1.5, x from ~12.6 to 16)
|
|
48
|
-
{ minX: 12.6, maxX: 16, minZ: 1.35, maxZ: 1.65 },
|
|
49
|
-
// Door collider (only active when closed) — handled dynamically below
|
|
50
|
-
|
|
51
|
-
// Glass partition between workspace and rec (z=-7, gap at x=-1 to 1)
|
|
52
|
-
{ minX: -7, maxX: -1, minZ: -7.15, maxZ: -6.85 },
|
|
53
|
-
{ minX: 1, maxX: 7, minZ: -7.15, maxZ: -6.85 },
|
|
54
|
-
|
|
55
|
-
// Glass partition designer/main (x=-8, gap at z=2 to 4)
|
|
56
|
-
{ minX: -8.15, maxX: -7.85, minZ: -5, maxZ: 2 },
|
|
57
|
-
{ minX: -8.15, maxX: -7.85, minZ: 4, maxZ: 7 },
|
|
58
|
-
|
|
59
|
-
// Reception desk (ground floor)
|
|
60
|
-
{ minX: -2.2, maxX: 2.2, minZ: 13.5, maxZ: 15, floor: 'ground' },
|
|
61
|
-
// Reception logo wall
|
|
62
|
-
{ minX: -3, maxX: 3, minZ: 15.5, maxZ: 16, floor: 'ground' },
|
|
63
|
-
// Water feature
|
|
64
|
-
{ minX: -1.5, maxX: 1.5, minZ: 9.5, maxZ: 10.5, floor: 'ground' },
|
|
65
|
-
|
|
66
|
-
// Bar counter (ground floor)
|
|
67
|
-
{ minX: -17, maxX: -11, minZ: -12.7, maxZ: -11.3, floor: 'ground' },
|
|
68
|
-
|
|
69
|
-
// Pool table (ground floor)
|
|
70
|
-
{ minX: -3.3, maxX: -0.7, minZ: -12.7, maxZ: -11.3, floor: 'ground' },
|
|
71
|
-
// Foosball (ground floor)
|
|
72
|
-
{ minX: 1.8, maxX: 3.2, minZ: -12.4, maxZ: -11.6, floor: 'ground' },
|
|
73
|
-
|
|
74
|
-
// Mezzanine support columns (thin cylinders, approximate as small boxes)
|
|
75
|
-
// Columns at x: -18,-9,0,9,18 z: -CAMPUS_D/2 + MEZZ_DEPTH = -17.5+12 = -5.5
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
// Desk colliders (gaming desks)
|
|
79
|
-
var CAMPUS_DESKS = [
|
|
80
|
-
{ x: -4.5, z: 2 }, { x: -1.5, z: 2 }, { x: 1.5, z: 2 }, { x: 4.5, z: 2 },
|
|
81
|
-
{ x: -4.5, z: -1 }, { x: -1.5, z: -1 }, { x: 1.5, z: -1 }, { x: 4.5, z: -1 },
|
|
82
|
-
{ x: -4.5, z: -4 }, { x: -1.5, z: -4 }, { x: 1.5, z: -4 }, { x: 4.5, z: -4 },
|
|
83
|
-
{ x: -14, z: 1 }, { x: -11, z: 1 },
|
|
84
|
-
{ x: -14, z: -2 }, { x: -11, z: -2 },
|
|
85
|
-
];
|
|
86
|
-
CAMPUS_DESKS.forEach(function(d) {
|
|
87
|
-
// Desk body
|
|
88
|
-
colliders.push({ minX: d.x - 1.1, maxX: d.x + 1.1, minZ: d.z - 0.5, maxZ: d.z +
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Manager's desk inside office (ground floor)
|
|
92
|
-
colliders.push({ minX: 10.5, maxX: 14.5, minZ: 5.5, maxZ:
|
|
93
|
-
|
|
94
|
-
// Bar counter (ground floor)
|
|
95
|
-
// Pool table, foosball (ground floor) — already added above without tag, let me not duplicate
|
|
96
|
-
|
|
97
|
-
// Reception area (ground floor)
|
|
98
|
-
// Already added above
|
|
99
|
-
|
|
100
|
-
return colliders;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function getModernColliders() {
|
|
104
|
-
// Simple colliders for the old modern/startup office
|
|
105
|
-
var colliders = [
|
|
106
|
-
{ minX: -14.3, maxX: -14, minZ: -8, maxZ: 8 }, // left wall
|
|
107
|
-
{ minX: 14, maxX: 14.3, minZ: -8, maxZ: 8 }, // right wall
|
|
108
|
-
{ minX: -14, maxX: 14, minZ: -8.3, maxZ: -8 }, // back wall
|
|
109
|
-
{ minX: -14, maxX: 14, minZ: 8, maxZ: 8.3 }, // front wall
|
|
110
|
-
];
|
|
111
|
-
return colliders;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
var _cachedColliders = null;
|
|
115
|
-
var _cachedCollidersEnv = null;
|
|
116
|
-
|
|
117
|
-
function getColliders() {
|
|
118
|
-
if (_cachedCollidersEnv === S.currentEnv && _cachedColliders) return _cachedColliders;
|
|
119
|
-
_cachedCollidersEnv = S.currentEnv;
|
|
120
|
-
if (S.currentEnv === 'campus') {
|
|
121
|
-
_cachedColliders = getCampusColliders();
|
|
122
|
-
} else {
|
|
123
|
-
_cachedColliders = getModernColliders();
|
|
124
|
-
}
|
|
125
|
-
return _cachedColliders;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Check if a circle (player) at (x,z) with radius r collides with any AABB
|
|
129
|
-
function checkCollision(x, z, r) {
|
|
130
|
-
var playerY = S._player ? S._player.pos.y : 0;
|
|
131
|
-
var onMezzanine = playerY > MEZZ_HEIGHT * 0.4;
|
|
132
|
-
var onStairs = S._player && x >= STAIR_X_MIN && x <= STAIR_X_MAX && z <= STAIR_Z_BOTTOM && z >= STAIR_Z_TOP;
|
|
133
|
-
|
|
134
|
-
var colliders = getColliders();
|
|
135
|
-
for (var i = 0; i < colliders.length; i++) {
|
|
136
|
-
var c = colliders[i];
|
|
137
|
-
// Skip ground-floor furniture colliders when on mezzanine (desks etc)
|
|
138
|
-
if (onMezzanine && c.floor === 'ground') continue;
|
|
139
|
-
// Skip mezzanine colliders when on ground floor
|
|
140
|
-
if (!onMezzanine && !onStairs && c.floor === 'upper') continue;
|
|
141
|
-
var cx = Math.max(c.minX, Math.min(x, c.maxX));
|
|
142
|
-
var cz = Math.max(c.minZ, Math.min(z, c.maxZ));
|
|
143
|
-
var dx = x - cx, dz = z - cz;
|
|
144
|
-
if (dx * dx + dz * dz < r * r) return true;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Mezzanine railing — blocks walking off the edge (only when on mezzanine)
|
|
148
|
-
if (onMezzanine && !onStairs) {
|
|
149
|
-
// Front edge of mezzanine at z = -5.5 (except staircase gap at x 18.75-21.25)
|
|
150
|
-
if (z > -5.7 && z < -5.3 && !(x >= STAIR_X_MIN - 0.5 && x <= STAIR_X_MAX + 0.5)) {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Dynamic: manager door (closed = collider, open = passable)
|
|
156
|
-
if (S.currentEnv === 'campus' && S._managerDoorLerp < 0.5 && !onMezzanine) {
|
|
157
|
-
var doorBox = { minX: 11.4, maxX: 12.6, minZ: 1.35, maxZ: 1.65 };
|
|
158
|
-
var dcx = Math.max(doorBox.minX, Math.min(x, doorBox.maxX));
|
|
159
|
-
var dcz = Math.max(doorBox.minZ, Math.min(z, doorBox.maxZ));
|
|
160
|
-
var ddx = x - dcx, ddz = z - dcz;
|
|
161
|
-
if (ddx * ddx + ddz * ddz < r * r) return true;
|
|
162
|
-
}
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Check collision with agents (circle vs circle)
|
|
167
|
-
function checkAgentCollision(x, z, r) {
|
|
168
|
-
for (var name in S.agents3d) {
|
|
169
|
-
var ag = S.agents3d[name];
|
|
170
|
-
if (!ag.registered || ag.dying) continue;
|
|
171
|
-
var dx = x - ag.pos.x, dz = z - ag.pos.z;
|
|
172
|
-
if (dx * dx + dz * dz < (r + 0.3) * (r + 0.3)) return true;
|
|
173
|
-
}
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Resolve movement with sliding collision (try X and Z independently)
|
|
178
|
-
function resolveMovement(oldX, oldZ, newX, newZ, r) {
|
|
179
|
-
// Try full movement
|
|
180
|
-
if (!checkCollision(newX, newZ, r) && !checkAgentCollision(newX, newZ, r)) {
|
|
181
|
-
return { x: newX, z: newZ };
|
|
182
|
-
}
|
|
183
|
-
// Try sliding along X
|
|
184
|
-
if (!checkCollision(newX, oldZ, r) && !checkAgentCollision(newX, oldZ, r)) {
|
|
185
|
-
return { x: newX, z: oldZ };
|
|
186
|
-
}
|
|
187
|
-
// Try sliding along Z
|
|
188
|
-
if (!checkCollision(oldX, newZ, r) && !checkAgentCollision(oldX, newZ, r)) {
|
|
189
|
-
return { x: oldX, z: newZ };
|
|
190
|
-
}
|
|
191
|
-
// Stuck — don't move
|
|
192
|
-
return { x: oldX, z: oldZ };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ==================== HEIGHT SYSTEM ====================
|
|
196
|
-
// Staircase: x 18.75-21.25, z -3.5 (bottom, y=0) to -9.5 (top, y=3.2)
|
|
197
|
-
// Mezzanine: y=3.2, z from -17.5 to -5.5, full width
|
|
198
|
-
var STAIR_X_MIN = 18.75, STAIR_X_MAX = 21.25;
|
|
199
|
-
var STAIR_Z_BOTTOM = -3.5, STAIR_Z_TOP = -9.5;
|
|
200
|
-
var MEZZ_HEIGHT = 3.2;
|
|
201
|
-
var MEZZ_Z_BACK = -17.5, MEZZ_Z_FRONT = -5.5;
|
|
202
|
-
|
|
203
|
-
function getGroundHeight(x, z, currentY) {
|
|
204
|
-
if (S.currentEnv !== 'campus') return 0;
|
|
205
|
-
|
|
206
|
-
// On the staircase?
|
|
207
|
-
if (x >= STAIR_X_MIN && x <= STAIR_X_MAX && z <= STAIR_Z_BOTTOM && z >= STAIR_Z_TOP) {
|
|
208
|
-
// Interpolate height: bottom (z=-3.5, y=0) to top (z=-9.5, y=3.2)
|
|
209
|
-
var t = (STAIR_Z_BOTTOM - z) / (STAIR_Z_BOTTOM - STAIR_Z_TOP);
|
|
210
|
-
t = Math.max(0, Math.min(1, t));
|
|
211
|
-
return t * MEZZ_HEIGHT;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// On the mezzanine? (must have come from stairs — check if currentY > halfway)
|
|
215
|
-
if (currentY > MEZZ_HEIGHT * 0.4 && z <= MEZZ_Z_FRONT && z >= MEZZ_Z_BACK) {
|
|
216
|
-
return MEZZ_HEIGHT;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Ground level
|
|
220
|
-
return 0;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Invalidate collider cache on env switch
|
|
224
|
-
export function invalidateColliders() {
|
|
225
|
-
_cachedColliders = null;
|
|
226
|
-
_cachedCollidersEnv = null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function spawnPlayer() {
|
|
230
|
-
if (S._player) despawnPlayer();
|
|
231
|
-
|
|
232
|
-
// Load appearance from localStorage
|
|
233
|
-
var appearance = {};
|
|
234
|
-
try {
|
|
235
|
-
var stored = localStorage.getItem('ltt_player_appearance');
|
|
236
|
-
if (stored) appearance = JSON.parse(stored);
|
|
237
|
-
} catch (e) {}
|
|
238
|
-
|
|
239
|
-
var parts = createCharacter('Player', appearance);
|
|
240
|
-
parts.group.position.set(0, 0, 12); // spawn at lobby
|
|
241
|
-
|
|
242
|
-
// Remove typing dots and task indicator (player doesn't need them)
|
|
243
|
-
parts.typingLabel.visible = false;
|
|
244
|
-
parts.taskLabel.visible = false;
|
|
245
|
-
|
|
246
|
-
// Update name label
|
|
247
|
-
var nameEl = parts.labelDiv.querySelector('.office3d-label-name');
|
|
248
|
-
var dotEl = parts.labelDiv.querySelector('.office3d-label-dot');
|
|
249
|
-
if (nameEl) nameEl.textContent = 'You';
|
|
250
|
-
if (dotEl) dotEl.style.background = '#58a6ff';
|
|
251
|
-
|
|
252
|
-
S.scene.add(parts.group);
|
|
253
|
-
|
|
254
|
-
S._player = {
|
|
255
|
-
parts: parts,
|
|
256
|
-
pos: { x: 0, y: 0, z: 12 },
|
|
257
|
-
facing: 0, // radians, 0 = +z direction
|
|
258
|
-
velocity: { x: 0, z: 0 },
|
|
259
|
-
isMoving: false,
|
|
260
|
-
appearance: appearance,
|
|
261
|
-
camYaw: Math.PI, // camera orbit angle around player (horizontal)
|
|
262
|
-
camPitch: 0.4, // camera pitch (vertical angle, 0=level, positive=looking down)
|
|
263
|
-
camDist: 6, // distance from player
|
|
264
|
-
_jumping: false,
|
|
265
|
-
_jumpVel: 0,
|
|
266
|
-
_jumpY: 0,
|
|
267
|
-
_landSquash: 0,
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
// Disable spectator camera movement but keep key/mouse tracking alive
|
|
271
|
-
if (S.controls) {
|
|
272
|
-
S.controls.enabled = false;
|
|
273
|
-
S.controls._playerZoomCb = function(deltaY) {
|
|
274
|
-
if (S._player) {
|
|
275
|
-
S._player.camDist = Math.max(2, Math.min(15, S._player.camDist + deltaY * 0.01));
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return S._player;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function despawnPlayer() {
|
|
284
|
-
if (!S._player) return;
|
|
285
|
-
// Clean up "Press E to sit" prompt so it doesn't leak to other tabs
|
|
286
|
-
if (S._player._sitPrompt) {
|
|
287
|
-
S._player._sitPrompt.style.display = 'none';
|
|
288
|
-
if (S._player._sitPrompt.parentElement) S._player._sitPrompt.remove();
|
|
289
|
-
S._player._sitPrompt = null;
|
|
290
|
-
}
|
|
291
|
-
S.scene.remove(S._player.parts.group);
|
|
292
|
-
S._player.parts.group.traverse(function(child) {
|
|
293
|
-
if (child.geometry) child.geometry.dispose();
|
|
294
|
-
if (child.material) {
|
|
295
|
-
if (child.material.map) child.material.map.dispose();
|
|
296
|
-
child.material.dispose();
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
S._player = null;
|
|
300
|
-
|
|
301
|
-
// Re-enable spectator camera
|
|
302
|
-
if (S.controls) {
|
|
303
|
-
S.controls.enabled = true;
|
|
304
|
-
S.controls._playerZoomCb = null;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function isPlayerMode() {
|
|
309
|
-
return !!S._player;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export function getPlayer() {
|
|
313
|
-
return S._player;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function savePlayerAppearance(appearance) {
|
|
317
|
-
try {
|
|
318
|
-
localStorage.setItem('ltt_player_appearance', JSON.stringify(appearance));
|
|
319
|
-
} catch (e) {}
|
|
320
|
-
if (S._player) S._player.appearance = appearance;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export function getPlayerAppearance() {
|
|
324
|
-
try {
|
|
325
|
-
var stored = localStorage.getItem('ltt_player_appearance');
|
|
326
|
-
if (stored) return JSON.parse(stored);
|
|
327
|
-
} catch (e) {}
|
|
328
|
-
return {};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Called every frame from the animation loop
|
|
332
|
-
export function updatePlayer(dt, time, keys) {
|
|
333
|
-
var player = S._player;
|
|
334
|
-
if (!player) return;
|
|
335
|
-
|
|
336
|
-
// --- Movement from keyboard ---
|
|
337
|
-
var moveX = 0, moveZ = 0;
|
|
338
|
-
if (keys['KeyW'] || keys['ArrowUp']) moveZ -= 1;
|
|
339
|
-
if (keys['KeyS'] || keys['ArrowDown']) moveZ += 1;
|
|
340
|
-
if (keys['KeyA'] || keys['ArrowLeft']) moveX -= 1;
|
|
341
|
-
if (keys['KeyD'] || keys['ArrowRight']) moveX += 1;
|
|
342
|
-
|
|
343
|
-
var isMoving = moveX !== 0 || moveZ !== 0;
|
|
344
|
-
player.isMoving = isMoving;
|
|
345
|
-
|
|
346
|
-
// --- Jump with Space ---
|
|
347
|
-
if (keys['Space'] && !player.sitting && !player._jumping) {
|
|
348
|
-
player._jumping = true;
|
|
349
|
-
player._jumpVel = 5.5; // initial upward velocity
|
|
350
|
-
player._jumpY = 0;
|
|
351
|
-
}
|
|
352
|
-
if (player._jumping) {
|
|
353
|
-
player._jumpVel -= 18 * dt; // gravity
|
|
354
|
-
player._jumpY += player._jumpVel * dt;
|
|
355
|
-
if (player._jumpY <= 0) {
|
|
356
|
-
player._jumpY = 0;
|
|
357
|
-
player._jumping = false;
|
|
358
|
-
player._jumpVel = 0;
|
|
359
|
-
player._landSquash = 0.3; // trigger landing squash
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// Landing squash/stretch decay
|
|
363
|
-
if (player._landSquash > 0) {
|
|
364
|
-
player._landSquash = Math.max(0, player._landSquash - dt * 3);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (isMoving) {
|
|
368
|
-
// Movement relative to camera yaw (orbit angle)
|
|
369
|
-
var camYaw = S.controls && S.controls._euler ? S.controls._euler.y : 0;
|
|
370
|
-
_tmpForward.set(-Math.sin(camYaw), 0, -Math.cos(camYaw)).normalize();
|
|
371
|
-
_tmpRight.set(-_tmpForward.z, 0, _tmpForward.x);
|
|
372
|
-
|
|
373
|
-
_tmpDir.set(0, 0, 0);
|
|
374
|
-
_tmpDir.addScaledVector(_tmpForward, -moveZ);
|
|
375
|
-
_tmpDir.addScaledVector(_tmpRight, moveX);
|
|
376
|
-
_tmpDir.normalize();
|
|
377
|
-
|
|
378
|
-
var speed = PLAYER_SPEED * (keys['ShiftLeft'] || keys['ShiftRight'] ? 2 : 1);
|
|
379
|
-
var newX = player.pos.x + _tmpDir.x * speed * dt;
|
|
380
|
-
var newZ = player.pos.z + _tmpDir.z * speed * dt;
|
|
381
|
-
|
|
382
|
-
// Collision resolution (sliding)
|
|
383
|
-
var resolved = resolveMovement(player.pos.x, player.pos.z, newX, newZ, PLAYER_RADIUS);
|
|
384
|
-
player.pos.x = resolved.x;
|
|
385
|
-
player.pos.z = resolved.z;
|
|
386
|
-
|
|
387
|
-
// Face movement direction
|
|
388
|
-
player.facing = Math.atan2(_tmpDir.x, _tmpDir.z);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Update character position
|
|
392
|
-
// Update height based on position (stairs/mezzanine)
|
|
393
|
-
var targetY = getGroundHeight(player.pos.x, player.pos.z, player.pos.y);
|
|
394
|
-
player.pos.y += (targetY - player.pos.y) * Math.min(1, dt * 8); // smooth height transition
|
|
395
|
-
|
|
396
|
-
player.parts.group.position.x = player.pos.x;
|
|
397
|
-
player.parts.group.position.y = player.pos.y + (player._jumpY || 0);
|
|
398
|
-
player.parts.group.position.z = player.pos.z;
|
|
399
|
-
|
|
400
|
-
// Jump squash/stretch visual
|
|
401
|
-
var jumpSquash = player._landSquash || 0;
|
|
402
|
-
if (player._jumping && player._jumpVel > 0) {
|
|
403
|
-
// Stretch upward during ascent
|
|
404
|
-
player.parts.body.scale.set(0.9, 1.15, 0.9);
|
|
405
|
-
} else if (jumpSquash > 0) {
|
|
406
|
-
// Squash on landing — wide and short
|
|
407
|
-
var sq = 1 + jumpSquash * 0.5; // width: up to 1.15
|
|
408
|
-
var sy = 1 - jumpSquash * 0.4; // height: down to 0.88
|
|
409
|
-
player.parts.body.scale.set(sq, sy, sq);
|
|
410
|
-
} else if (!isMoving) {
|
|
411
|
-
// Reset to breathing (handled below)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Smooth rotation
|
|
415
|
-
var currentRot = player.parts.group.rotation.y;
|
|
416
|
-
var diff = player.facing - currentRot;
|
|
417
|
-
while (diff > Math.PI) diff -= Math.PI * 2;
|
|
418
|
-
while (diff < -Math.PI) diff += Math.PI * 2;
|
|
419
|
-
player.parts.group.rotation.y += diff * Math.min(1, dt * 8);
|
|
420
|
-
|
|
421
|
-
// --- Walking / Jump / Idle animation ---
|
|
422
|
-
if (player._jumping) {
|
|
423
|
-
// Jump pose — tuck legs, raise arms
|
|
424
|
-
var tuck = player._jumpVel > 0 ? 0.6 : 0.3; // more tuck on ascent
|
|
425
|
-
player.parts.leftLeg.rotation.x = -tuck;
|
|
426
|
-
player.parts.rightLeg.rotation.x = -tuck;
|
|
427
|
-
player.parts.leftLowerLeg.rotation.x = tuck * 1.2;
|
|
428
|
-
player.parts.rightLowerLeg.rotation.x = tuck * 1.2;
|
|
429
|
-
player.parts.leftArm.rotation.x = -1.2; // arms up
|
|
430
|
-
player.parts.rightArm.rotation.x = -1.2;
|
|
431
|
-
player.parts.leftForearm.rotation.x = -0.5;
|
|
432
|
-
player.parts.rightForearm.rotation.x = -0.5;
|
|
433
|
-
} else if (isMoving) {
|
|
434
|
-
player.parts.body.scale.set(1, 1, 1); // reset from jump squash
|
|
435
|
-
var swing = Math.sin(time * 10) * 0.5;
|
|
436
|
-
player.parts.leftLeg.rotation.x = swing;
|
|
437
|
-
player.parts.rightLeg.rotation.x = -swing;
|
|
438
|
-
player.parts.leftLowerLeg.rotation.x = Math.max(0, -swing) * 0.8;
|
|
439
|
-
player.parts.rightLowerLeg.rotation.x = Math.max(0, swing) * 0.8;
|
|
440
|
-
player.parts.leftArm.rotation.x = -swing * 0.7;
|
|
441
|
-
player.parts.rightArm.rotation.x = swing * 0.7;
|
|
442
|
-
player.parts.leftForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
|
|
443
|
-
player.parts.rightForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
|
|
444
|
-
} else {
|
|
445
|
-
// Idle — breathing + slight head bob
|
|
446
|
-
player.parts.leftLeg.rotation.x *= 0.9;
|
|
447
|
-
player.parts.rightLeg.rotation.x *= 0.9;
|
|
448
|
-
player.parts.leftArm.rotation.x *= 0.9;
|
|
449
|
-
player.parts.rightArm.rotation.x *= 0.9;
|
|
450
|
-
player.parts.leftLowerLeg.rotation.x *= 0.9;
|
|
451
|
-
player.parts.rightLowerLeg.rotation.x *= 0.9;
|
|
452
|
-
player.parts.leftForearm.rotation.x *= 0.9;
|
|
453
|
-
player.parts.rightForearm.rotation.x *= 0.9;
|
|
454
|
-
var breathe = 1 + Math.sin(time * 2) * 0.02;
|
|
455
|
-
if (!player._landSquash) player.parts.body.scale.set(1, breathe, 1);
|
|
456
|
-
player.parts.head.rotation.z = Math.sin(time * 0.5) * 0.03;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// --- E-to-sit at desk system ---
|
|
460
|
-
var desks = S._campusDeskPositions || [];
|
|
461
|
-
var SIT_RANGE = 2.5; // max distance to sit at a desk
|
|
462
|
-
var nearestDesk = -1;
|
|
463
|
-
var nearestDist = SIT_RANGE;
|
|
464
|
-
|
|
465
|
-
if (!player.sitting) {
|
|
466
|
-
// Find nearest desk
|
|
467
|
-
for (var di = 0; di < desks.length; di++) {
|
|
468
|
-
var ddx = player.pos.x - desks[di].x;
|
|
469
|
-
var ddz = player.pos.z - (desks[di].z + 0.7); // chair is 0.7 in front of desk
|
|
470
|
-
var dd = Math.sqrt(ddx * ddx + ddz * ddz);
|
|
471
|
-
if (dd < nearestDist) { nearestDist = dd; nearestDesk = di; }
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Show/hide "Press E to sit" prompt
|
|
476
|
-
if (nearestDesk >= 0 && !player.sitting) {
|
|
477
|
-
if (!player._sitPrompt) {
|
|
478
|
-
player._sitPrompt = document.createElement('div');
|
|
479
|
-
player._sitPrompt.className = 'office3d-sit-prompt';
|
|
480
|
-
player._sitPrompt.textContent = 'Press E to sit';
|
|
481
|
-
player._sitPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#58a6ff;padding:8px 16px;border-radius:8px;font-size:14px;z-index:1000;pointer-events:none;border:1px solid #30363d;';
|
|
482
|
-
document.body.appendChild(player._sitPrompt);
|
|
483
|
-
}
|
|
484
|
-
player._sitPrompt.style.display = 'block';
|
|
485
|
-
player._nearDesk = nearestDesk;
|
|
486
|
-
} else if (player._sitPrompt && !player.sitting) {
|
|
487
|
-
player._sitPrompt.style.display = 'none';
|
|
488
|
-
player._nearDesk = -1;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// E key: sit down or stand up
|
|
492
|
-
if (keys['KeyE'] && !player._ePressed) {
|
|
493
|
-
player._ePressed = true;
|
|
494
|
-
if (!player.sitting && player._nearDesk >= 0) {
|
|
495
|
-
// Sit down at desk
|
|
496
|
-
player.sitting = true;
|
|
497
|
-
player.sittingDeskIdx = player._nearDesk;
|
|
498
|
-
player.sittingLerp = 0;
|
|
499
|
-
var deskPos = desks[player._nearDesk];
|
|
500
|
-
player._sitTarget = { x: deskPos.x, y: player.pos.y, z: deskPos.z + 0.7 };
|
|
501
|
-
if (player._sitPrompt) player._sitPrompt.style.display = 'none';
|
|
502
|
-
// Notify: player sat down (Builder's iframe hook)
|
|
503
|
-
if (typeof window.onPlayerSit === 'function') window.onPlayerSit(player.sittingDeskIdx);
|
|
504
|
-
} else if (player.sitting) {
|
|
505
|
-
// Stand up — push player AWAY from desk center to avoid collision trapping
|
|
506
|
-
var desks = S._campusDeskPositions || [];
|
|
507
|
-
var dIdx = player.sittingDeskIdx;
|
|
508
|
-
player.sitting = false;
|
|
509
|
-
player.sittingDeskIdx = -1;
|
|
510
|
-
if (dIdx >= 0 && desks[dIdx]) {
|
|
511
|
-
var ddx = player.pos.x - desks[dIdx].x;
|
|
512
|
-
var ddz = player.pos.z - desks[dIdx].z;
|
|
513
|
-
var dd = Math.sqrt(ddx * ddx + ddz * ddz) || 1;
|
|
514
|
-
player.pos.x += (ddx / dd) * 0.8;
|
|
515
|
-
player.pos.z += (ddz / dd) * 0.8;
|
|
516
|
-
} else {
|
|
517
|
-
player.pos.z += 0.8; // fallback
|
|
518
|
-
}
|
|
519
|
-
// Notify: player stood up
|
|
520
|
-
if (typeof window.onPlayerStand === 'function') window.onPlayerStand();
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (!keys['KeyE']) player._ePressed = false;
|
|
524
|
-
|
|
525
|
-
// --- Jukebox E-to-interact ---
|
|
526
|
-
if (S._jukebox && !player.sitting) {
|
|
527
|
-
var jbx = player.pos.x - S._jukebox.pos.x;
|
|
528
|
-
var jbz = player.pos.z - S._jukebox.pos.z;
|
|
529
|
-
var jbDist = Math.sqrt(jbx * jbx + jbz * jbz);
|
|
530
|
-
if (jbDist < 2.0) {
|
|
531
|
-
// Show prompt
|
|
532
|
-
if (!player._jukeboxPrompt) {
|
|
533
|
-
player._jukeboxPrompt = document.createElement('div');
|
|
534
|
-
player._jukeboxPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#ff4488;padding:8px 16px;border-radius:8px;font-size:14px;z-index:1000;pointer-events:none;border:1px solid #ff4488;';
|
|
535
|
-
player._jukeboxPrompt.textContent = 'Press E for Jukebox';
|
|
536
|
-
document.body.appendChild(player._jukeboxPrompt);
|
|
537
|
-
}
|
|
538
|
-
player._jukeboxPrompt.style.display = 'block';
|
|
539
|
-
// E key activates jukebox (only if not near a desk)
|
|
540
|
-
if (keys['KeyE'] && player._ePressed && nearestDesk < 0) {
|
|
541
|
-
if (typeof window.onJukeboxInteract === 'function') window.onJukeboxInteract();
|
|
542
|
-
if (player._jukeboxPrompt) player._jukeboxPrompt.style.display = 'none';
|
|
543
|
-
}
|
|
544
|
-
} else if (player._jukeboxPrompt) {
|
|
545
|
-
player._jukeboxPrompt.style.display = 'none';
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Sitting animation: lerp position to chair, set sitting pose
|
|
550
|
-
if (player.sitting && player._sitTarget) {
|
|
551
|
-
player.sittingLerp = Math.min(1, (player.sittingLerp || 0) + dt * 3);
|
|
552
|
-
var sl = player.sittingLerp;
|
|
553
|
-
player.pos.x += (player._sitTarget.x - player.pos.x) * Math.min(1, dt * 5);
|
|
554
|
-
player.pos.z += (player._sitTarget.z - player.pos.z) * Math.min(1, dt * 5);
|
|
555
|
-
player.facing = Math.PI; // face the desk
|
|
556
|
-
|
|
557
|
-
// Sitting pose (same as agent sitting)
|
|
558
|
-
var sitHip = -1.5 * sl;
|
|
559
|
-
player.parts.leftLeg.rotation.x = sitHip;
|
|
560
|
-
player.parts.rightLeg.rotation.x = sitHip;
|
|
561
|
-
player.parts.leftLowerLeg.rotation.x = 1.5 * sl;
|
|
562
|
-
player.parts.rightLowerLeg.rotation.x = 1.5 * sl;
|
|
563
|
-
player.parts.leftForearm.rotation.x = -0.4 * sl;
|
|
564
|
-
player.parts.rightForearm.rotation.x = -0.4 * sl;
|
|
565
|
-
player.parts.group.position.y = player.pos.y + sl * 0.14;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// --- Third-person camera follow (or desk POV when sitting) ---
|
|
569
|
-
if (player.sitting) {
|
|
570
|
-
updatePlayerCameraDesk(dt);
|
|
571
|
-
} else {
|
|
572
|
-
updatePlayerCamera(dt);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function updatePlayerCamera(dt) {
|
|
577
|
-
var player = S._player;
|
|
578
|
-
if (!player) return;
|
|
579
|
-
|
|
580
|
-
// Use spectator camera's euler for orbit angle (from mouse right-drag)
|
|
581
|
-
var yaw = 0, pitch = 0.4;
|
|
582
|
-
if (S.controls && S.controls._euler) {
|
|
583
|
-
yaw = S.controls._euler.y;
|
|
584
|
-
pitch = Math.max(0.05, Math.min(1.3, -S.controls._euler.x));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Orbit camera around the player using yaw/pitch (+ player height)
|
|
588
|
-
var dist = player.camDist;
|
|
589
|
-
var baseY = player.pos.y;
|
|
590
|
-
var camX = player.pos.x + Math.sin(yaw) * Math.cos(pitch) * dist;
|
|
591
|
-
var camZ = player.pos.z + Math.cos(yaw) * Math.cos(pitch) * dist;
|
|
592
|
-
var camY = baseY + 1 + Math.sin(pitch) * dist;
|
|
593
|
-
|
|
594
|
-
_tmpCamTarget.set(camX, camY, camZ);
|
|
595
|
-
|
|
596
|
-
// Smooth follow
|
|
597
|
-
S.camera.position.lerp(_tmpCamTarget, Math.min(1, dt * 6));
|
|
598
|
-
|
|
599
|
-
// Look at player chest (offset by player height)
|
|
600
|
-
_tmpLookAt.set(
|
|
601
|
-
player.pos.x,
|
|
602
|
-
baseY + CAMERA_LOOK_OFFSET.y,
|
|
603
|
-
player.pos.z + CAMERA_LOOK_OFFSET.z
|
|
604
|
-
);
|
|
605
|
-
S.camera.lookAt(_tmpLookAt);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Desk POV camera — when player is sitting, camera moves behind the character looking at the monitor
|
|
609
|
-
function updatePlayerCameraDesk(dt) {
|
|
610
|
-
var player = S._player;
|
|
611
|
-
if (!player || !player._sitTarget) return;
|
|
612
|
-
|
|
613
|
-
// Camera behind player's head, looking at the desk/monitor
|
|
614
|
-
var deskX = player._sitTarget.x;
|
|
615
|
-
var deskZ = player._sitTarget.z;
|
|
616
|
-
var baseY = player.pos.y;
|
|
617
|
-
|
|
618
|
-
// Camera position: slightly behind and above the seated player
|
|
619
|
-
_tmpCamTarget.set(deskX, baseY + 1.6, deskZ + 1.8);
|
|
620
|
-
S.camera.position.lerp(_tmpCamTarget, Math.min(1, dt * 4));
|
|
621
|
-
|
|
622
|
-
// Look at the monitor (desk is at z, monitor is slightly behind at z - 0.3)
|
|
623
|
-
_tmpLookAt.set(deskX, baseY + 1.2, deskZ - 0.5);
|
|
624
|
-
S.camera.lookAt(_tmpLookAt);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// --- Public API for iframe integration ---
|
|
628
|
-
export function isPlayerSitting() {
|
|
629
|
-
return S._player && S._player.sitting;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
export function getPlayerDeskIdx() {
|
|
633
|
-
return S._player ? S._player.sittingDeskIdx : -1;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Force stand from external code (LEAVE button, Escape key handler in iframe overlay)
|
|
637
|
-
// Sets sitting=false so WASD movement resumes and camera returns to third-person
|
|
638
|
-
window.playerForceStand = function() {
|
|
639
|
-
var player = S._player;
|
|
640
|
-
if (player && player.sitting) {
|
|
641
|
-
// Push player AWAY from desk center (works for all desk orientations)
|
|
642
|
-
var desks = S._campusDeskPositions || [];
|
|
643
|
-
var dIdx = player.sittingDeskIdx;
|
|
644
|
-
player.sitting = false;
|
|
645
|
-
player.sittingDeskIdx = -1;
|
|
646
|
-
if (player._sitPrompt) player._sitPrompt.style.display = 'none';
|
|
647
|
-
if (dIdx >= 0 && desks[dIdx]) {
|
|
648
|
-
// Always stand up FORWARD (toward camera/door side, negative Z in desk-local space)
|
|
649
|
-
// This avoids pushing into walls behind the desk
|
|
650
|
-
player.pos.z = desks[dIdx].z + 1.2; // 1.2 units in front of desk center
|
|
651
|
-
// Slight X offset to avoid chair mesh
|
|
652
|
-
var ddx = player.pos.x - desks[dIdx].x;
|
|
653
|
-
if (Math.abs(ddx) < 0.3) player.pos.x += 0.4;
|
|
654
|
-
} else {
|
|
655
|
-
player.pos.z += 0.8; // fallback
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
};
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { S } from './state.js';
|
|
3
|
+
import { createCharacter } from './character.js';
|
|
4
|
+
import { resolveAppearance } from './appearance.js';
|
|
5
|
+
// ============================================================
|
|
6
|
+
// PLAYER AVATAR — Walk around the 3D world as a character
|
|
7
|
+
// Not an agent — no MCP, no messages — just visual presence
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
var PLAYER_SPEED = 3;
|
|
11
|
+
var CAMERA_OFFSET = new THREE.Vector3(0, 3, 5); // behind + above
|
|
12
|
+
var CAMERA_LOOK_OFFSET = new THREE.Vector3(0, 1.2, 0); // look at chest height
|
|
13
|
+
var PLAYER_RADIUS = 0.35; // collision radius
|
|
14
|
+
|
|
15
|
+
// Reusable Vector3s (avoid per-frame allocation)
|
|
16
|
+
var _tmpForward = new THREE.Vector3();
|
|
17
|
+
var _tmpRight = new THREE.Vector3();
|
|
18
|
+
var _tmpDir = new THREE.Vector3();
|
|
19
|
+
var _tmpCamTarget = new THREE.Vector3();
|
|
20
|
+
var _tmpLookAt = new THREE.Vector3();
|
|
21
|
+
var _tmpTargetPos = new THREE.Vector3();
|
|
22
|
+
|
|
23
|
+
// ==================== COLLISION SYSTEM ====================
|
|
24
|
+
// AABB boxes: { minX, maxX, minZ, maxZ }
|
|
25
|
+
// Thin walls as boxes with small thickness
|
|
26
|
+
|
|
27
|
+
function getCampusColliders() {
|
|
28
|
+
var W = 50, D = 35;
|
|
29
|
+
var colliders = [
|
|
30
|
+
// Building walls (0.3 thick)
|
|
31
|
+
{ minX: -W/2 - 0.3, maxX: -W/2, minZ: -D/2, maxZ: D/2 }, // left wall
|
|
32
|
+
{ minX: W/2, maxX: W/2 + 0.3, minZ: -D/2, maxZ: D/2 }, // right wall
|
|
33
|
+
{ minX: -W/2, maxX: W/2, minZ: -D/2 - 0.3, maxZ: -D/2 }, // back wall
|
|
34
|
+
// Front wall with entrance gap (gap at x: -4 to 4)
|
|
35
|
+
{ minX: -W/2, maxX: -4, minZ: D/2, maxZ: D/2 + 0.3 },
|
|
36
|
+
{ minX: 4, maxX: W/2, minZ: D/2, maxZ: D/2 + 0.3 },
|
|
37
|
+
|
|
38
|
+
// Manager office walls: group at (12,5), offW=8, offD=7
|
|
39
|
+
// Left wall (x=8): z from 1.5 to 8.5
|
|
40
|
+
{ minX: 7.85, maxX: 8.15, minZ: 1.5, maxZ: 8.5 },
|
|
41
|
+
// Right wall (x=16): z from 1.5 to 8.5
|
|
42
|
+
{ minX: 15.85, maxX: 16.15, minZ: 1.5, maxZ: 8.5 },
|
|
43
|
+
// Back wall (z=8.5): x from 8 to 16
|
|
44
|
+
{ minX: 8, maxX: 16, minZ: 8.35, maxZ: 8.65 },
|
|
45
|
+
// Front wall left of door (z=1.5, x from 8 to ~11.4)
|
|
46
|
+
{ minX: 8, maxX: 11.4, minZ: 1.35, maxZ: 1.65 },
|
|
47
|
+
// Front wall right of door (z=1.5, x from ~12.6 to 16)
|
|
48
|
+
{ minX: 12.6, maxX: 16, minZ: 1.35, maxZ: 1.65 },
|
|
49
|
+
// Door collider (only active when closed) — handled dynamically below
|
|
50
|
+
|
|
51
|
+
// Glass partition between workspace and rec (z=-7, gap at x=-1 to 1)
|
|
52
|
+
{ minX: -7, maxX: -1, minZ: -7.15, maxZ: -6.85 },
|
|
53
|
+
{ minX: 1, maxX: 7, minZ: -7.15, maxZ: -6.85 },
|
|
54
|
+
|
|
55
|
+
// Glass partition designer/main (x=-8, gap at z=2 to 4)
|
|
56
|
+
{ minX: -8.15, maxX: -7.85, minZ: -5, maxZ: 2 },
|
|
57
|
+
{ minX: -8.15, maxX: -7.85, minZ: 4, maxZ: 7 },
|
|
58
|
+
|
|
59
|
+
// Reception desk (ground floor)
|
|
60
|
+
{ minX: -2.2, maxX: 2.2, minZ: 13.5, maxZ: 15, floor: 'ground' },
|
|
61
|
+
// Reception logo wall
|
|
62
|
+
{ minX: -3, maxX: 3, minZ: 15.5, maxZ: 16, floor: 'ground' },
|
|
63
|
+
// Water feature
|
|
64
|
+
{ minX: -1.5, maxX: 1.5, minZ: 9.5, maxZ: 10.5, floor: 'ground' },
|
|
65
|
+
|
|
66
|
+
// Bar counter (ground floor)
|
|
67
|
+
{ minX: -17, maxX: -11, minZ: -12.7, maxZ: -11.3, floor: 'ground' },
|
|
68
|
+
|
|
69
|
+
// Pool table (ground floor)
|
|
70
|
+
{ minX: -3.3, maxX: -0.7, minZ: -12.7, maxZ: -11.3, floor: 'ground' },
|
|
71
|
+
// Foosball (ground floor)
|
|
72
|
+
{ minX: 1.8, maxX: 3.2, minZ: -12.4, maxZ: -11.6, floor: 'ground' },
|
|
73
|
+
|
|
74
|
+
// Mezzanine support columns (thin cylinders, approximate as small boxes)
|
|
75
|
+
// Columns at x: -18,-9,0,9,18 z: -CAMPUS_D/2 + MEZZ_DEPTH = -17.5+12 = -5.5
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Desk colliders (gaming desks)
|
|
79
|
+
var CAMPUS_DESKS = [
|
|
80
|
+
{ x: -4.5, z: 2 }, { x: -1.5, z: 2 }, { x: 1.5, z: 2 }, { x: 4.5, z: 2 },
|
|
81
|
+
{ x: -4.5, z: -1 }, { x: -1.5, z: -1 }, { x: 1.5, z: -1 }, { x: 4.5, z: -1 },
|
|
82
|
+
{ x: -4.5, z: -4 }, { x: -1.5, z: -4 }, { x: 1.5, z: -4 }, { x: 4.5, z: -4 },
|
|
83
|
+
{ x: -14, z: 1 }, { x: -11, z: 1 },
|
|
84
|
+
{ x: -14, z: -2 }, { x: -11, z: -2 },
|
|
85
|
+
];
|
|
86
|
+
CAMPUS_DESKS.forEach(function(d) {
|
|
87
|
+
// Desk body only — chair area excluded so player can stand up without getting stuck
|
|
88
|
+
colliders.push({ minX: d.x - 1.1, maxX: d.x + 1.1, minZ: d.z - 0.5, maxZ: d.z + 0.3, floor: 'ground' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Manager's desk inside office — chair side excluded (ground floor)
|
|
92
|
+
colliders.push({ minX: 10.5, maxX: 14.5, minZ: 5.5, maxZ: 6.8, floor: 'ground' });
|
|
93
|
+
|
|
94
|
+
// Bar counter (ground floor)
|
|
95
|
+
// Pool table, foosball (ground floor) — already added above without tag, let me not duplicate
|
|
96
|
+
|
|
97
|
+
// Reception area (ground floor)
|
|
98
|
+
// Already added above
|
|
99
|
+
|
|
100
|
+
return colliders;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getModernColliders() {
|
|
104
|
+
// Simple colliders for the old modern/startup office
|
|
105
|
+
var colliders = [
|
|
106
|
+
{ minX: -14.3, maxX: -14, minZ: -8, maxZ: 8 }, // left wall
|
|
107
|
+
{ minX: 14, maxX: 14.3, minZ: -8, maxZ: 8 }, // right wall
|
|
108
|
+
{ minX: -14, maxX: 14, minZ: -8.3, maxZ: -8 }, // back wall
|
|
109
|
+
{ minX: -14, maxX: 14, minZ: 8, maxZ: 8.3 }, // front wall
|
|
110
|
+
];
|
|
111
|
+
return colliders;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var _cachedColliders = null;
|
|
115
|
+
var _cachedCollidersEnv = null;
|
|
116
|
+
|
|
117
|
+
function getColliders() {
|
|
118
|
+
if (_cachedCollidersEnv === S.currentEnv && _cachedColliders) return _cachedColliders;
|
|
119
|
+
_cachedCollidersEnv = S.currentEnv;
|
|
120
|
+
if (S.currentEnv === 'campus') {
|
|
121
|
+
_cachedColliders = getCampusColliders();
|
|
122
|
+
} else {
|
|
123
|
+
_cachedColliders = getModernColliders();
|
|
124
|
+
}
|
|
125
|
+
return _cachedColliders;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if a circle (player) at (x,z) with radius r collides with any AABB
|
|
129
|
+
function checkCollision(x, z, r) {
|
|
130
|
+
var playerY = S._player ? S._player.pos.y : 0;
|
|
131
|
+
var onMezzanine = playerY > MEZZ_HEIGHT * 0.4;
|
|
132
|
+
var onStairs = S._player && x >= STAIR_X_MIN && x <= STAIR_X_MAX && z <= STAIR_Z_BOTTOM && z >= STAIR_Z_TOP;
|
|
133
|
+
|
|
134
|
+
var colliders = getColliders();
|
|
135
|
+
for (var i = 0; i < colliders.length; i++) {
|
|
136
|
+
var c = colliders[i];
|
|
137
|
+
// Skip ground-floor furniture colliders when on mezzanine (desks etc)
|
|
138
|
+
if (onMezzanine && c.floor === 'ground') continue;
|
|
139
|
+
// Skip mezzanine colliders when on ground floor
|
|
140
|
+
if (!onMezzanine && !onStairs && c.floor === 'upper') continue;
|
|
141
|
+
var cx = Math.max(c.minX, Math.min(x, c.maxX));
|
|
142
|
+
var cz = Math.max(c.minZ, Math.min(z, c.maxZ));
|
|
143
|
+
var dx = x - cx, dz = z - cz;
|
|
144
|
+
if (dx * dx + dz * dz < r * r) return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Mezzanine railing — blocks walking off the edge (only when on mezzanine)
|
|
148
|
+
if (onMezzanine && !onStairs) {
|
|
149
|
+
// Front edge of mezzanine at z = -5.5 (except staircase gap at x 18.75-21.25)
|
|
150
|
+
if (z > -5.7 && z < -5.3 && !(x >= STAIR_X_MIN - 0.5 && x <= STAIR_X_MAX + 0.5)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Dynamic: manager door (closed = collider, open = passable)
|
|
156
|
+
if (S.currentEnv === 'campus' && S._managerDoorLerp < 0.5 && !onMezzanine) {
|
|
157
|
+
var doorBox = { minX: 11.4, maxX: 12.6, minZ: 1.35, maxZ: 1.65 };
|
|
158
|
+
var dcx = Math.max(doorBox.minX, Math.min(x, doorBox.maxX));
|
|
159
|
+
var dcz = Math.max(doorBox.minZ, Math.min(z, doorBox.maxZ));
|
|
160
|
+
var ddx = x - dcx, ddz = z - dcz;
|
|
161
|
+
if (ddx * ddx + ddz * ddz < r * r) return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check collision with agents (circle vs circle)
|
|
167
|
+
function checkAgentCollision(x, z, r) {
|
|
168
|
+
for (var name in S.agents3d) {
|
|
169
|
+
var ag = S.agents3d[name];
|
|
170
|
+
if (!ag.registered || ag.dying) continue;
|
|
171
|
+
var dx = x - ag.pos.x, dz = z - ag.pos.z;
|
|
172
|
+
if (dx * dx + dz * dz < (r + 0.3) * (r + 0.3)) return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Resolve movement with sliding collision (try X and Z independently)
|
|
178
|
+
function resolveMovement(oldX, oldZ, newX, newZ, r) {
|
|
179
|
+
// Try full movement
|
|
180
|
+
if (!checkCollision(newX, newZ, r) && !checkAgentCollision(newX, newZ, r)) {
|
|
181
|
+
return { x: newX, z: newZ };
|
|
182
|
+
}
|
|
183
|
+
// Try sliding along X
|
|
184
|
+
if (!checkCollision(newX, oldZ, r) && !checkAgentCollision(newX, oldZ, r)) {
|
|
185
|
+
return { x: newX, z: oldZ };
|
|
186
|
+
}
|
|
187
|
+
// Try sliding along Z
|
|
188
|
+
if (!checkCollision(oldX, newZ, r) && !checkAgentCollision(oldX, newZ, r)) {
|
|
189
|
+
return { x: oldX, z: newZ };
|
|
190
|
+
}
|
|
191
|
+
// Stuck — don't move
|
|
192
|
+
return { x: oldX, z: oldZ };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ==================== HEIGHT SYSTEM ====================
|
|
196
|
+
// Staircase: x 18.75-21.25, z -3.5 (bottom, y=0) to -9.5 (top, y=3.2)
|
|
197
|
+
// Mezzanine: y=3.2, z from -17.5 to -5.5, full width
|
|
198
|
+
var STAIR_X_MIN = 18.75, STAIR_X_MAX = 21.25;
|
|
199
|
+
var STAIR_Z_BOTTOM = -3.5, STAIR_Z_TOP = -9.5;
|
|
200
|
+
var MEZZ_HEIGHT = 3.2;
|
|
201
|
+
var MEZZ_Z_BACK = -17.5, MEZZ_Z_FRONT = -5.5;
|
|
202
|
+
|
|
203
|
+
function getGroundHeight(x, z, currentY) {
|
|
204
|
+
if (S.currentEnv !== 'campus') return 0;
|
|
205
|
+
|
|
206
|
+
// On the staircase?
|
|
207
|
+
if (x >= STAIR_X_MIN && x <= STAIR_X_MAX && z <= STAIR_Z_BOTTOM && z >= STAIR_Z_TOP) {
|
|
208
|
+
// Interpolate height: bottom (z=-3.5, y=0) to top (z=-9.5, y=3.2)
|
|
209
|
+
var t = (STAIR_Z_BOTTOM - z) / (STAIR_Z_BOTTOM - STAIR_Z_TOP);
|
|
210
|
+
t = Math.max(0, Math.min(1, t));
|
|
211
|
+
return t * MEZZ_HEIGHT;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// On the mezzanine? (must have come from stairs — check if currentY > halfway)
|
|
215
|
+
if (currentY > MEZZ_HEIGHT * 0.4 && z <= MEZZ_Z_FRONT && z >= MEZZ_Z_BACK) {
|
|
216
|
+
return MEZZ_HEIGHT;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ground level
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Invalidate collider cache on env switch
|
|
224
|
+
export function invalidateColliders() {
|
|
225
|
+
_cachedColliders = null;
|
|
226
|
+
_cachedCollidersEnv = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function spawnPlayer() {
|
|
230
|
+
if (S._player) despawnPlayer();
|
|
231
|
+
|
|
232
|
+
// Load appearance from localStorage
|
|
233
|
+
var appearance = {};
|
|
234
|
+
try {
|
|
235
|
+
var stored = localStorage.getItem('ltt_player_appearance');
|
|
236
|
+
if (stored) appearance = JSON.parse(stored);
|
|
237
|
+
} catch (e) {}
|
|
238
|
+
|
|
239
|
+
var parts = createCharacter('Player', appearance);
|
|
240
|
+
parts.group.position.set(0, 0, 12); // spawn at lobby
|
|
241
|
+
|
|
242
|
+
// Remove typing dots and task indicator (player doesn't need them)
|
|
243
|
+
parts.typingLabel.visible = false;
|
|
244
|
+
parts.taskLabel.visible = false;
|
|
245
|
+
|
|
246
|
+
// Update name label
|
|
247
|
+
var nameEl = parts.labelDiv.querySelector('.office3d-label-name');
|
|
248
|
+
var dotEl = parts.labelDiv.querySelector('.office3d-label-dot');
|
|
249
|
+
if (nameEl) nameEl.textContent = 'You';
|
|
250
|
+
if (dotEl) dotEl.style.background = '#58a6ff';
|
|
251
|
+
|
|
252
|
+
S.scene.add(parts.group);
|
|
253
|
+
|
|
254
|
+
S._player = {
|
|
255
|
+
parts: parts,
|
|
256
|
+
pos: { x: 0, y: 0, z: 12 },
|
|
257
|
+
facing: 0, // radians, 0 = +z direction
|
|
258
|
+
velocity: { x: 0, z: 0 },
|
|
259
|
+
isMoving: false,
|
|
260
|
+
appearance: appearance,
|
|
261
|
+
camYaw: Math.PI, // camera orbit angle around player (horizontal)
|
|
262
|
+
camPitch: 0.4, // camera pitch (vertical angle, 0=level, positive=looking down)
|
|
263
|
+
camDist: 6, // distance from player
|
|
264
|
+
_jumping: false,
|
|
265
|
+
_jumpVel: 0,
|
|
266
|
+
_jumpY: 0,
|
|
267
|
+
_landSquash: 0,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Disable spectator camera movement but keep key/mouse tracking alive
|
|
271
|
+
if (S.controls) {
|
|
272
|
+
S.controls.enabled = false;
|
|
273
|
+
S.controls._playerZoomCb = function(deltaY) {
|
|
274
|
+
if (S._player) {
|
|
275
|
+
S._player.camDist = Math.max(2, Math.min(15, S._player.camDist + deltaY * 0.01));
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return S._player;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function despawnPlayer() {
|
|
284
|
+
if (!S._player) return;
|
|
285
|
+
// Clean up "Press E to sit" prompt so it doesn't leak to other tabs
|
|
286
|
+
if (S._player._sitPrompt) {
|
|
287
|
+
S._player._sitPrompt.style.display = 'none';
|
|
288
|
+
if (S._player._sitPrompt.parentElement) S._player._sitPrompt.remove();
|
|
289
|
+
S._player._sitPrompt = null;
|
|
290
|
+
}
|
|
291
|
+
S.scene.remove(S._player.parts.group);
|
|
292
|
+
S._player.parts.group.traverse(function(child) {
|
|
293
|
+
if (child.geometry) child.geometry.dispose();
|
|
294
|
+
if (child.material) {
|
|
295
|
+
if (child.material.map) child.material.map.dispose();
|
|
296
|
+
child.material.dispose();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
S._player = null;
|
|
300
|
+
|
|
301
|
+
// Re-enable spectator camera
|
|
302
|
+
if (S.controls) {
|
|
303
|
+
S.controls.enabled = true;
|
|
304
|
+
S.controls._playerZoomCb = null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function isPlayerMode() {
|
|
309
|
+
return !!S._player;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function getPlayer() {
|
|
313
|
+
return S._player;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function savePlayerAppearance(appearance) {
|
|
317
|
+
try {
|
|
318
|
+
localStorage.setItem('ltt_player_appearance', JSON.stringify(appearance));
|
|
319
|
+
} catch (e) {}
|
|
320
|
+
if (S._player) S._player.appearance = appearance;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function getPlayerAppearance() {
|
|
324
|
+
try {
|
|
325
|
+
var stored = localStorage.getItem('ltt_player_appearance');
|
|
326
|
+
if (stored) return JSON.parse(stored);
|
|
327
|
+
} catch (e) {}
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Called every frame from the animation loop
|
|
332
|
+
export function updatePlayer(dt, time, keys) {
|
|
333
|
+
var player = S._player;
|
|
334
|
+
if (!player) return;
|
|
335
|
+
|
|
336
|
+
// --- Movement from keyboard ---
|
|
337
|
+
var moveX = 0, moveZ = 0;
|
|
338
|
+
if (keys['KeyW'] || keys['ArrowUp']) moveZ -= 1;
|
|
339
|
+
if (keys['KeyS'] || keys['ArrowDown']) moveZ += 1;
|
|
340
|
+
if (keys['KeyA'] || keys['ArrowLeft']) moveX -= 1;
|
|
341
|
+
if (keys['KeyD'] || keys['ArrowRight']) moveX += 1;
|
|
342
|
+
|
|
343
|
+
var isMoving = moveX !== 0 || moveZ !== 0;
|
|
344
|
+
player.isMoving = isMoving;
|
|
345
|
+
|
|
346
|
+
// --- Jump with Space ---
|
|
347
|
+
if (keys['Space'] && !player.sitting && !player._jumping) {
|
|
348
|
+
player._jumping = true;
|
|
349
|
+
player._jumpVel = 5.5; // initial upward velocity
|
|
350
|
+
player._jumpY = 0;
|
|
351
|
+
}
|
|
352
|
+
if (player._jumping) {
|
|
353
|
+
player._jumpVel -= 18 * dt; // gravity
|
|
354
|
+
player._jumpY += player._jumpVel * dt;
|
|
355
|
+
if (player._jumpY <= 0) {
|
|
356
|
+
player._jumpY = 0;
|
|
357
|
+
player._jumping = false;
|
|
358
|
+
player._jumpVel = 0;
|
|
359
|
+
player._landSquash = 0.3; // trigger landing squash
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Landing squash/stretch decay
|
|
363
|
+
if (player._landSquash > 0) {
|
|
364
|
+
player._landSquash = Math.max(0, player._landSquash - dt * 3);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (isMoving) {
|
|
368
|
+
// Movement relative to camera yaw (orbit angle)
|
|
369
|
+
var camYaw = S.controls && S.controls._euler ? S.controls._euler.y : 0;
|
|
370
|
+
_tmpForward.set(-Math.sin(camYaw), 0, -Math.cos(camYaw)).normalize();
|
|
371
|
+
_tmpRight.set(-_tmpForward.z, 0, _tmpForward.x);
|
|
372
|
+
|
|
373
|
+
_tmpDir.set(0, 0, 0);
|
|
374
|
+
_tmpDir.addScaledVector(_tmpForward, -moveZ);
|
|
375
|
+
_tmpDir.addScaledVector(_tmpRight, moveX);
|
|
376
|
+
_tmpDir.normalize();
|
|
377
|
+
|
|
378
|
+
var speed = PLAYER_SPEED * (keys['ShiftLeft'] || keys['ShiftRight'] ? 2 : 1);
|
|
379
|
+
var newX = player.pos.x + _tmpDir.x * speed * dt;
|
|
380
|
+
var newZ = player.pos.z + _tmpDir.z * speed * dt;
|
|
381
|
+
|
|
382
|
+
// Collision resolution (sliding)
|
|
383
|
+
var resolved = resolveMovement(player.pos.x, player.pos.z, newX, newZ, PLAYER_RADIUS);
|
|
384
|
+
player.pos.x = resolved.x;
|
|
385
|
+
player.pos.z = resolved.z;
|
|
386
|
+
|
|
387
|
+
// Face movement direction
|
|
388
|
+
player.facing = Math.atan2(_tmpDir.x, _tmpDir.z);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Update character position
|
|
392
|
+
// Update height based on position (stairs/mezzanine)
|
|
393
|
+
var targetY = getGroundHeight(player.pos.x, player.pos.z, player.pos.y);
|
|
394
|
+
player.pos.y += (targetY - player.pos.y) * Math.min(1, dt * 8); // smooth height transition
|
|
395
|
+
|
|
396
|
+
player.parts.group.position.x = player.pos.x;
|
|
397
|
+
player.parts.group.position.y = player.pos.y + (player._jumpY || 0);
|
|
398
|
+
player.parts.group.position.z = player.pos.z;
|
|
399
|
+
|
|
400
|
+
// Jump squash/stretch visual
|
|
401
|
+
var jumpSquash = player._landSquash || 0;
|
|
402
|
+
if (player._jumping && player._jumpVel > 0) {
|
|
403
|
+
// Stretch upward during ascent
|
|
404
|
+
player.parts.body.scale.set(0.9, 1.15, 0.9);
|
|
405
|
+
} else if (jumpSquash > 0) {
|
|
406
|
+
// Squash on landing — wide and short
|
|
407
|
+
var sq = 1 + jumpSquash * 0.5; // width: up to 1.15
|
|
408
|
+
var sy = 1 - jumpSquash * 0.4; // height: down to 0.88
|
|
409
|
+
player.parts.body.scale.set(sq, sy, sq);
|
|
410
|
+
} else if (!isMoving) {
|
|
411
|
+
// Reset to breathing (handled below)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Smooth rotation
|
|
415
|
+
var currentRot = player.parts.group.rotation.y;
|
|
416
|
+
var diff = player.facing - currentRot;
|
|
417
|
+
while (diff > Math.PI) diff -= Math.PI * 2;
|
|
418
|
+
while (diff < -Math.PI) diff += Math.PI * 2;
|
|
419
|
+
player.parts.group.rotation.y += diff * Math.min(1, dt * 8);
|
|
420
|
+
|
|
421
|
+
// --- Walking / Jump / Idle animation ---
|
|
422
|
+
if (player._jumping) {
|
|
423
|
+
// Jump pose — tuck legs, raise arms
|
|
424
|
+
var tuck = player._jumpVel > 0 ? 0.6 : 0.3; // more tuck on ascent
|
|
425
|
+
player.parts.leftLeg.rotation.x = -tuck;
|
|
426
|
+
player.parts.rightLeg.rotation.x = -tuck;
|
|
427
|
+
player.parts.leftLowerLeg.rotation.x = tuck * 1.2;
|
|
428
|
+
player.parts.rightLowerLeg.rotation.x = tuck * 1.2;
|
|
429
|
+
player.parts.leftArm.rotation.x = -1.2; // arms up
|
|
430
|
+
player.parts.rightArm.rotation.x = -1.2;
|
|
431
|
+
player.parts.leftForearm.rotation.x = -0.5;
|
|
432
|
+
player.parts.rightForearm.rotation.x = -0.5;
|
|
433
|
+
} else if (isMoving) {
|
|
434
|
+
player.parts.body.scale.set(1, 1, 1); // reset from jump squash
|
|
435
|
+
var swing = Math.sin(time * 10) * 0.5;
|
|
436
|
+
player.parts.leftLeg.rotation.x = swing;
|
|
437
|
+
player.parts.rightLeg.rotation.x = -swing;
|
|
438
|
+
player.parts.leftLowerLeg.rotation.x = Math.max(0, -swing) * 0.8;
|
|
439
|
+
player.parts.rightLowerLeg.rotation.x = Math.max(0, swing) * 0.8;
|
|
440
|
+
player.parts.leftArm.rotation.x = -swing * 0.7;
|
|
441
|
+
player.parts.rightArm.rotation.x = swing * 0.7;
|
|
442
|
+
player.parts.leftForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
|
|
443
|
+
player.parts.rightForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
|
|
444
|
+
} else {
|
|
445
|
+
// Idle — breathing + slight head bob
|
|
446
|
+
player.parts.leftLeg.rotation.x *= 0.9;
|
|
447
|
+
player.parts.rightLeg.rotation.x *= 0.9;
|
|
448
|
+
player.parts.leftArm.rotation.x *= 0.9;
|
|
449
|
+
player.parts.rightArm.rotation.x *= 0.9;
|
|
450
|
+
player.parts.leftLowerLeg.rotation.x *= 0.9;
|
|
451
|
+
player.parts.rightLowerLeg.rotation.x *= 0.9;
|
|
452
|
+
player.parts.leftForearm.rotation.x *= 0.9;
|
|
453
|
+
player.parts.rightForearm.rotation.x *= 0.9;
|
|
454
|
+
var breathe = 1 + Math.sin(time * 2) * 0.02;
|
|
455
|
+
if (!player._landSquash) player.parts.body.scale.set(1, breathe, 1);
|
|
456
|
+
player.parts.head.rotation.z = Math.sin(time * 0.5) * 0.03;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// --- E-to-sit at desk system ---
|
|
460
|
+
var desks = S._campusDeskPositions || [];
|
|
461
|
+
var SIT_RANGE = 2.5; // max distance to sit at a desk
|
|
462
|
+
var nearestDesk = -1;
|
|
463
|
+
var nearestDist = SIT_RANGE;
|
|
464
|
+
|
|
465
|
+
if (!player.sitting) {
|
|
466
|
+
// Find nearest desk
|
|
467
|
+
for (var di = 0; di < desks.length; di++) {
|
|
468
|
+
var ddx = player.pos.x - desks[di].x;
|
|
469
|
+
var ddz = player.pos.z - (desks[di].z + 0.7); // chair is 0.7 in front of desk
|
|
470
|
+
var dd = Math.sqrt(ddx * ddx + ddz * ddz);
|
|
471
|
+
if (dd < nearestDist) { nearestDist = dd; nearestDesk = di; }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Show/hide "Press E to sit" prompt
|
|
476
|
+
if (nearestDesk >= 0 && !player.sitting) {
|
|
477
|
+
if (!player._sitPrompt) {
|
|
478
|
+
player._sitPrompt = document.createElement('div');
|
|
479
|
+
player._sitPrompt.className = 'office3d-sit-prompt';
|
|
480
|
+
player._sitPrompt.textContent = 'Press E to sit';
|
|
481
|
+
player._sitPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#58a6ff;padding:8px 16px;border-radius:8px;font-size:14px;z-index:1000;pointer-events:none;border:1px solid #30363d;';
|
|
482
|
+
document.body.appendChild(player._sitPrompt);
|
|
483
|
+
}
|
|
484
|
+
player._sitPrompt.style.display = 'block';
|
|
485
|
+
player._nearDesk = nearestDesk;
|
|
486
|
+
} else if (player._sitPrompt && !player.sitting) {
|
|
487
|
+
player._sitPrompt.style.display = 'none';
|
|
488
|
+
player._nearDesk = -1;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// E key: sit down or stand up
|
|
492
|
+
if (keys['KeyE'] && !player._ePressed) {
|
|
493
|
+
player._ePressed = true;
|
|
494
|
+
if (!player.sitting && player._nearDesk >= 0) {
|
|
495
|
+
// Sit down at desk
|
|
496
|
+
player.sitting = true;
|
|
497
|
+
player.sittingDeskIdx = player._nearDesk;
|
|
498
|
+
player.sittingLerp = 0;
|
|
499
|
+
var deskPos = desks[player._nearDesk];
|
|
500
|
+
player._sitTarget = { x: deskPos.x, y: player.pos.y, z: deskPos.z + 0.7 };
|
|
501
|
+
if (player._sitPrompt) player._sitPrompt.style.display = 'none';
|
|
502
|
+
// Notify: player sat down (Builder's iframe hook)
|
|
503
|
+
if (typeof window.onPlayerSit === 'function') window.onPlayerSit(player.sittingDeskIdx);
|
|
504
|
+
} else if (player.sitting) {
|
|
505
|
+
// Stand up — push player AWAY from desk center to avoid collision trapping
|
|
506
|
+
var desks = S._campusDeskPositions || [];
|
|
507
|
+
var dIdx = player.sittingDeskIdx;
|
|
508
|
+
player.sitting = false;
|
|
509
|
+
player.sittingDeskIdx = -1;
|
|
510
|
+
if (dIdx >= 0 && desks[dIdx]) {
|
|
511
|
+
var ddx = player.pos.x - desks[dIdx].x;
|
|
512
|
+
var ddz = player.pos.z - desks[dIdx].z;
|
|
513
|
+
var dd = Math.sqrt(ddx * ddx + ddz * ddz) || 1;
|
|
514
|
+
player.pos.x += (ddx / dd) * 0.8;
|
|
515
|
+
player.pos.z += (ddz / dd) * 0.8;
|
|
516
|
+
} else {
|
|
517
|
+
player.pos.z += 0.8; // fallback
|
|
518
|
+
}
|
|
519
|
+
// Notify: player stood up
|
|
520
|
+
if (typeof window.onPlayerStand === 'function') window.onPlayerStand();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (!keys['KeyE']) player._ePressed = false;
|
|
524
|
+
|
|
525
|
+
// --- Jukebox E-to-interact ---
|
|
526
|
+
if (S._jukebox && !player.sitting) {
|
|
527
|
+
var jbx = player.pos.x - S._jukebox.pos.x;
|
|
528
|
+
var jbz = player.pos.z - S._jukebox.pos.z;
|
|
529
|
+
var jbDist = Math.sqrt(jbx * jbx + jbz * jbz);
|
|
530
|
+
if (jbDist < 2.0) {
|
|
531
|
+
// Show prompt
|
|
532
|
+
if (!player._jukeboxPrompt) {
|
|
533
|
+
player._jukeboxPrompt = document.createElement('div');
|
|
534
|
+
player._jukeboxPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#ff4488;padding:8px 16px;border-radius:8px;font-size:14px;z-index:1000;pointer-events:none;border:1px solid #ff4488;';
|
|
535
|
+
player._jukeboxPrompt.textContent = 'Press E for Jukebox';
|
|
536
|
+
document.body.appendChild(player._jukeboxPrompt);
|
|
537
|
+
}
|
|
538
|
+
player._jukeboxPrompt.style.display = 'block';
|
|
539
|
+
// E key activates jukebox (only if not near a desk)
|
|
540
|
+
if (keys['KeyE'] && player._ePressed && nearestDesk < 0) {
|
|
541
|
+
if (typeof window.onJukeboxInteract === 'function') window.onJukeboxInteract();
|
|
542
|
+
if (player._jukeboxPrompt) player._jukeboxPrompt.style.display = 'none';
|
|
543
|
+
}
|
|
544
|
+
} else if (player._jukeboxPrompt) {
|
|
545
|
+
player._jukeboxPrompt.style.display = 'none';
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Sitting animation: lerp position to chair, set sitting pose
|
|
550
|
+
if (player.sitting && player._sitTarget) {
|
|
551
|
+
player.sittingLerp = Math.min(1, (player.sittingLerp || 0) + dt * 3);
|
|
552
|
+
var sl = player.sittingLerp;
|
|
553
|
+
player.pos.x += (player._sitTarget.x - player.pos.x) * Math.min(1, dt * 5);
|
|
554
|
+
player.pos.z += (player._sitTarget.z - player.pos.z) * Math.min(1, dt * 5);
|
|
555
|
+
player.facing = Math.PI; // face the desk
|
|
556
|
+
|
|
557
|
+
// Sitting pose (same as agent sitting)
|
|
558
|
+
var sitHip = -1.5 * sl;
|
|
559
|
+
player.parts.leftLeg.rotation.x = sitHip;
|
|
560
|
+
player.parts.rightLeg.rotation.x = sitHip;
|
|
561
|
+
player.parts.leftLowerLeg.rotation.x = 1.5 * sl;
|
|
562
|
+
player.parts.rightLowerLeg.rotation.x = 1.5 * sl;
|
|
563
|
+
player.parts.leftForearm.rotation.x = -0.4 * sl;
|
|
564
|
+
player.parts.rightForearm.rotation.x = -0.4 * sl;
|
|
565
|
+
player.parts.group.position.y = player.pos.y + sl * 0.14;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// --- Third-person camera follow (or desk POV when sitting) ---
|
|
569
|
+
if (player.sitting) {
|
|
570
|
+
updatePlayerCameraDesk(dt);
|
|
571
|
+
} else {
|
|
572
|
+
updatePlayerCamera(dt);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function updatePlayerCamera(dt) {
|
|
577
|
+
var player = S._player;
|
|
578
|
+
if (!player) return;
|
|
579
|
+
|
|
580
|
+
// Use spectator camera's euler for orbit angle (from mouse right-drag)
|
|
581
|
+
var yaw = 0, pitch = 0.4;
|
|
582
|
+
if (S.controls && S.controls._euler) {
|
|
583
|
+
yaw = S.controls._euler.y;
|
|
584
|
+
pitch = Math.max(0.05, Math.min(1.3, -S.controls._euler.x));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Orbit camera around the player using yaw/pitch (+ player height)
|
|
588
|
+
var dist = player.camDist;
|
|
589
|
+
var baseY = player.pos.y;
|
|
590
|
+
var camX = player.pos.x + Math.sin(yaw) * Math.cos(pitch) * dist;
|
|
591
|
+
var camZ = player.pos.z + Math.cos(yaw) * Math.cos(pitch) * dist;
|
|
592
|
+
var camY = baseY + 1 + Math.sin(pitch) * dist;
|
|
593
|
+
|
|
594
|
+
_tmpCamTarget.set(camX, camY, camZ);
|
|
595
|
+
|
|
596
|
+
// Smooth follow
|
|
597
|
+
S.camera.position.lerp(_tmpCamTarget, Math.min(1, dt * 6));
|
|
598
|
+
|
|
599
|
+
// Look at player chest (offset by player height)
|
|
600
|
+
_tmpLookAt.set(
|
|
601
|
+
player.pos.x,
|
|
602
|
+
baseY + CAMERA_LOOK_OFFSET.y,
|
|
603
|
+
player.pos.z + CAMERA_LOOK_OFFSET.z
|
|
604
|
+
);
|
|
605
|
+
S.camera.lookAt(_tmpLookAt);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Desk POV camera — when player is sitting, camera moves behind the character looking at the monitor
|
|
609
|
+
function updatePlayerCameraDesk(dt) {
|
|
610
|
+
var player = S._player;
|
|
611
|
+
if (!player || !player._sitTarget) return;
|
|
612
|
+
|
|
613
|
+
// Camera behind player's head, looking at the desk/monitor
|
|
614
|
+
var deskX = player._sitTarget.x;
|
|
615
|
+
var deskZ = player._sitTarget.z;
|
|
616
|
+
var baseY = player.pos.y;
|
|
617
|
+
|
|
618
|
+
// Camera position: slightly behind and above the seated player
|
|
619
|
+
_tmpCamTarget.set(deskX, baseY + 1.6, deskZ + 1.8);
|
|
620
|
+
S.camera.position.lerp(_tmpCamTarget, Math.min(1, dt * 4));
|
|
621
|
+
|
|
622
|
+
// Look at the monitor (desk is at z, monitor is slightly behind at z - 0.3)
|
|
623
|
+
_tmpLookAt.set(deskX, baseY + 1.2, deskZ - 0.5);
|
|
624
|
+
S.camera.lookAt(_tmpLookAt);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// --- Public API for iframe integration ---
|
|
628
|
+
export function isPlayerSitting() {
|
|
629
|
+
return S._player && S._player.sitting;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function getPlayerDeskIdx() {
|
|
633
|
+
return S._player ? S._player.sittingDeskIdx : -1;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Force stand from external code (LEAVE button, Escape key handler in iframe overlay)
|
|
637
|
+
// Sets sitting=false so WASD movement resumes and camera returns to third-person
|
|
638
|
+
window.playerForceStand = function() {
|
|
639
|
+
var player = S._player;
|
|
640
|
+
if (player && player.sitting) {
|
|
641
|
+
// Push player AWAY from desk center (works for all desk orientations)
|
|
642
|
+
var desks = S._campusDeskPositions || [];
|
|
643
|
+
var dIdx = player.sittingDeskIdx;
|
|
644
|
+
player.sitting = false;
|
|
645
|
+
player.sittingDeskIdx = -1;
|
|
646
|
+
if (player._sitPrompt) player._sitPrompt.style.display = 'none';
|
|
647
|
+
if (dIdx >= 0 && desks[dIdx]) {
|
|
648
|
+
// Always stand up FORWARD (toward camera/door side, negative Z in desk-local space)
|
|
649
|
+
// This avoids pushing into walls behind the desk
|
|
650
|
+
player.pos.z = desks[dIdx].z + 1.2; // 1.2 units in front of desk center
|
|
651
|
+
// Slight X offset to avoid chair mesh
|
|
652
|
+
var ddx = player.pos.x - desks[dIdx].x;
|
|
653
|
+
if (Math.abs(ddx) < 0.3) player.pos.x += 0.4;
|
|
654
|
+
} else {
|
|
655
|
+
player.pos.z += 0.8; // fallback
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|