PageSourceSearch

https://fastidious-biscochitos-4a693f.netlify.app/game.js?v=weapon-shop-map

js fastidious-biscochitos-4a693f.netlify.app collected 2026-10-03 10:43:33 UTC 240,998 bytes, 6,416 lines download raw bytes

1import * as THREE from "three";
2
3const canvas = document.getElementById("game");
4const minimap = document.getElementById("minimap");
5const mapCtx = minimap.getContext("2d");
6const speedEl = document.getElementById("speed");
7const turboEl = document.getElementById("turbo");
8const killsEl = document.getElementById("kills");
9const timerEl = document.getElementById("timer");
10const moneyEl = document.getElementById("money");
11const healthEl = document.getElementById("health");
12const energyEl = document.getElementById("energy");
13const statusEl = document.getElementById("status");
14const focusEl = document.getElementById("focus");
15const menuCursorEl = document.getElementById("menuCursor");
16const menuEl = document.getElementById("menu");
17const menuTitleEl = document.getElementById("menuTitle");
18const settingsBtn = document.getElementById("settings");
19const settingsPanel = document.getElementById("settingsPanel");
20const settingsBackBtn = document.getElementById("settingsBack");
21const soundToggle = document.getElementById("soundToggle");
22const weaponShopEl = document.getElementById("weaponShop");
23const shopTitleEl = document.getElementById("shopTitle");
24const shopLevelEl = document.getElementById("shopLevel");
25const shopCostEl = document.getElementById("shopCost");
26const shopMoneyEl = document.getElementById("shopMoney");
27const shopMessageEl = document.getElementById("shopMessage");
28const shopBuyBtn = document.getElementById("shopBuy");
29const shopCloseBtn = document.getElementById("shopClose");
30const roundTimeSelect = document.getElementById("roundTime");
31const roundResultEl = document.getElementById("roundResult");
32const resultTitleEl = document.getElementById("resultTitle");
33const resultStatsEl = document.getElementById("resultStats");
34const resultRestartBtn = document.getElementById("resultRestart");
35const resultMenuBtn = document.getElementById("resultMenu");
36const deathScreenEl = document.getElementById("deathScreen");
37const deathReasonEl = document.getElementById("deathReason");
38const playBtn = document.getElementById("play");
39const namePanel = document.getElementById("namePanel");
40const nameLabel = document.getElementById("nameLabel");
41const nameWarning = document.getElementById("nameWarning");
42const playerNameInput = document.getElementById("playerName");
43const joinGameBtn = document.getElementById("joinGame");
44const nameBackBtn = document.getElementById("nameBack");
45const menuBackBtn = document.getElementById("menuBack");
46const restartBtn = document.getElementById("restart");
47const leftBtn = document.getElementById("left");
48const rightBtn = document.getElementById("right");
49const gasBtn = document.getElementById("gas");
50const brakeBtn = document.getElementById("brake");
51const runsFromWebsite = ["http:", "https:"].includes(window.location.protocol);
52let soundEnabled = true;
53
54const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, powerPreference: "high-performance" });
55renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.6));
56renderer.shadowMap.enabled = false;
57renderer.shadowMap.type = THREE.PCFSoftShadowMap;
58renderer.setClearColor(0x8cc9e8, 1);
59renderer.toneMapping = THREE.ACESFilmicToneMapping;
60renderer.toneMappingExposure = 1.05;
61
62const scene = new THREE.Scene();
63scene.background = new THREE.Color(0x8cc9e8);
64scene.fog = new THREE.Fog(0x8cc9e8, 90, 310);
65
66const camera = new THREE.PerspectiveCamera(62, 1, 0.1, 380);
67const clock = new THREE.Clock();
68const keys = new Set();
69const gameControlKeys = new Set([" ", "w", "a", "s", "d", "x", "arrowup", "arrowdown", "arrowleft", "arrowright", "shift"]);
70const held = { left: false, right: false, gas: false, brake: false };
71let audio;
72
73const text = {
74  htmlLang: "en",
75  title: "Open World Drive",
76  play: "Play",
77  settings: "Settings",
78  name: "Name",
79  warning: "Do not use inappropriate names. Max 8 characters.",
80  join: "Join",
81  back: "Back",
82  menu: "Menu",
83  restart: "Restart",
84  started: "Free drive started. Explore the city until the timer ends.",
85  status: "Free drive: enter a name, then explore the city.",
86};
87
88const buildings = [];
89const autoDoors = [];
90const parkingGates = [];
91const weaponBenches = [];
92const obstacles = [];
93const props = [];
94const pickups = [];
95const sparks = [];
96const shots = [];
97const zombies = [];
98const smokePuffs = [];
99const exhaustPorts = [];
100const streetLights = [];
101const trafficLights = [];
102const npcCars = [];
103const pedestrians = [];
104const policeCars = [];
105const policeOfficers = [];
106const militaryZones = [];
107const waterSurfaces = [];
108const wheels = [];
109const frontWheelParts = [];
110const brakeLights = [];
111const carHeadlights = [];
112let smokeTimer = 0;
113let zombieId = 0;
114let multiplayerSocket;
115let multiplayerId = null;
116let multiplayerName = "Player";
117let multiplayerSendTimer = 0;
118let activeWeaponBench = null;
119const remotePlayers = new Map();
120const worldSize = 240;
121const blockSize = 40;
122const roadWidth = 13;
123const twinCityOffset = worldSize * 2 + 120;
124const districtCenters = [0, twinCityOffset];
125const worldMinX = -worldSize - 15;
126const worldMaxX = districtCenters[districtCenters.length - 1] + worldSize + 15;
127const worldMinZ = -worldSize - 15;
128const worldMaxZ = worldSize + 15;
129const dayDuration = 5 * 60;
130const nightDuration = 0;
131const fullDayCycle = dayDuration + nightDuration;
132const defaultRoundDuration = 0;
133const maxZombies = 14;
134const baseZombieSpawnInterval = 6.5;
135const hospitalSpawn = new THREE.Vector3(-59, 0, 33);
136const militaryBase = { x: twinCityOffset + 118, z: -122, halfX: 78, halfZ: 62 };
137let sunDisc;
138let skyHaze;
139
140const world = {
141  started: false,
142  dayTimer: 0,
143  health: 100,
144  energy: 100,
145  turbo: 100,
146  turboHeld: false,
147  kills: 0,
148  money: 0,
149  wantedLevel: 0,
150  wantedTimer: 0,
151  policeAttackTimer: 0,
152  militaryAlertTimer: 0,
153  weaponLevel: 1,
154  roundDuration: defaultRoundDuration,
155  timeLeft: 0,
156  roundElapsed: 0,
157  zombieSpawnTimer: 0,
158  zombieSoundTimer: 0,
159  speed: 0,
160  rpm: 0.18,
161  gear: 1,
162  shiftTimer: 0,
163  heading: 0,
164  planeHeading: Math.PI / 2,
165  planeSpeed: 0,
166  planeLift: 0,
167  walkHeading: 0,
168  inCar: true,
169  inPlane: false,
170  cameraYaw: 0,
171  cameraPitch: 0.08,
172  mouseLookTimer: 0,
173  aiming: false,
174  jumpVelocity: 0,
175  grounded: true,
176  parachuting: false,
177  parachuteHeading: 0,
178  parachuteForwardSpeed: 0,
179  sinking: false,
180  sinkTimer: 0,
181  sinkStartY: 0.55,
182  over: false,
183  deathScreen: false,
184  shopOpen: false,
185  messageTimer: 0,
186};
187
188function applyText() {
189  document.documentElement.lang = text.htmlLang;
190  document.documentElement.dir = "ltr";
191  document.title = text.title;
192  if (menuTitleEl) menuTitleEl.textContent = text.title;
193  playBtn.textContent = text.play;
194  settingsBtn.textContent = text.settings;
195  nameLabel.textContent = text.name;
196  nameWarning.textContent = text.warning;
197  joinGameBtn.textContent = text.join;
198  nameBackBtn.textContent = text.back;
199  menuBackBtn.textContent = text.menu;
200  restartBtn.textContent = text.restart;
201  if (!world.started) statusEl.textContent = text.status;
202}
203
204const asphaltTexture = makeAsphaltTexture();
205const grassTexture = makeGrassTexture();
206const concreteTexture = makeConcreteTexture();
207
208const mats = {
209  asphalt: new THREE.MeshStandardMaterial({ map: asphaltTexture, color: 0x34383c, roughness: 0.96 }),
210  ground: new THREE.MeshStandardMaterial({ map: grassTexture, color: 0x5d9a5d, roughness: 0.98 }),
211  sidewalk: new THREE.MeshStandardMaterial({ map: concreteTexture, color: 0xb8b0a3, roughness: 0.86 }),
212  stripe: new THREE.MeshStandardMaterial({ color: 0xf7e37b, roughness: 0.55, emissive: 0x302500 }),
213  roadPaint: new THREE.MeshStandardMaterial({ color: 0xe9e7d5, roughness: 0.72 }),
214  fadedPaint: new THREE.MeshBasicMaterial({ color: 0xf0eddc, transparent: true, opacity: 0.58 }),
215  tireMark: new THREE.MeshBasicMaterial({ color: 0x080808, transparent: true, opacity: 0.26 }),
216  drain: new THREE.MeshStandardMaterial({ color: 0x2a2d2e, roughness: 0.65, metalness: 0.25 }),
217  roadPatch: new THREE.MeshBasicMaterial({ color: 0x151719, transparent: true, opacity: 0.32 }),
218  dirt: new THREE.MeshBasicMaterial({ color: 0x6d583c, transparent: true, opacity: 0.28 }),
219  curbStone: new THREE.MeshStandardMaterial({ color: 0xd4d0c2, roughness: 0.8 }),
220  bark: new THREE.MeshStandardMaterial({ color: 0x6b432c, roughness: 0.9 }),
221  leafDark: new THREE.MeshStandardMaterial({ color: 0x1f5f34, roughness: 0.88 }),
222  leafLight: new THREE.MeshStandardMaterial({ color: 0x3f8b49, roughness: 0.86 }),
223  grassBlade: new THREE.MeshBasicMaterial({ color: 0x72b95b, transparent: true, opacity: 0.72, side: THREE.DoubleSide }),
224  car: new THREE.MeshPhysicalMaterial({ color: 0x0f83c5, roughness: 0.22, metalness: 0.55, clearcoat: 0.65, clearcoatRoughness: 0.18 }),
225  carDark: new THREE.MeshStandardMaterial({ color: 0x111519, roughness: 0.72, metalness: 0.15 }),
226  rubber: new THREE.MeshStandardMaterial({ color: 0x08090a, roughness: 0.88 }),
227  chrome: new THREE.MeshStandardMaterial({ color: 0xd7dde2, roughness: 0.22, metalness: 0.8 }),
228  glass: new THREE.MeshPhysicalMaterial({ color: 0x8fd9ff, roughness: 0.08, metalness: 0.03, transmission: 0.18, transparent: true, opacity: 0.82, side: THREE.DoubleSide }),
229  marker: new THREE.MeshStandardMaterial({ color: 0x42d67d, roughness: 0.35, emissive: 0x0d5224 }),
230  coin: new THREE.MeshStandardMaterial({ color: 0xf3c84b, roughness: 0.3, metalness: 0.25, emissive: 0x443000 }),
231  cone: new THREE.MeshStandardMaterial({ color: 0xf0693f, roughness: 0.62, emissive: 0x240a00 }),
232  window: new THREE.MeshStandardMaterial({ color: 0xf2f6f5, roughness: 0.18, metalness: 0.08, emissive: 0x9fb7bd, emissiveIntensity: 0.22 }),
233  lamp: new THREE.MeshBasicMaterial({ color: 0xffefc0 }),
234  facadeTrim: new THREE.MeshStandardMaterial({ color: 0xd0c7b7, roughness: 0.72 }),
235  darkGlass: new THREE.MeshStandardMaterial({ color: 0x173040, roughness: 0.18, metalness: 0.12, emissive: 0x07141c }),
236  windowFrame: new THREE.MeshStandardMaterial({ color: 0x263239, roughness: 0.68 }),
237  trafficRed: new THREE.MeshBasicMaterial({ color: 0x3a0808 }),
238  trafficYellow: new THREE.MeshBasicMaterial({ color: 0x3a2b08 }),
239  trafficGreen: new THREE.MeshBasicMaterial({ color: 0x0a3414 }),
240  interiorWall: new THREE.MeshStandardMaterial({ color: 0x68645e, roughness: 0.86, side: THREE.DoubleSide }),
241  interiorFloor: new THREE.MeshStandardMaterial({ color: 0x3d3d3a, roughness: 0.82, side: THREE.DoubleSide }),
242  balconyFloor: new THREE.MeshStandardMaterial({ color: 0xbcb4a7, roughness: 0.78 }),
243  balconyRail: new THREE.MeshStandardMaterial({ color: 0x39454a, roughness: 0.52, metalness: 0.18 }),
244  bridgeDeck: new THREE.MeshStandardMaterial({ color: 0x555b5d, roughness: 0.82, metalness: 0.08 }),
245  bridgeRail: new THREE.MeshStandardMaterial({ color: 0xd4d8d2, roughness: 0.55, metalness: 0.18 }),
246  bridgeCable: new THREE.MeshStandardMaterial({ color: 0xb7c0bd, roughness: 0.42, metalness: 0.28 }),
247  water: new THREE.MeshPhysicalMaterial({ color: 0x0b77a5, roughness: 0.16, metalness: 0.02, clearcoat: 0.55, clearcoatRoughness: 0.14, transparent: true, op
247acity: 0.9 }),
248  waterDeep: new THREE.MeshBasicMaterial({ color: 0x073c5d, transparent: true, opacity: 0.28 }),
249  foam: new THREE.MeshBasicMaterial({ color: 0xe8fbff, transparent: true, opacity: 0.42 }),
250  sand: new THREE.MeshStandardMaterial({ color: 0xd7bd79, roughness: 0.92 }),
251  lounge: new THREE.MeshStandardMaterial({ color: 0xf0f5ed, roughness: 0.55 }),
252  loungeStripe: new THREE.MeshStandardMaterial({ color: 0x2e88b8, roughness: 0.52 }),
253  umbrellaRed: new THREE.MeshStandardMaterial({ color: 0xe85345, roughness: 0.58 }),
254  umbrellaBlue: new THREE.MeshStandardMaterial({ color: 0x3f8fd2, roughness: 0.58 }),
255  skin: new THREE.MeshStandardMaterial({ color: 0xc98b62, roughness: 0.72 }),
256  zombieSkin: new THREE.MeshStandardMaterial({ color: 0x7fa36b, roughness: 0.84 }),
257  zombieCloth: new THREE.MeshStandardMaterial({ color: 0x35443a, roughness: 0.9 }),
258  shirt: new THREE.MeshStandardMaterial({ color: 0x2d5bd1, roughness: 0.78 }),
259  pants: new THREE.MeshStandardMaterial({ color: 0x6f7378, roughness: 0.82 }),
260  shoe: new THREE.MeshStandardMaterial({ color: 0x111317, roughness: 0.82 }),
261  eye: new THREE.MeshBasicMaterial({ color: 0x101010 }),
262  eyebrow: new THREE.MeshBasicMaterial({ color: 0x1b120e }),
263  characterDetail: new THREE.MeshStandardMaterial({ color: 0xf0f0e8, roughness: 0.66 }),
264  gun: new THREE.MeshStandardMaterial({ color: 0x1a1d20, roughness: 0.42, metalness: 0.55 }),
265  tracer: new THREE.MeshBasicMaterial({ color: 0xfff2a4, transparent: true, opacity: 0.72 }),
266  muzzle: new THREE.MeshBasicMaterial({ color: 0xffdd66, transparent: true, opacity: 0.9 }),
267};
268
269const sun = new THREE.DirectionalLight(0xfff0d2, 2.8);
270sun.position.set(-35, 58, 28);
271sun.castShadow = false;
272sun.shadow.mapSize.set(1024, 1024);
273sun.shadow.camera.left = -140;
274sun.shadow.camera.right = 140;
275sun.shadow.camera.top = 140;
276sun.shadow.camera.bottom = -140;
277scene.add(sun);
278const hemisphereLight = new THREE.HemisphereLight(0xb7e6ff, 0x315c36, 1.35);
279scene.add(hemisphereLight);
280
281const city = new THREE.Group();
282scene.add(city);
283addSkyAndMountains();
284
285const car = createCar();
286scene.add(car);
287const plane = createLargePlane();
288scene.add(plane);
289const character = createCharacter();
290scene.add(character);
291const policeHelicopter = createPoliceHelicopter();
292policeHelicopter.visible = false;
293scene.add(policeHelicopter);
294
295buildWorld();
296applyText();
297reset();
298resize();
299requestAnimationFrame(loop);
300
301function makeNoiseTexture(base, fleck, flecks, size, repeat) {
302  const textureCanvas = document.createElement("canvas");
303  textureCanvas.width = size;
304  textureCanvas.height = size;
305  const ctx = textureCanvas.getContext("2d");
306  ctx.fillStyle = base;
307  ctx.fillRect(0, 0, size, size);
308  for (let i = 0; i < Math.floor(flecks * 0.42); i += 1) {
309    const alpha = 0.035 + Math.random() * 0.055;
310    ctx.fillStyle = hexWithAlpha(fleck, alpha);
311    const dot = 1.4 + Math.random() * 4.2;
312    ctx.fillRect(Math.random() * size, Math.random() * size, dot, dot);
313  }
314  const texture = new THREE.CanvasTexture(textureCanvas);
315  texture.wrapS = THREE.RepeatWrapping;
316  texture.wrapT = THREE.RepeatWrapping;
317  texture.repeat.set(repeat / size, repeat / size);
318  texture.colorSpace = THREE.SRGBColorSpace;
319  return texture;
320}
321
322function makeAsphaltTexture() {
323  const textureCanvas = document.createElement("canvas");
324  textureCanvas.width = 512;
325  textureCanvas.height = 512;
326  const ctx = textureCanvas.getContext("2d");
327  ctx.fillStyle = "#24282b";
328  ctx.fillRect(0, 0, 512, 512);
329  for (let i = 0; i < 720; i += 1) {
330    ctx.fillStyle = Math.random() < 0.5 ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.035)";
331    const s = 1.4 + Math.random() * 4.8;
332    ctx.fillRect(Math.random() * 512, Math.random() * 512, s, s);
333  }
334  ctx.lineCap = "round";
335  for (let i = 0; i < 42; i += 1) {
336    ctx.strokeStyle = `rgba(10, 12, 13, ${0.07 + Math.random() * 0.08})`;
337    ctx.lineWidth = 1.5 + Math.random() * 1.8;
338    ctx.beginPath();
339    let x = Math.random() * 512;
340    let y = Math.random() * 512;
341    ctx.moveTo(x, y);
342    for (let p = 0; p < 4; p += 1) {
343      x += (Math.random() - 0.5) * 45;
344      y += 12 + Math.random() * 40;
345      ctx.lineTo(x, y);
346    }
347    ctx.stroke();
348  }
349  for (let i = 0; i < 9; i += 1) {
350    ctx.strokeStyle = "rgba(0,0,0,0.055)";
351    ctx.lineWidth = 12 + Math.random() * 14;
352    const x = Math.random() * 512;
353    ctx.beginPath();
354    ctx.moveTo(x, 0);
355    ctx.lineTo(x + (Math.random() - 0.5) * 80, 512);
356    ctx.stroke();
357  }
358  const texture = new THREE.CanvasTexture(textureCanvas);
359  texture.wrapS = THREE.RepeatWrapping;
360  texture.wrapT = THREE.RepeatWrapping;
361  texture.repeat.set(18, 18);
362  texture.colorSpace = THREE.SRGBColorSpace;
363  return texture;
364}
365
366function makeGrassTexture() {
367  const texture = makeNoiseTexture("#2f713c", "#6fa866", 1900, 512, 2900);
368  return texture;
369}
370
371function makeConcreteTexture() {
372  const texture = makeNoiseTexture("#a8a194", "#d0c8bb", 1200, 512, 1100);
373  return texture;
374}
375
376function addSkyAndMountains() {
377  sunDisc = new THREE.Mesh(
378    new THREE.CircleGeometry(13, 36),
379    new THREE.MeshBasicMaterial({ color: 0xfff1b7, fog: false }),
380  );
381  sunDisc.position.set(-82, 96, -165);
382  sunDisc.lookAt(camera.position);
383  scene.add(sunDisc);
384
385  skyHaze = new THREE.Mesh(
386    new THREE.SphereGeometry(230, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2),
387    new THREE.MeshBasicMaterial({ color: 0xaeddf2, transparent: true, opacity: 0.32, side: THREE.BackSide, fog: false }),
388  );
389  skyHaze.position.y = -8;
390  scene.add(skyHaze);
391
392  for (let i = 0; i < 14; i += 1) {
393    const cloud = new THREE.Group();
394    const x = -150 + Math.random() * 300;
395    const z = -180 + Math.random() * 280;
396    const y = 56 + Math.random() * 28;
397    for (let p = 0; p < 4; p += 1) {
398      const puff = new THREE.Mesh(
399        new THREE.SphereGeometry(4 + Math.random() * 5, 10, 8),
400        new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.55, fog: false }),
401      );
402      puff.position.set((Math.random() - 0.5) * 12, (Math.random() - 0.5) * 3, (Math.random() - 0.5) * 5);
403      puff.scale.y = 0.35;
404      cloud.add(puff);
405    }
406    cloud.position.set(x, y, z);
407    scene.add(cloud);
408  }
409}
410
411function createMountainRidge(width, height, depth, material) {
412  const group = new THREE.Group();
413  const points = 7;
414  const verts = [];
415  const indices = [];
416  for (let i = 0; i < points; i += 1) {
417    const t = i / (points - 1);
418    const x = (t - 0.5) * width;
419    const peak = height * (0.4 + Math.random() * 0.75) * Math.sin(Math.PI * t) + 8;
420    const baseJitter = (Math.random() - 0.5) * 4;
421    verts.push(x, 0, -depth / 2 + baseJitter, x, 0, depth / 2 + baseJitter, x + (Math.random() - 0.5) * 5, peak, 0);
422  }
423  for (let i = 0; i < points - 1; i += 1) {
424    const a = i * 3;
425    const b = a + 3;
426    indices.push(a, b, a + 2, b, b + 2, a + 2, a + 1, a + 2, b + 1, b + 1, a + 2, b + 2, a, a + 1, b, b, a + 1, b + 1);
427  }
428  const geometry = new THREE.BufferGeometry();
429  geometry.setAttribute("position", new THREE.Float32BufferAttribute(verts, 3));
430  geometry.setIndex(indices);
431  geometry.computeVertexNormals();
432  const ridge = new THREE.Mesh(geometry, material);
433  ridge.castShadow = false;
434  ridge.receiveShadow = true;
435  group.add(ridge);
436
437  const snowMat = new THREE.MeshBasicMaterial({ color: 0xe7f0ed, transparent: true, opacity: 0.55, fog: true });
438  for (let i = 1; i < points - 1; i += 2) {
439    const cap = new THREE.Mesh(new THREE.ConeGeometry(4 + Math.random() * 4, 5 + Math.random() * 5, 3), snowMat);
440    const x = ((i / (points - 1)) - 0.5) * width;
441    cap.position.set(x, height * (0.52 + Math.random() * 0.25), 0);
442    cap.rotation.y = Math.random() * Math.PI;
443    cap.scale.z = 0.5;
444    group.add(cap);
445  }
446  return group;
447}
448
449function hexWithAlpha(hex, alpha) {
450  const color = new THREE.Color(hex);
451  return `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${alpha})`;
452}
453
454function buildWorld() {
455  addOuterOcean();
456  addSeaBetweenDistricts();
457  for (const centerX of districtCenters) {
458    addBeachRing(centerX);
459    const ground = new THREE.Mesh(new THREE.PlaneGeometry(worldSize * 2.03, worldSize * 2.03), mats.ground);
460    ground.rotation.x = -Math.PI / 2;
461    ground.position.x = centerX;
462    ground.receiveShadow = true;
463    city.add(ground);
464  }
465  for (const centerX of districtCenters) withSeededRandom(7241, () => addDistrict(centerX));
466  addConnectorBridges();
467  addMilitaryBase(militaryBase.x, militaryBase.z);
468  withSeededRandom(3917, addTrafficActors);
469
470  // Keep the city clean for free driving: no arcade rings, coins, or cone hazards.
471}
472
473function addOuterOcean() {
474  const oceanWidth = worldMaxX - worldMinX + 220;
475  const oceanDepth = worldSize * 2.9;
476  const ocean = new THREE.Mesh(new THREE.PlaneGeometry(oceanWidth, oceanDepth, 32, 24), mats.water);
477  ocean.rotation.x = -Math.PI / 2;
478  ocean.position.set((worldMinX + worldMaxX) / 2, -1.72, 0);
479  ocean.userData.waveSeed = 9.5;
480  waterSurfaces.push(ocean);
481  city.add(ocean);
482}
483
484function addBeachRing(centerX) {
485  const inner = worldSize * 2.03;
486  const outer = worldSize * 2.34;
487  const strip = (outer - inner) / 2;
488  const y = 0.015;
489  const north = new THREE.Mesh(new THREE.PlaneGeometry(outer, strip), mats.sand);
490  north.rotation.x = -Math.PI / 2;
491  north.position.set(centerX, y, inner / 2 + strip / 2);
492  city.add(north);
493
494  const south = north.clone();
495  south.position.z = -inner / 2 - strip / 2;
496  city.add(south);
497
498  const east = new THREE.Mesh(new THREE.PlaneGeometry(strip, inner), mats.sand);
499  east.rotation.x = -Math.PI / 2;
500  east.position.set(centerX + inner / 2 + strip / 2, y, 0);
501  city.add(east);
502
503  const west = east.clone();
504  west.position.x = centerX - inner / 2 - strip / 2;
505  city.add(west);
506
507  addBeachLoungers(centerX, inner / 2 + strip * 0.45, -1);
508  addBeachLoungers(centerX, -inner / 2 - strip * 0.45, 1);
509}
510
511function addBeachLoungers(centerX, z, facing) {
512  const count = 9;
513  const spacing = 26;
514  for (let i = 0; i < count; i += 1) {
515    const x = centerX + (i - (count - 1) / 2) * spacing;
516    addLoungeChair(x, z, facing);
517    if (i % 2 === 0) addBeachUmbrella(x + 4.4, z + facing * 1.6, i);
518  }
519}
520
521function addLoungeChair(x, z, facing) {
522  const chair = new THREE.Group();
523  const seat = new THREE.Mesh(new THREE.BoxGeometry(2.7, 0.12, 4.2), mats.lounge);
524  seat.position.y = 0.18;
525  chair.add(seat);
526
527  for (const sx of [-0.72, 0.72]) {
528    const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.13, 4.24), mats.loungeStripe);
529    stripe.position.set(sx, 0.255, 0);
530    chair.add(stripe);
531  }
532
533  const back = new THREE.Mesh(new THREE.BoxGeometry(2.7, 0.12, 1.7), mats.lounge);
534  back.position.set(0, 0.62, -1.75);
535  back.rotation.x = -0.58;
536  chair.add(back);
537
538  for (const sx of [-1.1, 1.1]) {
539    for (const sz of [-1.55, 1.55]) {
540      const leg = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.35, 0.13), mats.chrome);
541      leg.position.set(sx, -0.02, sz);
542      chair.add(leg);
543    }
544  }
545
546  chair.position.set(x, 0.23, z);
547  chair.rotation.y = facing > 0 ? 0 : Math.PI;
548  city.add(chair);
549  props.push({ mesh: chair, radius: 2.1, fixed: true });
550}
551
552function addBeachUmbrella(x, z, index) {
553  const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.09, 2.4, 8), mats.chrome);
554  pole.position.set(x, 1.2, z);
555  city.add(pole);
556
557  const canopy = new THREE.Mesh(new THREE.ConeGeometry(1.75, 0.75, 14), index % 4 === 0 ? mats.umbrellaRed : mats.umbrellaBlue);
558  canopy.position.set(x, 2.55, z);
559  canopy.rotation.y = index * 0.55;
560  city.add(canopy);
561
562  const rim = new THREE.Mesh(new THREE.TorusGeometry(1.26, 0.045, 6, 18), mats.lounge);
563  rim.position.set(x, 2.18, z);
564  rim.rotation.x = Math.PI / 2;
565  city.add(rim);
566  props.push({ mesh: pole, radius: 1.35, fixed: true });
567}
568
569function addDistrict(centerX) {
570  for (let x = -worldSize; x <= worldSize; x += blockSize) {
571    if (isMilitaryRoad(centerX + x, 0, false)) continue;
572    addRoad(centerX + x, 0, roadWidth, worldSize * 2 + roadWidth, false);
573  }
574  for (let z = -worldSize; z <= worldSize; z += blockSize) {
575    if (isMilitaryRoad(centerX, z, true)) continue;
576    addRoad(centerX, z, worldSize * 2 + roadWidth, roadWidth, true);
577  }
578  addRoadSurfaceDetails(centerX);
579  for (let x = -worldSize + blockSize / 2; x < worldSize; x += blockSize) {
580    for (let z = -worldSize + blockSize / 2; z < worldSize; z += blockSize) {
581      if (centerX === 0 && Math.abs(x) < 34 && Math.abs(z) < 34) continue;
582      if (centerX === 0 && x === 60 && z === 20) {
583        Math.random();
584        addParkingLot(centerX + x, z);
585      } else if (centerX === 0 && x === -60 && z === 20) {
586        addBuilding(centerX + x, z, { hospital: true });
587      } else if (centerX !== 0 && x >= 40 && z <= -60) {
588        Math.random();
589      } else if (centerX !== 0 && x === 20 && z === -20) {
590        addWeaponShop(centerX + x, z);
591      } else if (Math.random() < 0.18) {
592        addPark(centerX + x, z);
593      } else {
594        addBuilding(centerX + x, z);
595      }
596    }
597  }
598}
599
600function isMilitaryRoad(x, z, horizontal) {
601  const extra = 11;
602  if (horizontal) {
603    return Math.abs(z - militaryBase.z) < militaryBase.halfZ + extra && Math.abs(x - militaryBase.x) < militaryBase.halfX + worldSize * 0.72;
604  }
605  return Math.abs(x - militaryBase.x) < militaryBase.halfX + extra && Math.abs(z - militaryBase.z) < militaryBase.halfZ + worldSize * 0.72;
606}
607
608function addSeaBetweenDistricts() {
609  for (let i = 0; i < districtCenters.length - 1; i += 1) {
610    const startX = districtCenters[i] + worldSize - 8;
611    const endX = districtCenters[i + 1] - worldSize + 8;
612    const width = endX - startX;
613    const centerX = (startX + endX) / 2;
614    const sea = new THREE.Mesh(new THREE.PlaneGeometry(width, worldSize * 2.18, 18, 22), mats.water);
615    sea.rotation.x = -Math.PI / 2;
616    sea.position.set(centerX, -1.65, 0);
617    sea.userData.waveSeed = i * 3.7;
618    waterSurfaces.push(sea);
619    city.add(sea);
620
621    const deep = new THREE.Mesh(new THREE.PlaneGeometry(width * 0.72, worldSize * 1.55), mats.waterDeep);
622    deep.rotation.x = -Math.PI / 2;
623    deep.position.set(centerX, -1.62, 0);
624    city.add(deep);
625
626    for (const shoreX of [startX + 1.5, endX - 1.5]) {
627      const foam = new THREE.Mesh(new THREE.PlaneGeometry(2.8, worldSize * 2.05), mats.foam);
628      foam.rotation.x = -Math.PI / 2;
629      foam.position.set(shoreX + (shoreX < centerX ? 2.2 : -2.2), -1.58, 0);
630      city.add(foam);
631    }
632
633  }
634}
635
636function withSeededRandom(seed, build) {
637  const originalRandom = Math.random;
638  let value = seed >>> 0;
639  Math.random = () => {
640    value = (value * 1664525 + 1013904223) >>> 0;
641    return value / 4294967296;
642  };
643  try {
644    build();
645  } finally {
646    Math.random = originalRandom;
647  }
648}
649
650function addRoad(x, z, width, depth, horizontal, showMarkings = true) {
651  const road = new THREE.Mesh(new THREE.BoxGeometry(width, 0.08, depth), mats.asphalt);
652  road.position.set(x, 0.02, z);
653  road.receiveShadow = true;
654  city.add(road);
655
656  if (showMarkings) addSidewalkSegments(x, z, width, depth, horizontal);
657
658  if (showMarkings) {
659    addRoadCenterLines(x, z, width, depth, horizontal);
660    addStreetLightRows(x, z, width, depth, horizontal);
661  }
662}
663
664function addSidewalkSegments(x, z, width, depth, horizontal) {
665  const segmentLength = Math.max(8, blockSize - roadWidth - 4);
666  for (let offset = -worldSize + blockSize / 2; offset < worldSize; offset += blockSize) {
667    for (const side of [-1, 1]) {
668      const sidewalk = new THREE.Mesh(
669        new THREE.BoxGeometry(horizontal ? segmentLength : 3.4, 0.18, horizontal ? 3.4 : segmentLength),
670        mats.sidewalk,
671      );
672      sidewalk.position.set(
673        horizontal ? x + offset : x + side * (width / 2 + 1.7),
674        0.16,
675        horizontal ? z + side * (depth / 2 + 1.7) : z + offset,
676      );
677      sidewalk.receiveShadow = true;
678      city.add(sidewalk);
679
680      const curbStone = new THREE.Mesh(
681        new THREE.BoxGeometry(horizontal ? segmentLength : 0.28, 0.28, horizontal ? 0.28 : segmentLength),
682        mats.curbStone,
683      );
684      curbStone.position.set(
685        horizontal ? x + offset : x + side * (width / 2 + 0.15),
686        0.25,
687        horizontal ? z + side * (depth / 2 + 0.15) : z + offset,
688      );
689      curbStone.castShadow = true;
690      curbStone.receiveShadow = true;
691      city.add(curbStone);
692
693      const seam = new THREE.Mesh(
694        new THREE.BoxGeometry(horizontal ? 0.035 : 2.8, 0.02, horizontal ? 2.8 : 0.035),
695        mats.windowFrame,
696      );
697      seam.position.set(
698        horizontal ? x + offset : x + side * (width / 2 + 1.7),
699        0.265,
700        horizontal ? z + side * (depth / 2 + 1.7) : z + offset,
701      );
702      city.add(seam);
703    }
704  }
705}
706
707function addRoadCenterLines(x, z, width, depth, horizontal) {
708  const span = horizontal ? width : depth;
709  const dashLength = 7.5;
710  const gap = 16;
711  const start = -span / 2 + gap;
712  const end = span / 2 - gap;
713  for (let along = start; along <= end; along += dashLength + gap) {
714    const line = new THREE.Mesh(
715      new THREE.BoxGeometry(horizontal ? dashLength : 0.14, 0.035, horizontal ? 0.14 : dashLength),
716      mats.stripe,
717    );
718    line.position.set(
719      horizontal ? x + along + dashLength / 2 : x,
720      0.17,
721      horizontal ? z : z + along + dashLength / 2,
722    );
723    city.add(line);
724  }
725}
726
727function addStreetLightRows(x, z, width, depth, horizontal) {
728  if (!horizontal) return;
729  const roadIndex = Math.round((horizontal ? z : x) / blockSize);
730  if (Math.abs(roadIndex) % 2 === 1) return;
731  const span = horizontal ? width : depth;
732  const count = Math.floor(span / 260);
733  const edgeOffset = roadWidth / 2 + 8.2;
734  for (let i = -count; i <= count; i += 1) {
735    const along = i * 260 + 70;
736    for (const side of [-1, 1]) {
737      addStreetLight(
738        horizontal ? x + along : x + side * edgeOffset,
739        horizontal ? z + side * edgeOffset : z + along,
740        horizontal ? (side > 0 ? Math.PI : 0) : (side > 0 ? -Math.PI / 2 : Math.PI / 2),
741      );
742    }
743  }
744}
745
746function addRoadSurfaceDetails(centerX = 0) {
747  const xRoadCoords = [];
748  const zRoadCoords = [];
749  for (let v = -worldSize; v <= worldSize; v += blockSize) {
750    xRoadCoords.push(centerX + v);
751    zRoadCoords.push(v);
752  }
753
754  for (let xi = 0; xi < xRoadCoords.length; xi += 1) {
755    for (let zi = 0; zi < zRoadCoords.length; zi += 1) {
756      if ((xi + zi) % 3 !== 0) continue;
757      const x = xRoadCoords[xi];
758      const z = zRoadCoords[zi];
759      addCrosswalk(x, z, true);
760      addCrosswalk(x, z, false);
761      if (xi % 3 === 0 && zi % 3 === 0) addTrafficLightSet(x, z);
762      if (Math.random() < 0.18) addDrain(x + roadWidth * 0.38, z + roadWidth * 0.38);
763    }
764  }
765
766  for (let i = 0; i < 14; i += 1) {
767    const onVertical = Math.random() < 0.5;
768    const road = onVertical
769      ? xRoadCoords[Math.floor(Math.random() * xRoadCoords.length)]
770      : zRoadCoords[Math.floor(Math.random() * zRoadCoords.length)];
771    const along = -worldSize + Math.random() * worldSize * 2;
772    addTireMark(onVertical ? road + (Math.random() - 0.5) * 4 : centerX + along, onVertical ? along : road + (Math.random() - 0.5) * 4, onVertical);
773  }
774
775  for (let i = 0; i < 12; i += 1) {
776    const onVertical = Math.random() < 0.5;
777    const road = onVertical
778      ? xRoadCoords[Math.floor(Math.random() * xRoadCoords.length)]
779      : zRoadCoords[Math.floor(Math.random() * zRoadCoords.length)];
780    const along = -worldSize + Math.random() * worldSize * 2;
781    addRoadPatch(onVertical ? road + (Math.random() - 0.5) * 5 : centerX + along, onVertical ? along : road + (Math.random() - 0.5) * 5, onVertical);
782  }
783
784  for (let i = 0; i < 24; i += 1) {
785    addDirtPatch(centerX - worldSize + Math.random() * worldSize * 2, -worldSize + Math.random() * worldSize * 2, centerX);
786  }
787
788  for (let i = 0; i < 7; i += 1) {
789    const horizontal = i % 2 === 0;
790    const road = horizontal
791      ? zRoadCoords[(i * 2 + 3) % zRoadCoords.length]
792      : xRoadCoords[(i * 3 + 2) % xRoadCoords.length];
793    const along = -worldSize + 36 + ((i * 67) % (worldSize * 2 - 72));
794    const x = horizontal ? centerX + along : road + (i % 2 ? 1 : -1) * (roadWidth / 2 + 22);
795    const z = horizontal ? road + (i % 2 ? 1 : -1) * (roadWidth / 2 + 22) : along;
796    if (!isInMilitaryBase(x, z, 24)) addBusStop(x, z, horizontal ? 0 : Math.PI / 2);
797  }
798
799  for (let i = 0; i < 5; i += 1) {
800    const horizontal = i % 2 === 0;
801    const road = horizontal
802      ? zRoadCoords[(i * 3 + 1) % zRoadCoords.length]
803      : xRoadCoords[(i * 2 + 4) % xRoadCoords.length];
804    const along = -worldSize + 44 + ((i * 91) % (worldSize * 2 - 88));
805    const side = i % 3 === 0 ? -1 : 1;
806    const x = horizontal ? centerX + along : road + side * (roadWidth / 2 + 18);
807    const z = horizontal ? road + side * (roadWidth / 2 + 18) : along;
808    if (!isInMilitaryBase(x, z, 34)) addBillboard(x, z, i);
809  }
810}
811
812function addShopfrontDetails(x, z, w, d) {
813  const names = ["24/7 MART", "AUTO PARTS", "BURGER", "COFFEE", "PHONES", "MARKET", "LAUNDRY", "ARCADE"];
814  const name = names[Math.floor(Math.random() * names.length)];
815  const sign = new THREE.Mesh(
816    new THREE.PlaneGeometry(Math.min(w * 0.78, 7.8), 1.05),
817    new THREE.MeshBasicMaterial({ map: makeStreetSignTexture(name, 0x15191d, 0xf6f0e3), transparent: true }),
818  );
819  sign.position.set(x, 2.08, z + d / 2 + 0.43);
820  city.add(sign);
821
822  const awningMat = new THREE.MeshStandardMaterial({ color: Math.random() < 0.5 ? 0xd84534 : 0x2e88b8, roughness: 0.58 });
823  const awning = new THREE.Mesh(new THREE.BoxGeometry(Math.min(w * 0.82, 8.2), 0.12, 0.86), awningMat);
824  awning.position.set(x, 1.44, z + d / 2 + 0.72);
825  awning.castShadow = true;
826  city.add(awning);
827
828  for (const sx of [-1, 1]) {
829    const bin = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.78, 0.5), sx < 0 ? mats.windowFrame : mats.cone);
830    bin.position.set(x + sx * Math.min(w * 0.34, 3.2), 0.46, z + d / 2 + 0.9);
831    city.add(bin);
832    props.push({ mesh: bin, radius: 0.45, fixed: false });
833  }
834}
835
836function addBuildingStyleDetails(x, z, w, d, h, style) {
837  if (style === "glass-office") {
838    for (let y = 5; y < h - 2; y += 5.2) {
839      const belt = new THREE.Mesh(new THREE.BoxGeometry(w * 0.92, 0.16, 0.08), mats.chrome);
840      belt.position.set(x, y, z + d * 0.43);
841      city.add(belt);
842      const beltBack = belt.clone();
843      beltBack.position.z = z - d * 0.43;
844      city.add(beltBack);
845    }
846    const crown = new THREE.Mesh(new THREE.BoxGeometry(w * 0.62, 2.4, d * 0.62), mats.darkGlass);
847    crown.position.set(x, h + 1.35, z);
848    city.add(crown);
849    return;
850  }
851
852  if (style === "stepped-tower") {
853    for (let level = 0; level < 3; level += 1) {
854      const scale = 0.78 - level * 0.16;
855      const tier = new THREE.Mesh(createBuildingBodyGeometry(w * scale, 7 + level * 1.2, d * scale, "glass-office"), mats.windowFrame);
856      tier.position.set(x, h + 3.5 + level * 4.2, z);
857      city.add(tier);
858    }
859    return;
860  }
861
862  if (style === "brick-flat") {
863    for (let y = 3.2; y < h - 1; y += 3.1) {
864      const ledge = new THREE.Mesh(new THREE.BoxGeometry(w * 0.95, 0.1, 0.2), mats.curbStone);
865      ledge.position.set(x, y, z + d * 0.49);
866      city.add(ledge);
867    }
868    const waterTank = new THREE.Mesh(new THREE.CylinderGeometry(0.65, 0.75, 1.8, 12), mats.bark);
869    waterTank.position.set(x + w * 0.24, h + 1.25, z - d * 0.18);
870    city.add(waterTank);
871    return;
872  }
873
874  if (style === "stucco-apartment") {
875    for (const side of [-1, 1]) {
876      const stair = new THREE.Mesh(new THREE.BoxGeometry(1.1, Math.min(h * 0.58, 9), 1.4), mats.facadeTrim);
877      stair.position.set(x + side * (w * 0.5 + 0.28), Math.min(h * 0.29, 4.5), z - d * 0.18);
878      city.add(stair);
879    }
880    const terrace = new THREE.Mesh(new THREE.BoxGeometry(w * 0.55, 0.24, d * 0.55), mats.sidewalk);
881    terrace.position.set(x, h + 0.62, z);
882    city.add(terrace);
883    return;
884  }
885
886  if (style === "corner-market" || style === "shop-block") {
887    const cornerTower = new THREE.Mesh(createBuildingBodyGeometry(w * 0.36, Math.min(h * 0.45, 7), d * 0.36, "corner-market"), mats.windowFrame);
888    cornerTower.position.set(x + w * 0.26, h + Math.min(h * 0.22, 3.5), z + d * 0.25);
889    city.add(cornerTower);
890  }
891}
892
893function addBusStop(x, z, rotation = 0) {
894  const stop = new THREE.Group();
895  stop.position.set(x, 0, z);
896  stop.rotation.y = rotation;
897
898  const pad = new THREE.Mesh(new THREE.BoxGeometry(7.6, 0.08, 2.4), mats.sidewalk);
899  pad.position.y = 0.08;
900  stop.add(pad);
901  const back = new THREE.Mesh(new THREE.BoxGeometry(7.0, 2.1, 0.12), mats.darkGlass);
902  back.position.set(0, 1.18, -0.88);
903  stop.add(back);
904  const roof = new THREE.Mesh(new THREE.BoxGeometry(7.4, 0.18, 2.3), mats.chrome);
905  roof.position.set(0, 2.38, 0);
906  stop.add(roof);
907  for (const sx of [-3.2, 3.2]) {
908    const post = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.07, 2.25, 8), mats.chrome);
909    post.position.set(sx, 1.14, 0.86);
910    stop.add(post);
911  }
912  const bench = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.24, 
9120.7), mats.facadeTrim);
913  bench.position.set(0, 0.62, 0.38);
914  stop.add(bench);
915  const sign = new THREE.Mesh(
916    new THREE.PlaneGeometry(1.5, 0.8),
917    new THREE.MeshBasicMaterial({ map: makeStreetSignTexture("BUS", 0x2e88b8, 0xffffff), transparent: true }),
918  );
919  sign.position.set(-2.9, 1.55, -0.79);
920  stop.add(sign);
921  city.add(stop);
922  props.push({ mesh: stop, radius: 3.7, fixed: true });
923}
924
925function addBillboard(x, z, index) {
926  const board = new THREE.Group();
927  board.position.set(x, 0, z);
928  board.rotation.y = index % 2 ? Math.PI / 2 : 0;
929  const poleA = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.11, 5.2, 8), mats.chrome);
930  poleA.position.set(-2.2, 2.6, 0);
931  board.add(poleA);
932  const poleB = poleA.clone();
933  poleB.position.x = 2.2;
934  board.add(poleB);
935  const panel = new THREE.Mesh(
936    new THREE.PlaneGeometry(8.8, 3.2),
937    new THREE.MeshBasicMaterial({ map: makeStreetSignTexture(index % 2 ? "DRIVE FAST" : "CITY MALL", 0x101318, index % 2 ? 0xf5c84b : 0x8fd9ff), transparent: true }),
938  );
939  panel.position.set(0, 5.4, 0.08);
940  board.add(panel);
941  const frame = new THREE.Mesh(new THREE.BoxGeometry(9.2, 3.55, 0.16), mats.windowFrame);
942  frame.position.set(0, 5.4, 0);
943  board.add(frame);
944  board.add(panel);
945  city.add(board);
946  props.push({ mesh: { position: new THREE.Vector3(x, 0, z) }, radius: 3.0, fixed: true });
947}
948
949function makeStreetSignTexture(text, background, foreground) {
950  const canvas = document.createElement("canvas");
951  canvas.width = 512;
952  canvas.height = 192;
953  const ctx = canvas.getContext("2d");
954  ctx.fillStyle = hexWithAlpha(background, 1);
955  ctx.fillRect(0, 0, canvas.width, canvas.height);
956  ctx.strokeStyle = "rgba(255,255,255,0.28)";
957  ctx.lineWidth = 10;
958  ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
959  ctx.fillStyle = hexWithAlpha(foreground, 1);
960  ctx.font = "900 54px Arial";
961  ctx.textAlign = "center";
962  ctx.textBaseline = "middle";
963  ctx.fillText(text, canvas.width / 2, canvas.height / 2);
964  const texture = new THREE.CanvasTexture(canvas);
965  texture.colorSpace = THREE.SRGBColorSpace;
966  return texture;
967}
968
969function addCrosswalk(x, z, horizontal) {
970  const barCount = 3;
971  for (let i = 0; i < barCount; i += 1) {
972    const bar = new THREE.Mesh(
973      new THREE.BoxGeometry(horizontal ? 0.42 : 4.4, 0.035, horizontal ? 4.4 : 0.42),
974      mats.roadPaint,
975    );
976    const offset = (i - (barCount - 1) / 2) * 0.78;
977    bar.position.set(horizontal ? x + offset : x, 0.155, horizontal ? z : z + offset);
978    city.add(bar);
979  }
980
981  for (const side of [-1, 1]) {
982    const stop = new THREE.Mesh(
983      new THREE.BoxGeometry(horizontal ? 0.25 : roadWidth * 0.72, 0.038, horizontal ? roadWidth * 0.72 : 0.25),
984      mats.roadPaint,
985    );
986    stop.position.set(horizontal ? x + side * (roadWidth * 0.5 + 1.6) : x, 0.158, horizontal ? z : z + side * (roadWidth * 0.5 + 1.6));
987    city.add(stop);
988  }
989}
990
991function addTrafficLightSet(x, z) {
992  const cornerOffset = roadWidth / 2 + 2.8;
993  const corners = [
994    { x: x - cornerOffset, z: z - cornerOffset, facesVertical: true },
995    { x: x + cornerOffset, z: z + cornerOffset, facesVertical: true },
996    { x: x + cornerOffset, z: z - cornerOffset, facesVertical: false },
997    { x: x - cornerOffset, z: z + cornerOffset, facesVertical: false },
998  ];
999
1000  for (const corner of corners) {
1001    const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.075, 2.9, 8), mats.chrome);
1002    pole.position.set(corner.x, 1.45, corner.z);
1003    city.add(pole);
1004
1005    const box = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.82, 0.22), mats.carDark);
1006    box.position.set(corner.x, 2.75, corner.z);
1007    box.rotation.y = corner.facesVertical ? 0 : Math.PI / 2;
1008    city.add(box);
1009
1010    const bulbs = {};
1011    const colors = [
1012      ["red", 0.24, 0xff1f1f, 0x3a0808],
1013      ["yellow", 0, 0xffd447, 0x3a2b08],
1014      ["green", -0.24, 0x38ff63, 0x0a3414],
1015    ];
1016    for (const [name, y, bright, dim] of colors) {
1017      const material = new THREE.MeshBasicMaterial({ color: dim });
1018      const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.075, 10, 8), material);
1019      bulb.position.set(corner.x, 2.75 + y, corner.z + (corner.facesVertical ? 0.12 : 0));
1020      if (!corner.facesVertical) bulb.position.x += 0.12;
1021      city.add(bulb);
1022      bulbs[name] = { material, bright, dim };
1023    }
1024
1025    trafficLights.push({ x, z, facesVertical: corner.facesVertical, bulbs });
1026  }
1027}
1028
1029function addTireMark(x, z, vertical) {
1030  const mark = new THREE.Mesh(
1031    new THREE.PlaneGeometry(vertical ? 0.34 : 7.5, vertical ? 7.5 : 0.34),
1032    mats.tireMark,
1033  );
1034  mark.rotation.x = -Math.PI / 2;
1035  mark.rotation.z = (Math.random() - 0.5) * 0.18;
1036  mark.position.set(x, 0.17, z);
1037  city.add(mark);
1038}
1039
1040function addDrain(x, z) {
1041  const drain = new THREE.Mesh(new THREE.BoxGeometry(0.95, 0.04, 0.58), mats.drain);
1042  drain.position.set(x, 0.17, z);
1043  city.add(drain);
1044  for (let i = -2; i <= 2; i += 1) {
1045    const slit = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.02, 0.5), mats.carDark);
1046    slit.position.set(x + i * 0.16, 0.195, z);
1047    city.add(slit);
1048  }
1049}
1050
1051function addRoadPatch(x, z, vertical) {
1052  const patch = new THREE.Mesh(
1053    new THREE.PlaneGeometry(vertical ? 2.2 + Math.random() * 2.5 : 7 + Math.random() * 6, vertical ? 7 + Math.random() * 6 : 2.2 + Math.random() * 2.5),
1054    mats.roadPatch,
1055  );
1056  patch.rotation.x = -Math.PI / 2;
1057  patch.rotation.z = (Math.random() - 0.5) * 0.28;
1058  patch.position.set(x, 0.168, z);
1059  city.add(patch);
1060}
1061
1062function addDirtPatch(x, z, centerX = 0) {
1063  const nearRoadX = centerX + Math.round((x - centerX) / blockSize) * blockSize;
1064  const nearRoadZ = Math.round(z / blockSize) * blockSize;
1065  if (Math.abs(x - nearRoadX) < roadWidth * 0.6 || Math.abs(z - nearRoadZ) < roadWidth * 0.6) return;
1066  const patch = new THREE.Mesh(new THREE.PlaneGeometry(3 + Math.random() * 8, 2 + Math.random() * 6), mats.dirt);
1067  patch.rotation.x = -Math.PI / 2;
1068  patch.rotation.z = Math.random() * Math.PI;
1069  patch.position.set(x, 0.075, z);
1070  city.add(patch);
1071}
1072
1073function addStreetLight(x, z, rotation = 0) {
1074  const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 4.4, 10), mats.chrome);
1075  pole.position.set(x, 2.2, z);
1076  pole.castShadow = true;
1077  city.add(pole);
1078  props.push({ mesh: pole, radius: 0.8, fixed: true });
1079
1080  const arm = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.1, 0.1), mats.chrome);
1081  arm.position.set(x, 4.3, z);
1082  arm.rotation.y = rotation;
1083  arm.castShadow = true;
1084  city.add(arm);
1085
1086  const lampMaterial = new THREE.MeshBasicMaterial({ color: 0x6d654e });
1087  const lamp = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 8), lampMaterial);
1088  lamp.position.set(x, 4.55, z);
1089  city.add(lamp);
1090
1091  const glowMaterial = new THREE.MeshBasicMaterial({ color: 0xffd982, transparent: true, op
1091acity: 0, depthWrite: false });
1092  const glow = new THREE.Mesh(new THREE.SphereGeometry(0.56, 14, 8), glowMaterial);
1093  glow.position.copy(lamp.position);
1094  city.add(glow);
1095
1096  const groundGlowMaterial = new THREE.MeshBasicMaterial({ color: 0xffc35f, transparent: true, opacity: 0, depthWrite: false, side: THREE.DoubleSide });
1097  const groundGlow = new THREE.Mesh(new THREE.CircleGeometry(2.7, 24), groundGlowMaterial);
1098  groundGlow.rotation.x = -Math.PI / 2;
1099  groundGlow.position.set(x, 0.19, z);
1100  city.add(groundGlow);
1101
1102  streetLights.push({ lamp, glow, groundGlow });
1103}
1104
1105function addTrafficActors() {
1106  for (const centerX of districtCenters) {
1107    const roadChoices = [-200, -160, -120, -80, -40, 40, 80, 120, 160, 200];
1108    for (let i = 0; i < 24; i += 1) {
1109      const vertical = i % 2 === 0;
1110      const road = roadChoices[(i * 3 + Math.floor(Math.random() * 3)) % roadChoices.length];
1111      if (trafficRouteTouchesMilitaryBase(centerX, road, vertical, 8)) continue;
1112      const type = i % 8 === 0 ? "bus" : i % 5 === 0 ? "taxi" : "car";
1113      const lane = (Math.random() < 0.5 ? -1 : 1) * 2.1;
1114      const dir = Math.random() < 0.5 ? -1 : 1;
1115      const pos = randomTrafficPositionOutsideMilitary(centerX, road, vertical);
1116      const mesh = type === "bus" ? createBus(i) : type === "taxi" ? createTaxi(i) : createNpcCar(i + Math.round(centerX));
1117      city.add(mesh);
1118      npcCars.push({
1119        mesh,
1120        type,
1121        centerX,
1122        vertical,
1123        road,
1124        lane,
1125        dir,
1126        pos,
1127        speed: type === "bus" ? 5.5 + Math.random() * 3.2 : type === "taxi" ? 9 + Math.random() * 8 : 7 + Math.random() * 10,
1128        wait: 0,
1129        panicTimer: 0,
1130        laneChangeTimer: 2 + Math.random() * 5,
1131      });
1132    }
1133
1134    for (let i = 0; i < 58; i += 1) {
1135      const horizontal = i % 2 === 0;
1136      const road = roadChoices[(i * 2 + Math.floor(Math.random() * 4)) % roadChoices.length];
1137      if (trafficRouteTouchesMilitaryBase(centerX, road, !horizontal, 14)) continue;
1138      const side = Math.random() < 0.5 ? -1 : 1;
1139      const start = randomTrafficPositionOutsideMilitary(centerX, road, !horizontal);
1140      const mesh = createPedestrian(i + Math.round(centerX));
1141      city.add(mesh);
1142      pedestrians.push({
1143        mesh,
1144        centerX,
1145        horizontal,
1146        road,
1147        side,
1148        pos: start,
1149        dir: Math.random() < 0.5 ? -1 : 1,
1150        speed: 1.2 + Math.random() * 1.1,
1151        walkSeed: Math.random() * Math.PI * 2,
1152        panicTimer: 0,
1153        panicDir: new THREE.Vector3(),
1154        down: false,
1155      });
1156    }
1157
1158    for (let i = 0; i < 3 && policeCars.length < 5; i += 1) {
1159      const road = roadChoices[(i * 4 + 1) % roadChoices.length];
1160      const pos = -worldSize + 45 + i * 120;
1161      const carMesh = createPoliceCar(i);
1162      carMesh.visible = false;
1163      city.add(carMesh);
1164      policeCars.push({
1165        mesh: carMesh,
1166        home: new THREE.Vector3(centerX + road, 0.08, pos),
1167        speed: 0,
1168        heading: 0,
1169        deployed: false,
1170        officerOut: false,
1171        surroundOffset: new THREE.Vector3(),
1172      });
1173
1174      const officer = createPoliceOfficer(i);
1175      officer.position.set(centerX + road + 4, 0, pos + 3);
1176      officer.visible = false;
1177      city.add(officer);
1178      policeOfficers.push({
1179        mesh: officer,
1180        home: officer.position.clone(),
1181        speed: 1.8 + Math.random() * 0.8,
1182        attackCooldown: 0,
1183        deployed: false,
1184      });
1185    }
1186  }
1187}
1188
1189function trafficRouteTouchesMilitaryBase(centerX, road, vertical, padding = 0) {
1190  if (vertical) return Math.abs(centerX + road - militaryBase.x) < militaryBase.halfX + padding;
1191  return Math.abs(road - militaryBase.z) < militaryBase.halfZ + padding && Math.abs(centerX - militaryBase.x) < worldSize + militaryBase.halfX;
1192}
1193
1194function randomTrafficPositionOutsideMilitary(centerX, road, vertical) {
1195  for (let attempt = 0; attempt < 12; attempt += 1) {
1196    const pos = -worldSize + 25 + Math.random() * (worldSize * 2 - 50);
1197    const x = vertical ? centerX + road : centerX + pos;
1198    const z = vertical ? pos : road;
1199    if (!isInMilitaryBase(x, z, 18)) return pos;
1200  }
1201  return -worldSize + 20;
1202}
1203
1204function createNpcCar(index) {
1205  const colors = [0xd84a3f, 0x2f80d8, 0xe0d15a, 0x343a40, 0xf2f0e8, 0x7a4bd8, 0x44a86f];
1206  const paint = new THREE.MeshPhysicalMaterial({
1207    color: colors[index % colors.length],
1208    roughness: 0.22,
1209    metalness: 0.55,
1210    clearcoat: 0.65,
1211    clearcoatRoughness: 0.18,
1212  });
1213  const group = createCar({ paint, registerParts: false, lights: false });
1214  group.scale.setScalar(0.92);
1215  return group;
1216}
1217
1218function createTaxi(index) {
1219  const paint = new THREE.MeshPhysicalMaterial({
1220    color: 0xf3c84b,
1221    roughness: 0.24,
1222    metalness: 0.32,
1223    clearcoat: 0.55,
1224    clearcoatRoughness: 0.2,
1225  });
1226  const taxi = createCar({ paint, registerParts: false, lights: false });
1227  taxi.scale.setScalar(0.92);
1228  const roofSign = new THREE.Mesh(new THREE.BoxGeometry(0.72, 0.18, 0.26), new THREE.MeshBasicMaterial({ color: 0xfff4b8 }));
1229  roofSign.position.set(0, 1.72, -0.45);
1230  taxi.add(roofSign);
1231  const checker = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.1, 0.055), new THREE.MeshBasicMaterial({ color: 0x151515 }));
1232  checker.position.set(0, 0.82, -0.2);
1233  taxi.add(checker);
1234  return taxi;
1235}
1236
1237function createBus(index) {
1238  const bus = new THREE.Group();
1239  const color = index % 2 ? 0x2e88b8 : 0xd84534;
1240  const bodyMat = new THREE.MeshStandardMaterial({ color, roughness: 0.52, metalness: 0.08 });
1241  const body = new THREE.Mesh(new THREE.BoxGeometry(3.0, 1.9, 8.6), bodyMat);
1242  body.position.set(0, 1.05, 0);
1243  body.castShadow = true;
1244  bus.add(body);
1245
1246  const lower = new THREE.Mesh(new THREE.BoxGeometry(3.12, 0.36, 8.75), mats.carDark);
1247  lower.position.set(0, 0.42, 0);
1248  bus.add(lower);
1249
1250  for (let i = 0; i < 6; i += 1) {
1251    for (const side of [-1, 1]) {
1252      const window = new THREE.Mesh(new THREE.BoxGeometry(0.055, 0.58, 0.78), mats.glass);
1253      window.position.set(side * 1.54, 1.36, -2.85 + i * 1.05);
1254      bus.add(window);
1255    }
1256  }
1257  const windshield = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.72, 0.06), mats.glass);
1258  windshield.position.set(0, 1.44, 4.34);
1259  bus.add(windshield);
1260
1261  for (const x of [-1.35, 1.35]) {
1262    for (const z of [-3.0, 2.8]) {
1263      const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.42, 0.42, 0.28, 18), mats.rubber);
1264      wheel.rotation.z = Math.PI / 2;
1265      wheel.position.set(x, 0.38, z);
1266      bus.add(wheel);
1267    }
1268  }
1269  const sign = new THREE.Mesh(
1270    new THREE.PlaneGeometry(1.6, 0.42),
1271    new THREE.MeshBasicMaterial({ map: makeStreetSignTexture("BUS", 0x111317, 0xf6f0e3), transparent: true }),
1272  );
1273  sign.position.set(0, 1.92, 4.39);
1274  bus.add(sign);
1275  bus.scale.setScalar(0.92);
1276  return bus;
1277}
1278
1279function createPoliceCar(index) {
1280  const paint = new THREE.MeshPhysicalMaterial({
1281    color: index % 2 ? 0xf2f4f7 : 0x1b2734,
1282    roughness: 0.24,
1283    metalness: 0.5,
1284    clearcoat: 0.7,
1285    clearcoatRoughness: 0.16,
1286  });
1287  const group = createCar({ paint, registerParts: false, lights: false });
1288  group.scale.setScalar(0.95);
1289
1290  const doorStripe = new THREE.Mesh(new THREE.BoxGeometry(2.35, 0.16, 0.08), new THREE.MeshBasicMaterial({ color: 0x1e5bd7 }));
1291  doorStripe.position.set(0, 0.82, -0.18);
1292  group.add(doorStripe);
1293  const lightbarBase = new THREE.Mesh(new THREE.BoxGeometry(0.82, 0.12, 0.24), mats.carDark);
1294  lightbarBase.position.set(0, 1.62, -0.52);
1295  group.add(lightbarBase);
1296  const red = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.13, 0.22), new THREE.MeshBasicMaterial({ color: 0xff2c2c }));
1297  red.position.set(-0.22, 1.72, -0.52);
1298  group.add(red);
1299  const blue = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.13, 0.22), new THREE.MeshBasicMaterial({ color: 0x2c72ff }));
1300  blue.position.set(0.22, 1.72, -0.52);
1301  group.add(blue);
1302  group.userData.sirenLights = [red, blue];
1303  return group;
1304}
1305
1306function createPoliceOfficer(index) {
1307  const group = createPedestrian(index + 101);
1308  const uniform = new THREE.MeshStandardMaterial({ color: 0x172944, roughness: 0.78 });
1309  const vest = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.58, 0.14), uniform);
1310  vest.position.set(0, 1.1, 0.16);
1311  group.add(vest);
1312  const badge = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 6), mats.coin);
1313  badge.position.set(0.12, 1.28, 0.24);
1314  group.add(badge);
1315  const cap = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.08, 0.28), uniform);
1316  cap.position.set(0, 1.92, 0.06);
1317  group.add(cap);
1318  const pistol = new THREE.Mesh(new THREE.BoxGeometry(0.09, 0.08, 0.34), mats.gun);
1319  pistol.position.set(0.34, 1.08, 0.34);
1320  pistol.rotation.x = -0.1;
1321  group.add(pistol);
1322  return group;
1323}
1324
1325function createPoliceHelicopter() {
1326  const heli = new THREE.Group();
1327  const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.65, 2.4, 6, 12), mats.carDark);
1328  body.rotation.z = Math.PI / 2;
1329  heli.add(body);
1330  const cockpit = new THREE.Mesh(new THREE.SphereGeometry(0.42, 12, 8), mats.darkGlass);
1331  cockpit.position.set(1.25, 0.05, 0);
1332  cockpit.scale.set(1.25, 0.8, 0.8);
1333  heli.add(cockpit);
1334  const tail = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.16, 0.16), mats.chrome);
1335  tail.position.set(-2.25, 0, 0);
1336  heli.add(tail);
1337  const rotor = new THREE.Mesh(new THREE.BoxGeometry(4.4, 0.04, 0.18), mats.chrome);
1338  rotor.position.y = 0.72;
1339  heli.add(rotor);
1340  const rotorCross = rotor.clone();
1341  rotorCross.rotation.y = Math.PI / 2;
1342  heli.add(rotorCross);
1343  const skidA = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.08, 0.08), mats.chrome);
1344  skidA.position.set(0, -0.62, -0.55);
1345  heli.add(skidA);
1346  const skidB = skidA.clone();
1347  skidB.position.z = 0.55;
1348  heli.add(skidB);
1349  heli.userData.rotors = [rotor, rotorCross];
1350  return heli;
1351}
1352
1353function createPedestrian(index) {
1354  const group = new THREE.Group();
1355  const isFemale = index % 2 === 0;
1356  const shirtColors = isFemale ? [0xc85c9a, 0x8f4bd8, 0xd95f6a, 0x7a62d8] : [0x3f8b49, 0xd9ad45, 0x4b7fd8, 0x8a7658];
1357  const shirt = new THREE.MeshStandardMaterial({ color: shirtColors[index % shirtColors.length], roughness: 0.8 });
1358  const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.18, 0.72, 4, 8), shirt);
1359  body.position.y = 1.08;
1360  group.add(body);
1361  const head = new THREE.Mesh(new THREE.SphereGeometry(0.2, 12, 9), mats.skin);
1362  head.position.y = 1.72;
1363  group.add(head);
1364
1365  const hair = new THREE.Mesh(new THREE.SphereGeometry(0.21, 10, 7), mats.carDark);
1366  hair.position.set(0, 1.84, -0.03);
1367  hair.scale.set(1, 0.42, 0.9);
1368  group.add(hair);
1369  if (isFemale) {
1370    const backHair = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.42, 0.13), mats.carDark);
1371    backHair.position.set(0, 1.58, -0.16);
1372    group.add(backHair);
1373    const skirt = new THREE.Mesh(new THREE.ConeGeometry(0.27, 0.42, 4), shirt);
1374    skirt.position.set(0, 0.74, 0);
1375    skirt.rotation.y = Math.PI / 4;
1376    skirt.scale.z = 0.75;
1377    group.add(skirt);
1378  } else {
1379    const cap = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.06, 0.18), mats.carDark);
1380    cap.position.set(0, 1.89, 0.08);
1381    group.add(cap);
1382  }
1383
1384  for (const x of [-0.07, 0.07]) {
1385    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.02, 6, 5), mats.eye);
1386    eye.position.set(x, 1.76, 0.18);
1387    group.add(eye);
1388  }
1389  const mouth = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.014, 0.012), mats.eye);
1390  mouth.position.set(0, 1.64, 0.19);
1391  group.add(mouth);
1392
1393  const limbs = { arms: [], forearms: [], hands: [], legs: [], shins: [], feet: [] };
1394  for (const side of [-1, 1]) {
1395    const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.06, 0.36, 4, 7), shirt);
1396    arm.position.set(side * 0.28, 1.15, 0.02);
1397    group.add(arm);
1398    limbs.arms.push(arm);
1399
1400    const forearm = new THREE.Mesh(new THREE.CapsuleGeometry(0.052, 0.32, 4, 7), mats.skin);
1401    forearm.position.set(side * 0.31, 0.88, 0.02);
1402    group.add(forearm);
1403    limbs.forearms.push(forearm);
1404
1405    const hand = new THREE.Mesh(new THREE.SphereGeometry(0.065, 8, 6), mats.skin);
1406    hand.position.set(side * 0.32, 0.68, 0.03);
1407    group.add(hand);
1408    limbs.hands.push(hand);
1409
1410    const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.075, 0.42, 4, 7), mats.pants);
1411    leg.position.set(side * 0.09, 0.58, 0);
1412    group.add(leg);
1413    limbs.legs.push(leg);
1414
1415    const shin = new THREE.Mesh(new THREE.CapsuleGeometry(0.068, 0.38, 4, 7), mats.pants);
1416    shin.position.set(side * 0.09, 0.28, 0);
1417    group.add(shin);
1418    limbs.shins.push(shin);
1419
1420    const foot = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.07, 0.28), mats.shoe);
1421    foot.position.set(side * 0.09, 0.04, 0.08);
1422    group.add(foot);
1423    limbs.feet.push(foot);
1424  }
1425
1426  group.userData.limbs = limbs;
1427  return group;
1428}
1429
1430function addConnectorBridges() {
1431  for (let i = 0; i < districtCenters.length - 1; i += 1) {
1432    addConnectorBridge(districtCenters[i], districtCenters[i + 1]);
1433  }
1434}
1435
1436function addConnectorBridge(fromCenterX, toCenterX) {
1437  const startX = fromCenterX + worldSize;
1438  const endX = toCenterX - worldSize;
1439  const centerX = (startX + endX) / 2;
1440  const bridgeLength = endX - startX;
1441  const roadLength = bridgeLength + roadWidth * 2;
1442  addRoad(centerX, 0, roadLength, roadWidth, true, false);
1443
1444  const deck = new THREE.Mesh(new THREE.BoxGeometry(bridgeLength, 0.5, roadWidth + 8.2), mats.bridgeDeck);
1445  deck.position.set(centerX, -0.24, 0);
1446  deck.receiveShadow = true;
1447  city.add(deck);
1448
1449  const asphalt = new THREE.Mesh(new THREE.BoxGeometry(bridgeLength - 4, 0.08, roadWidth), mats.asphalt);
1450  asphalt.position.set(centerX, 0.04, 0);
1451  asphalt.receiveShadow = true;
1452  city.add(asphalt);
1453
1454  const dashLength = 8;
1455  const gap = 13;
1456  for (let x = startX + 14; x < endX - 14; x += dashLength + gap) {
1457    const stripe = new THREE.Mesh(new THREE.BoxGeometry(dashLength, 0.04, 0.16), mats.roadPaint);
1458    stripe.position.set(x + dashLength / 2, 0.12, 0);
1459    city.add(stripe);
1460  }
1461
1462  for (const side of [-1, 1]) {
1463    const edgeLine = new THREE.Mesh(new THREE.BoxGeometry(bridgeLength - 10, 0.035, 0.12), mats.roadPaint);
1464    edgeLine.position.set(centerX, 0.125, side * (roadWidth / 2 - 0.55));
1465    city.add(edgeLine);
1466  }
1467
1468  for (let x = startX + 18; x <= endX - 18; x += 28) {
1469    const joint = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.055, roadWidth + 5.8), mats.drain);
1470    joint.position.set(x, 0.13, 0);
1471    city.add(joint);
1472  }
1473
1474  const railLength = Math.max(20, bridgeLength - 28);
1475  for (const side of [-1, 1]) {
1476    const curb = new THREE.Mesh(new THREE.BoxGeometry(railLength, 0.36, 0.72), mats.curbStone);
1477    curb.position.set(centerX, 0.34, side * (roadWidth / 2 + 1.05));
1478    curb.castShadow = true;
1479    curb.receiveShadow = true;
1480    city.add(curb);
1481
1482    const rail = new THREE.Mesh(new THREE.BoxGeometry(railLength, 0.42, 0.28), mats.bridgeRail);
1483    rail.position.set(centerX, 1.28, side * (roadWidth / 2 + 3.3));
1484    rail.castShadow = true;
1485    city.add(rail);
1486    obstacles.push({ x: rail.position.x, z: rail.position.z, halfX: railLength / 2, halfZ: 0.85, label: "You hit the guardrail." });
1487
1488    for (let x = startX + 10; x <= endX - 10; x += 12) {
1489      const post = new THREE.Mesh(new THREE.BoxGeometry(0.28, 1.26, 0.32), mats.bridgeRail);
1490      post.position.set(x, 0.86, side * (roadWidth / 2 + 3.3));
1491      post.castShadow = true;
1492      city.add(post);
1493    }
1494
1495    for (let x = startX + 24; x <= endX - 24; x += 54) {
1496      const lampPole = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.075, 3.2, 8), mats.chrome);
1497      lampPole.position.set(x, 1.95, side * (roadWidth / 2 + 2.25));
1498      city.add(lampPole);
1499      const lamp = new THREE.Mesh(new THREE.SphereGeometry(0.16, 10, 8), mats.lamp);
1500      lamp.position.set(x, 3.62, side * (roadWidth / 2 + 2.25));
1501      city.add(lamp);
1502    }
1503  }
1504
1505  for (let i = -3; i <= 3; i += 1) {
1506    if (i === 0) continue;
1507    for (const side of [-1, 1]) {
1508      const pier = new THREE.Mesh(new THREE.BoxGeometry(2.2, 3.2, 2.0), mats.bridgeDeck);
1509      pier.position.set(centerX + i * (bridgeLength / 8), -1.42, side * (roadWidth / 2 + 2.75));
1510      pier.receiveShadow = true;
1511      city.add(pier);
1512      obstacles.push({ x: pier.position.x, z: pier.position.z, halfX: 1.05, halfZ: 1.05, label: "You hit the bridge support." });
1513
1514      const cap = new THREE.Mesh(new THREE.BoxGeometry(5.4, 0.42, 2.9), mats.bridgeRail);
1515      cap.position.set(pier.position.x, 0.12, pier.position.z);
1516      cap.receiveShadow = true;
1517      city.add(cap);
1518    }
1519  }
1520
1521  for (let x = startX + 20; x <= endX - 20; x += 34) {
1522    const beam = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.45, roadWidth + 7.4), mats.windowFrame);
1523    beam.position.set(x, -0.68, 0);
1524    city.add(beam);
1525  }
1526  for (const side of [-1, 1]) {
1527    const sideBeam = new THREE.Mesh(new THREE.BoxGeometry(bridgeLength - 18, 0.28, 0.34), mats.windowFrame);
1528    sideBeam.position.set(centerX, -0.72, side * (roadWidth / 2 + 2.25));
1529    city.add(sideBeam);
1530  }
1531
1532  for (const x of [startX + 18, endX - 18]) {
1533    for (const side of [-1, 1]) {
1534      const tower = new THREE.Mesh(new THREE.BoxGeometry(2.4, 21, 2.2), mats.bridgeRail);
1535      tower.position.set(x, 10.5, side * (roadWidth / 2 + 2.9));
1536      tower.castShadow = true;
1537      tower.receiveShadow = true;
1538      city.add(tower);
1539      obstacles.push({ x: tower.position.x, z: tower.position.z, halfX: 1.25, halfZ: 1.25, label: "You hit the bridge tower." });
1540
1541      const towerCap = new THREE.Mesh(new THREE.BoxGeometry(3.4, 0.7, 3.0), mats.bridgeDeck);
1542      towerCap.position.set(x, 21.25, side * (roadWidth / 2 + 2.9));
1543      city.add(towerCap);
1544    }
1545    for (let i = 1; i <= 7; i += 1) {
1546      const anchorX = x + (x < centerX ? i : -i) * 9.6;
1547      for (const side of [-1, 1]) {
1548        const z = side * (roadWidth / 2 + 3.1);
1549        addCable(new THREE.Vector3(x, 20.7, z), new THREE.Vector3(anchorX, 1.45, z), 0.055);
1550        addCable(new THREE.Vector3(anchorX, 6.4 + i * 1.15, z), new THREE.Vector3(anchorX, 1.45, z), 0.035);
1551      }
1552    }
1553  }
1554}
1555
1556function addCable(start, end, radius) {
1557  const mid = start.clone().add(end).multiplyScalar(0.5);
1558  const direction = end.clone().sub(start);
1559  const cable = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, direction.length(), 8), mats.bridgeCable);
1560  cable.position.copy(mid);
1561  cable.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.normalize());
1562  cable.castShadow = true;
1563  city.add(cable);
1564}
1565
1566function createBuildingBodyGeometry(width, height, depth, style) {
1567  const chamfer = style === "glass-office" ? 0.18 : style === "corner-market" ? 0.32 : style === "stepped-tower" ? 0.12 : 0.08;
1568  const topScale = style === "stepped-tower" ? 0.68 : style === "glass-office" ? 0.82 : style === "stucco-apartment" ? 0.9 : 0.96;
1569  const bw = width / 2;
1570  const bd = depth / 2;
1571  const cutX = Math.min(bw * chamfer, 2.4);
1572  const cutZ = Math.min(bd * chamfer, 2.4);
1573  const bottom = [
1574    [-bw + cutX, -height / 2, -bd],
1575    [bw - cutX, -height / 2, -bd],
1576    [bw, -height / 2, -bd + cutZ],
1577    [bw, -height / 2, bd - cutZ],
1578    [bw - cutX, -height / 2, bd],
1579    [-bw + cutX, -height / 2, bd],
1580    [-bw, -height / 2, bd - cutZ],
1581    [-bw, -height / 2, -bd + cutZ],
1582  ];
1583  const top = bottom.map(([x, , z]) => [x * topScale, height / 2, z * topScale]);
1584  const bottomCenterIndex = 16;
1585  const topCenterIndex = 17;
1586  const positions = [
1587    ...bottom,
1588    ...top,
1589    [0, -height / 2, 0],
1590    [0, height / 2, 0],
1591  ].flat();
1592  const indices = [];
1593  for (let i = 0; i < 8; i += 1) {
1594    const next = (i + 1) % 8;
1595    indices.push(bottomCenterIndex, next, i);
1596    indices.push(topCenterIndex, 8 + i, 8 + next);
1597  }
1598  for (let i = 0; i < 8; i += 1) {
1599    const next = (i + 1) % 8;
1600    indices.push(i, next, 8 + next, i, 8 + next, 8 + i);
1601  }
1602  const geometry = new THREE.BufferGeometry();
1603  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
1604  geometry.setIndex(indices);
1605  geometry.computeVertexNormals();
1606  return geometry;
1607}
1608
1609function addBuilding(x, z, options = {}) {
1610  const downtown = Math.abs(x) > 70 || Math.abs(z) > 70;
1611  const skyscraper = downtown && Math.random() < 0.26;
1612  const hospital = options.hospital === true;
1613  const megaSkyscraper = options.megaSkyscraper === true;
1614  const buildingStyle = hospital ? "hospital" : megaSkyscraper ? "tower" : skyscraper
1615    ? (Math.random() < 0.55 ? "glass-office" : "stepped-tower")
1616    : ["brick-flat", "shop-block", "stucco-apartment", "corner-market"][Math.floor(Math.random() * 4)];
1617  const w = megaSkyscraper ? 56 : hospital ? 17 : skyscraper ? 13 + Math.random() * 9 : 8 + Math.random() * 6;
1618  const d = megaSkyscraper ? 48 : hospital ? 15 : skyscraper ? 13 + Math.random() * 9 : 8 + Math.random() * 6;
1619  const h = megaSkyscraper ? 142 : hospital ? 23 : skyscraper ? 44 + Math.random() * 62 : 6 + Math.random() * 24;
1620  const palette = buildingStyle === "brick-flat"
1621    ? [0x9a5345, 0x8b4b3f, 0xa56550, 0x7f4a42]
1622    : buildingStyle === "shop-block"
1623      ? [0xc9b26f, 0xbfc6c2, 0xcc7b55, 0x8fa7af]
1624      : buildingStyle === "stucco-apartment"
1625        ? [0xd8d0bd, 0xc7b89f, 0xb7c0ad, 0xd7c3ae]
1626        : buildingStyle === "glass-office"
1627          ? [0x5f7680, 0x738892, 0x607783, 0x8a999e]
1628          : skyscraper
1629            ? [0x80919a, 0x9ba8ad, 0x6f848f, 0xb1aca1, 0x77858b]
1630            : [0xd8b48a, 0x9fb3bf, 0xc7715e, 0xd9d1bd, 0x8f9d86];
1631  const material = new THREE.MeshStandardMaterial({
1632    color: megaSkyscraper ? 0x6e8790 : hospital ? 0xecf0ef : palette[Math.floor(Math.random() * palette.length)],
1633    roughness: megaSkyscraper ? 0.48 : 0.78,
1634    metalness: megaSkyscraper ? 0.18 : 0.05,
1635    side: THREE.DoubleSide,
1636  });
1637  const mesh = new THREE.Mesh(createBuildingBodyGeometry(w, h, d, buildingStyle), material);
1638  mesh.position.set(x + (hospital || megaSkyscraper ? 0 : (Math.random() - 0.5) * 4), h / 2, z + (hospital || megaSkyscraper ? 0 : (Math.random() - 0.5) * 4));
1639  mesh.castShadow = true;
1640  mesh.receiveShadow = true;
1641  city.add(mesh);
1642  if (hospital) hospitalSpawn.set(mesh.position.x, 0, mesh.position.z + d / 2 + 6.2);
1643  const doorHalfX = Math.min(w / 2 - 0.55, Math.max(2.4, w * 0.42));
1644  buildings.push({
1645    mesh,
1646    halfX: w / 2 + 1.2,
1647    halfZ: d / 2 + 1.2,
1648    doorX: mesh.position.x,
1649    doorZ: mesh.position.z + d / 2,
1650    doorHalfX,
1651    lobbyBackZ: mesh.position.z - d / 2 + 0.55,
1652  });
1653
1654  const base = new THREE.Mesh(new THREE.BoxGeometry(w + 0.35, 1.2, d + 0.35), mats.facadeTrim);
1655  base.position.set(mesh.position.x, 0.62, mesh.position.z);
1656  base.castShadow = true;
1657  base.receiveShadow = true;
1658  city.add(base);
1659
1660  const storefrontCount = Math.max(2, Math.floor(w / 3.2));
1661  for (let i = 0; i < storefrontCount; i += 1) {
1662    const sx = mesh.position.x - w / 2 + 1.1 + i * ((w - 2.2) / Math.max(1, storefrontCount - 1));
1663    const shop = new THREE.Mesh(new THREE.BoxGeometry(1.15, 0.72, 0.055), mats.darkGlass);
1664    shop.position.set(sx, 0.72, mesh.position.z + d / 2 + 0.22);
1665    city.add(shop);
1666  }
1667
1668  const awning = new THREE.Mesh(new THREE.BoxGeometry(w * 0.72, 0.16, 0.55), mats.facadeTrim);
1669  awning.position.set(mesh.position.x, 1.32, mesh.position.z + d / 2 + 0.34);
1670  awning.castShadow = true;
1671  city.add(awning);
1672
1673  const sign = new THREE.Mesh(new THREE.BoxGeometry(w * 0.42, 0.34, 0.06), mats.windowFrame);
1674  sign.position.set(mesh.position.x, 1.64, mesh.position.z + d / 2 + 0.37);
1675  city.add(sign);
1676  if (!skyscraper && !hospital && Math.random() < 0.62) addShopfrontDetails(mesh.position.x, mesh.position.z, w, d);
1677
1678  addAutomaticDoors(mesh.position.x, mesh.position.z, mesh.position.z + d / 2 + 0.28, w, d);
1679  if (hospital) addHospitalDetails(mesh.position.x, mesh.position.z, w, d, h);
1680  if (megaSkyscraper) addMegaSkyscraperDetails(mesh.position.x, mesh.position.z, w, d, h);
1681
1682  for (const face of [-1, 1]) {
1683    const verticalTrim = new THREE.Mesh(new THREE.BoxGeometry(0.08, h * 0.82, 0.06), mats.facadeTrim);
1684    verticalTrim.position.set(mesh.position.x - w / 2 + 0.35, h * 0.54, mesh.position.z + face * (d / 2 + 0.07));
1685    city.add(verticalTrim);
1686    const verticalTrim2 = verticalTrim.clone();
1687    verticalTrim2.position.x = mesh.position.x + w / 2 - 0.35;
1688    city.add(verticalTrim2);
1689  }
1690
1691  const floorCount = Math.max(1, Math.floor((h - 2.2) / 3.8));
1692  for (let floor = 1; floor <= Math.min(floorCount, skyscraper ? 5 : 3); floor += 1) {
1693    const y = 1.5 + floor * 2.65;
1694    for (const face of [-1, 1]) {
1695      const ledge = new THREE.Mesh(new THREE.BoxGeometry(w + 0.22, 0.08, 0.16), mats.facadeTrim);
1696      ledge.position.set(mesh.position.x, y, mesh.position.z + face * (d / 2 + 0.14));
1697      city.add(ledge);
1698    }
1699  }
1700
1701  const rows = Math.min(skyscraper ? 6 : 3, Math.max(1, Math.floor(h / 6.4)));
1702  const cols = Math.max(3, Math.floor(w / 3.1));
1703  const windowBandWidth = Math.max(2.4, w - 2.2);
1704  for (let row = 0; row < rows; row += 1) {
1705    for (const face of [-1, 1]) {
1706      const y = 1.95 + row * 2.65;
1707      const band = new THREE.Mesh(new THREE.BoxGeometry(windowBandWidth, 0.58, 0.055), mats.window);
1708      band.position.set(mesh.position.x, y, mesh.position.z + face * (d / 2 + 0.095));
1709      city.add(band);
1710
1711      for (let col = 1; col < cols; col += 2) {
1712        const divider = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.66, 0.065), mats.windowFrame);
1713        divider.position.set(
1714          mesh.position.x - windowBandWidth / 2 + col * (windowBandWidth / cols),
1715          y,
1716          mesh.position.z + face * (d / 2 + 0.115),
1717        );
1718        city.add(divider);
1719      }
1720
1721      if (row % 2 === 0) {
1722        const sill = new THREE.Mesh(new THREE.BoxGeometry(windowBandWidth + 0.34, 0.06, 0.13), mats.facadeTrim);
1723        sill.position.set(mesh.position.x, y - 0.42, mesh.position.z + face * (d / 2 + 0.13));
1724        city.add(sill);
1725      }
1726    }
1727  }
1728
1729  const sideRows = Math.min(skyscraper ? 3 : 1, Math.max(1, Math.floor(h / 8.5)));
1730  const sideCols = Math.max(3, Math.floor(d / 3.5));
1731  const sideBandWidth = Math.max(2.4, d - 2.2);
1732  for (let row = 0; row < sideRows; row += 1) {
1733    for (const face of [-1, 1]) {
1734      const y = 2.1 + row * 3.05;
1735      const band = new THREE.Mesh(new THREE.BoxGeometry(0.055, 0.56, sideBandWidth), mats.window);
1736      band.position.set(mesh.position.x + face * (w / 2 + 0.095), y, mesh.position.z);
1737      city.add(band);
1738
1739      for (let col = 1; col < sideCols; col += 2) {
1740        const divider = new THREE.Mesh(new THREE.BoxGeometry(0.065, 0.64, 0.035), mats.windowFrame);
1741        divider.position.set(
1742          mesh.position.x + face * (w / 2 + 0.115),
1743          y,
1744          mesh.position.z - sideBandWidth / 2 + col * (sideBandWidth / sideCols),
1745        );
1746        city.add(divider);
1747      }
1748
1749    }
1750  }
1751
1752  if (!skyscraper && h > 11 && Math.random() < 0.38) {
1753    addBalconies(mesh.position.x, mesh.position.z, w, d, h, 1);
1754  }
1755  if (!skyscraper && h > 16 && Math.random() < 0.14) {
1756    addBalconies(mesh.position.x, mesh.position.z, w, d, h, -1);
1757  }
1758  addBuildingStyleDetails(mesh.position.x, mesh.position.z, w, d, h, buildingStyle);
1759
1760  const roof = new THREE.Mesh(new THREE.BoxGeometry(w + 0.5, 0.35, d + 0.5), mats.carDark);
1761  roof.position.set(mesh.position.x, h + 0.18, mesh.position.z);
1762  roof.castShadow = true;
1763  city.add(roof);
1764
1765  const parapet = new THREE.Mesh(new THREE.BoxGeometry(w + 0.9, 0.32, 0.24), mats.carDark);
1766  parapet.position.set(mesh.position.x, h + 0.55, mesh.position.z + d / 2 + 0.2);
1767  city.add(parapet);
1768  const parapetBack = parapet.clone();
1769  parapetBack.position.z = mesh.position.z - d / 2 - 0.2;
1770  city.add(parapetBack);
1771
1772  if (Math.random() < 0.65) {
1773    const roofUnit = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.65, 1.25), mats.facadeTrim);
1774    roofUnit.position.set(mesh.position.x + (Math.random() - 0.5) * w * 0.35, h + 0.68, mesh.position.z + (Math.random() - 0.5) * d * 0.35);
1775    roofUnit.castShadow = true;
1776    city.add(roofUnit);
1777  }
1778
1779  const ventCount = skyscraper ? 1 : 1;
1780  for (let i = 0; i < ventCount; i += 1) {
1781    const vent = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.18, 0.65, 10), mats.chrome);
1782    vent.position.set(mesh.position.x + (Math.random() - 0.5) * w * 0.52, h + 0.72, mesh.position.z + (Math.random() - 0.5) * d * 0.52);
1783    vent.castShadow = true;
1784    city.add(vent);
1785  }
1786
1787  if (skyscraper && Math.random() < 0.7) {
1788    const spire = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.16, 7 + Math.random() * 5, 8), mats.chrome);
1789    spire.position.set(mesh.position.x, h + 4.2, mesh.position.z);
1790    spire.castShadow = true;
1791    city.add(spire);
1792  }
1793}
1794
1795function addAutomaticDoors(x, buildingCenterZ, doorZ, buildingWidth, buildingDepth) {
1796  const z = doorZ;
1797  const lobbyWidth = Math.max(3.6, buildingWidth - 1.4);
1798  const doorWidth = Math.min(3.4, lobbyWidth * 0.72);
1799  const doorHeight = 2.35;
1800  const roomDepth = Math.max(4.2, buildingDepth - 1.1);
1801  const roomHeight = 6.2;
1802  const roomCenterZ = buildingCenterZ;
1803
1804  const floor = new THREE.Mesh(new THREE.BoxGeometry(lobbyWidth, 0.035, roomDepth), mats.interiorFloor);
1805  floor.position.set(x, 0.025, roomCenterZ);
1806  city.add(floor);
1807
1808  const thresholdDepth = Math.max(0.35, z - (buildingCenterZ + roomDepth / 2));
1809  if (thresholdDepth > 0) {
1810    const threshold = new THREE.Mesh(new THREE.BoxGeometry(doorWidth, 0.035, thresholdDepth + 0.16), mats.interiorFloor);
1811    threshold.position.set(x, 0.028, buildingCenterZ + roomDepth / 2 + thresholdDepth / 2 - 0.08);
1812    city.add(threshold);
1813  }
1814
1815  for (let i = -2; i <= 2; i += 1) {
1816    const tileLine = new THREE.Mesh(new THREE.BoxGeometry(lobbyWidth, 0.008, 0.025), mats.windowFrame);
1817    tileLine.position.set(x, 0.052, roomCenterZ + i * (roomDepth / 5));
1818    city.add(tileLine);
1819  }
1820
1821  const ceiling = new THREE.Mesh(new THREE.BoxGeometry(lobbyWidth, 0.06, roomDepth), mats.interiorWall);
1822  ceiling.position.set(x, roomHeight, roomCenterZ);
1823  city.add(ceiling);
1824
1825  const backWall = new THREE.Mesh(new THREE.BoxGeometry(lobbyWidth, roomHeight, 0.1), mats.interiorWall);
1826  backWall.position.set(x, roomHeight / 2, buildingCenterZ - roomDepth / 2);
1827  city.add(backWall);
1828
1829  for (const side of [-1, 1]) {
1830    const sideWall = new THREE.Mesh(new THREE.BoxGeometry(0.1, roomHeight, roomDepth), mats.interiorWall);
1831    sideWall.position.set(x + side * (lobbyWidth / 2), roomHeight / 2, roomCenterZ);
1832    city.add(sideWall);
1833  }
1834
1835  const frontUpperWallHeight = roomHeight - doorHeight;
1836  const frontUpperWall = new THREE.Mesh(new THREE.BoxGeometry(lobbyWidth, frontUpperWallHeight, 0.12), mats.interiorWall);
1837  frontUpperWall.position.set(x, doorHeight + frontUpperWallHeight / 2, z + 0.02);
1838  city.add(frontUpperWall);
1839
1840  const sideFrontWidth = Math.max(0.1, (lobbyWidth - doorWidth) / 2);
1841  for (const side of [-1, 1]) {
1842    const frontSideWall = new THREE.Mesh(new THREE.BoxGeometry(sideFrontWidth, doorHeight, 0.12), mats.interiorWall);
1843    frontSideWall.position.set(x + side * (doorWidth / 2 + sideFrontWidth / 2), doorHeight / 2, z + 0.02);
1844    city.add(frontSideWall);
1845  }
1846
1847  const innerHeader = new THREE.Mesh(new THREE.BoxGeometry(doorWidth, 0.18, 0.18), mats.interiorWall);
1848  innerHeader.position.set(x, doorHeight + 0.08, z - 0.18);
1849  city.add(innerHeader);
1850
1851  const lampPanel = new THREE.Mesh(new THREE.BoxGeometry(lobbyWidth * 0.48, 0.055, 0.32), mats.lamp);
1852  lampPanel.position.set(x, roomHeight - 0.06, roomCenterZ);
1853  city.add(lampPanel);
1854
1855  const interior = new THREE.Mesh(new THREE.BoxGeometry(doorWidth, doorHeight + 0.25, 0.08), mats.darkGlass);
1856  interior.position.set(x, doorHeight / 2 + 0.08, z - 0.06);
1857  city.add(interior);
1858
1859  const leftDoor = new THREE.Mesh(new THREE.BoxGeometry(doorWidth / 2 - 0.08, doorHeight, 0.05), mats.glass);
1860  const rightDoor = new THREE.Mesh(new THREE.BoxGeometry(doorWidth / 2 - 0.08, doorHeight, 0.05), mats.glass);
1861  const closedOffset = doorWidth / 4;
1862  leftDoor.position.set(x - closedOffset, doorHeight / 2, z + 0.1);
1863  rightDoor.position.set(x + closedOffset, doorHeight / 2, z + 0.1);
1864  city.add(leftDoor);
1865  city.add(rightDoor);
1866
1867  const frameTop = new THREE.Mesh(new THREE.BoxGeometry(doorWidth + 0.22, 0.12, 0.1), mats.windowFrame);
1868  frameTop.position.set(x, doorHeight + 0.08, z + 0.13);
1869  city.add(frameTop);
1870  for (const side of [-1, 1]) {
1871    const frame = new THREE.Mesh(new THREE.BoxGeometry(0.1, doorHeight + 0.15, 0.1), mats.windowFrame);
1872    frame.position.set(x + side * (doorWidth / 2 + 0.06), doorHeight / 2, z + 0.13);
1873    city.add(frame);
1874  }
1875
1876  autoDoors.push({
1877    x,
1878    z,
1879    leftDoor,
1880    rightDoor,
1881    closedOffset,
1882    openOffset: doorWidth / 2 + 0.34,
1883    open: 0,
1884  });
1885}
1886
1887function addHospitalDetails(x, z, w, d, h) {
1888  const frontZ = z + d / 2;
1889  const redMat = new THREE.MeshBasicMaterial({ color: 0xd7262f });
1890  const signBack = new THREE.Mesh(new THREE.BoxGeometry(4.8, 2.2, 0.12), mats.window);
1891  signBack.position.set(x, 4.1, frontZ + 0.42);
1892  city.add(signBack);
1893
1894  const crossV = new THREE.Mesh(new THREE.BoxGeometry(0.55, 1.6, 0.16), redMat);
1895  crossV.position.set(x - 1.55, 4.1, frontZ + 0.5);
1896  city.add(crossV);
1897  const crossH = new THREE.Mesh(new THREE.BoxGeometry(1.45, 0.5, 0.17), redMat);
1898  crossH.position.set(x - 1.55, 4.1, frontZ + 0.52);
1899  city.add(crossH);
1900
1901  const label = new THREE.Mesh(new THREE.PlaneGeometry(2.3, 1.25), new THREE.MeshBasicMaterial({ map: makeHospitalSignTexture(), transparent: true }));
1902  label.position.set(x + 0.85, 4.1, frontZ + 0.53);
1903  city.add(label);
1904
1905  const roofCrossV = new THREE.Mesh(new THREE.BoxGeometry(1.05, 0.12, 4.4), redMat);
1906  roofCrossV.position.set(x, h + 0.76, z);
1907  city.add(roofCrossV);
1908  const roofCrossH = new THREE.Mesh(new THREE.BoxGeometry(4.4, 0.13, 1.05), redMat);
1909  roofCrossH.position.set(x, h + 0.78, z);
1910  city.add(roofCrossH);
1911
1912  const ambulanceBay = new THREE.Mesh(new THREE.BoxGeometry(6.2, 0.04, 4.1), mats.roadPaint);
1913  ambulanceBay.position.set(x, 0.09, frontZ + 4.3);
1914  city.add(ambulanceBay);
1915}
1916
1917function addMegaSkyscraperDetails(x, z, w, d, h) {
1918  const frontZ = z + d / 2;
1919  const lobbyCanopy = new THREE.Mesh(new THREE.BoxGeometry(w * 0.72, 0.5, 3.8), mats.chrome);
1920  lobbyCanopy.position.set(x, 3.25, frontZ + 1.65);
1921  city.add(lobbyCanopy);
1922
1923  const entryPlaza = new THREE.Mesh(new THREE.BoxGeometry(w * 0.8, 0.045, 13), mats.sidewalk);
1924  entryPlaza.position.set(x, 0.085, frontZ + 7);
1925  city.add(entryPlaza);
1926
1927  const sign = new THREE.Mesh(new THREE.PlaneGeometry(14, 3.1), new THREE.MeshBasicMaterial({ map: makeTowerSignTexture(), transparent: true }));
1928  sign.position.set(x, 7.6, frontZ + 0.62);
1929  city.add(sign);
1930
1931  const crown = new THREE.Mesh(new THREE.BoxGeometry(w * 0.72, 4.2, d * 0.72), mats.darkGlass);
1932  crown.position.set(x, h + 2.1, z);
1933  city.add(crown);
1934
1935  const spire = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.52, 24, 12), mats.chrome);
1936  spire.position.set(x, h + 17.5, z);
1937  city.add(spire);
1938
1939  for (const side of [-1, 1]) {
1940    const lightStrip = new THREE.Mesh(new THREE.BoxGeometry(0.16, h * 0.82, 0.08), mats.lamp);
1941    lightStrip.position.set(x + side * (w / 2 + 0.16), h * 0.48, frontZ + 0.18);
1942    city.add(lightStrip);
1943  }
1944}
1945
1946function addPark(x, z) {
1947  const park = new THREE.Mesh(new THREE.BoxGeometry(17, 0.1, 17), new THREE.MeshStandardMaterial({ color: 0x347d42, roughness: 0.9 }));
1948  park.position.set(x, 0.07, z);
1949  park.receiveShadow = true;
1950  city.add(park);
1951  for (let i = 0; i < 2; i += 1) addTree(x + (Math.random() - 0.5) * 12, z + (Math.random() - 0.5) * 12);
1952}
1953
1954function addParkingLot(x, z) {
1955  const lot = new THREE.Group();
1956  lot.position.set(x, 0, z);
1957
1958  const base = new THREE.Mesh(new THREE.BoxGeometry(30, 0.08, 25), mats.asphalt);
1959  base.position.y = 0.055;
1960  base.receiveShadow = true;
1961  lot.add(base);
1962
1963  const curbMaterial = mats.curbStone;
1964  for (const side of [-1, 1]) {
1965    const curb = new THREE.Mesh(new THREE.BoxGeometry(30.5, 0.22, 0.45), curbMaterial);
1966    curb.position.set(0, 0.18, side * 12.7);
1967    lot.add(curb);
1968  }
1969  for (const side of [-1, 1]) {
1970    const curb = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.22, 25.4), curbMaterial);
1971    curb.position.set(side * 15.25, 0.18, 0);
1972    lot.add(curb);
1973  }
1974
1975  addParkingFence(lot, x, z);
1976
1977  for (const rowZ of [-5.2, 4.8]) {
1978    for (let i = -2; i <= 2; i += 1) {
1979      const xLine = i * 4.6;
1980      const line = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.035, 6.1), mats.roadPaint);
1981      line.position.set(xLine, 0.13, rowZ);
1982      lot.add(line);
1983    }
1984    const backLine = new THREE.Mesh(new THREE.BoxGeometry(22.2, 0.035, 0.12), mats.roadPaint);
1985    backLine.position.set(0, 0.132, rowZ - 3);
1986    lot.add(backLine);
1987  }
1988
1989  const arrow = new THREE.Mesh(new THREE.ConeGeometry(0.7, 2.1, 3), mats.roadPaint);
1990  arrow.position.set(-10.7, 0.15, 0);
1991  arrow.rotation.set(Math.PI / 2, 0, Math.PI / 2);
1992  lot.add(arrow);
1993
1994  const booth = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.7, 2.3), mats.facadeTrim);
1995  booth.position.set(11.8, 0.9, -8.8);
1996  booth.castShadow = true;
1997  lot.add(booth);
1998
1999  const boothWindow = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.68, 0.06), mats.window);
2000  boothWindow.position.set(11.8, 1.05, -7.62);
2001  lot.add(boothWindow);
2002
2003  const barrierPost = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.16, 0.85, 8), mats.chrome);
2004  barrierPost.position.set(-12.4, 0.47, 12.9);
2005  lot.add(barrierPost);
2006
2007  const gate = new THREE.Group();
2008  gate.position.set(-12.4, 0.9, 12.9);
2009  const barrier = new THREE.Mesh(new THREE.BoxGeometry(6.8, 0.16, 0.18), mats.roadPaint);
2010  barrier.position.set(3.4, 0, 0);
2011  gate.add(barrier);
2012  const barrierStripe = new THREE.Mesh(new THREE.BoxGeometry(6.85, 0.17, 0.045), mats.cone);
2013  barrierStripe.position.set(3.4, 0.015, 0.095);
2014  gate.add(barrierStripe);
2015  lot.add(gate);
2016  parkingGates.push({ x: x - 9.1, z: z + 12.9, gate, open: 0 });
2017
2018  const signPole = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.08, 2.7, 8), mats.chrome);
2019  signPole.position.set(-13, 1.35, -10.5);
2020  lot.add(signPole);
2021  const sign = new THREE.Mesh(new THREE.BoxGeometry(1.7, 1.25, 0.12), mats.darkGlass);
2022  sign.position.set(-13, 2.75, -10.5);
2023  lot.add(sign);
2024  const signFace = new THREE.Mesh(new THREE.PlaneGeometry(1.45, 0.95), new THREE.MeshBasicMaterial({ map: makeParkingSignTexture(), transparent: true }));
2025  signFace.position.set(-13, 2.75, -10.43);
2026  lot.add(signFace);
2027
2028  city.add(lot);
2029  props.push({ mesh: { position: new THREE.Vector3(x + 11.8, 0, z - 8.8) }, radius: 1.7, fixed: true });
2030  props.push({ mesh: { position: new THREE.Vector3(x - 12.4, 0, z + 12.9) }, radius: 0.8, fixed: true });
2031  props.push({ mesh: { position: new THREE.Vector3(x - 13, 0, z - 10.5) }, radius: 0.65, fixed: true });
2032}
2033
2034function addParkingFence(lot, worldX, worldZ) {
2035  const railMaterial = mats.balconyRail;
2036  const postMaterial = mats.chrome;
2037  const segments = [
2038    { x: 0, z: -13.2, w: 30.5, d: 0.16 },
2039    { x: -15.45, z: 0, w: 0.16, d: 25.8 },
2040    { x: 15.45, z: 0, w: 0.16, d: 25.8 },
2041    { x: -14.12, z: 13.2, w: 2.65, d: 0.16 },
2042    { x: 5.12, z: 13.2, w: 20.65, d: 0.16 },
2043  ];
2044
2045  for (const segment of segments) {
2046    for (const y of [0.92, 1.52]) {
2047      const rail = new THREE.Mesh(new THREE.BoxGeometry(segment.w, 0.09, segment.d), railMaterial);
2048      rail.position.set(segment.x, y, segment.z);
2049      lot.add(rail);
2050    }
2051    obstacles.push({
2052      x: worldX + segment.x,
2053      z: worldZ + segment.z,
2054      halfX: Math.max(0.18, segment.w / 2),
2055      halfZ: Math.max(0.18, segment.d / 2),
2056      label: "You hit the parking fence.",
2057    });
2058  }
2059
2060  const posts = [
2061    [-15.45, -13.2],
2062    [-10.5, -13.2],
2063    [-5.5, -13.2],
2064    [-0.5, -13.2],
2065    [4.5, -13.2],
2066    [9.5, -13.2],
2067    [15.45, -13.2],
2068    [-15.45, -8],
2069    [-15.45, -3],
2070    [-15.45, 2],
2071    [-15.45, 7],
2072    [-15.45, 13.2],
2073    [15.45, -8],
2074    [15.45, -3],
2075    [15.45, 2],
2076    [15.45, 7],
2077    [15.45, 13.2],
2078    [-12.4, 13.2],
2079    [-1.5, 13.2],
2080    [7.5, 13.2],
2081    [15.45, 13.2],
2082  ];
2083  for (const [px, pz] of posts) {
2084    const post = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.11, 1.85, 8), postMaterial);
2085    post.position.set(px, 0.93, pz);
2086    lot.add(post);
2087  }
2088}
2089
2090function addAirstrip(x, z) {
2091  const strip = new THREE.Group();
2092  strip.position.set(x, 0, z);
2093
2094  const pad = new THREE.Mesh(new THREE.BoxGeometry(116, 0.08, 35.5), mats.asphalt);
2095  pad.position.y = 0.06;
2096  strip.add(pad);
2097
2098  const runway = new THREE.Mesh(new THREE.BoxGeometry(106, 0.06, 8), mats.bridgeDeck);
2099  runway.position.set(0, 0.12, 0);
2100  strip.add(runway);
2101
2102  for (let i = -2; i <= 2; i += 1) {
2103    const dash = new THREE.Mesh(new THREE.BoxGeometry(5.2, 0.035, 0.38), mats.roadPaint);
2104    dash.position.set(i * 18, 0.17, 0);
2105    strip.add(dash);
2106  }
2107
2108  for (const side of [-1, 1]) {
2109    const edge = new THREE.Mesh(new THREE.BoxGeometry(102, 0.035, 0.12), mats.fadedPaint);
2110    edge.position.set(0, 0.17, side * 3.45);
2111    strip.add(edge);
2112  }
2113
2114  const parkingMark = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.035, 9.5), mats.roadPaint);
2115  parkingMark.position.set(20, 0.17, 11);
2116  strip.add(parkingMark);
2117  const parkingMarkBack = parkingMark.clone();
2118  parkingMarkBack.position.x = 33;
2119  strip.add(parkingMarkBack);
2120
2121  const windsockPole = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.08, 2.8, 8), mats.chrome);
2122  windsockPole.position.set(50, 1.4, -14);
2123  strip.add(windsockPole);
2124  const windsock = new THREE.Mesh(new THREE.ConeGeometry(0.28, 1.2, 10, 1, true), mats.cone);
2125  windsock.rotation.z = Math.PI / 2;
2126  windsock.position.set(50.6, 2.65, -14);
2127  strip.add(windsock);
2128
2129  city.add(strip);
2130  props.push({ mesh: { position: new THREE.Vector3(x + 50, 0, z - 14) }, radius: 0.7, fixed: true });
2131}
2132
2133function addMilitaryBase(x, z) {
2134  const base = new THREE.Group();
2135  base.position.set(x, 0, z);
2136
2137  const ground = new THREE.Mesh(new THREE.BoxGeometry(militaryBase.halfX * 2, 0.1, militaryBase.halfZ * 2), mats.bridgeDeck);
2138  ground.position.y = 0.055;
2139  ground.receiveShadow = true;
2140  base.add(ground);
2141
2142  const runway = new THREE.Mesh(new THREE.BoxGeometry(126, 0.07, 14), mats.asphalt);
2143  runway.position.set(0, 0.14, -18);
2144  base.add(runway);
2145  for (let i = -3; i <= 3; i += 1) {
2146    const dash = new THREE.Mesh(new THREE.BoxGeometry(8, 0.035, 0.42), mats.roadPaint);
2147    dash.position.set(i * 16, 0.2, -18);
2148    base.add(dash);
2149  }
2150
2151  const apron = new THREE.Mesh(new THREE.BoxGeometry(64, 0.06, 38), mats.asphalt);
2152  apron.position.set(-34, 0.14, 24);
2153  base.add(apron);
2154
2155  const hangar = new THREE.Group();
2156  const hangarBack = new THREE.Mesh(new THREE.BoxGeometry(32, 13, 2), mats.windowFrame);
2157  hangarBack.position.set(36, 6.5, 35);
2158  hangar.add(hangarBack);
2159  const hangarRoof = new THREE.Mesh(new THREE.BoxGeometry(36, 2, 30), mats.bridgeRail);
2160  hangarRoof.position.set(36, 13.3, 23);
2161  hangarRoof.rotation.x = 0.1;
2162  hangar.add(hangarRoof);
2163  const hangarFloor = new THREE.Mesh(new THREE.BoxGeometry(36, 0.18, 30), mats.asphalt);
2164  hangarFloor.position.set(36, 0.18, 23);
2165  hangar.add(hangarFloor);
2166  base.add(hangar);
2167
2168  for (const side of [-1, 1]) {
2169    const fenceA = new THREE.Mesh(new THREE.BoxGeometry(militaryBase.halfX * 2, 1.55, 0.22), mats.windowFrame);
2170    fenceA.position.set(0, 0.9, side * militaryBase.halfZ);
2171    base.add(fenceA);
2172    const fenceB = new THREE.Mesh(new THREE.BoxGeometry(0.22, 1.55, militaryBase.halfZ * 2), mats.windowFrame);
2173    fenceB.position.set(side * militaryBase.halfX, 0.9, 0);
2174    base.add(fenceB);
2175  }
2176  const gate = new THREE.Mesh(new THREE.BoxGeometry(16, 1.4, 0.28), mats.chrome);
2177  gate.position.set(0, 0.82, militaryBase.halfZ + 0.2);
2178  base.add(gate);
2179  addMilitarySecurityDetails(base);
2180
2181  for (const [tx, tz, heading] of [[-58, 42, 0.2], [58, -42, Math.PI - 0.25]]) {
2182    const tower = new THREE.Group();
2183    const legs = new THREE.Mesh(new THREE.BoxGeometry(3.8, 7, 3.8), mats.windowFrame);
2184    legs.position.set(tx, 3.5, tz);
2185    tower.add(legs);
2186    const cabin = new THREE.Mesh(new THREE.BoxGeometry(6.2, 3.0, 6.2), mats.darkGlass);
2187    cabin.position.set(tx, 8.35, tz);
2188    tower.add(cabin);
2189    const spotlight = new THREE.SpotLight(0xfff2c6, 1.2, 68, 0.42, 0.5, 1.4);
2190    spotlight.position.set(tx, 9.7, tz);
2191    const target = new THREE.Object3D();
2192    target.position.set(tx + Math.sin(heading) * 22, 0.2, tz + Math.cos(heading) * 22);
2193    spotlight.target = target;
2194    tower.add(spotlight, target);
2195    base.add(tower);
2196  }
2197
2198  for (const [tx, tz, rot] of [[-52, -45, Math.PI / 2], [48, -44, Math.PI / 2], [24, 40, -Math.PI / 3]]) {
2199    const tank = createTank();
2200    tank.position.set(tx, 0.25, tz);
2201    tank.rotation.y = rot;
2202    base.add(tank);
2203    props.push({ mesh: { position: new THREE.Vector3(x + tx, 0, z + tz) }, radius: 4.6, fixed: true });
2204  }
2205
2206  for (const [jx, jz, rot] of [[-42, 21, Math.PI / 2], [-18, 38, Math.PI / 2]]) {
2207    const jet = createLargePlane();
2208    jet.scale.setScalar(0.72);
2209    jet.position.set(jx, 0.38, jz);
2210    jet.rotation.y = rot;
2211    base.add(jet);
2212  }
2213
2214  for (let i = 0; i < 12; i += 1) {
2215    const soldier = createMilitarySoldier(i);
2216    soldier.position.set(-66 + (i % 6) * 18, 0, -55 + Math.floor(i / 6) * 106);
2217    soldier.rotation.y = i % 2 ? Math.PI : 0;
2218    base.add(soldier);
2219  }
2220
2221  city.add(base);
2222  militaryZones.push({ x, z, halfX: militaryBase.halfX, halfZ: militaryBase.halfZ });
2223}
2224
2225function addMilitarySecurityDetails(base) {
2226  const concreteMat = new THREE.MeshStandardMaterial({ color: 0x8c8c80, roughness: 0.88 });
2227  const cautionMat = new THREE.MeshBasicMaterial({ color: 0xe5c842 });
2228  for (const side of [-1, 1]) {
2229    for (let x = -66; x <= 66; x += 12) {
2230      if (Math.abs(x) < 11 && side > 0) continue;
2231      const barrier = new THREE.Mesh(new THREE.BoxGeometry(6.8, 0.82, 0.72), concreteMat);
2232      barrier.position.set(x, 0.5, side * (militaryBase.halfZ - 4.2));
2233      barrier.rotation.y = (x % 24 === 0 ? 0.06 : -0.06);
2234      base.add(barrier);
2235    }
2236  }
2237  for (const side of [-1, 1]) {
2238    for (let z = -48; z <= 48; z += 12) {
2239      const barrier = new THREE.Mesh(new THREE.BoxGeometry(0.72, 0.82, 6.8), concreteMat);
2240      barrier.position.set(side * (militaryBase.halfX - 4.2), 0.5, z);
2241      barrier.rotation.y = (z % 24 === 0 ? 0.06 : -0.06);
2242      base.add(barrier);
2243    }
2244  }
2245
2246  const guardHouse = new THREE.Group();
2247  const hut = new THREE.Mesh(new THREE.BoxGeometry(7.5, 3.2, 5.4), mats.facadeTrim);
2248  hut.position.set(-17, 1.7, militaryBase.halfZ - 7.5);
2249  guardHouse.add(hut);
2250  const roof = new THREE.Mesh(new THREE.BoxGeometry(8.3, 0.42, 6.2), mats.windowFrame);
2251  roof.position.set(-17, 3.55, militaryBase.halfZ - 7.5);
2252  guardHouse.add(roof);
2253  const window = new THREE.Mesh(new THREE.BoxGeometry(3.0, 1.05, 0.08), mats.darkGlass);
2254  window.position.set(-17, 2.08, militaryBase.halfZ - 4.75);
2255  guardHouse.add(window);
2256  base.add(guardHouse);
2257
2258  const barrierArm = new THREE.Mesh(new THREE.BoxGeometry(18, 0.16, 0.22), cautionMat);
2259  barrierArm.position.set(9, 1.42, militaryBase.halfZ - 2.2);
2260  barrierArm.rotation.z = -0.1;
2261  base.add(barrierArm);
2262  for (let i = 0; i < 6; i += 1) {
2263    const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.48, 0.18, 0.24), mats.cone);
2264    stripe.position.set(1.4 + i * 2.8, 1.43, militaryBase.halfZ - 2.18);
2265    stripe.rotation.z = -0.1;
2266    base.add(stripe);
2267  }
2268
2269  for (const [bx, bz] of [[-9, 47], [14, 47], [-32, 52], [32, 52]]) {
2270    const cone = new THREE.Mesh(new THREE.ConeGeometry(0.48, 1.35, 12), mats.cone);
2271    cone.position.set(bx, 0.72, bz);
2272    base.add(cone);
2273  }
2274}
2275
2276function createTank() {
2277  const tank = new THREE.Group();
2278  const body = new THREE.Mesh(new THREE.BoxGeometry(6.4, 1.15, 3.8), new THREE.MeshStandardMaterial({ color: 0x42513d, roughness: 0.82, metalness: 0.12 }));
2279  body.position.y = 0.85;
2280  tank.add(body);
2281  const turret = new THREE.Mesh(new THREE.BoxGeometry(2.8, 0.9, 2.3), new THREE.MeshStandardMaterial({ color: 0x536344, roughness: 0.78, metalness: 0.1 }));
2282  turret.position.set(0.4, 1.72, 0);
2283  tank.add(turret);
2284  const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.18, 5.2, 12), mats.gun);
2285  barrel.rotation.x = Math.PI / 2;
2286  barrel.position.set(0.4, 1.8, 3.1);
2287  tank.add(barrel);
2288  for (const x of [-2.2, 0, 2.2]) {
2289    for (const z of [-2.05, 2.05]) {
2290      const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.44, 0.44, 0.28, 14), mats.rubber);
2291      wheel.rotation.z = Math.PI / 2;
2292      wheel.position.set(x, 0.45, z);
2293      tank.add(wheel);
2294    }
2295  }
2296  return tank;
2297}
2298
2299function createMilitarySoldier(index) {
2300  const soldier = createPedestrian(index + 300);
2301  const camoA = new THREE.MeshStandardMaterial({ color: 0x4f6041, roughness: 0.88 });
2302  const camoB = new THREE.MeshStandardMaterial({ color: 0x2f3b2b, roughness: 0.9 });
2303  const tan = new THREE.MeshStandardMaterial({ color: 0x81704f, roughness: 0.86 });
2304  const limbs = soldier.userData.limbs;
2305
2306  if (limbs) {
2307    for (const arm of limbs.arms) arm.material = camoA;
2308    for (const forearm of limbs.forearms) forearm.material = camoB;
2309    for (const leg of limbs.legs) leg.material = camoB;
2310    for (const shin of limbs.shins) shin.material = camoB;
2311  }
2312  if (soldier.userData.torso) soldier.userData.torso.material = camoA;
2313  if (soldier.userData.shoulders) soldier.userData.shoulders.material = camoA;
2314
2315  const vest = new THREE.Mesh(new THREE.BoxGeometry(0.48, 0.62, 0.18), camoB);
2316  vest.position.set(0, 1.28, 0.18);
2317  soldier.add(vest);
2318
2319  const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.26, 14, 8), camoB);
2320  helmet.position.set(0, 2.18, 0);
2321  helmet.scale.set(1.08, 0.5, 1);
2322  soldier.add(helmet);
2323
2324  const helmetBrim = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.04, 0.18), camoB);
2325  helmetBrim.position.set(0, 2.08, 0.19);
2326  soldier.add(helmetBrim);
2327
2328  for (let i = 0; i < 5; i += 1) {
2329    const patch = new THREE.Mesh(new THREE.BoxGeometry(0.11, 0.045, 0.025), i % 2 ? tan : camoB);
2330    patch.position.set(-0.22 + i * 0.11, 1.44 + (i % 2) * 0.12, 0.285);
2331    patch.rotation.z = (i - 2) * 0.08;
2332    soldier.add(patch);
2333  }
2334
2335  const rifle = new THREE.Group();
2336  const rifleBody = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.12, 0.88), mats.gun);
2337  rifleBody.position.set(0, 0, 0.14);
2338  rifle.add(rifleBody);
2339  const rifleBarrel = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.82, 8), mats.gun);
2340  rifleBarrel.rotation.x = Math.PI / 2;
2341  rifleBarrel.position.set(0, 0.02, 0.88);
2342  rifle.add(rifleBarrel);
2343  rifle.position.set(0.28, 1.2, 0.36);
2344  rifle.rotation.x = -0.12;
2345  soldier.add(rifle);
2346
2347  if (limbs) {
2348    limbs.arms[0].rotation.x = -1.05;
2349    limbs.arms[0].rotation.z = -0.22;
2350    limbs.arms[1].rotation.x = -1.0;
2351    limbs.arms[1].rotation.z = 0.2;
2352    limbs.forearms[0].rotation.x = -0.28;
2353    limbs.forearms[1].rotation.x = -0.28;
2354  }
2355  return soldier;
2356}
2357
2358function addWeaponShop(x, z) {
2359  const shop = new THREE.Group();
2360  shop.position.set(x, 0, z);
2361
2362  const floor = new THREE.Mesh(new THREE.BoxGeometry(22, 0.12, 18), mats.asphalt);
2363  floor.position.y = 0.06;
2364  shop.add(floor);
2365
2366  const facadeMat = new THREE.MeshStandardMaterial({ color: 0x2a3034, roughness: 0.76, metalness: 0.08 });
2367  const storeBody = new THREE.Mesh(createBuildingBodyGeometry(16, 6.4, 12, "corner-market"), facadeMat);
2368  storeBody.position.set(0, 3.2, -1.2);
2369  storeBody.castShadow = true;
2370  shop.add(storeBody);
2371
2372  const frontGlass = new THREE.Mesh(new THREE.BoxGeometry(12.4, 2.2, 0.12), mats.darkGlass);
2373  frontGlass.position.set(0, 2.05, 4.92);
2374  shop.add(frontGlass);
2375
2376  const door = new THREE.Mesh(new THREE.BoxGeometry(2.2, 2.7, 0.16), mats.glass);
2377  door.position.set(0, 1.45, 5.05);
2378  shop.add(door);
2379
2380  const awning = new THREE.Mesh(new THREE.BoxGeometry(17.4, 0.34, 1.25), mats.carDark);
2381  awning.position.set(0, 3.45, 5.55);
2382  shop.add(awning);
2383
2384  const sign = new THREE.Mesh(new THREE.PlaneGeometry(9.2, 2.0), new THREE.MeshBasicMaterial({ map: makeWeaponShopSignTexture(), transparent: true }));
2385  sign.position.set(0, 5.0, 5.68);
2386  shop.add(sign);
2387
2388  const counter = new THREE.Mesh(new THREE.BoxGeometry(8.4, 0.95, 1.7), mats.facadeTrim);
2389  counter.position.set(0, 1.0, 2.4);
2390  shop.add(counter);
2391
2392  const wallRack = new THREE.Mesh(new THREE.BoxGeometry(12.2, 3.2, 0.22), mats.windowFrame);
2393  wallRack.position.set(0, 2.65, -6.65);
2394  shop.add(wallRack);
2395
2396  for (let row = 0; row < 3; row += 1) {
2397    const shelf = new THREE.Mesh(new THREE.BoxGeometry(11.2, 0.12, 0.36), mats.chrome);
2398    shelf.position.set(0, 1.55 + row * 0.88, -6.35);
2399    shop.add(shelf);
2400    for (let i = -4; i <= 4; i += 2) {
2401      const gun = new THREE.Mesh(new THREE.BoxGeometry(1.25, 0.12, 0.28), mats.gun);
2402      gun.position.set(i * 0.62, 1.77 + row * 0.88, -6.08);
2403      gun.rotation.z = (i % 4) * 0.05;
2404      shop.add(gun);
2405      const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.7, 8), mats.gun);
2406      barrel.rotation.z = Math.PI / 2;
2407      barrel.position.set(i * 0.62 + 0.72, 1.77 + row * 0.88, -6.08);
2408      shop.add(barrel);
2409    }
2410  }
2411
2412  for (const sx of [-6.8, 6.8]) {
2413    const bollard = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.22, 1.15, 10), mats.cone);
2414    bollard.position.set(sx, 0.6, 6.25);
2415    shop.add(bollard);
2416  }
2417
2418  const neon = new THREE.Mesh(new THREE.BoxGeometry(14.8, 0.12, 0.12), mats.muzzle);
2419  neon.position.set(0, 4.03, 5.72);
2420  shop.add(neon);
2421
2422  city.add(shop);
2423  weaponBenches.push({ x, z, interactionX: x, interactionZ: z + 7.2, level: 1, type: "shop" });
2424  buildings.push({
2425    mesh: { position: new THREE.Vector3(x, 0, z - 1.2) },
2426    halfX: 8.8,
2427    halfZ: 6.7,
2428    doorX: x,
2429    doorZ: z + 5.2,
2430    doorHalfX: 1.8,
2431    lobbyBackZ: z + 3.7,
2432  });
2433}
2434
2435function makeWeaponShopSignTexture() {
2436  const labelCanvas = document.createElement("canvas");
2437  labelCanvas.width = 512;
2438  labelCanvas.height = 192;
2439  const ctx = labelCanvas.getContext("2d");
2440  ctx.fillStyle = "#111317";
2441  ctx.fillRect(0, 0, 512, 192);
2442  ctx.fillStyle = "#8d1f1f";
2443  ctx.fillRect(0, 0, 512, 38);
2444  ctx.fillRect(0, 154, 512, 38);
2445  ctx.strokeStyle = "#f6f0e3";
2446  ctx.lineWidth = 9;
2447  ctx.strokeRect(12, 12, 488, 168);
2448  ctx.fillStyle = "#f6f0e3";
2449  ctx.font = "900 50px sans-serif";
2450  ctx.textAlign = "center";
2451  ctx.textBaseline = "middle";
2452  ctx.fillText("WEAPON SHOP", 256, 84);
2453  ctx.font = "800 24px sans-serif";
2454  ctx.fillText("UPGRADES  -  AMMO  -  GEAR", 256, 126);
2455  const texture = new THREE.CanvasTexture(labelCanvas);
2456  texture.colorSpace = THREE.SRGBColorSpace;
2457  return texture;
2458}
2459
2460function makeParkingSignTexture() {
2461  const labelCanvas = document.createElement("canvas");
2462  labelCanvas.width = 128;
2463  labelCanvas.height = 96;
2464  const ctx = labelCanvas.getContext("2d");
2465  ctx.fillStyle = "#145db8";
2466  ctx.fillRect(0, 0, 128, 96);
2467  ctx.strokeStyle = "#f7f7f2";
2468  ctx.lineWidth = 7;
2469  ctx.strokeRect(6, 6, 116, 84);
2470  ctx.fillStyle = "#ffffff";
2471  ctx.font = "bold 68px sans-serif";
2472  ctx.textAlign = "center";
2473  ctx.textBaseline = "middle";
2474  ctx.fillText("P", 64, 51);
2475  const texture = new THREE.CanvasTexture(labelCanvas);
2476  texture.colorSpace = THREE.SRGBColorSpace;
2477  return texture;
2478}
2479
2480function makeHospitalSignTexture() {
2481  const labelCanvas = document.createElement("canvas");
2482  labelCanvas.width = 192;
2483  labelCanvas.height = 96;
2484  const ctx = labelCanvas.getContext("2d");
2485  ctx.fillStyle = "#ffffff";
2486  ctx.fillRect(0, 0, 192, 96);
2487  ctx.strokeStyle = "#d7262f";
2488  ctx.lineWidth = 8;
2489  ctx.strokeRect(6, 6, 180, 84);
2490  ctx.fillStyle = "#d7262f";
2491  ctx.font = "bold 54px sans-serif";
2492  ctx.textAlign = "center";
2493  ctx.textBaseline = "middle";
2494  ctx.fillText("HOSP", 96, 52);
2495  const texture = new THREE.CanvasTexture(labelCanvas);
2496  texture.colorSpace = THREE.SRGBColorSpace;
2497  return texture;
2498}
2499
2500function makeTowerSignTexture() {
2501  const labelCanvas = document.createElement("canvas");
2502  labelCanvas.width = 512;
2503  labelCanvas.height = 128;
2504  const ctx = labelCanvas.getContext("2d");
2505  ctx.fillStyle = "rgba(8, 14, 18, 0.9)";
2506  ctx.fillRect(0, 0, 512, 128);
2507  ctx.strokeStyle = "#d7dde2";
2508  ctx.lineWidth = 8;
2509  ctx.strokeRect(8, 8, 496, 112);
2510  ctx.fillStyle = "#f6f0e3";
2511  ctx.font = "bold 48px sans-serif";
2512  ctx.textAlign = "center";
2513  ctx.textBaseline = "middle";
2514  ctx.fillText("SKY TOWER", 256, 66);
2515  const texture = new THREE.CanvasTexture(labelCanvas);
2516  texture.colorSpace = THREE.SRGBColorSpace;
2517  return texture;
2518}
2519
2520function addBalconies(x, z, w, d, h, face) {
2521  const levels = Math.min(3, Math.max(1, Math.floor(h / 7)));
2522  const columns = Math.max(1, Math.floor(w / 5));
2523  for (let row = 0; row < levels; row += 1) {
2524    const y = 3.2 + row * 3.4;
2525    if (y > h - 1.2) continue;
2526    for (let col = 0; col < columns; col += 1) {
2527      if (Math.random() < 0.18) continue;
2528      const bx = x - w / 2 + 2.2 + col * ((w - 4.4) / Math.max(1, columns - 1));
2529      const bz = z + face * (d / 2 + 0.48);
2530      const floor = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.13, 0.9), mats.balconyFloor);
2531      floor.position.set(bx, y, bz);
2532      floor.castShadow = true;
2533      city.add(floor);
2534
2535      const frontRail = new THREE.Mesh(new THREE.BoxGeometry(2.05, 0.08, 0.06), mats.balconyRail);
2536      frontRail.position.set(bx, y + 0.55, z + face * (d / 2 + 0.95));
2537      city.add(frontRail);
2538
2539      for (const sx of [-0.95, 0, 0.95]) {
2540        const post = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.55, 0.045), mats.balconyRail);
2541        post.position.set(bx + sx, y + 0.32, z + face * (d / 2 + 0.95));
2542        city.add(post);
2543      }
2544
2545      for (const sx of [-1.05, 1.05]) {
2546        const sideRail = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.08, 0.75), mats.balconyRail);
2547        sideRail.position.set(bx + sx, y + 0.55, z + face * (d / 2 + 0.58));
2548        city.add(sideRail);
2549      }
2550    }
2551  }
2552}
2553
2554function addTree(x, z) {
2555  addTreeGrass(x, z);
2556
2557  const trunkHeight = 2.2 + Math.random() * 1.1;
2558  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.42, trunkHeight, 9), mats.bark);
2559  trunk.position.set(x, trunkHeight / 2, z);
2560  trunk.castShadow = true;
2561  city.add(trunk);
2562
2563  const canopyHeight = trunkHeight + 0.8;
2564  const crown = new THREE.Group();
2565  const crownParts = [
2566    { px: 0, py: 0.6, pz: 0, s: 1.7 },
2567    { px: -0.85, py: 0.15, pz: 0.25, s: 1.2 },
2568    { px: 0.85, py: 0.08, pz: -0.15, s: 1.25 },
2569  ];
2570  for (const part of crownParts) {
2571    const leaf = new THREE.Mesh(new THREE.SphereGeometry(part.s, 8, 6), Math.random() < 0.55 ? mats.leafDark : mats.leafLight);
2572    leaf.position.set(part.px, part.py, part.pz);
2573    leaf.scale.y = 0.82 + Math.random() * 0.28;
2574    leaf.castShadow = true;
2575    crown.add(leaf);
2576  }
2577  crown.position.set(x, canopyHeight, z);
2578  city.add(crown);
2579
2580  const lowerBranches = new THREE.Mesh(new THREE.ConeGeometry(1.45, 1.6, 8), mats.leafDark);
2581  lowerBranches.position.set(x, trunkHeight + 0.5, z);
2582  lowerBranches.castShadow = true;
2583  city.add(lowerBranches);
2584  props.push({ mesh: trunk, radius: 1.45 });
2585}
2586
2587function addTreeGrass(x, z) {
2588  const soil = new THREE.Mesh(new THREE.CircleGeometry(2.6 + Math.random() * 0.9, 18), mats.dirt);
2589  soil.rotation.x = -Math.PI / 2;
2590  soil.position.set(x, 0.09, z);
2591  city.add(soil);
2592
2593  for (let i = 0; i < 4; i += 1) {
2594    const blade = new THREE.Mesh(new THREE.PlaneGeometry(0.18, 0.7 + Math.random() * 0.5), mats.grassBlade);
2595    const angle = Math.random() * Math.PI * 2;
2596    const radius = 0.6 + Math.random() * 2.1;
2597    blade.position.set(x + Math.cos(angle) * radius, 0.28, z + Math.sin(angle) * radius);
2598    blade.rotation.y = angle;
2599    blade.rotation.z = (Math.random() - 0.5) * 0.45;
2600    city.add(blade);
2601  }
2602}
2603
2604function addProp() {
2605  const position = randomRoadPoint();
2606  const cone = new THREE.Mesh(new THREE.ConeGeometry(0.55, 1.35, 12), mats.cone);
2607  cone.position.set(position.x, 0.7, position.z);
2608  cone.castShadow = true;
2609  city.add(cone);
2610  props.push({ mesh: cone, radius: 1.1 });
2611}
2612
2613function addPickup(index) {
2614  const position = randomRoadPoint();
2615  const type = index % 4 === 0 ? "mission" : "cash";
2616  const mesh =
2617    type === "mission"
2618      ? new THREE.Mesh(new THREE.TorusGeometry(1.3, 0.16, 12, 36), mats.marker)
2619      : new THREE.Mesh(new THREE.CylinderGeometry(0.8, 0.8, 0.18, 28), mats.coin);
2620  mesh.position.set(position.x, 1.1, position.z);
2621  mesh.rotation.x = Math.PI / 2;
2622  scene.add(mesh);
2623  pickups.push({ mesh, type, taken: false });
2624}
2625
2626function setBrakeLights(on) {
2627  for (const light of brakeLights) {
2628    light.material.color.setHex(on ? 0xff1635 : 0x7c1826);
2629    light.material.opacity = 1;
2630  }
2631}
2632
2633function randomRoadPoint() {
2634  const onVertical = Math.random() < 0.5;
2635  const road = Math.round((Math.random() * 2 - 1) * (worldSize / blockSize)) * blockSize;
2636  const along = (Math.random() * 2 - 1) * (worldSize - 12);
2637  return onVertical ? { x: road + (Math.random() - 0.5) * 5, z: along } : { x: along, z: road + (Math.random() - 0.5) * 5 };
2638}
2639
2640function createCar(options = {}) {
2641  const paint = options.paint || mats.car;
2642  const registerParts = options.registerParts !== false;
2643  const lights = options.lights !== false;
2644  const group = new THREE.Group();
2645
2646  const body = new THREE.Mesh(
2647    createTaperedBox({
2648      bottomWidth: 2.28,
2649      topWidth: 1.88,
2650      bottomDepth: 4.24,
2651      topDepth: 3.52,
2652      height: 0.68,
2653    }),
2654    paint,
2655  );
2656  body.position.y = 0.24;
2657  body.castShadow = true;
2658  group.add(body);
2659
2660  const cabin = new THREE.Mesh(
2661    createCabinGeometry({
2662      bottomWidth: 1.52,
2663      topWidth: 1.02,
2664      bottomDepth: 1.55,
2665      topDepth: 0.84,
2666      height: 0.7,
2667      topShiftZ: -0.18,
2668    }),
2669    mats.glass,
2670  );
2671  cabin.position.set(0, 0.7, -0.42);
2672  cabin.castShadow = true;
2673  group.add(cabin);
2674
2675  const hood = new THREE.Mesh(
2676    createWedgeGeometry({
2677      width: 1.7,
2678      depth: 1.34,
2679      low: 0.08,
2680      high: 0.3,
2681      slopeToFront: true,
2682    }),
2683    paint,
2684  );
2685  hood.position.set(0, 0.62, 1.24);
2686  hood.castShadow = true;
2687  group.add(hood);
2688
2689  const trunk = new THREE.Mesh(
2690    createWedgeGeometry({
2691      width: 1.76,
2692      depth: 0.98,
2693      low: 0.1,
2694      high: 0.28,
2695      slopeToFront: false,
2696    }),
2697    paint,
2698  );
2699  trunk.position.set(0, 0.61, -1.54);
2700  trunk.castShadow = true;
2701  group.add(trunk);
2702
2703  const frontBumper = new THREE.Mesh(new THREE.CapsuleGeometry(0.12, 1.8, 4, 12), mats.chrome);
2704  frontBumper.position.set(0, 0.3, 2.1);
2705  frontBumper.rotation.z = Math.PI / 2;
2706  group.add(frontBumper);
2707
2708  const rearBumper = new THREE.Mesh(new THREE.CapsuleGeometry(0.12, 1.8, 4, 12), mats.chrome);
2709  rearBumper.position.set(0, 0.3, -2.1);
2710  rearBumper.rotation.z = Math.PI / 2;
2711  group.add(rearBumper);
2712
2713  const grille = new THREE.Mesh(new THREE.BoxGeometry(0.98, 0.24, 0.045), mats.carDark);
2714  grille.position.set(0, 0.49, 2.18);
2715  group.add(grille);
2716  for (const x of [-0.32, -0.16, 0, 0.16, 0.32]) {
2717    const grilleBar = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.25, 0.055), mats.chrome);
2718    grilleBar.position.set(x, 0.49, 2.205);
2719    group.add(grilleBar);
2720  }
2721
2722  const roof = new THREE.Mesh(
2723    createTaperedBox({
2724      bottomWidth: 1.22,
2725      topWidth: 1.05,
2726      bottomDepth: 0.98,
2727      topDepth: 0.78,
2728      height: 0.08,
2729    }),
2730    paint,
2731  );
2732  roof.position.set(0, 1.42, -0.58);
2733  roof.castShadow = true;
2734  group.add(roof);
2735
2736  const windshield = new THREE.Mesh(createGlassPanel(1.24, 0.72), mats.glass);
2737  windshield.position.set(0, 1.0, 0.18);
2738  windshield.rotation.x = -0.5;
2739  group.add(windshield);
2740
2741  const rearWindow = new THREE.Mesh(createGlassPanel(1.12, 0.62), mats.glass);
2742  rearWindow.position.set(0, 1.0, -1.02);
2743  rearWindow.rotation.x = 0.5;
2744  group.add(rearWindow);
2745
2746  const sunroof = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.012, 0.46), mats.glass);
2747  sunroof.position.set(0, 1.48, -0.56);
2748  group.add(sunroof);
2749
2750  for (const x of [-0.63, 0.63]) {
2751    const rail = new THREE.Mesh(new THREE.BoxGeometry(0.032, 0.032, 
27511.12), mats.chrome);
2752    rail.position.set(x, 1.49, -0.56);
2753    group.add(rail);
2754  }
2755
2756  for (const z of [-0.84, -0.28]) {
2757    const seal = new THREE.Mesh(new THREE.BoxGeometry(0.74, 0.018, 0.026), mats.carDark);
2758    seal.position.set(0, 1.495, z);
2759    group.add(seal);
2760  }
2761  for (const x of [-0.37, 0.37]) {
2762    const seal = new THREE.Mesh(new THREE.BoxGeometry(0.026, 0.018, 0.56), mats.carDark);
2763    seal.position.set(x, 1.495, -0.56);
2764    group.add(seal);
2765  }
2766  sunroof.position.y = 1.492;
2767
2768  const frontSeal = new THREE.Mesh(new THREE.BoxGeometry(1.28, 0.04, 0.045), mats.carDark);
2769  frontSeal.position.set(0, 1.18, -0.02);
2770  group.add(frontSeal);
2771
2772  const rearSeal = new THREE.Mesh(new THREE.BoxGeometry(1.18, 0.04, 0.045), mats.carDark);
2773  rearSeal.position.set(0, 1.18, -1.03);
2774  group.add(rearSeal);
2775
2776  for (const x of [-0.42, 0.42]) {
2777    const hoodLine = new THREE.Mesh(new THREE.BoxGeometry(0.022, 0.018, 1.08), mats.carDark);
2778    hoodLine.position.set(x, 0.91, 1.18);
2779    hoodLine.rotation.x = -0.08;
2780    group.add(hoodLine);
2781  }
2782  const hoodCenterCrease = new THREE.Mesh(new THREE.BoxGeometry(0.018, 0.02, 1.12), mats.chrome);
2783  hoodCenterCrease.position.set(0, 0.93, 1.18);
2784  hoodCenterCrease.rotation.x = -0.08;
2785  group.add(hoodCenterCrease);
2786
2787  for (const x of [-1.08, 1.08]) {
2788    const doorLine = new THREE.Mesh(new THREE.BoxGeometry(0.028, 0.38, 0.9), mats.carDark);
2789    doorLine.position.set(x, 0.7, -0.34);
2790    group.add(doorLine);
2791
2792    const rearDoorLine = new THREE.Mesh(new THREE.BoxGeometry(0.026, 0.34, 0.62), mats.carDark);
2793    rearDoorLine.position.set(x, 0.68, -0.98);
2794    group.add(rearDoorLine);
2795
2796    for (const z of [0.06, -0.78]) {
2797      const handle = new THREE.Mesh(new THREE.CapsuleGeometry(0.025, 0.18, 4, 8), mats.chrome);
2798      handle.position.set(x * 1.01, 0.78, z);
2799      handle.rotation.z = Math.PI / 2;
2800      group.add(handle);
2801    }
2802
2803    const sideWindow = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.34, 0.84), mats.glass);
2804    sideWindow.position.set(x * 0.82, 1.0, -0.45);
2805    sideWindow.rotation.z = x > 0 ? -0.03 : 0.03;
2806    group.add(sideWindow);
2807
2808    const lowerSkirt = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.08, 2.9), mats.carDark);
2809    lowerSkirt.position.set(x, 0.18, -0.08);
2810    group.add(lowerSkirt);
2811  }
2812
2813  for (const x of [-1.18, 1.18]) {
2814    for (const z of [-1.2, 1.25]) {
2815      const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.39, 0.39, 0.34, 24), mats.rubber);
2816      wheel.rotation.z = Math.PI / 2;
2817      wheel.position.set(x, 0.3, z);
2818      wheel.castShadow = true;
2819      group.add(wheel);
2820      if (registerParts) {
2821        wheels.push(wheel);
2822        if (z > 0) frontWheelParts.push(wheel);
2823      }
2824
2825      const rim = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.36, 18), mats.chrome);
2826      rim.rotation.z = Math.PI / 2;
2827      rim.position.copy(wheel.position);
2828      group.add(rim);
2829      if (registerParts && z > 0) frontWheelParts.push(rim);
2830
2831      const rimDetail = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 0.38, 12), mats.carDark);
2832      rimDetail.rotation.z = Math.PI / 2;
2833      rimDetail.position.copy(wheel.position);
2834      group.add(rimDetail);
2835      if (registerParts && z > 0) frontWheelParts.push(rimDetail);
2836
2837      const arch = new THREE.Mesh(new THREE.TorusGeometry(0.45, 0.035, 8, 28), mats.carDark);
2838      arch.position.set(Math.sign(x) * 1.11, 0.43, z);
2839      arch.rotation.y = Math.PI / 2;
2840      arch.scale.y = 0.74;
2841      group.add(arch);
2842    }
2843  }
2844
2845  for (const x of [-0.58, 0.58]) {
2846    const headlightMaterial = new THREE.MeshBasicMaterial({ color: 0xfff4cb });
2847    const headlight = new THREE.Mesh(new THREE.SphereGeometry(0.18, 16, 8), headlightMaterial);
2848    headlight.position.set(x, 0.55, 2.2);
2849    headlight.scale.set(1.45, 0.45, 0.18);
2850    group.add(headlight);
2851
2852    if (lights) {
2853      const spot = new THREE.SpotLight(0xfff2c6, 0, 65, 0.35, 0.48, 1.35);
2854      spot.position.set(x, 0.6, 2.26);
2855      spot.castShadow = false;
2856      const target = new THREE.Object3D();
2857      target.position.set(x * 0.45, 0.28, 24);
2858      spot.target = target;
2859      group.add(spot);
2860      group.add(target);
2861
2862      const beam = new THREE.Mesh(
2863        new THREE.ConeGeometry(3.1, 22, 24, 1, true),
2864        new THREE.MeshBasicMaterial({ color: 0xfff1b8, transparent: true, op
2864acity: 0, depthWrite: false, side: THREE.DoubleSide }),
2865      );
2866      beam.position.set(x * 0.45, 0.38, 12.2);
2867      beam.rotation.x = Math.PI / 2;
2868      beam.scale.x = 0.72;
2869      group.add(beam);
2870      if (registerParts) carHeadlights.push({ bulb: headlight, bulbMaterial: headlightMaterial, spot, beam });
2871    }
2872
2873    const headlightTrim = new THREE.Mesh(new THREE.TorusGeometry(0.2, 0.012, 6, 18), mats.chrome);
2874    headlightTrim.position.copy(headlight.position);
2875    headlightTrim.rotation.x = Math.PI / 2;
2876    headlightTrim.scale.set(1.35, 0.55, 1);
2877    group.add(headlightTrim);
2878  }
2879
2880  for (const x of [-1.28, 1.28]) {
2881    const side = Math.sign(x);
2882    const mirrorStem = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.055, 0.055), mats.carDark);
2883    mirrorStem.position.set(side * 1.13, 0.9, 0.42);
2884    mirrorStem.rotation.y = side * 0.16;
2885    group.add(mirrorStem);
2886
2887    const mirror = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.18, 0.08), mats.chrome);
2888    mirror.position.set(side * 1.36, 0.92, 0.42);
2889    mirror.rotation.y = side * 0.34;
2890    mirror.castShadow = true;
2891    group.add(mirror);
2892
2893    const mirrorGlass = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.13, 0.012), mats.glass);
2894    mirrorGlass.position.set(side * 1.37, 0.92, 0.36);
2895    mirrorGlass.rotation.y = mirror.rotation.y;
2896    group.add(mirrorGlass);
2897  }
2898
2899  const rearViewStem = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.2, 0.045), mats.carDark);
2900  rearViewStem.position.set(0, 1.08, 0.18);
2901  group.add(rearViewStem);
2902
2903  const rearViewMirror = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.16, 0.045), mats.carDark);
2904  rearViewMirror.position.set(0, 0.98, 0.1);
2905  rearViewMirror.rotation.x = -0.18;
2906  group.add(rearViewMirror);
2907
2908  for (const x of [-0.58, 0.58]) {
2909    const tailLight = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.13, 0.07), new THREE.MeshBasicMaterial({ color: 0x7c1826 }));
2910    tailLight.position.set(x, 0.48, -2.15);
2911    group.add(tailLight);
2912    if (registerParts) brakeLights.push(tailLight);
2913  }
2914
2915  for (const x of [-0.34, 0.34]) {
2916    const exhaust = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, 0.34, 14), mats.chrome);
2917    exhaust.position.set(x, 0.23, -2.28);
2918    exhaust.rotation.x = Math.PI / 2;
2919    group.add(exhaust);
2920    if (registerParts) exhaustPorts.push(new THREE.Vector3(x, 0.23, -2.48));
2921  }
2922
2923  const plate = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.22, 0.04), new THREE.MeshBasicMaterial({ color: 0xe9e2cc }));
2924  plate.position.set(0, 0.36, -2.18);
2925  group.add(plate);
2926  return group;
2927}
2928
2929function createGlassPanel(width, height) {
2930  const geometry = new THREE.PlaneGeometry(width, height);
2931  geometry.computeVertexNormals();
2932  return geometry;
2933}
2934
2935function createLargePlane() {
2936  const group = new THREE.Group();
2937  const jetBody = new THREE.MeshStandardMaterial({ color: 0x87949a, roughness: 0.48, metalness: 0.28 });
2938  const jetPanel = new THREE.MeshStandardMaterial({ color: 0x657279, roughness: 0.56, metalness: 0.22, side: THREE.DoubleSide });
2939  const darkPanel = new THREE.MeshStandardMaterial({ color: 0x283238, roughness: 0.52, metalness: 0.2, side: THREE.DoubleSide });
2940  const missileMat = new THREE.MeshStandardMaterial({ color: 0xe8ece8, roughness: 0.45, metalness: 0.14 });
2941
2942  const addFlatPanel = (points, material) => {
2943    const geometry = new THREE.BufferGeometry();
2944    geometry.setAttribute("position", new THREE.Float32BufferAttribute(points.flatMap((p) => [p[0], p[1], p[2]]), 3));
2945    const indices = [];
2946    for (let i = 1; i < points.length - 1; i += 1) indices.push(0, i, i + 1);
2947    geometry.setIndex(indices);
2948    geometry.computeVertexNormals();
2949    const mesh = new THREE.Mesh(geometry, material);
2950    mesh.castShadow = true;
2951    mesh.receiveShadow = true;
2952    group.add(mesh);
2953    return mesh;
2954  };
2955
2956  const fuselage = new THREE.Mesh(new THREE.CylinderGeometry(0.58, 0.82, 9.6, 20), jetBody);
2957  fuselage.rotation.x = Math.PI / 2;
2958  fuselage.position.y = 1.22;
2959  fuselage.castShadow = true;
2960  group.add(fuselage);
2961
2962  const nose = new THREE.Mesh(new THREE.ConeGeometry(0.6, 2.45, 20), jetBody);
2963  nose.rotation.x = Math.PI / 2;
2964  nose.position.set(0, 1.22, 6.02);
2965  nose.castShadow = true;
2966  group.add(nose);
2967
2968  const noseProbe = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.03, 0.9, 8), mats.gun);
2969  noseProbe.rotation.x = Math.PI / 2;
2970  noseProbe.position.set(0, 1.22, 7.68);
2971  group.add(noseProbe);
2972
2973  const tailCone = new THREE.Mesh(new THREE.ConeGeometry(0.86, 1.35, 20), jetBody);
2974  tailCone.rotation.x = -Math.PI / 2;
2975  tailCone.position.set(0, 1.22, -5.18);
2976  tailCone.castShadow = true;
2977  group.add(tailCone);
2978
2979  const nozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.52, 0.64, 0.74, 18), mats.gun);
2980  nozzle.rotation.x = Math.PI / 2;
2981  nozzle.position.set(0, 1.22, -5.96);
2982  group.add(nozzle);
2983
2984  const afterburner = new THREE.Mesh(new THREE.TorusGeometry(0.48, 0.055, 8, 24), mats.chrome);
2985  afterburner.position.set(0, 1.22, -6.34);
2986  group.add(afterburner);
2987
2988  const cockpit = new THREE.Mesh(new THREE.SphereGeometry(0.62, 18, 10), mats.darkGlass);
2989  cockpit.position.set(0, 1.72, 3.65);
2990  cockpit.scale.set(0.82, 0.48, 1.85);
2991  cockpit.castShadow = true;
2992  group.add(cockpit);
2993
2994  const canopyFrame = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.08, 1.85), mats.carDark);
2995  canopyFrame.position.set(0, 1.95, 3.46);
2996  canopyFrame.rotation.x = -0.12;
2997  group.add(canopyFrame);
2998
2999  const intake = new THREE.Mesh(new THREE.BoxGeometry(1.05, 0.34, 1.15), darkPanel);
3000  intake.position.set(0, 0.72, 2.0);
3001  intake.rotation.x = -0.12;
3002  intake.castShadow = true;
3003  group.add(intake);
3004
3005  addFlatPanel([
3006    [-0.48, 1.16, 1.1],
3007    [-5.9, 1.1, -0.48],
3008    [-4.95, 1.08, -1.52],
3009    [-0.74, 1.14, -0.82],
3010  ], jetPanel);
3011  addFlatPanel([
3012    [0.48, 1.16, 1.1],
3013    [5.9, 1.1, -0.48],
3014    [4.95, 1.08, -1.52],
3015    [0.74, 1.14, -0.82],
3016  ], jetPanel);
3017
3018  addFlatPanel([
3019    [-0.34, 1.42, -3.95],
3020    [-2.65, 1.46, -4.82],
3021    [-2.12, 1.4, -5.35],
3022    [-0.44, 1.38, -4.82],
3023  ], jetPanel);
3024  addFlatPanel([
3025    [0.34, 1.42, -3.95],
3026    [2.65, 1.46, -4.82],
3027    [2.12, 1.4, -5.35],
3028    [0.44, 1.38, -4.82],
3029  ], jetPanel);
3030
3031  addFlatPanel([
3032    [0, 1.66, -3.95],
3033    [0, 3.58, -4.92],
3034    [0, 1.6, -5.8],
3035  ], jetPanel);
3036
3037  for (const x of [-0.52, 0.52]) {
3038    const vent = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.28, 1.2), darkPanel);
3039    vent.position.set(x, 1.18, -3.05);
3040    vent.rotation.z = -Math.sign(x) * 0.12;
3041    group.add(vent);
3042  }
3043
3044  for (const x of [-4.35, 4.35]) {
3045    const gunPod = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.22, 
30451.1), mats.gun);
3046    gunPod.position.set(x, 0.94, 0.18);
3047    group.add(gunPod);
3048
3049    const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, 1.15, 10), mats.gun);
3050    barrel.rotation.x = Math.PI / 2;
3051    barrel.position.set(x, 0.94, 0.92);
3052    group.add(barrel);
3053  }
3054
3055  for (const x of [-3.35, 3.35]) {
3056    const missile = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 1.3, 12), missileMat);
3057    missile.rotation.x = Math.PI / 2;
3058    missile.position.set(x, 0.88, -0.72);
3059    group.add(missile);
3060
3061    const missileTip = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.32, 12), missileMat);
3062    missileTip.rotation.x = Math.PI / 2;
3063    missileTip.position.set(x, 0.88, -0.0);
3064    group.add(missileTip);
3065  }
3066
3067  for (const x of [-0.85, 0.85]) {
3068    const mainWheel = new THREE.Mesh(new THREE.CylinderGeometry(0.34, 0.34, 0.22, 16), mats.rubber);
3069    mainWheel.rotation.z = Math.PI / 2;
3070    mainWheel.position.set(x, 0.25, -0.35);
3071    group.add(mainWheel);
3072  }
3073  const noseWheel = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.25, 0.18, 14), mats.rubber);
3074  noseWheel.rotation.z = Math.PI / 2;
3075  noseWheel.position.set(0, 0.22, 3.25);
3076  group.add(noseWheel);
3077
3078  const centerStripe = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.08, 7.3), mats.car);
3079  centerStripe.position.set(0, 1.83, -0.65);
3080  centerStripe.rotation.x = 0.02;
3081  group.add(centerStripe);
3082
3083  return group;
3084}
3085
3086function createCharacter() {
3087  const group = new THREE.Group();
3088
3089  const hips = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.22, 0.28), mats.pants);
3090  hips.position.y = 0.92;
3091  hips.castShadow = true;
3092  group.add(hips);
3093
3094  const belt = new THREE.Mesh(new THREE.BoxGeometry(0.58, 0.055, 0.31), mats.carDark);
3095  belt.position.set(0, 1.04, 0.005);
3096  group.add(belt);
3097
3098  const torso = new THREE.Mesh(
3099    createTaperedBox({
3100      bottomWidth: 0.58,
3101      topWidth: 0.78,
3102      bottomDepth: 0.32,
3103      topDepth: 0.38,
3104      height: 0.72,
3105    }),
3106    mats.shirt,
3107  );
3108  torso.position.y = 1.0;
3109  torso.castShadow = true;
3110  group.add(torso);
3111
3112  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.11, 0.13, 0.18, 10), mats.skin);
3113  neck.position.y = 1.76;
3114  group.add(neck);
3115
3116  const collar = new THREE.Mesh(new THREE.BoxGeometry(0.46, 0.07, 0.06), mats.characterDetail);
3117  collar.position.set(0, 1.75, 0.19);
3118  group.add(collar);
3119
3120  const head = new THREE.Mesh(new THREE.SphereGeometry(0.25, 16, 12), mats.skin);
3121  head.position.y = 1.98;
3122  head.scale.set(0.88, 1.12, 0.82);
3123  head.castShadow = true;
3124  group.add(head);
3125
3126  const hair = new THREE.Mesh(new THREE.SphereGeometry(0.285, 16, 10), mats.carDark);
3127  hair.position.set(0, 2.1, -0.04);
3128  hair.scale.set(1.05, 0.46, 0.98);
3129  group.add(hair);
3130
3131  const backHair = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.28, 0.14), mats.carDark);
3132  backHair.position.set(0, 1.96, -0.2);
3133  group.add(backHair);
3134
3135  for (const x of [-0.12, 0, 0.12]) {
3136    const bang = new THREE.Mesh(new THREE.ConeGeometry(0.055, 0.22, 6), mats.carDark);
3137    bang.position.set(x, 2.02, 0.2);
3138    bang.rotation.x = Math.PI * 0.78;
3139    bang.rotation.z = x * 2.2;
3140    group.add(bang);
3141  }
3142
3143  for (const x of [-0.24, 0.24]) {
3144    const sideHair = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.22, 0.08), mats.carDark);
3145    sideHair.position.set(x, 1.94, 0.02);
3146    sideHair.rotation.z = -Math.sign(x) * 0.15;
3147    group.add(sideHair);
3148  }
3149
3150  for (const x of [-0.23, 0.23]) {
3151    const ear = new THREE.Mesh(new THREE.SphereGeometry(0.055, 8, 6), mats.skin);
3152    ear.position.set(x, 1.99, 0.02);
3153    ear.scale.set(0.62, 1, 0.45);
3154    group.add(ear);
3155  }
3156
3157  const nose = new THREE.Mesh(new THREE.ConeGeometry(0.038, 0.09, 8), mats.skin);
3158  nose.position.set(0, 1.99, 0.24);
3159  nose.rotation.x = Math.PI / 2;
3160  group.add(nose);
3161
3162  for (const x of [-0.08, 0.08]) {
3163    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.025, 8, 6), mats.eye);
3164    eye.position.set(x, 2.04, 0.225);
3165    group.add(eye);
3166
3167    const brow = new THREE.Mesh(new THREE.BoxGeometry(0.09, 0.014, 0.012), mats.eyebrow);
3168    brow.position.set(x, 2.095, 0.225);
3169    brow.rotation.z = -Math.sign(x) * 0.16;
3170    group.add(brow);
3171  }
3172
3173  const mouth = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.018, 0.012), mats.eye);
3174  mouth.position.set(0, 1.91, 0.225);
3175  group.add(mouth);
3176
3177  const shoulders = new THREE.Mesh(new THREE.CapsuleGeometry(0.13, 0.62, 4, 8), mats.shirt);
3178  shoulders.rotation.z = Math.PI / 2;
3179  shoulders.position.y = 1.58;
3180  shoulders.castShadow = true;
3181  group.add(shoulders);
3182
3183  const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.05, 0.035), mats.characterDetail);
3184  stripe.position.set(0, 1.36, 0.22);
3185  group.add(stripe);
3186
3187  const chestShadow = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.055, 0.035), mats.carDark);
3188  chestShadow.position.set(0, 1.5, 0.22);
3189  chestShadow.scale.x = 0.6;
3190  group.add(chestShadow);
3191
3192  const limbs = { arms: [], forearms: [], hands: [], legs: [], shins: [], feet: [] };
3193  for (const x of [-0.48, 0.48]) {
3194    const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.075, 0.42, 4, 8), mats.shirt);
3195    arm.position.set(x, 1.36, 0);
3196    arm.castShadow = true;
3197    group.add(arm);
3198    limbs.arms.push(arm);
3199
3200    const forearm = new THREE.Mesh(new THREE.CapsuleGeometry(0.07, 0.38, 4, 8), mats.skin);
3201    forearm.position.set(x, 1.02, 0);
3202    forearm.castShadow = true;
3203    group.add(forearm);
3204    limbs.forearms.push(forearm);
3205
3206    const hand = new THREE.Mesh(new THREE.SphereGeometry(0.085, 10, 8), mats.skin);
3207    hand.position.set(x, 0.78, 0.02);
3208    hand.scale.set(0.82, 1, 0.86);
3209    group.add(hand);
3210    limbs.hands.push(hand);
3211
3212    const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.105, 0.48, 4, 8), mats.pants);
3213    leg.position.set(x * 0.28, 0.61, 0);
3214    leg.castShadow = true;
3215    group.add(leg);
3216    limbs.legs.push(leg);
3217
3218    const shin = new THREE.Mesh(new THREE.CapsuleGeometry(0.095, 0.42, 4, 8), mats.pants);
3219    shin.position.set(x * 0.28, 0.28, 0);
3220    shin.castShadow = true;
3221    group.add(shin);
3222    limbs.shins.push(shin);
3223
3224    const knee = new THREE.Mesh(new THREE.SphereGeometry(0.11, 8, 6), mats.pants);
3225    knee.position.set(x * 0.28, 0.45, 0.03);
3226    knee.scale.set(0.82, 0.5, 0.7);
3227    group.add(knee);
3228
3229    const foot = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.1, 0.38), mats.shoe);
3230    foot.position.set(x * 0.28, 0.04, 0.08);
3231    foot.castShadow = true;
3232    group.add(foot);
3233    limbs.feet.push(foot);
3234
3235    const sole = new THREE.Mesh(new THREE.BoxGeometry(0.23, 0.035, 0.4), mats.carDark);
3236    sole.position.set(x * 0.28, -0.015, 0.09);
3237    group.add(sole);
3238  }
3239
3240  group.userData.limbs = limbs;
3241  group.userData.torso = torso;
3242  group.userData.shoulders = shoulders;
3243  group.userData.head = head;
3244  group.userData.parachute = createParachute();
3245  group.userData.parachute.visible = false;
3246  group.add(group.userData.parachute);
3247  group.userData.weapon = createWeapon();
3248  group.add(group.userData.weapon);
3249  group.visible = false;
3250  return group;
3251}
3252
3253function createParachute() {
3254  const parachute = new THREE.Group();
3255  parachute.position.set(0, 2.85, -0.08);
3256
3257  const canopy = new THREE.Mesh(
3258    new THREE.SphereGeometry(1.45, 18, 8, 0, Math.PI * 2, 0, Math.PI / 2),
3259    new THREE.MeshStandardMaterial({ color: 0xf2f1e8, roughness: 0.62, side: THREE.DoubleSide }),
3260  );
3261  canopy.scale.set(1.45, 0.58, 1.0);
3262  parachute.add(canopy);
3263
3264  for (const x of [-0.9, -0.3, 0.3, 0.9]) {
3265    const cord = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 2.2, 6), mats.chrome);
3266    cord.position.set(x * 0.45, -1.0, 0.08);
3267    cord.rotation.z = x * 0.16;
3268    parachute.add(cord);
3269  }
3270
3271  const harness = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.08, 0.12), mats.carDark);
3272  harness.position.set(0, -2.05, 0.08);
3273  parachute.add(harness);
3274  return parachute;
3275}
3276
3277function setParachuteVisible(visible) {
3278  const parachute = character?.userData?.parachute;
3279  if (parachute) parachute.visible = visible;
3280}
3281
3282function createWeapon() {
3283  const weapon = new THREE.Group();
3284
3285  const body = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.16, 0.78), mats.gun);
3286  body.position.set(0, 0, 0.1);
3287  weapon.add(body);
3288
3289  const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.72, 10), mats.gun);
3290  barrel.rotation.x = Math.PI / 2;
3291  barrel.position.set(0, 0.035, 0.75);
3292  weapon.add(barrel);
3293
3294  const stock = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.17, 0.34), mats.gun);
3295  stock.position.set(0, -0.02, -0.42);
3296  stock.rotation.x = -0.08;
3297  weapon.add(stock);
3298
3299  const grip = new THREE.Mesh(new THREE.BoxGeometry(0.09, 0.3, 0.09), mats.gun);
3300  grip.position.set(0, -0.22, -0.05);
3301  grip.rotation.x = 0.48;
3302  weapon.add(grip);
3303
3304  const foreGrip = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.22, 0.08), mats.gun);
3305  foreGrip.position.set(0, -0.18, 0.38);
3306  foreGrip.rotation.x = -0.18;
3307  weapon.add(foreGrip);
3308
3309  const sight = new THREE.Mesh(new THREE.BoxGeometry(0.11, 0.055, 0.22), mats.carDark);
3310  sight.position.set(0, 0.13, 0.12);
3311  weapon.add(sight);
3312
3313  weapon.position.set(0.18, 1.32, 0.42);
3314  weapon.rotation.set(-0.1, -0.32, 0.02);
3315  return weapon;
3316}
3317
3318function createZombie() {
3319  const group = new THREE.Group();
3320  group.name = `zombie-${zombieId += 1}`;
3321
3322  const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.24, 0.72, 4, 8), mats.zombieCloth);
3323  torso.position.y = 1.08;
3324  torso.scale.set(1.05, 1, 0.72);
3325  group.add(torso);
3326
3327  const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 9), mats.zombieSkin);
3328  head.position.y = 1.72;
3329  head.scale.set(0.9, 1.08, 0.82);
3330  group.add(head);
3331
3332  for (const x of [-0.08, 0.08]) {
3333    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.025, 6, 5), mats.muzzle);
3334    eye.position.set(x, 1.77, 0.19);
3335    group.add(eye);
3336  }
3337
3338  const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.035, 0.035), mats.carDark);
3339  jaw.position.set(0, 1.62, 0.19);
3340  group.add(jaw);
3341
3342  const limbs = { arms: [], legs: [] };
3343  for (const side of [-1, 1]) {
3344    const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.065, 0.64, 4, 7), mats.zombieSkin);
3345    arm.position.set(side * 0.34, 1.18, 0.17);
3346    arm.rotation.x = -0.72;
3347    arm.rotation.z = side * 0.16;
3348    group.add(arm);
3349    limbs.arms.push(arm);
3350
3351    const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.085, 0.58, 4, 7), mats.zombieCloth);
3352    leg.position.set(side * 0.12, 0.43, 0);
3353    group.add(leg);
3354    limbs.legs.push(leg);
3355  }
3356
3357  const healthBar = new THREE.Group();
3358  healthBar.position.set(0, 2.12, 0);
3359  const healthBack = new THREE.Mesh(
3360    new THREE.BoxGeometry(0.72, 0.08, 0.035),
3361    new THREE.MeshBasicMaterial({ color: 0x2b1010 }),
3362  );
3363  const healthFill = new THREE.Mesh(
3364    new THREE.BoxGeometry(0.68, 0.052, 0.045),
3365    new THREE.MeshBasicMaterial({ color: 0x35e06f }),
3366  );
3367  healthFill.position.z = 0.01;
3368  healthBar.add(healthBack, healthFill);
3369  group.add(healthBar);
3370
3371  group.userData = {
3372    health: 5,
3373    maxHealth: 5,
3374    radius: 1.9,
3375    speed: 2.6 + Math.random() * 0.75,
3376    attackCooldown: 0,
3377    soundTimer: 0.4 + Math.random() * 1.8,
3378    limbs,
3379    healthBar,
3380    healthFill,
3381    bobSeed: Math.random() * Math.PI * 2,
3382  };
3383  return group;
3384}
3385
3386function clearZombies() {
3387  for (const zombie of zombies.splice(0)) scene.remove(zombie);
3388}
3389
3390function startZombieRound() {
3391  hideRoundResult();
3392  clearZombies();
3393  world.roundDuration = 0;
3394  world.timeLeft = 0;
3395  world.roundElapsed = 0;
3396  world.zombieSpawnTimer = 0;
3397  world.kills = 0;
3398  world.health = 100;
3399  world.over = false;
3400  statusEl.textContent = text.started;
3401  updateHud();
3402}
3403
3404function spawnZombie() {
3405  return;
3406  if (zombies.length >= maxZombies) return;
3407  const actor = controlledActor();
3408  for (let attempt = 0; attempt < 18; attempt += 1) {
3409    const angle = Math.random() * Math.PI * 2;
3410    const distance = 34 + Math.random() * 74;
3411    const x = THREE.MathUtils.clamp(actor.position.x + Math.sin(angle) * distance, worldMinX + 8, worldMaxX - 8);
3412    const z = THREE.MathUtils.clamp(actor.position.z + Math.cos(angle) * distance, worldMinZ + 8, worldMaxZ - 8);
3413    if (!isPointInLand(x, z)) continue;
3414    if (isInsideBlockedArea(x, z, 1.4)) continue;
3415    const zombie = createZombie();
3416    zombie.position.set(x, 0, z);
3417    zombie.rotation.y = Math.random() * Math.PI * 2;
3418    scene.add(zombie);
3419    zombies.push(zombie);
3420    return;
3421  }
3422}
3423
3424function isInsideBlockedArea(x, z, padding = 0) {
3425  if (isInMilitaryBase(x, z, padding)) return true;
3426  for (const building of buildings) {
3427    if (Math.abs(x - building.mesh.position.x) < building.halfX + padding && Math.abs(z - building.mesh.position.z) < building.halfZ + padding) return true;
3428  }
3429  for (const obstacle of obstacles) {
3430    if (Math.abs(x - obstacle.x) < obstacle.halfX + padding && Math.abs(z - obstacle.z) < obstacle.halfZ + padding) return true;
3431  }
3432  return false;
3433}
3434
3435function createTaperedBox({ bottomWidth, topWidth, bottomDepth, topDepth, height }) {
3436  const bw = bottomWidth / 2;
3437  const tw = topWidth / 2;
3438  const bd = bottomDepth / 2;
3439  const td = topDepth / 2;
3440  const geometry = new THREE.BufferGeometry();
3441  geometry.setAttribute(
3442    "position",
3443    new THREE.Float32BufferAttribute(
3444      [
3445        -bw, 0, -bd,
3446        bw, 0, -bd,
3447        bw, 0, bd,
3448        -bw, 0, bd,
3449        -tw, height, -td,
3450        tw, height, -td,
3451        tw, height, td,
3452        -tw, height, td,
3453      ],
3454      3,
3455    ),
3456  );
3457  geometry.setIndex([0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, 2, 6, 7, 2, 7, 3, 3, 7, 4, 3, 4, 0]);
3458  geometry.computeVertexNormals();
3459  return geometry;
3460}
3461
3462function roundedLoop(width, depth, y, zOffset = 0, segments = 32, exponent = 0.58) {
3463  const points = [];
3464  const w = width / 2;
3465  const d = depth / 2;
3466  for (let i = 0; i < segments; i += 1) {
3467    const angle = (i / segments) * Math.PI * 2;
3468    const c = Math.cos(angle);
3469    const s = Math.sin(angle);
3470    const x = Math.sign(c) * Math.pow(Math.abs(c), exponent) * w;
3471    const z = Math.sign(s) * Math.pow(Math.abs(s), exponent) * d + zOffset;
3472    points.push([x, y, z]);
3473  }
3474  return points;
3475}
3476
3477function createRoundedCarBody({ bottomWidth, topWidth, bottomDepth, topDepth, height }) {
3478  const segments = 36;
3479  const rings = [
3480    roundedLoop(bottomWidth * 0.96, bottomDepth * 0.96, 0, 0, segments, 0.54),
3481    roundedLoop(bottomWidth, bottomDepth, height * 0.24, 0, segments, 0.5),
3482    roundedLoop((bottomWidth + topWidth) * 0.52, (bottomDepth + topDepth) * 0.52, height * 0.72, -0.03, segments, 0.56),
3483    roundedLoop(topWidth, topDepth, height, -0.08, segments, 0.62),
3484  ];
3485  const positions = rings.flat(2);
3486  const indices = [];
3487  for (let r = 0; r < rings.length - 1; r += 1) {
3488    const a = r * segments;
3489    const b = (r + 1) * segments;
3490    for (let i = 0; i < segments; i += 1) {
3491      const next = (i + 1) % segments;
3492      indices.push(a + i, b + i, b + next, a + i, b + next, a + next);
3493    }
3494  }
3495
3496  const bottomCenter = positions.length / 3;
3497  positions.push(0, 0, 0);
3498  const topCenter = positions.length / 3;
3499  positions.push(0, height + 0.035, -0.08);
3500  const topStart = (rings.length - 1) * segments;
3501  for (let i = 0; i < segments; i += 1) {
3502    const next = (i + 1) % segments;
3503    indices.push(bottomCenter, next, i);
3504    indices.push(topCenter, topStart + i, topStart + next);
3505  }
3506
3507  const geometry = new THREE.BufferGeometry();
3508  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
3509  geometry.setIndex(indices);
3510  geometry.computeVertexNormals();
3511  return geometry;
3512}
3513
3514function createCabinGeometry({ bottomWidth, topWidth, bottomDepth, topDepth, height, topShiftZ }) {
3515  const bw = bottomWidth / 2;
3516  const tw = topWidth / 2;
3517  const bd = bottomDepth / 2;
3518  const td = topDepth / 2;
3519  const geometry = new THREE.BufferGeometry();
3520  geometry.setAttribute(
3521    "position",
3522    new THREE.Float32BufferAttribute(
3523      [
3524        -bw, 0, -bd,
3525        bw, 0, -bd,
3526        bw, 0, bd,
3527        -bw, 0, bd,
3528        -tw, height, -td + topShiftZ,
3529        tw, height, -td + topShiftZ,
3530        tw, height, td + topShiftZ,
3531        -tw, height, td + topShiftZ,
3532      ],
3533      3,
3534    ),
3535  );
3536  geometry.setIndex([0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, 2, 6, 7, 2, 7, 3, 3, 7, 4, 3, 4, 0]);
3537  geometry.computeVertexNormals();
3538  return geometry;
3539}
3540
3541function createWedgeGeometry({ width, depth, low, high, slopeToFront }) {
3542  const w = width / 2;
3543  const d = depth / 2;
3544  const frontY = slopeToFront ? low : high;
3545  const backY = slopeToFront ? high : low;
3546  const geometry = new THREE.BufferGeometry();
3547  geometry.setAttribute(
3548    "position",
3549    new THREE.Float32BufferAttribute(
3550      [
3551        -w, 0, -d,
3552        w, 0, -d,
3553        w, 0, d,
3554        -w, 0, d,
3555        -w * 0.9, backY, -d,
3556        w * 0.9, backY, -d,
3557        w * 0.9, frontY, d,
3558        -w * 0.9, frontY, d,
3559      ],
3560      3,
3561    ),
3562  );
3563  geometry.setIndex([0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, 2, 6, 7, 2, 7, 3, 3, 7, 4, 3, 4, 0]);
3564  geometry.computeVertexNormals();
3565  return geometry;
3566}
3567
3568function createRoundedWedgeGeometry({ width, depth, low, high, slopeToFront }) {
3569  const segments = 32;
3570  const positions = [];
3571  const indices = [];
3572  const bottom = roundedLoop(width, depth, 0, 0, segments, 0.56);
3573  for (const [x, y, z] of bottom) positions.push(x, y, z);
3574  for (const [x, y, z] of bottom) {
3575    const zProgress = (z / (depth / 2) + 1) / 2;
3576    const frontMix = slopeToFront ? zProgress : 1 - zProgress;
3577    const topY = THREE.MathUtils.lerp(high, low, frontMix);
3578    const sideCurve = 1 - Math.min(1, Math.abs(x) / (width / 2)) * 0.16;
3579    positions.push(x * 0.9, topY * sideCurve, z * 0.94);
3580  }
3581  for (let i = 0; i < segments; i += 1) {
3582    const next = (i + 1) % segments;
3583    indices.push(i, segments + i, segments + next, i, segments + next, next);
3584  }
3585  const bottomCenter = positions.length / 3;
3586  positions.push(0, 0, 0);
3587  const topCenter = positions.length / 3;
3588  positions.push(0, (low + high) * 0.5 + 0.025, 0);
3589  for (let i = 0; i < segments; i += 1) {
3590    const next = (i + 1) % segments;
3591    indices.push(bottomCenter, next, i);
3592    indices.push(topCenter, segments + i, segments + next);
3593  }
3594  const geometry = new THREE.BufferGeometry();
3595  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
3596  geometry.setIndex(indices);
3597  geometry.computeVertexNormals();
3598  return geometry;
3599}
3600
3601function reset() {
3602  const wasStarted = world.started;
3603  for (const spark of sparks.splice(0)) scene.remove(spark.mesh);
3604  for (const smoke of smokePuffs.splice(0)) scene.remove(smoke.mesh);
3605  clearZombies();
3606  for (const pickup of pickups) {
3607    pickup.taken = false;
3608    pickup.mesh.visible = true;
3609  }
3610  Object.assign(world, {
3611    started: wasStarted,
3612    health: 100,
3613    energy: 100,
3614    turbo: 100,
3615    turboHeld: false,
3616    kills: 0,
3617    money: 0,
3618    weaponLevel: 1,
3619    wantedLevel: 0,
3620    wantedTimer: 0,
3621    policeAttackTimer: 0,
3622    militaryAlertTimer: 0,
3623    timeLeft: 0,
3624    roundElapsed: 0,
3625    zombieSpawnTimer: 0,
3626    zombieSoundTimer: 0,
3627    speed: 0,
3628    rpm: 0.18,
3629    gear: 1,
3630    shiftTimer: 0,
3631    heading: 0,
3632    planeHeading: Math.PI / 2,
3633    planeSpeed: 0,
3634    planeLift: 0,
3635    walkHeading: 0,
3636    inCar: true,
3637    inPlane: false,
3638    cameraYaw: 0,
3639    cameraPitch: 0.08,
3640    mouseLookTimer: 0,
3641    aiming: false,
3642    jumpVelocity: 0,
3643    grounded: true,
3644    parachuting: false,
3645    parachuteHeading: 0,
3646    parachuteForwardSpeed: 0,
3647    sinking: false,
3648    sinkTimer: 0,
3649    sinkStartY: 0.55,
3650    over: false,
3651    deathScreen: false,
3652    shopOpen: false,
3653    messageTimer: 0,
3654  });
3655  closeWeaponShop();
3656  if (deathScreenEl) deathScreenEl.hidden = true;
3657  clearPoliceDeployment();
3658  car.position.set(0, 0.55, 0);
3659  car.rotation.set(0, 0, 0);
3660  plane.position.set(militaryBase.x - 56, 0.45, militaryBase.z - 18);
3661  plane.rotation.set(0, Math.PI / 2, 0);
3662  character.position.set(2.6, 0, 0);
3663  character.rotation.set(0, 0, 0);
3664  character.visible = false;
3665  setParachuteVisible(false);
3666  statusEl.textContent = text.status;
3667  updateWeaponVisual();
3668  if (wasStarted) startZombieRound();
3669  updateHud();
3670}
3671
3672function update(dt) {
3673  if (!world.started) {
3674    updateDayNight(0);
3675    updateHud();
3676    return;
3677  }
3678  updateDayNight(dt);
3679  if (world.over) {
3680    updateZombies(dt);
3681    updateSparks(dt);
3682    updateShots(dt);
3683    updateSmoke(dt);
3684    updateHud();
3685    return;
3686  }
3687  world.mouseLookTimer = Math.max(0, world.mouseLookTimer - dt);
3688  updateAutomaticDoors(dt);
3689  updateTraffic(dt);
3690  updateNpcActors(dt);
3691  updatePolice(dt);
3692  updateMilitaryZone(dt);
3693  updateWeaponBenchPrompt();
3694  if (world.shopOpen) {
3695    updateWeaponShopOffer();
3696    updateHud();
3697    return;
3698  }
3699  updateZombieRound(dt);
3700  if (world.over) {
3701    updateZombies(dt);
3702    updateSparks(dt);
3703    updateShots(dt);
3704    updateSmoke(dt);
3705    updateHud();
3706    return;
3707  }
3708
3709  if (world.sinking) {
3710    updateCarSinking(dt);
3711    updateEngineSound(false, false);
3712    updateZombies(dt);
3713    updateSparks(dt);
3714    updateShots(dt);
3715    updateExhaustSmoke(dt, false);
3716    updateSmoke(dt);
3717    updateHud();
3718    return;
3719  }
3720
3721  if (world.inPlane) {
3722    updatePlane(dt);
3723    updateEngineSound(keys.has("w") || keys.has("arrowup") || held.gas || (keys.has("shift") && world.turbo > 0), false);
3724    updateZombies(dt);
3725    updateSparks(dt);
3726    updateShots(dt);
3727    updateSmoke(dt);
3728    updateHud();
3729    return;
3730  }
3731
3732  if (!world.inCar) {
3733    updateCharacter(dt);
3734    updateEngineSound(false, false);
3735    updateZombies(dt);
3736    updateSparks(dt);
3737    updateShots(dt);
3738    updateSmoke(dt);
3739    world.turboHeld = false;
3740    world.turbo = THREE.MathUtils.clamp(world.turbo + 16 * dt, 0, 100);
3741    updateHud();
3742    return;
3743  }
3744
3745  const gas = keys.has("w") || keys.has("arrowup") || held.gas;
3746  const brake = keys.has("s") || keys.has("arrowdown") || held.brake;
3747  const left = keys.has("a") || keys.has("arrowleft") || held.left;
3748  const right = keys.has("d") || keys.has("arrowright") || held.right;
3749  const handbrake = keys.has(" ");
3750  const previousSpeed = world.speed;
3751  const turboActive = keys.has("shift") && world.turbo > 1 && world.speed >= -1;
3752
3753  world.shiftTimer = Math.max(0, world.shiftTimer - dt);
3754  const absSpeed = Math.abs(world.speed);
3755  const gearTops = [0, 13, 26, 40, 58];
3756  const nextGear = world.gear < 4 && absSpeed > gearTops[world.gear] * 0.94;
3757  if (nextGear && world.shiftTimer <= 0) {
3758    world.gear += 1;
3759    world.shiftTimer = 0.28;
3760  }
3761  if (world.gear > 1 && absSpeed < gearTops[world.gear - 1] * 0.62) world.gear -= 1;
3762
3763  const gearStart = gearTops[world.gear - 1];
3764  const gearEnd = gearTops[world.gear];
3765  const gearProgress = THREE.MathUtils.clamp((absSpeed - gearStart) / Math.max(1, gearEnd - gearStart), 0, 1);
3766  const targetRpm = gas ? 0.24 + gearProgress * 0.72 : 0.16 + gearProgress * 0.35;
3767  world.rpm += (targetRpm - world.rpm) * Math.min(1, dt * (gas ? 2.6 : 4.5));
3768
3769  const launchFactor = THREE.MathUtils.smoothstep(absSpeed, 2, 22);
3770  const clutchSlip = THREE.MathUtils.clamp(absSpeed / 8, 0.42, 1);
3771  const gearTorque = [0, 1, 0.82, 0.64, 0.48][world.gear];
3772  const rollingDrag = world.speed * (0.42 + absSpeed * 0.018);
3773  if (gas) {
3774    const shiftCut = world.shiftTimer > 0 ? 0.28 : 1;
3775    const enginePull = (6.4 + launchFactor * 28 + (turboActive ? 34 : 0)) * clutchSlip * gearTorque * shiftCut;
3776    world.speed += enginePull * dt;
3777  }
3778  if (brake) {
3779    const reverseLaunch = THREE.MathUtils.smoothstep(absSpeed, 0, 9);
3780    const reverseClutch = THREE.MathUtils.clamp(absSpeed / 7, 0.45, 1);
3781    const brakeForce = world.speed > 1 ? 58 : (7.8 + reverseLaunch * 18) * reverseClutch;
3782    world.speed -= brakeForce * dt;
3783  }
3784  if (!gas && !brake) world.speed -= rollingDrag * dt;
3785  if (handbrake) world.speed *= 1 - Math.min(1, dt * 3.4);
3786  if (Math.abs(world.speed) < 0.08 && !gas && !brake) world.speed = 0;
3787  if (turboActive && !gas) world.speed += 18 * dt;
3788  world.speed = THREE.MathUtils.clamp(world.speed, -18, turboActive ? 82 : 58);
3789  const slowingDown = world.speed > 1 && world.speed < previousSpeed - 0.02;
3790  setBrakeLights(brake || handbrake || slowingDown);
3791  updateEngineSound(gas || turboActive, brake);
3792
3793  const turnPower = THREE.MathUtils.clamp(Math.abs(world.speed) / 18, 0.25, 1);
3794  if (left) world.heading += dt * 2.3 * turnPower * Math.sign(world.speed || 1);
3795  if (right) world.heading -= dt * 2.3 * turnPower * Math.sign(world.speed || 1);
3796
3797  const previous = car.position.clone();
3798  car.rotation.y = world.heading;
3799  car.position.x += Math.sin(world.heading) * world.speed * dt;
3800  car.position.z += Math.cos(world.heading) * world.speed * dt;
3801  car.rotation.z = (Number(right) - Number(left)) * 0.08 * turnPower;
3802  car.rotation.x = -Math.min(0.06, Math.abs(world.speed) * 0.0015);
3803  for (const wheel of wheels) wheel.rotation.x += world.speed * dt * 2.2;
3804  const targetSteer = (Number(left) - Number(right)) * 0.42 * turnPower;
3805  for (const part of frontWheelParts) {
3806    part.rotation.y += (targetSteer - part.rotation.y) * Math.min(1, dt * 12);
3807  }
3808
3809  keepInsideWorld(previous);
3810  collideWithCity(previous);
3811  checkCarWaterEntry();
3812  collectPickups();
3813  updateZombies(dt);
3814  updateSparks(dt);
3815  updateShots(dt);
3816  updateExhaustSmoke(dt, gas || turboActive);
3817  updateSmoke(dt);
3818  world.energy = Math.min(100, world.energy + 9 * dt);
3819  world.turbo = THREE.MathUtils.clamp(world.turbo + (turboActive ? -32 : 13) * dt, 0, 100);
3820
3821  if (world.health <= 0) {
3822    showDeathScreen("You woke up at the hospital.");
3823  }
3824  updateHud();
3825}
3826
3827function updateZombieRound(dt) {
3828  const endless = world.roundDuration <= 0;
3829  world.roundElapsed += dt;
3830  if (!endless) world.timeLeft = Math.max(0, world.timeLeft - dt);
3831  clearZombies();
3832  if (!endless && world.timeLeft <= 0) {
3833    finishZombieRound();
3834  }
3835}
3836
3837function finishZombieRound() {
3838  if (world.over) return;
3839  world.over = true;
3840  world.shopOpen = false;
3841  closeWeaponShop();
3842  if (document.pointerLockElement === canvas) document.exitPointerLock?.();
3843  world.speed = 0;
3844  keys.clear();
3845  Object.keys(held).forEach((name) => {
3846    held[name] = false;
3847  });
3848  statusEl.textContent = "Time is over.";
3849  showRoundResult("Time is over", `Money: $${world.money}`);
3850  updateHud();
3851}
3852
3853function showRoundResult(title, stats) {
3854  resultTitleEl.textContent = title;
3855  resultStatsEl.textContent = stats;
3856  roundResultEl.hidden = false;
3857  menuCursorEl.hidden = false;
3858}
3859
3860function controlledActor() {
3861  if (world.inPlane) return plane;
3862  return world.inCar ? car : character;
3863}
3864
3865function controlledHeading() {
3866  if (world.inPlane) return world.planeHeading;
3867  return world.inCar ? world.heading : character.rotation.y;
3868}
3869
3870function isInVehicle() {
3871  return world.inCar || world.inPlane;
3872}
3873
3874function hideRoundResult() {
3875  if (roundResultEl) roundResultEl.hidden = true;
3876}
3877
3878function isRoundResultOpen() {
3879  return roundResultEl && !roundResultEl.hidden;
3880}
3881
3882function showDeathScreen(message = "You were taken to the hospital.") {
3883  if (world.deathScreen) return;
3884  world.deathScreen = true;
3885  world.over = true;
3886  world.shopOpen = false;
3887  closeWeaponShop();
3888  if (document.pointerLockElement === canvas) document.exitPointerLock?.();
3889  world.speed = 0;
3890  world.planeSpeed = 0;
3891  keys.clear();
3892  Object.keys(held).forEach((name) => {
3893    held[name] = false;
3894  });
3895  deathReasonEl.textContent = message;
3896  deathScreenEl.hidden = false;
3897  menuCursorEl.hidden = false;
3898  setTimeout(() => {
3899    deathScreenEl.hidden = true;
3900    world.deathScreen = false;
3901    respawnAtHospital(message);
3902  }, 2600);
3903}
3904
3905function updateZombies(dt) {
3906  if (zombies.length) clearZombies();
3907  return;
3908  const target = controlledActor();
3909  let nearestDistance = Infinity;
3910  let soundsThisFrame = 0;
3911  for (let i = zombies.length - 1; i >= 0; i -= 1) {
3912    const zombie = zombies[i];
3913    if (!zombie) {
3914      zombies.splice(i, 1);
3915      continue;
3916    }
3917    const data = zombie.userData;
3918    data.attackCooldown = Math.max(0, data.attackCooldown - dt);
3919    data.soundTimer = Math.max(0, data.soundTimer - dt);
3920    const dx = target.position.x - zombie.position.x;
3921    const dz = target.position.z - zombie.position.z;
3922    const distance = Math.max(0.001, Math.hypot(dx, dz));
3923    nearestDistance = Math.min(nearestDistance, distance);
3924    const heading = Math.atan2(dx, dz);
3925    zombie.rotation.y += angleDelta(heading, zombie.rotation.y) * Math.min(1, dt * 8);
3926
3927    const attackRange = isInVehicle() ? 3.2 : 2.15;
3928    if (!world.over && distance > attackRange * 0.78) {
3929      const step = Math.min(distance - attackRange * 0.7, data.speed * dt);
3930      const previous = zombie.position.clone();
3931      zombie.position.x += (dx / distance) * step;
3932      zombie.position.z += (dz / distance) * step;
3933      if (!isPointInLand(zombie.position.x, zombie.position.z) || isInsideBlockedArea(zombie.position.x, zombie.position.z, 0.55)) {
3934        zombie.position.copy(previous);
3935      }
3936    }
3937
3938    const walk = clock.elapsedTime * (4.4 + data.speed) + data.bobSeed;
3939    zombie.position.y = Math.abs(Math.sin(walk)) * 0.035;
3940    data.limbs.arms[0].rotation.x = -0.85 + Math.sin(walk) * 0.14;
3941    data.limbs.arms[1].rotation.x = -0.85 - Math.sin(walk) * 0.14;
3942    data.limbs.legs[0].rotation.x = Math.sin(walk) * 0.48;
3943    data.limbs.legs[1].rotation.x = -Math.sin(walk) * 0.48;
3944
3945    if (!world.over && distance < 95 && data.soundTimer <= 0 && soundsThisFrame < 2) {
3946      zombieGroan(distance, false);
3947      data.soundTimer = 1.25 + Math.random() * 2.1 + THREE.MathUtils.clamp(distance / 70, 0, 1.4);
3948      soundsThisFrame += 1;
3949    }
3950
3951    if (!world.over && distance < attackRange && data.attackCooldown <= 0) {
3952      if (world.inCar && Math.abs(world.speed) > 10) {
3953        killZombie(zombie, i);
3954        world.speed *= 0.82;
3955        continue;
3956      }
3957      data.attackCooldown = 0.8;
3958      const zombieDamage = isInVehicle() ? 9 : 14;
3959      world.health = Math.max(0, world.health - zombieDamage);
3960      statusEl.textContent = isInVehicle() ? `A zombie hit your vehicle. -${zombieDamage} health.` : `A zombie attacked you. -${zombieDamage} health.`;
3961      zombieGroan(distance, true);
3962      pop(zombie.position, 0x7fa36b, 7);
3963      updateHud();
3964      if (world.health <= 0) {
3965        showDeathScreen("The zombies got you. You woke up at the hospital.");
3966      }
3967    }
3968  }
3969  updateZombieSounds(dt, nearestDistance);
3970}
3971
3972function updateZombieSounds(dt, nearestDistance) {
3973  return;
3974  if (!Number.isFinite(nearestDistance) || world.over || !world.started) return;
3975  world.zombieSoundTimer -= dt;
3976  if (world.zombieSoundTimer > 0) return;
3977  const close = THREE.MathUtils.clamp(1 - nearestDistance / 90, 0, 1);
3978  if (close <= 0.02) {
3979    world.zombieSoundTimer = 1.2;
3980    return;
3981  }
3982  zombieGroan(nearestDistance, false);
3983  world.zombieSoundTimer = THREE.MathUtils.lerp(3.2, 1.05, close) + Math.random() * 0.8;
3984}
3985
3986function zombieGroan(distance, attack = false) {
3987  return;
3988  try {
3989    startEngineSound();
3990    if (!audio) return;
3991    const now = audio.context.currentTime;
3992    const close = THREE.MathUtils.clamp(1 - distance / 95, 0.12, 1);
3993    const duration = attack ? 0.48 : 1.05 + Math.random() * 0.65;
3994    const baseGain = (attack ? 0.42 : 0.28) * close;
3995
3996    const throat = audio.context.createOscillator();
3997    const rasp = audio.context.createOscillator();
3998    const growl = audio.context.createOscillator();
3999    const filter = audio.context.createBiquadFilter();
4000    const raspFilter = audio.context.createBiquadFilter();
4001    const gain = audio.context.createGain();
4002    const raspGain = audio.context.createGain();
4003    const wobble = 0.4 + Math.random() * 0.28;
4004
4005    throat.type = "sawtooth";
4006    rasp.type = "square";
4007    growl.type = "sawtooth";
4008    throat.frequency.setValueAtTime(attack ? 96 : 58 + Math.random() * 18, now);
4009    throat.frequency.exponentialRampToValueAtTime(attack ? 38 : 28 + Math.random() * 10, now + duration);
4010    rasp.frequency.setValueAtTime(attack ? 210 : 130 + Math.random() * 50, now);
4011    rasp.frequency.exponentialRampToValueAtTime(attack ? 74 : 62 + Math.random() * 22, now + duration);
4012    growl.frequency.setValueAtTime(attack ? 34 : 26 + Math.random() * 8, now);
4013    growl.frequency.exponentialRampToValueAtTime(attack ? 22 : 18 + Math.random() * 5, now + duration);
4014    filter.type = "lowpass";
4015    filter.frequency.setValueAtTime(attack ? 760 : 420 + close * 260, now);
4016    filter.Q.value = 6.2;
4017    raspFilter.type = "bandpass";
4018    raspFilter.frequency.setValueAtTime(attack ? 620 : 430 + Math.random() * 180, now);
4019    raspFilter.Q.value = 5.5;
4020    gain.gain.setValueAtTime(0.001, now);
4021    gain.gain.exponentialRampToValueAtTime(baseGain, now + 0.05);
4022    gain.gain.setTargetAtTime(baseGain * 0.7, now + duration * 0.22, wobble);
4023    gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
4024    raspGain.gain.setValueAtTime(baseGain * 0.36, now);
4025    raspGain.gain.exponentialRampToValueAtTime(0.001, now + duration);
4026
4027    throat.connect(filter);
4028    growl.connect(filter);
4029    rasp.connect(raspFilter).connect(raspGain);
4030    filter.connect(gain).connect(audio.context.destination);
4031    raspGain.connect(audio.context.destination);
4032    throat.start(now);
4033    growl.start(now + 0.02);
4034    rasp.start(now + 0.035);
4035    throat.stop(now + duration);
4036    growl.stop(now + duration);
4037    rasp.stop(now + duration);
4038  } catch {
4039    // Zombie audio is optional.
4040  }
4041}
4042
4043function killZombie(zombie, index = zombies.indexOf(zombie)) {
4044  if (!zombie || zombie.userData.dead) return;
4045  zombie.userData.dead = true;
4046  if (index >= 0) zombies.splice(index, 1);
4047  scene.remove(zombie);
4048  world.kills += 1;
4049  world.money += 5;
4050  statusEl.textContent = `Zombie down. +$5. Money: $${world.money}.`;
4051  pop(zombie.position, 0x9bcf7a, 14);
4052  updateHud();
4053}
4054
4055function getNearestWeaponBench() {
4056  if (isInVehicle()) return null;
4057  let nearest = null;
4058  let bestDistance = Infinity;
4059  for (const bench of weaponBenches) {
4060    const bx = bench.interactionX ?? bench.x;
4061    const bz = bench.interactionZ ?? bench.z;
4062    const distance = Math.hypot(character.position.x - bx, character.position.z - bz);
4063    if (distance < bestDistance) {
4064      nearest = bench;
4065      bestDistance = distance;
4066    }
4067  }
4068  return bestDistance < 8.5 ? nearest : null;
4069}
4070
4071function weaponUpgradeCost() {
4072  return [0, 20, 40, 75][world.weaponLevel] || 0;
4073}
4074
4075function weaponDamage() {
4076  return [0, 1, 2, 2, 3][world.weaponLevel] || 1;
4077}
4078
4079function weaponShotCount() {
4080  if (world.weaponLevel >= 4) return 3;
4081  if (world.weaponLevel >= 3) return 2;
4082  return 1;
4083}
4084
4085function updateWeaponShopOffer() {
4086  if (!weaponShopEl || weaponShopEl.hidden) return;
4087  const cost = weaponUpgradeCost();
4088  const nextLevel = Math.min(world.weaponLevel + 1, 4);
4089  shopTitleEl.textContent = world.weaponLevel >= 4 ? "Max Level" : "Upgrade Offer";
4090  shopLevelEl.textContent = world.weaponLevel >= 4 ? "Your weapon is fully upgraded." : `Level ${world.weaponLevel} to Level ${nextLevel}`;
4091  shopCostEl.textContent = world.weaponLevel >= 4 ? "MAX" : `$${cost}`;
4092  shopMoneyEl.textContent = `$${world.money}`;
4093  shopBuyBtn.disabled = world.weaponLevel >= 4;
4094  shopMessageEl.classList.toggle("warning", world.weaponLevel < 4 && world.money < cost);
4095  if (world.weaponLevel >= 4) {
4096    shopMessageEl.textContent = "No more upgrades are available for this weapon.";
4097  } else if (world.money < cost) {
4098    shopMessageEl.textContent = `You need $${cost - world.money} more to buy this upgrade.`;
4099  } else {
4100    const nextShots = world.weaponLevel + 1 >= 4 ? 3 : world.weaponLevel + 1 >= 3 ? 2 : 1;
4101    const nextDamage = [0, 1, 2, 2, 3][world.weaponLevel + 1] || 1;
4102    shopMessageEl.textContent = `Buy this offer: damage ${nextDamage}, bullets ${nextShots}.`;
4103  }
4104}
4105
4106function openWeaponShop() {
4107  const bench = getNearestWeaponBench();
4108  if (!bench) return false;
4109  if (document.pointerLockElement === canvas) document.exitPointerLock?.();
4110  activeWeaponBench = bench;
4111  world.shopOpen = true;
4112  weaponShopEl.hidden = false;
4113  menuCursorEl.hidden = false;
4114  keys.clear();
4115  Object.keys(held).forEach((name) => {
4116    held[name] = false;
4117  });
4118  updateWeaponShopOffer();
4119  statusEl.textContent = "Weapon bench offer opened.";
4120  return true;
4121}
4122
4123function closeWeaponShop() {
4124  activeWeaponBench = null;
4125  world.shopOpen = false;
4126  if (weaponShopEl) weaponShopEl.hidden = true;
4127  menuCursorEl.hidden = !isRoundResultOpen() && (world.started || menuEl.hidden);
4128}
4129
4130function setSoundEnabled(enabled) {
4131  soundEnabled = enabled;
4132  soundToggle.checked = enabled;
4133  if (!audio) return;
4134  const now = audio.context.currentTime;
4135  audio.master.gain.setTargetAtTime(enabled ? 0.18 : 0, now, 0.08);
4136  if (enabled && audio.context.state === "suspended") audio.context.resume();
4137}
4138
4139function buyWeaponUpgrade() {
4140  if (!activeWeaponBench || !getNearestWeaponBench()) {
4141    closeWeaponShop();
4142    statusEl.textContent = "Move closer to the weapon bench.";
4143    return true;
4144  }
4145  if (world.weaponLevel >= 4) {
4146    statusEl.textContent = "Weapon bench: weapon is already fully upgraded.";
4147    updateWeaponShopOffer();
4148    return true;
4149  }
4150  const cost = weaponUpgradeCost();
4151  if (world.money < cost) {
4152    statusEl.textContent = `Weapon bench: need $${cost}. Money: $${world.money}.`;
4153    updateWeaponShopOffer();
4154    return true;
4155  }
4156  world.money -= cost;
4157  world.weaponLevel += 1;
4158  activeWeaponBench.level = world.weaponLevel;
4159  updateWeaponVisual();
4160  statusEl.textContent = `Bought weapon upgrade level ${world.weaponLevel}.`;
4161  updateHud();
4162  updateWeaponShopOffer();
4163  return true;
4164}
4165
4166function updateWeaponBenchPrompt() {
4167  const bench = getNearestWeaponBench();
4168  if (!bench || world.over) return;
4169  if (world.weaponLevel >= 4) {
4170    statusEl.textContent = "Weapon bench: max level reached.";
4171    return;
4172  }
4173  statusEl.textContent = `Weapon bench: press E to see offer for $${weaponUpgradeCost()}.`;
4174}
4175
4176function updateWeaponVisual() {
4177  const weapon = character.userData.weapon;
4178  if (!weapon) return;
4179  weapon.scale.setScalar(1 + (world.weaponLevel - 1) * 0.08);
4180}
4181
4182function returnToMenu() {
4183  world.started = false;
4184  if (document.pointerLockElement === canvas) document.exitPointerLock?.();
4185  reset();
4186  hideRoundResult();
4187  if (deathScreenEl) deathScreenEl.hidden = true;
4188  menuEl.hidden = false;
4189  namePanel.hidden = true;
4190  settingsPanel.hidden = true;
4191  playBtn.hidden = false;
4192  settingsBtn.hidden = false;
4193  menuCursorEl.hidden = false;
4194  playerNameInput.blur();
4195  statusEl.textContent = text.status;
4196  updateHud();
4197}
4198
4199function respawnAtHospital(message) {
4200  clearZombies();
4201  world.health = 100;
4202  world.energy = 100;
4203  world.turbo = 100;
4204  world.turboHeld = false;
4205  world.over = false;
4206  world.sinking = false;
4207  world.sinkTimer = 0;
4208  world.speed = 0;
4209  world.rpm = 0.18;
4210  world.gear = 1;
4211  world.inCar = false;
4212  world.inPlane = false;
4213  world.planeSpeed = 0;
4214  world.planeLift = 0;
4215  world.aiming = false;
4216  world.grounded = true;
4217  world.jumpVelocity = 0;
4218  world.parachuting = false;
4219  world.parachuteForwardSpeed = 0;
4220  character.visible = true;
4221  world.turboHeld = false;
4222  setParachuteVisible(false);
4223  character.position.copy(hospitalSpawn);
4224  character.rotation.y = 0;
4225  world.cameraYaw = 0;
4226  world.cameraPitch = 0.08;
4227  car.position.copy(hospitalSpawn).add(new THREE.Vector3(3.4, 0.55, 1.7));
4228  car.rotation.set(0, 0, 0);
4229  world.heading = 0;
4230  plane.position.set(militaryBase.x - 56, 0.45, militaryBase.z - 18);
4231  plane.rotation.set(0, Math.PI / 2, 0);
4232  world.planeHeading = Math.PI / 2;
4233  world.wantedLevel = 0;
4234  world.wantedTimer = 0;
4235  world.policeAttackTimer = 0;
4236  clearPoliceDeployment();
4237  statusEl.textContent = message;
4238  updateHud();
4239}
4240
4241function updateCharacter(dt) {
4242  const forward = keys.has("w") || keys.has("arrowup") || held.gas;
4243  const backward = keys.has("s") || keys.has("arrowdown") || held.brake;
4244  const left = keys.has("a") || keys.has("arrowleft") || held.left;
4245  const right = keys.has("d") || keys.has("arrowright") || held.right;
4246  const runningRequested = keys.has("shift");
4247  const previous = character.position.clone();
4248
4249  if (world.parachuting) {
4250    updateParachuteDescent(dt, { forward, backward, left, right });
4251    world.speed = 0;
4252    return;
4253  }
4254
4255  const move = new THREE.Vector3(0, 0, 0);
4256  if (forward) move.z += 1;
4257  if (backward) move.z -= 1;
4258  if (left) move.x += 1;
4259  if (right) move.x -= 1;
4260  const isMoving = move.lengthSq() > 0;
4261  const running = runningRequested && isMoving && world.energy > 1;
4262  if (running) {
4263    world.energy = Math.max(0, world.energy - 24 * dt);
4264  } else {
4265    world.energy = Math.min(100, world.energy + 11 * dt);
4266  }
4267
4268  if (isMoving) {
4269    move.normalize();
4270    const cameraForward = new THREE.Vector3(Math.sin(world.cameraYaw), 0, Math.cos(world.cameraYaw));
4271    const cameraRight = new THREE.Vector3(cameraForward.z, 0, -cameraForward.x);
4272    const direction = cameraForward.multiplyScalar(move.z).add(cameraRight.multiplyScalar(move.x)).normalize();
4273    character.position.addScaledVector(direction, (running ? 11.2 : 7.2) * dt);
4274    world.walkHeading = Math.atan2(direction.x, direction.z);
4275    animateCharacterWalk(clock.elapsedTime * (running ? 13 : 9));
4276  } else {
4277    animateCharacterWalk(0, true);
4278  }
4279  character.rotation.y += angleDelta(world.cameraYaw, character.rotation.y) * 0.26;
4280
4281  if (keys.has(" ") && world.grounded) {
4282    world.jumpVelocity = 7.3;
4283    world.grounded = false;
4284  }
4285  world.jumpVelocity -= 18 * dt;
4286  character.position.y += world.jumpVelocity * dt;
4287  if (character.position.y <= 0) {
4288    character.position.y = 0;
4289    world.jumpVelocity = 0;
4290    world.grounded = true;
4291  }
4292
4293  keepCharacterInside(previous);
4294  collideCharacterWithCity(previous);
4295  world.speed = 0;
4296}
4297
4298function updateParachuteDescent(dt, input) {
4299  const previous = character.position.clone();
4300  setParachuteVisible(true);
4301  world.energy = Math.min(100, world.energy + 8 * dt);
4302
4303  const move = new THREE.Vector3(0, 0, 0);
4304  if (input.forward) move.z += 1;
4305  if (input.backward) move.z -= 1;
4306  if (input.left) move.x += 1;
4307  if (input.right) move.x -= 1;
4308
4309  if (move.lengthSq() > 0) {
4310    move.normalize();
4311    const cameraForward = new THREE.Vector3(Math.sin(world.cameraYaw), 0, Math.cos(world.cameraYaw));
4312    const cameraRight = new THREE.Vector3(cameraForward.z, 0, -cameraForward.x);
4313    const direction = cameraForward.multiplyScalar(move.z).add(cameraRight.multiplyScalar(move.x)).normalize();
4314    character.position.addScaledVector(direction, 4.4 * dt);
4315    world.walkHeading = Math.atan2(direction.x, direction.z);
4316  }
4317
4318  const glide = new THREE.Vector3(Math.sin(world.parachuteHeading), 0, Math.cos(world.parachuteHeading));
4319  character.position.addScaledVector(glide, world.parachuteForwardSpeed * dt);
4320  world.parachuteForwardSpeed = Math.max(1.2, world.parachuteForwardSpeed - 3.2 * dt);
4321  character.position.y -= (keys.has("shift") ? 4.5 : 2.7) * dt;
4322  character.rotation.y += angleDelta(world.cameraYaw, character.rotation.y) * 0.16;
4323  animateCharacterParachute(clock.elapsedTime);
4324  keepCharacterInside(previous);
4325
4326  if (character.position.y <= 0) {
4327    character.position.y = 0;
4328    world.parachuting = false;
4329    world.grounded = true;
4330    world.jumpVelocity = 0;
4331    world.parachuteForwardSpeed = 0;
4332    setParachuteVisible(false);
4333    animateCharacterWalk(0, true);
4334    statusEl.textContent = "Landed with the parachute.";
4335  }
4336}
4337
4338function animateCharacterParachute(time) {
4339  const limbs = character.userData.limbs;
4340  const sway = Math.sin(time * 2.2) * 0.08;
4341  limbs.arms[0].position.set(-0.42, 1.42, 0.04);
4342  limbs.arms[1].position.set(0.42, 1.42, 0.04);
4343  limbs.forearms[0].position.set(-0.55, 1.76, 0.04);
4344  limbs.forearms[1].position.set(0.55, 1.76, 0.04);
4345  limbs.hands[0].position.set(-0.62, 1.94, 0.05);
4346  limbs.hands[1].position.set(0.62, 1.94, 0.05);
4347  limbs.arms[0].rotation.set(-0.24 + sway, 0, -0.52);
4348  limbs.arms[1].rotation.set(-0.24 - sway, 0, 0.52);
4349  limbs.forearms[0].rotation.set(-0.12, 0, -0.2);
4350  limbs.forearms[1].rotation.set(-0.12, 0, 0.2);
4351  limbs.legs[0].rotation.x = 0.12 + sway;
4352  limbs.legs[1].rotation.x = 0.12 - sway;
4353  limbs.shins[0].rotation.x = -0.28;
4354  limbs.shins[1].rotation.x = -0.28;
4355  character.userData.torso.rotation.z = sway * 0.5;
4356  character.userData.shoulders.rotation.z = 0;
4357  character.userData.head.rotation.z = 0;
4358}
4359
4360function animateCharacterWalk(time, idle = false) {
4361  const stride = Math.sin(time);
4362  const oppositeStride = Math.sin(time + Math.PI);
4363  const stepLift = Math.max(0, Math.cos(time));
4364  const oppositeLift = Math.max(0, Math.cos(time + Math.PI));
4365  const swing = idle ? 0 : stride * 0.62;
4366  const counter = idle ? 0 : Math.cos(time) * 0.22;
4367  const limbs = character.userData.limbs;
4368  limbs.arms[0].rotation.x = swing;
4369  limbs.arms[1].rotation.x = -swing;
4370  limbs.forearms[0].rotation.x = swing * 0.55 - 0.18;
4371  limbs.forearms[1].rotation.x = -swing * 0.55 - 0.18;
4372  limbs.hands[0].rotation.x = swing * 0.55;
4373  limbs.hands[1].rotation.x = -swing * 0.55;
4374  if (!world.inCar) {
4375    limbs.arms[0].position.set(-0.36, 1.38, 0.12);
4376    limbs.arms[1].position.set(0.36, 1.38, 0.12);
4377    limbs.forearms[0].position.set(-0.2, 1.24, 0.38);
4378    limbs.forearms[1].position.set(0.28, 1.24, 0.38);
4379    limbs.arms[0].rotation.x = -0.58 + swing * 0.04;
4380    limbs.arms[1].rotation.x = -0.58 - swing * 0.04;
4381    limbs.arms[0].rotation.z = -0.16;
4382    limbs.arms[1].rotation.z = 0.16;
4383    limbs.forearms[0].rotation.x = -0.62;
4384    limbs.forearms[1].rotation.x = -0.62;
4385    limbs.forearms[0].rotation.z = -0.08;
4386    limbs.forearms[1].rotation.z = 0.08;
4387    limbs.hands[0].position.set(-0.08, 1.16, 0.5);
4388    limbs.hands[1].position.set(0.28, 1.17, 0.42);
4389  } else {
4390    limbs.arms[0].position.set(-0.48, 1.36, 0);
4391    limbs.arms[1].position.set(0.48, 1.36, 0);
4392    limbs.forearms[0].position.set(-0.48, 1.02, 0);
4393    limbs.forearms[1].position.set(0.48, 1.02, 0);
4394    limbs.arms[0].rotation.z = 0;
4395    limbs.arms[1].rotation.z = 0;
4396    limbs.forearms[0].rotation.z = 0;
4397    limbs.forearms[1].rotation.z = 0;
4398    limbs.hands[0].position.set(-0.48, 0.78, 0.02);
4399    limbs.hands[1].position.set(0.48, 0.78, 0.02);
4400  }
4401  const legSwingLeft = idle ? 0 : -stride * 0.72;
4402  const legSwingRight = idle ? 0 : -oppositeStride * 0.72;
4403  const kneeLeft = idle ? 0 : stepLift * 0.82 + Math.max(0, -stride) * 0.18;
4404  const kneeRight = idle ? 0 : oppositeLift * 0.82 + Math.max(0, stride) * 0.18;
4405  const footLeft = idle ? 0 : -legSwingLeft * 0.38 - stepLift * 0.42;
4406  const footRight = idle ? 0 : -legSwingRight * 0.38 - oppositeLift * 0.42;
4407  limbs.legs[0].rotation.x = legSwingLeft;
4408  limbs.legs[1].rotation.x = legSwingRight;
4409  limbs.shins[0].rotation.x = kneeLeft;
4410  limbs.shins[1].rotation.x = kneeRight;
4411  limbs.feet[0].rotation.x = footLeft;
4412  limbs.feet[1].rotation.x = footRight;
4413  limbs.legs[0].position.set(-0.134, 0.61, idle ? 0 : stride * 0.12);
4414  limbs.legs[1].position.set(0.134, 0.61, idle ? 0 : oppositeStride * 0.12);
4415  limbs.shins[0].position.set(-0.134, 0.28 + stepLift * 0.05, idle ? 0 : stride * 0.2);
4416  limbs.shins[1].position.set(0.134, 0.28 + oppositeLift * 0.05, idle ? 0 : oppositeStride * 0.2);
4417  limbs.feet[0].position.set(-0.134, 0.04 + stepLift * 0.11, 0.08 + (idle ? 0 : stride * 0.34));
4418  limbs.feet[1].position.set(0.134, 0.04 + oppositeLift * 0.11, 0.08 + (idle ? 0 : oppositeStride * 0.34));
4419  character.userData.torso.rotation.z = counter * 0.16;
4420  character.userData.torso.rotation.x = idle ? 0 : 0.035 + Math.abs(stride) * 0.025;
4421  character.userData.shoulders.rotation.z = -counter * 0.24;
4422  character.userData.head.rotation.z = counter * 0.08;
4423  character.userData.weapon.position.y = 1.32 + (idle ? 0 : Math.sin(time) * 0.018);
4424  character.userData.weapon.rotation.x = -0.1 + (idle ? 0 : Math.sin(time) * 0.025);
4425  if (world.grounded) character.position.y = idle ? 0 : Math.abs(Math.sin(time * 0.5)) * 0.035;
4426}
4427
4428function keepCharacterInside(previous) {
4429  if (character.position.x < worldMinX || character.position.x > worldMaxX || character.position.z < worldMinZ || character.position.z > worldMaxZ) {
4430    character.position.copy(previous);
4431  }
4432}
4433
4434function collideCharacterWithCity(previous) {
4435  for (const building of buildings) {
4436    const dx = Math.abs(character.position.x - building.mesh.position.x);
4437    const dz = Math.abs(character.position.z - building.mesh.position.z);
4438    if (dx < building.halfX + 0.25 && dz < building.halfZ + 0.25) {
4439      const inDoorWidth = Math.abs(character.position.x - building.doorX) < building.doorHalfX;
4440      const inDoorDepth = character.position.z > building.lobbyBackZ && character.position.z < building.doorZ + 1.45;
4441      if (inDoorWidth && inDoorDepth) continue;
4442      character.position.copy(previous);
4443      return;
4444    }
4445  }
4446
4447  for (const obstacle of obstacles) {
4448    const dx = Math.abs(character.position.x - obstacle.x);
4449    const dz = Math.abs(character.position.z - obstacle.z);
4450    if (dx < obstacle.halfX + 0.28 && dz < obstacle.halfZ + 0.28) {
4451      character.position.copy(previous);
4452      return;
4453    }
4454  }
4455
4456  for (const prop of props) {
4457    const dx = character.position.x - prop.mesh.position.x;
4458    const dz = character.position.z - prop.mesh.position.z;
4459    if (Math.hypot(dx, dz) < prop.radius + 0.28) {
4460      character.position.copy(previous);
4461      return;
4462    }
4463  }
4464}
4465
4466function toggleCarEntry() {
4467  if (world.over) return;
4468  if (world.inPlane) {
4469    if (plane.position.y > 3.2) {
4470      jumpFromPlane();
4471      return;
4472    }
4473    world.inPlane = false;
4474    world.aiming = false;
4475    world.planeSpeed = 0;
4476    const side = new THREE.Vector3(Math.cos(world.planeHeading), 0, -Math.sin(world.planeHeading));
4477    character.position.copy(plane.position).addScaledVector(side, 5.6);
4478    character.position.y = Math.max(0, plane.position.y - 0.45);
4479    character.rotation.y = world.planeHeading;
4480    character.visible = true;
4481    statusEl.textContent = "On foot: use WASD to walk, E to enter a vehicle.";
4482    return;
4483  }
4484  if (world.inCar) {
4485    world.inCar = false;
4486    world.aiming = false;
4487    world.speed = 0;
4488    const side = new THREE.Vector3(Math.cos(world.heading), 0, -Math.sin(world.heading));
4489    character.position.copy(car.position).addScaledVector(side, 2.35);
4490    character.position.y = 0;
4491    character.rotation.y = world.heading;
4492    character.visible = true;
4493    statusEl.textContent = "On foot: use WASD to walk, E to enter the car.";
4494    return;
4495  }
4496
4497  const planeDistance = Math.hypot(character.position.x - plane.position.x, character.position.z - plane.position.z);
4498  if (planeDistance < 9.5) {
4499    world.inPlane = true;
4500    world.inCar = false;
4501    world.aiming = false;
4502    world.planeHeading = plane.rotation.y;
4503    world.planeSpeed = Math.max(world.planeSpeed, 0);
4504    character.visible = false;
4505    statusEl.textContent = "In the plane: W speeds up, S slows down, A/D turns, Space climbs, Shift descends, left click fires, E exits or parachutes.";
4506    return;
4507  }
4508
4509  const distance = Math.hypot(character.position.x - car.position.x, character.position.z - car.position.z);
4510  if (distance < 3.2) {
4511    world.inCar = true;
4512    world.aiming = false;
4513    character.visible = false;
4514    statusEl.textContent = "In the car: W/S throttle-brake, A/D steering, E to exit.";
4515  } else {
4516  statusEl.textContent = "Move a little closer to enter a vehicle.";
4517  }
4518}
4519
4520function jumpFromPlane() {
4521  world.inPlane = false;
4522  world.inCar = false;
4523  world.aiming = false;
4524  world.parachuting = true;
4525  world.grounded = false;
4526  world.jumpVelocity = 0;
4527  world.parachuteHeading = world.planeHeading;
4528  world.parachuteForwardSpeed = THREE.MathUtils.clamp(world.planeSpeed * 0.32, 7, 18);
4529
4530  const back = new THREE.Vector3(-Math.sin(world.planeHeading), 0, -Math.cos(world.planeHeading));
4531  const side = new THREE.Vector3(Math.cos(world.planeHeading), 0, -Math.sin(world.planeHeading));
4532  character.position.copy(plane.position).addScaledVector(back, 4.4).addScaledVector(side, -1.4);
4533  character.position.y = Math.max(6, plane.position.y - 1.2);
4534  character.rotation.y = world.planeHeading;
4535  character.visible = true;
4536  setParachuteVisible(true);
4537  world.cameraYaw = world.planeHeading;
4538  world.cameraPitch = 0.02;
4539  statusEl.textContent = "Parachute opened. Glide down with WASD.";
4540}
4541
4542function updatePlane(dt) {
4543  const gas = keys.has("w") || keys.has("arrowup") || held.gas;
4544  const brake = keys.has("s") || keys.has("arrowdown") || held.brake;
4545  const left = keys.has("a") || keys.has("arrowleft") || held.left;
4546  const right = keys.has("d") || keys.has("arrowright") || held.right;
4547  const climb = keys.has(" ");
4548  const descend = keys.has("x");
4549  const turboActive = keys.has("shift") && world.turbo > 1;
4550
4551  if (gas) world.planeSpeed += (plane.position.y > 1.2 ? 18 : 11) * dt;
4552  if (turboActive) world.planeSpeed += (plane.position.y > 1.2 ? 34 : 22) * dt;
4553  if (brake) world.planeSpeed -= 16 * dt;
4554  if (!gas && !brake) world.planeSpeed -= world.planeSpeed * 0.18 * dt;
4555  world.planeSpeed = THREE.MathUtils.clamp(world.planeSpeed, 0, turboActive ? 112 : 78);
4556
4557  const airborne = plane.position.y > 1.15 || world.planeSpeed > 29;
4558  const turnRate = airborne ? 0.82 : 0.48;
4559  if (left) world.planeHeading += dt * turnRate;
4560  if (right) world.planeHeading -= dt * turnRate;
4561
4562  const liftTarget = airborne ? THREE.MathUtils.clamp((world.planeSpeed - 25) / 28, 0, 1) : 0;
4563  const climbPower = climb ? 1 : descend ? -0.85 : 0;
4564  world.planeLift += ((liftTarget + climbPower) - world.planeLift) * Math.min(1, dt * 1.8);
4565  const verticalSpeed = world.planeLift * 7.4 - (world.planeSpeed < 22 ? 4.6 : 0);
4566
4567  const previous = plane.position.clone();
4568  plane.position.x += Math.sin(world.planeHeading) * world.planeSpeed * dt;
4569  plane.position.z += Math.cos(world.planeHeading) * world.planeSpeed * dt;
4570  plane.position.y += verticalSpeed * dt;
4571  if (plane.position.y < 0.45) {
4572    plane.position.y = 0.45;
4573    world.planeLift = Math.max(0, world.planeLift);
4574  }
4575  plane.position.y = Math.min(95, plane.position.y);
4576
4577  plane.rotation.y = world.planeHeading;
4578  plane.rotation.x = THREE.MathUtils.clamp(-world.planeLift * 0.16 + (descend ? 0.08 : 0), -0.22, 0.18);
4579  plane.rotation.z = (Number(right) - Number(left)) * (airborne ? 0.24 : 0.08);
4580  world.rpm = THREE.MathUtils.clamp(0.2 + world.planeSpeed / 80, 0.2, 1);
4581
4582  if (plane.position.y < 2.2) {
4583    collidePlaneOnGround(previous);
4584  }
4585
4586  if (plane.position.x < worldMinX - 80 || plane.position.x > worldMaxX + 80 || plane.position.z < worldMinZ - 80 || plane.position.z > worldMaxZ + 80) {
4587    plane.position.copy(previous);
4588    world.planeSpeed *= 0.45;
4589    statusEl.textContent = "The plane reached the edge of the map.";
4590  }
4591
4592  updatePlaneVapor(dt, turboActive);
4593  world.energy = Math.min(100, world.energy + 8 * dt);
4594  world.turbo = THREE.MathUtils.clamp(world.turbo + (turboActive ? -26 : 12) * dt, 0, 100);
4595}
4596
4597function collidePlaneOnGround(previous) {
4598  for (const building of buildings) {
4599    const dx = Math.abs(plane.position.x - building.mesh.position.x);
4600    const dz = Math.abs(plane.position.z - building.mesh.position.z);
4601    if (dx < building.halfX + 4.7 && dz < building.halfZ + 4.7) {
4602      plane.position.copy(previous);
4603      world.planeSpeed *= 0.35;
4604      hit("You hit a building with the plane.");
4605      return;
4606    }
4607  }
4608  for (const obstacle of obstacles) {
4609    const dx = Math.abs(plane.position.x - obstacle.x);
4610    const dz = Math.abs(plane.position.z - obstacle.z);
4611    if (dx < obstacle.halfX + 4.8 && dz < obstacle.halfZ + 4.8) {
4612      plane.position.copy(previous);
4613      world.planeSpeed *= 0.35;
4614      hit("You hit an object with the plane.");
4615      return;
4616    }
4617  }
4618}
4619
4620function trafficPhase() {
4621  const t = clock.elapsedTime % 20;
4622  if (t < 8) return "vertical";
4623  if (t < 10) return "vertical-yellow";
4624  if (t < 18) return "horizontal";
4625  return "horizontal-yellow";
4626}
4627
4628function laneHasGreen(vertical) {
4629  const phase = trafficPhase();
4630  if (vertical) return phase === "vertical";
4631  return phase === "horizontal";
4632}
4633
4634function updateTraffic() {
4635  const phase = trafficPhase();
4636  for (const light of trafficLights) {
4637    const green = light.facesVertical ? phase === "vertical" : phase === "horizontal";
4638    const yellow = light.facesVertical ? phase === "vertical-yellow" : phase === "horizontal-yellow";
4639    light.bulbs.red.material.color.setHex(!green && !yellow ? light.bulbs.red.bright : light.bulbs.red.dim);
4640    light.bulbs.yellow.material.color.setHex(yellow ? light.bulbs.yellow.bright : light.bulbs.yellow.dim);
4641    light.bulbs.green.material.color.setHex(green ? light.bulbs.green.bright : light.bulbs.green.dim);
4642  }
4643}
4644
4645function updateNpcActors(dt) {
4646  updateNpcCars(dt);
4647  updatePedestrians(dt);
4648}
4649
4650function alertNpcCrowd(position, radius = 46) {
4651  for (const pedestrian of pedestrians) {
4652    if (pedestrian.down) continue;
4653    const dx = pedestrian.mesh.position.x - position.x;
4654    const dz = pedestrian.mesh.position.z - position.z;
4655    const distance = Math.hypot(dx, dz);
4656    if (distance > radius) continue;
4657    pedestrian.panicTimer = 3.5 + Math.random() * 2.2;
4658    pedestrian.panicDir.set(dx, 0, dz).normalize();
4659    if (!Number.isFinite(pedestrian.panicDir.x)) pedestrian.panicDir.set(Math.random() - 0.5, 0, Math.random() - 0.5).normalize();
4660  }
4661  for (const npc of npcCars) {
4662    const distance = Math.hypot(npc.mesh.position.x - position.x, npc.mesh.position.z - position.z);
4663    if (distance < radius * 0.8) npc.panicTimer = 2.5 + Math.random() * 1.8;
4664  }
4665}
4666
4667function updateNpcCars(dt) {
4668  const actor = controlledActor();
4669  for (const npc of npcCars) {
4670    npc.panicTimer = Math.max(0, npc.panicTimer - dt);
4671    npc.laneChangeTimer -= dt;
4672    if (npc.type !== "bus" && npc.laneChangeTimer <= 0 && npc.panicTimer <= 0) {
4673      npc.lane = THREE.MathUtils.clamp(npc.lane + (Math.random() < 0.5 ? -0.45 : 0.45), -2.7, 2.7);
4674      npc.laneChangeTimer = 3.5 + Math.random() * 6;
4675    }
4676    const nextIntersection = Math.round(npc.pos / blockSize) * blockSize;
4677    const distanceToLight = (nextIntersection - npc.pos) * npc.dir;
4678    const nearRed = distanceToLight > 0 && distanceToLight < 12 && !laneHasGreen(npc.vertical);
4679    const distanceToPlayer = Math.hypot(npc.mesh.position.x - actor.position.x, npc.mesh.position.z - actor.position.z);
4680    const playerTooClose = distanceToPlayer < (world.inPlane ? 10 : world.inCar ? 9 : 4.5);
4681    const targetSpeed = nearRed || playerTooClose ? 0 : npc.panicTimer > 0 ? npc.speed * 1.25 : npc.speed;
4682    npc.wait += ((targetSpeed - npc.wait) * Math.min(1, dt * 3.5));
4683    npc.pos += npc.dir * npc.wait * dt;
4684
4685    if (npc.pos > worldSize + 20) {
4686      npc.pos = -worldSize - 20;
4687      npc.speed = npc.type === "bus" ? 5.5 + Math.random() * 3.2 : npc.type === "taxi" ? 9 + Math.random() * 8 : 7 + Math.random() * 10;
4688    }
4689    if (npc.pos < -worldSize - 20) {
4690      npc.pos = worldSize + 20;
4691      npc.speed = npc.type === "bus" ? 5.5 + Math.random() * 3.2 : npc.type === "taxi" ? 9 + Math.random() * 8 : 7 + Math.random() * 10;
4692    }
4693
4694    if (npc.vertical) {
4695      npc.mesh.position.set(npc.centerX + npc.road + npc.lane, 0.08, npc.pos);
4696      npc.mesh.rotation.y = npc.dir > 0 ? 0 : Math.PI;
4697    } else {
4698      npc.mesh.position.set(npc.centerX + npc.pos, 0.08, npc.road + npc.lane);
4699      npc.mesh.rotation.y = npc.dir > 0 ? Math.PI / 2 : -Math.PI / 2;
4700    }
4701    keepNpcOutOfMilitaryBase(npc);
4702  }
4703}
4704
4705function updatePedestrians(dt) {
4706  const actor = controlledActor();
4707  for (const pedestrian of pedestrians) {
4708    if (pedestrian.down) {
4709      pedestrian.mesh.position.y = 0.04;
4710      continue;
4711    }
4712    const dx = pedestrian.mesh.position.x - actor.position.x;
4713    const dz = pedestrian.mesh.position.z - actor.position.z;
4714    const actorDistance = Math.hypot(dx, dz);
4715    const scaryVehicle = (world.inCar && Math.abs(world.speed) > 18 && actorDistance < 15) || (world.inPlane && actorDistance < 20 && plane.position.y < 8);
4716    if (scaryVehicle) {
4717      pedestrian.panicTimer = Math.max(pedestrian.panicTimer, 2.2);
4718      pedestrian.panicDir.set(dx, 0, dz).normalize();
4719    }
4720
4721    pedestrian.panicTimer = Math.max(0, pedestrian.panicTimer - dt);
4722    const panicking = pedestrian.panicTimer > 0;
4723    const walkSpeed = pedestrian.speed * (panicking ? 3.2 : 1);
4724    pedestrian.pos += pedestrian.dir * walkSpeed * dt;
4725    if (pedestrian.pos > worldSize - 18 || pedestrian.pos < -worldSize + 18) pedestrian.dir *= -1;
4726
4727    const sidewalkOffset = roadWidth / 2 + 2.1;
4728    if (pedestrian.horizontal) {
4729      pedestrian.mesh.position.set(pedestrian.centerX + pedestrian.pos, 0, pedestrian.road + pedestrian.side * sidewalkOffset);
4730      pedestrian.mesh.rotation.y = pedestrian.dir > 0 ? Math.PI / 2 : -Math.PI / 2;
4731    } else {
4732      pedestrian.mesh.position.set(pedestrian.centerX + pedestrian.road + pedestrian.side * sidewalkOffset, 0, pedestrian.pos);
4733      pedestrian.mesh.rotation.y = pedestrian.dir > 0 ? 0 : Math.PI;
4734    }
4735    if (panicking && pedestrian.panicDir.lengthSq() > 0.001) {
4736      pedestrian.mesh.position.addScaledVector(pedestrian.panicDir, 2.8 * dt);
4737      pedestrian.mesh.rotation.y = Math.atan2(pedestrian.panicDir.x, pedestrian.panicDir.z);
4738    }
4739    keepPedestrianOutOfMilitaryBase(pedestrian);
4740
4741    const limbs = pedestrian.mesh.userData.limbs;
4742    if (limbs) {
4743      const walk = clock.elapsedTime * walkSpeed * 5.2 + pedestrian.walkSeed;
4744      const stride = Math.sin(walk);
4745      const opposite = Math.sin(walk + Math.PI);
4746      const lift = Math.max(0, Math.cos(walk));
4747      const oppositeLift = Math.max(0, Math.cos(walk + Math.PI));
4748      limbs.arms[0].rotation.x = -stride * (panicking ? 0.9 : 0.55);
4749      limbs.arms[1].rotation.x = -opposite * (panicking ? 0.9 : 0.55);
4750      limbs.forearms[0].rotation.x = -stride * 0.32 - 0.12;
4751      limbs.forearms[1].rotation.x = -opposite * 0.32 - 0.12;
4752      limbs.hands[0].position.z = 0.03 - stride * 0.08;
4753      limbs.hands[1].position.z = 0.03 - opposite * 0.08;
4754      limbs.legs[0].rotation.x = stride * 0.58;
4755      limbs.legs[1].rotation.x = opposite * 0.58;
4756      limbs.shins[0].rotation.x = lift * 0.48;
4757      limbs.shins[1].rotation.x = oppositeLift * 0.48;
4758      limbs.feet[0].position.z = 0.08 + stride * 0.16;
4759      limbs.feet[1].position.z = 0.08 + opposite * 0.16;
4760      pedestrian.mesh.position.y = Math.abs(Math.sin(walk * 0.5)) * (panicking ? 0.06 : 0.025);
4761    }
4762  }
4763}
4764
4765function isInMilitaryBase(x, z, padding = 0) {
4766  return militaryZones.some((zone) => Math.abs(x - zone.x) < zone.halfX + padding && Math.abs(z - zone.z) < zone.halfZ + padding);
4767}
4768
4769function keepNpcOutOfMilitaryBase(npc) {
4770  if (!isInMilitaryBase(npc.mesh.position.x, npc.mesh.position.z, 2.5)) return;
4771  npc.dir *= -1;
4772  npc.pos += npc.dir * 18;
4773  npc.wait = 0;
4774  npc.panicTimer = 2.2;
4775}
4776
4777function keepPedestrianOutOfMilitaryBase(pedestrian) {
4778  if (!isInMilitaryBase(pedestrian.mesh.position.x, pedestrian.mesh.position.z, 1.6)) return;
4779  pedestrian.dir *= -1;
4780  pedestrian.pos += pedestrian.dir * 12;
4781  pedestrian.panicTimer = 2.5;
4782  pedestrian.panicDir
4783    .set(pedestrian.mesh.position.x - militaryBase.x, 0, pedestrian.mesh.position.z - militaryBase.z)
4784    .normalize();
4785}
4786
4787function raiseWanted(level = 1) {
4788  const wasCalm = world.wantedLevel <= 0;
4789  world.wantedLevel = Math.min(5, Math.max(world.wantedLevel, level));
4790  world.wantedTimer = 28 + world.wantedLevel * 12;
4791  if (wasCalm || policeCars.some((police) => !police.deployed)) {
4792    deployPoliceAround(controlledActor().position);
4793  }
4794}
4795
4796function deployPoliceAround(position) {
4797  const count = Math.min(5, policeCars.length, policeOfficers.length);
4798  if (count <= 0) return;
4799
4800  for (let i = 0; i < count; i += 1) {
4801    const angle = (i / count) * Math.PI * 2 + Math.PI / 5;
4802    const surroundOffset = new THREE.Vector3(Math.sin(angle) * 14.5, 0, Math.cos(angle) * 14.5);
4803    const spawnOffset = new THREE.Vector3(Math.sin(angle) * (72 + i * 5), 0, Math.cos(angle) * (72 + i * 5));
4804    const carPos = new THREE.Vector3(
4805      THREE.MathUtils.clamp(position.x + spawnOffset.x, worldMinX + 12, worldMaxX - 12),
4806      0.08,
4807      THREE.MathUtils.clamp(position.z + spawnOffset.z, worldMinZ + 12, worldMaxZ - 12),
4808    );
4809    const police = policeCars[i];
4810    const officer = policeOfficers[i];
4811    police.deployed = true;
4812    police.officerOut = false;
4813    police.mode = "responding";
4814    police.arrestCenter = null;
4815    police.holdPosition = null;
4816    police.surroundOffset.copy(surroundOffset);
4817    police.speed = 12 + i * 0.7;
4818    police.mesh.visible = true;
4819    police.mesh.position.copy(carPos);
4820    police.heading = Math.atan2(position.x - carPos.x, position.z - carPos.z);
4821    police.mesh.rotation.y = police.heading;
4822
4823    officer.deployed = true;
4824    officer.mesh.visible = false;
4825    officer.mesh.position.set(carPos.x, 0, carPos.z);
4826    officer.mesh.rotation.y = police.heading;
4827  }
4828
4829  policeHelicopter.visible = true;
4830  policeHelicopter.position.set(position.x - 52, 30, position.z - 52);
4831  policeHelicopter.lookAt(position.x, 0, position.z);
4832  statusEl.textContent = `Police are responding. Wanted level ${world.wantedLevel}.`;
4833}
4834
4835function clearPoliceDeployment() {
4836  for (const police of policeCars) {
4837    police.deployed = false;
4838    police.officerOut = false;
4839    police.mode = "hidden";
4840    police.arrestCenter = null;
4841    police.holdPosition = null;
4842    police.speed = 0;
4843    police.mesh.visible = false;
4844    const sirens = police.mesh.userData.sirenLights;
4845    if (sirens) {
4846      sirens[0].visible = false;
4847      sirens[1].visible = false;
4848    }
4849  }
4850
4851  for (const officer of policeOfficers) {
4852    officer.deployed = false;
4853    officer.mesh.visible = false;
4854  }
4855  policeHelicopter.visible = false;
4856}
4857
4858function updatePolice(dt) {
4859  if (world.wantedLevel <= 0) {
4860    clearPoliceDeployment();
4861    return;
4862  }
4863
4864  const target = controlledActor();
4865  world.wantedTimer = Math.max(0, world.wantedTimer - dt);
4866  if (world.wantedTimer <= 0) {
4867    world.wantedLevel = 0;
4868    clearPoliceDeployment();
4869    return;
4870  }
4871  world.policeAttackTimer = Math.max(0, world.policeAttackTimer - dt);
4872
4873  if (!policeCars.some((police) => police.deployed)) deployPoliceAround(target.position);
4874
4875  const targetSpeed = Math.abs(world.speed) + (world.inPlane ? Math.abs(world.planeSpeed) : 0);
4876  const walkingAway = !isInVehicle() && (keys.has("w") || keys.has("a") || keys.has("s") || keys.has("d") || keys.has("arrowup") || keys.has("arrowdown") || keys.has("arrowleft") || keys.has("arrowright"));
4877  const targetIsEscaping = world.inCar || world.inPlane || targetSpeed > 9;
4878  const heliOffset = new THREE.Vector3(
4879    -Math.sin(controlledHeading()) * 18,
4880    30,
4881    -Math.cos(controlledHeading()) * 18,
4882  );
4883  policeHelicopter.visible = true;
4884  policeHelicopter.position.lerp(target.position.clone().add(heliOffset), Math.min(1, dt * 1.6));
4885  policeHelicopter.lookAt(target.position.x, target.position.y + 1.5, target.position.z);
4886  const rotors = policeHelicopter.userData.rotors;
4887  if (rotors) {
4888    for (const rotor of rotors) rotor.rotation.y += dt * 24;
4889  }
4890
4891  for (let i = 0; i < policeCars.length; i += 1) {
4892    const police = policeCars[i];
4893    const officer = policeOfficers[i];
4894    if (!police.deployed) continue;
4895
4896    const dx = target.position.x - police.mesh.position.x;
4897    const dz = target.position.z - police.mesh.position.z;
4898    const carDistance = Math.hypot(dx, dz);
4899
4900    const sirens = police.mesh.userData.sirenLights;
4901    if (sirens) {
4902      const flash = Math.floor(clock.elapsedTime * 9) % 2;
4903      sirens[0].visible = flash === 0;
4904      sirens[1].visible = flash === 1;
4905    }
4906
4907    if (targetIsEscaping) police.mode = "chasing";
4908    if (police.mode !== "surrounding" && police.mode !== "shooting" && !targetIsEscaping && carDistance < 17) {
4909      police.mode = "surrounding";
4910      police.arrestCenter = target.position.clone();
4911      police.holdPosition = police.mesh.position.clone();
4912      police.speed = 0;
4913      if (officer) {
4914        const exitDir = new THREE.Vector3(target.position.x - police.mesh.position.x, 0, target.position.z - police.mesh.position.z);
4915        if (exitDir.lengthSq() < 0.001) exitDir.set(0, 0, 1);
4916        exitDir.normalize();
4917        officer.mesh.position.set(police.mesh.position.x + exitDir.x * 2.4, 0, police.mesh.position.z + exitDir.z * 2.4);
4918      }
4919    }
4920    if (police.mode === "surrounding" && police.arrestCenter) {
4921      const escapedRing = Math.hypot(target.position.x - police.arrestCenter.x, target.position.z - police.arrestCenter.z) > 11 || walkingAway;
4922      if (escapedRing) police.mode = "shooting";
4923    }
4924    if (police.mode === "shooting" && carDistance > 34) police.mode = "chasing";
4925
4926    if (police.mode === "responding" || police.mode === "chasing") {
4927      police.officerOut = false;
4928      if (officer) officer.mesh.visible = false;
4929      const desiredSpeed = police.mode === "chasing" ? 22 + world.wantedLevel * 2.6 : 18 + world.wantedLevel * 2;
4930      police.speed += (desiredSpeed - police.speed) * Math.min(1, dt * 1.8);
4931      if (carDistance > 0.3) {
4932        const heading = Math.atan2(dx, dz);
4933        police.heading += angleDelta(heading, police.heading) * Math.min(1, dt * 2.9);
4934        police.mesh.position.x += Math.sin(police.heading) * police.speed * dt;
4935        police.mesh.position.z += Math.cos(police.heading) * police.speed * dt;
4936        police.mesh.rotation.y = police.heading;
4937      }
4938      if (carDistance < 4.2 && world.policeAttackTimer <= 0) {
4939        world.policeAttackTimer = 1.1;
4940        world.health = Math.max(0, world.health - (5 + world.wantedLevel * 2));
4941        statusEl.textContent = `Police caught you. Wanted level ${world.wantedLevel}.`;
4942        pop(target.position, 0x2c72ff, 5);
4943        if (world.health <= 0) showDeathScreen("Police took you to the hospital.");
4944      }
4945      continue;
4946    }
4947
4948    police.officerOut = true;
4949    police.speed *= Math.max(0, 1 - dt * 4);
4950    if (police.holdPosition) police.mesh.position.copy(police.holdPosition);
4951    police.heading += angleDelta(Math.atan2(target.position.x - police.mesh.position.x, target.position.z - police.mesh.position.z), police.heading) * Math.min(1, dt * 5);
4952    police.mesh.rotation.y = police.heading;
4953
4954    if (!officer) continue;
4955    const towardPlayer = new THREE.Vector3(target.position.x - police.mesh.position.x, 0, target.position.z - police.mesh.position.z);
4956    if (towardPlayer.lengthSq() < 0.001) towardPlayer.set(0, 0, 1);
4957    towardPlayer.normalize();
4958    officer.mesh.visible = true;
4959    const standOff = police.mode === "shooting" ? 9.5 : 6.2;
4960    const desiredOfficer = target.position.clone().addScaledVector(towardPlayer, -standOff);
4961    const maxFromCar = 13.5;
4962    const fromCar = desiredOfficer.clone().sub(police.mesh.position);
4963    fromCar.y = 0;
4964    if (fromCar.length() > maxFromCar) desiredOfficer.copy(police.mesh.position).addScaledVector(fromCar.normalize(), maxFromCar);
4965    desiredOfficer.y = 0;
4966    const officerMove = desiredOfficer.clone().sub(officer.mesh.position);
4967    const officerMoveDistance = Math.hypot(officerMove.x, officerMove.z);
4968    if (officerMoveDistance > 0.25) {
4969      const step = Math.min(officerMoveDistance, 3.2 * dt);
4970      officer.mesh.position.x += (officerMove.x / officerMoveDistance) * step;
4971      officer.mesh.position.z += (officerMove.z / officerMoveDistance) * step;
4972      animatePoliceOfficer(officer.mesh, clock.elapsedTime * 8);
4973    }
4974    officer.mesh.rotation.y = Math.atan2(target.position.x - officer.mesh.position.x, target.position.z - officer.mesh.position.z);
4975    posePoliceAim(officer.mesh);
4976
4977    const officerDistance = Math.hypot(target.position.x - officer.mesh.position.x, target.position.z - officer.mesh.position.z);
4978    if (police.mode === "shooting" && officerDistance < 35 && world.policeAttackTimer <= 0) {
4979      world.policeAttackTimer = 0.95;
4980      world.health = Math.max(0, world.health - (4 + world.wantedLevel));
4981      statusEl.textContent = `Police opened fire. Wanted level ${world.wantedLevel}.`;
4982      addPoliceTracer(officer.mesh.position.clone().add(new THREE.Vector3(0, 1.15, 0)), target.position.clone().add(new THREE.Vector3(0, 1.0, 0)));
4983      pop(target.position, 0x2c72ff, 4);
4984      if (world.health <= 0) showDeathScreen("Police took you to the hospital.");
4985    } else if (police.mode === "surrounding" && officerDistance < 15.5) {
4986      statusEl.textContent = `Police have surrounded you. Wanted level ${world.wantedLevel}.`;
4987    }
4988  }
4989}
4990
4991function updateMilitaryZone(dt) {
4992  world.militaryAlertTimer = Math.max(0, world.militaryAlertTimer - dt);
4993  if (world.over || world.wantedLevel >= 3) return;
4994  const actor = controlledActor();
4995  for (const zone of militaryZones) {
4996    const inside = Math.abs(actor.position.x - zone.x) < zone.halfX && Math.abs(actor.position.z - zone.z) < zone.halfZ;
4997    if (!inside) continue;
4998    if (world.militaryAlertTimer <= 0) {
4999      world.militaryAlertTimer = 4.5;
5000      statusEl.textContent = "Restricted military zone. Soldiers called the police.";
5001      raiseWanted(3);
5002    }
5003    return;
5004  }
5005}
5006
5007function addPoliceTracer(start, end) {
5008  const direction = end.clone().sub(start);
5009  const length = Math.max(0.5, direction.length());
5010  const tracer = new THREE.Mesh(
5011    new THREE.CapsuleGeometry(0.018, length, 4, 8),
5012    new THREE.MeshBasicMaterial({ color: 0xfff2a4, transparent: true, opacity: 0.82 }),
5013  );
5014  tracer.position.copy(start).add(end).multiplyScalar(0.5);
5015  tracer.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.normalize());
5016  scene.add(tracer);
5017  shots.push({ mesh: tracer, life: 0.08, startOpacity: 0.82 });
5018}
5019
5020function posePoliceAim(mesh) {
5021  const limbs = mesh.userData.limbs;
5022  if (!limbs) return;
5023  limbs.arms[0].rotation.x = -1.18;
5024  limbs.arms[0].rotation.z = -0.24;
5025  limbs.arms[1].rotation.x = -1.12;
5026  limbs.arms[1].rotation.z = 0.24;
5027  limbs.legs[0].rotation.x = 0.04;
5028  limbs.legs[1].rotation.x = -0.04;
5029}
5030
5031function animatePoliceOfficer(mesh, time) {
5032  const limbs = mesh.userData.limbs;
5033  if (!limbs) return;
5034  const stride = Math.sin(time);
5035  const opposite = Math.sin(time + Math.PI);
5036  limbs.arms[0].rotation.x = -stride * 0.75;
5037  limbs.arms[1].rotation.x = -opposite * 0.75;
5038  limbs.legs[0].rotation.x = stride * 0.62;
5039  limbs.legs[1].rotation.x = opposite * 0.62;
5040}
5041
5042function startEngineSound() {
5043  if (!soundEnabled) return;
5044  if (audio) {
5045    if (audio.context.state === "suspended") audio.context.resume();
5046    return;
5047  }
5048
5049  try {
5050    const context = new AudioContext();
5051    const master = context.createGain();
5052    const low = context.createOscillator();
5053    const mid = context.createOscillator();
5054    const grit = context.createOscillator();
5055    const filter = context.createBiquadFilter();
5056    const rumble = context.createBiquadFilter();
5057    const lowGain = context.createGain();
5058    const midGain = context.createGain();
5059    const gritGain = context.createGain();
5060
5061    low.type = "sawtooth";
5062    mid.type = "triangle";
5063    grit.type = "square";
5064    filter.type = "lowpass";
5065    rumble.type = "lowshelf";
5066    filter.frequency.value = 420;
5067    rumble.frequency.value = 95;
5068    rumble.gain.value = 7;
5069    low.frequency.value = 52;
5070    mid.frequency.value = 78;
5071    grit.frequency.value = 104;
5072    lowGain.gain.value = 0.025;
5073    midGain.gain.value = 0.018;
5074    gritGain.gain.value = 0.006;
5075    master.gain.value = 0.18;
5076
5077    low.connect(lowGain).connect(filter);
5078    mid.connect(midGain).connect(filter);
5079    grit.connect(gritGain).connect(filter);
5080    filter.connect(rumble).connect(master).connect(context.destination);
5081    low.start();
5082    mid.start();
5083    grit.start();
5084
5085    audio = { context, low, mid, grit, filter, rumble, lowGain, midGain, gritGain, master };
5086  } catch {
5087    audio = null;
5088  }
5089}
5090
5091function updateEngineSound(gas, brake) {
5092  if (!audio) return;
5093  const now = audio.context.currentTime;
5094  if (!soundEnabled) {
5095    audio.master.gain.setTargetAtTime(0, now, 0.08);
5096    return;
5097  }
5098  const rev = THREE.MathUtils.clamp(world.rpm, 0.12, 1);
5099  const throttle = gas ? 1 : brake ? 0.2 : 0.42;
5100  const shiftDip = world.shiftTimer > 0 ? 0.72 : 1;
5101  const idleShake = Math.sin(clock.elapsedTime * 42) * 1.8;
5102  const base = (38 + rev * 118 + throttle * 16 + idleShake) * shiftDip;
5103  audio.low.frequency.setTargetAtTime(base, now, 0.075);
5104  audio.mid.frequency.setTargetAtTime(base * 1.52, now, 0.075);
5105  audio.grit.frequency.setTargetAtTime(base * 2.04, now, 0.075);
5106  audio.filter.frequency.setTargetAtTime(240 + rev * 980 + throttle * 240, now, 0.09);
5107  audio.lowGain.gain.setTargetAtTime(0.02 + throttle * 0.02 + rev * 0.014, now, 0.08);
5108  audio.midGain.gain.setTargetAtTime(0.012 + throttle * 0.016 + rev * 0.018, now, 0.08);
5109  audio.gritGain.gain.setTargetAtTime(0.002 + throttle * 0.006 + rev * 0.01, now, 0.08);
5110  audio.rumble.gain.setTargetAtTime(5 + throttle * 4 - rev * 2, now, 0.1);
5111  audio.master.gain.setTargetAtTime(world.over ? 0.02 : 0.18 + throttle * 0.04, now, 0.12);
5112}
5113
5114function keepInsideWorld(previous) {
5115  if (car.position.x < worldMinX || car.position.x > worldMaxX || car.position.z < worldMinZ || car.position.z > worldMaxZ) {
5116    startCarSinking();
5117  }
5118}
5119
5120function checkCarWaterEntry() {
5121  if (!world.inCar || world.sinking || world.over) return;
5122  if (isCarOnBridge()) return;
5123  if (isPointInLand(car.position.x, car.position.z)) return;
5124  startCarSinking();
5125}
5126
5127function isPointInLand(x, z) {
5128  const beachLimit = worldSize * 1.17;
5129  return districtCenters.some((centerX) => Math.abs(x - centerX) <= beachLimit && Math.abs(z) <= beachLimit);
5130}
5131
5132function isCarOnBridge() {
5133  for (let i = 0; i < districtCenters.length - 1; i += 1) {
5134    const startX = districtCenters[i] + worldSize - 8;
5135    const endX = districtCenters[i + 1] - worldSize + 8;
5136    if (car.position.x > startX && car.position.x < endX && Math.abs(car.position.z) < roadWidth / 2 + 4.5) return true;
5137  }
5138  return false;
5139}
5140
5141function startCarSinking() {
5142  if (world.sinking || world.over) return;
5143  world.sinking = true;
5144  world.sinkTimer = 0;
5145  world.sinkStartY = car.position.y;
5146  world.speed *= 0.35;
5147  setBrakeLights(true);
5148  statusEl.textContent = "The car fell into the sea.";
5149  pop(car.position, 0x8eeaff, 18);
5150}
5151
5152function updateCarSinking(dt) {
5153  world.sinkTimer += dt;
5154  const progress = THREE.MathUtils.clamp(world.sinkTimer / 3.2, 0, 1);
5155  world.speed *= 1 - Math.min(1, dt * 2.2);
5156  car.position.x += Math.sin(world.heading) * world.speed * dt;
5157  car.position.z += Math.cos(world.heading) * world.speed * dt;
5158  car.position.y = world.sinkStartY - THREE.MathUtils.smoothstep(progress, 0, 1) * 3.2;
5159  car.rotation.y = world.heading + Math.sin(world.sinkTimer * 2.4) * 0.035;
5160  car.rotation.x = -0.12 - progress * 0.42;
5161  car.rotation.z = Math.sin(world.sinkTimer * 3.1) * 0.16 + progress * 0.28;
5162
5163  if (world.sinkTimer < 1.45 && Math.random() < 0.45) {
5164    pop(car.position.clone().add(new THREE.Vector3((Math.random() - 0.5) * 2.2, 0.2, (Math.random() - 0.5) * 2.2)), 0xbff7ff, 2);
5165  }
5166
5167  if (progress >= 1) {
5168    showDeathScreen("The car sank. You woke up at the hospital.");
5169  }
5170}
5171
5172function collideWithCity(previous) {
5173  for (const building of buildings) {
5174    const dx = Math.abs(car.position.x - building.mesh.position.x);
5175    const dz = Math.abs(car.position.z - building.mesh.position.z);
5176    if (dx < building.halfX + 1.1 && dz < building.halfZ + 1.7) {
5177      car.position.copy(previous);
5178      world.speed *= -0.28;
5179      hit("You hit a building.");
5180      return;
5181    }
5182  }
5183
5184  for (const prop of props) {
5185    const dx = car.position.x - prop.mesh.position.x;
5186    const dz = car.position.z - prop.mesh.position.z;
5187    if (Math.hypot(dx, dz) < prop.radius + 1.15) {
5188      if (!prop.fixed) {
5189        prop.mesh.position.x += dx * 0.18;
5190        prop.mesh.position.z += dz * 0.18;
5191      }
5192      world.speed *= 0.72;
5193      hit("You hit a street object.");
5194      pop(prop.mesh.position, 0xf0693f, 8);
5195      return;
5196    }
5197  }
5198
5199  for (const obstacle of obstacles) {
5200    const dx = Math.abs(car.position.x - obstacle.x);
5201    const dz = Math.abs(car.position.z - obstacle.z);
5202    if (dx < obstacle.halfX + 1.1 && dz < obstacle.halfZ + 1.65) {
5203      car.position.copy(previous);
5204      world.speed *= -0.32;
5205      hit(obstacle.label || "You hit an object.");
5206      return;
5207    }
5208  }
5209}
5210
5211function updateAutomaticDoors(dt) {
5212  const actor = controlledActor();
5213  const speed = Math.min(1, dt * 5.5);
5214  for (const door of autoDoors) {
5215    const distance = Math.hypot(actor.position.x - door.x, actor.position.z - door.z);
5216    const target = distance < 6.5 ? 1 : 0;
5217    door.open += (target - door.open) * speed;
5218    const leftOffset = THREE.MathUtils.lerp(door.closedOffset, door.openOffset, door.open);
5219    const rightOffset = THREE.MathUtils.lerp(door.closedOffset, door.openOffset, door.open);
5220    door.leftDoor.position.x = door.x - leftOffset;
5221    door.rightDoor.position.x = door.x + rightOffset;
5222  }
5223  for (const gate of parkingGates) {
5224    const distance = Math.hypot(actor.position.x - gate.x, actor.position.z - gate.z);
5225    const target = distance < 8 ? 1 : 0;
5226    gate.open += (target - gate.open) * speed;
5227    gate.gate.rotation.z = THREE.MathUtils.lerp(0, Math.PI / 2, gate.open);
5228  }
5229}
5230
5231function collectPickups() {
5232  for (const pickup of pickups) {
5233    pickup.mesh.rotation.z += 0.04;
5234    pickup.mesh.position.y = 1.1 + Math.sin(clock.elapsedTime * 3 + pickup.mesh.position.x) * 0.13;
5235    if (pickup.taken) continue;
5236    const dx = car.position.x - pickup.mesh.position.x;
5237    const dz = car.position.z - pickup.mesh.position.z;
5238    if (Math.hypot(dx, dz) < 2.6) {
5239      pickup.taken = true;
5240      pickup.mesh.visible = false;
5241      statusEl.textContent = pickup.type === "mission" ? "Mission point completed." : "Cash collected.";
5242      pop(pickup.mesh.position, pickup.type === "mission" ? 0x42d67d : 0xf3c84b, 16);
5243    }
5244  }
5245}
5246
5247function hit(message) {
5248  world.health -= 7;
5249  statusEl.textContent = message;
5250  pop(controlledActor().position, 0xff563b, 10);
5251}
5252
5253function pop(position, color, count) {
5254  for (let i = 0; i < count; i += 1) {
5255    const mesh = new THREE.Mesh(new THREE.SphereGeometry(0.09, 8, 8), new THREE.MeshBasicMaterial({ color }));
5256    mesh.position.copy(position);
5257    scene.add(mesh);
5258    sparks.push({
5259      mesh,
5260      life: 0.35 + Math.random() * 0.55,
5261      size: 1.6 + Math.random() * 2.5,
5262      velocity: new THREE.Vector3((Math.random() - 0.5) * 9, 2 + Math.random() * 5, (Math.random() - 0.5) * 9),
5263    });
5264  }
5265}
5266
5267function updateSparks(dt) {
5268  for (let i = sparks.length - 1; i >= 0; i -= 1) {
5269    const spark = sparks[i];
5270    spark.life -= dt;
5271    spark.velocity.y -= 7 * dt;
5272    spark.mesh.position.addScaledVector(spark.velocity, dt);
5273    spark.mesh.scale.setScalar(Math.max(0.01, spark.life * spark.size));
5274    if (spark.life <= 0) {
5275      scene.remove(spark.mesh);
5276      sparks.splice(i, 1);
5277    }
5278  }
5279}
5280
5281function updateExhaustSmoke(dt, gas) {
5282  smokeTimer -= dt;
5283  const absSpeed = Math.abs(world.speed);
5284  const interval = gas ? 0.075 : absSpeed > 1 ? 0.14 : 0.28;
5285  if (smokeTimer > 0) return;
5286  smokeTimer = interval;
5287  while (smokePuffs.length > 80) {
5288    const oldSmoke = smokePuffs.shift();
5289    if (oldSmoke) scene.remove(oldSmoke.mesh);
5290  }
5291  const back = new THREE.Vector3(-Math.sin(world.heading), 0, -Math.cos(world.heading));
5292  const side = new THREE.Vector3(Math.cos(world.heading), 0, -Math.sin(world.heading));
5293  for (const port of exhaustPorts) {
5294    const worldPort = port.clone().applyAxisAngle(new THREE.Vector3(0, 1, 0), world.heading).add(car.position);
5295    const mesh = new THREE.Mesh(
5296      new THREE.SphereGeometry(0.12 + Math.random() * 0.05, 8, 8),
5297      new THREE.MeshBasicMaterial({ color: 0xb8b8b8, transparent: true, op
5297acity: gas ? 0.34 : 0.22 }),
5298    );
5299    mesh.position.copy(worldPort).addScaledVector(back, 0.12);
5300    scene.add(mesh);
5301    smokePuffs.push({
5302      mesh,
5303      life: 0.85 + Math.random() * 0.35,
5304      maxLife: 0.85,
5305      velocity: back
5306        .clone()
5307        .multiplyScalar(0.8 + Math.random() * 0.8 + absSpeed * 0.012)
5308        .addScaledVector(side, (Math.random() - 0.5) * 0.35)
5309        .add(new THREE.Vector3(0, 0.28 + Math.random() * 0.22, 0)),
5310      grow: 1.8 + Math.random() * 1.6,
5311    });
5312  }
5313}
5314
5315function updatePlaneVapor(dt, turboActive) {
5316  if (!world.inPlane || world.planeSpeed < 10) return;
5317  smokeTimer -= dt;
5318  const fast = THREE.MathUtils.clamp((world.planeSpeed - 14) / 72, 0, 1);
5319  const interval = turboActive ? 0.035 : THREE.MathUtils.lerp(0.11, 0.045, fast);
5320  if (smokeTimer > 0) return;
5321  smokeTimer = interval;
5322  while (smokePuffs.length > 92) {
5323    const oldSmoke = smokePuffs.shift();
5324    if (oldSmoke) scene.remove(oldSmoke.mesh);
5325  }
5326
5327  const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(plane.quaternion).normalize();
5328  const back = forward.clone().multiplyScalar(-1);
5329  const up = new THREE.Vector3(0, 1, 0);
5330  const enginePort = new THREE.Vector3(0, 1.12, -6.45).applyQuaternion(plane.quaternion).add(plane.position);
5331  addPlaneVaporPuff(
5332    enginePort,
5333    back.clone().multiplyScalar(3.6 + world.planeSpeed * 0.035).addScaledVector(up, 0.18),
5334    0xbfc4c6,
5335    turboActive ? 0.48 : 0.34,
5336    0.2,
5337    2.8,
5338  );
5339
5340  if (world.planeSpeed < 24) return;
5341  for (const x of [-6.1, 6.1]) {
5342    const wingTip = new THREE.Vector3(x, 1.05, -0.62).applyQuaternion(plane.quaternion).add(plane.position);
5343    addPlaneVaporPuff(
5344      wingTip,
5345      back.clone().multiplyScalar(4.8 + world.planeSpeed * 0.055).add(new THREE.Vector3(0, 0.05, 0)),
5346      0xeaf8ff,
5347      0.36 + fast * 0.18,
5348      0.095,
5349      5.2 + fast * 2.4,
5350    );
5351  }
5352}
5353
5354function addPlaneVaporPuff(position, velocity, color, opacity, radius, grow) {
5355  const mesh = new THREE.Mesh(
5356    new THREE.SphereGeometry(radius + Math.random() * radius * 0.35, 8, 8),
5357    new THREE.MeshBasicMaterial({ color, transparent: true, opacity, depthWrite: false }),
5358  );
5359  mesh.position.copy(position);
5360  scene.add(mesh);
5361  smokePuffs.push({
5362    mesh,
5363    life: 0.72 + Math.random() * 0.28,
5364    maxLife: 0.82,
5365    velocity: velocity.add(new THREE.Vector3((Math.random() - 0.5) * 0.28, (Math.random() - 0.5) * 0.12, (Math.random() - 0.5) * 0.28)),
5366    grow,
5367  });
5368}
5369
5370function updateSmoke(dt) {
5371  for (let i = smokePuffs.length - 1; i >= 0; i -= 1) {
5372    const smoke = smokePuffs[i];
5373    smoke.life -= dt;
5374    smoke.mesh.position.addScaledVector(smoke.velocity, dt);
5375    smoke.velocity.y += 0.18 * dt;
5376    const age = 1 - Math.max(0, smoke.life) / smoke.maxLife;
5377    smoke.mesh.scale.setScalar(1 + age * smoke.grow);
5378    smoke.mesh.material.opacity = Math.max(0, smoke.life / smoke.maxLife) * 0.32;
5379    if (smoke.life <= 0) {
5380      scene.remove(smoke.mesh);
5381      smokePuffs.splice(i, 1);
5382    }
5383  }
5384}
5385
5386function shootWeapon() {
5387  if (isInVehicle() || world.over) return;
5388  character.rotation.y += angleDelta(world.cameraYaw, character.rotation.y) * 0.7;
5389  world.walkHeading = world.cameraYaw;
5390
5391  const forward = new THREE.Vector3(Math.sin(world.cameraYaw), 0, Math.cos(world.cameraYaw)).normalize();
5392  const start = character.position.clone().add(new THREE.Vector3(0, 1.28, 0)).addScaledVector(forward, 0.95);
5393  const cameraRay = getAimRayDirection();
5394  const maxDistance = 130 + world.weaponLevel * 35;
5395  const tracerLength = 5.8 + world.weaponLevel * 0.65;
5396  const shotCount = weaponShotCount();
5397  const damage = weaponDamage();
5398  const spreadStep = shotCount === 1 ? 0 : 0.018;
5399  const flash = new THREE.Mesh(new THREE.SphereGeometry(0.16 + shotCount * 0.025, 10, 8), mats.muzzle);
5400  flash.position.copy(start).addScaledVector(forward, 0.42);
5401  scene.add(flash);
5402
5403  for (let i = 0; i < shotCount; i += 1) {
5404    const offset = (i - (shotCount - 1) / 2) * spreadStep;
5405    const direction = getAimDirection(start).applyAxisAngle(new THREE.Vector3(0, 1, 0), offset).normalize();
5406    const cameraDirection = cameraRay.clone().applyAxisAngle(new THREE.Vector3(0, 1, 0), offset).normalize();
5407    const hit = hitZombieRay(camera.position, cameraDirection, maxDistance, damage);
5408    const npcHit = hitPedestrianRay(camera.position, cameraDirection, maxDistance);
5409    if (!hit && !npcHit) {
5410      hitZombieRay(start, direction, maxDistance, damage);
5411      hitPedestrianRay(start, direction, maxDistance);
5412    }
5413
5414    const tracer = new THREE.Mesh(new THREE.CapsuleGeometry(0.022 + world.weaponLevel * 0.006, tracerLength, 4, 8), mats.tracer);
5415    tracer.position.copy(start).addScaledVector(direction, tracerLength / 2);
5416    tracer.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
5417    scene.add(tracer);
5418    shots.push({
5419      mesh: tracer,
5420      life: 0.52,
5421      startOpacity: 0.78,
5422      velocity: direction.clone().multiplyScalar(145),
5423      distance: 0,
5424      maxDistance,
5425      damage: hit || npcHit ? 0 : damage,
5426    });
5427  }
5428  shots.push({ mesh: flash, life: 0.06, startOpacity: 0.9 });
5429  statusEl.textContent = shotCount > 1 ? `${shotCount} bullets fired.` : "Shot fired.";
5430  alertNpcCrowd(character.position, 58);
5431  raiseWanted(1);
5432  shotSound();
5433}
5434
5435function shootPlaneGuns() {
5436  if (!world.inPlane || world.over) return;
5437  const direction = new THREE.Vector3(0, 0, 1).applyQuaternion(plane.quaternion).normalize();
5438  const gunPorts = [new THREE.Vector3(-4.35, 0.9, 2.15), new THREE.Vector3(4.35, 0.9, 2.15)];
5439  const maxDistance = 240;
5440  let hitCount = 0;
5441
5442  for (const port of gunPorts) {
5443    const start = port.clone().applyQuaternion(plane.quaternion).add(plane.position);
5444    const hit = hitZombieRay(start, direction, maxDistance, 2);
5445    const npcHit = hitPedestrianRay(start, direction, maxDistance);
5446    if (hit || npcHit) hitCount += 1;
5447
5448    const tracer = new THREE.Mesh(new THREE.CapsuleGeometry(0.038, 9.5, 4, 8), mats.tracer);
5449    tracer.position.copy(start).addScaledVector(direction, 4.75);
5450    tracer.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
5451    scene.add(tracer);
5452    shots.push({
5453      mesh: tracer,
5454      life: 0.62,
5455      startOpacity: 0.86,
5456      velocity: direction.clone().multiplyScalar(190),
5457      distance: 0,
5458      maxDistance,
5459      damage: hit || npcHit ? 0 : 2,
5460    });
5461
5462    const flash = new THREE.Mesh(new THREE.SphereGeometry(0.18, 10, 8), mats.muzzle);
5463    flash.position.copy(start).addScaledVector(direction, 0.45);
5464    scene.add(flash);
5465    shots.push({ mesh: flash, life: 0.055, startOpacity: 0.9 });
5466  }
5467
5468  statusEl.textContent = "Plane guns fired.";
5469  alertNpcCrowd(plane.position, 70);
5470  raiseWanted(2);
5471  shotSound();
5472}
5473
5474function shootPlaneMissile() {
5475  if (!world.inPlane || world.over) return;
5476  const direction = new THREE.Vector3(0, 0, 1).applyQuaternion(plane.quaternion).normalize();
5477  const start = new THREE.Vector3(0, 0.12, -0.72).applyQuaternion(plane.quaternion).add(plane.position);
5478  const missile = new THREE.Group();
5479
5480  const body = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 1.45, 14), mats.carDark);
5481  body.rotation.x = Math.PI / 2;
5482  missile.add(body);
5483
5484  const tip = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.38, 14), mats.chrome);
5485  tip.rotation.x = Math.PI / 2;
5486  tip.position.z = 0.9;
5487  missile.add(tip);
5488
5489  for (const x of [-0.16, 0.16]) {
5490    const fin = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.22, 0.32), mats.gun);
5491    fin.position.set(x, -0.02, -0.48);
5492    missile.add(fin);
5493  }
5494
5495  missile.position.copy(start);
5496  missile.quaternion.copy(plane.quaternion);
5497  scene.add(missile);
5498  shots.push({
5499    mesh: missile,
5500    life: 2.2,
5501    startOpacity: 1,
5502    velocity: direction.clone().multiplyScalar(105 + world.planeSpeed * 0.55).add(new THREE.Vector3(0, -7, 0)),
5503    distance: 0,
5504    maxDistance: 280,
5505    damage: 7,
5506    radius: 2.2,
5507    missile: true,
5508  });
5509  pop(start, 0xffdd66, 9);
5510  statusEl.textContent = "Missile fired.";
5511  alertNpcCrowd(plane.position, 90);
5512  raiseWanted(3);
5513  shotSound();
5514}
5515
5516function hitZombieRay(start, direction, maxDistance, damage = 1) {
5517  let bestZombie = null;
5518  let bestIndex = -1;
5519  let bestDistance = maxDistance;
5520  let bestPoint = null;
5521  for (let i = zombies.length - 1; i >= 0; i -= 1) {
5522    const zombie = zombies[i];
5523    const center = zombie.position.clone().add(new THREE.Vector3(0, 1.05, 0));
5524    const along = center.clone().sub(start).dot(direction);
5525    if (along < 0 || along > maxDistance || along > bestDistance) continue;
5526    const closest = start.clone().addScaledVector(direction, along);
5527    if (closest.distanceTo(center) < zombie.userData.radius + (world.weaponLevel - 1) * 0.28) {
5528      bestZombie = zombie;
5529      bestIndex = i;
5530      bestDistance = along;
5531      bestPoint = closest;
5532    }
5533  }
5534  if (!bestZombie) return false;
5535  pop(bestPoint, 0xffdd66, 6);
5536  damageZombie(bestZombie, bestIndex, damage, bestPoint);
5537  return true;
5538}
5539
5540function hitPedestrianRay(start, direction, maxDistance) {
5541  let bestPedestrian = null;
5542  let bestDistance = maxDistance;
5543  let bestPoint = null;
5544  for (const pedestrian of pedestrians) {
5545    if (pedestrian.down) continue;
5546    const center = pedestrian.mesh.position.clone().add(new THREE.Vector3(0, 1.05, 0));
5547    const along = center.clone().sub(start).dot(direction);
5548    if (along < 0 || along > maxDistance || along > bestDistance) continue;
5549    const closest = start.clone().addScaledVector(direction, along);
5550    if (closest.distanceTo(center) < 0.65) {
5551      bestPedestrian = pedestrian;
5552      bestDistance = along;
5553      bestPoint = closest;
5554    }
5555  }
5556  if (!bestPedestrian) return false;
5557  knockDownPedestrian(bestPedestrian, bestPoint);
5558  return true;
5559}
5560
5561function knockDownPedestrian(pedestrian, hitPoint = pedestrian.mesh.position) {
5562  if (!pedestrian || pedestrian.down) return;
5563  pedestrian.down = true;
5564  pedestrian.panicTimer = 0;
5565  pedestrian.mesh.rotation.x = Math.PI / 2;
5566  pedestrian.mesh.rotation.z = (Math.random() - 0.5) * 0.7;
5567  pedestrian.mesh.position.y = 0.04;
5568  const limbs = pedestrian.mesh.userData.limbs;
5569  if (limbs) {
5570    for (const arm of limbs.arms) arm.rotation.x = -0.9;
5571    for (const forearm of limbs.forearms) forearm.rotation.x = -0.45;
5572    for (const leg of limbs.legs) leg.rotation.x = 0.45;
5573    for (const shin of limbs.shins) shin.rotation.x = -0.35;
5574  }
5575  pop(hitPoint, 0xffdd66, 5);
5576  alertNpcCrowd(pedestrian.mesh.position, 52);
5577  raiseWanted(3);
5578  statusEl.textContent = "NPC knocked down.";
5579}
5580
5581function getAimDirection(start) {
5582  const cameraRay = getAimRayDirection();
5583  const focusWorldPoint = camera.position.clone().addScaledVector(cameraRay, 90);
5584  return focusWorldPoint.sub(start).normalize();
5585}
5586
5587function getAimRayDirection() {
5588  const focusX = Number.parseFloat(focusEl.style.getPropertyValue("--focus-x")) || window.innerWidth / 2;
5589  const focusY = Number.parseFloat(focusEl.style.getPropertyValue("--focus-y")) || window.innerHeight / 2;
5590  const pointer = new THREE.Vector2(
5591    (focusX / window.innerWidth) * 2 - 1,
5592    -(focusY / window.innerHeight) * 2 + 1,
5593  );
5594  return new THREE.Vector3(pointer.x, pointer.y, 0.5).unproject(camera).sub(camera.position).normalize();
5595}
5596
5597function updateShots(dt) {
5598  for (let i = shots.length - 1; i >= 0; i -= 1) {
5599    const shot = shots[i];
5600    shot.life -= dt;
5601    if (shot.velocity) {
5602      const step = shot.velocity.length() * dt;
5603      const previous = shot.mesh.position.clone();
5604      shot.mesh.position.addScaledVector(shot.velocity, dt);
5605      shot.distance += step;
5606      if (shot.damage > 0 && (hitZombieWithShot(shot, previous, shot.mesh.position) || hitPedestrianWithShot(shot, previous, shot.mesh.position))) {
5607        if (shot.missile) pop(shot.mesh.position, 0xff8c3a, 18);
5608        shot.life = 0;
5609      }
5610      if (shot.distance > shot.maxDistance) shot.life = 0;
5611    }
5612    if (shot.mesh.material?.opacity !== undefined) shot.mesh.material.opacity = Math.max(0, shot.life * shot.startOpacity * 14);
5613    if (shot.life <= 0) {
5614      scene.remove(shot.mesh);
5615      shots.splice(i, 1);
5616    }
5617  }
5618}
5619
5620function hitZombieWithShot(shot, from, to) {
5621  const segment = to.clone().sub(from);
5622  const lengthSq = segment.lengthSq();
5623  if (lengthSq <= 0.0001) return false;
5624  for (let i = zombies.length - 1; i >= 0; i -= 1) {
5625    const zombie = zombies[i];
5626    const center = zombie.position.clone().add(new THREE.Vector3(0, 1.05, 0));
5627    const t = THREE.MathUtils.clamp(center.clone().sub(from).dot(segment) / lengthSq, 0, 1);
5628    const closest = from.clone().addScaledVector(segment, t);
5629    if (closest.distanceTo(center) < (shot.radius || zombie.userData.radius + (world.weaponLevel - 1) * 0.28)) {
5630      pop(closest, 0xffdd66, 5);
5631      damageZombie(zombie, i, shot.damage || 1, closest);
5632      return true;
5633    }
5634  }
5635  return false;
5636}
5637
5638function hitPedestrianWithShot(shot, from, to) {
5639  const segment = to.clone().sub(from);
5640  const lengthSq = segment.lengthSq();
5641  if (lengthSq <= 0.0001) return false;
5642  for (const pedestrian of pedestrians) {
5643    if (pedestrian.down) continue;
5644    const center = pedestrian.mesh.position.clone().add(new THREE.Vector3(0, 1.05, 0));
5645    const t = THREE.MathUtils.clamp(center.clone().sub(from).dot(segment) / lengthSq, 0, 1);
5646    const closest = from.clone().addScaledVector(segment, t);
5647    if (closest.distanceTo(center) < (shot.radius || 0.65)) {
5648      knockDownPedestrian(pedestrian, closest);
5649      return true;
5650    }
5651  }
5652  return false;
5653}
5654
5655function damageZombie(zombie, index, damage, hitPoint = zombie.position) {
5656  if (!zombie || zombie.userData.dead) return;
5657  const data = zombie.userData;
5658  data.health = Math.max(0, data.health - damage);
5659  updateZombieHealthBar(zombie);
5660  if (data.health <= 0) {
5661    killZombie(zombie, index);
5662    return;
5663  }
5664  pop(hitPoint, 0x7fa36b, 4);
5665  statusEl.textContent = `Zombie hit: ${data.health}/${data.maxHealth} health.`;
5666}
5667
5668function updateZombieHealthBar(zombie) {
5669  const data = zombie.userData;
5670  if (!data.healthFill || !data.maxHealth) return;
5671  const ratio = THREE.MathUtils.clamp(data.health / data.maxHealth, 0, 1);
5672  data.healthFill.scale.x = ratio;
5673  data.healthFill.position.x = -0.34 * (1 - ratio);
5674  data.healthFill.material.color.setHex(ratio > 0.5 ? 0x35e06f : ratio > 0.25 ? 0xf5c84b : 0xff563b);
5675}
5676
5677function shotSound() {
5678  if (!soundEnabled) return;
5679  try {
5680    startEngineSound();
5681    if (!audio) return;
5682    const now = audio.context.currentTime;
5683    const duration = 0.34;
5684    const sampleRate = audio.context.sampleRate;
5685    const buffer = audio.context.createBuffer(1, Math.floor(sampleRate * duration), sampleRate);
5686    const data = buffer.getChannelData(0);
5687    for (let i = 0; i < data.length; i += 1) {
5688      const t = i / data.length;
5689      const muzzle = t < 0.035 ? 1.9 : t < 0.13 ? 0.7 : 0.28;
5690      const pressure = Math.sin(t * Math.PI * 31) * 0.12 + Math.sin(t * Math.PI * 9) * 0.08;
5691      data[i] = ((Math.random() * 2 - 1) * muzzle + pressure) * Math.pow(1 - t, 5.1);
5692    }
5693
5694    const compressor = audio.context.createDynamicsCompressor();
5695    compressor.threshold.value = -20;
5696    compressor.knee.value = 7;
5697    compressor.ratio.value = 10;
5698    compressor.attack.value = 0.002;
5699    compressor.release.value = 0.12;
5700    compressor.connect(audio.context.destination);
5701
5702    const blast = audio.context.createBufferSource();
5703    const blastFilter = audio.context.createBiquadFilter();
5704    const blastGain = audio.context.createGain();
5705    blast.buffer = buffer;
5706    blastFilter.type = "highpass";
5707    blastFilter.frequency.value = 720;
5708    blastFilter.Q.value = 0.95;
5709    blastGain.gain.setValueAtTime(0.78, now);
5710    blastGain.gain.exponentialRampToValueAtTime(0.001, now + 0.16);
5711    blast.connect(blastFilter).connect(blastGain).connect(compressor);
5712
5713    const body = audio.context.createBufferSource();
5714    const bodyFilter = audio.context.createBiquadFilter();
5715    const bodyGain = audio.context.createGain();
5716    body.buffer = buffer;
5717    bodyFilter.type = "lowpass";
5718    bodyFilter.frequency.value = 460;
5719    bodyFilter.Q.value = 0.82;
5720    bodyGain.gain.setValueAtTime(0.32, now + 0.008);
5721    bodyGain.gain.exponentialRampToValueAtTime(0.001, now + 0.27);
5722    body.connect(bodyFilter).connect(bodyGain).connect(compressor);
5723
5724    const thump = audio.context.createOscillator();
5725    const thumpGain = audio.context.createGain();
5726    thump.type = "sawtooth";
5727    thump.frequency.setValueAtTime(112, now);
5728    thump.frequency.exponentialRampToValueAtTime(38, now + 0.18);
5729    thumpGain.gain.setValueAtTime(0.5, now);
5730    thumpGain.gain.exponentialRampToValueAtTime(0.001, now + 0.21);
5731    thump.connect(thumpGain).connect(compressor);
5732
5733    const crack = audio.context.createOscillator();
5734    const crackGain = audio.context.createGain();
5735    const crackFilter = audio.context.createBiquadFilter();
5736    crack.type = "square";
5737    crack.frequency.setValueAtTime(2450, now);
5738    crack.frequency.exponentialRampToValueAtTime(760, now + 0.05);
5739    crackFilter.type = "bandpass";
5740    crackFilter.frequency.value = 2200;
5741    crackFilter.Q.value = 1.8;
5742    crackGain.gain.setValueAtTime(0.18, now);
5743    crackGain.gain.exponentialRampToValueAtTime(0.001, now + 0.058);
5744    crack.connect(crackFilter).connect(crackGain).connect(compressor);
5745
5746    const actionClick = audio.context.createOscillator();
5747    const clickGain = audio.context.createGain();
5748    const clickFilter = audio.context.createBiquadFilter();
5749    actionClick.type = "triangle";
5750    actionClick.frequency.setValueAtTime(760, now + 0.048);
5751    actionClick.frequency.exponentialRampToValueAtTime(210, now + 0.105);
5752    clickFilter.type = "highpass";
5753    clickFilter.frequency.value = 340;
5754    clickGain.gain.setValueAtTime(0.075, now + 0.048);
5755    clickGain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
5756    actionClick.connect(clickFilter).connect(clickGain).connect(compressor);
5757
5758    const metallic = audio.context.createOscillator();
5759    const metallicGain = audio.context.createGain();
5760    const metallicFilter = audio.context.createBiquadFilter();
5761    metallic.type = "triangle";
5762    metallic.frequency.setValueAtTime(1180, now + 0.032);
5763    metallic.frequency.exponentialRampToValueAtTime(540, now + 0.13);
5764    metallicFilter.type = "bandpass";
5765    metallicFilter.frequency.value = 980;
5766    metallicFilter.Q.value = 2.6;
5767    metallicGain.gain.setValueAtTime(0.045, now + 0.032);
5768    metallicGain.gain.exponentialRampToValueAtTime(0.001, now + 0.14);
5769    metallic.connect(metallicFilter).connect(metallicGain).connect(compressor);
5770
5771    const echo = audio.context.createDelay();
5772    const echoGain = audio.context.createGain();
5773    echo.delayTime.value = 0.13;
5774    echoGain.gain.value = 0.085;
5775    blastGain.connect(echo).connect(echoGain).connect(compressor);
5776
5777    blast.start(now);
5778    body.start(now + 0.012);
5779    thump.start(now);
5780    crack.start(now);
5781    actionClick.start(now + 0.045);
5782    metallic.start(now + 0.032);
5783    body.stop(now + 0.29);
5784    thump.stop(now + 0.22);
5785    crack.stop(now + 0.06);
5786    actionClick.stop(now + 0.13);
5787    metallic.stop(now + 0.15);
5788  } catch {
5789    // Audio is optional.
5790  }
5791}
5792
5793function render() {
5794  const followedHeading = controlledHeading();
5795  if (world.inCar) {
5796    world.cameraYaw = world.heading;
5797  } else if (world.inPlane) {
5798    world.cameraYaw = world.planeHeading;
5799  } else if (world.mouseLookTimer <= 0) {
5800    world.cameraYaw += angleDelta(followedHeading, world.cameraYaw) * 0.08;
5801  }
5802  const back = new THREE.Vector3(Math.sin(world.cameraYaw), 0, Math.cos(world.cameraYaw));
5803  const cameraRight = new THREE.Vector3(Math.cos(world.cameraYaw), 0, -Math.sin(world.cameraYaw));
5804  const target = controlledActor().position.clone().add(new THREE.Vector3(0, world.inPlane ? 1.4 : world.inCar ? 0.55 : 1.35, 0));
5805  const characterZoom = world.aiming ? -2.0 : -4.6;
5806  const characterHeight = world.aiming ? 1.65 : 2.8;
5807  const shoulderOffset = !world.inCar ? (world.aiming ? -0.9 : -1.15) : 0;
5808  const cameraDistance = world.inPlane ? -18 : world.inCar ? -7.2 : characterZoom;
5809  const cameraHeight = world.inPlane ? 7.4 : (world.inCar ? 1.75 : characterHeight) + world.cameraPitch * (world.inCar ? 1.5 : 5);
5810  const cameraPos = target
5811    .clone()
5812    .addScaledVector(back, cameraDistance)
5813    .addScaledVector(cameraRight, world.inPlane ? 0 : shoulderOffset)
5814    .add(new THREE.Vector3(0, cameraHeight, 0));
5815  if (world.inCar || world.inPlane) {
5816    camera.position.copy(cameraPos);
5817  } else {
5818    camera.position.lerp(cameraPos, 0.18);
5819  }
5820  const lookForward = new THREE.Vector3(Math.sin(world.cameraYaw), 0, Math.cos(world.cameraYaw));
5821  const lookTarget = isInVehicle()
5822    ? target.clone().addScaledVector(lookForward, world.inPlane ? 16 : 4.5)
5823    : target.clone().addScaledVector(lookForward, world.aiming ? 5.8 : 0.8).addScaledVector(cameraRight, world.aiming ? -0.35 : -0.36);
5824  camera.lookAt(lookTarget.x, lookTarget.y + (world.inPlane ? 0.45 : world.inCar ? 0.15 : 0.2) - world.cameraPitch * 2, lookTarget.z);
5825  camera.updateMatrixWorld();
5826  updateFocusPoint();
5827  renderer.render(scene, camera);
5828  drawMinimap();
5829}
5830
5831function updateFocusPoint() {
5832  if (isInVehicle() || world.over) {
5833    focusEl.hidden = true;
5834    return;
5835  }
5836
5837  const aimDirection = new THREE.Vector3(Math.sin(world.cameraYaw), 0, Math.cos(world.cameraYaw)).normalize();
5838  const aimPoint = character.position
5839    .clone()
5840    .add(new THREE.Vector3(0, 1.45 - world.cameraPitch * 7.5, 0))
5841    .addScaledVector(aimDirection, 18);
5842  const projected = aimPoint.project(camera);
5843  if (projected.z < -1 || projected.z > 1) {
5844    focusEl.hidden = true;
5845    return;
5846  }
5847
5848  const x = (projected.x * 0.5 + 0.5) * window.innerWidth;
5849  const y = (-projected.y * 0.5 + 0.5) * window.innerHeight;
5850  focusEl.hidden = false;
5851  focusEl.style.setProperty("--focus-x", `${x}px`);
5852  focusEl.style.setProperty("--focus-y", `${y}px`);
5853}
5854
5855function drawMinimap() {
5856  const size = minimap.width;
5857  const center = size / 2;
5858  const scale = size / (worldSize * 2.35);
5859  const followed = controlledActor();
5860  mapCtx.clearRect(0, 0, size, size);
5861  mapCtx.fillStyle = "rgba(8, 13, 13, 0.84)";
5862  mapCtx.fillRect(0, 0, size, size);
5863
5864  mapCtx.save();
5865  mapCtx.translate(center, center);
5866  mapCtx.scale(scale, scale);
5867  mapCtx.translate(-followed.position.x, -followed.position.z);
5868
5869  mapCtx.fillStyle = "rgba(26, 126, 178, 0.9)";
5870  mapCtx.fillRect(worldMinX - 120, -worldSize * 1.45, worldMaxX - worldMinX + 240, worldSize * 2.9);
5871  mapCtx.fillStyle = "#cdb56f";
5872  for (const centerX of districtCenters) {
5873    mapCtx.fillRect(centerX - worldSize * 1.17, -worldSize * 1.17, worldSize * 2.34, worldSize * 2.34);
5874  }
5875  mapCtx.fillStyle = "#264f2e";
5876  for (const centerX of districtCenters) {
5877    mapCtx.fillRect(centerX - worldSize * 1.015, -worldSize * 1.015, worldSize * 2.03, worldSize * 2.03);
5878  }
5879  mapCtx.fillStyle = "#465244";
5880  mapCtx.fillRect(militaryBase.x - militaryBase.halfX, militaryBase.z - militaryBase.halfZ, militaryBase.halfX * 2, militaryBase.halfZ * 2);
5881  mapCtx.fillStyle = "#17191a";
5882  mapCtx.fillRect(militaryBase.x - 63, militaryBase.z - 25, 126, 14);
5883  mapCtx.fillStyle = "rgba(26, 126, 178, 0.9)";
5884  for (let i = 0; i < districtCenters.length - 1; i += 1) {
5885    const startX = districtCenters[i] + worldSize;
5886    const endX = districtCenters[i + 1] - worldSize;
5887    mapCtx.fillRect(startX, -worldSize, endX - startX, worldSize * 2);
5888  }
5889  mapCtx.fillStyle = "#191b1c";
5890  for (const centerX of districtCenters) {
5891    for (let v = -worldSize; v <= worldSize; v += blockSize) {
5892      mapCtx.fillRect(centerX + v - roadWidth / 2, -worldSize, roadWidth, worldSize * 2);
5893      mapCtx.fillRect(centerX - worldSize, v - roadWidth / 2, worldSize * 2, roadWidth);
5894    }
5895  }
5896  mapCtx.fillStyle = "rgba(180, 190, 188, 0.72)";
5897  for (let i = 0; i < districtCenters.length - 1; i += 1) {
5898    const startX = districtCenters[i] + worldSize;
5899    const endX = districtCenters[i + 1] - worldSize;
5900    mapCtx.fillRect(startX, -roadWidth / 2 - 2.5, endX - startX, roadWidth + 5);
5901  }
5902  mapCtx.fillStyle = "rgba(205, 199, 180, 0.9)";
5903  for (const building of buildings) {
5904    const w = building.halfX * 1.2;
5905    const d = building.halfZ * 1.2;
5906    mapCtx.fillRect(building.mesh.position.x - w / 2, building.mesh.position.z - d / 2, w, d);
5907  }
5908  mapCtx.fillStyle = "#d94735";
5909  for (const zombie of zombies) {
5910    mapCtx.beginPath();
5911    mapCtx.arc(zombie.position.x, zombie.position.z, 3.2, 0, Math.PI * 2);
5912    mapCtx.fill();
5913  }
5914  for (const npc of npcCars) {
5915    mapCtx.fillStyle = npc.type === "bus" ? "#d84534" : npc.type === "taxi" ? "#f3c84b" : "#55a7ff";
5916    const w = npc.type === "bus" ? 7.2 : 4.4;
5917    const h = npc.type === "bus" ? 3.0 : 2.4;
5918    mapCtx.fillRect(npc.mesh.position.x - w / 2, npc.mesh.position.z - h / 2, w, h);
5919  }
5920  mapCtx.fillStyle = "#f2f0e8";
5921  for (const pedestrian of pedestrians) {
5922    mapCtx.beginPath();
5923    mapCtx.arc(pedestrian.mesh.position.x, pedestrian.mesh.position.z, 1.8, 0, Math.PI * 2);
5924    mapCtx.fill();
5925  }
5926  for (const bench of weaponBenches) {
5927    const bx = bench.x;
5928    const bz = bench.z;
5929    mapCtx.fillStyle = "#111317";
5930    mapCtx.beginPath();
5931    mapCtx.arc(bx, bz, 6.6, 0, Math.PI * 2);
5932    mapCtx.fill();
5933    mapCtx.strokeStyle = "#f5c84b";
5934    mapCtx.lineWidth = 1.7 / scale;
5935    mapCtx.stroke();
5936    mapCtx.fillStyle = "#f5c84b";
5937    mapCtx.fillRect(bx - 4.2, bz - 0.7, 8.4, 1.4);
5938    mapCtx.fillRect(bx - 0.6, bz - 3.4, 1.2, 2.6);
5939  }
5940  mapCtx.save();
5941  mapCtx.translate(plane.position.x, plane.position.z);
5942  mapCtx.rotate(plane.rotation.y);
5943  mapCtx.fillStyle = "#d7dde2";
5944  mapCtx.fillRect(-7, -1.2, 14, 2.4);
5945  mapCtx.fillRect(-1, -5.2, 2, 10.4);
5946  mapCtx.restore();
5947  mapCtx.restore();
5948
5949  mapCtx.save();
5950  mapCtx.translate(center, center);
5951  mapCtx.rotate(controlledHeading());
5952  mapCtx.fillStyle = world.inPlane ? "#d7dde2" : world.inCar ? "#36a3ff" : "#f4f1e8";
5953  mapCtx.beginPath();
5954  mapCtx.moveTo(0, -9);
5955  mapCtx.lineTo(6, 7);
5956  mapCtx.lineTo(0, 4);
5957  mapCtx.lineTo(-6, 7);
5958  mapCtx.closePath();
5959  mapCtx.fill();
5960  mapCtx.restore();
5961
5962  mapCtx.strokeStyle = "rgba(246, 240, 227, 0.2)";
5963  mapCtx.lineWidth = 2;
5964  mapCtx.strokeRect(1, 1, size - 2, size - 2);
5965}
5966
5967function angleDelta(a, b) {
5968  return Math.atan2(Math.sin(a - b), Math.cos(a - b));
5969}
5970
5971function resize() {
5972  const width = window.innerWidth;
5973  const height = window.innerHeight;
5974  camera.aspect = width / height;
5975  camera.updateProjectionMatrix();
5976  renderer.setSize(width, height, false);
5977}
5978
5979function updateHud() {
5980  const displaySpeed = world.inPlane ? world.planeSpeed : world.speed;
5981  speedEl.textContent = String(Math.abs(Math.round(displaySpeed * 4.2)));
5982  turboEl.style.transform = `scaleX(${THREE.MathUtils.clamp(world.turbo / 100, 0, 1)})`;
5983  killsEl.textContent = String(world.kills);
5984  moneyEl.textContent = world.wantedLevel > 0 ? `Wanted ${"★".repeat(world.wantedLevel)}` : `$${world.money}`;
5985  if (timerEl) timerEl.textContent = "";
5986  healthEl.style.transform = `scaleX(${THREE.MathUtils.clamp(world.health / 100, 0, 1)})`;
5987  energyEl.style.transform = `scaleX(${THREE.MathUtils.clamp(world.energy / 100, 0, 1)})`;
5988  focusEl.hidden = isInVehicle() || world.over;
5989}
5990
5991function startMultiplayer(playerName) {
5992  multiplayerName = playerName;
5993  if (!runsFromWebsite) {
5994    statusEl.textContent = "Open with the server to play multiplayer.";
5995    return;
5996  }
5997  if (multiplayerSocket && multiplayerSocket.readyState <= WebSocket.OPEN) return;
5998  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
5999  multiplayerSocket = new WebSocket(`${protocol}//${window.location.host}/ws`);
6000  multiplayerSocket.addEventListener("open", () => {
6001    multiplayerSocket.send(JSON.stringify({ type: "join", name: multiplayerName }));
6002    statusEl.textContent = "Connected to multiplayer server.";
6003  });
6004  multiplayerSocket.addEventListener("message", (event) => {
6005    try {
6006      handleMultiplayerMessage(JSON.parse(event.data));
6007    } catch {
6008      // Ignore bad network packets.
6009    }
6010  });
6011  multiplayerSocket.addEventListener("close", () => {
6012    statusEl.textContent = "Multiplayer server disconnected.";
6013  });
6014}
6015
6016function handleMultiplayerMessage(message) {
6017  if (message.type === "welcome") {
6018    multiplayerId = message.id;
6019    for (const player of message.players || []) upsertRemotePlayer(player);
6020    return;
6021  }
6022  if (message.type === "player-update") {
6023    if (message.player?.id !== multiplayerId) upsertRemotePlayer(message.player);
6024    return;
6025  }
6026  if (message.type === "player-left") removeRemotePlayer(message.id);
6027}
6028
6029function sendMultiplayerState(dt) {
6030  if (!multiplayerSocket || multiplayerSocket.readyState !== WebSocket.OPEN || !multiplayerId) return;
6031  multiplayerSendTimer -= dt;
6032  if (multiplayerSendTimer > 0) return;
6033  multiplayerSendTimer = 0.08;
6034  const actor = controlledActor();
6035  multiplayerSocket.send(
6036    JSON.stringify({
6037      type: "state",
6038      name: multiplayerName,
6039      mode: world.inPlane ? "plane" : world.inCar ? "car" : "foot",
6040      x: actor.position.x,
6041      y: actor.position.y,
6042      z: actor.position.z,
6043      ry: actor.rotation.y,
6044      speed: world.speed,
6045      health: world.health,
6046    }),
6047  );
6048}
6049
6050function upsertRemotePlayer(player) {
6051  if (!player || player.id === multiplayerId) return;
6052  let remote = remotePlayers.get(player.id);
6053  if (!remote) {
6054    remote = createRemotePlayer(player.name || "Player");
6055    remotePlayers.set(player.id, remote);
6056    scene.add(remote.group);
6057  }
6058  remote.name = player.name || remote.name;
6059  remote.target.position.set(player.x || 0, player.y || 0, player.z || 0);
6060  remote.target.rotationY = Number.isFinite(player.ry) ? player.ry : 0;
6061  remote.mode = player.mode || "car";
6062  remote.label.material.map = makeNameTexture(remote.name);
6063  remote.label.material.needsUpdate = true;
6064}
6065
6066function removeRemotePlayer(id) {
6067  const remote = remotePlayers.get(id);
6068  if (!remote) return;
6069  scene.remove(remote.group);
6070  remotePlayers.delete(id);
6071}
6072
6073function updateRemotePlayers(dt) {
6074  for (const remote of remotePlayers.values()) {
6075    remote.group.position.lerp(remote.target.position, Math.min(1, dt * 8));
6076    remote.group.rotation.y += angleDelta(remote.target.rotationY, remote.group.rotation.y) * Math.min(1, dt * 10);
6077    remote.person.visible = remote.mode === "foot";
6078    remote.car.visible = remote.mode !== "foot";
6079    remote.label.lookAt(camera.position);
6080  }
6081}
6082
6083function createRemotePlayer(name) {
6084  const group = new THREE.Group();
6085  const carGroup = new THREE.Group();
6086  const body = new THREE.Mesh(new THREE.BoxGeometry(2.05, 0.6, 3.65), new THREE.MeshStandardMaterial({ color: 0xd64c4c, roughness: 0.38, metalness: 0.35 }));
6087  body.position.y = 0.55;
6088  carGroup.add(body);
6089  const cabin = new THREE.Mesh(new THREE.BoxGeometry(1.15, 0.55, 1.1), mats.glass);
6090  cabin.position.set(0, 1.05, -0.35);
6091  carGroup.add(cabin);
6092  for (const x of [-1.05, 1.05]) {
6093    for (const z of [-1.2, 1.2]) {
6094      const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 0.24, 12), mats.rubber);
6095      wheel.rotation.z = Math.PI / 2;
6096      wheel.position.set(x, 0.28, z);
6097      carGroup.add(wheel);
6098    }
6099  }
6100  group.add(carGroup);
6101
6102  const person = new THREE.Group();
6103  const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.18, 0.55, 4, 8), mats.shirt);
6104  torso.position.y = 1.05;
6105  person.add(torso);
6106  const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 8), mats.skin);
6107  head.position.y = 1.55;
6108  person.add(head);
6109  group.add(person);
6110
6111  const label = new THREE.Sprite(new THREE.SpriteMaterial({ map: makeNameTexture(name), transparent: true, depthTest: false }));
6112  label.position.set(0, 2.2, 0);
6113  label.scale.set(2.4, 0.55, 1);
6114  group.add(label);
6115
6116  return {
6117    group,
6118    car: carGroup,
6119    person,
6120    label,
6121    name,
6122    mode: "car",
6123    target: { position: new THREE.Vector3(), rotationY: 0 },
6124  };
6125}
6126
6127function makeNameTexture(name) {
6128  const labelCanvas = document.createElement("canvas");
6129  labelCanvas.width = 256;
6130  labelCanvas.height = 64;
6131  const ctx = labelCanvas.getContext("2d");
6132  ctx.fillStyle = "rgba(0, 0, 0, 0.62)";
6133  ctx.fillRect(0, 0, 256, 64);
6134  ctx.fillStyle = "#f6f0e3";
6135  ctx.font = "bold 28px sans-serif";
6136  ctx.textAlign = "center";
6137  ctx.textBaseline = "middle";
6138  ctx.fillText(String(name).slice(0, 18), 128, 33);
6139  const texture = new THREE.CanvasTexture(labelCanvas);
6140  texture.colorSpace = THREE.SRGBColorSpace;
6141  return texture;
6142}
6143
6144function updateDayNight(dt) {
6145  if (nightDuration <= 0) {
6146    scene.background = new THREE.Color(0x8cc9e8);
6147    scene.fog.color.setHex(0x8cc9e8);
6148    renderer.setClearColor(0x8cc9e8, 1);
6149    sun.intensity = 2.8;
6150    sun.color.setHex(0xfff0d2);
6151    sun.position.set(-35, 58, 28);
6152    hemisphereLight.intensity = 1.35;
6153    hemisphereLight.color.setHex(0xb7e6ff);
6154    hemisphereLight.groundColor.setHex(0x315c36);
6155    if (sunDisc) {
6156      sunDisc.visible = true;
6157      sunDisc.material.opacity = 1;
6158    }
6159    if (skyHaze) {
6160      skyHaze.material.color.setHex(0xaeddf2);
6161      skyHaze.material.opacity = 0.32;
6162    }
6163    updateCarHeadlights(0);
6164    updateStreetLights(0);
6165    return;
6166  }
6167  world.dayTimer = (world.dayTimer + dt) % fullDayCycle;
6168  const isDay = world.dayTimer < dayDuration;
6169  const phase = isDay ? world.dayTimer / dayDuration : (world.dayTimer - dayDuration) / nightDuration;
6170  const blend = isDay ? THREE.MathUtils.smoothstep(phase, 0.78, 1) : 1 - THREE.MathUtils.smoothstep(phase, 0.78, 1);
6171  const daySky = new THREE.Color(0x8cc9e8);
6172  const nightSky = new THREE.Color(0x071225);
6173  const skyColor = daySky.clone().lerp(nightSky, blend);
6174  scene.background = skyColor;
6175  scene.fog.color.copy(skyColor);
6176  renderer.setClearColor(skyColor, 1);
6177
6178  sun.intensity = THREE.MathUtils.lerp(2.8, 0.18, blend);
6179  sun.color.setHex(blend > 0.5 ? 0xbfd8ff : 0xfff0d2);
6180  const sunAngle = (world.dayTimer / fullDayCycle) * Math.PI * 2 - Math.PI * 0.35;
6181  sun.position.set(Math.cos(sunAngle) * 70, Math.sin(sunAngle) * 62 + 18, 28);
6182
6183  hemisphereLight.intensity = THREE.MathUtils.lerp(1.35, 0.34, blend);
6184  hemisphereLight.color.setHex(blend > 0.5 ? 0x264a7c : 0xb7e6ff);
6185  hemisphereLight.groundColor.setHex(blend > 0.5 ? 0x101821 : 0x315c36);
6186
6187  if (sunDisc) {
6188    sunDisc.visible = blend < 0.94;
6189    sunDisc.position.set(Math.cos(sunAngle) * 130, Math.sin(sunAngle) * 96 + 10, -165);
6190    sunDisc.material.opacity = Math.max(0, 1 - blend * 1.25);
6191    sunDisc.material.transparent = true;
6192    sunDisc.lookAt(camera.position);
6193  }
6194  if (skyHaze) {
6195    skyHaze.material.color.setHex(blend > 0.5 ? 0x1c355a : 0xaeddf2);
6196    skyHaze.material.opacity = THREE.MathUtils.lerp(0.32, 0.18, blend);
6197  }
6198  updateCarHeadlights(blend);
6199  updateStreetLights(blend);
6200}
6201
6202function updateCarHeadlights(nightBlend) {
6203  const glow = THREE.MathUtils.smoothstep(nightBlend, 0.25, 0.78);
6204  for (const headlight of carHeadlights) {
6205    headlight.spot.intensity = glow * 4.8;
6206    headlight.spot.distance = 52 + glow * 24;
6207    headlight.beam.material.opacity = glow * 0.18;
6208    headlight.bulbMaterial.color.setHex(glow > 0.08 ? 0xfff8d8 : 0xd7c491);
6209  }
6210}
6211
6212function updateStreetLights(nightBlend) {
6213  const glow = THREE.MathUtils.smoothstep(nightBlend, 0.35, 0.82);
6214  for (const light of streetLights) {
6215    light.lamp.material.color.setHex(glow > 0.08 ? 0xfff0aa : 0x6d654e);
6216    light.glow.material.opacity = glow * 0.42;
6217    light.groundGlow.material.opacity = glow * 0.18;
6218    light.glow.scale.setScalar(0.75 + glow * 0.75);
6219  }
6220}
6221
6222function updateWater() {
6223  const time = clock.elapsedTime;
6224  for (const sea of waterSurfaces) {
6225    const positions = sea.geometry.attributes.position;
6226    for (let i = 0; i < positions.count; i += 1) {
6227      const x = positions.getX(i);
6228      const y = positions.getY(i);
6229      const wave =
6230        Math.sin(x * 0.055 + time * 1.35 + sea.userData.waveSeed) * 0.1 +
6231        Math.cos(y * 0.045 + time * 0.9) * 0.07;
6232      positions.setZ(i, wave);
6233    }
6234    positions.needsUpdate = true;
6235    sea.geometry.computeVertexNormals();
6236  }
6237}
6238
6239function loop() {
6240  const dt = Math.min(0.033, clock.getDelta());
6241  update(dt);
6242  updateWater();
6243  render();
6244  requestAnimationFrame(loop);
6245}
6246
6247function bindHold(button, name) {
6248  button.addEventListener("pointerdown", (event) => {
6249    event.preventDefault();
6250    startEngineSound();
6251    held[name] = true;
6252  });
6253  button.addEventListener("pointerup", () => {
6254    held[name] = false;
6255  });
6256  button.addEventListener("pointerleave", () => {
6257    held[name] = false;
6258  });
6259  button.addEventListener("pointercancel", () => {
6260    held[name] = false;
6261  });
6262}
6263
6264window.addEventListener("keydown", (event) => {
6265  const key = event.key.toLowerCase();
6266  const typingName = event.target === playerNameInput;
6267  if (world.started && !typingName && (gameControlKeys.has(key) || key === "e")) {
6268    event.preventDefault();
6269    if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
6270  }
6271  startEngineSound();
6272  if (key === "escape" && !event.repeat) {
6273    event.preventDefault();
6274    if (world.shopOpen) closeWeaponShop();
6275    else if (world.started) returnToMenu();
6276    else if (!namePanel.hidden) {
6277      namePanel.hidden = true;
6278      playBtn.hidden = false;
6279      settingsBtn.hidden = false;
6280    } else if (!settingsPanel.hidden) {
6281      settingsPanel.hidden = true;
6282      playBtn.hidden = false;
6283      settingsBtn.hidden = false;
6284    }
6285    return;
6286  }
6287  if (world.shopOpen) {
6288    if (key === "enter" && !event.repeat) buyWeaponUpgrade();
6289    return;
6290  }
6291  if (key === "e" && !event.repeat) {
6292    if (openWeaponShop()) return;
6293    toggleCarEntry();
6294    return;
6295  }
6296  if (!typingName) keys.add(key);
6297});
6298window.addEventListener("keyup", (event) => {
6299  const key = event.key.toLowerCase();
6300  if (world.started && gameControlKeys.has(key)) event.preventDefault();
6301  keys.delete(key);
6302});
6303window.addEventListener("resize", resize);
6304window.addEventListener("mousemove", (event) => {
6305  document.documentElement.style.setProperty("--menu-cursor-x", `${event.clientX}px`);
6306  document.documentElement.style.setProperty("--menu-cursor-y", `${event.clientY}px`);
6307  menuCursorEl.hidden = !world.shopOpen && !isRoundResultOpen() && (world.started || menuEl.hidden);
6308});
6309canvas.addEventListener("mousemove", (event) => {
6310  if (document.pointerLockElement === canvas) return;
6311  if (world.shopOpen) return;
6312  if (isInVehicle()) return;
6313  world.cameraYaw -= event.movementX * 0.004;
6314  world.cameraPitch = THREE.MathUtils.clamp(world.cameraPitch + event.movementY * 0.0025, -0.32, 0.36);
6315  world.mouseLookTimer = 2.2;
6316});
6317document.addEventListener("mousemove", (event) => {
6318  if (document.pointerLockElement !== canvas || world.shopOpen || isInVehicle() || !world.started) return;
6319  world.cameraYaw -= event.movementX * 0.004;
6320  world.cameraPitch = THREE.MathUtils.clamp(world.cameraPitch + event.movementY * 0.0025, -0.32, 0.36);
6321  world.mouseLookTimer = 2.2;
6322});
6323canvas.addEventListener("contextmenu", (event) => event.preventDefault());
6324canvas.addEventListener("mousedown", (event) => {
6325  if (world.shopOpen) return;
6326  if (world.inPlane) {
6327    if (event.button === 0) {
6328      event.preventDefault();
6329      shootPlaneGuns();
6330    }
6331    if (event.button === 2) {
6332      event.preventDefault();
6333      shootPlaneMissile();
6334    }
6335    return;
6336  }
6337  if (world.inCar) return;
6338  if (world.started && document.pointerLockElement !== canvas) canvas.requestPointerLock?.();
6339  if (event.button === 0) {
6340    event.preventDefault();
6341    shootWeapon();
6342  }
6343  if (event.button === 2) {
6344    event.preventDefault();
6345    world.aiming = true;
6346  }
6347});
6348window.addEventListener("mouseup", (event) => {
6349  if (event.button === 2) {
6350    world.aiming = false;
6351    world.turboHeld = false;
6352  }
6353});
6354window.addEventListener("blur", () => {
6355  world.aiming = false;
6356  world.turboHeld = false;
6357});
6358playBtn.addEventListener("click", () => {
6359  namePanel.hidden = false;
6360  settingsPanel.hidden = true;
6361  playBtn.hidden = true;
6362  settingsBtn.hidden = true;
6363  nameWarning.textContent = text.warning;
6364  playerNameInput.focus();
6365});
6366settingsBtn.addEventListener("click", () => {
6367  settingsPanel.hidden = false;
6368  namePanel.hidden = true;
6369  playBtn.hidden = true;
6370  settingsBtn.hidden = true;
6371});
6372settingsBackBtn.addEventListener("click", () => {
6373  settingsPanel.hidden = true;
6374  playBtn.hidden = false;
6375  settingsBtn.hidden = false;
6376});
6377soundToggle.addEventListener("change", () => {
6378  setSoundEnabled(soundToggle.checked);
6379});
6380nameBackBtn.addEventListener("click", () => {
6381  namePanel.hidden = true;
6382  playBtn.hidden = false;
6383  settingsBtn.hidden = false;
6384  nameWarning.textContent = text.warning;
6385  playerNameInput.blur();
6386});
6387namePanel.addEventListener("submit", (event) => {
6388  event.preventDefault();
6389  const playerName = playerNameInput.value.trim().slice(0, 8);
6390  if (!playerName) {
6391    nameWarning.textContent = "Please enter a name.";
6392    playerNameInput.focus();
6393    return;
6394  }
6395  playerNameInput.value = playerName;
6396  world.roundDuration = 0;
6397  world.timeLeft = 0;
6398  world.started = true;
6399  menuEl.hidden = true;
6400  menuCursorEl.hidden = true;
6401  if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
6402  startEngineSound();
6403  multiplayerName = playerName;
6404  startZombieRound();
6405  clock.getDelta();
6406});
6407menuBackBtn.addEventListener("click", returnToMenu);
6408restartBtn.addEventListener("click", reset);
6409resultRestartBtn.addEventListener("click", reset);
6410resultMenuBtn.addEventListener("click", returnToMenu);
6411shopBuyBtn.addEventListener("click", buyWeaponUpgrade);
6412shopCloseBtn.addEventListener("click", closeWeaponShop);
6413bindHold(leftBtn, "left");
6414bindHold(rightBtn, "right");
6415bindHold(gasBtn, "gas");
6416bindHold(brakeBtn, "brake");

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.