three-player-controller 0.3.7 → 0.3.8

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/dist/index.js CHANGED
@@ -36,7 +36,7 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/playerController.ts
39
- var THREE3 = __toESM(require("three"));
39
+ var THREE4 = __toESM(require("three"));
40
40
  var import_three_mesh_bvh = require("three-mesh-bvh");
41
41
  var import_RoundedBoxGeometry = require("three/examples/jsm/geometries/RoundedBoxGeometry.js");
42
42
  var import_DRACOLoader = require("three/examples/jsm/loaders/DRACOLoader.js");
@@ -58,6 +58,231 @@ var vehicle_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAY
58
58
  // assets/imgs/view.png
59
59
  var view_default = "data:image/png;base64,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";
60
60
 
61
+ // src/utils/mobileControls.ts
62
+ var MobileControls = class {
63
+ constructor(setInput, controls) {
64
+ // 摇杆状态
65
+ this.nippleModule = null;
66
+ this.joystickManager = null;
67
+ this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
68
+ // DOM 元素
69
+ this.joystickZoneEl = null;
70
+ this.lookAreaEl = null;
71
+ this.jumpBtnEl = null;
72
+ this.flyBtnEl = null;
73
+ this.viewBtnEl = null;
74
+ this.vehicleBtnEl = null;
75
+ // 触摸状态
76
+ this.lookPointerId = null;
77
+ this.isLookDown = false;
78
+ this.lastTouchX = 0;
79
+ this.lastTouchY = 0;
80
+ // 触摸按下
81
+ this.onPointerDown = (e) => {
82
+ if (e.pointerType !== "touch") return;
83
+ this.isLookDown = true;
84
+ this.lookPointerId = e.pointerId;
85
+ this.lastTouchX = e.clientX;
86
+ this.lastTouchY = e.clientY;
87
+ this.lookAreaEl?.setPointerCapture?.(e.pointerId);
88
+ e.preventDefault();
89
+ };
90
+ // 触摸移动
91
+ this.onPointerMove = (e) => {
92
+ if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
93
+ const dx = e.clientX - this.lastTouchX;
94
+ const dy = e.clientY - this.lastTouchY;
95
+ this.lastTouchX = e.clientX;
96
+ this.lastTouchY = e.clientY;
97
+ this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
98
+ e.preventDefault();
99
+ };
100
+ // 触摸抬起
101
+ this.onPointerUp = (e) => {
102
+ if (e.pointerId !== this.lookPointerId) return;
103
+ this.isLookDown = false;
104
+ this.lookPointerId = null;
105
+ this.lookAreaEl?.releasePointerCapture?.(e.pointerId);
106
+ };
107
+ this.setInput = setInput;
108
+ this.controls = controls;
109
+ }
110
+ // 初始化移动端控制
111
+ async init() {
112
+ this.controls.maxPolarAngle = Math.PI * (300 / 360);
113
+ this.controls.touches = { ONE: null, TWO: null };
114
+ this.nippleModule = await import("nipplejs");
115
+ const nipple = this.nippleModule?.default;
116
+ const JOY_SIZE = 120;
117
+ const container = document.body;
118
+ this.joystickZoneEl = document.createElement("div");
119
+ this.joystickZoneEl.id = "joy-zone";
120
+ Object.assign(this.joystickZoneEl.style, {
121
+ position: "absolute",
122
+ left: "16px",
123
+ bottom: "16px",
124
+ width: `${JOY_SIZE + 40}px`,
125
+ height: `${JOY_SIZE + 40}px`,
126
+ touchAction: "none",
127
+ zIndex: "999",
128
+ pointerEvents: "auto",
129
+ WebkitUserSelect: "none",
130
+ userSelect: "none"
131
+ });
132
+ container.appendChild(this.joystickZoneEl);
133
+ this.blockTouch(this.joystickZoneEl);
134
+ this.joystickManager = nipple.create({
135
+ zone: this.joystickZoneEl,
136
+ mode: "static",
137
+ position: { left: `${(JOY_SIZE + 40) / 2}px`, bottom: `${(JOY_SIZE + 40) / 2}px` },
138
+ color: "#ffffff",
139
+ size: JOY_SIZE,
140
+ multitouch: true,
141
+ maxNumberOfNipples: 1
142
+ });
143
+ this.joystickManager.on("move", (_evt, data) => {
144
+ if (!data) return;
145
+ const rawX = data.vector?.x ?? 0;
146
+ const rawY = data.vector?.y ?? 0;
147
+ const distance = data.distance ?? 0;
148
+ const deadzone = 0.5;
149
+ const dirX = rawX > deadzone ? 1 : rawX < -deadzone ? -1 : 0;
150
+ const dirY = rawY > deadzone ? 1 : rawY < -deadzone ? -1 : 0;
151
+ const isSprinting = distance >= JOY_SIZE / 2;
152
+ const prev = this.prevJoyState;
153
+ if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) return;
154
+ this.prevJoyState = { dirX, dirY, shift: isSprinting };
155
+ this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
156
+ });
157
+ this.joystickManager.on("end", () => {
158
+ const prev = this.prevJoyState;
159
+ if (prev.dirX !== 0 || prev.dirY !== 0 || prev.shift) {
160
+ this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
161
+ this.setInput({ moveX: 0, moveY: 0, shift: false });
162
+ }
163
+ });
164
+ this.lookAreaEl = document.createElement("div");
165
+ Object.assign(this.lookAreaEl.style, {
166
+ position: "absolute",
167
+ right: "0",
168
+ bottom: "0",
169
+ width: "50%",
170
+ height: "100%",
171
+ zIndex: "998",
172
+ touchAction: "none",
173
+ WebkitUserSelect: "none",
174
+ userSelect: "none"
175
+ });
176
+ container.appendChild(this.lookAreaEl);
177
+ this.blockTouch(this.lookAreaEl);
178
+ this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, { passive: false });
179
+ this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, { passive: false });
180
+ this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, { passive: false });
181
+ this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, { passive: false });
182
+ this.jumpBtnEl = this.createBtn(container, 14, 14, jump_default);
183
+ this.flyBtnEl = this.createBtn(container, 14, 14 + 80, fly_default);
184
+ this.viewBtnEl = this.createBtn(container, 14, 14 + 200, view_default);
185
+ this.vehicleBtnEl = this.createBtn(container, 14 + 100, 14 + 120, vehicle_default);
186
+ this.vehicleBtnEl.style.display = "none";
187
+ this.jumpBtnEl.addEventListener("touchstart", (e) => {
188
+ e.preventDefault();
189
+ this.setInput({ jump: true });
190
+ }, { passive: false });
191
+ this.jumpBtnEl.addEventListener("touchend", (e) => {
192
+ e.preventDefault();
193
+ this.setInput({ jump: false });
194
+ }, { passive: false });
195
+ this.jumpBtnEl.addEventListener("touchcancel", (e) => {
196
+ e.preventDefault();
197
+ this.setInput({ jump: false });
198
+ }, { passive: false });
199
+ this.flyBtnEl.addEventListener("touchstart", (e) => {
200
+ e.preventDefault();
201
+ this.setInput({ toggleFly: true });
202
+ }, { passive: false });
203
+ this.viewBtnEl.addEventListener("touchstart", (e) => {
204
+ e.preventDefault();
205
+ this.setInput({ toggleView: true });
206
+ }, { passive: false });
207
+ this.vehicleBtnEl.addEventListener("touchstart", (e) => {
208
+ e.preventDefault();
209
+ this.setInput({ toggleVehicle: true });
210
+ }, { passive: false });
211
+ }
212
+ // 销毁移动端控制
213
+ destroy() {
214
+ try {
215
+ this.joystickManager?.destroy?.();
216
+ this.joystickManager = null;
217
+ if (this.lookAreaEl) {
218
+ this.lookAreaEl.removeEventListener("pointerdown", this.onPointerDown);
219
+ this.lookAreaEl.removeEventListener("pointermove", this.onPointerMove);
220
+ this.lookAreaEl.removeEventListener("pointerup", this.onPointerUp);
221
+ this.lookAreaEl.removeEventListener("pointercancel", this.onPointerUp);
222
+ }
223
+ [this.joystickZoneEl, this.lookAreaEl, this.jumpBtnEl, this.flyBtnEl, this.viewBtnEl, this.vehicleBtnEl].forEach((el) => el?.parentElement?.removeChild(el));
224
+ this.joystickZoneEl = this.lookAreaEl = this.jumpBtnEl = this.flyBtnEl = this.viewBtnEl = this.vehicleBtnEl = null;
225
+ } catch (e) {
226
+ console.warn("\u9500\u6BC1\u79FB\u52A8\u7AEF\u63A7\u5236\u65F6\u51FA\u9519\uFF1A", e);
227
+ }
228
+ }
229
+ // 同步车辆按钮显隐
230
+ syncVehicleBtn(show) {
231
+ if (this.vehicleBtnEl) this.vehicleBtnEl.style.display = show ? "block" : "none";
232
+ }
233
+ // 同步控制模式按钮
234
+ syncControllerModeBtn(mode) {
235
+ if (!this.flyBtnEl || !this.jumpBtnEl) return;
236
+ if (mode === 0) {
237
+ this.flyBtnEl.style.display = "block";
238
+ this.jumpBtnEl.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${jump_default}")`;
239
+ } else {
240
+ this.flyBtnEl.style.display = "none";
241
+ this.jumpBtnEl.style.backgroundImage = `url(${break_default})`;
242
+ }
243
+ }
244
+ // 阻止默认触摸
245
+ blockTouch(el) {
246
+ ["touchstart", "touchmove", "touchend", "touchcancel"].forEach((name) => {
247
+ el.addEventListener(name, (e) => e.preventDefault(), { passive: false });
248
+ });
249
+ }
250
+ // 创建圆形按钮
251
+ createBtn(container, rightPx, bottomPx, bgUrl) {
252
+ const btn = document.createElement("button");
253
+ Object.assign(btn.style, {
254
+ position: "absolute",
255
+ right: `${rightPx}px`,
256
+ bottom: `${bottomPx}px`,
257
+ width: "56px",
258
+ height: "56px",
259
+ zIndex: "1000",
260
+ borderRadius: "50%",
261
+ border: "2px solid black",
262
+ padding: "20px",
263
+ opacity: "0.95",
264
+ touchAction: "none",
265
+ fontSize: "14px",
266
+ userSelect: "none",
267
+ overflow: "hidden",
268
+ boxSizing: "border-box",
269
+ backgroundColor: "transparent",
270
+ backgroundRepeat: "no-repeat, no-repeat",
271
+ backgroundPosition: "center center, center center",
272
+ backgroundSize: "100% 100%, 80% 80%",
273
+ backgroundImage: `linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${bgUrl}")`
274
+ });
275
+ container.appendChild(btn);
276
+ ["touchstart", "touchend", "touchcancel"].forEach((name) => {
277
+ btn.addEventListener(name, (e) => e.preventDefault(), { passive: false });
278
+ });
279
+ return btn;
280
+ }
281
+ };
282
+
283
+ // src/utils/vehicleLoader.ts
284
+ var THREE3 = __toESM(require("three"));
285
+
61
286
  // src/utils/pathPlanner.ts
62
287
  var THREE = __toESM(require("three"));
63
288
  var PathNode = class {
@@ -71,6 +296,7 @@ var PathNode = class {
71
296
  this.parent = null;
72
297
  this.position = position.clone();
73
298
  }
299
+ // 判断节点相等
74
300
  equals(other) {
75
301
  return this.position.distanceTo(other.position) < 0.01;
76
302
  }
@@ -79,10 +305,12 @@ var PriorityQueue = class {
79
305
  constructor() {
80
306
  this.elements = [];
81
307
  }
308
+ // 入队并排序
82
309
  enqueue(item, priority) {
83
310
  this.elements.push({ priority, item });
84
311
  this.elements.sort((a, b) => a.priority - b.priority);
85
312
  }
313
+ // 出队最小优先级
86
314
  dequeue() {
87
315
  return this.elements.shift()?.item;
88
316
  }
@@ -92,6 +320,7 @@ var PriorityQueue = class {
92
320
  contains(item, compareFn) {
93
321
  return this.elements.some((e) => compareFn(e.item, item));
94
322
  }
323
+ // 更新节点优先级
95
324
  update(item, newPriority, compareFn) {
96
325
  const index = this.elements.findIndex((e) => compareFn(e.item, item));
97
326
  if (index !== -1) {
@@ -105,22 +334,15 @@ var PathPlanner = class {
105
334
  this.debugLines = [];
106
335
  this.debugPoints = [];
107
336
  this.obstacleChecker = obstacleChecker;
108
- this.config = {
109
- debugEnabled: false,
110
- scale: 1,
111
- ...config
112
- };
337
+ this.config = { debugEnabled: false, scale: 1, ...config };
113
338
  }
114
- // 计算启发式距离
339
+ // 启发式距离
115
340
  heuristic(a, b) {
116
341
  return a.distanceTo(b);
117
342
  }
118
- // A*路径规划算法
343
+ // A* 寻路主入口
119
344
  findPath(start, goal) {
120
- const startTime = performance.now();
121
- if (!this.obstacleChecker.isBlocked(start, goal)) {
122
- return [goal];
123
- }
345
+ if (!this.obstacleChecker.isBlocked(start, goal)) return [goal];
124
346
  const navigationPoints = this.obstacleChecker.getNavigationNodes(start, goal);
125
347
  const allNodes = [new PathNode(start), new PathNode(goal), ...navigationPoints.map((p) => new PathNode(p))];
126
348
  if (allNodes.length < 2) {
@@ -134,45 +356,35 @@ var PathPlanner = class {
134
356
  startNode.f = startNode.h;
135
357
  const openList = new PriorityQueue();
136
358
  const closedSet = /* @__PURE__ */ new Set();
137
- openList.enqueue(startNode, startNode.f);
138
359
  const nodeEquals = (a, b) => a.equals(b);
360
+ openList.enqueue(startNode, startNode.f);
139
361
  while (!openList.isEmpty()) {
140
362
  const current = openList.dequeue();
141
363
  if (!current) break;
142
364
  if (current.equals(goalNode)) {
143
365
  const path = this.reconstructPath(current);
144
- const endTime = performance.now();
145
- if (this.config.debugEnabled) {
146
- this.visualizePath([start, ...path]);
147
- }
366
+ if (this.config.debugEnabled) this.visualizePath([start, ...path]);
148
367
  return path;
149
368
  }
150
369
  closedSet.add(current);
151
370
  for (const neighbor of allNodes) {
152
371
  if (closedSet.has(neighbor)) continue;
153
- if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) {
154
- continue;
155
- }
372
+ if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) continue;
156
373
  const tentativeG = current.g + current.position.distanceTo(neighbor.position);
157
374
  if (tentativeG < neighbor.g) {
158
375
  neighbor.parent = current;
159
376
  neighbor.g = tentativeG;
160
377
  neighbor.h = this.heuristic(neighbor.position, goalNode.position);
161
378
  neighbor.f = neighbor.g + neighbor.h;
162
- if (openList.contains(neighbor, nodeEquals)) {
163
- openList.update(neighbor, neighbor.f, nodeEquals);
164
- } else {
165
- openList.enqueue(neighbor, neighbor.f);
166
- }
379
+ if (openList.contains(neighbor, nodeEquals)) openList.update(neighbor, neighbor.f, nodeEquals);
380
+ else openList.enqueue(neighbor, neighbor.f);
167
381
  }
168
382
  }
169
383
  }
170
- console.warn("A*\u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
384
+ console.warn("A* \u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
171
385
  return [goal];
172
386
  }
173
- /**
174
- * 重建路径
175
- */
387
+ // 重建路径
176
388
  reconstructPath(endNode) {
177
389
  const path = [];
178
390
  let current = endNode;
@@ -180,14 +392,10 @@ var PathPlanner = class {
180
392
  path.unshift(current.position.clone());
181
393
  current = current.parent;
182
394
  }
183
- if (path.length > 0) {
184
- path.shift();
185
- }
395
+ if (path.length > 0) path.shift();
186
396
  return this.smoothPath(path);
187
397
  }
188
- /**
189
- * 路径平滑
190
- */
398
+ // 路径平滑优化
191
399
  smoothPath(path) {
192
400
  if (path.length <= 2) return path;
193
401
  const smoothed = [path[0]];
@@ -205,38 +413,28 @@ var PathPlanner = class {
205
413
  }
206
414
  return smoothed;
207
415
  }
208
- /**
209
- * 可视化路径
210
- */
416
+ // 可视化路径
211
417
  visualizePath(path) {
212
418
  if (!this.config.scene || !this.config.debugEnabled) return;
213
419
  this.clearVisualization();
214
420
  const scale = this.config.scale || 1;
215
421
  if (path.length > 1) {
216
- const points = path.map((p) => p.clone());
217
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
218
- const material = new THREE.LineBasicMaterial({
219
- color: 65280,
220
- linewidth: 3
221
- });
222
- const line = new THREE.Line(geometry, material);
422
+ const geometry = new THREE.BufferGeometry().setFromPoints(path.map((p) => p.clone()));
423
+ const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 65280, linewidth: 3 }));
223
424
  this.config.scene.add(line);
224
425
  this.debugLines.push(line);
225
426
  }
226
427
  path.forEach((point, index) => {
227
- const geometry = new THREE.SphereGeometry(20 * scale);
228
- const material = new THREE.MeshBasicMaterial({
229
- color: index === path.length - 1 ? 16711680 : 65280
230
- });
231
- const sphere = new THREE.Mesh(geometry, material);
428
+ const sphere = new THREE.Mesh(
429
+ new THREE.SphereGeometry(20 * scale),
430
+ new THREE.MeshBasicMaterial({ color: index === path.length - 1 ? 16711680 : 65280 })
431
+ );
232
432
  sphere.position.copy(point);
233
433
  this.config.scene.add(sphere);
234
434
  this.debugPoints.push(sphere);
235
435
  });
236
436
  }
237
- /**
238
- * 清除路径可视化
239
- */
437
+ // 清除路径可视化
240
438
  clearVisualization() {
241
439
  if (!this.config.scene) return;
242
440
  this.debugLines.forEach((line) => {
@@ -252,15 +450,11 @@ var PathPlanner = class {
252
450
  });
253
451
  this.debugPoints = [];
254
452
  }
255
- /**
256
- * 更新配置
257
- */
453
+ // 更新配置
258
454
  updateConfig(config) {
259
455
  this.config = { ...this.config, ...config };
260
456
  }
261
- /**
262
- * 销毁
263
- */
457
+ // 销毁规划器
264
458
  dispose() {
265
459
  this.clearVisualization();
266
460
  }
@@ -280,7 +474,7 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
280
474
  vehicle.setWheelAxleCs(index, wheel.axleCs);
281
475
  vehicle.setWheelSuspensionRestLength(index, wheel.suspensionRestLength);
282
476
  vehicle.setWheelRadius(index, wheel.radius);
283
- vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength * 1);
477
+ vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength);
284
478
  vehicle.setWheelSuspensionStiffness(index, 250);
285
479
  vehicle.setWheelSuspensionCompression(index, 6);
286
480
  vehicle.setWheelSuspensionRelaxation(index, 6);
@@ -292,8 +486,8 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
292
486
  vehicle.setWheelSideFrictionStiffness(index, 2);
293
487
  });
294
488
  const up = new THREE2.Vector3(0, 1, 0);
295
- const _wheelSteeringQuat = new THREE2.Quaternion();
296
- const _wheelRotationQuat = new THREE2.Quaternion();
489
+ const wheelSteeringQuat = new THREE2.Quaternion();
490
+ const wheelRotationQuat = new THREE2.Quaternion();
297
491
  function updateWheelVisuals() {
298
492
  for (const [index, wheelObj] of wheels.entries()) {
299
493
  if (!wheelObj) continue;
@@ -304,9 +498,9 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
304
498
  const steering = vehicle.wheelSteering(index) ?? 0;
305
499
  const rotationRad = vehicle.wheelRotation(index) ?? 0;
306
500
  wheelObj.position.y = connection - suspension;
307
- _wheelSteeringQuat.setFromAxisAngle(up, steering);
308
- _wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
309
- wheelObj.quaternion.copy(_wheelSteeringQuat).multiply(_wheelRotationQuat);
501
+ wheelSteeringQuat.setFromAxisAngle(up, steering);
502
+ wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
503
+ wheelObj.quaternion.copy(wheelSteeringQuat).multiply(wheelRotationQuat);
310
504
  } catch (e) {
311
505
  }
312
506
  }
@@ -317,37 +511,247 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
317
511
  } catch {
318
512
  }
319
513
  }
514
+ return { vehicle, updateWheelVisuals, destroy };
515
+ }
516
+
517
+ // src/utils/vehicleLoader.ts
518
+ function getBbox(object) {
519
+ const bbox = new THREE3.Box3().setFromObject(object);
520
+ const center = new THREE3.Vector3();
521
+ const size = new THREE3.Vector3();
522
+ bbox.getCenter(center);
523
+ bbox.getSize(size);
524
+ return { bbox, center, size };
525
+ }
526
+ function createObstacleChecker(vehicleGroup, bbox, scale, playerScale) {
527
+ return {
528
+ // 射线检测路径是否被车辆遮挡
529
+ isBlocked(start, end) {
530
+ const vehiclePos = vehicleGroup.position;
531
+ const vehicleQuat = vehicleGroup.quaternion;
532
+ const center = new THREE3.Vector3();
533
+ const size = new THREE3.Vector3();
534
+ bbox.getCenter(center);
535
+ bbox.getSize(size);
536
+ center.applyQuaternion(vehicleQuat).add(vehiclePos);
537
+ const halfSize = size.clone().multiplyScalar(0.5 * scale);
538
+ const corners = [];
539
+ for (let x = -1; x <= 1; x += 2)
540
+ for (let y = -1; y <= 1; y += 2)
541
+ for (let z = -1; z <= 1; z += 2)
542
+ corners.push(
543
+ new THREE3.Vector3(halfSize.x * x, halfSize.y * y, halfSize.z * z).applyQuaternion(vehicleQuat).add(center)
544
+ );
545
+ const expandedBBox = new THREE3.Box3();
546
+ corners.forEach((c) => expandedBBox.expandByPoint(c));
547
+ expandedBBox.expandByScalar(100 * playerScale);
548
+ const direction = new THREE3.Vector3().subVectors(end, start);
549
+ const length = direction.length();
550
+ const ray = new THREE3.Ray(start, direction.normalize());
551
+ const intersects = ray.intersectBox(expandedBBox, new THREE3.Vector3());
552
+ return intersects !== null && start.distanceTo(intersects) < length;
553
+ },
554
+ // 生成绕行导航节点
555
+ getNavigationNodes(start, _goal) {
556
+ const nodes = [];
557
+ const vehiclePos = vehicleGroup.position;
558
+ const vehicleQuat = vehicleGroup.quaternion;
559
+ const bboxSize = new THREE3.Vector3();
560
+ bbox.getSize(bboxSize);
561
+ const fwd = new THREE3.Vector3(0, 0, 1).applyQuaternion(vehicleQuat);
562
+ const right = new THREE3.Vector3(1, 0, 0).applyQuaternion(vehicleQuat);
563
+ const halfLen = bboxSize.z / 2 * scale;
564
+ const halfWidth = bboxSize.x / 2 * scale;
565
+ const groundY = start.y;
566
+ for (const margin of [300 * playerScale, 500 * playerScale]) {
567
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(halfLen + margin)).add(right.clone().multiplyScalar(-halfWidth - margin)).setY(groundY));
568
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(halfLen + margin)).add(right.clone().multiplyScalar(halfWidth + margin)).setY(groundY));
569
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(-halfLen - margin)).add(right.clone().multiplyScalar(-halfWidth - margin)).setY(groundY));
570
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(-halfLen - margin)).add(right.clone().multiplyScalar(halfWidth + margin)).setY(groundY));
571
+ }
572
+ return nodes;
573
+ }
574
+ };
575
+ }
576
+ async function loadVehicleModel(opts, ctx) {
577
+ const { loader, scene, world, RAPIER, vehicleParams, vehicleLength, playerScale } = ctx;
578
+ const scale = opts.scale ?? 1;
579
+ const chassisRatio = opts.chassisRatio ?? 0.2;
580
+ const suspensionRestLengthRatio = opts.suspensionRestLengthRatio ?? 0.2;
581
+ const speedMultiplier = opts.speedMultiplier ?? 1;
582
+ vehicleParams.power.accelerateForce = 50 * scale;
583
+ vehicleParams.power.brakeForce = 200 * scale;
584
+ vehicleParams.power.maxSpeed = 1e4 * scale;
585
+ vehicleParams.followVehicleDirection = opts.followVehicleDirection ?? true;
586
+ const vehicleModel = await loader.loadAsync(opts.url);
587
+ const { size: originalSize } = getBbox(vehicleModel.scene);
588
+ const modelScale = vehicleLength / Math.max(originalSize.x, originalSize.y, originalSize.z);
589
+ const vehicleMixer = new THREE3.AnimationMixer(vehicleModel.scene);
590
+ const vehicleActions = /* @__PURE__ */ new Map();
591
+ const animations = vehicleModel.animations ?? [];
592
+ const openDoorClip = animations.find((a) => a.name === (opts.animations?.openDoorAnim ?? ""));
593
+ if (openDoorClip) {
594
+ const action = vehicleMixer.clipAction(openDoorClip);
595
+ action.setLoop(THREE3.LoopOnce, 1);
596
+ action.clampWhenFinished = true;
597
+ action.setEffectiveTimeScale(openDoorClip.duration);
598
+ action.enabled = true;
599
+ action.setEffectiveWeight(0);
600
+ vehicleActions.set("openDoor", action);
601
+ }
602
+ const wheelObjects = [];
603
+ for (const name of opts.wheelsNames) {
604
+ let found = false;
605
+ vehicleModel.scene.traverse((child) => {
606
+ if (child.name === name && !found) {
607
+ wheelObjects.push(child);
608
+ found = true;
609
+ }
610
+ });
611
+ if (!found) console.warn(`\u672A\u627E\u5230\u8F6E\u5B50: ${name}`);
612
+ }
613
+ const tempGroup = new THREE3.Group();
614
+ scene.add(tempGroup);
615
+ vehicleModel.scene.scale.multiplyScalar(modelScale * scale);
616
+ vehicleModel.scene.rotateY(vehicleParams.model.rotation);
617
+ const { size, bbox, center } = getBbox(vehicleModel.scene);
618
+ vehicleModel.scene.position.set(-center.x, -center.y, -center.z);
619
+ tempGroup.add(vehicleModel.scene);
620
+ tempGroup.updateMatrixWorld(true);
621
+ let wheelRadius = 0, wheelWidth = 0, suspensionRestLength = 0, chassisHeight = 0, wheelSizeInit = false;
622
+ const wheelsInfo = [];
623
+ for (const wheel of wheelObjects) {
624
+ const worldPos = new THREE3.Vector3();
625
+ const worldQuat = new THREE3.Quaternion();
626
+ const worldScale = new THREE3.Vector3();
627
+ wheel.getWorldPosition(worldPos);
628
+ wheel.getWorldQuaternion(worldQuat);
629
+ wheel.getWorldScale(worldScale);
630
+ if (!wheelSizeInit) {
631
+ const { size: ws } = getBbox(wheel);
632
+ wheelRadius = Number((Math.max(ws.x, ws.y, ws.z) / 2).toFixed(2));
633
+ wheelWidth = Number(Math.min(ws.x, ws.y, ws.z).toFixed(2));
634
+ suspensionRestLength = Number((wheelRadius * 2 * suspensionRestLengthRatio).toFixed(2));
635
+ chassisHeight = Number((wheelRadius * 2 * chassisRatio).toFixed(2));
636
+ wheelSizeInit = true;
637
+ }
638
+ wheelsInfo.push({ axleCs: new THREE3.Vector3(0, 0, -1), position: worldPos, quaternion: worldQuat, scale: worldScale, radius: wheelRadius, width: wheelWidth, suspensionRestLength, object: wheel });
639
+ }
640
+ tempGroup.remove(vehicleModel.scene);
641
+ scene.remove(tempGroup);
642
+ const vehicleGroup = new THREE3.Group();
643
+ scene.add(vehicleGroup);
644
+ vehicleGroup.add(vehicleModel.scene);
645
+ vehicleGroup.updateMatrixWorld(true);
646
+ const wheelWrappers = [];
647
+ for (let i = 0; i < wheelsInfo.length; i++) {
648
+ const wheel = wheelsInfo[i];
649
+ const wheelWrapper = new THREE3.Group();
650
+ wheelWrapper.position.copy(vehicleGroup.worldToLocal(wheel.position.clone()));
651
+ const wheelObj = wheel.object;
652
+ wheelObj.parent?.remove(wheelObj);
653
+ wheelObj.position.set(0, 0, 0);
654
+ wheelObj.quaternion.copy(wheel.quaternion);
655
+ wheelObj.scale.copy(wheel.scale);
656
+ wheelObj.updateMatrixWorld();
657
+ wheelWrapper.add(wheelObj);
658
+ vehicleGroup.add(wheelWrapper);
659
+ wheelWrappers.push(wheelWrapper);
660
+ }
661
+ const halfExtents = size.clone().multiplyScalar(0.5);
662
+ halfExtents.y -= chassisHeight / 2;
663
+ vehicleModel.scene.position.y -= chassisHeight / 2;
664
+ halfExtents.x *= 0.95;
665
+ halfExtents.z *= 0.95;
666
+ const chassisBody = world.createRigidBody(
667
+ RAPIER.RigidBodyDesc.dynamic().setTranslation(opts.position.x, opts.position.y, opts.position.z).setLinearDamping(vehicleParams.chassis.linearDamping).setAngularDamping(vehicleParams.chassis.angularDamping).setCanSleep(true).setAdditionalMass(10)
668
+ );
669
+ world.createCollider(RAPIER.ColliderDesc.cuboid(halfExtents.x, halfExtents.y, halfExtents.z), chassisBody);
670
+ if (vehicleParams.debug.showPhysicsBox) {
671
+ vehicleGroup.add(new THREE3.Mesh(
672
+ new THREE3.BoxGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2),
673
+ new THREE3.MeshBasicMaterial({ color: 16711680, wireframe: true, transparent: true, opacity: 0.3 })
674
+ ));
675
+ }
676
+ vehicleGroup.position.copy(opts.position);
677
+ vehicleGroup.updateMatrixWorld(true);
678
+ const { vehicle, updateWheelVisuals } = createVehicleController(world, chassisBody, wheelWrappers, wheelsInfo);
320
679
  return {
321
- vehicle,
680
+ vehicleGroup,
681
+ chassisBody,
682
+ vehicleController: vehicle,
322
683
  updateWheelVisuals,
323
- destroy
684
+ vehicleMixer,
685
+ vehicleActions,
686
+ vehiclIsOpenDoor: false,
687
+ vehicleBBox: bbox.clone(),
688
+ pathPlanner: new PathPlanner(
689
+ createObstacleChecker(vehicleGroup, bbox, scale, playerScale),
690
+ { debugEnabled: false, scene, scale: playerScale }
691
+ ),
692
+ scale,
693
+ boardingPoint: opts.boardingPoint,
694
+ seatOffset: opts.seatOffset ?? new THREE3.Vector3(),
695
+ enterVehicleTime: 1.5,
696
+ chassisRatio,
697
+ suspensionRestLengthRatio,
698
+ size: { l: Math.max(size.x, size.z), w: Math.min(size.x, size.z), h: size.y },
699
+ speedMultiplier
324
700
  };
325
701
  }
326
702
 
327
703
  // src/playerController.ts
328
- THREE3.Mesh.prototype.raycast = import_three_mesh_bvh.acceleratedRaycast;
704
+ THREE4.Mesh.prototype.raycast = import_three_mesh_bvh.acceleratedRaycast;
329
705
  var controllerInstance = null;
330
- var clock = new THREE3.Clock();
706
+ var clock = new THREE4.Clock();
707
+ function isMobileDevice() {
708
+ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
709
+ }
331
710
  var PlayerController = class {
332
711
  constructor() {
333
- // ==================== 基本配置与参数 ====================
712
+ // ==================== 场景引用 ====================
334
713
  this.loader = new import_GLTFLoader.GLTFLoader();
335
- this.controllerMode = 0;
336
- // 0: 人物 1: 车辆
714
+ // 物理参数
715
+ this.gravity = -2400;
716
+ this.jumpHeight = 600;
717
+ this.playerSpeed = 300;
718
+ this.playerFlySpeed = 2100;
719
+ this.curPlayerSpeed = 0;
720
+ // 交互参数
721
+ this.mouseSensitivity = 5;
722
+ this.thirdMouseMode = 1;
723
+ this.enableZoom = false;
724
+ this.playerFlyEnabled = true;
725
+ this.isShowMobileControls = true;
337
726
  this.enableOverShoulderView = false;
338
- this.isChangeControllerTransitionTimer = null;
339
- // ==================== 玩家基本属性 ====================
727
+ // ==================== 玩家胶囊体 ====================
340
728
  this.playerCapsuleRadius = 45;
341
- this.playerCapsuleRadiusRatio = 1;
342
729
  this.playerCapsuleHeight = 180;
343
- // 玩家参考身高
730
+ this.playerCapsuleRadiusRatio = 1;
344
731
  this.isFirstPerson = false;
345
732
  this.boundingBoxMinY = 0;
346
- // ==================== 测试参数 ====================
347
- this.displayPlayer = false;
348
- this.displayCollider = false;
349
- this.displayVisualizer = false;
350
- // ==================== 场景对象 ====================
733
+ // ==================== 运行状态 ====================
734
+ this.controllerMode = 0;
735
+ // 0:人物 1:车辆
736
+ this.playerIsOnGround = false;
737
+ this.isupdate = true;
738
+ this.isFlying = false;
739
+ this.isChangeControllerTransitionTimer = null;
740
+ // ==================== 输入按键 ====================
741
+ this.fwdPressed = false;
742
+ this.bkdPressed = false;
743
+ this.lftPressed = false;
744
+ this.rgtPressed = false;
745
+ this.spacePressed = false;
746
+ this.ctPressed = false;
747
+ this.shiftPressed = false;
748
+ // ==================== 相机参数 ====================
749
+ this.camCollisionLerp = 0.18;
750
+ this.camEpsilon = 35;
751
+ this.minCamDistance = 100;
752
+ this.maxCamDistance = 440;
753
+ this.orginMaxCamDistance = 440;
754
+ // ==================== 场景碰撞体 ====================
351
755
  this.collider = null;
352
756
  this.visualizer = null;
353
757
  this.person = null;
@@ -355,150 +759,78 @@ var PlayerController = class {
355
759
  this.collected = [];
356
760
  this.dynamicCollider = null;
357
761
  this.dynamicCollected = [];
358
- // ==================== 多车辆相关 ====================
762
+ // ==================== 车辆系统 ====================
359
763
  this.vehicles = [];
360
- // 所有已加载车辆
361
764
  this.activeVehicle = null;
362
- // 当前驾驶/交互的车辆
363
765
  this.vehicleLength = 6;
364
- // 车辆参考长度
365
- this.wheelSteeringQuat = new THREE3.Quaternion();
366
- this.wheelRotationQuat = new THREE3.Quaternion();
367
766
  this.RAPIER = null;
368
767
  this.world = null;
369
- // 全局车辆共享参数
768
+ this.wheelSteeringQuat = new THREE4.Quaternion();
769
+ this.wheelRotationQuat = new THREE4.Quaternion();
770
+ this.camBehindDir = new THREE4.Vector3(0, 0, 1);
370
771
  this.vehicleParams = {
371
- debug: {
372
- showPhysicsBox: false
373
- },
374
- chassis: {
375
- linearDamping: 0.5,
376
- angularDamping: 0.5
377
- },
378
- model: {
379
- rotation: -Math.PI / 2
380
- },
381
- power: {
382
- accelerateForce: 50,
383
- // 推进
384
- brakeForce: 200,
385
- // 刹车
386
- maxSpeed: 1e4
387
- // 最大速度
388
- },
389
- steering: {
390
- maxSteerAngle: Math.PI / 4,
391
- steerSpeed: 0.5,
392
- steerReturnSpeed: 1
393
- },
772
+ debug: { showPhysicsBox: false },
773
+ chassis: { linearDamping: 0.5, angularDamping: 0.5 },
774
+ model: { rotation: -Math.PI / 2 },
775
+ power: { accelerateForce: 50, brakeForce: 200, maxSpeed: 1e4 },
776
+ steering: { maxSteerAngle: Math.PI / 4, steerSpeed: 0.5, steerReturnSpeed: 1 },
394
777
  followVehicleDirection: true
395
778
  };
396
- this.camBehindDir = new THREE3.Vector3(0, 0, 1);
397
- // ==================== 上车相关 ====================
779
+ // ==================== 上车流程 ====================
398
780
  this.isMovingToBoardingPoint = false;
399
781
  this.boardingWaypoints = [];
400
782
  this.currentWaypointIndex = 0;
401
783
  this.boardingTargetDir = null;
402
784
  this.boardingMoveSpeed = 300;
403
785
  this.boardingRotateSpeed = 10;
404
- this.flip180Quat = new THREE3.Quaternion().setFromAxisAngle(
405
- new THREE3.Vector3(0, 1, 0),
406
- Math.PI
407
- );
408
- this.closeVehicleDoorTimer = null;
409
786
  this.boardingPointWorld = null;
410
787
  this.isBoardingAnimPlaying = false;
411
788
  this.closeDoorTriggered = false;
412
789
  this.isExitAnimPlaying = false;
413
790
  this.closeExitDoorTriggered = false;
414
- // ==================== 状态开关 ====================
415
- this.playerIsOnGround = false;
416
- this.isupdate = true;
417
- this.isFlying = false;
418
- // ==================== 输入状态 ====================
419
- this.fwdPressed = false;
420
- this.bkdPressed = false;
421
- this.lftPressed = false;
422
- this.rgtPressed = false;
423
- this.spacePressed = false;
424
- this.ctPressed = false;
425
- this.shiftPressed = false;
426
- // ==================== 移动端输入 ====================
427
- this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
428
- this.nippleModule = null;
429
- this.joystickManager = null;
430
- this.joystickZoneEl = null;
431
- this.lookAreaEl = null;
432
- this.jumpBtnEl = null;
433
- this.flyBtnEl = null;
434
- this.viewBtnEl = null;
435
- this.vehicleBtnEl = null;
436
- this.lookPointerId = null;
437
- this.isLookDown = false;
438
- this.lastTouchX = 0;
439
- this.lastTouchY = 0;
440
- this.nearCheckLocal = new THREE3.Vector3();
441
- this.nearCheckWorld = new THREE3.Vector3();
442
- this.isNearVehicle = false;
443
- // ==================== 第三人称相机参数 ====================
444
- this._camCollisionLerp = 0.18;
445
- this._camEpsilon = 0.35;
446
- this.minCamDistance = 1;
447
- this.maxCamDistance = 4.4;
448
- this.orginMaxCamDistance = 4.4;
449
- // ==================== 物理/运动 ====================
450
- this.playerVelocity = new THREE3.Vector3();
451
- this.upVector = new THREE3.Vector3(0, 1, 0);
452
- // ==================== 临时复用向量/矩阵 ====================
453
- this.tempVector = new THREE3.Vector3();
454
- this.tempVector2 = new THREE3.Vector3();
455
- this.tempBox = new THREE3.Box3();
456
- this.tempMat = new THREE3.Matrix4();
457
- this.tempSegment = new THREE3.Line3();
791
+ this.closeVehicleDoorTimer = null;
792
+ this.flip180Quat = new THREE4.Quaternion().setFromAxisAngle(new THREE4.Vector3(0, 1, 0), Math.PI);
458
793
  this.recheckAnimTimer = null;
459
- // ==================== 相机朝向/移动复用向量 ====================
460
- this.camDir = new THREE3.Vector3();
461
- this.moveDir = new THREE3.Vector3();
462
- this.targetQuat = new THREE3.Quaternion();
463
- this.targetMat = new THREE3.Matrix4();
794
+ this.allAnimations = [];
795
+ // ==================== 移动端控制 ====================
796
+ this.mobileControls = null;
797
+ this.nearCheckLocal = new THREE4.Vector3();
798
+ this.nearCheckWorld = new THREE4.Vector3();
799
+ this.isNearVehicle = false;
800
+ // ==================== 调试显示 ====================
801
+ this.displayPlayer = false;
802
+ this.displayCollider = false;
803
+ this.displayVisualizer = false;
804
+ // ==================== 方向常量 ====================
464
805
  this.rotationSpeed = 10;
465
- this.DIR_FWD = new THREE3.Vector3(0, 0, -1);
466
- this.DIR_BKD = new THREE3.Vector3(0, 0, 1);
467
- this.DIR_LFT = new THREE3.Vector3(-1, 0, 0);
468
- this.DIR_RGT = new THREE3.Vector3(1, 0, 0);
469
- this.DIR_UP = new THREE3.Vector3(0, 1, 0);
806
+ this.DIR_FWD = new THREE4.Vector3(0, 0, -1);
807
+ this.DIR_BKD = new THREE4.Vector3(0, 0, 1);
808
+ this.DIR_LFT = new THREE4.Vector3(-1, 0, 0);
809
+ this.DIR_RGT = new THREE4.Vector3(1, 0, 0);
810
+ this.DIR_UP = new THREE4.Vector3(0, 1, 0);
811
+ // ==================== 复用向量 ====================
812
+ this.upVector = new THREE4.Vector3(0, 1, 0);
813
+ this.playerVelocity = new THREE4.Vector3();
814
+ this.camDir = new THREE4.Vector3();
815
+ this.moveDir = new THREE4.Vector3();
816
+ this.targetQuat = new THREE4.Quaternion();
817
+ this.targetMat = new THREE4.Matrix4();
818
+ this.tempVector = new THREE4.Vector3();
819
+ this.tempVector2 = new THREE4.Vector3();
820
+ this.tempBox = new THREE4.Box3();
821
+ this.tempMat = new THREE4.Matrix4();
822
+ this.tempSegment = new THREE4.Line3();
470
823
  // ==================== 射线检测 ====================
471
- this._personToCam = new THREE3.Vector3();
472
- this._originTmp = new THREE3.Vector3();
473
- this._raycaster = new THREE3.Raycaster(
474
- new THREE3.Vector3(),
475
- new THREE3.Vector3(0, -1, 0)
476
- );
477
- this._raycasterPersonToCam = new THREE3.Raycaster(
478
- new THREE3.Vector3(),
479
- new THREE3.Vector3()
480
- );
481
- this.centerRay = new THREE3.Raycaster();
482
- this.centerMouse = new THREE3.Vector2();
483
- // ==================== 物理与碰撞检测 ====================
484
- this.ensureAttributesMinimal = (geom) => {
485
- if (!geom.attributes.position) return null;
486
- if (!geom.attributes.normal) geom.computeVertexNormals();
487
- if (!geom.attributes.uv) {
488
- const count = geom.attributes.position.count;
489
- const dummyUV = new Float32Array(count * 2);
490
- geom.setAttribute("uv", new THREE3.BufferAttribute(dummyUV, 2));
491
- }
492
- return geom;
493
- };
824
+ this.personToCam = new THREE4.Vector3();
825
+ this.originTmp = new THREE4.Vector3();
826
+ this.raycaster = new THREE4.Raycaster(new THREE4.Vector3(), new THREE4.Vector3(0, -1, 0));
827
+ this.raycasterPersonToCam = new THREE4.Raycaster(new THREE4.Vector3(), new THREE4.Vector3());
828
+ this.centerRay = new THREE4.Raycaster();
829
+ this.centerMouse = new THREE4.Vector2();
830
+ // 按键动画同步
494
831
  this.setAnimationByPressed = () => {
495
832
  this.maxCamDistance = this.orginMaxCamDistance;
496
- if (this.isMovingToBoardingPoint) {
497
- this.isMovingToBoardingPoint = false;
498
- this.boardingWaypoints = [];
499
- this.currentWaypointIndex = 0;
500
- this.boardingTargetDir = null;
501
- }
833
+ this.cancelBoarding();
502
834
  if (this.isExitAnimPlaying) {
503
835
  this.isExitAnimPlaying = false;
504
836
  this.closeExitDoorTriggered = false;
@@ -526,15 +858,11 @@ var PlayerController = class {
526
858
  return;
527
859
  }
528
860
  if (this.fwdPressed) {
529
- this.playPersonAnimationByName(
530
- this.shiftPressed ? "running" : "walking"
531
- );
861
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
532
862
  return;
533
863
  }
534
864
  if (!this.isFirstPerson && (this.lftPressed || this.rgtPressed || this.bkdPressed)) {
535
- this.playPersonAnimationByName(
536
- this.shiftPressed ? "running" : "walking"
537
- );
865
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
538
866
  return;
539
867
  }
540
868
  if (this.lftPressed) {
@@ -550,19 +878,16 @@ var PlayerController = class {
550
878
  return;
551
879
  }
552
880
  }
553
- if (this.recheckAnimTimer !== null) {
554
- clearTimeout(this.recheckAnimTimer);
555
- }
881
+ if (this.recheckAnimTimer !== null) clearTimeout(this.recheckAnimTimer);
556
882
  this.recheckAnimTimer = setTimeout(() => {
557
883
  this.setAnimationByPressed();
558
884
  this.recheckAnimTimer = null;
559
885
  }, 200);
560
886
  };
561
887
  // ==================== 事件处理 ====================
562
- this._boundOnKeydown = async (e) => {
563
- if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) {
564
- e.preventDefault();
565
- }
888
+ // 键盘按下事件
889
+ this.boundOnKeydown = async (e) => {
890
+ if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) e.preventDefault();
566
891
  switch (e.code) {
567
892
  case "KeyW":
568
893
  case "ArrowUp":
@@ -591,17 +916,11 @@ var PlayerController = class {
591
916
  this.controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 };
592
917
  break;
593
918
  case "Space":
594
- if (this.isMovingToBoardingPoint) {
595
- this.isMovingToBoardingPoint = false;
596
- this.boardingWaypoints = [];
597
- this.currentWaypointIndex = 0;
598
- this.boardingTargetDir = null;
599
- }
919
+ this.cancelBoarding();
600
920
  this.spacePressed = true;
601
- if (this.controllerMode == 1) return;
921
+ if (this.controllerMode === 1) return;
602
922
  if (!this.playerIsOnGround || this.isFlying) return;
603
- const next = this.personActions?.get("jumping");
604
- if (next && this.actionState === next) return;
923
+ if (this.personActions?.get("jumping") === this.actionState) return;
605
924
  this.playPersonAnimationByName("jumping");
606
925
  this.playerVelocity.y = this.jumpHeight;
607
926
  this.playerIsOnGround = false;
@@ -613,25 +932,21 @@ var PlayerController = class {
613
932
  this.changeView();
614
933
  break;
615
934
  case "KeyF":
616
- if (this.controllerMode == 0 && this.playerFlyEnabled) {
935
+ if (this.controllerMode === 0 && this.playerFlyEnabled) {
617
936
  this.isFlying = !this.isFlying;
618
937
  this.setAnimationByPressed();
619
- if (!this.isFlying && !this.playerIsOnGround) {
620
- this.playPersonAnimationByName("jumping");
621
- }
938
+ if (!this.isFlying && !this.playerIsOnGround) this.playPersonAnimationByName("jumping");
622
939
  }
623
940
  break;
624
941
  case "KeyE":
625
942
  if (this.isFlying) return;
626
- if (this.controllerMode == 0) {
627
- this.enterVehicle();
628
- } else {
629
- this.exitVehicle();
630
- }
943
+ if (this.controllerMode === 0) this.enterVehicle();
944
+ else this.exitVehicle();
631
945
  break;
632
946
  }
633
947
  };
634
- this._boundOnKeyup = (e) => {
948
+ // 键盘抬起事件
949
+ this.boundOnKeyup = (e) => {
635
950
  switch (e.code) {
636
951
  case "KeyW":
637
952
  case "ArrowUp":
@@ -667,192 +982,112 @@ var PlayerController = class {
667
982
  break;
668
983
  }
669
984
  };
670
- this._mouseMove = (e) => {
671
- if (document.pointerLockElement !== document.body) return;
672
- this.setToward(e.movementX, e.movementY, 1e-4);
673
- };
674
- this._mouseClick = (_e) => {
675
- this.setPointerLock();
676
- };
677
- // ==================== 移动端控制 ====================
678
- this.onPointerDown = (e) => {
679
- if (e.pointerType !== "touch") return;
680
- this.isLookDown = true;
681
- this.lookPointerId = e.pointerId;
682
- this.lastTouchX = e.clientX;
683
- this.lastTouchY = e.clientY;
684
- this.lookAreaEl?.setPointerCapture?.(e.pointerId);
685
- e.preventDefault();
686
- };
687
- this.onPointerMove = (e) => {
688
- if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
689
- const dx = e.clientX - this.lastTouchX;
690
- const dy = e.clientY - this.lastTouchY;
691
- this.lastTouchX = e.clientX;
692
- this.lastTouchY = e.clientY;
693
- this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
694
- e.preventDefault();
695
- };
696
- this.onPointerUp = (e) => {
697
- if (e.pointerId !== this.lookPointerId) return;
698
- this.isLookDown = false;
699
- this.lookPointerId = null;
700
- this.lookAreaEl?.releasePointerCapture?.(e.pointerId);
985
+ this.mouseMove = (e) => {
986
+ if (document.pointerLockElement === document.body) this.setToward(e.movementX, e.movementY, 1e-4);
701
987
  };
702
- this._raycaster.firstHitOnly = true;
703
- this._raycasterPersonToCam.firstHitOnly = true;
988
+ this.mouseClick = () => this.setPointerLock();
989
+ this.raycaster.firstHitOnly = true;
990
+ this.raycasterPersonToCam.firstHitOnly = true;
704
991
  }
705
- // ==================== 初始化相关方法 ====================
992
+ // ==================== 初始化 ====================
993
+ // 初始化控制器
706
994
  async init(opts, callback) {
995
+ const m = opts.playerModel;
996
+ const s = m.scale ?? 1;
707
997
  this.scene = opts.scene;
708
998
  this.camera = opts.camera;
709
999
  this.camera.rotation.order = "YXZ";
710
1000
  this.controls = opts.controls;
711
- this.playerModel = opts.playerModel;
712
- this.initPos = opts.initPos ?? new THREE3.Vector3(0, 0, 0);
713
- this.mouseSensity = opts.mouseSensity ?? 5;
714
- const s = this.playerModel.scale;
715
- this.gravity = (opts.playerModel.gravity ?? -2400) * s;
716
- this.jumpHeight = (opts.playerModel.jumpHeight ?? 600) * s;
717
- this.playerSpeed = (opts.playerModel.speed ?? 300) * s;
718
- this.playerFlySpeed = (opts.playerModel.flySpeed ?? 2100) * s;
1001
+ this.playerModel = m;
1002
+ this.initPos = opts.initPos ? new THREE4.Vector3(opts.initPos.x, opts.initPos.y, opts.initPos.z) : new THREE4.Vector3(0, 0, 0);
1003
+ const pm = this.playerModel;
1004
+ this.gravity = (pm.gravity ?? this.gravity) * s;
1005
+ this.jumpHeight = (pm.jumpHeight ?? this.jumpHeight) * s;
1006
+ this.playerSpeed = (pm.speed ?? this.playerSpeed) * s;
1007
+ this.playerFlySpeed = (pm.flySpeed ?? this.playerFlySpeed) * s;
719
1008
  this.curPlayerSpeed = this.playerSpeed;
720
- this.playerModel.rotateY = opts.playerModel.rotateY ?? 0;
721
- this.playerFlyEnabled = opts.playerModel.flyEnabled ?? true;
722
- this._camCollisionLerp = 0.18;
723
- this._camEpsilon = 35 * s;
724
- this.minCamDistance = (opts.minCamDistance ?? 100) * s;
725
- this.maxCamDistance = (opts.maxCamDistance ?? 440) * s;
1009
+ this.playerFlyEnabled = pm.flyEnabled ?? this.playerFlyEnabled;
1010
+ this.playerCapsuleRadiusRatio = pm.capsuleRadiusRatio ?? this.playerCapsuleRadiusRatio;
1011
+ this.mouseSensitivity = opts.mouseSensitivity ?? this.mouseSensitivity;
1012
+ this.thirdMouseMode = opts.thirdMouseMode ?? this.thirdMouseMode;
1013
+ this.enableZoom = opts.enableZoom ?? this.enableZoom;
1014
+ this.minCamDistance = (opts.minCamDistance ?? this.minCamDistance) * s;
1015
+ this.maxCamDistance = (opts.maxCamDistance ?? this.maxCamDistance) * s;
726
1016
  this.orginMaxCamDistance = this.maxCamDistance;
727
- this.thirdMouseMode = opts.thirdMouseMode ?? 1;
728
- this.enableZoom = opts.enableZoom ?? false;
729
- this.playerCapsuleRadiusRatio = opts.playerModel.capsuleRadiusRatio ?? 1;
730
- const isMobileDevice = () => navigator.maxTouchPoints && navigator.maxTouchPoints > 0 || "ontouchstart" in window || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
731
- this.isShowMobileControls = (opts.isShowMobileControls ?? true) && isMobileDevice();
1017
+ this.camEpsilon = this.camEpsilon * s;
1018
+ this.isShowMobileControls = (opts.isShowMobileControls ?? this.isShowMobileControls) && isMobileDevice();
1019
+ this.enableOverShoulderView = opts.enableOverShoulderView ?? this.enableOverShoulderView;
732
1020
  if (this.isShowMobileControls) {
733
- await this.initMobileControls();
1021
+ this.mobileControls = new MobileControls((i) => this.setInput(i), this.controls);
1022
+ await this.mobileControls.init();
734
1023
  }
735
1024
  await this.createBVH(opts.colliderMeshUrl);
736
1025
  await this.loadPersonGLB();
737
1026
  this.onAllEvent();
738
1027
  this.setCameraPos();
739
1028
  this.setControls();
740
- if (callback) callback();
741
- this.enableOverShoulderView = opts.enableOverShoulderView ?? false;
742
1029
  this.setOverShoulderView(this.enableOverShoulderView);
1030
+ callback?.();
743
1031
  }
1032
+ // 过肩视角切换
744
1033
  setOverShoulderView(enable) {
745
- if (!enable || this.controllerMode == 1) {
1034
+ if (!enable || this.controllerMode === 1) {
746
1035
  this.camera.clearViewOffset();
747
1036
  return;
748
1037
  }
749
1038
  const w = window.innerWidth;
750
1039
  const h = window.innerHeight;
751
- this.camera.setViewOffset(
752
- w,
753
- h,
754
- -w * -0.15,
755
- 0,
756
- w,
757
- h
758
- );
1040
+ this.camera.setViewOffset(w, h, -w * -0.15, 0, w, h);
759
1041
  }
1042
+ // 初始化加载器
760
1043
  async initLoader() {
761
1044
  const dracoLoader = new import_DRACOLoader.DRACOLoader();
762
1045
  dracoLoader.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/gltf/");
763
1046
  dracoLoader.setDecoderConfig({ type: "js" });
764
1047
  this.loader.setDRACOLoader(dracoLoader);
765
1048
  }
1049
+ // 初始化物理引擎
766
1050
  async initRapier() {
767
1051
  if (this.RAPIER) return;
768
1052
  this.RAPIER = await import("@dimforge/rapier3d-compat");
769
1053
  await this.RAPIER.init();
770
- const gravity = new this.RAPIER.Vector3(0, -9.81, 0);
771
- this.world = new this.RAPIER.World(gravity);
1054
+ this.world = new this.RAPIER.World(new this.RAPIER.Vector3(0, -9.81, 0));
772
1055
  this.world.maxCcdSubsteps = 2;
773
- const addGeometryAsTrimesh = (RAPIER, world, geom) => {
774
- let geometry = geom.index ? geom.clone().toNonIndexed() : geom.clone();
775
- const posAttr = geometry.attributes.position;
776
- const vertexCount = posAttr.count;
777
- if (vertexCount % 3 !== 0) {
778
- console.warn("\u9876\u70B9\u6570\u4E0D\u662F3\u7684\u500D\u6570\uFF0C\u4E09\u89D2\u5F62\u53EF\u80FD\u4E0D\u5B8C\u6574");
779
- }
780
- const vertices = new Float32Array(vertexCount * 3);
781
- const tmp = new THREE3.Vector3();
782
- for (let i = 0; i < vertexCount; i++) {
783
- tmp.fromBufferAttribute(posAttr, i);
784
- vertices[i * 3 + 0] = tmp.x;
785
- vertices[i * 3 + 1] = tmp.y;
786
- vertices[i * 3 + 2] = tmp.z;
1056
+ const addTrimesh = (RAPIER, world, geom) => {
1057
+ let g = geom.index ? geom.clone().toNonIndexed() : geom.clone();
1058
+ const pos = g.attributes.position;
1059
+ const count = pos.count;
1060
+ const verts = new Float32Array(count * 3);
1061
+ const tmp = new THREE4.Vector3();
1062
+ for (let i = 0; i < count; i++) {
1063
+ tmp.fromBufferAttribute(pos, i);
1064
+ verts[i * 3] = tmp.x;
1065
+ verts[i * 3 + 1] = tmp.y;
1066
+ verts[i * 3 + 2] = tmp.z;
787
1067
  }
788
- const indices = vertexCount > 65535 ? new Uint32Array(vertexCount) : new Uint16Array(vertexCount);
789
- for (let i = 0; i < vertexCount; i++) indices[i] = i;
790
- const bodyDesc = RAPIER.RigidBodyDesc.fixed();
791
- const body = world.createRigidBody(bodyDesc);
792
- const colliderDesc = RAPIER.ColliderDesc.trimesh(vertices, indices).setRestitution(0).setFriction(0.8);
793
- world.createCollider(colliderDesc, body);
1068
+ const indices = count > 65535 ? new Uint32Array(count) : new Uint16Array(count);
1069
+ for (let i = 0; i < count; i++) indices[i] = i;
1070
+ const body = world.createRigidBody(RAPIER.RigidBodyDesc.fixed());
1071
+ world.createCollider(
1072
+ RAPIER.ColliderDesc.trimesh(verts, indices).setRestitution(0).setFriction(0.8),
1073
+ body
1074
+ );
794
1075
  };
795
- for (const g of this.collected) {
796
- addGeometryAsTrimesh(this.RAPIER, this.world, g);
797
- }
798
- const groundDesc = this.RAPIER.RigidBodyDesc.fixed();
799
- const groundBody = this.world.createRigidBody(groundDesc);
1076
+ for (const g of this.collected) addTrimesh(this.RAPIER, this.world, g);
1077
+ const groundBody = this.world.createRigidBody(this.RAPIER.RigidBodyDesc.fixed());
800
1078
  groundBody.userData = { outOfBounds: true };
801
1079
  }
802
- // ==================== 玩家模型相关方法 ====================
1080
+ // ==================== 玩家模型 ====================
1081
+ // 加载玩家模型
803
1082
  async loadPersonGLB() {
804
1083
  try {
805
- const gltf = await this.loader.loadAsync(
806
- this.playerModel.url
807
- );
1084
+ const gltf = await this.loader.loadAsync(this.playerModel.url);
808
1085
  this.person = gltf.scene;
809
- const { size } = this.getBbox(this.person);
810
- const ratio = this.playerCapsuleHeight / size.y;
811
- const modelScale = ratio;
812
- this.playerCapsuleRadius = Number(Math.min(size.x, size.z).toFixed(0)) * modelScale * this.playerCapsuleRadiusRatio;
813
- this.playerCapsuleHeight = Number(size.y.toFixed(0)) * modelScale;
814
- const scale = this.playerModel.scale;
815
- const material = new THREE3.MeshStandardMaterial({
816
- color: new THREE3.Color(1, 0, 0),
817
- shadowSide: THREE3.DoubleSide,
818
- depthTest: false,
819
- transparent: true,
820
- opacity: this.displayPlayer ? 0.5 : 0,
821
- wireframe: true,
822
- depthWrite: false
823
- });
824
- const r = this.playerCapsuleRadius * scale;
825
- const h = this.playerCapsuleHeight * scale;
826
- this.player = new THREE3.Mesh(
827
- new import_RoundedBoxGeometry.RoundedBoxGeometry(r * 2, h, r * 2, 1, 75),
828
- material
829
- );
830
- this.player.geometry.translate(0, -h * 0.25, 0);
831
- this.player.capsuleInfo = {
832
- radius: r,
833
- segment: new THREE3.Line3(
834
- new THREE3.Vector3(),
835
- new THREE3.Vector3(0, -h * 0.5, 0)
836
- )
837
- };
838
- this.player.name = "capsule";
839
- this.scene.add(this.player);
840
- this.reset();
841
- this.player.rotateY(this.playerModel.rotateY ?? 0);
842
- this.person.scale.multiplyScalar(modelScale * scale);
843
- this.person.position.set(0, -h * 0.75, 0);
844
- this.person.traverse((child) => {
845
- if (child.name == this.playerModel?.headObjName) {
846
- this.personHead = child;
847
- }
848
- });
849
- this.player.add(this.person);
850
- this.reset();
851
- this.personMixer = new THREE3.AnimationMixer(this.person);
1086
+ this.personMixer = new THREE4.AnimationMixer(this.person);
852
1087
  const animations = gltf.animations ?? [];
853
- console.log("animations", animations);
1088
+ this.allAnimations = animations;
854
1089
  this.personActions = /* @__PURE__ */ new Map();
855
- const animationMappings = [
1090
+ const mappings = [
856
1091
  [this.playerModel.idleAnim, "idle"],
857
1092
  [this.playerModel.walkAnim, "walking"],
858
1093
  [this.playerModel.leftWalkAnim || this.playerModel.walkAnim, "left_walking"],
@@ -865,43 +1100,30 @@ var PlayerController = class {
865
1100
  [this.playerModel.enterCarAnim || this.playerModel.idleAnim, "enterCar"],
866
1101
  [this.playerModel.exitCarAnim || this.playerModel.idleAnim, "exitCar"]
867
1102
  ];
868
- const findClip = (name) => animations.find((a) => a.name === name);
869
- for (const [clipName, actionName] of animationMappings) {
870
- const clip = findClip(clipName);
1103
+ for (const [clipName, actionName] of mappings) {
1104
+ const clip = animations.find((a) => a.name === clipName);
871
1105
  if (!clip) continue;
872
1106
  const action = this.personMixer.clipAction(clip);
873
1107
  if (actionName === "jumping") {
874
- action.setLoop(THREE3.LoopOnce, 1);
1108
+ action.setLoop(THREE4.LoopOnce, 1);
875
1109
  action.clampWhenFinished = true;
876
1110
  action.setEffectiveTimeScale(1.2);
877
1111
  } else {
878
- action.setLoop(THREE3.LoopRepeat, Infinity);
879
- action.clampWhenFinished = false;
1112
+ action.setLoop(THREE4.LoopRepeat, Infinity);
880
1113
  action.setEffectiveTimeScale(1);
881
1114
  }
882
1115
  action.enabled = true;
883
1116
  action.setEffectiveWeight(0);
884
1117
  this.personActions.set(actionName, action);
885
1118
  }
886
- this.idleAction = this.personActions.get("idle");
887
- this.walkAction = this.personActions.get("walking");
888
- this.leftWalkAction = this.personActions.get("left_walking");
889
- this.rightWalkAction = this.personActions.get("right_walking");
890
- this.backwardAction = this.personActions.get("walking_backward");
891
- this.jumpAction = this.personActions.get("jumping");
892
- this.runAction = this.personActions.get("running");
893
- this.flyidleAction = this.personActions.get("flyidle");
894
- this.flyAction = this.personActions.get("flying");
895
- this.idleAction.setEffectiveWeight(1);
896
- this.idleAction.play();
897
- this.actionState = this.idleAction;
1119
+ this.personActions.get("idle").setEffectiveWeight(1);
1120
+ this.personActions.get("idle").play();
1121
+ this.actionState = this.personActions.get("idle");
898
1122
  this.personMixer.addEventListener("finished", (ev) => {
899
- const finishedAction = ev.action;
900
- if (finishedAction === this.jumpAction) {
1123
+ const done = ev.action;
1124
+ if (done === this.personActions.get("jumping")) {
901
1125
  if (this.fwdPressed) {
902
- this.playPersonAnimationByName(
903
- this.shiftPressed ? "running" : "walking"
904
- );
1126
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
905
1127
  return;
906
1128
  }
907
1129
  if (this.bkdPressed) {
@@ -914,26 +1136,56 @@ var PlayerController = class {
914
1136
  }
915
1137
  this.playPersonAnimationByName("idle");
916
1138
  }
917
- if (finishedAction === this.personActions?.get("enterCar")) {
918
- this.onEnterCarAnimFinished();
919
- }
920
- if (finishedAction === this.personActions?.get("exitCar")) {
921
- }
1139
+ if (done === this.personActions?.get("enterCar")) this.onEnterCarAnimFinished();
1140
+ });
1141
+ this.personMixer.update(0);
1142
+ this.person.updateMatrixWorld(true);
1143
+ const { size } = this.getBbox(this.person);
1144
+ const modelScale = this.playerCapsuleHeight / size.y;
1145
+ this.playerCapsuleRadius = Number(Math.min(size.x, size.z).toFixed(0)) * modelScale * this.playerCapsuleRadiusRatio;
1146
+ this.playerCapsuleHeight = Number(size.y.toFixed(0)) * modelScale;
1147
+ const s = this.playerModel.scale;
1148
+ const r = this.playerCapsuleRadius * s;
1149
+ const h = this.playerCapsuleHeight * s;
1150
+ this.player = new THREE4.Mesh(
1151
+ new import_RoundedBoxGeometry.RoundedBoxGeometry(r * 2, h, r * 2, 1, 75),
1152
+ new THREE4.MeshStandardMaterial({
1153
+ color: new THREE4.Color(1, 0, 0),
1154
+ shadowSide: THREE4.DoubleSide,
1155
+ depthTest: false,
1156
+ transparent: true,
1157
+ opacity: this.displayPlayer ? 0.5 : 0,
1158
+ wireframe: true,
1159
+ depthWrite: false
1160
+ })
1161
+ );
1162
+ this.player.geometry.translate(0, -h * 0.25, 0);
1163
+ this.player.capsuleInfo = {
1164
+ radius: r,
1165
+ segment: new THREE4.Line3(new THREE4.Vector3(), new THREE4.Vector3(0, -h * 0.5, 0))
1166
+ };
1167
+ this.player.name = "capsule";
1168
+ this.scene.add(this.player);
1169
+ this.reset();
1170
+ this.player.rotateY(this.playerModel.rotateY ?? 0);
1171
+ this.person.scale.multiplyScalar(modelScale * s);
1172
+ this.person.position.set(0, -h * 0.75, 0);
1173
+ this.person.traverse((child) => {
1174
+ if (child.name === this.playerModel?.headObjName) this.personHead = child;
922
1175
  });
923
- } catch (error) {
924
- console.error("\u52A0\u8F7D\u73A9\u5BB6\u6A21\u578B\u5931\u8D25:", error);
1176
+ this.player.add(this.person);
1177
+ this.reset();
1178
+ } catch (e) {
1179
+ console.error("\u52A0\u8F7D\u73A9\u5BB6\u6A21\u578B\u5931\u8D25:", e);
925
1180
  }
926
1181
  }
1182
+ // 切换玩家模型
927
1183
  async switchPlayerModel(newPlayerModel) {
928
1184
  const savedPos = this.player.position.clone();
929
1185
  const savedQuat = this.player.quaternion.clone();
930
1186
  const wasFirstPerson = this.isFirstPerson;
931
- if (wasFirstPerson) {
932
- this.scene.attach(this.camera);
933
- }
934
- if (this.player) {
935
- this.scene.remove(this.player);
936
- }
1187
+ if (wasFirstPerson) this.scene.attach(this.camera);
1188
+ if (this.player) this.scene.remove(this.player);
937
1189
  if (this.person) {
938
1190
  this.player.remove(this.person);
939
1191
  this.person = null;
@@ -952,45 +1204,76 @@ var PlayerController = class {
952
1204
  this.playerSpeed *= ratio;
953
1205
  this.playerFlySpeed *= ratio;
954
1206
  this.curPlayerSpeed *= ratio;
955
- this._camEpsilon *= ratio;
1207
+ this.camEpsilon *= ratio;
956
1208
  this.minCamDistance *= ratio;
957
1209
  this.maxCamDistance *= ratio;
958
1210
  this.orginMaxCamDistance *= ratio;
959
1211
  await this.loadPersonGLB();
960
1212
  this.player.position.copy(savedPos);
961
1213
  this.player.quaternion.copy(savedQuat);
962
- if (wasFirstPerson) {
963
- this.setFirstPersonCamera();
964
- }
1214
+ if (wasFirstPerson) this.setFirstPersonCamera();
965
1215
  this.setDebug(this.displayCollider);
966
1216
  }
1217
+ // 播放动画
967
1218
  playPersonAnimationByName(name, fade = 0.18) {
968
1219
  if (!this.personActions || this.ctPressed) return;
969
1220
  const next = this.personActions.get(name);
970
1221
  if (!next || this.actionState === next) return;
971
- const duration = next.getClip().duration;
972
1222
  const prev = this.actionState;
973
1223
  next.reset();
974
1224
  next.setEffectiveWeight(1);
975
- if (name == "enterCar" || name == "exitCar") {
1225
+ if (name === "enterCar" || name === "exitCar") {
1226
+ const duration = next.getClip().duration;
976
1227
  const enterTime = this.activeVehicle?.enterVehicleTime ?? 1.5;
977
1228
  next.setEffectiveTimeScale(duration / enterTime);
978
- next.setLoop(THREE3.LoopOnce, 1);
1229
+ next.setLoop(THREE4.LoopOnce, 1);
979
1230
  next.clampWhenFinished = true;
980
1231
  }
981
1232
  next.play();
982
1233
  if (prev && prev !== next) {
983
1234
  prev.fadeOut(fade);
984
1235
  next.fadeIn(fade);
985
- } else {
986
- next.fadeIn(fade);
987
- }
1236
+ } else next.fadeIn(fade);
988
1237
  this.actionState = next;
989
1238
  }
990
- // ==================== 车辆模型相关 ====================
991
- /**
992
- * 加载车辆模型
993
- */
1239
+ // 注册自定义动画
1240
+ registerAnimation(key, clipName, opts) {
1241
+ if (!this.personMixer || !this.personActions) return;
1242
+ const mixer = this.personMixer;
1243
+ const clip = this.allAnimations.find((c) => c.name === clipName);
1244
+ if (!clip) {
1245
+ console.warn(`\u627E\u4E0D\u5230 "${clipName}" \u52A8\u753B`);
1246
+ return;
1247
+ }
1248
+ const action = mixer.clipAction(clip);
1249
+ const timeScale = opts?.duration ? clip.duration / opts.duration : opts?.timeScale ?? 1;
1250
+ action.setLoop(opts?.loop === false ? THREE4.LoopOnce : THREE4.LoopRepeat, Infinity);
1251
+ action.clampWhenFinished = opts?.clampWhenFinished ?? false;
1252
+ action.setEffectiveTimeScale(timeScale);
1253
+ action.enabled = true;
1254
+ action.setEffectiveWeight(0);
1255
+ this.personActions.set(key, action);
1256
+ if (opts?.onFinished) {
1257
+ this.personMixer.addEventListener("finished", (ev) => {
1258
+ if (ev.action === action) opts.onFinished();
1259
+ });
1260
+ }
1261
+ }
1262
+ // 外部播放动画
1263
+ playAnimation(key, opts) {
1264
+ if (!this.personActions) return;
1265
+ if (!this.personActions.has(key)) {
1266
+ console.warn(`playAnimation: "${key}" \u672A\u6CE8\u518C`);
1267
+ return;
1268
+ }
1269
+ if (opts?.force) {
1270
+ const action = this.personActions.get(key);
1271
+ action.reset();
1272
+ }
1273
+ this.playPersonAnimationByName(key, opts?.fade ?? 0.18);
1274
+ }
1275
+ // ==================== 车辆 ====================
1276
+ // 加载车辆模型
994
1277
  async loadVehicleModel(opts) {
995
1278
  try {
996
1279
  if (!this.playerModel.enterCarAnim) {
@@ -998,287 +1281,31 @@ var PlayerController = class {
998
1281
  }
999
1282
  await this.initRapier();
1000
1283
  if (!this.world) return;
1001
- const scale = opts.scale ?? 1;
1002
- const chassisRatio = opts.chassisRatio ?? 0.2;
1003
- const suspensionRestLengthRatio = opts.suspensionRestLengthRatio ?? 0.2;
1004
- const speedMultiplier = opts.speedMultiplier ?? 1;
1005
- const followVehicleDirection = opts.followVehicleDirection ?? true;
1006
- this.vehicleParams.power.accelerateForce = 50 * scale;
1007
- this.vehicleParams.power.brakeForce = 200 * scale;
1008
- this.vehicleParams.power.maxSpeed = 1e4 * scale;
1009
- this.vehicleParams.followVehicleDirection = followVehicleDirection;
1010
- const vehicleModel = await this.loader.loadAsync(opts.url);
1011
- const { size: originalSize } = this.getBbox(vehicleModel.scene);
1012
- const ratio = this.vehicleLength / Math.max(originalSize.x, originalSize.y, originalSize.z);
1013
- const modelScale = ratio;
1014
- const vehicleMixer = new THREE3.AnimationMixer(vehicleModel.scene);
1015
- const animations = vehicleModel.animations ?? [];
1016
- const vehicleActions = /* @__PURE__ */ new Map();
1017
- const findClip = (name) => animations.find((a) => a.name === name);
1018
- const openDoorClip = findClip(opts.animations?.openDoorAnim || "");
1019
- if (openDoorClip) {
1020
- const action = vehicleMixer.clipAction(openDoorClip);
1021
- action.setLoop(THREE3.LoopOnce, 1);
1022
- action.clampWhenFinished = true;
1023
- action.setEffectiveTimeScale(openDoorClip.duration);
1024
- action.enabled = true;
1025
- action.setEffectiveWeight(0);
1026
- vehicleActions.set("openDoor", action);
1027
- }
1028
- const wheelObjects = [];
1029
- for (const wheelName of opts.wheelsNames) {
1030
- let found = false;
1031
- vehicleModel.scene.traverse((child) => {
1032
- if (child.name === wheelName && !found) {
1033
- wheelObjects.push(child);
1034
- found = true;
1035
- }
1036
- });
1037
- if (!found) console.warn(`\u672A\u627E\u5230\u8F6E\u5B50: ${wheelName}`);
1038
- }
1039
- const tempGroup = new THREE3.Group();
1040
- this.scene.add(tempGroup);
1041
- vehicleModel.scene.scale.multiplyScalar(modelScale * scale);
1042
- vehicleModel.scene.rotateY(this.vehicleParams.model.rotation);
1043
- const { size, bbox, center } = this.getBbox(vehicleModel.scene);
1044
- vehicleModel.scene.position.set(-center.x, -center.y, -center.z);
1045
- tempGroup.add(vehicleModel.scene);
1046
- tempGroup.updateMatrixWorld(true);
1047
- const wheelsInfo = [];
1048
- let wheelRadius = 0;
1049
- let wheelWidth = 0;
1050
- let suspensionRestLength = 0;
1051
- let chassisHeight = 0;
1052
- let wheelSizeInit = false;
1053
- for (let i = 0; i < wheelObjects.length; i++) {
1054
- const wheel = wheelObjects[i];
1055
- const worldPos = new THREE3.Vector3();
1056
- const worldQuat = new THREE3.Quaternion();
1057
- const worldScale = new THREE3.Vector3();
1058
- wheel.getWorldPosition(worldPos);
1059
- wheel.getWorldQuaternion(worldQuat);
1060
- wheel.getWorldScale(worldScale);
1061
- if (!wheelSizeInit) {
1062
- const { size: ws } = this.getBbox(wheel);
1063
- wheelRadius = Number((Math.max(ws.x, ws.y, ws.z) / 2).toFixed(2));
1064
- wheelWidth = Number(Math.min(ws.x, ws.y, ws.z).toFixed(2));
1065
- suspensionRestLength = Number((wheelRadius * 2 * suspensionRestLengthRatio).toFixed(2));
1066
- chassisHeight = Number((wheelRadius * 2 * chassisRatio).toFixed(2));
1067
- wheelSizeInit = true;
1068
- }
1069
- wheelsInfo.push({
1070
- axleCs: new THREE3.Vector3(0, 0, -1),
1071
- position: worldPos,
1072
- quaternion: worldQuat,
1073
- scale: worldScale,
1074
- radius: wheelRadius,
1075
- width: wheelWidth,
1076
- suspensionRestLength,
1077
- object: wheel
1078
- });
1079
- }
1080
- tempGroup.remove(vehicleModel.scene);
1081
- this.scene.remove(tempGroup);
1082
- const vehicleGroup = new THREE3.Group();
1083
- this.scene.add(vehicleGroup);
1084
- vehicleGroup.add(vehicleModel.scene);
1085
- vehicleGroup.updateMatrixWorld(true);
1086
- const wheelWrappers = [];
1087
- for (let i = 0; i < wheelsInfo.length; i++) {
1088
- const wheel = wheelsInfo[i];
1089
- const localPos = vehicleGroup.worldToLocal(wheel.position.clone());
1090
- const wheelWrapper = new THREE3.Group();
1091
- wheelWrapper.position.copy(localPos);
1092
- const wheelObj = wheelsInfo[i].object;
1093
- if (wheelObj.parent) wheelObj.parent.remove(wheelObj);
1094
- wheelObj.position.set(0, 0, 0);
1095
- wheelObj.quaternion.copy(wheel.quaternion);
1096
- wheelObj.scale.copy(wheel.scale);
1097
- wheelObj.updateMatrixWorld();
1098
- wheelWrapper.add(wheelObj);
1099
- vehicleGroup.add(wheelWrapper);
1100
- wheelWrappers.push(wheelWrapper);
1101
- }
1102
- const halfExtents = size.clone().multiplyScalar(0.5);
1103
- halfExtents.y -= chassisHeight / 2;
1104
- vehicleModel.scene.position.y -= chassisHeight / 2;
1105
- halfExtents.x *= 0.95;
1106
- halfExtents.z *= 0.95;
1107
- const chassisDesc = this.RAPIER.RigidBodyDesc.dynamic().setTranslation(
1108
- opts.position.x,
1109
- opts.position.y,
1110
- opts.position.z
1111
- ).setLinearDamping(this.vehicleParams.chassis.linearDamping).setAngularDamping(this.vehicleParams.chassis.angularDamping).setCanSleep(true).setAdditionalMass(10);
1112
- const chassisBody = this.world.createRigidBody(chassisDesc);
1113
- const chassisCollider = this.RAPIER.ColliderDesc.cuboid(
1114
- halfExtents.x,
1115
- halfExtents.y,
1116
- halfExtents.z
1117
- );
1118
- this.world.createCollider(chassisCollider, chassisBody);
1119
- if (this.vehicleParams.debug.showPhysicsBox) {
1120
- const debugBox = new THREE3.Mesh(
1121
- new THREE3.BoxGeometry(
1122
- halfExtents.x * 2,
1123
- halfExtents.y * 2,
1124
- halfExtents.z * 2
1125
- ),
1126
- new THREE3.MeshBasicMaterial({
1127
- color: 16711680,
1128
- wireframe: true,
1129
- transparent: true,
1130
- opacity: 0.3
1131
- })
1132
- );
1133
- vehicleGroup.add(debugBox);
1134
- }
1135
- vehicleGroup.position.copy(opts.position);
1136
- vehicleGroup.updateMatrixWorld(true);
1137
- const { vehicle, updateWheelVisuals } = createVehicleController(
1138
- this.world,
1139
- chassisBody,
1140
- wheelWrappers,
1141
- wheelsInfo
1142
- );
1143
- const vehicleInstance = {
1144
- vehicleGroup,
1145
- chassisBody,
1146
- vehicleController: vehicle,
1147
- updateWheelVisuals,
1148
- vehicleMixer,
1149
- vehicleActions,
1150
- vehiclIsOpenDoor: false,
1151
- vehicleBBox: bbox.clone(),
1152
- pathPlanner: new PathPlanner(
1153
- this._createObstacleCheckerFor(vehicleGroup, bbox, scale),
1154
- {
1155
- debugEnabled: false,
1156
- scene: this.scene,
1157
- scale: this.playerModel.scale
1158
- }
1159
- ),
1160
- scale,
1161
- boardingPoint: opts.boardingPoint,
1162
- seatOffset: opts.seatOffset ?? new THREE3.Vector3(0, 0, 0),
1163
- enterVehicleTime: 1.5,
1164
- chassisRatio,
1165
- suspensionRestLengthRatio,
1166
- size: {
1167
- l: Math.max(size.x, size.z),
1168
- w: Math.min(size.x, size.z),
1169
- h: size.y
1170
- },
1171
- speedMultiplier
1172
- };
1173
- this.vehicles.push(vehicleInstance);
1284
+ const instance = await loadVehicleModel(opts, {
1285
+ loader: this.loader,
1286
+ scene: this.scene,
1287
+ world: this.world,
1288
+ RAPIER: this.RAPIER,
1289
+ vehicleParams: this.vehicleParams,
1290
+ vehicleLength: this.vehicleLength,
1291
+ playerScale: this.playerModel.scale
1292
+ });
1293
+ this.vehicles.push(instance);
1174
1294
  this.setControllerTransition();
1175
- } catch (error) {
1176
- console.error("\u52A0\u8F7D\u8F66\u8F86\u6A21\u578B\u5931\u8D25:", error);
1295
+ } catch (e) {
1296
+ console.error("\u52A0\u8F7D\u8F66\u8F86\u6A21\u578B\u5931\u8D25:", e);
1177
1297
  }
1178
1298
  }
1299
+ // 获取包围盒
1179
1300
  getBbox(object) {
1180
- const bbox = new THREE3.Box3().setFromObject(object);
1181
- const center = new THREE3.Vector3();
1182
- const size = new THREE3.Vector3();
1301
+ const bbox = new THREE4.Box3().setFromObject(object);
1302
+ const center = new THREE4.Vector3();
1303
+ const size = new THREE4.Vector3();
1183
1304
  bbox.getCenter(center);
1184
1305
  bbox.getSize(size);
1185
1306
  return { bbox, center, size };
1186
1307
  }
1187
- /**
1188
- * 为指定车辆创建障碍物检测器
1189
- */
1190
- _createObstacleCheckerFor(vehicleGroup, bbox, scale) {
1191
- return {
1192
- isBlocked: (start, end) => {
1193
- const vehiclePos = vehicleGroup.position;
1194
- const vehicleQuat = vehicleGroup.quaternion;
1195
- const center = new THREE3.Vector3();
1196
- const size = new THREE3.Vector3();
1197
- bbox.getCenter(center);
1198
- bbox.getSize(size);
1199
- center.applyQuaternion(vehicleQuat).add(vehiclePos);
1200
- const halfSize = size.clone().multiplyScalar(0.5 * scale);
1201
- const corners = [];
1202
- for (let x = -1; x <= 1; x += 2) {
1203
- for (let y = -1; y <= 1; y += 2) {
1204
- for (let z = -1; z <= 1; z += 2) {
1205
- const localCorner = new THREE3.Vector3(
1206
- halfSize.x * x,
1207
- halfSize.y * y,
1208
- halfSize.z * z
1209
- );
1210
- const worldCorner = localCorner.applyQuaternion(vehicleQuat).add(center);
1211
- corners.push(worldCorner);
1212
- }
1213
- }
1214
- }
1215
- const expandedBBox = new THREE3.Box3();
1216
- corners.forEach((corner) => expandedBBox.expandByPoint(corner));
1217
- expandedBBox.expandByScalar(100 * this.playerModel.scale);
1218
- const direction = new THREE3.Vector3().subVectors(end, start);
1219
- const length = direction.length();
1220
- direction.normalize();
1221
- const ray = new THREE3.Ray(start, direction);
1222
- const intersection = new THREE3.Vector3();
1223
- const intersects = ray.intersectBox(expandedBBox, intersection);
1224
- return intersects !== null && start.distanceTo(intersection) < length;
1225
- },
1226
- getNavigationNodes: (start, _goal) => {
1227
- const nodes = [];
1228
- const vehiclePos = vehicleGroup.position;
1229
- const vehicleQuat = vehicleGroup.quaternion;
1230
- const vehicleForward = new THREE3.Vector3(
1231
- 0,
1232
- 0,
1233
- 1
1234
- ).applyQuaternion(vehicleQuat);
1235
- const vehicleRight = new THREE3.Vector3(1, 0, 0).applyQuaternion(
1236
- vehicleQuat
1237
- );
1238
- const bboxSize = new THREE3.Vector3();
1239
- bbox.getSize(bboxSize);
1240
- const halfLength = bboxSize.z / 2 * scale;
1241
- const halfWidth = bboxSize.x / 2 * scale;
1242
- const bypassMargin = 300 * this.playerModel.scale;
1243
- const extendedMargin = 500 * this.playerModel.scale;
1244
- const groundY = start.y;
1245
- for (const margin of [bypassMargin, extendedMargin]) {
1246
- nodes.push(
1247
- vehiclePos.clone().add(
1248
- vehicleForward.clone().multiplyScalar(halfLength + margin)
1249
- ).add(
1250
- vehicleRight.clone().multiplyScalar(-halfWidth - margin)
1251
- ).setY(groundY)
1252
- );
1253
- nodes.push(
1254
- vehiclePos.clone().add(
1255
- vehicleForward.clone().multiplyScalar(halfLength + margin)
1256
- ).add(
1257
- vehicleRight.clone().multiplyScalar(halfWidth + margin)
1258
- ).setY(groundY)
1259
- );
1260
- nodes.push(
1261
- vehiclePos.clone().add(
1262
- vehicleForward.clone().multiplyScalar(-halfLength - margin)
1263
- ).add(
1264
- vehicleRight.clone().multiplyScalar(-halfWidth - margin)
1265
- ).setY(groundY)
1266
- );
1267
- nodes.push(
1268
- vehiclePos.clone().add(
1269
- vehicleForward.clone().multiplyScalar(-halfLength - margin)
1270
- ).add(
1271
- vehicleRight.clone().multiplyScalar(halfWidth + margin)
1272
- ).setY(groundY)
1273
- );
1274
- }
1275
- return nodes;
1276
- }
1277
- };
1278
- }
1279
- /**
1280
- * 开关车门动画(操作当前 activeVehicle)
1281
- */
1308
+ // 开关车门动画
1282
1309
  openVehicleDoor(isOpen = true) {
1283
1310
  const v = this.activeVehicle;
1284
1311
  if (!v?.vehicleActions) return;
@@ -1296,112 +1323,70 @@ var PlayerController = class {
1296
1323
  next.time = duration;
1297
1324
  v.vehiclIsOpenDoor = false;
1298
1325
  }
1299
- next.setLoop(THREE3.LoopOnce, 1);
1326
+ next.setLoop(THREE4.LoopOnce, 1);
1300
1327
  next.clampWhenFinished = true;
1301
1328
  next.play();
1302
1329
  }
1303
- /**
1304
- * 上车:自动寻找最近的车辆
1305
- */
1330
+ // 触发上车流程
1306
1331
  enterVehicle() {
1307
- if (this.vehicles.length === 0 || this.isMovingToBoardingPoint) return;
1332
+ if (!this.vehicles.length || this.isMovingToBoardingPoint) return;
1308
1333
  let nearestVehicle = null;
1309
1334
  let nearestDist = Infinity;
1310
1335
  let nearBoardingPointWorld = null;
1311
1336
  for (const v2 of this.vehicles) {
1312
- const boardingPointLocal = v2.boardingPoint.clone().multiplyScalar(v2.scale);
1313
- const boardingPointWorld = new THREE3.Vector3();
1314
- v2.vehicleGroup.localToWorld(
1315
- boardingPointWorld.copy(boardingPointLocal)
1316
- );
1317
- const dist = this.player.position.distanceTo(boardingPointWorld);
1337
+ const boardingLocal = v2.boardingPoint.clone().multiplyScalar(v2.scale);
1338
+ const boardingWorld = v2.vehicleGroup.localToWorld(boardingLocal);
1339
+ const dist = this.player.position.distanceTo(boardingWorld);
1318
1340
  if (dist < 800 * this.playerModel.scale && dist < nearestDist) {
1319
1341
  nearestDist = dist;
1320
1342
  nearestVehicle = v2;
1321
- nearBoardingPointWorld = boardingPointWorld;
1343
+ nearBoardingPointWorld = boardingWorld;
1322
1344
  }
1323
1345
  }
1324
1346
  if (!nearestVehicle || !nearBoardingPointWorld) return;
1325
1347
  this.activeVehicle = nearestVehicle;
1326
1348
  const v = nearestVehicle;
1327
1349
  const vel = v.chassisBody.linvel();
1328
- const horizSpeed = Math.sqrt(vel.x * vel.x + vel.z * vel.z);
1329
- if (horizSpeed > 0.1) return;
1350
+ if (Math.sqrt(vel.x ** 2 + vel.z ** 2) > 0.1) return;
1330
1351
  this.boardingPointWorld = nearBoardingPointWorld;
1331
- const vehicleForward = new THREE3.Vector3(0, 0, 1).applyQuaternion(v.vehicleGroup.quaternion).normalize();
1332
- const path = v.pathPlanner.findPath(
1333
- this.player.position.clone(),
1334
- this.boardingPointWorld
1335
- );
1336
- this.boardingWaypoints = path;
1352
+ this.boardingWaypoints = v.pathPlanner.findPath(this.player.position.clone(), nearBoardingPointWorld);
1337
1353
  this.currentWaypointIndex = 0;
1338
- this.boardingTargetDir = vehicleForward;
1354
+ this.boardingTargetDir = new THREE4.Vector3(0, 0, 1).applyQuaternion(v.vehicleGroup.quaternion).normalize();
1339
1355
  this.isMovingToBoardingPoint = true;
1340
1356
  this.playPersonAnimationByName("walking");
1341
1357
  }
1342
- /**
1343
- * 走向上车点
1344
- */
1358
+ // 寻路移动到上车点
1345
1359
  updateMoveToBoardingPoint(delta) {
1346
- if (!this.isMovingToBoardingPoint || !this.boardingTargetDir || this.boardingWaypoints.length === 0) {
1347
- return;
1348
- }
1360
+ if (!this.isMovingToBoardingPoint || !this.boardingTargetDir || !this.boardingWaypoints.length) return;
1349
1361
  if (this.currentWaypointIndex >= this.boardingWaypoints.length) {
1350
1362
  this.finalizeBoarding(delta);
1351
1363
  return;
1352
1364
  }
1353
- const currentWaypoint = this.boardingWaypoints[this.currentWaypointIndex];
1365
+ const waypoint = this.boardingWaypoints[this.currentWaypointIndex];
1354
1366
  const currentPos = this.player.position.clone();
1355
- const horizontalDistance = new THREE3.Vector2(
1356
- currentWaypoint.x - currentPos.x,
1357
- currentWaypoint.z - currentPos.z
1358
- ).length();
1359
- const isLastWaypoint = this.currentWaypointIndex === this.boardingWaypoints.length - 1;
1360
- const waypointThreshold = isLastWaypoint ? 0 : 10 * this.playerModel.scale;
1361
- if (horizontalDistance > waypointThreshold) {
1362
- const moveDir = new THREE3.Vector3(
1363
- currentWaypoint.x - currentPos.x,
1364
- 0,
1365
- currentWaypoint.z - currentPos.z
1366
- ).normalize();
1367
- const moveDistance = Math.min(
1368
- this.boardingMoveSpeed * this.playerModel.scale * delta,
1369
- horizontalDistance
1370
- );
1371
- this.player.position.add(moveDir.multiplyScalar(moveDistance));
1372
- const lookTarget = this.player.position.clone().add(moveDir);
1373
- this.targetMat.lookAt(
1374
- this.player.position,
1375
- lookTarget,
1376
- this.player.up
1377
- );
1378
- this.targetQuat.setFromRotationMatrix(this.targetMat);
1379
- this.targetQuat.multiply(this.flip180Quat);
1380
- const rotateAlpha = Math.min(1, this.boardingRotateSpeed * delta);
1381
- this.player.quaternion.slerp(this.targetQuat, rotateAlpha);
1367
+ const isLast = this.currentWaypointIndex === this.boardingWaypoints.length - 1;
1368
+ const threshold = isLast ? 0 : 10 * this.playerModel.scale;
1369
+ const horizDist = new THREE4.Vector2(waypoint.x - currentPos.x, waypoint.z - currentPos.z).length();
1370
+ if (horizDist > threshold) {
1371
+ const moveDir = new THREE4.Vector3(waypoint.x - currentPos.x, 0, waypoint.z - currentPos.z).normalize();
1372
+ this.player.position.add(moveDir.clone().multiplyScalar(Math.min(this.boardingMoveSpeed * this.playerModel.scale * delta, horizDist)));
1373
+ this.targetMat.lookAt(this.player.position, this.player.position.clone().add(moveDir), this.player.up);
1374
+ this.targetQuat.setFromRotationMatrix(this.targetMat).multiply(this.flip180Quat);
1375
+ this.player.quaternion.slerp(this.targetQuat, Math.min(1, this.boardingRotateSpeed * delta));
1382
1376
  } else {
1383
1377
  this.currentWaypointIndex++;
1384
1378
  }
1385
1379
  }
1386
- /**
1387
- * 完成上车
1388
- */
1380
+ // 最终对齐朝向
1389
1381
  finalizeBoarding(delta) {
1390
1382
  const v = this.activeVehicle;
1391
1383
  if (!this.boardingTargetDir || !v || !this.isMovingToBoardingPoint) return;
1392
- const currentDir = new THREE3.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion).normalize();
1393
- const targetDir = this.boardingTargetDir.clone().normalize();
1394
- const angleDiff = currentDir.angleTo(targetDir);
1395
- if (angleDiff > 0.01) {
1396
- const lookTarget = this.player.position.clone().add(targetDir);
1397
- this.targetMat.lookAt(
1398
- this.player.position,
1399
- lookTarget,
1400
- this.player.up
1401
- );
1384
+ const currentDir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion).normalize();
1385
+ if (currentDir.angleTo(this.boardingTargetDir) > 0.01) {
1386
+ const lookTarget = this.player.position.clone().add(this.boardingTargetDir);
1387
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1402
1388
  this.targetQuat.setFromRotationMatrix(this.targetMat);
1403
- const rotateAlpha = Math.min(1, this.boardingRotateSpeed * delta);
1404
- this.player.quaternion.slerp(this.targetQuat, rotateAlpha);
1389
+ this.player.quaternion.slerp(this.targetQuat, Math.min(1, this.boardingRotateSpeed * delta));
1405
1390
  } else {
1406
1391
  this.boardingWaypoints = [];
1407
1392
  this.currentWaypointIndex = 0;
@@ -1415,23 +1400,20 @@ var PlayerController = class {
1415
1400
  this.player.quaternion.multiply(this.flip180Quat);
1416
1401
  }
1417
1402
  }
1403
+ // 上车动画完成
1418
1404
  onEnterCarAnimFinished() {
1419
1405
  const v = this.activeVehicle;
1420
1406
  if (!v || !this.isMovingToBoardingPoint) return;
1421
1407
  this.player.updateMatrixWorld(true);
1422
1408
  const offsetY = this.boardingPointWorld.y - this.player.position.y;
1423
1409
  this.controllerMode = 1;
1424
- this.syncControllerModeBtnEl();
1410
+ this.syncMobileControllerMode();
1425
1411
  this.setOverShoulderView(false);
1426
1412
  v.vehicleGroup.attach(this.player);
1427
- this.player.position.add(
1428
- v.seatOffset.clone().multiplyScalar(v.scale).add(new THREE3.Vector3(0, offsetY, 0))
1429
- );
1413
+ this.player.position.add(v.seatOffset.clone().multiplyScalar(v.scale).add(new THREE4.Vector3(0, offsetY, 0)));
1430
1414
  this.isMovingToBoardingPoint = false;
1431
1415
  }
1432
- /**
1433
- * 下车
1434
- */
1416
+ // 触发下车流程
1435
1417
  exitVehicle() {
1436
1418
  const v = this.activeVehicle;
1437
1419
  if (!v) return;
@@ -1440,9 +1422,7 @@ var PlayerController = class {
1440
1422
  this.currentWaypointIndex = 0;
1441
1423
  this.boardingTargetDir = null;
1442
1424
  const vel = v.chassisBody.linvel();
1443
- const horizSpeed = Math.sqrt(vel.x * vel.x + vel.z * vel.z);
1444
- const isStationary = horizSpeed < 0.1;
1445
- if (isStationary) {
1425
+ if (Math.sqrt(vel.x ** 2 + vel.z ** 2) < 0.1) {
1446
1426
  this.playPersonAnimationByName("exitCar");
1447
1427
  this.isExitAnimPlaying = true;
1448
1428
  this.closeExitDoorTriggered = false;
@@ -1451,95 +1431,90 @@ var PlayerController = class {
1451
1431
  }
1452
1432
  this.openVehicleDoor(true);
1453
1433
  this.controllerMode = 0;
1454
- this.syncControllerModeBtnEl();
1434
+ this.syncMobileControllerMode();
1455
1435
  this.setOverShoulderView(this.enableOverShoulderView);
1456
1436
  this.scene.attach(this.player);
1457
- if (this.isFirstPerson) {
1458
- this.setFirstPersonCamera();
1459
- }
1437
+ if (this.isFirstPerson) this.setFirstPersonCamera();
1460
1438
  this.setControllerTransition();
1461
1439
  }
1462
- // ==================== 相机与视角控制 ====================
1440
+ // ==================== 相机与视角 ====================
1441
+ // 切换第一/三人称
1463
1442
  changeView() {
1464
1443
  this.isFirstPerson = !this.isFirstPerson;
1465
1444
  if (this.isFirstPerson) {
1466
- this.setFirstPersonCamera();
1445
+ const camWorldDir = new THREE4.Vector3();
1446
+ this.camera.getWorldDirection(camWorldDir);
1447
+ const flatDir = new THREE4.Vector3(camWorldDir.x, 0, camWorldDir.z).normalize();
1448
+ if (flatDir.lengthSq() > 1e-3) {
1449
+ const yAngle = Math.atan2(flatDir.x, flatDir.z);
1450
+ this.player.rotation.set(0, yAngle + Math.PI, 0);
1451
+ }
1452
+ const vertAngle = Math.asin(THREE4.MathUtils.clamp(-camWorldDir.y, -1, 1));
1453
+ this.setFirstPersonCamera(vertAngle);
1467
1454
  this.setOverShoulderView(false);
1468
1455
  } else {
1469
1456
  this.controls.enabled = true;
1470
1457
  this.scene.attach(this.camera);
1471
- const worldPos = this.player.position.clone();
1472
- const dir = new THREE3.Vector3(0, 0, -1).applyQuaternion(
1473
- this.player.quaternion
1474
- );
1458
+ const dir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1475
1459
  const angle = Math.atan2(dir.z, dir.x);
1476
- const offset = new THREE3.Vector3(
1477
- Math.cos(angle) * 400 * this.playerModel.scale,
1478
- 200 * this.playerModel.scale,
1479
- Math.sin(angle) * 400 * this.playerModel.scale
1480
- );
1481
- this.camera.position.copy(worldPos).add(offset);
1482
- this.controls.target.copy(worldPos);
1460
+ const s = this.playerModel.scale;
1461
+ this.camera.position.copy(this.player.position).add(new THREE4.Vector3(Math.cos(angle) * 400 * s, 200 * s, Math.sin(angle) * 400 * s));
1462
+ this.controls.target.copy(this.player.position);
1483
1463
  this.controls.enableZoom = this.enableZoom;
1484
1464
  this.setOverShoulderView(this.enableOverShoulderView);
1485
1465
  }
1486
1466
  this.setPointerLock();
1487
1467
  }
1488
- setFirstPersonCamera() {
1468
+ // 设置第一人称相机
1469
+ setFirstPersonCamera(vertAngle = 0) {
1489
1470
  this.controls.enabled = false;
1490
1471
  if (this.personHead) {
1491
- this.personHead?.attach(this.camera);
1472
+ this.personHead.attach(this.camera);
1492
1473
  this.camera.position.set(0, 10, 20);
1493
1474
  } else {
1494
1475
  this.player.attach(this.camera);
1495
- this.camera.position.set(
1496
- 0,
1497
- 40 * this.playerModel.scale,
1498
- 30 * this.playerModel.scale
1499
- );
1476
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1500
1477
  }
1501
- this.camera.rotation.set(0, Math.PI, 0);
1478
+ this.camera.rotation.set(
1479
+ THREE4.MathUtils.clamp(vertAngle, -1.1, 1.4),
1480
+ Math.PI,
1481
+ 0
1482
+ );
1502
1483
  this.controls.enableZoom = false;
1503
1484
  }
1485
+ // 设置鼠标锁定
1504
1486
  setPointerLock() {
1487
+ if (!document.body.requestPointerLock) return;
1505
1488
  if ((this.thirdMouseMode === 0 || this.thirdMouseMode === 1) && !this.isFirstPerson || this.isFirstPerson) {
1506
1489
  document.body.requestPointerLock();
1507
1490
  } else {
1508
1491
  document.exitPointerLock();
1509
1492
  }
1510
1493
  }
1494
+ // 初始相机位置
1511
1495
  setCameraPos() {
1512
1496
  requestAnimationFrame(() => {
1513
- if (this.isFirstPerson) {
1514
- this.person.add(this.camera);
1515
- this.camera.position.set(
1516
- 0,
1517
- 40 * this.playerModel.scale,
1518
- 30 * this.playerModel.scale
1519
- );
1520
- } else {
1521
- const worldPos = this.player.position.clone();
1522
- const dir = new THREE3.Vector3(0, 0, -1).applyQuaternion(
1523
- this.player.quaternion
1524
- );
1497
+ if (!this.isFirstPerson) {
1498
+ const dir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1525
1499
  const angle = Math.atan2(dir.z, dir.x);
1526
- const offset = new THREE3.Vector3(
1527
- Math.cos(angle) * 400 * this.playerModel.scale,
1528
- 200 * this.playerModel.scale,
1529
- Math.sin(angle) * 400 * this.playerModel.scale
1530
- );
1531
- this.camera.position.copy(worldPos).add(offset);
1500
+ const s = this.playerModel.scale;
1501
+ this.camera.position.copy(this.player.position).add(new THREE4.Vector3(Math.cos(angle) * 400 * s, 200 * s, Math.sin(angle) * 400 * s));
1532
1502
  this.controls.enableZoom = this.enableZoom;
1503
+ } else {
1504
+ this.person.add(this.camera);
1505
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1533
1506
  }
1534
1507
  this.camera.updateProjectionMatrix();
1535
1508
  });
1536
1509
  }
1510
+ // 初始化轨道控制
1537
1511
  setControls() {
1538
1512
  this.controls.enableZoom = this.enableZoom;
1539
- this.controls.rotateSpeed = this.mouseSensity * 0.05;
1513
+ this.controls.rotateSpeed = this.mouseSensitivity * 0.05;
1540
1514
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
1541
1515
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
1542
1516
  }
1517
+ // 重置轨道控制
1543
1518
  resetControls() {
1544
1519
  if (!this.controls) return;
1545
1520
  this.controls.enabled = true;
@@ -1549,214 +1524,131 @@ var PlayerController = class {
1549
1524
  this.controls.enableZoom = true;
1550
1525
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
1551
1526
  }
1527
+ // 视角旋转处理
1552
1528
  setToward(dx, dy, speed) {
1553
- if (this.controllerMode == 0) {
1529
+ const sens = this.mouseSensitivity;
1530
+ if (this.controllerMode === 0) {
1554
1531
  if (this.isFirstPerson) {
1555
1532
  if (this.isMovingToBoardingPoint) return;
1556
- const yaw = -dx * speed * this.mouseSensity;
1557
- const pitch = -dy * speed * this.mouseSensity;
1558
- this.player.rotateY(yaw);
1559
- this.camera.rotation.x = THREE3.MathUtils.clamp(
1560
- this.camera.rotation.x + pitch,
1561
- -1.1,
1562
- 1.4
1563
- );
1533
+ this.player.rotateY(-dx * speed * sens);
1534
+ this.camera.rotation.x = THREE4.MathUtils.clamp(this.camera.rotation.x + -dy * speed * sens, -1.1, 1.4);
1564
1535
  } else {
1565
- const sensitivity = this.mouseSensity;
1566
- const deltaX = -dx * speed * sensitivity;
1567
- const deltaY = -dy * speed * sensitivity;
1568
- const target = this.player.position.clone();
1569
- const distance = this.camera.position.distanceTo(target);
1570
- const currentPosition = this.camera.position.clone().sub(target);
1571
- let theta = Math.atan2(currentPosition.x, currentPosition.z);
1572
- let phi = Math.acos(currentPosition.y / distance);
1573
- theta += deltaX;
1574
- phi += deltaY;
1575
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1576
- const newX = distance * Math.sin(phi) * Math.sin(theta);
1577
- const newY = distance * Math.cos(phi);
1578
- const newZ = distance * Math.sin(phi) * Math.cos(theta);
1579
- this.camera.position.set(
1580
- target.x + newX,
1581
- target.y + newY,
1582
- target.z + newZ
1583
- );
1584
- this.camera.lookAt(target);
1536
+ this.orbitCamera(this.player.position, -dx * speed * sens, -dy * speed * sens);
1585
1537
  }
1586
1538
  } else {
1587
1539
  const v = this.activeVehicle;
1588
1540
  if (!v) return;
1589
1541
  if (this.isFirstPerson) {
1590
- const yaw = -dx * speed * this.mouseSensity;
1591
- const pitch = -dy * speed * this.mouseSensity;
1592
- this.camera.rotation.y = THREE3.MathUtils.clamp(
1593
- this.camera.rotation.y + yaw,
1594
- Math.PI * (3 / 4),
1595
- Math.PI * (5 / 4)
1596
- );
1597
- this.camera.rotation.x = THREE3.MathUtils.clamp(
1598
- this.camera.rotation.x + pitch,
1599
- 0,
1600
- Math.PI * (1 / 3)
1601
- );
1542
+ this.camera.rotation.y = THREE4.MathUtils.clamp(this.camera.rotation.y + -dx * speed * sens, Math.PI * (3 / 4), Math.PI * (5 / 4));
1543
+ this.camera.rotation.x = THREE4.MathUtils.clamp(this.camera.rotation.x + -dy * speed * sens, 0, Math.PI * (1 / 3));
1602
1544
  } else {
1603
- const sensitivity = this.mouseSensity;
1604
- const deltaX = -dx * speed * sensitivity;
1605
- const deltaY = -dy * speed * sensitivity;
1606
- const target = v.vehicleGroup.position.clone();
1607
- const distance = this.camera.position.distanceTo(target);
1608
- const currentPosition = this.camera.position.clone().sub(target);
1609
- let theta = Math.atan2(currentPosition.x, currentPosition.z);
1610
- let phi = Math.acos(currentPosition.y / distance);
1611
- theta += deltaX;
1612
- phi += deltaY;
1613
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1614
- const newX = distance * Math.sin(phi) * Math.sin(theta);
1615
- const newY = distance * Math.cos(phi);
1616
- const newZ = distance * Math.sin(phi) * Math.cos(theta);
1617
- this.camera.position.set(
1618
- target.x + newX,
1619
- target.y + newY,
1620
- target.z + newZ
1621
- );
1622
- this.camera.lookAt(target);
1545
+ this.orbitCamera(v.vehicleGroup.position, -dx * speed * sens, -dy * speed * sens);
1623
1546
  }
1624
1547
  }
1625
1548
  }
1549
+ // 球面轨道旋转
1550
+ orbitCamera(target, deltaX, deltaY) {
1551
+ const distance = this.camera.position.distanceTo(target);
1552
+ const cur = this.camera.position.clone().sub(target);
1553
+ let theta = Math.atan2(cur.x, cur.z) + deltaX;
1554
+ let phi = Math.acos(THREE4.MathUtils.clamp(cur.y / distance, -1, 1)) + deltaY;
1555
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1556
+ this.camera.position.set(
1557
+ target.x + distance * Math.sin(phi) * Math.sin(theta),
1558
+ target.y + distance * Math.cos(phi),
1559
+ target.z + distance * Math.sin(phi) * Math.cos(theta)
1560
+ );
1561
+ this.camera.lookAt(target);
1562
+ }
1563
+ // ==================== 碰撞体构建 ====================
1564
+ // 补全几何属性
1565
+ ensureAttributesMinimal(geom) {
1566
+ if (!geom.attributes.position) return null;
1567
+ if (!geom.attributes.normal) geom.computeVertexNormals();
1568
+ if (!geom.attributes.uv) {
1569
+ const count = geom.attributes.position.count;
1570
+ geom.setAttribute("uv", new THREE4.BufferAttribute(new Float32Array(count * 2), 2));
1571
+ }
1572
+ return geom;
1573
+ }
1574
+ // 统一几何属性格式
1626
1575
  unifiedAttribute(collected) {
1627
1576
  const attrMap = /* @__PURE__ */ new Map();
1628
1577
  const attrConflict = /* @__PURE__ */ new Set();
1629
- const requiredAttrs = /* @__PURE__ */ new Set(["position", "normal", "uv"]);
1630
- for (const g of collected) {
1631
- const attrNames2 = Object.keys(g.attributes);
1632
- for (const name of attrNames2) {
1633
- if (!requiredAttrs.has(name)) {
1634
- g.deleteAttribute(name);
1635
- }
1636
- }
1637
- }
1578
+ const required = /* @__PURE__ */ new Set(["position", "normal", "uv"]);
1579
+ for (const g of collected)
1580
+ for (const name of Object.keys(g.attributes))
1581
+ if (!required.has(name)) g.deleteAttribute(name);
1638
1582
  for (const g of collected) {
1639
1583
  for (const name of Object.keys(g.attributes)) {
1640
1584
  const attr = g.attributes[name];
1641
1585
  const ctor = attr.array.constructor;
1642
- const itemSize = attr.itemSize;
1643
- const normalized = attr.normalized;
1644
1586
  if (!attrMap.has(name)) {
1645
- attrMap.set(name, {
1646
- itemSize,
1647
- arrayCtor: ctor,
1648
- examples: 1,
1649
- normalized
1650
- });
1587
+ attrMap.set(name, { itemSize: attr.itemSize, arrayCtor: ctor, examples: 1, normalized: attr.normalized });
1651
1588
  } else {
1652
1589
  const m = attrMap.get(name);
1653
- if (m.itemSize !== itemSize || m.arrayCtor !== ctor || m.normalized !== normalized) {
1654
- attrConflict.add(name);
1655
- } else {
1656
- m.examples++;
1657
- }
1590
+ if (m.itemSize !== attr.itemSize || m.arrayCtor !== ctor || m.normalized !== attr.normalized) attrConflict.add(name);
1591
+ else m.examples++;
1658
1592
  }
1659
1593
  }
1660
1594
  }
1661
- if (attrConflict.size) {
1662
- for (const g of collected) {
1663
- for (const name of Array.from(attrConflict)) {
1664
- if (g.attributes[name]) g.deleteAttribute(name);
1665
- }
1666
- }
1667
- for (const name of attrConflict) attrMap.delete(name);
1595
+ for (const name of attrConflict) {
1596
+ for (const g of collected) if (g.attributes[name]) g.deleteAttribute(name);
1597
+ attrMap.delete(name);
1668
1598
  }
1669
- const attrNames = Array.from(attrMap.keys());
1670
- for (const g of collected) {
1671
- const count = g.attributes.position.count;
1672
- for (const name of attrNames) {
1599
+ for (const [name, meta] of attrMap) {
1600
+ for (const g of collected) {
1673
1601
  if (!g.attributes[name]) {
1674
- const meta = attrMap.get(name);
1675
- const len = count * meta.itemSize;
1676
- const array = new meta.arrayCtor(len);
1677
- g.setAttribute(
1678
- name,
1679
- new THREE3.BufferAttribute(
1680
- array,
1681
- meta.itemSize,
1682
- meta.normalized
1683
- )
1684
- );
1602
+ const count = g.attributes.position.count;
1603
+ g.setAttribute(name, new THREE4.BufferAttribute(new meta.arrayCtor(count * meta.itemSize), meta.itemSize, meta.normalized));
1685
1604
  }
1686
1605
  }
1687
1606
  }
1688
1607
  return collected;
1689
1608
  }
1609
+ // 构建静态 BVH
1690
1610
  async createBVH(meshUrl = "") {
1691
1611
  await this.initLoader();
1612
+ const collectMesh = (mesh) => {
1613
+ try {
1614
+ let geom = mesh.geometry.clone();
1615
+ geom.applyMatrix4(mesh.matrixWorld);
1616
+ if (geom.index) geom = geom.toNonIndexed();
1617
+ const safe = this.ensureAttributesMinimal(geom);
1618
+ if (safe) this.collected.push(safe);
1619
+ } catch (e) {
1620
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1621
+ }
1622
+ };
1692
1623
  if (meshUrl === "") {
1693
1624
  if (this.collider) {
1694
1625
  this.scene.remove(this.collider);
1695
1626
  this.collider = null;
1696
1627
  }
1697
1628
  this.scene.traverse((c) => {
1698
- const mesh = c;
1699
- if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1700
- try {
1701
- let geom = mesh.geometry.clone();
1702
- geom.applyMatrix4(mesh.matrixWorld);
1703
- if (geom.index) geom = geom.toNonIndexed();
1704
- const safe = this.ensureAttributesMinimal(geom);
1705
- if (safe) this.collected.push(safe);
1706
- } catch (e) {
1707
- console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1708
- }
1709
- }
1629
+ const m = c;
1630
+ if (m?.isMesh && m.geometry && c.name !== "capsule") collectMesh(m);
1710
1631
  });
1711
- if (!this.collected.length) return;
1712
- this.collected = this.unifiedAttribute(this.collected);
1713
1632
  } else {
1714
1633
  const gltf = await this.loader.loadAsync(meshUrl);
1715
- const obj = gltf.scene.children[0];
1716
- if (obj && obj?.geometry) {
1717
- const mesh = obj;
1718
- let geom = mesh.geometry.clone();
1719
- geom.applyMatrix4(mesh.matrixWorld);
1720
- if (geom.index) geom = geom.toNonIndexed();
1721
- const safe = this.ensureAttributesMinimal(geom);
1722
- if (safe) this.collected.push(safe);
1634
+ const root = gltf.scene.children[0];
1635
+ if (root?.geometry) {
1636
+ collectMesh(root);
1723
1637
  } else {
1724
- obj.traverse((c) => {
1725
- const mesh = c;
1726
- if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1727
- try {
1728
- let geom = mesh.geometry.clone();
1729
- geom.applyMatrix4(mesh.matrixWorld);
1730
- if (geom.index) geom = geom.toNonIndexed();
1731
- const safe = this.ensureAttributesMinimal(geom);
1732
- if (safe) this.collected.push(safe);
1733
- } catch (e) {
1734
- console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1735
- }
1736
- }
1638
+ root?.traverse((c) => {
1639
+ if (c?.isMesh && c.geometry && c.name !== "capsule") collectMesh(c);
1737
1640
  });
1738
- if (!this.collected.length) return;
1739
- this.collected = this.unifiedAttribute(this.collected);
1740
1641
  }
1741
1642
  }
1742
- const merged = BufferGeometryUtils.mergeGeometries(
1743
- this.collected,
1744
- false
1745
- );
1643
+ if (!this.collected.length) return;
1644
+ this.collected = this.unifiedAttribute(this.collected);
1645
+ const merged = BufferGeometryUtils.mergeGeometries(this.collected, false);
1746
1646
  if (!merged) {
1747
1647
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1748
1648
  return;
1749
1649
  }
1750
1650
  merged.boundsTree = new import_three_mesh_bvh.MeshBVH(merged, { maxDepth: 100 });
1751
- this.collider = new THREE3.Mesh(
1752
- merged,
1753
- new THREE3.MeshBasicMaterial({
1754
- opacity: 0.5,
1755
- transparent: true,
1756
- wireframe: true,
1757
- depthTest: true
1758
- })
1759
- );
1651
+ this.collider = new THREE4.Mesh(merged, new THREE4.MeshBasicMaterial({ opacity: 0.5, transparent: true, wireframe: true, depthTest: true }));
1760
1652
  if (this.displayCollider) this.scene.add(this.collider);
1761
1653
  if (this.displayVisualizer) {
1762
1654
  if (this.visualizer) this.scene.remove(this.visualizer);
@@ -1765,165 +1657,108 @@ var PlayerController = class {
1765
1657
  }
1766
1658
  this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
1767
1659
  }
1660
+ // 构建动态 BVH
1768
1661
  createDynamicBVH(objects = []) {
1769
1662
  if (this.dynamicCollider) {
1770
1663
  this.scene.remove(this.dynamicCollider);
1771
1664
  this.dynamicCollider = null;
1772
1665
  }
1773
1666
  this.dynamicCollected = [];
1774
- objects.forEach((object) => {
1775
- object.traverse((c) => {
1776
- const mesh = c;
1777
- if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1778
- try {
1779
- let geom = mesh.geometry.clone();
1780
- geom.applyMatrix4(mesh.matrixWorld);
1781
- if (geom.index) geom = geom.toNonIndexed();
1782
- const safe = this.ensureAttributesMinimal(geom);
1783
- if (safe) this.dynamicCollected.push(safe);
1784
- } catch (e) {
1785
- console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1786
- }
1667
+ objects.forEach((obj) => obj.traverse((c) => {
1668
+ const m = c;
1669
+ if (m?.isMesh && m.geometry && c.name !== "capsule") {
1670
+ try {
1671
+ let geom = m.geometry.clone();
1672
+ geom.applyMatrix4(m.matrixWorld);
1673
+ if (geom.index) geom = geom.toNonIndexed();
1674
+ const safe = this.ensureAttributesMinimal(geom);
1675
+ if (safe) this.dynamicCollected.push(safe);
1676
+ } catch (e) {
1677
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", m, e);
1787
1678
  }
1788
- });
1789
- });
1679
+ }
1680
+ }));
1790
1681
  if (!this.dynamicCollected.length) return;
1791
1682
  this.dynamicCollected = this.unifiedAttribute(this.dynamicCollected);
1792
- const merged = BufferGeometryUtils.mergeGeometries(
1793
- this.dynamicCollected,
1794
- false
1795
- );
1683
+ const merged = BufferGeometryUtils.mergeGeometries(this.dynamicCollected, false);
1796
1684
  if (!merged) {
1797
1685
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1798
1686
  return;
1799
1687
  }
1800
1688
  merged.boundsTree = new import_three_mesh_bvh.MeshBVH(merged);
1801
- this.dynamicCollider = new THREE3.Mesh(
1802
- merged,
1803
- new THREE3.MeshBasicMaterial({
1804
- opacity: 0.5,
1805
- transparent: true,
1806
- wireframe: true,
1807
- depthTest: true
1808
- })
1809
- );
1689
+ this.dynamicCollider = new THREE4.Mesh(merged, new THREE4.MeshBasicMaterial({ opacity: 0.5, transparent: true, wireframe: true, depthTest: true }));
1810
1690
  if (this.displayCollider) this.scene.add(this.dynamicCollider);
1811
1691
  }
1812
- getAngleWithYAxis(normal) {
1813
- const yAxis = { x: 0, y: 1, z: 0 };
1814
- const dotProduct = normal.x * yAxis.x + normal.y * yAxis.y + normal.z * yAxis.z;
1815
- const normalMagnitude = Math.sqrt(
1816
- normal.x * normal.x + normal.y * normal.y + normal.z * normal.z
1817
- );
1818
- const cosTheta = dotProduct / normalMagnitude;
1819
- return Math.acos(cosTheta);
1820
- }
1821
- // ==================== 设置控制器过渡 ====================
1692
+ // ==================== 控制器过渡 ====================
1693
+ // 车辆切换过渡
1822
1694
  setControllerTransition() {
1823
1695
  if (this.isChangeControllerTransitionTimer) {
1824
1696
  clearTimeout(this.isChangeControllerTransitionTimer);
1825
1697
  this.isChangeControllerTransitionTimer = null;
1826
1698
  }
1827
- let vGroups = [];
1828
- for (const v of this.vehicles) {
1829
- vGroups.push(v.vehicleGroup);
1830
- }
1699
+ const vGroups = this.vehicles.map((v) => v.vehicleGroup);
1831
1700
  this.createDynamicBVH(vGroups);
1832
1701
  this.isChangeControllerTransitionTimer = setTimeout(() => {
1833
1702
  this.isChangeControllerTransitionTimer = null;
1834
- for (const v of this.vehicles) {
1835
- this.clearVehicleVelocity(v);
1836
- }
1703
+ this.vehicles.forEach((v) => this.clearVehicleVelocity(v));
1837
1704
  this.createDynamicBVH(vGroups);
1838
1705
  }, 3e3);
1839
1706
  }
1840
- // 清除车辆速度
1707
+ // 清零车辆速度
1841
1708
  clearVehicleVelocity(v) {
1842
1709
  if (!v || !this.world || !this.RAPIER) return;
1843
1710
  const { chassisBody, vehicleController } = v;
1844
1711
  const ZERO = new this.RAPIER.Vector3(0, 0, 0);
1845
1712
  chassisBody.setLinvel(ZERO, true);
1846
1713
  chassisBody.setAngvel(ZERO, true);
1847
- const BIG_BRAKE = 1e6;
1848
1714
  for (let i = 0; i < 4; i++) {
1849
1715
  vehicleController.setWheelEngineForce(i, 0);
1850
- vehicleController.setWheelBrake(i, BIG_BRAKE);
1716
+ vehicleController.setWheelBrake(i, 1e6);
1851
1717
  }
1852
1718
  vehicleController.updateVehicle(1 / 60);
1853
1719
  this.world.timestep = 1 / 60;
1854
1720
  this.world.step();
1855
1721
  chassisBody.setLinvel(ZERO, true);
1856
1722
  chassisBody.setAngvel(ZERO, true);
1857
- for (let i = 0; i < 4; i++) {
1858
- vehicleController.setWheelBrake(i, 0);
1859
- }
1723
+ for (let i = 0; i < 4; i++) vehicleController.setWheelBrake(i, 0);
1860
1724
  }
1861
1725
  // ==================== 循环更新 ====================
1726
+ // 主循环更新
1862
1727
  async update(delta = clock.getDelta()) {
1863
1728
  if (!this.isupdate || !this.player || !this.collider) return;
1864
1729
  delta = Math.min(delta, 1 / 40);
1865
- if (this.controllerMode == 1) {
1730
+ if (this.controllerMode === 1) {
1866
1731
  this.updateVehicle(delta);
1867
1732
  } else {
1868
1733
  this.updatePlayer(delta);
1869
- if (this.isChangeControllerTransitionTimer)
1870
- this.updateVehicleInertia(delta);
1734
+ if (this.isChangeControllerTransitionTimer) this.updateVehicleInertia(delta);
1871
1735
  }
1872
1736
  }
1873
- /**
1874
- * 更新当前驾驶的车辆
1875
- */
1737
+ // 车辆帧更新
1876
1738
  updateVehicle(delta) {
1877
1739
  const v = this.activeVehicle;
1878
1740
  if (!v || !this.world) return;
1879
1741
  const { vehicleController, chassisBody, vehicleGroup } = v;
1880
1742
  const rotation = chassisBody.rotation();
1881
- const quat = new THREE3.Quaternion(
1882
- rotation.x,
1883
- rotation.y,
1884
- rotation.z,
1885
- rotation.w
1886
- );
1887
- const forward = new THREE3.Vector3(1, 0, 0).applyQuaternion(quat);
1743
+ const quat = new THREE4.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
1744
+ const forward = new THREE4.Vector3(1, 0, 0).applyQuaternion(quat);
1888
1745
  const slopeAngle = Math.asin(forward.y);
1889
- let factor = 1;
1890
- if (slopeAngle < -0.05 && this.fwdPressed) factor = -Math.sin(slopeAngle) * 10;
1746
+ const factor = slopeAngle < -0.05 && this.fwdPressed ? -Math.sin(slopeAngle) * 10 : 1;
1891
1747
  const accelerateForce = this.vehicleParams.power.accelerateForce * v.speedMultiplier;
1892
1748
  const maxSpeed = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
1893
- const engineForce = (Number(this.fwdPressed) * accelerateForce - Number(this.bkdPressed) * accelerateForce) * factor;
1894
- vehicleController.setWheelEngineForce(0, engineForce);
1895
- vehicleController.setWheelEngineForce(1, engineForce);
1896
- vehicleController.setWheelEngineForce(2, engineForce);
1897
- vehicleController.setWheelEngineForce(3, engineForce);
1749
+ const engineForce = (Number(this.fwdPressed) - Number(this.bkdPressed)) * accelerateForce * factor;
1750
+ for (let i = 0; i < 4; i++) vehicleController.setWheelEngineForce(i, engineForce);
1898
1751
  const wheelBrake = Number(this.spacePressed) * this.vehicleParams.power.brakeForce * delta;
1899
- vehicleController.setWheelBrake(0, wheelBrake);
1900
- vehicleController.setWheelBrake(1, wheelBrake);
1901
- vehicleController.setWheelBrake(2, wheelBrake);
1902
- vehicleController.setWheelBrake(3, wheelBrake);
1752
+ for (let i = 0; i < 4; i++) vehicleController.setWheelBrake(i, wheelBrake);
1903
1753
  const currentSteering = vehicleController.wheelSteering(0) || 0;
1904
- const steerDirection = Number(this.lftPressed) - Number(this.rgtPressed);
1905
- let steerSpeed;
1906
- if (steerDirection === 0) {
1907
- steerSpeed = this.vehicleParams.steering.steerReturnSpeed || 0.15;
1908
- } else {
1909
- steerSpeed = this.vehicleParams.steering.steerSpeed || 0.08;
1910
- }
1911
- const steerLerpFactor = 1 - Math.pow(1 - steerSpeed, delta);
1912
- const targetSteering = this.vehicleParams.steering.maxSteerAngle * steerDirection;
1913
- const steering = THREE3.MathUtils.lerp(
1914
- currentSteering,
1915
- targetSteering,
1916
- steerLerpFactor
1917
- );
1754
+ const steerDir = Number(this.lftPressed) - Number(this.rgtPressed);
1755
+ const steerSpeed = steerDir === 0 ? this.vehicleParams.steering.steerReturnSpeed : this.vehicleParams.steering.steerSpeed;
1756
+ const steering = THREE4.MathUtils.lerp(currentSteering, this.vehicleParams.steering.maxSteerAngle * steerDir, 1 - Math.pow(1 - steerSpeed, delta));
1918
1757
  vehicleController.setWheelSteering(0, steering);
1919
1758
  vehicleController.setWheelSteering(1, steering);
1920
- if ((this.rgtPressed || this.lftPressed) && this.shiftPressed) {
1921
- vehicleController.setWheelSideFrictionStiffness(2, 0.5);
1922
- vehicleController.setWheelSideFrictionStiffness(3, 0.5);
1923
- } else {
1924
- vehicleController.setWheelSideFrictionStiffness(2, 2);
1925
- vehicleController.setWheelSideFrictionStiffness(3, 2);
1926
- }
1759
+ const driftFriction = (this.rgtPressed || this.lftPressed) && this.shiftPressed ? 0.5 : 2;
1760
+ vehicleController.setWheelSideFrictionStiffness(2, driftFriction);
1761
+ vehicleController.setWheelSideFrictionStiffness(3, driftFriction);
1927
1762
  this.updateVehicleInertia(delta);
1928
1763
  if (!this.isFirstPerson) {
1929
1764
  const lookTarget = vehicleGroup.position.clone();
@@ -1932,99 +1767,49 @@ var PlayerController = class {
1932
1767
  this.camera.position.add(lookTarget);
1933
1768
  this.controls.update();
1934
1769
  const velocity = chassisBody.linvel();
1935
- const currentSpeed = Math.sqrt(
1936
- velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z
1937
- );
1770
+ const currentSpeed = new THREE4.Vector3(velocity.x, velocity.y, velocity.z).length();
1938
1771
  const speedRatio = Math.min(currentSpeed / maxSpeed, 1);
1939
- const baseCamDistance = v.size.l * 0.8;
1940
- const maxCamDistanceLimit = v.size.l * 5;
1941
- const targetDistance = THREE3.MathUtils.lerp(
1942
- baseCamDistance,
1943
- maxCamDistanceLimit,
1944
- speedRatio
1945
- );
1946
- this._personToCam.subVectors(
1947
- this.camera.position,
1948
- vehicleGroup.position
1949
- );
1950
- const origin = vehicleGroup.position.clone().add(new THREE3.Vector3(0, 0, 0));
1951
- const direction = this._personToCam.clone().normalize();
1952
- const desiredDist = targetDistance;
1953
- this._raycasterPersonToCam.set(origin, direction);
1954
- this._raycasterPersonToCam.far = desiredDist;
1955
- const intersects = this._raycasterPersonToCam.intersectObject(
1956
- this.collider,
1957
- false
1958
- );
1959
- if (intersects.length > 0) {
1960
- const hit = intersects[0];
1961
- const safeDist = Math.max(
1962
- hit.distance - this._camEpsilon,
1963
- this.minCamDistance
1964
- );
1965
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1966
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1772
+ const baseDist = v.size.l * 0.8;
1773
+ const maxDist = v.size.l * 5;
1774
+ const desiredDist = THREE4.MathUtils.lerp(baseDist, maxDist, speedRatio);
1775
+ const minSafeDist = baseDist;
1776
+ this.personToCam.subVectors(this.camera.position, vehicleGroup.position);
1777
+ const direction = this.personToCam.clone().normalize();
1778
+ this.raycasterPersonToCam.set(vehicleGroup.position, direction);
1779
+ this.raycasterPersonToCam.far = desiredDist;
1780
+ const hits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1781
+ if (hits.length > 0) {
1782
+ const safeDist = Math.max(hits[0].distance - this.camEpsilon, minSafeDist);
1783
+ this.camera.position.lerp(vehicleGroup.position.clone().add(direction.clone().multiplyScalar(safeDist)), this.camCollisionLerp);
1967
1784
  } else {
1968
- this._raycasterPersonToCam.far = maxCamDistanceLimit;
1969
- const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(
1970
- this.collider,
1971
- false
1972
- );
1973
- let safeDist = desiredDist;
1974
- if (intersectsMaxDis.length) {
1975
- const hitMax = intersectsMaxDis[0];
1976
- safeDist = Math.min(
1977
- desiredDist,
1978
- hitMax.distance - this._camEpsilon
1979
- );
1980
- }
1981
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1982
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1785
+ this.raycasterPersonToCam.far = maxDist;
1786
+ const maxHits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1787
+ const safeDist = maxHits.length > 0 ? Math.min(desiredDist, maxHits[0].distance - this.camEpsilon) : desiredDist;
1788
+ this.camera.position.lerp(vehicleGroup.position.clone().add(direction.clone().multiplyScalar(safeDist)), this.camCollisionLerp);
1983
1789
  }
1984
1790
  if ((this.fwdPressed || this.bkdPressed) && this.vehicleParams.followVehicleDirection) {
1985
1791
  const vel = chassisBody.linvel();
1986
- const velHorizontal = new THREE3.Vector3(vel.x, vel.y, vel.z);
1987
- const velSpeed = velHorizontal.length();
1988
- if (velSpeed > 0.3) {
1989
- const targetBehindDir = velHorizontal.clone().normalize().negate();
1990
- this.camBehindDir.lerp(targetBehindDir, this._camCollisionLerp).normalize();
1991
- const camHeightOffset = v.size.h;
1992
- const targetCamPos = lookTarget.clone().add(
1993
- this.camBehindDir.clone().multiplyScalar(desiredDist)
1994
- ).add(new THREE3.Vector3(0, camHeightOffset, 0));
1995
- this.camera.position.lerp(
1996
- targetCamPos,
1997
- this._camCollisionLerp
1998
- );
1792
+ const velVec = new THREE4.Vector3(vel.x, vel.y, vel.z);
1793
+ if (velVec.length() > 0.3) {
1794
+ this.camBehindDir.lerp(velVec.normalize().negate(), this.camCollisionLerp).normalize();
1795
+ const targetCamPos = lookTarget.clone().add(this.camBehindDir.clone().multiplyScalar(desiredDist)).add(new THREE4.Vector3(0, v.size.h, 0));
1796
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
1999
1797
  this.controls.update();
2000
1798
  }
2001
1799
  }
2002
1800
  }
2003
1801
  const vehicleUp = this.upVector.clone().applyQuaternion(vehicleGroup.quaternion);
2004
- const angleWithUp = vehicleUp.angleTo(this.upVector);
2005
- if (angleWithUp > Math.PI / 2) {
2006
- const size = new THREE3.Vector3();
1802
+ if (vehicleUp.angleTo(this.upVector) > Math.PI / 2) {
1803
+ const size = new THREE4.Vector3();
2007
1804
  v.vehicleBBox?.getSize(size);
2008
- const translation2 = chassisBody.translation();
2009
- chassisBody.setTranslation(
2010
- new this.RAPIER.Vector3(
2011
- translation2.x,
2012
- translation2.y + size.y,
2013
- translation2.z
2014
- ),
2015
- true
2016
- );
2017
- chassisBody.setRotation(
2018
- new this.RAPIER.Quaternion(0, 0, 0, 1),
2019
- true
2020
- );
1805
+ const t = chassisBody.translation();
1806
+ chassisBody.setTranslation(new this.RAPIER.Vector3(t.x, t.y + size.y, t.z), true);
1807
+ chassisBody.setRotation(new this.RAPIER.Quaternion(0, 0, 0, 1), true);
2021
1808
  chassisBody.setLinvel(new this.RAPIER.Vector3(0, 0, 0), true);
2022
1809
  chassisBody.setAngvel(new this.RAPIER.Vector3(0, 0, 0), true);
2023
1810
  }
2024
1811
  }
2025
- /**
2026
- * 更新所有车辆物理和位置
2027
- */
1812
+ // 物理步进 & 同步
2028
1813
  updateVehicleInertia(delta) {
2029
1814
  if (!this.world) return;
2030
1815
  this.world.timestep = delta;
@@ -2033,41 +1818,21 @@ var PlayerController = class {
2033
1818
  const { vehicleController, chassisBody, vehicleGroup, updateWheelVisuals } = v;
2034
1819
  vehicleController.updateVehicle(delta);
2035
1820
  if (chassisBody.isSleeping()) continue;
2036
- const velocity = chassisBody.linvel();
2037
- const currentSpeed = Math.sqrt(
2038
- velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z
2039
- );
2040
- const maxSpeed = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
2041
- if (currentSpeed > maxSpeed) {
2042
- const s = maxSpeed / currentSpeed;
2043
- chassisBody.setLinvel(
2044
- new this.RAPIER.Vector3(
2045
- velocity.x * s,
2046
- velocity.y * s,
2047
- velocity.z * s
2048
- ),
2049
- true
2050
- );
1821
+ const vel = chassisBody.linvel();
1822
+ const speed = new THREE4.Vector3(vel.x, vel.y, vel.z).length();
1823
+ const max = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
1824
+ if (speed > max) {
1825
+ const s = max / speed;
1826
+ chassisBody.setLinvel(new this.RAPIER.Vector3(vel.x * s, vel.y * s, vel.z * s), true);
2051
1827
  }
2052
- const translation = chassisBody.translation();
2053
- const rotationSync = chassisBody.rotation();
2054
- vehicleGroup.position.set(
2055
- translation.x,
2056
- translation.y,
2057
- translation.z
2058
- );
2059
- vehicleGroup.quaternion.set(
2060
- rotationSync.x,
2061
- rotationSync.y,
2062
- rotationSync.z,
2063
- rotationSync.w
2064
- );
2065
- if (updateWheelVisuals) updateWheelVisuals();
1828
+ const t = chassisBody.translation();
1829
+ const r = chassisBody.rotation();
1830
+ vehicleGroup.position.set(t.x, t.y, t.z);
1831
+ vehicleGroup.quaternion.set(r.x, r.y, r.z, r.w);
1832
+ updateWheelVisuals?.();
2066
1833
  }
2067
1834
  }
2068
- /**
2069
- * 设置人物缩放
2070
- */
1835
+ // 缩放玩家比例
2071
1836
  setPlayerScale(newScale) {
2072
1837
  if (newScale <= 0) return;
2073
1838
  const ratio = newScale / this.playerModel.scale;
@@ -2077,7 +1842,7 @@ var PlayerController = class {
2077
1842
  this.playerSpeed *= ratio;
2078
1843
  this.playerFlySpeed *= ratio;
2079
1844
  this.curPlayerSpeed *= ratio;
2080
- this._camEpsilon *= ratio;
1845
+ this.camEpsilon *= ratio;
2081
1846
  this.minCamDistance *= ratio;
2082
1847
  this.maxCamDistance *= ratio;
2083
1848
  this.orginMaxCamDistance *= ratio;
@@ -2086,38 +1851,32 @@ var PlayerController = class {
2086
1851
  if (this.player?.capsuleInfo) this.player.capsuleInfo.radius *= ratio;
2087
1852
  if (this.isFirstPerson) this.setFirstPersonCamera();
2088
1853
  }
2089
- /**
2090
- * 更新人物
2091
- */
1854
+ // 玩家帧更新
2092
1855
  updatePlayer(delta) {
2093
- if (this.isMovingToBoardingPoint) {
2094
- this.updateMoveToBoardingPoint(delta);
2095
- }
2096
- if (!this.isFlying) {
2097
- this.player.position.addScaledVector(this.playerVelocity, delta);
2098
- }
1856
+ if (this.isMovingToBoardingPoint) this.updateMoveToBoardingPoint(delta);
1857
+ if (!this.isFlying) this.player.position.addScaledVector(this.playerVelocity, delta);
2099
1858
  if (this.isBoardingAnimPlaying) {
2100
1859
  const action = this.personActions?.get("enterCar");
2101
- const duration = action.getClip().duration;
2102
- const timeScale = action.getEffectiveTimeScale();
2103
- const remaining = (duration - action.time) / timeScale * 1e3;
2104
- if (!this.closeDoorTriggered && remaining <= 500) {
2105
- this.closeDoorTriggered = true;
2106
- this.openVehicleDoor(false);
2107
- }
2108
- if (action.time >= duration) {
2109
- this.isBoardingAnimPlaying = false;
2110
- this.closeDoorTriggered = false;
2111
- this.onEnterCarAnimFinished();
2112
- return;
1860
+ if (action) {
1861
+ const duration = action.getClip().duration;
1862
+ const remaining = (duration - action.time) / action.getEffectiveTimeScale() * 1e3;
1863
+ if (!this.closeDoorTriggered && remaining <= 500) {
1864
+ this.closeDoorTriggered = true;
1865
+ this.openVehicleDoor(false);
1866
+ }
1867
+ if (action.time >= duration) {
1868
+ this.isBoardingAnimPlaying = false;
1869
+ this.closeDoorTriggered = false;
1870
+ this.onEnterCarAnimFinished();
1871
+ return;
1872
+ }
2113
1873
  }
2114
1874
  }
2115
1875
  if (this.isExitAnimPlaying) {
2116
1876
  const action = this.personActions?.get("exitCar");
2117
1877
  if (action) {
2118
1878
  const duration = action.getClip().duration;
2119
- const timeScale = action.getEffectiveTimeScale();
2120
- const remaining = (duration - action.time) / timeScale * 1e3;
1879
+ const remaining = (duration - action.time) / action.getEffectiveTimeScale() * 1e3;
2121
1880
  if (!this.closeExitDoorTriggered && remaining <= 500) {
2122
1881
  this.closeExitDoorTriggered = true;
2123
1882
  this.openVehicleDoor(false);
@@ -2131,71 +1890,47 @@ var PlayerController = class {
2131
1890
  this.updateMixers(delta);
2132
1891
  if (this.controllerMode === 1) return;
2133
1892
  this.camera.getWorldDirection(this.camDir);
2134
- let angle = Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2;
2135
- angle = 2 * Math.PI - angle;
1893
+ const angle = 2 * Math.PI - (Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2);
2136
1894
  this.moveDir.set(0, 0, 0);
2137
1895
  if (this.fwdPressed) this.moveDir.add(this.DIR_FWD);
2138
1896
  if (this.bkdPressed) this.moveDir.add(this.DIR_BKD);
2139
1897
  if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
2140
1898
  if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
2141
1899
  if (this.isFlying) {
2142
- if (this.fwdPressed) this.moveDir.y = this.camDir.y;
2143
- else this.moveDir.y = 0;
1900
+ this.moveDir.y = this.fwdPressed ? this.camDir.y : 0;
2144
1901
  if (this.spacePressed) this.moveDir.add(this.DIR_UP);
2145
- }
2146
- if (this.isFlying && this.fwdPressed) {
2147
1902
  this.curPlayerSpeed = this.shiftPressed ? this.playerFlySpeed * 2 : this.playerFlySpeed;
2148
1903
  } else {
2149
1904
  this.curPlayerSpeed = this.shiftPressed ? this.playerSpeed * 2 : this.playerSpeed;
2150
1905
  }
2151
1906
  this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
2152
- this.player.position.addScaledVector(
2153
- this.moveDir,
2154
- this.curPlayerSpeed * delta
2155
- );
2156
- let playerDistanceFromGround = Infinity;
2157
- this._originTmp.set(
2158
- this.player.position.x,
2159
- this.player.position.y,
2160
- this.player.position.z
2161
- );
2162
- this._raycaster.ray.origin.copy(this._originTmp);
2163
- const intersects = this._raycaster.intersectObject(
2164
- this.collider,
2165
- false
2166
- );
2167
- if (intersects.length > 0) {
2168
- playerDistanceFromGround = this.player.position.y - intersects[0].point.y;
2169
- const maxH = this.playerCapsuleHeight * this.playerModel.scale * 0.9;
2170
- const h = this.playerCapsuleHeight * this.playerModel.scale * 0.75;
2171
- const minH = this.playerCapsuleHeight * this.playerModel.scale * 0.7;
2172
- if (!this.isFlying) {
2173
- if (playerDistanceFromGround >= maxH) {
2174
- this.playerVelocity.y += delta * this.gravity;
2175
- this.player.position.addScaledVector(
2176
- this.playerVelocity,
2177
- delta
2178
- );
2179
- this.playerIsOnGround = false;
2180
- } else if (playerDistanceFromGround >= h && playerDistanceFromGround < maxH) {
2181
- if (!this.spacePressed) {
2182
- this.playerVelocity.set(0, 0, 0);
2183
- this.playerIsOnGround = true;
2184
- this.player.position.y = intersects[0].point.y + h;
2185
- }
2186
- } else if (playerDistanceFromGround >= minH && playerDistanceFromGround < h) {
2187
- this.playerVelocity.set(0, 0, 0);
2188
- this.playerIsOnGround = true;
2189
- this.player.position.y = intersects[0].point.y + h;
2190
- } else if (playerDistanceFromGround < minH) {
1907
+ this.player.position.addScaledVector(this.moveDir, this.curPlayerSpeed * delta);
1908
+ this.raycaster.ray.origin.copy(this.player.position);
1909
+ const hits = this.raycaster.intersectObject(this.collider, false);
1910
+ if (hits.length > 0 && !this.isFlying) {
1911
+ const dist = this.player.position.y - hits[0].point.y;
1912
+ const s = this.playerModel.scale;
1913
+ const maxH = this.playerCapsuleHeight * s * 0.9;
1914
+ const h = this.playerCapsuleHeight * s * 0.75;
1915
+ const minH = this.playerCapsuleHeight * s * 0.7;
1916
+ if (dist >= maxH) {
1917
+ this.playerVelocity.y += delta * this.gravity;
1918
+ this.player.position.addScaledVector(this.playerVelocity, delta);
1919
+ this.playerIsOnGround = false;
1920
+ } else if (dist >= h && dist < maxH) {
1921
+ if (!this.spacePressed) {
2191
1922
  this.playerVelocity.set(0, 0, 0);
2192
- this.player.position.set(
2193
- this.player.position.x,
2194
- intersects[0].point.y + h,
2195
- this.player.position.z
2196
- );
2197
1923
  this.playerIsOnGround = true;
1924
+ this.player.position.y = hits[0].point.y + h;
2198
1925
  }
1926
+ } else if (dist >= minH) {
1927
+ this.playerVelocity.set(0, 0, 0);
1928
+ this.playerIsOnGround = true;
1929
+ this.player.position.y = hits[0].point.y + h;
1930
+ } else {
1931
+ this.playerVelocity.set(0, 0, 0);
1932
+ this.player.position.y = hits[0].point.y + h;
1933
+ this.playerIsOnGround = true;
2199
1934
  }
2200
1935
  }
2201
1936
  this.player.updateMatrixWorld();
@@ -2205,113 +1940,49 @@ var PlayerController = class {
2205
1940
  this.tempSegment.copy(capsuleInfo.segment);
2206
1941
  this.tempSegment.start.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
2207
1942
  this.tempSegment.end.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
2208
- this.tempBox.expandByPoint(this.tempSegment.start);
2209
- this.tempBox.expandByPoint(this.tempSegment.end);
1943
+ this.tempBox.expandByPoint(this.tempSegment.start).expandByPoint(this.tempSegment.end);
2210
1944
  this.tempBox.expandByScalar(capsuleInfo.radius);
2211
1945
  if (!this.isMovingToBoardingPoint) {
2212
- this.collider?.geometry?.boundsTree?.shapecast({
2213
- intersectsBounds: (box) => box.intersectsBox(this.tempBox),
2214
- intersectsTriangle: (tri) => {
2215
- const triPoint = this.tempVector;
2216
- const capsulePoint = this.tempVector2;
2217
- const distance = tri.closestPointToSegment(
2218
- this.tempSegment,
2219
- triPoint,
2220
- capsulePoint
2221
- );
2222
- if (distance < capsuleInfo.radius) {
2223
- const normal = tri.getNormal(new THREE3.Vector3());
2224
- if (normal.y > 0.5 && !this.isFlying) return;
2225
- const depth = capsuleInfo.radius - distance;
2226
- const direction = capsulePoint.sub(triPoint).normalize();
2227
- this.tempSegment.start.addScaledVector(
2228
- direction,
2229
- depth
2230
- );
2231
- this.tempSegment.end.addScaledVector(direction, depth);
2232
- }
2233
- }
2234
- });
2235
- this.dynamicCollider?.geometry?.boundsTree?.shapecast({
2236
- intersectsBounds: (box) => box.intersectsBox(this.tempBox),
2237
- intersectsTriangle: (tri) => {
2238
- const triPoint = this.tempVector;
2239
- const capsulePoint = this.tempVector2;
2240
- const distance = tri.closestPointToSegment(
2241
- this.tempSegment,
2242
- triPoint,
2243
- capsulePoint
2244
- );
2245
- if (distance < capsuleInfo.radius) {
2246
- const depth = capsuleInfo.radius - distance;
2247
- const direction = capsulePoint.sub(triPoint).normalize();
2248
- this.tempSegment.start.addScaledVector(
2249
- direction,
2250
- depth
2251
- );
2252
- this.tempSegment.end.addScaledVector(direction, depth);
1946
+ const resolveCollision = (collider) => {
1947
+ if (!collider) return;
1948
+ collider.geometry?.boundsTree?.shapecast({
1949
+ intersectsBounds: (box) => box.intersectsBox(this.tempBox),
1950
+ intersectsTriangle: (tri) => {
1951
+ const distance = tri.closestPointToSegment(this.tempSegment, this.tempVector, this.tempVector2);
1952
+ if (distance < capsuleInfo.radius) {
1953
+ const normal = tri.getNormal(new THREE4.Vector3());
1954
+ if (normal.y > 0.5 && !this.isFlying) return;
1955
+ const dir = this.tempVector2.sub(this.tempVector).normalize();
1956
+ const depth = capsuleInfo.radius - distance;
1957
+ this.tempSegment.start.addScaledVector(dir, depth);
1958
+ this.tempSegment.end.addScaledVector(dir, depth);
1959
+ }
2253
1960
  }
2254
- }
2255
- });
1961
+ });
1962
+ };
1963
+ resolveCollision(this.collider);
1964
+ resolveCollision(this.dynamicCollider);
2256
1965
  }
2257
- const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
2258
- const deltaVector = this.tempVector2.subVectors(
2259
- newPosition,
2260
- this.player.position
2261
- );
2262
- const offset = Math.max(0, deltaVector.length() - 1e-5);
2263
- deltaVector.normalize().multiplyScalar(offset);
2264
- this.player.position.add(deltaVector);
2265
- if (!this.isFirstPerson && !this.isFlying) {
2266
- this.camDir.y = 0;
2267
- this.camDir.normalize();
2268
- this.camDir.negate();
2269
- this.moveDir.normalize();
2270
- this.moveDir.negate();
2271
- let lookTarget;
2272
- if (this.thirdMouseMode === 0 || this.thirdMouseMode === 2) {
2273
- if (this.moveDir.lengthSq() > 0) {
2274
- lookTarget = this.player.position.clone().add(this.moveDir);
2275
- } else {
2276
- lookTarget = this.player.position.clone().add(this.camDir);
1966
+ const newPos = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
1967
+ const deltaVec = this.tempVector2.subVectors(newPos, this.player.position);
1968
+ const offset = Math.max(0, deltaVec.length() - 1e-5);
1969
+ this.player.position.add(deltaVec.normalize().multiplyScalar(offset));
1970
+ if (!this.isFirstPerson) {
1971
+ const camDirFlat = this.camDir.clone().setY(0).normalize().negate();
1972
+ const moveDirFlat = this.moveDir.clone().normalize().negate();
1973
+ if (!this.isFlying) {
1974
+ if (this.thirdMouseMode === 0 || this.thirdMouseMode === 2) {
1975
+ const lookTarget = this.player.position.clone().add(moveDirFlat.lengthSq() > 0 ? moveDirFlat : camDirFlat);
1976
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1977
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
1978
+ } else if (moveDirFlat.lengthSq() > 0) {
1979
+ this.targetMat.lookAt(this.player.position, this.player.position.clone().add(moveDirFlat), this.player.up);
1980
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
2277
1981
  }
2278
- this.targetMat.lookAt(
2279
- this.player.position,
2280
- lookTarget,
2281
- this.player.up
2282
- );
2283
- this.targetQuat.setFromRotationMatrix(this.targetMat);
2284
- const alpha = Math.min(1, this.rotationSpeed * delta);
2285
- this.player.quaternion.slerp(this.targetQuat, alpha);
2286
- }
2287
- if ((this.thirdMouseMode === 1 || this.thirdMouseMode === 3) && this.moveDir.lengthSq() > 0) {
2288
- lookTarget = this.player.position.clone().add(this.moveDir);
2289
- this.targetMat.lookAt(
2290
- this.player.position,
2291
- lookTarget,
2292
- this.player.up
2293
- );
2294
- this.targetQuat.setFromRotationMatrix(this.targetMat);
2295
- const alpha = Math.min(1, this.rotationSpeed * delta);
2296
- this.player.quaternion.slerp(this.targetQuat, alpha);
2297
- }
2298
- }
2299
- if (this.isFlying) {
2300
- if (!this.isFirstPerson) {
2301
- this.camDir.y = 0;
2302
- this.camDir.normalize();
2303
- this.camDir.negate();
2304
- this.moveDir.normalize();
2305
- this.moveDir.negate();
2306
- const lookTarget = this.player.position.clone().add(this.fwdPressed ? this.moveDir : this.camDir);
2307
- this.targetMat.lookAt(
2308
- this.player.position,
2309
- lookTarget,
2310
- this.player.up
2311
- );
2312
- this.targetQuat.setFromRotationMatrix(this.targetMat);
2313
- const alpha = Math.min(1, this.rotationSpeed * delta);
2314
- this.player.quaternion.slerp(this.targetQuat, alpha);
1982
+ } else {
1983
+ const lookTarget = this.player.position.clone().add(this.fwdPressed ? moveDirFlat : camDirFlat);
1984
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1985
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
2315
1986
  }
2316
1987
  }
2317
1988
  if (!this.isFirstPerson) {
@@ -2322,131 +1993,84 @@ var PlayerController = class {
2322
1993
  this.camera.position.add(lookTarget);
2323
1994
  this.controls.update();
2324
1995
  if (!this.enableZoom) {
2325
- this._personToCam.subVectors(
2326
- this.camera.position,
2327
- this.player.position
2328
- );
2329
- const origin = this.player.position.clone();
2330
- const direction = this._personToCam.clone().normalize();
2331
- const desiredDist = this._personToCam.length();
2332
- this._raycasterPersonToCam.set(origin, direction);
2333
- this._raycasterPersonToCam.far = desiredDist;
2334
- const intersectsCamera = this._raycasterPersonToCam.intersectObject(
2335
- this.collider,
2336
- false
1996
+ this.updateCameraWithRaycast(
1997
+ this.player.position,
1998
+ this.personToCam.subVectors(this.camera.position, this.player.position).length(),
1999
+ this.maxCamDistance
2337
2000
  );
2338
- if (intersectsCamera.length > 0) {
2339
- const hit = intersectsCamera[0];
2340
- const safeDist = Math.max(
2341
- hit.distance - this._camEpsilon,
2342
- this.minCamDistance
2343
- );
2344
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
2345
- this.camera.position.lerp(
2346
- targetCamPos,
2347
- this._camCollisionLerp
2348
- );
2349
- } else {
2350
- this._raycasterPersonToCam.far = this.maxCamDistance;
2351
- const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(
2352
- this.collider,
2353
- false
2354
- );
2355
- let safeDist = this.maxCamDistance;
2356
- if (intersectsMaxDis.length) {
2357
- const hitMax = intersectsMaxDis[0];
2358
- safeDist = hitMax.distance - this._camEpsilon;
2359
- }
2360
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
2361
- this.camera.position.lerp(
2362
- targetCamPos,
2363
- this._camCollisionLerp
2364
- );
2365
- }
2366
2001
  }
2367
2002
  }
2368
2003
  if (this.player.position.y < this.boundingBoxMinY - 1) {
2369
- this._originTmp.set(
2004
+ this.raycaster.ray.origin.set(this.player.position.x, 1e4, this.player.position.z);
2005
+ const fallHits = this.raycaster.intersectObject(this.collider, false);
2006
+ this.reset(new THREE4.Vector3(
2370
2007
  this.player.position.x,
2371
- 1e4,
2008
+ fallHits.length > 0 ? fallHits[0].point.y + 5 : this.player.position.y + 15,
2372
2009
  this.player.position.z
2373
- );
2374
- this._raycaster.ray.origin.copy(this._originTmp);
2375
- const intersectsFall = this._raycaster.intersectObject(
2376
- this.collider,
2377
- false
2378
- );
2379
- if (intersectsFall.length > 0) {
2380
- console.log("\u73A9\u5BB6\u4E3Abug\u610F\u5916\u6389\u843D");
2381
- this.reset(
2382
- new THREE3.Vector3(
2383
- this.player.position.x,
2384
- intersectsFall[0].point.y + 5,
2385
- this.player.position.z
2386
- )
2387
- );
2388
- } else {
2389
- console.log("\u73A9\u5BB6\u6B63\u5E38\u6389\u843D");
2390
- this.reset(
2391
- new THREE3.Vector3(
2392
- this.player.position.x,
2393
- this.player.position.y + 15,
2394
- this.player.position.z
2395
- )
2396
- );
2397
- }
2010
+ ));
2398
2011
  }
2399
- if (this.isShowMobileControls) {
2400
- if (this.vehicles.length) {
2401
- let near = false;
2402
- for (const v of this.vehicles) {
2403
- this.nearCheckLocal.copy(v.boardingPoint).multiplyScalar(v.scale);
2404
- v.vehicleGroup.localToWorld(
2405
- this.nearCheckWorld.copy(this.nearCheckLocal)
2406
- );
2407
- if (this.player.position.distanceTo(this.nearCheckWorld) < 800 * this.playerModel.scale) {
2408
- near = true;
2409
- this.syncVehicleBtnEl(near);
2410
- break;
2411
- }
2412
- }
2413
- if (near !== this.isNearVehicle) {
2414
- this.isNearVehicle = near;
2415
- this.syncVehicleBtnEl(near);
2012
+ if (this.isShowMobileControls && this.vehicles.length) {
2013
+ let near = false;
2014
+ for (const v of this.vehicles) {
2015
+ this.nearCheckLocal.copy(v.boardingPoint).multiplyScalar(v.scale);
2016
+ v.vehicleGroup.localToWorld(this.nearCheckWorld.copy(this.nearCheckLocal));
2017
+ if (this.player.position.distanceTo(this.nearCheckWorld) < 800 * this.playerModel.scale) {
2018
+ near = true;
2019
+ break;
2416
2020
  }
2417
- } else {
2418
- this.isNearVehicle = false;
2419
- this.syncVehicleBtnEl(false);
2420
2021
  }
2022
+ if (near !== this.isNearVehicle) {
2023
+ this.isNearVehicle = near;
2024
+ this.mobileControls?.syncVehicleBtn(near);
2025
+ }
2026
+ }
2027
+ }
2028
+ // 相机碰撞射线
2029
+ updateCameraWithRaycast(origin, desiredDist, maxDist) {
2030
+ this.personToCam.subVectors(this.camera.position, origin);
2031
+ const direction = this.personToCam.clone().normalize();
2032
+ this.raycasterPersonToCam.set(origin, direction);
2033
+ this.raycasterPersonToCam.far = desiredDist;
2034
+ const hits = this.raycasterPersonToCam.intersectObject(this.collider, false);
2035
+ if (hits.length > 0) {
2036
+ const safeDist = Math.max(hits[0].distance - this.camEpsilon, this.minCamDistance);
2037
+ const targetCamPos = origin.clone().add(direction.multiplyScalar(safeDist));
2038
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
2039
+ } else {
2040
+ this.raycasterPersonToCam.far = maxDist;
2041
+ const maxHits = this.raycasterPersonToCam.intersectObject(this.collider, false);
2042
+ const safeDist = maxHits.length > 0 ? Math.min(maxDist, maxHits[0].distance - this.camEpsilon) : maxDist;
2043
+ const targetCamPos = origin.clone().add(direction.multiplyScalar(safeDist));
2044
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
2421
2045
  }
2422
2046
  }
2423
- /**
2424
- * 获取屏幕中心点向前射线与碰撞体的交点
2425
- */
2047
+ // 屏幕中心射线
2426
2048
  getCenterScreenRaycastHit() {
2427
2049
  this.camera.updateMatrixWorld();
2428
2050
  this.centerRay.setFromCamera(this.centerMouse, this.camera);
2429
- const intersects = this.centerRay.intersectObject(this.collider, false);
2430
- return intersects[0];
2051
+ return this.centerRay.intersectObject(this.collider, false)[0];
2431
2052
  }
2432
- /**
2433
- * 更新模型动画
2434
- */
2053
+ // 获取当前人物动画名称
2054
+ getCurrentPersonAnimationName() {
2055
+ return this.actionState._clip.name;
2056
+ }
2057
+ // 更新动画混合器
2435
2058
  updateMixers(delta) {
2436
- if (this.personMixer) this.personMixer.update(delta);
2437
- for (const v of this.vehicles) {
2438
- v.vehicleMixer?.update(delta);
2439
- }
2059
+ this.personMixer?.update(delta);
2060
+ for (const v of this.vehicles) v.vehicleMixer?.update(delta);
2440
2061
  }
2062
+ // 重置玩家位置
2441
2063
  reset(position) {
2442
2064
  if (!this.player) return;
2443
2065
  this.playerVelocity.set(0, 0, 0);
2444
2066
  this.player.position.copy(position ?? this.initPos);
2445
2067
  }
2068
+ // 获取玩家位置
2446
2069
  getPosition() {
2447
2070
  return this.player?.position;
2448
2071
  }
2449
2072
  // ==================== 输入处理 ====================
2073
+ // 外部输入接口
2450
2074
  setInput(input) {
2451
2075
  if (typeof input.moveX === "number") {
2452
2076
  this.lftPressed = input.moveX === -1;
@@ -2463,14 +2087,9 @@ var PlayerController = class {
2463
2087
  }
2464
2088
  if (typeof input.jump === "boolean") {
2465
2089
  if (input.jump) {
2466
- if (this.isMovingToBoardingPoint) {
2467
- this.isMovingToBoardingPoint = false;
2468
- this.boardingWaypoints = [];
2469
- this.currentWaypointIndex = 0;
2470
- this.boardingTargetDir = null;
2471
- }
2090
+ this.cancelBoarding();
2472
2091
  this.spacePressed = true;
2473
- if (this.controllerMode == 1) return;
2092
+ if (this.controllerMode === 1) return;
2474
2093
  if (!this.playerIsOnGround || this.isFlying) return;
2475
2094
  this.playPersonAnimationByName("jumping");
2476
2095
  this.playerVelocity.y = this.jumpHeight;
@@ -2479,344 +2098,91 @@ var PlayerController = class {
2479
2098
  this.spacePressed = false;
2480
2099
  }
2481
2100
  }
2482
- if (typeof input.shift === "boolean") {
2483
- this.shiftPressed = input.shift;
2484
- }
2485
- if (input.toggleView) {
2486
- this.changeView();
2487
- }
2488
- if (input.toggleFly && this.playerFlyEnabled && this.controllerMode == 0) {
2101
+ if (typeof input.shift === "boolean") this.shiftPressed = input.shift;
2102
+ if (input.toggleView) this.changeView();
2103
+ if (input.toggleFly && this.playerFlyEnabled && this.controllerMode === 0) {
2489
2104
  this.isFlying = !this.isFlying;
2490
2105
  this.setAnimationByPressed();
2491
- if (!this.isFlying && !this.playerIsOnGround) {
2492
- this.playPersonAnimationByName("jumping");
2493
- }
2106
+ if (!this.isFlying && !this.playerIsOnGround) this.playPersonAnimationByName("jumping");
2494
2107
  }
2495
2108
  if (input.toggleVehicle) {
2496
- if (this.controllerMode == 0) {
2497
- this.enterVehicle();
2498
- } else {
2499
- this.exitVehicle();
2500
- }
2109
+ if (this.controllerMode === 0) this.enterVehicle();
2110
+ else this.exitVehicle();
2501
2111
  }
2502
2112
  }
2113
+ // 取消上车寻路
2114
+ cancelBoarding() {
2115
+ this.isMovingToBoardingPoint = false;
2116
+ this.boardingWaypoints = [];
2117
+ this.currentWaypointIndex = 0;
2118
+ this.boardingTargetDir = null;
2119
+ }
2120
+ // 注册所有事件
2503
2121
  onAllEvent() {
2504
2122
  this.isupdate = true;
2505
2123
  this.setPointerLock();
2506
- window.addEventListener("keydown", this._boundOnKeydown);
2507
- window.addEventListener("keyup", this._boundOnKeyup);
2508
- window.addEventListener("mousemove", this._mouseMove);
2509
- window.addEventListener("click", this._mouseClick);
2124
+ window.addEventListener("keydown", this.boundOnKeydown);
2125
+ window.addEventListener("keyup", this.boundOnKeyup);
2126
+ window.addEventListener("mousemove", this.mouseMove);
2127
+ window.addEventListener("click", this.mouseClick);
2510
2128
  }
2129
+ // 注销所有事件
2511
2130
  offAllEvent() {
2512
2131
  this.isupdate = false;
2513
2132
  document.exitPointerLock();
2514
- window.removeEventListener("keydown", this._boundOnKeydown);
2515
- window.removeEventListener("keyup", this._boundOnKeyup);
2516
- window.removeEventListener("mousemove", this._mouseMove);
2517
- window.removeEventListener("click", this._mouseClick);
2518
- }
2519
- async initMobileControls() {
2520
- this.controls.maxPolarAngle = Math.PI * (300 / 360);
2521
- this.controls.touches = { ONE: null, TWO: null };
2522
- this.nippleModule = await import("nipplejs");
2523
- const nipple = this.nippleModule?.default;
2524
- const JOY_SIZE = 120;
2525
- const container = document.body;
2526
- this.joystickZoneEl = document.createElement("div");
2527
- this.joystickZoneEl.id = "joy-zone";
2528
- Object.assign(this.joystickZoneEl.style, {
2529
- position: "absolute",
2530
- left: "16px",
2531
- bottom: "16px",
2532
- width: `${JOY_SIZE + 40}px`,
2533
- height: `${JOY_SIZE + 40}px`,
2534
- touchAction: "none",
2535
- zIndex: "999",
2536
- pointerEvents: "auto",
2537
- WebkitUserSelect: "none",
2538
- userSelect: "none"
2539
- });
2540
- container.appendChild(this.joystickZoneEl);
2541
- ["touchstart", "touchmove", "touchend", "touchcancel"].forEach(
2542
- (evtName) => {
2543
- this.joystickZoneEl?.addEventListener(
2544
- evtName,
2545
- (e) => e.preventDefault(),
2546
- {
2547
- passive: false
2548
- }
2549
- );
2550
- }
2551
- );
2552
- this.joystickManager = nipple.create({
2553
- zone: this.joystickZoneEl,
2554
- mode: "static",
2555
- position: {
2556
- left: `${(JOY_SIZE + 40) / 2}px`,
2557
- bottom: `${(JOY_SIZE + 40) / 2}px`
2558
- },
2559
- color: "#ffffff",
2560
- size: JOY_SIZE,
2561
- multitouch: true,
2562
- maxNumberOfNipples: 1
2563
- });
2564
- this.joystickManager.on("move", (_evt, data) => {
2565
- if (!data) return;
2566
- const rawX = data.vector?.x ?? 0;
2567
- const rawY = data.vector?.y ?? 0;
2568
- const distance = data.distance ?? 0;
2569
- const deadzone = 0.5;
2570
- const dirX = rawX > deadzone ? 1 : rawX < -deadzone ? -1 : 0;
2571
- const dirY = rawY > deadzone ? 1 : rawY < -deadzone ? -1 : 0;
2572
- const sprintThreshold = JOY_SIZE / 2;
2573
- const isSprinting = distance >= sprintThreshold;
2574
- const prev = this.prevJoyState || {
2575
- dirX: 0,
2576
- dirY: 0,
2577
- shift: false
2578
- };
2579
- if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift)
2580
- return;
2581
- this.prevJoyState = { dirX, dirY, shift: isSprinting };
2582
- this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
2583
- });
2584
- this.joystickManager.on("end", () => {
2585
- const prev = this.prevJoyState || {
2586
- dirX: 0,
2587
- dirY: 0,
2588
- shift: false
2589
- };
2590
- if (prev.dirX !== 0 || prev.dirY !== 0 || prev.shift !== false) {
2591
- this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
2592
- this.setInput({ moveX: 0, moveY: 0, shift: false });
2593
- }
2594
- });
2595
- this.lookAreaEl = document.createElement("div");
2596
- Object.assign(this.lookAreaEl.style, {
2597
- position: "absolute",
2598
- right: "0",
2599
- bottom: "0",
2600
- width: "50%",
2601
- height: "100%",
2602
- zIndex: "998",
2603
- touchAction: "none",
2604
- WebkitUserSelect: "none",
2605
- userSelect: "none"
2606
- });
2607
- container.appendChild(this.lookAreaEl);
2608
- ["touchstart", "touchmove", "touchend", "touchcancel"].forEach(
2609
- (evtName) => {
2610
- this.lookAreaEl?.addEventListener(
2611
- evtName,
2612
- (e) => e.preventDefault(),
2613
- {
2614
- passive: false
2615
- }
2616
- );
2617
- }
2618
- );
2619
- this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, {
2620
- passive: false
2621
- });
2622
- this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, {
2623
- passive: false
2624
- });
2625
- this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, {
2626
- passive: false
2627
- });
2628
- this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, {
2629
- passive: false
2630
- });
2631
- const createBtn = (rightPx, bottomPx, bgUrl) => {
2632
- const btn = document.createElement("button");
2633
- const styles = {
2634
- position: "absolute",
2635
- right: `${rightPx}px`,
2636
- bottom: `${bottomPx}px`,
2637
- width: "56px",
2638
- height: "56px",
2639
- zIndex: "1000",
2640
- borderRadius: "50%",
2641
- border: "2px solid black",
2642
- background: "rgba(0,0,0)",
2643
- padding: "20px",
2644
- opacity: "0.95",
2645
- touchAction: "none",
2646
- fontSize: "14px",
2647
- userSelect: "none",
2648
- overflow: "hidden",
2649
- boxSizing: "border-box",
2650
- backgroundColor: "transparent",
2651
- backgroundRepeat: "no-repeat, no-repeat",
2652
- backgroundPosition: "center center, center center",
2653
- backgroundSize: "100% 100%, 80% 80%"
2654
- };
2655
- if (bgUrl) {
2656
- const overlayColor = "rgba(0,0,0,0.5)";
2657
- styles.backgroundImage = `linear-gradient(${overlayColor}, ${overlayColor}), url("${bgUrl}")`;
2658
- }
2659
- Object.assign(btn.style, styles);
2660
- container.appendChild(btn);
2661
- ["touchstart", "touchend", "touchcancel"].forEach((evtName) => {
2662
- btn.addEventListener(evtName, (e) => e.preventDefault(), {
2663
- passive: false
2664
- });
2665
- });
2666
- return btn;
2667
- };
2668
- this.jumpBtnEl = createBtn(14, 14, jump_default);
2669
- this.jumpBtnEl.addEventListener(
2670
- "touchstart",
2671
- (e) => {
2672
- e.preventDefault();
2673
- this.setInput({ jump: true });
2674
- },
2675
- { passive: false }
2676
- );
2677
- this.jumpBtnEl.addEventListener(
2678
- "touchend",
2679
- (e) => {
2680
- e.preventDefault();
2681
- this.setInput({ jump: false });
2682
- },
2683
- { passive: false }
2684
- );
2685
- this.jumpBtnEl.addEventListener(
2686
- "touchcancel",
2687
- (e) => {
2688
- e.preventDefault();
2689
- this.setInput({ jump: false });
2690
- },
2691
- { passive: false }
2692
- );
2693
- this.flyBtnEl = createBtn(14, 14 + 80, fly_default);
2694
- this.flyBtnEl.addEventListener(
2695
- "touchstart",
2696
- (e) => {
2697
- e.preventDefault();
2698
- this.setInput({ toggleFly: true });
2699
- },
2700
- { passive: false }
2701
- );
2702
- this.viewBtnEl = createBtn(14, 14 + 200, view_default);
2703
- this.viewBtnEl.addEventListener(
2704
- "touchstart",
2705
- (e) => {
2706
- e.preventDefault();
2707
- this.setInput({ toggleView: true });
2708
- },
2709
- { passive: false }
2710
- );
2711
- this.vehicleBtnEl = createBtn(14 + 100, 14 + 120, vehicle_default);
2712
- this.vehicleBtnEl.addEventListener(
2713
- "touchstart",
2714
- (e) => {
2715
- e.preventDefault();
2716
- this.setInput({ toggleVehicle: true });
2717
- },
2718
- { passive: false }
2719
- );
2720
- }
2721
- destroyMobileControls() {
2722
- try {
2723
- if (this.joystickManager && this.joystickManager.destroy) {
2724
- this.joystickManager.destroy();
2725
- this.joystickManager = null;
2726
- }
2727
- if (this.joystickZoneEl?.parentElement) {
2728
- this.joystickZoneEl.parentElement.removeChild(
2729
- this.joystickZoneEl
2730
- );
2731
- this.joystickZoneEl = null;
2732
- }
2733
- if (this.lookAreaEl?.parentElement) {
2734
- this.lookAreaEl.parentElement.removeChild(this.lookAreaEl);
2735
- this.lookAreaEl = null;
2736
- }
2737
- if (this.jumpBtnEl?.parentElement) {
2738
- this.jumpBtnEl.parentElement.removeChild(this.jumpBtnEl);
2739
- this.jumpBtnEl = null;
2740
- }
2741
- if (this.flyBtnEl?.parentElement) {
2742
- this.flyBtnEl.parentElement.removeChild(this.flyBtnEl);
2743
- this.flyBtnEl = null;
2744
- }
2745
- if (this.viewBtnEl?.parentElement) {
2746
- this.viewBtnEl.parentElement.removeChild(this.viewBtnEl);
2747
- this.viewBtnEl = null;
2748
- }
2749
- if (this.vehicleBtnEl?.parentElement) {
2750
- this.vehicleBtnEl.parentElement.removeChild(this.vehicleBtnEl);
2751
- this.vehicleBtnEl = null;
2752
- }
2753
- this.lookAreaEl?.removeEventListener(
2754
- "pointerdown",
2755
- this.onPointerDown
2756
- );
2757
- this.lookAreaEl?.removeEventListener(
2758
- "pointermove",
2759
- this.onPointerMove
2760
- );
2761
- this.lookAreaEl?.removeEventListener("pointerup", this.onPointerUp);
2762
- this.lookAreaEl?.removeEventListener(
2763
- "pointercancel",
2764
- this.onPointerUp
2765
- );
2766
- } catch (e) {
2767
- console.warn("\u9500\u6BC1\u79FB\u52A8\u7AEF\u6447\u6746\u63A7\u5236\u65F6\u51FA\u9519\uFF1A", e);
2768
- }
2769
- }
2770
- syncVehicleBtnEl(show) {
2771
- if (!this.vehicleBtnEl) return;
2772
- this.vehicleBtnEl.style.display = show ? "block" : "none";
2773
- }
2774
- syncControllerModeBtnEl() {
2775
- if (!this.isShowMobileControls) return;
2776
- if (this.controllerMode == 0) {
2777
- this.flyBtnEl.style.display = "block";
2778
- const overlayColor = "rgba(0,0,0,0.5)";
2779
- this.jumpBtnEl.style.backgroundImage = `linear-gradient(${overlayColor}, ${overlayColor}), url("${jump_default}")`;
2780
- } else {
2781
- this.flyBtnEl.style.display = "none";
2782
- const overlayColor = "rgba(0,0,0,0.5)";
2783
- this.jumpBtnEl.style.backgroundImage = `linear-gradient(${overlayColor}, ${overlayColor}), url("${break_default}")`;
2784
- this.jumpBtnEl.style.backgroundImage = `url(${break_default})`;
2785
- }
2786
- }
2787
- // ==================== 更新参数 ====================
2788
- setMouseSensitivity(mouseSensity) {
2789
- this.mouseSensity = mouseSensity;
2790
- this.controls.rotateSpeed = this.mouseSensity * 0.05;
2791
- }
2133
+ window.removeEventListener("keydown", this.boundOnKeydown);
2134
+ window.removeEventListener("keyup", this.boundOnKeyup);
2135
+ window.removeEventListener("mousemove", this.mouseMove);
2136
+ window.removeEventListener("click", this.mouseClick);
2137
+ }
2138
+ // ==================== 移动端同步 ====================
2139
+ // 同步移动端按钮
2140
+ syncMobileControllerMode() {
2141
+ this.mobileControls?.syncControllerModeBtn(this.controllerMode);
2142
+ }
2143
+ // ==================== Setters ====================
2144
+ // 设置鼠标灵敏度
2145
+ setMouseSensitivity(value) {
2146
+ this.mouseSensitivity = value;
2147
+ this.controls.rotateSpeed = value * 0.05;
2148
+ }
2149
+ // 设置重力
2792
2150
  setGravity(gravity) {
2793
2151
  this.gravity = gravity * this.playerModel.scale;
2794
2152
  }
2153
+ // 设置跳跃高度
2795
2154
  setJumpHeight(jumpHeight) {
2796
2155
  this.jumpHeight = jumpHeight * this.playerModel.scale;
2797
2156
  }
2798
- setPlayerSpeed(playerSpeed) {
2799
- this.playerSpeed = playerSpeed * this.playerModel.scale;
2157
+ // 设置移动速度
2158
+ setPlayerSpeed(speed) {
2159
+ this.playerSpeed = speed * this.playerModel.scale;
2800
2160
  this.curPlayerSpeed = this.playerSpeed;
2801
2161
  }
2802
- setPlayerFlySpeed(playerFlySpeed) {
2803
- this.playerFlySpeed = playerFlySpeed * this.playerModel.scale;
2162
+ // 设置飞行速度
2163
+ setPlayerFlySpeed(flySpeed) {
2164
+ this.playerFlySpeed = flySpeed * this.playerModel.scale;
2804
2165
  }
2805
- setMinCamDistance(minCamDistance) {
2806
- this.minCamDistance = minCamDistance * this.playerModel.scale;
2166
+ // 设置最小相机距离
2167
+ setMinCamDistance(dist) {
2168
+ this.minCamDistance = dist * this.playerModel.scale;
2807
2169
  }
2808
- setMaxCamDistance(maxCamDistance) {
2809
- this.maxCamDistance = maxCamDistance * this.playerModel.scale;
2170
+ // 设置最大相机距离
2171
+ setMaxCamDistance(dist) {
2172
+ this.maxCamDistance = dist * this.playerModel.scale;
2810
2173
  this.orginMaxCamDistance = this.maxCamDistance;
2811
2174
  }
2812
- setThirdMouseMode(thirdMouseMode) {
2813
- this.thirdMouseMode = thirdMouseMode;
2175
+ // 设置鼠标模式
2176
+ setThirdMouseMode(mode) {
2177
+ this.thirdMouseMode = mode;
2814
2178
  this.setPointerLock();
2815
2179
  }
2816
- setEnableZoom(enableZoom) {
2817
- this.enableZoom = enableZoom;
2818
- this.controls.enableZoom = this.enableZoom;
2180
+ // 设置滚轮缩放
2181
+ setEnableZoom(enable) {
2182
+ this.enableZoom = enable;
2183
+ this.controls.enableZoom = enable;
2819
2184
  }
2185
+ // 设置调试显示
2820
2186
  setDebug(debug) {
2821
2187
  if (this.collider) this.scene.remove(this.collider);
2822
2188
  if (debug) {
@@ -2827,6 +2193,7 @@ var PlayerController = class {
2827
2193
  }
2828
2194
  }
2829
2195
  // ==================== 销毁 ====================
2196
+ // 销毁控制器
2830
2197
  destroy() {
2831
2198
  this.offAllEvent();
2832
2199
  if (this.player) {
@@ -2847,7 +2214,8 @@ var PlayerController = class {
2847
2214
  this.scene.remove(this.collider);
2848
2215
  this.collider = null;
2849
2216
  }
2850
- this.destroyMobileControls();
2217
+ this.mobileControls?.destroy();
2218
+ this.mobileControls = null;
2851
2219
  for (const v of this.vehicles) {
2852
2220
  this.scene.remove(v.vehicleGroup);
2853
2221
  v.pathPlanner?.dispose();
@@ -2861,31 +2229,34 @@ function playerController() {
2861
2229
  if (!controllerInstance) controllerInstance = new PlayerController();
2862
2230
  const c = controllerInstance;
2863
2231
  return {
2864
- init: (opts, callback) => c.init(opts, callback),
2865
- loadVehicleModel: (params) => c.loadVehicleModel(params),
2866
- changeView: () => c.changeView(),
2867
- reset: (pos) => c.reset(pos),
2232
+ init: (opts, cb) => c.init(opts, cb),
2233
+ loadVehicleModel: (opts) => c.loadVehicleModel(opts),
2868
2234
  update: (dt) => c.update(dt),
2869
2235
  destroy: () => c.destroy(),
2236
+ reset: (pos) => c.reset(pos),
2870
2237
  setInput: (i) => c.setInput(i),
2238
+ changeView: () => c.changeView(),
2871
2239
  getPosition: () => c.getPosition(),
2872
2240
  getCenterScreenRaycastHit: () => c.getCenterScreenRaycastHit(),
2241
+ getCurrentPersonAnimationName: () => c.getCurrentPersonAnimationName(),
2873
2242
  getPerson: () => c.person,
2874
2243
  getActiveVehicle: () => c.activeVehicle,
2875
2244
  getAllVehicles: () => c.vehicles,
2876
2245
  switchPlayerModel: (model) => c.switchPlayerModel(model),
2877
- setMouseSensitivity: (mouseSensity) => c.setMouseSensitivity(mouseSensity),
2878
- setGravity: (gravity) => c.setGravity(gravity),
2879
- setJumpHeight: (jumpHeight) => c.setJumpHeight(jumpHeight),
2880
- setPlayerSpeed: (playerSpeed) => c.setPlayerSpeed(playerSpeed),
2881
- setPlayerFlySpeed: (playerFlySpeed) => c.setPlayerFlySpeed(playerFlySpeed),
2882
- setMinCamDistance: (minCamDistance) => c.setMinCamDistance(minCamDistance),
2883
- setMaxCamDistance: (maxCamDistance) => c.setMaxCamDistance(maxCamDistance),
2884
- setThirdMouseMode: (thirdMouseMode) => c.setThirdMouseMode(thirdMouseMode),
2885
- setEnableZoom: (enableZoom) => c.setEnableZoom(enableZoom),
2886
- setDebug: (debug) => c.setDebug(debug),
2887
- setOverShoulderView: (enable) => c.setOverShoulderView(enable),
2888
- setPlayerScale: (scale) => c.setPlayerScale(scale)
2246
+ setPlayerScale: (scale) => c.setPlayerScale(scale),
2247
+ setMouseSensitivity: (v) => c.setMouseSensitivity(v),
2248
+ setGravity: (v) => c.setGravity(v),
2249
+ setJumpHeight: (v) => c.setJumpHeight(v),
2250
+ setPlayerSpeed: (v) => c.setPlayerSpeed(v),
2251
+ setPlayerFlySpeed: (v) => c.setPlayerFlySpeed(v),
2252
+ setMinCamDistance: (v) => c.setMinCamDistance(v),
2253
+ setMaxCamDistance: (v) => c.setMaxCamDistance(v),
2254
+ setThirdMouseMode: (v) => c.setThirdMouseMode(v),
2255
+ setEnableZoom: (v) => c.setEnableZoom(v),
2256
+ setDebug: (v) => c.setDebug(v),
2257
+ setOverShoulderView: (v) => c.setOverShoulderView(v),
2258
+ registerAnimation: (key, clipName, opts) => c.registerAnimation(key, clipName, opts),
2259
+ playAnimation: (key, opts) => c.playAnimation(key, opts)
2889
2260
  };
2890
2261
  }
2891
2262
  function onAllEvent() {
@@ -2893,8 +2264,7 @@ function onAllEvent() {
2893
2264
  controllerInstance.onAllEvent();
2894
2265
  }
2895
2266
  function offAllEvent() {
2896
- if (!controllerInstance) return;
2897
- controllerInstance.offAllEvent();
2267
+ controllerInstance?.offAllEvent();
2898
2268
  }
2899
2269
  // Annotate the CommonJS export names for ESM import in node:
2900
2270
  0 && (module.exports = {