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.