three-player-controller 0.3.7 → 0.3.9

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
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -36,7 +37,7 @@ __export(index_exports, {
36
37
  module.exports = __toCommonJS(index_exports);
37
38
 
38
39
  // src/playerController.ts
39
- var THREE3 = __toESM(require("three"));
40
+ var THREE4 = __toESM(require("three"));
40
41
  var import_three_mesh_bvh = require("three-mesh-bvh");
41
42
  var import_RoundedBoxGeometry = require("three/examples/jsm/geometries/RoundedBoxGeometry.js");
42
43
  var import_DRACOLoader = require("three/examples/jsm/loaders/DRACOLoader.js");
@@ -58,6 +59,231 @@ var vehicle_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAY
58
59
  // assets/imgs/view.png
59
60
  var view_default = "data:image/png;base64,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";
60
61
 
62
+ // src/utils/mobileControls.ts
63
+ var MobileControls = class {
64
+ constructor(setInput, controls) {
65
+ // 摇杆状态
66
+ this.nippleModule = null;
67
+ this.joystickManager = null;
68
+ this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
69
+ // DOM 元素
70
+ this.joystickZoneEl = null;
71
+ this.lookAreaEl = null;
72
+ this.jumpBtnEl = null;
73
+ this.flyBtnEl = null;
74
+ this.viewBtnEl = null;
75
+ this.vehicleBtnEl = null;
76
+ // 触摸状态
77
+ this.lookPointerId = null;
78
+ this.isLookDown = false;
79
+ this.lastTouchX = 0;
80
+ this.lastTouchY = 0;
81
+ // 触摸按下
82
+ this.onPointerDown = (e) => {
83
+ if (e.pointerType !== "touch") return;
84
+ this.isLookDown = true;
85
+ this.lookPointerId = e.pointerId;
86
+ this.lastTouchX = e.clientX;
87
+ this.lastTouchY = e.clientY;
88
+ this.lookAreaEl?.setPointerCapture?.(e.pointerId);
89
+ e.preventDefault();
90
+ };
91
+ // 触摸移动
92
+ this.onPointerMove = (e) => {
93
+ if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
94
+ const dx = e.clientX - this.lastTouchX;
95
+ const dy = e.clientY - this.lastTouchY;
96
+ this.lastTouchX = e.clientX;
97
+ this.lastTouchY = e.clientY;
98
+ this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
99
+ e.preventDefault();
100
+ };
101
+ // 触摸抬起
102
+ this.onPointerUp = (e) => {
103
+ if (e.pointerId !== this.lookPointerId) return;
104
+ this.isLookDown = false;
105
+ this.lookPointerId = null;
106
+ this.lookAreaEl?.releasePointerCapture?.(e.pointerId);
107
+ };
108
+ this.setInput = setInput;
109
+ this.controls = controls;
110
+ }
111
+ // 初始化移动端控制
112
+ async init() {
113
+ this.controls.maxPolarAngle = Math.PI * (300 / 360);
114
+ this.controls.touches = { ONE: null, TWO: null };
115
+ this.nippleModule = await import("nipplejs");
116
+ const nipple = this.nippleModule?.default;
117
+ const JOY_SIZE = 120;
118
+ const container = document.body;
119
+ this.joystickZoneEl = document.createElement("div");
120
+ this.joystickZoneEl.id = "joy-zone";
121
+ Object.assign(this.joystickZoneEl.style, {
122
+ position: "absolute",
123
+ left: "16px",
124
+ bottom: "16px",
125
+ width: `${JOY_SIZE + 40}px`,
126
+ height: `${JOY_SIZE + 40}px`,
127
+ touchAction: "none",
128
+ zIndex: "999",
129
+ pointerEvents: "auto",
130
+ WebkitUserSelect: "none",
131
+ userSelect: "none"
132
+ });
133
+ container.appendChild(this.joystickZoneEl);
134
+ this.blockTouch(this.joystickZoneEl);
135
+ this.joystickManager = nipple.create({
136
+ zone: this.joystickZoneEl,
137
+ mode: "static",
138
+ position: { left: `${(JOY_SIZE + 40) / 2}px`, bottom: `${(JOY_SIZE + 40) / 2}px` },
139
+ color: "#ffffff",
140
+ size: JOY_SIZE,
141
+ multitouch: true,
142
+ maxNumberOfNipples: 1
143
+ });
144
+ this.joystickManager.on("move", (_evt, data) => {
145
+ if (!data) return;
146
+ const rawX = data.vector?.x ?? 0;
147
+ const rawY = data.vector?.y ?? 0;
148
+ const distance = data.distance ?? 0;
149
+ const deadzone = 0.5;
150
+ const dirX = rawX > deadzone ? 1 : rawX < -deadzone ? -1 : 0;
151
+ const dirY = rawY > deadzone ? 1 : rawY < -deadzone ? -1 : 0;
152
+ const isSprinting = distance >= JOY_SIZE / 2;
153
+ const prev = this.prevJoyState;
154
+ if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) return;
155
+ this.prevJoyState = { dirX, dirY, shift: isSprinting };
156
+ this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
157
+ });
158
+ this.joystickManager.on("end", () => {
159
+ const prev = this.prevJoyState;
160
+ if (prev.dirX !== 0 || prev.dirY !== 0 || prev.shift) {
161
+ this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
162
+ this.setInput({ moveX: 0, moveY: 0, shift: false });
163
+ }
164
+ });
165
+ this.lookAreaEl = document.createElement("div");
166
+ Object.assign(this.lookAreaEl.style, {
167
+ position: "absolute",
168
+ right: "0",
169
+ bottom: "0",
170
+ width: "50%",
171
+ height: "100%",
172
+ zIndex: "998",
173
+ touchAction: "none",
174
+ WebkitUserSelect: "none",
175
+ userSelect: "none"
176
+ });
177
+ container.appendChild(this.lookAreaEl);
178
+ this.blockTouch(this.lookAreaEl);
179
+ this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, { passive: false });
180
+ this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, { passive: false });
181
+ this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, { passive: false });
182
+ this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, { passive: false });
183
+ this.jumpBtnEl = this.createBtn(container, 14, 14, jump_default);
184
+ this.flyBtnEl = this.createBtn(container, 14, 14 + 80, fly_default);
185
+ this.viewBtnEl = this.createBtn(container, 14, 14 + 200, view_default);
186
+ this.vehicleBtnEl = this.createBtn(container, 14 + 100, 14 + 120, vehicle_default);
187
+ this.vehicleBtnEl.style.display = "none";
188
+ this.jumpBtnEl.addEventListener("touchstart", (e) => {
189
+ e.preventDefault();
190
+ this.setInput({ jump: true });
191
+ }, { passive: false });
192
+ this.jumpBtnEl.addEventListener("touchend", (e) => {
193
+ e.preventDefault();
194
+ this.setInput({ jump: false });
195
+ }, { passive: false });
196
+ this.jumpBtnEl.addEventListener("touchcancel", (e) => {
197
+ e.preventDefault();
198
+ this.setInput({ jump: false });
199
+ }, { passive: false });
200
+ this.flyBtnEl.addEventListener("touchstart", (e) => {
201
+ e.preventDefault();
202
+ this.setInput({ toggleFly: true });
203
+ }, { passive: false });
204
+ this.viewBtnEl.addEventListener("touchstart", (e) => {
205
+ e.preventDefault();
206
+ this.setInput({ toggleView: true });
207
+ }, { passive: false });
208
+ this.vehicleBtnEl.addEventListener("touchstart", (e) => {
209
+ e.preventDefault();
210
+ this.setInput({ toggleVehicle: true });
211
+ }, { passive: false });
212
+ }
213
+ // 销毁移动端控制
214
+ destroy() {
215
+ try {
216
+ this.joystickManager?.destroy?.();
217
+ this.joystickManager = null;
218
+ if (this.lookAreaEl) {
219
+ this.lookAreaEl.removeEventListener("pointerdown", this.onPointerDown);
220
+ this.lookAreaEl.removeEventListener("pointermove", this.onPointerMove);
221
+ this.lookAreaEl.removeEventListener("pointerup", this.onPointerUp);
222
+ this.lookAreaEl.removeEventListener("pointercancel", this.onPointerUp);
223
+ }
224
+ [this.joystickZoneEl, this.lookAreaEl, this.jumpBtnEl, this.flyBtnEl, this.viewBtnEl, this.vehicleBtnEl].forEach((el) => el?.parentElement?.removeChild(el));
225
+ this.joystickZoneEl = this.lookAreaEl = this.jumpBtnEl = this.flyBtnEl = this.viewBtnEl = this.vehicleBtnEl = null;
226
+ } catch (e) {
227
+ console.warn("\u9500\u6BC1\u79FB\u52A8\u7AEF\u63A7\u5236\u65F6\u51FA\u9519\uFF1A", e);
228
+ }
229
+ }
230
+ // 同步车辆按钮显隐
231
+ syncVehicleBtn(show) {
232
+ if (this.vehicleBtnEl) this.vehicleBtnEl.style.display = show ? "block" : "none";
233
+ }
234
+ // 同步控制模式按钮
235
+ syncControllerModeBtn(mode) {
236
+ if (!this.flyBtnEl || !this.jumpBtnEl) return;
237
+ if (mode === 0) {
238
+ this.flyBtnEl.style.display = "block";
239
+ this.jumpBtnEl.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${jump_default}")`;
240
+ } else {
241
+ this.flyBtnEl.style.display = "none";
242
+ this.jumpBtnEl.style.backgroundImage = `url(${break_default})`;
243
+ }
244
+ }
245
+ // 阻止默认触摸
246
+ blockTouch(el) {
247
+ ["touchstart", "touchmove", "touchend", "touchcancel"].forEach((name) => {
248
+ el.addEventListener(name, (e) => e.preventDefault(), { passive: false });
249
+ });
250
+ }
251
+ // 创建圆形按钮
252
+ createBtn(container, rightPx, bottomPx, bgUrl) {
253
+ const btn = document.createElement("button");
254
+ Object.assign(btn.style, {
255
+ position: "absolute",
256
+ right: `${rightPx}px`,
257
+ bottom: `${bottomPx}px`,
258
+ width: "56px",
259
+ height: "56px",
260
+ zIndex: "1000",
261
+ borderRadius: "50%",
262
+ border: "2px solid black",
263
+ padding: "20px",
264
+ opacity: "0.95",
265
+ touchAction: "none",
266
+ fontSize: "14px",
267
+ userSelect: "none",
268
+ overflow: "hidden",
269
+ boxSizing: "border-box",
270
+ backgroundColor: "transparent",
271
+ backgroundRepeat: "no-repeat, no-repeat",
272
+ backgroundPosition: "center center, center center",
273
+ backgroundSize: "100% 100%, 80% 80%",
274
+ backgroundImage: `linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${bgUrl}")`
275
+ });
276
+ container.appendChild(btn);
277
+ ["touchstart", "touchend", "touchcancel"].forEach((name) => {
278
+ btn.addEventListener(name, (e) => e.preventDefault(), { passive: false });
279
+ });
280
+ return btn;
281
+ }
282
+ };
283
+
284
+ // src/utils/vehicleLoader.ts
285
+ var THREE3 = __toESM(require("three"));
286
+
61
287
  // src/utils/pathPlanner.ts
62
288
  var THREE = __toESM(require("three"));
63
289
  var PathNode = class {
@@ -71,6 +297,7 @@ var PathNode = class {
71
297
  this.parent = null;
72
298
  this.position = position.clone();
73
299
  }
300
+ // 判断节点相等
74
301
  equals(other) {
75
302
  return this.position.distanceTo(other.position) < 0.01;
76
303
  }
@@ -79,10 +306,12 @@ var PriorityQueue = class {
79
306
  constructor() {
80
307
  this.elements = [];
81
308
  }
309
+ // 入队并排序
82
310
  enqueue(item, priority) {
83
311
  this.elements.push({ priority, item });
84
312
  this.elements.sort((a, b) => a.priority - b.priority);
85
313
  }
314
+ // 出队最小优先级
86
315
  dequeue() {
87
316
  return this.elements.shift()?.item;
88
317
  }
@@ -92,6 +321,7 @@ var PriorityQueue = class {
92
321
  contains(item, compareFn) {
93
322
  return this.elements.some((e) => compareFn(e.item, item));
94
323
  }
324
+ // 更新节点优先级
95
325
  update(item, newPriority, compareFn) {
96
326
  const index = this.elements.findIndex((e) => compareFn(e.item, item));
97
327
  if (index !== -1) {
@@ -105,22 +335,15 @@ var PathPlanner = class {
105
335
  this.debugLines = [];
106
336
  this.debugPoints = [];
107
337
  this.obstacleChecker = obstacleChecker;
108
- this.config = {
109
- debugEnabled: false,
110
- scale: 1,
111
- ...config
112
- };
338
+ this.config = { debugEnabled: false, scale: 1, ...config };
113
339
  }
114
- // 计算启发式距离
340
+ // 启发式距离
115
341
  heuristic(a, b) {
116
342
  return a.distanceTo(b);
117
343
  }
118
- // A*路径规划算法
344
+ // A* 寻路主入口
119
345
  findPath(start, goal) {
120
- const startTime = performance.now();
121
- if (!this.obstacleChecker.isBlocked(start, goal)) {
122
- return [goal];
123
- }
346
+ if (!this.obstacleChecker.isBlocked(start, goal)) return [goal];
124
347
  const navigationPoints = this.obstacleChecker.getNavigationNodes(start, goal);
125
348
  const allNodes = [new PathNode(start), new PathNode(goal), ...navigationPoints.map((p) => new PathNode(p))];
126
349
  if (allNodes.length < 2) {
@@ -134,45 +357,35 @@ var PathPlanner = class {
134
357
  startNode.f = startNode.h;
135
358
  const openList = new PriorityQueue();
136
359
  const closedSet = /* @__PURE__ */ new Set();
137
- openList.enqueue(startNode, startNode.f);
138
360
  const nodeEquals = (a, b) => a.equals(b);
361
+ openList.enqueue(startNode, startNode.f);
139
362
  while (!openList.isEmpty()) {
140
363
  const current = openList.dequeue();
141
364
  if (!current) break;
142
365
  if (current.equals(goalNode)) {
143
366
  const path = this.reconstructPath(current);
144
- const endTime = performance.now();
145
- if (this.config.debugEnabled) {
146
- this.visualizePath([start, ...path]);
147
- }
367
+ if (this.config.debugEnabled) this.visualizePath([start, ...path]);
148
368
  return path;
149
369
  }
150
370
  closedSet.add(current);
151
371
  for (const neighbor of allNodes) {
152
372
  if (closedSet.has(neighbor)) continue;
153
- if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) {
154
- continue;
155
- }
373
+ if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) continue;
156
374
  const tentativeG = current.g + current.position.distanceTo(neighbor.position);
157
375
  if (tentativeG < neighbor.g) {
158
376
  neighbor.parent = current;
159
377
  neighbor.g = tentativeG;
160
378
  neighbor.h = this.heuristic(neighbor.position, goalNode.position);
161
379
  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
- }
380
+ if (openList.contains(neighbor, nodeEquals)) openList.update(neighbor, neighbor.f, nodeEquals);
381
+ else openList.enqueue(neighbor, neighbor.f);
167
382
  }
168
383
  }
169
384
  }
170
- console.warn("A*\u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
385
+ console.warn("A* \u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
171
386
  return [goal];
172
387
  }
173
- /**
174
- * 重建路径
175
- */
388
+ // 重建路径
176
389
  reconstructPath(endNode) {
177
390
  const path = [];
178
391
  let current = endNode;
@@ -180,14 +393,10 @@ var PathPlanner = class {
180
393
  path.unshift(current.position.clone());
181
394
  current = current.parent;
182
395
  }
183
- if (path.length > 0) {
184
- path.shift();
185
- }
396
+ if (path.length > 0) path.shift();
186
397
  return this.smoothPath(path);
187
398
  }
188
- /**
189
- * 路径平滑
190
- */
399
+ // 路径平滑优化
191
400
  smoothPath(path) {
192
401
  if (path.length <= 2) return path;
193
402
  const smoothed = [path[0]];
@@ -205,38 +414,28 @@ var PathPlanner = class {
205
414
  }
206
415
  return smoothed;
207
416
  }
208
- /**
209
- * 可视化路径
210
- */
417
+ // 可视化路径
211
418
  visualizePath(path) {
212
419
  if (!this.config.scene || !this.config.debugEnabled) return;
213
420
  this.clearVisualization();
214
421
  const scale = this.config.scale || 1;
215
422
  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);
423
+ const geometry = new THREE.BufferGeometry().setFromPoints(path.map((p) => p.clone()));
424
+ const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 65280, linewidth: 3 }));
223
425
  this.config.scene.add(line);
224
426
  this.debugLines.push(line);
225
427
  }
226
428
  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);
429
+ const sphere = new THREE.Mesh(
430
+ new THREE.SphereGeometry(20 * scale),
431
+ new THREE.MeshBasicMaterial({ color: index === path.length - 1 ? 16711680 : 65280 })
432
+ );
232
433
  sphere.position.copy(point);
233
434
  this.config.scene.add(sphere);
234
435
  this.debugPoints.push(sphere);
235
436
  });
236
437
  }
237
- /**
238
- * 清除路径可视化
239
- */
438
+ // 清除路径可视化
240
439
  clearVisualization() {
241
440
  if (!this.config.scene) return;
242
441
  this.debugLines.forEach((line) => {
@@ -252,15 +451,11 @@ var PathPlanner = class {
252
451
  });
253
452
  this.debugPoints = [];
254
453
  }
255
- /**
256
- * 更新配置
257
- */
454
+ // 更新配置
258
455
  updateConfig(config) {
259
456
  this.config = { ...this.config, ...config };
260
457
  }
261
- /**
262
- * 销毁
263
- */
458
+ // 销毁规划器
264
459
  dispose() {
265
460
  this.clearVisualization();
266
461
  }
@@ -280,7 +475,7 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
280
475
  vehicle.setWheelAxleCs(index, wheel.axleCs);
281
476
  vehicle.setWheelSuspensionRestLength(index, wheel.suspensionRestLength);
282
477
  vehicle.setWheelRadius(index, wheel.radius);
283
- vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength * 1);
478
+ vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength);
284
479
  vehicle.setWheelSuspensionStiffness(index, 250);
285
480
  vehicle.setWheelSuspensionCompression(index, 6);
286
481
  vehicle.setWheelSuspensionRelaxation(index, 6);
@@ -292,8 +487,8 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
292
487
  vehicle.setWheelSideFrictionStiffness(index, 2);
293
488
  });
294
489
  const up = new THREE2.Vector3(0, 1, 0);
295
- const _wheelSteeringQuat = new THREE2.Quaternion();
296
- const _wheelRotationQuat = new THREE2.Quaternion();
490
+ const wheelSteeringQuat = new THREE2.Quaternion();
491
+ const wheelRotationQuat = new THREE2.Quaternion();
297
492
  function updateWheelVisuals() {
298
493
  for (const [index, wheelObj] of wheels.entries()) {
299
494
  if (!wheelObj) continue;
@@ -304,9 +499,9 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
304
499
  const steering = vehicle.wheelSteering(index) ?? 0;
305
500
  const rotationRad = vehicle.wheelRotation(index) ?? 0;
306
501
  wheelObj.position.y = connection - suspension;
307
- _wheelSteeringQuat.setFromAxisAngle(up, steering);
308
- _wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
309
- wheelObj.quaternion.copy(_wheelSteeringQuat).multiply(_wheelRotationQuat);
502
+ wheelSteeringQuat.setFromAxisAngle(up, steering);
503
+ wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
504
+ wheelObj.quaternion.copy(wheelSteeringQuat).multiply(wheelRotationQuat);
310
505
  } catch (e) {
311
506
  }
312
507
  }
@@ -317,37 +512,247 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
317
512
  } catch {
318
513
  }
319
514
  }
515
+ return { vehicle, updateWheelVisuals, destroy };
516
+ }
517
+
518
+ // src/utils/vehicleLoader.ts
519
+ function getBbox(object) {
520
+ const bbox = new THREE3.Box3().setFromObject(object);
521
+ const center = new THREE3.Vector3();
522
+ const size = new THREE3.Vector3();
523
+ bbox.getCenter(center);
524
+ bbox.getSize(size);
525
+ return { bbox, center, size };
526
+ }
527
+ function createObstacleChecker(vehicleGroup, bbox, scale, playerScale) {
528
+ return {
529
+ // 射线检测路径是否被车辆遮挡
530
+ isBlocked(start, end) {
531
+ const vehiclePos = vehicleGroup.position;
532
+ const vehicleQuat = vehicleGroup.quaternion;
533
+ const center = new THREE3.Vector3();
534
+ const size = new THREE3.Vector3();
535
+ bbox.getCenter(center);
536
+ bbox.getSize(size);
537
+ center.applyQuaternion(vehicleQuat).add(vehiclePos);
538
+ const halfSize = size.clone().multiplyScalar(0.5 * scale);
539
+ const corners = [];
540
+ for (let x = -1; x <= 1; x += 2)
541
+ for (let y = -1; y <= 1; y += 2)
542
+ for (let z = -1; z <= 1; z += 2)
543
+ corners.push(
544
+ new THREE3.Vector3(halfSize.x * x, halfSize.y * y, halfSize.z * z).applyQuaternion(vehicleQuat).add(center)
545
+ );
546
+ const expandedBBox = new THREE3.Box3();
547
+ corners.forEach((c) => expandedBBox.expandByPoint(c));
548
+ expandedBBox.expandByScalar(100 * playerScale);
549
+ const direction = new THREE3.Vector3().subVectors(end, start);
550
+ const length = direction.length();
551
+ const ray = new THREE3.Ray(start, direction.normalize());
552
+ const intersects = ray.intersectBox(expandedBBox, new THREE3.Vector3());
553
+ return intersects !== null && start.distanceTo(intersects) < length;
554
+ },
555
+ // 生成绕行导航节点
556
+ getNavigationNodes(start, _goal) {
557
+ const nodes = [];
558
+ const vehiclePos = vehicleGroup.position;
559
+ const vehicleQuat = vehicleGroup.quaternion;
560
+ const bboxSize = new THREE3.Vector3();
561
+ bbox.getSize(bboxSize);
562
+ const fwd = new THREE3.Vector3(0, 0, 1).applyQuaternion(vehicleQuat);
563
+ const right = new THREE3.Vector3(1, 0, 0).applyQuaternion(vehicleQuat);
564
+ const halfLen = bboxSize.z / 2 * scale;
565
+ const halfWidth = bboxSize.x / 2 * scale;
566
+ const groundY = start.y;
567
+ for (const margin of [300 * playerScale, 500 * playerScale]) {
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
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(-halfLen - margin)).add(right.clone().multiplyScalar(halfWidth + margin)).setY(groundY));
572
+ }
573
+ return nodes;
574
+ }
575
+ };
576
+ }
577
+ async function loadVehicleModel(opts, ctx) {
578
+ const { loader, scene, world, RAPIER, vehicleParams, vehicleLength, playerScale } = ctx;
579
+ const scale = opts.scale ?? 1;
580
+ const chassisRatio = opts.chassisRatio ?? 0.2;
581
+ const suspensionRestLengthRatio = opts.suspensionRestLengthRatio ?? 0.2;
582
+ const speedMultiplier = opts.speedMultiplier ?? 1;
583
+ vehicleParams.power.accelerateForce = 50 * scale;
584
+ vehicleParams.power.brakeForce = 200 * scale;
585
+ vehicleParams.power.maxSpeed = 1e4 * scale;
586
+ vehicleParams.followVehicleDirection = opts.followVehicleDirection ?? true;
587
+ const vehicleModel = await loader.loadAsync(opts.url);
588
+ const { size: originalSize } = getBbox(vehicleModel.scene);
589
+ const modelScale = vehicleLength / Math.max(originalSize.x, originalSize.y, originalSize.z);
590
+ const vehicleMixer = new THREE3.AnimationMixer(vehicleModel.scene);
591
+ const vehicleActions = /* @__PURE__ */ new Map();
592
+ const animations = vehicleModel.animations ?? [];
593
+ const openDoorClip = animations.find((a) => a.name === (opts.animations?.openDoorAnim ?? ""));
594
+ if (openDoorClip) {
595
+ const action = vehicleMixer.clipAction(openDoorClip);
596
+ action.setLoop(THREE3.LoopOnce, 1);
597
+ action.clampWhenFinished = true;
598
+ action.setEffectiveTimeScale(openDoorClip.duration);
599
+ action.enabled = true;
600
+ action.setEffectiveWeight(0);
601
+ vehicleActions.set("openDoor", action);
602
+ }
603
+ const wheelObjects = [];
604
+ for (const name of opts.wheelsNames) {
605
+ let found = false;
606
+ vehicleModel.scene.traverse((child) => {
607
+ if (child.name === name && !found) {
608
+ wheelObjects.push(child);
609
+ found = true;
610
+ }
611
+ });
612
+ if (!found) console.warn(`\u672A\u627E\u5230\u8F6E\u5B50: ${name}`);
613
+ }
614
+ const tempGroup = new THREE3.Group();
615
+ scene.add(tempGroup);
616
+ vehicleModel.scene.scale.multiplyScalar(modelScale * scale);
617
+ vehicleModel.scene.rotateY(vehicleParams.model.rotation);
618
+ const { size, bbox, center } = getBbox(vehicleModel.scene);
619
+ vehicleModel.scene.position.set(-center.x, -center.y, -center.z);
620
+ tempGroup.add(vehicleModel.scene);
621
+ tempGroup.updateMatrixWorld(true);
622
+ let wheelRadius = 0, wheelWidth = 0, suspensionRestLength = 0, chassisHeight = 0, wheelSizeInit = false;
623
+ const wheelsInfo = [];
624
+ for (const wheel of wheelObjects) {
625
+ const worldPos = new THREE3.Vector3();
626
+ const worldQuat = new THREE3.Quaternion();
627
+ const worldScale = new THREE3.Vector3();
628
+ wheel.getWorldPosition(worldPos);
629
+ wheel.getWorldQuaternion(worldQuat);
630
+ wheel.getWorldScale(worldScale);
631
+ if (!wheelSizeInit) {
632
+ const { size: ws } = getBbox(wheel);
633
+ wheelRadius = Number((Math.max(ws.x, ws.y, ws.z) / 2).toFixed(2));
634
+ wheelWidth = Number(Math.min(ws.x, ws.y, ws.z).toFixed(2));
635
+ suspensionRestLength = Number((wheelRadius * 2 * suspensionRestLengthRatio).toFixed(2));
636
+ chassisHeight = Number((wheelRadius * 2 * chassisRatio).toFixed(2));
637
+ wheelSizeInit = true;
638
+ }
639
+ wheelsInfo.push({ axleCs: new THREE3.Vector3(0, 0, -1), position: worldPos, quaternion: worldQuat, scale: worldScale, radius: wheelRadius, width: wheelWidth, suspensionRestLength, object: wheel });
640
+ }
641
+ tempGroup.remove(vehicleModel.scene);
642
+ scene.remove(tempGroup);
643
+ const vehicleGroup = new THREE3.Group();
644
+ scene.add(vehicleGroup);
645
+ vehicleGroup.add(vehicleModel.scene);
646
+ vehicleGroup.updateMatrixWorld(true);
647
+ const wheelWrappers = [];
648
+ for (let i = 0; i < wheelsInfo.length; i++) {
649
+ const wheel = wheelsInfo[i];
650
+ const wheelWrapper = new THREE3.Group();
651
+ wheelWrapper.position.copy(vehicleGroup.worldToLocal(wheel.position.clone()));
652
+ const wheelObj = wheel.object;
653
+ wheelObj.parent?.remove(wheelObj);
654
+ wheelObj.position.set(0, 0, 0);
655
+ wheelObj.quaternion.copy(wheel.quaternion);
656
+ wheelObj.scale.copy(wheel.scale);
657
+ wheelObj.updateMatrixWorld();
658
+ wheelWrapper.add(wheelObj);
659
+ vehicleGroup.add(wheelWrapper);
660
+ wheelWrappers.push(wheelWrapper);
661
+ }
662
+ const halfExtents = size.clone().multiplyScalar(0.5);
663
+ halfExtents.y -= chassisHeight / 2;
664
+ vehicleModel.scene.position.y -= chassisHeight / 2;
665
+ halfExtents.x *= 0.95;
666
+ halfExtents.z *= 0.95;
667
+ const chassisBody = world.createRigidBody(
668
+ 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)
669
+ );
670
+ world.createCollider(RAPIER.ColliderDesc.cuboid(halfExtents.x, halfExtents.y, halfExtents.z), chassisBody);
671
+ if (vehicleParams.debug.showPhysicsBox) {
672
+ vehicleGroup.add(new THREE3.Mesh(
673
+ new THREE3.BoxGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2),
674
+ new THREE3.MeshBasicMaterial({ color: 16711680, wireframe: true, transparent: true, opacity: 0.3 })
675
+ ));
676
+ }
677
+ vehicleGroup.position.copy(opts.position);
678
+ vehicleGroup.updateMatrixWorld(true);
679
+ const { vehicle, updateWheelVisuals } = createVehicleController(world, chassisBody, wheelWrappers, wheelsInfo);
320
680
  return {
321
- vehicle,
681
+ vehicleGroup,
682
+ chassisBody,
683
+ vehicleController: vehicle,
322
684
  updateWheelVisuals,
323
- destroy
685
+ vehicleMixer,
686
+ vehicleActions,
687
+ vehiclIsOpenDoor: false,
688
+ vehicleBBox: bbox.clone(),
689
+ pathPlanner: new PathPlanner(
690
+ createObstacleChecker(vehicleGroup, bbox, scale, playerScale),
691
+ { debugEnabled: false, scene, scale: playerScale }
692
+ ),
693
+ scale,
694
+ boardingPoint: opts.boardingPoint,
695
+ seatOffset: opts.seatOffset ?? new THREE3.Vector3(),
696
+ enterVehicleTime: 1.5,
697
+ chassisRatio,
698
+ suspensionRestLengthRatio,
699
+ size: { l: Math.max(size.x, size.z), w: Math.min(size.x, size.z), h: size.y },
700
+ speedMultiplier
324
701
  };
325
702
  }
326
703
 
327
704
  // src/playerController.ts
328
- THREE3.Mesh.prototype.raycast = import_three_mesh_bvh.acceleratedRaycast;
705
+ THREE4.Mesh.prototype.raycast = import_three_mesh_bvh.acceleratedRaycast;
329
706
  var controllerInstance = null;
330
- var clock = new THREE3.Clock();
707
+ var clock = new THREE4.Clock();
708
+ function isMobileDevice() {
709
+ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
710
+ }
331
711
  var PlayerController = class {
332
712
  constructor() {
333
- // ==================== 基本配置与参数 ====================
713
+ // ==================== 场景引用 ====================
334
714
  this.loader = new import_GLTFLoader.GLTFLoader();
335
- this.controllerMode = 0;
336
- // 0: 人物 1: 车辆
715
+ // 物理参数
716
+ this.gravity = -2400;
717
+ this.jumpHeight = 600;
718
+ this.playerSpeed = 300;
719
+ this.playerFlySpeed = 2100;
720
+ this.curPlayerSpeed = 0;
721
+ // 交互参数
722
+ this.mouseSensitivity = 5;
723
+ this.thirdMouseMode = 1;
724
+ this.enableZoom = false;
725
+ this.playerFlyEnabled = true;
726
+ this.isShowMobileControls = true;
337
727
  this.enableOverShoulderView = false;
338
- this.isChangeControllerTransitionTimer = null;
339
- // ==================== 玩家基本属性 ====================
728
+ // ==================== 玩家胶囊体 ====================
340
729
  this.playerCapsuleRadius = 45;
341
- this.playerCapsuleRadiusRatio = 1;
342
730
  this.playerCapsuleHeight = 180;
343
- // 玩家参考身高
731
+ this.playerCapsuleRadiusRatio = 1;
344
732
  this.isFirstPerson = false;
345
733
  this.boundingBoxMinY = 0;
346
- // ==================== 测试参数 ====================
347
- this.displayPlayer = false;
348
- this.displayCollider = false;
349
- this.displayVisualizer = false;
350
- // ==================== 场景对象 ====================
734
+ // ==================== 运行状态 ====================
735
+ this.controllerMode = 0;
736
+ // 0:人物 1:车辆
737
+ this.playerIsOnGround = false;
738
+ this.isupdate = true;
739
+ this.isFlying = false;
740
+ this.isChangeControllerTransitionTimer = null;
741
+ // ==================== 输入按键 ====================
742
+ this.fwdPressed = false;
743
+ this.bkdPressed = false;
744
+ this.lftPressed = false;
745
+ this.rgtPressed = false;
746
+ this.spacePressed = false;
747
+ this.ctPressed = false;
748
+ this.shiftPressed = false;
749
+ // ==================== 相机参数 ====================
750
+ this.camCollisionLerp = 0.18;
751
+ this.camEpsilon = 35;
752
+ this.minCamDistance = 100;
753
+ this.maxCamDistance = 440;
754
+ this.orginMaxCamDistance = 440;
755
+ // ==================== 场景碰撞体 ====================
351
756
  this.collider = null;
352
757
  this.visualizer = null;
353
758
  this.person = null;
@@ -355,150 +760,78 @@ var PlayerController = class {
355
760
  this.collected = [];
356
761
  this.dynamicCollider = null;
357
762
  this.dynamicCollected = [];
358
- // ==================== 多车辆相关 ====================
763
+ // ==================== 车辆系统 ====================
359
764
  this.vehicles = [];
360
- // 所有已加载车辆
361
765
  this.activeVehicle = null;
362
- // 当前驾驶/交互的车辆
363
766
  this.vehicleLength = 6;
364
- // 车辆参考长度
365
- this.wheelSteeringQuat = new THREE3.Quaternion();
366
- this.wheelRotationQuat = new THREE3.Quaternion();
367
767
  this.RAPIER = null;
368
768
  this.world = null;
369
- // 全局车辆共享参数
769
+ this.wheelSteeringQuat = new THREE4.Quaternion();
770
+ this.wheelRotationQuat = new THREE4.Quaternion();
771
+ this.camBehindDir = new THREE4.Vector3(0, 0, 1);
370
772
  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
- },
773
+ debug: { showPhysicsBox: false },
774
+ chassis: { linearDamping: 0.5, angularDamping: 0.5 },
775
+ model: { rotation: -Math.PI / 2 },
776
+ power: { accelerateForce: 50, brakeForce: 200, maxSpeed: 1e4 },
777
+ steering: { maxSteerAngle: Math.PI / 4, steerSpeed: 0.5, steerReturnSpeed: 1 },
394
778
  followVehicleDirection: true
395
779
  };
396
- this.camBehindDir = new THREE3.Vector3(0, 0, 1);
397
- // ==================== 上车相关 ====================
780
+ // ==================== 上车流程 ====================
398
781
  this.isMovingToBoardingPoint = false;
399
782
  this.boardingWaypoints = [];
400
783
  this.currentWaypointIndex = 0;
401
784
  this.boardingTargetDir = null;
402
785
  this.boardingMoveSpeed = 300;
403
786
  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
787
  this.boardingPointWorld = null;
410
788
  this.isBoardingAnimPlaying = false;
411
789
  this.closeDoorTriggered = false;
412
790
  this.isExitAnimPlaying = false;
413
791
  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();
792
+ this.closeVehicleDoorTimer = null;
793
+ this.flip180Quat = new THREE4.Quaternion().setFromAxisAngle(new THREE4.Vector3(0, 1, 0), Math.PI);
458
794
  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();
795
+ this.allAnimations = [];
796
+ // ==================== 移动端控制 ====================
797
+ this.mobileControls = null;
798
+ this.nearCheckLocal = new THREE4.Vector3();
799
+ this.nearCheckWorld = new THREE4.Vector3();
800
+ this.isNearVehicle = false;
801
+ // ==================== 调试显示 ====================
802
+ this.displayPlayer = false;
803
+ this.displayCollider = false;
804
+ this.displayVisualizer = false;
805
+ // ==================== 方向常量 ====================
464
806
  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);
807
+ this.DIR_FWD = new THREE4.Vector3(0, 0, -1);
808
+ this.DIR_BKD = new THREE4.Vector3(0, 0, 1);
809
+ this.DIR_LFT = new THREE4.Vector3(-1, 0, 0);
810
+ this.DIR_RGT = new THREE4.Vector3(1, 0, 0);
811
+ this.DIR_UP = new THREE4.Vector3(0, 1, 0);
812
+ // ==================== 复用向量 ====================
813
+ this.upVector = new THREE4.Vector3(0, 1, 0);
814
+ this.playerVelocity = new THREE4.Vector3();
815
+ this.camDir = new THREE4.Vector3();
816
+ this.moveDir = new THREE4.Vector3();
817
+ this.targetQuat = new THREE4.Quaternion();
818
+ this.targetMat = new THREE4.Matrix4();
819
+ this.tempVector = new THREE4.Vector3();
820
+ this.tempVector2 = new THREE4.Vector3();
821
+ this.tempBox = new THREE4.Box3();
822
+ this.tempMat = new THREE4.Matrix4();
823
+ this.tempSegment = new THREE4.Line3();
470
824
  // ==================== 射线检测 ====================
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
- };
825
+ this.personToCam = new THREE4.Vector3();
826
+ this.originTmp = new THREE4.Vector3();
827
+ this.raycaster = new THREE4.Raycaster(new THREE4.Vector3(), new THREE4.Vector3(0, -1, 0));
828
+ this.raycasterPersonToCam = new THREE4.Raycaster(new THREE4.Vector3(), new THREE4.Vector3());
829
+ this.centerRay = new THREE4.Raycaster();
830
+ this.centerMouse = new THREE4.Vector2();
831
+ // 按键动画同步
494
832
  this.setAnimationByPressed = () => {
495
833
  this.maxCamDistance = this.orginMaxCamDistance;
496
- if (this.isMovingToBoardingPoint) {
497
- this.isMovingToBoardingPoint = false;
498
- this.boardingWaypoints = [];
499
- this.currentWaypointIndex = 0;
500
- this.boardingTargetDir = null;
501
- }
834
+ this.cancelBoarding();
502
835
  if (this.isExitAnimPlaying) {
503
836
  this.isExitAnimPlaying = false;
504
837
  this.closeExitDoorTriggered = false;
@@ -526,15 +859,11 @@ var PlayerController = class {
526
859
  return;
527
860
  }
528
861
  if (this.fwdPressed) {
529
- this.playPersonAnimationByName(
530
- this.shiftPressed ? "running" : "walking"
531
- );
862
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
532
863
  return;
533
864
  }
534
865
  if (!this.isFirstPerson && (this.lftPressed || this.rgtPressed || this.bkdPressed)) {
535
- this.playPersonAnimationByName(
536
- this.shiftPressed ? "running" : "walking"
537
- );
866
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
538
867
  return;
539
868
  }
540
869
  if (this.lftPressed) {
@@ -550,19 +879,16 @@ var PlayerController = class {
550
879
  return;
551
880
  }
552
881
  }
553
- if (this.recheckAnimTimer !== null) {
554
- clearTimeout(this.recheckAnimTimer);
555
- }
882
+ if (this.recheckAnimTimer !== null) clearTimeout(this.recheckAnimTimer);
556
883
  this.recheckAnimTimer = setTimeout(() => {
557
884
  this.setAnimationByPressed();
558
885
  this.recheckAnimTimer = null;
559
886
  }, 200);
560
887
  };
561
888
  // ==================== 事件处理 ====================
562
- this._boundOnKeydown = async (e) => {
563
- if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) {
564
- e.preventDefault();
565
- }
889
+ // 键盘按下事件
890
+ this.boundOnKeydown = async (e) => {
891
+ if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) e.preventDefault();
566
892
  switch (e.code) {
567
893
  case "KeyW":
568
894
  case "ArrowUp":
@@ -591,17 +917,11 @@ var PlayerController = class {
591
917
  this.controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 };
592
918
  break;
593
919
  case "Space":
594
- if (this.isMovingToBoardingPoint) {
595
- this.isMovingToBoardingPoint = false;
596
- this.boardingWaypoints = [];
597
- this.currentWaypointIndex = 0;
598
- this.boardingTargetDir = null;
599
- }
920
+ this.cancelBoarding();
600
921
  this.spacePressed = true;
601
- if (this.controllerMode == 1) return;
922
+ if (this.controllerMode === 1) return;
602
923
  if (!this.playerIsOnGround || this.isFlying) return;
603
- const next = this.personActions?.get("jumping");
604
- if (next && this.actionState === next) return;
924
+ if (this.personActions?.get("jumping") === this.actionState) return;
605
925
  this.playPersonAnimationByName("jumping");
606
926
  this.playerVelocity.y = this.jumpHeight;
607
927
  this.playerIsOnGround = false;
@@ -613,25 +933,21 @@ var PlayerController = class {
613
933
  this.changeView();
614
934
  break;
615
935
  case "KeyF":
616
- if (this.controllerMode == 0 && this.playerFlyEnabled) {
936
+ if (this.controllerMode === 0 && this.playerFlyEnabled) {
617
937
  this.isFlying = !this.isFlying;
618
938
  this.setAnimationByPressed();
619
- if (!this.isFlying && !this.playerIsOnGround) {
620
- this.playPersonAnimationByName("jumping");
621
- }
939
+ if (!this.isFlying && !this.playerIsOnGround) this.playPersonAnimationByName("jumping");
622
940
  }
623
941
  break;
624
942
  case "KeyE":
625
943
  if (this.isFlying) return;
626
- if (this.controllerMode == 0) {
627
- this.enterVehicle();
628
- } else {
629
- this.exitVehicle();
630
- }
944
+ if (this.controllerMode === 0) this.enterVehicle();
945
+ else this.exitVehicle();
631
946
  break;
632
947
  }
633
948
  };
634
- this._boundOnKeyup = (e) => {
949
+ // 键盘抬起事件
950
+ this.boundOnKeyup = (e) => {
635
951
  switch (e.code) {
636
952
  case "KeyW":
637
953
  case "ArrowUp":
@@ -667,192 +983,111 @@ var PlayerController = class {
667
983
  break;
668
984
  }
669
985
  };
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);
986
+ this.mouseMove = (e) => {
987
+ if (document.pointerLockElement === document.body) this.setToward(e.movementX, e.movementY, 1e-4);
701
988
  };
702
- this._raycaster.firstHitOnly = true;
703
- this._raycasterPersonToCam.firstHitOnly = true;
989
+ this.mouseClick = () => this.setPointerLock();
990
+ this.raycaster.firstHitOnly = true;
991
+ this.raycasterPersonToCam.firstHitOnly = true;
704
992
  }
705
- // ==================== 初始化相关方法 ====================
993
+ // ==================== 初始化 ====================
994
+ // 初始化控制器
706
995
  async init(opts, callback) {
996
+ const m = opts.playerModel;
997
+ const s = m.scale ?? 1;
707
998
  this.scene = opts.scene;
708
999
  this.camera = opts.camera;
709
1000
  this.camera.rotation.order = "YXZ";
710
1001
  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;
1002
+ this.playerModel = m;
1003
+ this.initPos = opts.initPos ? new THREE4.Vector3(opts.initPos.x, opts.initPos.y, opts.initPos.z) : new THREE4.Vector3(0, 0, 0);
1004
+ const pm = this.playerModel;
1005
+ this.gravity = (pm.gravity ?? this.gravity) * s;
1006
+ this.jumpHeight = (pm.jumpHeight ?? this.jumpHeight) * s;
1007
+ this.playerSpeed = (pm.speed ?? this.playerSpeed) * s;
1008
+ this.playerFlySpeed = (pm.flySpeed ?? this.playerFlySpeed) * s;
719
1009
  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;
1010
+ this.playerFlyEnabled = pm.flyEnabled ?? this.playerFlyEnabled;
1011
+ this.playerCapsuleRadiusRatio = pm.capsuleRadiusRatio ?? this.playerCapsuleRadiusRatio;
1012
+ this.mouseSensitivity = opts.mouseSensitivity ?? this.mouseSensitivity;
1013
+ this.thirdMouseMode = opts.thirdMouseMode ?? this.thirdMouseMode;
1014
+ this.enableZoom = opts.enableZoom ?? this.enableZoom;
1015
+ this.minCamDistance = (opts.minCamDistance ?? this.minCamDistance) * s;
1016
+ this.maxCamDistance = (opts.maxCamDistance ?? this.maxCamDistance) * s;
726
1017
  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();
1018
+ this.camEpsilon = this.camEpsilon * s;
1019
+ this.isShowMobileControls = (opts.isShowMobileControls ?? this.isShowMobileControls) && isMobileDevice();
1020
+ this.enableOverShoulderView = opts.enableOverShoulderView ?? this.enableOverShoulderView;
732
1021
  if (this.isShowMobileControls) {
733
- await this.initMobileControls();
1022
+ this.mobileControls = new MobileControls((i) => this.setInput(i), this.controls);
1023
+ await this.mobileControls.init();
734
1024
  }
735
1025
  await this.createBVH(opts.colliderMeshUrl);
736
1026
  await this.loadPersonGLB();
737
1027
  this.onAllEvent();
738
1028
  this.setCameraPos();
739
1029
  this.setControls();
740
- if (callback) callback();
741
- this.enableOverShoulderView = opts.enableOverShoulderView ?? false;
742
1030
  this.setOverShoulderView(this.enableOverShoulderView);
1031
+ callback?.();
743
1032
  }
1033
+ // 过肩视角切换
744
1034
  setOverShoulderView(enable) {
745
- if (!enable || this.controllerMode == 1) {
1035
+ if (!enable || this.controllerMode === 1) {
746
1036
  this.camera.clearViewOffset();
747
1037
  return;
748
1038
  }
749
1039
  const w = window.innerWidth;
750
1040
  const h = window.innerHeight;
751
- this.camera.setViewOffset(
752
- w,
753
- h,
754
- -w * -0.15,
755
- 0,
756
- w,
757
- h
758
- );
1041
+ this.camera.setViewOffset(w, h, -w * -0.15, 0, w, h);
759
1042
  }
1043
+ // 初始化加载器
760
1044
  async initLoader() {
761
1045
  const dracoLoader = new import_DRACOLoader.DRACOLoader();
762
- dracoLoader.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/gltf/");
763
- dracoLoader.setDecoderConfig({ type: "js" });
1046
+ dracoLoader.setDecoderPath("https://unpkg.com/three@0.182.0/examples/jsm/libs/draco/gltf/");
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,88 @@ 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) {
1445
+ const playerFwd = new THREE4.Vector3(0, 0, 1).applyQuaternion(this.player.quaternion);
1446
+ const flatDir = new THREE4.Vector3(playerFwd.x, 0, playerFwd.z).normalize();
1447
+ if (flatDir.lengthSq() > 1e-3) {
1448
+ const yAngle = Math.atan2(flatDir.x, flatDir.z);
1449
+ this.player.rotation.set(0, yAngle, 0);
1450
+ }
1466
1451
  this.setFirstPersonCamera();
1467
1452
  this.setOverShoulderView(false);
1468
1453
  } else {
1469
1454
  this.controls.enabled = true;
1470
1455
  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
- );
1456
+ const dir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1475
1457
  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);
1458
+ const s = this.playerModel.scale;
1459
+ this.camera.position.copy(this.player.position).add(new THREE4.Vector3(Math.cos(angle) * 400 * s, 200 * s, Math.sin(angle) * 400 * s));
1460
+ this.controls.target.copy(this.player.position);
1483
1461
  this.controls.enableZoom = this.enableZoom;
1484
1462
  this.setOverShoulderView(this.enableOverShoulderView);
1485
1463
  }
1486
1464
  this.setPointerLock();
1487
1465
  }
1488
- setFirstPersonCamera() {
1466
+ // 设置第一人称相机
1467
+ setFirstPersonCamera(vertAngle = 0) {
1489
1468
  this.controls.enabled = false;
1490
1469
  if (this.personHead) {
1491
- this.personHead?.attach(this.camera);
1470
+ this.personHead.attach(this.camera);
1492
1471
  this.camera.position.set(0, 10, 20);
1493
1472
  } else {
1494
1473
  this.player.attach(this.camera);
1495
- this.camera.position.set(
1496
- 0,
1497
- 40 * this.playerModel.scale,
1498
- 30 * this.playerModel.scale
1499
- );
1474
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1500
1475
  }
1501
- this.camera.rotation.set(0, Math.PI, 0);
1476
+ this.camera.rotation.set(
1477
+ THREE4.MathUtils.clamp(vertAngle, -1.1, 1.4),
1478
+ Math.PI,
1479
+ 0
1480
+ );
1502
1481
  this.controls.enableZoom = false;
1503
1482
  }
1483
+ // 设置鼠标锁定
1504
1484
  setPointerLock() {
1485
+ if (!document.body.requestPointerLock) return;
1505
1486
  if ((this.thirdMouseMode === 0 || this.thirdMouseMode === 1) && !this.isFirstPerson || this.isFirstPerson) {
1506
1487
  document.body.requestPointerLock();
1507
1488
  } else {
1508
1489
  document.exitPointerLock();
1509
1490
  }
1510
1491
  }
1492
+ // 初始相机位置
1511
1493
  setCameraPos() {
1512
1494
  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
- );
1495
+ if (!this.isFirstPerson) {
1496
+ const dir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1525
1497
  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);
1498
+ const s = this.playerModel.scale;
1499
+ this.camera.position.copy(this.player.position).add(new THREE4.Vector3(Math.cos(angle) * 400 * s, 200 * s, Math.sin(angle) * 400 * s));
1532
1500
  this.controls.enableZoom = this.enableZoom;
1501
+ } else {
1502
+ this.person.add(this.camera);
1503
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1533
1504
  }
1534
1505
  this.camera.updateProjectionMatrix();
1535
1506
  });
1536
1507
  }
1508
+ // 初始化轨道控制
1537
1509
  setControls() {
1538
1510
  this.controls.enableZoom = this.enableZoom;
1539
- this.controls.rotateSpeed = this.mouseSensity * 0.05;
1511
+ this.controls.rotateSpeed = this.mouseSensitivity * 0.05;
1540
1512
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
1541
1513
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
1542
1514
  }
1515
+ // 重置轨道控制
1543
1516
  resetControls() {
1544
1517
  if (!this.controls) return;
1545
1518
  this.controls.enabled = true;
@@ -1549,214 +1522,131 @@ var PlayerController = class {
1549
1522
  this.controls.enableZoom = true;
1550
1523
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
1551
1524
  }
1525
+ // 视角旋转处理
1552
1526
  setToward(dx, dy, speed) {
1553
- if (this.controllerMode == 0) {
1527
+ const sens = this.mouseSensitivity;
1528
+ if (this.controllerMode === 0) {
1554
1529
  if (this.isFirstPerson) {
1555
1530
  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
- );
1531
+ this.player.rotateY(-dx * speed * sens);
1532
+ this.camera.rotation.x = THREE4.MathUtils.clamp(this.camera.rotation.x + -dy * speed * sens, -1.1, 1.4);
1564
1533
  } 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);
1534
+ this.orbitCamera(this.player.position, -dx * speed * sens, -dy * speed * sens);
1585
1535
  }
1586
1536
  } else {
1587
1537
  const v = this.activeVehicle;
1588
1538
  if (!v) return;
1589
1539
  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
- );
1540
+ this.camera.rotation.y = THREE4.MathUtils.clamp(this.camera.rotation.y + -dx * speed * sens, Math.PI * (3 / 4), Math.PI * (5 / 4));
1541
+ this.camera.rotation.x = THREE4.MathUtils.clamp(this.camera.rotation.x + -dy * speed * sens, 0, Math.PI * (1 / 3));
1602
1542
  } 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);
1543
+ this.orbitCamera(v.vehicleGroup.position, -dx * speed * sens, -dy * speed * sens);
1623
1544
  }
1624
1545
  }
1625
1546
  }
1547
+ // 球面轨道旋转
1548
+ orbitCamera(target, deltaX, deltaY) {
1549
+ const distance = this.camera.position.distanceTo(target);
1550
+ const cur = this.camera.position.clone().sub(target);
1551
+ let theta = Math.atan2(cur.x, cur.z) + deltaX;
1552
+ let phi = Math.acos(THREE4.MathUtils.clamp(cur.y / distance, -1, 1)) + deltaY;
1553
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1554
+ this.camera.position.set(
1555
+ target.x + distance * Math.sin(phi) * Math.sin(theta),
1556
+ target.y + distance * Math.cos(phi),
1557
+ target.z + distance * Math.sin(phi) * Math.cos(theta)
1558
+ );
1559
+ this.camera.lookAt(target);
1560
+ }
1561
+ // ==================== 碰撞体构建 ====================
1562
+ // 补全几何属性
1563
+ ensureAttributesMinimal(geom) {
1564
+ if (!geom.attributes.position) return null;
1565
+ if (!geom.attributes.normal) geom.computeVertexNormals();
1566
+ if (!geom.attributes.uv) {
1567
+ const count = geom.attributes.position.count;
1568
+ geom.setAttribute("uv", new THREE4.BufferAttribute(new Float32Array(count * 2), 2));
1569
+ }
1570
+ return geom;
1571
+ }
1572
+ // 统一几何属性格式
1626
1573
  unifiedAttribute(collected) {
1627
1574
  const attrMap = /* @__PURE__ */ new Map();
1628
1575
  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
- }
1576
+ const required = /* @__PURE__ */ new Set(["position", "normal", "uv"]);
1577
+ for (const g of collected)
1578
+ for (const name of Object.keys(g.attributes))
1579
+ if (!required.has(name)) g.deleteAttribute(name);
1638
1580
  for (const g of collected) {
1639
1581
  for (const name of Object.keys(g.attributes)) {
1640
1582
  const attr = g.attributes[name];
1641
1583
  const ctor = attr.array.constructor;
1642
- const itemSize = attr.itemSize;
1643
- const normalized = attr.normalized;
1644
1584
  if (!attrMap.has(name)) {
1645
- attrMap.set(name, {
1646
- itemSize,
1647
- arrayCtor: ctor,
1648
- examples: 1,
1649
- normalized
1650
- });
1585
+ attrMap.set(name, { itemSize: attr.itemSize, arrayCtor: ctor, examples: 1, normalized: attr.normalized });
1651
1586
  } else {
1652
1587
  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
- }
1588
+ if (m.itemSize !== attr.itemSize || m.arrayCtor !== ctor || m.normalized !== attr.normalized) attrConflict.add(name);
1589
+ else m.examples++;
1658
1590
  }
1659
1591
  }
1660
1592
  }
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);
1593
+ for (const name of attrConflict) {
1594
+ for (const g of collected) if (g.attributes[name]) g.deleteAttribute(name);
1595
+ attrMap.delete(name);
1668
1596
  }
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) {
1597
+ for (const [name, meta] of attrMap) {
1598
+ for (const g of collected) {
1673
1599
  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
- );
1600
+ const count = g.attributes.position.count;
1601
+ g.setAttribute(name, new THREE4.BufferAttribute(new meta.arrayCtor(count * meta.itemSize), meta.itemSize, meta.normalized));
1685
1602
  }
1686
1603
  }
1687
1604
  }
1688
1605
  return collected;
1689
1606
  }
1607
+ // 构建静态 BVH
1690
1608
  async createBVH(meshUrl = "") {
1691
1609
  await this.initLoader();
1610
+ const collectMesh = (mesh) => {
1611
+ try {
1612
+ let geom = mesh.geometry.clone();
1613
+ geom.applyMatrix4(mesh.matrixWorld);
1614
+ if (geom.index) geom = geom.toNonIndexed();
1615
+ const safe = this.ensureAttributesMinimal(geom);
1616
+ if (safe) this.collected.push(safe);
1617
+ } catch (e) {
1618
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1619
+ }
1620
+ };
1692
1621
  if (meshUrl === "") {
1693
1622
  if (this.collider) {
1694
1623
  this.scene.remove(this.collider);
1695
1624
  this.collider = null;
1696
1625
  }
1697
1626
  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
- }
1627
+ const m = c;
1628
+ if (m?.isMesh && m.geometry && c.name !== "capsule") collectMesh(m);
1710
1629
  });
1711
- if (!this.collected.length) return;
1712
- this.collected = this.unifiedAttribute(this.collected);
1713
1630
  } else {
1714
1631
  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);
1632
+ const root = gltf.scene.children[0];
1633
+ if (root?.geometry) {
1634
+ collectMesh(root);
1723
1635
  } 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
- }
1636
+ root?.traverse((c) => {
1637
+ if (c?.isMesh && c.geometry && c.name !== "capsule") collectMesh(c);
1737
1638
  });
1738
- if (!this.collected.length) return;
1739
- this.collected = this.unifiedAttribute(this.collected);
1740
1639
  }
1741
1640
  }
1742
- const merged = BufferGeometryUtils.mergeGeometries(
1743
- this.collected,
1744
- false
1745
- );
1641
+ if (!this.collected.length) return;
1642
+ this.collected = this.unifiedAttribute(this.collected);
1643
+ const merged = BufferGeometryUtils.mergeGeometries(this.collected, false);
1746
1644
  if (!merged) {
1747
1645
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1748
1646
  return;
1749
1647
  }
1750
1648
  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
- );
1649
+ this.collider = new THREE4.Mesh(merged, new THREE4.MeshBasicMaterial({ opacity: 0.5, transparent: true, wireframe: true, depthTest: true }));
1760
1650
  if (this.displayCollider) this.scene.add(this.collider);
1761
1651
  if (this.displayVisualizer) {
1762
1652
  if (this.visualizer) this.scene.remove(this.visualizer);
@@ -1765,165 +1655,108 @@ var PlayerController = class {
1765
1655
  }
1766
1656
  this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
1767
1657
  }
1658
+ // 构建动态 BVH
1768
1659
  createDynamicBVH(objects = []) {
1769
1660
  if (this.dynamicCollider) {
1770
1661
  this.scene.remove(this.dynamicCollider);
1771
1662
  this.dynamicCollider = null;
1772
1663
  }
1773
1664
  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
- }
1665
+ objects.forEach((obj) => obj.traverse((c) => {
1666
+ const m = c;
1667
+ if (m?.isMesh && m.geometry && c.name !== "capsule") {
1668
+ try {
1669
+ let geom = m.geometry.clone();
1670
+ geom.applyMatrix4(m.matrixWorld);
1671
+ if (geom.index) geom = geom.toNonIndexed();
1672
+ const safe = this.ensureAttributesMinimal(geom);
1673
+ if (safe) this.dynamicCollected.push(safe);
1674
+ } catch (e) {
1675
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", m, e);
1787
1676
  }
1788
- });
1789
- });
1677
+ }
1678
+ }));
1790
1679
  if (!this.dynamicCollected.length) return;
1791
1680
  this.dynamicCollected = this.unifiedAttribute(this.dynamicCollected);
1792
- const merged = BufferGeometryUtils.mergeGeometries(
1793
- this.dynamicCollected,
1794
- false
1795
- );
1681
+ const merged = BufferGeometryUtils.mergeGeometries(this.dynamicCollected, false);
1796
1682
  if (!merged) {
1797
1683
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1798
1684
  return;
1799
1685
  }
1800
1686
  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
- );
1687
+ this.dynamicCollider = new THREE4.Mesh(merged, new THREE4.MeshBasicMaterial({ opacity: 0.5, transparent: true, wireframe: true, depthTest: true }));
1810
1688
  if (this.displayCollider) this.scene.add(this.dynamicCollider);
1811
1689
  }
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
- // ==================== 设置控制器过渡 ====================
1690
+ // ==================== 控制器过渡 ====================
1691
+ // 车辆切换过渡
1822
1692
  setControllerTransition() {
1823
1693
  if (this.isChangeControllerTransitionTimer) {
1824
1694
  clearTimeout(this.isChangeControllerTransitionTimer);
1825
1695
  this.isChangeControllerTransitionTimer = null;
1826
1696
  }
1827
- let vGroups = [];
1828
- for (const v of this.vehicles) {
1829
- vGroups.push(v.vehicleGroup);
1830
- }
1697
+ const vGroups = this.vehicles.map((v) => v.vehicleGroup);
1831
1698
  this.createDynamicBVH(vGroups);
1832
1699
  this.isChangeControllerTransitionTimer = setTimeout(() => {
1833
1700
  this.isChangeControllerTransitionTimer = null;
1834
- for (const v of this.vehicles) {
1835
- this.clearVehicleVelocity(v);
1836
- }
1701
+ this.vehicles.forEach((v) => this.clearVehicleVelocity(v));
1837
1702
  this.createDynamicBVH(vGroups);
1838
1703
  }, 3e3);
1839
1704
  }
1840
- // 清除车辆速度
1705
+ // 清零车辆速度
1841
1706
  clearVehicleVelocity(v) {
1842
1707
  if (!v || !this.world || !this.RAPIER) return;
1843
1708
  const { chassisBody, vehicleController } = v;
1844
1709
  const ZERO = new this.RAPIER.Vector3(0, 0, 0);
1845
1710
  chassisBody.setLinvel(ZERO, true);
1846
1711
  chassisBody.setAngvel(ZERO, true);
1847
- const BIG_BRAKE = 1e6;
1848
1712
  for (let i = 0; i < 4; i++) {
1849
1713
  vehicleController.setWheelEngineForce(i, 0);
1850
- vehicleController.setWheelBrake(i, BIG_BRAKE);
1714
+ vehicleController.setWheelBrake(i, 1e6);
1851
1715
  }
1852
1716
  vehicleController.updateVehicle(1 / 60);
1853
1717
  this.world.timestep = 1 / 60;
1854
1718
  this.world.step();
1855
1719
  chassisBody.setLinvel(ZERO, true);
1856
1720
  chassisBody.setAngvel(ZERO, true);
1857
- for (let i = 0; i < 4; i++) {
1858
- vehicleController.setWheelBrake(i, 0);
1859
- }
1721
+ for (let i = 0; i < 4; i++) vehicleController.setWheelBrake(i, 0);
1860
1722
  }
1861
1723
  // ==================== 循环更新 ====================
1724
+ // 主循环更新
1862
1725
  async update(delta = clock.getDelta()) {
1863
1726
  if (!this.isupdate || !this.player || !this.collider) return;
1864
1727
  delta = Math.min(delta, 1 / 40);
1865
- if (this.controllerMode == 1) {
1728
+ if (this.controllerMode === 1) {
1866
1729
  this.updateVehicle(delta);
1867
1730
  } else {
1868
1731
  this.updatePlayer(delta);
1869
- if (this.isChangeControllerTransitionTimer)
1870
- this.updateVehicleInertia(delta);
1732
+ if (this.isChangeControllerTransitionTimer) this.updateVehicleInertia(delta);
1871
1733
  }
1872
1734
  }
1873
- /**
1874
- * 更新当前驾驶的车辆
1875
- */
1735
+ // 车辆帧更新
1876
1736
  updateVehicle(delta) {
1877
1737
  const v = this.activeVehicle;
1878
1738
  if (!v || !this.world) return;
1879
1739
  const { vehicleController, chassisBody, vehicleGroup } = v;
1880
1740
  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);
1741
+ const quat = new THREE4.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
1742
+ const forward = new THREE4.Vector3(1, 0, 0).applyQuaternion(quat);
1888
1743
  const slopeAngle = Math.asin(forward.y);
1889
- let factor = 1;
1890
- if (slopeAngle < -0.05 && this.fwdPressed) factor = -Math.sin(slopeAngle) * 10;
1744
+ const factor = slopeAngle < -0.05 && this.fwdPressed ? -Math.sin(slopeAngle) * 10 : 1;
1891
1745
  const accelerateForce = this.vehicleParams.power.accelerateForce * v.speedMultiplier;
1892
1746
  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);
1747
+ const engineForce = (Number(this.fwdPressed) - Number(this.bkdPressed)) * accelerateForce * factor;
1748
+ for (let i = 0; i < 4; i++) vehicleController.setWheelEngineForce(i, engineForce);
1898
1749
  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);
1750
+ for (let i = 0; i < 4; i++) vehicleController.setWheelBrake(i, wheelBrake);
1903
1751
  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
- );
1752
+ const steerDir = Number(this.lftPressed) - Number(this.rgtPressed);
1753
+ const steerSpeed = steerDir === 0 ? this.vehicleParams.steering.steerReturnSpeed : this.vehicleParams.steering.steerSpeed;
1754
+ const steering = THREE4.MathUtils.lerp(currentSteering, this.vehicleParams.steering.maxSteerAngle * steerDir, 1 - Math.pow(1 - steerSpeed, delta));
1918
1755
  vehicleController.setWheelSteering(0, steering);
1919
1756
  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
- }
1757
+ const driftFriction = (this.rgtPressed || this.lftPressed) && this.shiftPressed ? 0.5 : 2;
1758
+ vehicleController.setWheelSideFrictionStiffness(2, driftFriction);
1759
+ vehicleController.setWheelSideFrictionStiffness(3, driftFriction);
1927
1760
  this.updateVehicleInertia(delta);
1928
1761
  if (!this.isFirstPerson) {
1929
1762
  const lookTarget = vehicleGroup.position.clone();
@@ -1932,99 +1765,49 @@ var PlayerController = class {
1932
1765
  this.camera.position.add(lookTarget);
1933
1766
  this.controls.update();
1934
1767
  const velocity = chassisBody.linvel();
1935
- const currentSpeed = Math.sqrt(
1936
- velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z
1937
- );
1768
+ const currentSpeed = new THREE4.Vector3(velocity.x, velocity.y, velocity.z).length();
1938
1769
  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);
1770
+ const baseDist = v.size.l * 0.8;
1771
+ const maxDist = v.size.l * 5;
1772
+ const desiredDist = THREE4.MathUtils.lerp(baseDist, maxDist, speedRatio);
1773
+ const minSafeDist = baseDist;
1774
+ this.personToCam.subVectors(this.camera.position, vehicleGroup.position);
1775
+ const direction = this.personToCam.clone().normalize();
1776
+ this.raycasterPersonToCam.set(vehicleGroup.position, direction);
1777
+ this.raycasterPersonToCam.far = desiredDist;
1778
+ const hits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1779
+ if (hits.length > 0) {
1780
+ const safeDist = Math.max(hits[0].distance - this.camEpsilon, minSafeDist);
1781
+ this.camera.position.lerp(vehicleGroup.position.clone().add(direction.clone().multiplyScalar(safeDist)), this.camCollisionLerp);
1967
1782
  } 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);
1783
+ this.raycasterPersonToCam.far = maxDist;
1784
+ const maxHits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1785
+ const safeDist = maxHits.length > 0 ? Math.min(desiredDist, maxHits[0].distance - this.camEpsilon) : desiredDist;
1786
+ this.camera.position.lerp(vehicleGroup.position.clone().add(direction.clone().multiplyScalar(safeDist)), this.camCollisionLerp);
1983
1787
  }
1984
1788
  if ((this.fwdPressed || this.bkdPressed) && this.vehicleParams.followVehicleDirection) {
1985
1789
  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
- );
1790
+ const velVec = new THREE4.Vector3(vel.x, vel.y, vel.z);
1791
+ if (velVec.length() > 0.3) {
1792
+ this.camBehindDir.lerp(velVec.normalize().negate(), this.camCollisionLerp).normalize();
1793
+ const targetCamPos = lookTarget.clone().add(this.camBehindDir.clone().multiplyScalar(desiredDist)).add(new THREE4.Vector3(0, v.size.h, 0));
1794
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
1999
1795
  this.controls.update();
2000
1796
  }
2001
1797
  }
2002
1798
  }
2003
1799
  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();
1800
+ if (vehicleUp.angleTo(this.upVector) > Math.PI / 2) {
1801
+ const size = new THREE4.Vector3();
2007
1802
  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
- );
1803
+ const t = chassisBody.translation();
1804
+ chassisBody.setTranslation(new this.RAPIER.Vector3(t.x, t.y + size.y, t.z), true);
1805
+ chassisBody.setRotation(new this.RAPIER.Quaternion(0, 0, 0, 1), true);
2021
1806
  chassisBody.setLinvel(new this.RAPIER.Vector3(0, 0, 0), true);
2022
1807
  chassisBody.setAngvel(new this.RAPIER.Vector3(0, 0, 0), true);
2023
1808
  }
2024
1809
  }
2025
- /**
2026
- * 更新所有车辆物理和位置
2027
- */
1810
+ // 物理步进 & 同步
2028
1811
  updateVehicleInertia(delta) {
2029
1812
  if (!this.world) return;
2030
1813
  this.world.timestep = delta;
@@ -2033,41 +1816,21 @@ var PlayerController = class {
2033
1816
  const { vehicleController, chassisBody, vehicleGroup, updateWheelVisuals } = v;
2034
1817
  vehicleController.updateVehicle(delta);
2035
1818
  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
- );
1819
+ const vel = chassisBody.linvel();
1820
+ const speed = new THREE4.Vector3(vel.x, vel.y, vel.z).length();
1821
+ const max = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
1822
+ if (speed > max) {
1823
+ const s = max / speed;
1824
+ chassisBody.setLinvel(new this.RAPIER.Vector3(vel.x * s, vel.y * s, vel.z * s), true);
2051
1825
  }
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();
1826
+ const t = chassisBody.translation();
1827
+ const r = chassisBody.rotation();
1828
+ vehicleGroup.position.set(t.x, t.y, t.z);
1829
+ vehicleGroup.quaternion.set(r.x, r.y, r.z, r.w);
1830
+ updateWheelVisuals?.();
2066
1831
  }
2067
1832
  }
2068
- /**
2069
- * 设置人物缩放
2070
- */
1833
+ // 缩放玩家比例
2071
1834
  setPlayerScale(newScale) {
2072
1835
  if (newScale <= 0) return;
2073
1836
  const ratio = newScale / this.playerModel.scale;
@@ -2077,7 +1840,7 @@ var PlayerController = class {
2077
1840
  this.playerSpeed *= ratio;
2078
1841
  this.playerFlySpeed *= ratio;
2079
1842
  this.curPlayerSpeed *= ratio;
2080
- this._camEpsilon *= ratio;
1843
+ this.camEpsilon *= ratio;
2081
1844
  this.minCamDistance *= ratio;
2082
1845
  this.maxCamDistance *= ratio;
2083
1846
  this.orginMaxCamDistance *= ratio;
@@ -2086,38 +1849,32 @@ var PlayerController = class {
2086
1849
  if (this.player?.capsuleInfo) this.player.capsuleInfo.radius *= ratio;
2087
1850
  if (this.isFirstPerson) this.setFirstPersonCamera();
2088
1851
  }
2089
- /**
2090
- * 更新人物
2091
- */
1852
+ // 玩家帧更新
2092
1853
  updatePlayer(delta) {
2093
- if (this.isMovingToBoardingPoint) {
2094
- this.updateMoveToBoardingPoint(delta);
2095
- }
2096
- if (!this.isFlying) {
2097
- this.player.position.addScaledVector(this.playerVelocity, delta);
2098
- }
1854
+ if (this.isMovingToBoardingPoint) this.updateMoveToBoardingPoint(delta);
1855
+ if (!this.isFlying) this.player.position.addScaledVector(this.playerVelocity, delta);
2099
1856
  if (this.isBoardingAnimPlaying) {
2100
1857
  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;
1858
+ if (action) {
1859
+ const duration = action.getClip().duration;
1860
+ const remaining = (duration - action.time) / action.getEffectiveTimeScale() * 1e3;
1861
+ if (!this.closeDoorTriggered && remaining <= 500) {
1862
+ this.closeDoorTriggered = true;
1863
+ this.openVehicleDoor(false);
1864
+ }
1865
+ if (action.time >= duration) {
1866
+ this.isBoardingAnimPlaying = false;
1867
+ this.closeDoorTriggered = false;
1868
+ this.onEnterCarAnimFinished();
1869
+ return;
1870
+ }
2113
1871
  }
2114
1872
  }
2115
1873
  if (this.isExitAnimPlaying) {
2116
1874
  const action = this.personActions?.get("exitCar");
2117
1875
  if (action) {
2118
1876
  const duration = action.getClip().duration;
2119
- const timeScale = action.getEffectiveTimeScale();
2120
- const remaining = (duration - action.time) / timeScale * 1e3;
1877
+ const remaining = (duration - action.time) / action.getEffectiveTimeScale() * 1e3;
2121
1878
  if (!this.closeExitDoorTriggered && remaining <= 500) {
2122
1879
  this.closeExitDoorTriggered = true;
2123
1880
  this.openVehicleDoor(false);
@@ -2131,71 +1888,47 @@ var PlayerController = class {
2131
1888
  this.updateMixers(delta);
2132
1889
  if (this.controllerMode === 1) return;
2133
1890
  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;
1891
+ const angle = 2 * Math.PI - (Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2);
2136
1892
  this.moveDir.set(0, 0, 0);
2137
1893
  if (this.fwdPressed) this.moveDir.add(this.DIR_FWD);
2138
1894
  if (this.bkdPressed) this.moveDir.add(this.DIR_BKD);
2139
1895
  if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
2140
1896
  if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
2141
1897
  if (this.isFlying) {
2142
- if (this.fwdPressed) this.moveDir.y = this.camDir.y;
2143
- else this.moveDir.y = 0;
1898
+ this.moveDir.y = this.fwdPressed ? this.camDir.y : 0;
2144
1899
  if (this.spacePressed) this.moveDir.add(this.DIR_UP);
2145
- }
2146
- if (this.isFlying && this.fwdPressed) {
2147
1900
  this.curPlayerSpeed = this.shiftPressed ? this.playerFlySpeed * 2 : this.playerFlySpeed;
2148
1901
  } else {
2149
1902
  this.curPlayerSpeed = this.shiftPressed ? this.playerSpeed * 2 : this.playerSpeed;
2150
1903
  }
2151
1904
  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) {
1905
+ this.player.position.addScaledVector(this.moveDir, this.curPlayerSpeed * delta);
1906
+ this.raycaster.ray.origin.copy(this.player.position);
1907
+ const hits = this.raycaster.intersectObject(this.collider, false);
1908
+ if (hits.length > 0 && !this.isFlying) {
1909
+ const dist = this.player.position.y - hits[0].point.y;
1910
+ const s = this.playerModel.scale;
1911
+ const maxH = this.playerCapsuleHeight * s * 0.9;
1912
+ const h = this.playerCapsuleHeight * s * 0.75;
1913
+ const minH = this.playerCapsuleHeight * s * 0.7;
1914
+ if (dist >= maxH) {
1915
+ this.playerVelocity.y += delta * this.gravity;
1916
+ this.player.position.addScaledVector(this.playerVelocity, delta);
1917
+ this.playerIsOnGround = false;
1918
+ } else if (dist >= h && dist < maxH) {
1919
+ if (!this.spacePressed) {
2191
1920
  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
1921
  this.playerIsOnGround = true;
1922
+ this.player.position.y = hits[0].point.y + h;
2198
1923
  }
1924
+ } else if (dist >= minH) {
1925
+ this.playerVelocity.set(0, 0, 0);
1926
+ this.playerIsOnGround = true;
1927
+ this.player.position.y = hits[0].point.y + h;
1928
+ } else {
1929
+ this.playerVelocity.set(0, 0, 0);
1930
+ this.player.position.y = hits[0].point.y + h;
1931
+ this.playerIsOnGround = true;
2199
1932
  }
2200
1933
  }
2201
1934
  this.player.updateMatrixWorld();
@@ -2205,113 +1938,49 @@ var PlayerController = class {
2205
1938
  this.tempSegment.copy(capsuleInfo.segment);
2206
1939
  this.tempSegment.start.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
2207
1940
  this.tempSegment.end.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
2208
- this.tempBox.expandByPoint(this.tempSegment.start);
2209
- this.tempBox.expandByPoint(this.tempSegment.end);
1941
+ this.tempBox.expandByPoint(this.tempSegment.start).expandByPoint(this.tempSegment.end);
2210
1942
  this.tempBox.expandByScalar(capsuleInfo.radius);
2211
1943
  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);
1944
+ const resolveCollision = (collider) => {
1945
+ if (!collider) return;
1946
+ collider.geometry?.boundsTree?.shapecast({
1947
+ intersectsBounds: (box) => box.intersectsBox(this.tempBox),
1948
+ intersectsTriangle: (tri) => {
1949
+ const distance = tri.closestPointToSegment(this.tempSegment, this.tempVector, this.tempVector2);
1950
+ if (distance < capsuleInfo.radius) {
1951
+ const normal = tri.getNormal(new THREE4.Vector3());
1952
+ if (normal.y > 0.5 && !this.isFlying) return;
1953
+ const dir = this.tempVector2.sub(this.tempVector).normalize();
1954
+ const depth = capsuleInfo.radius - distance;
1955
+ this.tempSegment.start.addScaledVector(dir, depth);
1956
+ this.tempSegment.end.addScaledVector(dir, depth);
1957
+ }
2253
1958
  }
2254
- }
2255
- });
1959
+ });
1960
+ };
1961
+ resolveCollision(this.collider);
1962
+ resolveCollision(this.dynamicCollider);
2256
1963
  }
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);
1964
+ const newPos = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
1965
+ const deltaVec = this.tempVector2.subVectors(newPos, this.player.position);
1966
+ const offset = Math.max(0, deltaVec.length() - 1e-5);
1967
+ this.player.position.add(deltaVec.normalize().multiplyScalar(offset));
1968
+ if (!this.isFirstPerson) {
1969
+ const camDirFlat = this.camDir.clone().setY(0).normalize().negate();
1970
+ const moveDirFlat = this.moveDir.clone().normalize().negate();
1971
+ if (!this.isFlying) {
1972
+ if (this.thirdMouseMode === 0 || this.thirdMouseMode === 2) {
1973
+ const lookTarget = this.player.position.clone().add(moveDirFlat.lengthSq() > 0 ? moveDirFlat : camDirFlat);
1974
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1975
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
1976
+ } else if (moveDirFlat.lengthSq() > 0) {
1977
+ this.targetMat.lookAt(this.player.position, this.player.position.clone().add(moveDirFlat), this.player.up);
1978
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
2277
1979
  }
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);
1980
+ } else {
1981
+ const lookTarget = this.player.position.clone().add(this.fwdPressed ? moveDirFlat : camDirFlat);
1982
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1983
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
2315
1984
  }
2316
1985
  }
2317
1986
  if (!this.isFirstPerson) {
@@ -2322,131 +1991,84 @@ var PlayerController = class {
2322
1991
  this.camera.position.add(lookTarget);
2323
1992
  this.controls.update();
2324
1993
  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
1994
+ this.updateCameraWithRaycast(
1995
+ this.player.position,
1996
+ this.personToCam.subVectors(this.camera.position, this.player.position).length(),
1997
+ this.maxCamDistance
2337
1998
  );
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
1999
  }
2367
2000
  }
2368
2001
  if (this.player.position.y < this.boundingBoxMinY - 1) {
2369
- this._originTmp.set(
2002
+ this.raycaster.ray.origin.set(this.player.position.x, 1e4, this.player.position.z);
2003
+ const fallHits = this.raycaster.intersectObject(this.collider, false);
2004
+ this.reset(new THREE4.Vector3(
2370
2005
  this.player.position.x,
2371
- 1e4,
2006
+ fallHits.length > 0 ? fallHits[0].point.y + 5 : this.player.position.y + 15,
2372
2007
  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
- }
2008
+ ));
2398
2009
  }
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);
2010
+ if (this.isShowMobileControls && this.vehicles.length) {
2011
+ let near = false;
2012
+ for (const v of this.vehicles) {
2013
+ this.nearCheckLocal.copy(v.boardingPoint).multiplyScalar(v.scale);
2014
+ v.vehicleGroup.localToWorld(this.nearCheckWorld.copy(this.nearCheckLocal));
2015
+ if (this.player.position.distanceTo(this.nearCheckWorld) < 800 * this.playerModel.scale) {
2016
+ near = true;
2017
+ break;
2416
2018
  }
2417
- } else {
2418
- this.isNearVehicle = false;
2419
- this.syncVehicleBtnEl(false);
2420
2019
  }
2020
+ if (near !== this.isNearVehicle) {
2021
+ this.isNearVehicle = near;
2022
+ this.mobileControls?.syncVehicleBtn(near);
2023
+ }
2024
+ }
2025
+ }
2026
+ // 相机碰撞射线
2027
+ updateCameraWithRaycast(origin, desiredDist, maxDist) {
2028
+ this.personToCam.subVectors(this.camera.position, origin);
2029
+ const direction = this.personToCam.clone().normalize();
2030
+ this.raycasterPersonToCam.set(origin, direction);
2031
+ this.raycasterPersonToCam.far = desiredDist;
2032
+ const hits = this.raycasterPersonToCam.intersectObject(this.collider, false);
2033
+ if (hits.length > 0) {
2034
+ const safeDist = Math.max(hits[0].distance - this.camEpsilon, this.minCamDistance);
2035
+ const targetCamPos = origin.clone().add(direction.multiplyScalar(safeDist));
2036
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
2037
+ } else {
2038
+ this.raycasterPersonToCam.far = maxDist;
2039
+ const maxHits = this.raycasterPersonToCam.intersectObject(this.collider, false);
2040
+ const safeDist = maxHits.length > 0 ? Math.min(maxDist, maxHits[0].distance - this.camEpsilon) : maxDist;
2041
+ const targetCamPos = origin.clone().add(direction.multiplyScalar(safeDist));
2042
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
2421
2043
  }
2422
2044
  }
2423
- /**
2424
- * 获取屏幕中心点向前射线与碰撞体的交点
2425
- */
2045
+ // 屏幕中心射线
2426
2046
  getCenterScreenRaycastHit() {
2427
2047
  this.camera.updateMatrixWorld();
2428
2048
  this.centerRay.setFromCamera(this.centerMouse, this.camera);
2429
- const intersects = this.centerRay.intersectObject(this.collider, false);
2430
- return intersects[0];
2049
+ return this.centerRay.intersectObject(this.collider, false)[0];
2050
+ }
2051
+ // 获取当前人物动画名称
2052
+ getCurrentPersonAnimationName() {
2053
+ return this.actionState._clip.name;
2431
2054
  }
2432
- /**
2433
- * 更新模型动画
2434
- */
2055
+ // 更新动画混合器
2435
2056
  updateMixers(delta) {
2436
- if (this.personMixer) this.personMixer.update(delta);
2437
- for (const v of this.vehicles) {
2438
- v.vehicleMixer?.update(delta);
2439
- }
2057
+ this.personMixer?.update(delta);
2058
+ for (const v of this.vehicles) v.vehicleMixer?.update(delta);
2440
2059
  }
2060
+ // 重置玩家位置
2441
2061
  reset(position) {
2442
2062
  if (!this.player) return;
2443
2063
  this.playerVelocity.set(0, 0, 0);
2444
2064
  this.player.position.copy(position ?? this.initPos);
2445
2065
  }
2066
+ // 获取玩家位置
2446
2067
  getPosition() {
2447
2068
  return this.player?.position;
2448
2069
  }
2449
2070
  // ==================== 输入处理 ====================
2071
+ // 外部输入接口
2450
2072
  setInput(input) {
2451
2073
  if (typeof input.moveX === "number") {
2452
2074
  this.lftPressed = input.moveX === -1;
@@ -2463,14 +2085,9 @@ var PlayerController = class {
2463
2085
  }
2464
2086
  if (typeof input.jump === "boolean") {
2465
2087
  if (input.jump) {
2466
- if (this.isMovingToBoardingPoint) {
2467
- this.isMovingToBoardingPoint = false;
2468
- this.boardingWaypoints = [];
2469
- this.currentWaypointIndex = 0;
2470
- this.boardingTargetDir = null;
2471
- }
2088
+ this.cancelBoarding();
2472
2089
  this.spacePressed = true;
2473
- if (this.controllerMode == 1) return;
2090
+ if (this.controllerMode === 1) return;
2474
2091
  if (!this.playerIsOnGround || this.isFlying) return;
2475
2092
  this.playPersonAnimationByName("jumping");
2476
2093
  this.playerVelocity.y = this.jumpHeight;
@@ -2479,344 +2096,91 @@ var PlayerController = class {
2479
2096
  this.spacePressed = false;
2480
2097
  }
2481
2098
  }
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) {
2099
+ if (typeof input.shift === "boolean") this.shiftPressed = input.shift;
2100
+ if (input.toggleView) this.changeView();
2101
+ if (input.toggleFly && this.playerFlyEnabled && this.controllerMode === 0) {
2489
2102
  this.isFlying = !this.isFlying;
2490
2103
  this.setAnimationByPressed();
2491
- if (!this.isFlying && !this.playerIsOnGround) {
2492
- this.playPersonAnimationByName("jumping");
2493
- }
2104
+ if (!this.isFlying && !this.playerIsOnGround) this.playPersonAnimationByName("jumping");
2494
2105
  }
2495
2106
  if (input.toggleVehicle) {
2496
- if (this.controllerMode == 0) {
2497
- this.enterVehicle();
2498
- } else {
2499
- this.exitVehicle();
2500
- }
2107
+ if (this.controllerMode === 0) this.enterVehicle();
2108
+ else this.exitVehicle();
2501
2109
  }
2502
2110
  }
2111
+ // 取消上车寻路
2112
+ cancelBoarding() {
2113
+ this.isMovingToBoardingPoint = false;
2114
+ this.boardingWaypoints = [];
2115
+ this.currentWaypointIndex = 0;
2116
+ this.boardingTargetDir = null;
2117
+ }
2118
+ // 注册所有事件
2503
2119
  onAllEvent() {
2504
2120
  this.isupdate = true;
2505
2121
  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);
2122
+ window.addEventListener("keydown", this.boundOnKeydown);
2123
+ window.addEventListener("keyup", this.boundOnKeyup);
2124
+ window.addEventListener("mousemove", this.mouseMove);
2125
+ window.addEventListener("click", this.mouseClick);
2510
2126
  }
2127
+ // 注销所有事件
2511
2128
  offAllEvent() {
2512
2129
  this.isupdate = false;
2513
2130
  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
- }
2131
+ window.removeEventListener("keydown", this.boundOnKeydown);
2132
+ window.removeEventListener("keyup", this.boundOnKeyup);
2133
+ window.removeEventListener("mousemove", this.mouseMove);
2134
+ window.removeEventListener("click", this.mouseClick);
2135
+ }
2136
+ // ==================== 移动端同步 ====================
2137
+ // 同步移动端按钮
2138
+ syncMobileControllerMode() {
2139
+ this.mobileControls?.syncControllerModeBtn(this.controllerMode);
2140
+ }
2141
+ // ==================== Setters ====================
2142
+ // 设置鼠标灵敏度
2143
+ setMouseSensitivity(value) {
2144
+ this.mouseSensitivity = value;
2145
+ this.controls.rotateSpeed = value * 0.05;
2146
+ }
2147
+ // 设置重力
2792
2148
  setGravity(gravity) {
2793
2149
  this.gravity = gravity * this.playerModel.scale;
2794
2150
  }
2151
+ // 设置跳跃高度
2795
2152
  setJumpHeight(jumpHeight) {
2796
2153
  this.jumpHeight = jumpHeight * this.playerModel.scale;
2797
2154
  }
2798
- setPlayerSpeed(playerSpeed) {
2799
- this.playerSpeed = playerSpeed * this.playerModel.scale;
2155
+ // 设置移动速度
2156
+ setPlayerSpeed(speed) {
2157
+ this.playerSpeed = speed * this.playerModel.scale;
2800
2158
  this.curPlayerSpeed = this.playerSpeed;
2801
2159
  }
2802
- setPlayerFlySpeed(playerFlySpeed) {
2803
- this.playerFlySpeed = playerFlySpeed * this.playerModel.scale;
2160
+ // 设置飞行速度
2161
+ setPlayerFlySpeed(flySpeed) {
2162
+ this.playerFlySpeed = flySpeed * this.playerModel.scale;
2804
2163
  }
2805
- setMinCamDistance(minCamDistance) {
2806
- this.minCamDistance = minCamDistance * this.playerModel.scale;
2164
+ // 设置最小相机距离
2165
+ setMinCamDistance(dist) {
2166
+ this.minCamDistance = dist * this.playerModel.scale;
2807
2167
  }
2808
- setMaxCamDistance(maxCamDistance) {
2809
- this.maxCamDistance = maxCamDistance * this.playerModel.scale;
2168
+ // 设置最大相机距离
2169
+ setMaxCamDistance(dist) {
2170
+ this.maxCamDistance = dist * this.playerModel.scale;
2810
2171
  this.orginMaxCamDistance = this.maxCamDistance;
2811
2172
  }
2812
- setThirdMouseMode(thirdMouseMode) {
2813
- this.thirdMouseMode = thirdMouseMode;
2173
+ // 设置鼠标模式
2174
+ setThirdMouseMode(mode) {
2175
+ this.thirdMouseMode = mode;
2814
2176
  this.setPointerLock();
2815
2177
  }
2816
- setEnableZoom(enableZoom) {
2817
- this.enableZoom = enableZoom;
2818
- this.controls.enableZoom = this.enableZoom;
2178
+ // 设置滚轮缩放
2179
+ setEnableZoom(enable) {
2180
+ this.enableZoom = enable;
2181
+ this.controls.enableZoom = enable;
2819
2182
  }
2183
+ // 设置调试显示
2820
2184
  setDebug(debug) {
2821
2185
  if (this.collider) this.scene.remove(this.collider);
2822
2186
  if (debug) {
@@ -2827,6 +2191,7 @@ var PlayerController = class {
2827
2191
  }
2828
2192
  }
2829
2193
  // ==================== 销毁 ====================
2194
+ // 销毁控制器
2830
2195
  destroy() {
2831
2196
  this.offAllEvent();
2832
2197
  if (this.player) {
@@ -2847,7 +2212,8 @@ var PlayerController = class {
2847
2212
  this.scene.remove(this.collider);
2848
2213
  this.collider = null;
2849
2214
  }
2850
- this.destroyMobileControls();
2215
+ this.mobileControls?.destroy();
2216
+ this.mobileControls = null;
2851
2217
  for (const v of this.vehicles) {
2852
2218
  this.scene.remove(v.vehicleGroup);
2853
2219
  v.pathPlanner?.dispose();
@@ -2861,31 +2227,34 @@ function playerController() {
2861
2227
  if (!controllerInstance) controllerInstance = new PlayerController();
2862
2228
  const c = controllerInstance;
2863
2229
  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),
2230
+ init: (opts, cb) => c.init(opts, cb),
2231
+ loadVehicleModel: (opts) => c.loadVehicleModel(opts),
2868
2232
  update: (dt) => c.update(dt),
2869
2233
  destroy: () => c.destroy(),
2234
+ reset: (pos) => c.reset(pos),
2870
2235
  setInput: (i) => c.setInput(i),
2236
+ changeView: () => c.changeView(),
2871
2237
  getPosition: () => c.getPosition(),
2872
2238
  getCenterScreenRaycastHit: () => c.getCenterScreenRaycastHit(),
2239
+ getCurrentPersonAnimationName: () => c.getCurrentPersonAnimationName(),
2873
2240
  getPerson: () => c.person,
2874
2241
  getActiveVehicle: () => c.activeVehicle,
2875
2242
  getAllVehicles: () => c.vehicles,
2876
2243
  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)
2244
+ setPlayerScale: (scale) => c.setPlayerScale(scale),
2245
+ setMouseSensitivity: (v) => c.setMouseSensitivity(v),
2246
+ setGravity: (v) => c.setGravity(v),
2247
+ setJumpHeight: (v) => c.setJumpHeight(v),
2248
+ setPlayerSpeed: (v) => c.setPlayerSpeed(v),
2249
+ setPlayerFlySpeed: (v) => c.setPlayerFlySpeed(v),
2250
+ setMinCamDistance: (v) => c.setMinCamDistance(v),
2251
+ setMaxCamDistance: (v) => c.setMaxCamDistance(v),
2252
+ setThirdMouseMode: (v) => c.setThirdMouseMode(v),
2253
+ setEnableZoom: (v) => c.setEnableZoom(v),
2254
+ setDebug: (v) => c.setDebug(v),
2255
+ setOverShoulderView: (v) => c.setOverShoulderView(v),
2256
+ registerAnimation: (key, clipName, opts) => c.registerAnimation(key, clipName, opts),
2257
+ playAnimation: (key, opts) => c.playAnimation(key, opts)
2889
2258
  };
2890
2259
  }
2891
2260
  function onAllEvent() {
@@ -2893,8 +2262,7 @@ function onAllEvent() {
2893
2262
  controllerInstance.onAllEvent();
2894
2263
  }
2895
2264
  function offAllEvent() {
2896
- if (!controllerInstance) return;
2897
- controllerInstance.offAllEvent();
2265
+ controllerInstance?.offAllEvent();
2898
2266
  }
2899
2267
  // Annotate the CommonJS export names for ESM import in node:
2900
2268
  0 && (module.exports = {