Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Neon Drift: Overdrive</title> | |
| <!-- Import FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #00f3ff; | |
| --secondary-color: #ff00ff; | |
| --accent-color: #ffe600; | |
| --danger-color: #ff2a2a; | |
| --bg-dark: #050510; | |
| --glass-bg: rgba(10, 10, 25, 0.65); | |
| --glass-border: rgba(255, 255, 255, 0.15); | |
| --text-color: #ffffff; | |
| --font-main: 'Orbitron', 'Segoe UI', Roboto, sans-serif; | |
| } | |
| /* Import Google Font for Sci-Fi look */ | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap'); | |
| * { | |
| box-sizing: border-box; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| touch-action: none; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background-color: var(--bg-dark); | |
| font-family: var(--font-main); | |
| color: var(--text-color); | |
| } | |
| /* --- Canvas Container --- */ | |
| #game-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| z-index: 1; | |
| } | |
| /* --- UI Overlay Layer --- */ | |
| #ui-layer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 10; | |
| pointer-events: none; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| } | |
| /* --- Header / Top Bar --- */ | |
| .top-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 15px 25px; | |
| background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), transparent); | |
| pointer-events: auto; | |
| } | |
| .brand { | |
| font-size: 1.2rem; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| text-shadow: 0 0 10px var(--primary-color); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .brand a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| font-size: 0.8em; | |
| opacity: 0.8; | |
| } | |
| .brand a:hover { | |
| color: var(--secondary-color); | |
| opacity: 1; | |
| } | |
| /* --- HUD Stats --- */ | |
| .hud-stats { | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .stat-box { | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| padding: 8px 20px; | |
| border-radius: 4px; | |
| backdrop-filter: blur(8px); | |
| box-shadow: 0 0 15px rgba(0, 243, 255, 0.1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stat-box::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 2px; | |
| height: 100%; | |
| background: var(--primary-color); | |
| } | |
| .stat-box.score-box::after { background: var(--accent-color); } | |
| .stat-label { | |
| font-size: 0.6rem; | |
| color: #aaa; | |
| display: block; | |
| letter-spacing: 1px; | |
| } | |
| .stat-value { | |
| font-size: 1.4rem; | |
| font-weight: 700; | |
| color: var(--text-color); | |
| text-shadow: 0 0 5px rgba(255,255,255,0.5); | |
| } | |
| /* --- Nitro Bar --- */ | |
| .nitro-container { | |
| position: absolute; | |
| bottom: 30px; | |
| right: 30px; | |
| width: 200px; | |
| pointer-events: none; | |
| } | |
| .nitro-label { | |
| font-size: 0.7rem; | |
| margin-bottom: 5px; | |
| color: var(--secondary-color); | |
| text-transform: uppercase; | |
| text-align: right; | |
| text-shadow: 0 0 5px var(--secondary-color); | |
| } | |
| .nitro-bar-bg { | |
| width: 100%; | |
| height: 10px; | |
| background: rgba(0,0,0,0.5); | |
| border: 1px solid var(--secondary-color); | |
| transform: skewX(-20deg); | |
| overflow: hidden; | |
| } | |
| .nitro-bar-fill { | |
| height: 100%; | |
| width: 100%; | |
| background: linear-gradient(90deg, var(--secondary-color), #ff66aa); | |
| box-shadow: 0 0 10px var(--secondary-color); | |
| transform-origin: left; | |
| transition: transform 0.1s linear; | |
| } | |
| /* --- Main Menu (Start Screen) --- */ | |
| #start-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: radial-gradient(circle at center, rgba(20, 20, 40, 0.9), #000000); | |
| backdrop-filter: blur(15px); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 20; | |
| pointer-events: auto; | |
| transition: opacity 0.6s ease; | |
| } | |
| #start-screen.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| h1 { | |
| font-size: 4.5rem; | |
| margin-bottom: 0.5rem; | |
| font-weight: 900; | |
| font-style: italic; | |
| background: linear-gradient(180deg, #fff, var(--primary-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 30px var(--primary-color); | |
| text-align: center; | |
| letter-spacing: -2px; | |
| } | |
| p.subtitle { | |
| font-size: 1.1rem; | |
| color: #ccc; | |
| margin-bottom: 3rem; | |
| text-align: center; | |
| max-width: 500px; | |
| line-height: 1.6; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .btn { | |
| background: transparent; | |
| border: 2px solid var(--primary-color); | |
| padding: 15px 50px; | |
| color: var(--primary-color); | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| font-family: var(--font-main); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-transform: uppercase; | |
| letter-spacing: 3px; | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--primary-color); | |
| transition: left 0.3s ease; | |
| z-index: -1; | |
| } | |
| .btn:hover { | |
| color: #000; | |
| box-shadow: 0 0 30px var(--primary-color); | |
| } | |
| .btn:hover::before { | |
| left: 0; | |
| } | |
| .controls-info { | |
| margin-top: 50px; | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 30px; | |
| text-align: center; | |
| } | |
| .control-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| color: #888; | |
| transition: transform 0.3s; | |
| } | |
| .control-item:hover { | |
| transform: translateY(-5px); | |
| color: #fff; | |
| } | |
| .control-item i { | |
| font-size: 1.8rem; | |
| margin-bottom: 10px; | |
| color: var(--primary-color); | |
| text-shadow: 0 0 10px var(--primary-color); | |
| } | |
| /* --- Mobile Controls --- */ | |
| #mobile-controls { | |
| display: none; | |
| width: 100%; | |
| height: 100%; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| padding: 20px; | |
| } | |
| .touch-zone { | |
| pointer-events: auto; | |
| position: absolute; | |
| bottom: 30px; | |
| display: flex; | |
| gap: 15px; | |
| } | |
| .d-pad { | |
| left: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .pedals { | |
| right: 20px; | |
| display: flex; | |
| flex-direction: row-reverse; | |
| gap: 15px; | |
| align-items: flex-end; | |
| } | |
| .touch-btn { | |
| width: 70px; | |
| height: 70px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 2px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 50%; | |
| color: white; | |
| font-size: 1.5rem; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| backdrop-filter: blur(4px); | |
| transition: all 0.1s; | |
| } | |
| .touch-btn:active, | |
| .touch-btn.active { | |
| background: rgba(0, 243, 255, 0.3); | |
| transform: scale(0.9); | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 15px var(--primary-color); | |
| } | |
| .touch-btn.brake:active, | |
| .touch-btn.brake.active { | |
| background: rgba(255, 0, 100, 0.3); | |
| border-color: var(--secondary-color); | |
| box-shadow: 0 0 15px var(--secondary-color); | |
| } | |
| .touch-btn.boost { | |
| border-color: var(--accent-color); | |
| color: var(--accent-color); | |
| } | |
| .touch-btn.boost:active, | |
| .touch-btn.boost.active { | |
| background: rgba(255, 230, 0, 0.3); | |
| border-color: var(--accent-color); | |
| box-shadow: 0 0 15px var(--accent-color); | |
| } | |
| /* --- Reset Button --- */ | |
| #reset-btn { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0, 0, 0, 0.6); | |
| border: 1px solid rgba(255,255,255,0.3); | |
| color: #ccc; | |
| padding: 10px 25px; | |
| border-radius: 30px; | |
| pointer-events: auto; | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| backdrop-filter: blur(5px); | |
| display: none; | |
| } | |
| #reset-btn:hover { | |
| background: var(--secondary-color); | |
| border-color: var(--secondary-color); | |
| color: white; | |
| } | |
| /* --- Media Queries --- */ | |
| @media (max-width: 768px) { | |
| h1 { font-size: 2.8rem; } | |
| .controls-info { display: none; } | |
| #mobile-controls { display: block; } | |
| #reset-btn { display: block; } | |
| .nitro-container { | |
| bottom: 100px; /* Move up to avoid finger overlap */ | |
| right: 20px; | |
| width: 150px; | |
| } | |
| } | |
| </style> | |
| <!-- Three.js & Cannon-es via Import Map --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/", | |
| "cannon-es": "https://unpkg.com/cannon-es@0.20.0/dist/cannon-es.js" | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <!-- Game Canvas --> | |
| <div id="game-container"></div> | |
| <!-- UI Overlay --> | |
| <div id="ui-layer"> | |
| <div class="top-bar"> | |
| <div class="brand"> | |
| <i class="fa-solid fa-bolt"></i> NEON DRIFT | |
| <span style="font-size: 0.6em; margin-left: 15px; opacity: 0.6; font-weight: 400;"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </span> | |
| </div> | |
| <div class="hud-stats"> | |
| <div class="stat-box score-box"> | |
| <span class="stat-label">SCORE</span> | |
| <span class="stat-value" id="score-display">0</span> | |
| </div> | |
| <div class="stat-box"> | |
| <span class="stat-label">SPEED</span> | |
| <span class="stat-value" id="speed-display">0 <small style="font-size:0.6em">km/h</small></span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Nitro Bar --> | |
| <div class="nitro-container"> | |
| <div class="nitro-label">Nitro System</div> | |
| <div class="nitro-bar-bg"> | |
| <div class="nitro-bar-fill" id="nitro-fill"></div> | |
| </div> | |
| </div> | |
| <div id="reset-btn" onclick="resetCar()"> | |
| <i class="fa-solid fa-rotate-right"></i> Flip Car | |
| </div> | |
| <!-- Mobile Controls --> | |
| <div id="mobile-controls"> | |
| <div class="touch-zone d-pad"> | |
| <div class="touch-btn" id="btn-left"><i class="fa-solid fa-arrow-left"></i></div> | |
| <div class="touch-btn" id="btn-right"><i class="fa-solid fa-arrow-right"></i></div> | |
| </div> | |
| <div class="touch-zone pedals"> | |
| <div class="touch-btn boost" id="btn-boost"><i class="fa-solid fa-fire"></i></div> | |
| <div class="touch-btn brake" id="btn-brake" style="margin-right: 10px;"><i class="fa-solid fa-stop"></i></div> | |
| <div class="touch-btn" id="btn-gas"><i class="fa-solid fa-gas-pump"></i></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="start-screen"> | |
| <h1>NEON DRIFT<br><span style="font-size: 0.5em; color: var(--accent-color); text-shadow: 0 0 20px var(--accent-color);">OVERDRIVE</span></h1> | |
| <p class="subtitle">Sammle Energiekerne. Nutze Nitro. Überlebe die Unendlichkeit.</p> | |
| <button class="btn" id="start-btn">Start Engine</button> | |
| <div class="controls-info"> | |
| <div class="control-item"> | |
| <i class="fa-solid fa-keyboard"></i> | |
| <span>WASD / Shift</span> | |
| </div> | |
| <div class="control-item"> | |
| <i class="fa-solid fa-gamepad"></i> | |
| <span>Controller</span> | |
| </div> | |
| <div class="control-item"> | |
| <i class="fa-solid fa-mobile-screen"></i> | |
| <span>Touch</span> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import * as CANNON from 'cannon-es'; | |
| // Post Processing Imports | |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; | |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; | |
| // --- Configuration --- | |
| const CONFIG = { | |
| shadows: true, | |
| maxSteerVal: 0.5, | |
| maxForce: 1200, | |
| boostForce: 2500, | |
| brakeForce: 30, | |
| nitroMax: 100, | |
| nitroDepletion: 0.5, // per frame | |
| nitroRegen: 0.1, // per frame | |
| colors: { | |
| bg: 0x050510, | |
| grid: 0x00f3ff, | |
| car: 0x111111, | |
| neon: 0xff00ff, | |
| coin: 0xffaa00 | |
| } | |
| }; | |
| // --- Globals --- | |
| let scene, camera, renderer, composer; | |
| let world, vehicle, chassisBody, wheelBodies = []; | |
| let particles = []; | |
| let collectibles = []; | |
| let lastTime; | |
| let isGameActive = false; | |
| // Game State | |
| let score = 0; | |
| let nitro = 100; | |
| let isBoosting = false; | |
| // Input State | |
| const input = { | |
| forward: false, | |
| backward: false, | |
| left: false, | |
| right: false, | |
| brake: false, | |
| boost: false | |
| }; | |
| // --- Initialization --- | |
| function init() { | |
| // 1. Setup Three.js Scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(CONFIG.colors.bg); | |
| scene.fog = new THREE.FogExp2(CONFIG.colors.bg, 0.006); | |
| // 2. Camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| // 3. Renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.toneMapping = THREE.ReinhardToneMapping; | |
| document.getElementById('game-container').appendChild(renderer.domElement); | |
| // 4. Post-Processing (BLOOM) | |
| const renderScene = new RenderPass(scene, camera); | |
| const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); | |
| bloomPass.threshold = 0.2; | |
| bloomPass.strength = 1.2; // Intensity of glow | |
| bloomPass.radius = 0.5; | |
| composer = new EffectComposer(renderer); | |
| composer.addPass(renderScene); | |
| composer.addPass(bloomPass); | |
| // 5. Lights | |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.5); | |
| scene.add(ambientLight); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| dirLight.position.set(50, 100, 50); | |
| dirLight.castShadow = true; | |
| dirLight.shadow.mapSize.width = 2048; | |
| dirLight.shadow.mapSize.height = 2048; | |
| dirLight.shadow.camera.near = 0.5; | |
| dirLight.shadow.camera.far = 500; | |
| dirLight.shadow.camera.left = -100; | |
| dirLight.shadow.camera.right = 100; | |
| dirLight.shadow.camera.top = 100; | |
| dirLight.shadow.camera.bottom = -100; | |
| scene.add(dirLight); | |
| // 6. Physics World | |
| world = new CANNON.World(); | |
| world.gravity.set(0, -9.82, 0); | |
| world.broadphase = new CANNON.SAPBroadphase(world); | |
| // Materials | |
| const groundMat = new CANNON.Material(); | |
| const wheelMat = new CANNON.Material(); | |
| const wheelGroundContact = new CANNON.ContactMaterial(wheelMat, groundMat, { | |
| friction: 0.3, | |
| restitution: 0, | |
| contactEquationStiffness: 1000 | |
| }); | |
| world.addContactMaterial(wheelGroundContact); | |
| // 7. Create Environment | |
| createEnvironment(groundMat); | |
| // 8. Create Car | |
| createCar(wheelMat); | |
| // 9. Event Listeners | |
| window.addEventListener('resize', onWindowResize); | |
| setupInputs(); | |
| // Start Loop | |
| lastTime = performance.now(); | |
| requestAnimationFrame(animate); | |
| } | |
| // --- Environment Generation --- | |
| function createEnvironment(material) { | |
| // Floor | |
| const groundGeo = new THREE.PlaneGeometry(1000, 1000); | |
| const groundTex = createGridTexture(); | |
| groundTex.wrapS = THREE.RepeatWrapping; | |
| groundTex.wrapT = THREE.RepeatWrapping; | |
| groundTex.repeat.set(200, 200); | |
| const groundMesh = new THREE.Mesh( | |
| groundGeo, | |
| new THREE.MeshStandardMaterial({ | |
| map: groundTex, | |
| roughness: 0.4, | |
| metalness: 0.6, | |
| emissive: 0x001133, | |
| emissiveIntensity: 0.2 | |
| }) | |
| ); | |
| groundMesh.rotation.x = -Math.PI / 2; | |
| groundMesh.receiveShadow = true; | |
| scene.add(groundMesh); | |
| // Physics Ground | |
| const groundBody = new CANNON.Body({ | |
| mass: 0, | |
| shape: new CANNON.Plane(), | |
| material: material | |
| }); | |
| groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); | |
| world.addBody(groundBody); | |
| // Sun (Background) | |
| createSun(); | |
| // Mountains (Wireframe) | |
| createMountains(); | |
| // Obstacles | |
| createObstacle(10, 2, 10, -20, 1, -30); | |
| createRamp(10, 1, 15, 20, 0.5, -20); | |
| // Initial Collectibles | |
| for(let i=0; i<20; i++) { | |
| spawnCollectible(); | |
| } | |
| } | |
| function createSun() { | |
| const sunGeo = new THREE.CircleGeometry(40, 64); | |
| const sunMat = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| time: { value: 0 }, | |
| color1: { value: new THREE.Color(0xff00cc) }, | |
| color2: { value: new THREE.Color(0x3300cc) } | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform float time; | |
| uniform vec3 color1; | |
| uniform vec3 color2; | |
| varying vec2 vUv; | |
| void main() { | |
| // Create gradient stripes | |
| float stripes = sin(vUv.y * 40.0 + time * 0.5); | |
| float gradient = smoothstep(0.4, 0.6, vUv.y); | |
| vec3 color = mix(color1, color2, vUv.y + stripes * 0.05); | |
| float alpha = smoothstep(0.0, 0.5, vUv.y) * (1.0 - smoothstep(0.5, 1.0, vUv.y)); | |
| gl_FragColor = vec4(color, alpha * 0.8); | |
| } | |
| `, | |
| transparent: true, | |
| side: THREE.DoubleSide | |
| }); | |
| const sun = new THREE.Mesh(sunGeo, sunMat); | |
| sun.position.set(0, 20, -300); | |
| scene.add(sun); | |
| // Sun Glow (Behind) | |
| const glowGeo = new THREE.CircleGeometry(60, 32); | |
| const glowMat = new THREE.MeshBasicMaterial({ color: 0xff00aa, transparent: true, opacity: 0.3 }); | |
| const glow = new THREE.Mesh(glowGeo, glowMat); | |
| glow.position.set(0, 20, -310); | |
| scene.add(glow); | |
| // Update shader time | |
| sun.userData.update = (t) => { sunMat.uniforms.time.value = t * 0.001; }; | |
| scene.userData.sun = sun; | |
| } | |
| function createMountains() { | |
| const mountainGeo = new THREE.ConeGeometry(1, 1, 4); | |
| const mountainMat = new THREE.MeshBasicMaterial({ | |
| color: 0xaa00ff, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.3 | |
| }); | |
| const group = new THREE.Group(); | |
| for(let i=0; i<60; i++) { | |
| const mesh = new THREE.Mesh(mountainGeo, mountainMat); | |
| const scaleY = 20 + Math.random() * 60; | |
| const scaleXZ = 30 + Math.random() * 50; | |
| mesh.scale.set(scaleXZ, scaleY, scaleXZ); | |
| mesh.position.set( | |
| (Math.random() - 0.5) * 800, | |
| scaleY / 2 - 10, | |
| -200 - Math.random() * 400 | |
| ); | |
| mesh.rotation.y = Math.random() * Math.PI; | |
| group.add(mesh); | |
| } | |
| scene.add(group); | |
| } | |
| function createGridTexture() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 512; | |
| canvas.height = 512; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = '#050510'; | |
| ctx.fillRect(0, 0, 512, 512); | |
| ctx.strokeStyle = '#00f3ff'; | |
| ctx.lineWidth = 2; | |
| ctx.shadowBlur = 4; | |
| ctx.shadowColor = '#00f3ff'; | |
| ctx.beginPath(); | |
| for(let i=0; i<=512; i+=64) { | |
| ctx.moveTo(i, 0); | |
| ctx.lineTo(i, 512); | |
| ctx.moveTo(0, i); | |
| ctx.lineTo(512, i); | |
| } | |
| ctx.stroke(); | |
| const tex = new THREE.CanvasTexture(canvas); | |
| tex.anisotropy = 16; | |
| return tex; | |
| } | |
| function createObstacle(w, h, d, x, y, z) { | |
| const geo = new THREE.BoxGeometry(w, h, d); | |
| const mat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.1, metalness: 0.8 }); | |
| const mesh = new THREE.Mesh(geo, mat); | |
| mesh.position.set(x, y, z); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| scene.add(mesh); | |
| const edges = new THREE.EdgesGeometry(geo); | |
| const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xff00ff })); | |
| mesh.add(line); | |
| const shape = new CANNON.Box(new CANNON.Vec3(w/2, h/2, d/2)); | |
| const body = new CANNON.Body({ mass: 0 }); | |
| body.addShape(shape); | |
| body.position.set(x, y, z); | |
| world.addBody(body); | |
| } | |
| function createRamp(w, h, d, x, y, z) { | |
| const geo = new THREE.BoxGeometry(w, h, d); | |
| const mat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.5 }); | |
| const mesh = new THREE.Mesh(geo, mat); | |
| mesh.position.set(x, y, z); | |
| mesh.rotation.x = -0.3; | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| scene.add(mesh); | |
| const edges = new THREE.EdgesGeometry(geo); | |
| const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x00f3ff })); | |
| mesh.add(line); | |
| const shape = new CANNON.Box(new CANNON.Vec3(w/2, h/2, d/2)); | |
| const body = new CANNON.Body({ mass: 0 }); | |
| body.addShape(shape); | |
| body.position.set(x, y, z); | |
| body.quaternion.setFromEuler(-0.3, 0, 0); | |
| world.addBody(body); | |
| } | |
| function spawnCollectible() { | |
| const geo = new THREE.OctahedronGeometry(0.8); | |
| const mat = new THREE.MeshBasicMaterial({ color: CONFIG.colors.coin }); | |
| const mesh = new THREE.Mesh(geo, mat); | |
| // Random position around center | |
| const x = (Math.random() - 0.5) * 150; | |
| const z = (Math.random() - 0.5) * 150; | |
| const y = 2 + Math.random() * 3; | |
| mesh.position.set(x, y, z); | |
| // Add PointLight | |
| const light = new THREE.PointLight(CONFIG.colors.coin, 2, 10); | |
| mesh.add(light); | |
| scene.add(mesh); | |
| collectibles.push({ mesh: mesh, active: true }); | |
| } | |
| // --- Car Creation --- | |
| function createCar(material) { | |
| const chassisWidth = 1.8; | |
| const chassisHeight = 0.8; | |
| const chassisDepth = 4; | |
| const mass = 150; | |
| // Visual Chassis | |
| const chassisGeo = new THREE.BoxGeometry(chassisWidth, chassisHeight, chassisDepth); | |
| const chassisMat = new THREE.MeshStandardMaterial({ color: CONFIG.colors.car, roughness: 0.2, metalness: 0.8 }); | |
| const chassisMesh = new THREE.Mesh(chassisGeo, chassisMat); | |
| chassisMesh.castShadow = true; | |
| // Neon Strips | |
| const stripGeo = new THREE.BoxGeometry(1.82, 0.1, 4.02); | |
| const stripMat = new THREE.MeshBasicMaterial({ color: CONFIG.colors.neon }); | |
| const strip = new THREE.Mesh(stripGeo, stripMat); | |
| strip.position.y = -0.3; | |
| chassisMesh.add(strip); | |
| // Cabin | |
| const cabinGeo = new THREE.BoxGeometry(1.4, 0.6, 2); | |
| const cabinMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.0, metalness: 1.0 }); | |
| const cabinMesh = new THREE.Mesh(cabinGeo, cabinMat); | |
| cabinMesh.position.y = 0.5; | |
| cabinMesh.position.z = -0.2; | |
| chassisMesh.add(cabinMesh); | |
| // Engine Glow (Rear) | |
| const engineGeo = new THREE.BoxGeometry(1.2, 0.2, 0.1); | |
| const engineMat = new THREE.MeshBasicMaterial({ color: 0x00ffff }); | |
| const engine = new THREE.Mesh(engineGeo, engineMat); | |
| engine.position.set(0, 0, 2.05); | |
| chassisMesh.add(engine); | |
| // Headlights | |
| const spotLightL = new THREE.SpotLight(0xffffff, 10); | |
| spotLightL.position.set(-0.6, 0, -1.8); | |
| spotLightL.target.position.set(-0.6, -1, -10); | |
| spotLightL.angle = 0.5; | |
| spotLightL.penumbra = 0.5; | |
| spotLightL.castShadow = true; | |
| chassisMesh.add(spotLightL); | |
| chassisMesh.add(spotLightL.target); | |
| const spotLightR = spotLightL.clone(); | |
| spotLightR.position.set(0.6, 0, -1.8); | |
| spotLightR.target.position.set(0.6, -1, -10); | |
| chassisMesh.add(spotLightR); | |
| chassisMesh.add(spotLightR.target); | |
| scene.add(chassisMesh); | |
| // Physics Chassis | |
| const chassisShape = new CANNON.Box(new CANNON.Vec3(chassisWidth/2, chassisHeight/2, chassisDepth/2)); | |
| chassisBody = new CANNON.Body({ mass: mass, material: material }); | |
| chassisBody.addShape(chassisShape); | |
| chassisBody.position.set(0, 4, 0); | |
| chassisBody.angularDamping = 0.5; | |
| world.addBody(chassisBody); | |
| // Vehicle Setup | |
| vehicle = new CANNON.RaycastVehicle({ chassisBody: chassisBody }); | |
| const wheelOptions = { | |
| radius: 0.5, | |
| directionLocal: new CANNON.Vec3(0, -1, 0), | |
| suspensionStiffness: 30, | |
| suspensionRestLength: 0.3, | |
| frictionSlip: 2.0, // More |