three-player-controller 0.3.6 → 0.3.8

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