three-player-controller 0.3.7 → 0.3.8

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