diff --git a/js/Audio.js b/js/Audio.js index 1ec9709..f545741 100644 --- a/js/Audio.js +++ b/js/Audio.js @@ -38,6 +38,9 @@ export class GameAudio { this.gear = 0; this.shiftCooldown = 0; + this.nosWhoosh = null; + this.prevNosActive = false; + } init( camera ) { @@ -49,6 +52,7 @@ export class GameAudio { this.engineSound = new THREE.Audio( this.listener ); this.engineLayerSound = new THREE.Audio( this.listener ); + this.nosWhoosh = new THREE.Audio( this.listener ); this.engineFilter = this.listener.context.createBiquadFilter(); this.engineFilter.type = 'lowpass'; @@ -68,6 +72,9 @@ export class GameAudio { this.engineLayerSound.setLoop( true ); this.engineLayerSound.setVolume( 0 ); + this.nosWhoosh.setBuffer( buffer ); + this.nosWhoosh.setLoop( false ); + this.checkReady(); } ); @@ -144,10 +151,23 @@ export class GameAudio { } - update( dt, speed, throttle, driftIntensity ) { + update( dt, speed, throttle, driftIntensity, nosActive ) { if ( ! this.ready ) return; + const na = nosActive === true; + if ( na && ! this.prevNosActive && this.nosWhoosh && this.nosWhoosh.buffer ) { + + if ( this.nosWhoosh.isPlaying ) this.nosWhoosh.stop(); + + this.nosWhoosh.setPlaybackRate( 1.55 + Math.random() * 0.25 ); + this.nosWhoosh.setVolume( 0.2 ); + this.nosWhoosh.play(); + + } + + this.prevNosActive = na; + const absSpeed = THREE.MathUtils.clamp( Math.abs( speed ), 0, 1 ); // Only forward throttle counts as engine load. Brake/reverse (throttle < 0) // should let RPM fall so downshifts can fire as the car decelerates. diff --git a/js/Camera.js b/js/Camera.js index cde2d36..f6dc7c4 100644 --- a/js/Camera.js +++ b/js/Camera.js @@ -30,6 +30,8 @@ export class Camera { this.smoothedDesired = new THREE.Vector3(); this.initialized = false; + this.nosShakeOffset = new THREE.Vector3(); + const segments = 64; const points = []; for ( let i = 0; i <= segments; i ++ ) { @@ -55,7 +57,22 @@ export class Camera { } - update( dt, target, velocity ) { + _applyNosCameraShake( dt, nosIntensity ) { + + const ni = THREE.MathUtils.clamp( nosIntensity ?? 0, 0, 1 ); + this.nosShakeOffset.multiplyScalar( Math.exp( - dt * 13 ) ); + this.nosShakeOffset.x += ( Math.random() - 0.5 ) * ni * 3.4 * dt; + this.nosShakeOffset.y += ( Math.random() - 0.5 ) * ni * 2.6 * dt; + this.nosShakeOffset.z += ( Math.random() - 0.5 ) * ni * 3.4 * dt; + + const shakeLen = this.nosShakeOffset.length(); + if ( shakeLen > 0.26 ) this.nosShakeOffset.multiplyScalar( 0.26 / shakeLen ); + + this.camera.position.add( this.nosShakeOffset ); + + } + + update( dt, target, velocity, nosIntensity = 0 ) { const radius = this.deadzoneRadius; const radiusSq = radius * radius; @@ -106,6 +123,8 @@ export class Camera { this.debug.position.y += 0.05; this.debug.scale.set( radius, 1, radius ); + this._applyNosCameraShake( dt, nosIntensity ); + } } diff --git a/js/Controls.js b/js/Controls.js index 80b8d09..54b614b 100644 --- a/js/Controls.js +++ b/js/Controls.js @@ -5,6 +5,9 @@ export class Controls { this.keys = {}; this.x = 0; this.z = 0; + this.nos = false; + /** Pointer / UI hold on the NOS button (see NosHud). */ + this.nosUiHeld = false; // Touch state this.touchActive = false; @@ -16,6 +19,11 @@ export class Controls { window.addEventListener( 'keydown', ( e ) => this.keys[ e.code ] = true ); window.addEventListener( 'keyup', ( e ) => this.keys[ e.code ] = false ); + window.addEventListener( 'blur', () => { + + this.nosUiHeld = false; + + } ); this.setupTouchUI(); @@ -120,6 +128,8 @@ export class Controls { const gamepads = navigator.getGamepads(); + let nos = false; + for ( const gp of gamepads ) { if ( ! gp ) continue; @@ -132,6 +142,10 @@ export class Controls { if ( rt > 0.1 || lt > 0.1 ) z = rt - lt; + // RB — boost (NOS); avoids face buttons used for menus elsewhere. + const rb = gp.buttons[ 5 ]; + if ( rb && rb.pressed ) nos = true; + break; } @@ -153,10 +167,21 @@ export class Controls { } + if ( this.keys[ 'KeyX' ] ) nos = true; + if ( this.nosUiHeld ) nos = true; + this.x = x; this.z = z; + this.nos = nos; + + return { x, z, touchActive: this.touchActive, nos }; + + } + + /** Call from NOS HUD button pointer / keyboard hold. */ + setNosUiHeld( held ) { - return { x, z, touchActive: this.touchActive }; + this.nosUiHeld = !! held; } diff --git a/js/NosHud.js b/js/NosHud.js new file mode 100644 index 0000000..269c859 --- /dev/null +++ b/js/NosHud.js @@ -0,0 +1,195 @@ +import { NOS_DURATION } from './Vehicle.js'; + +export class NosHud { + + constructor( controls ) { + + this._controls = controls; + + const css = document.createElement( 'style' ); + css.textContent = ` + .nos-hud { + position: fixed; + left: 14px; + bottom: 14px; + z-index: 20; + font-family: system-ui, sans-serif; + user-select: none; + pointer-events: none; + } + .nos-hud__label-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 5px; + } + .nos-hud__key-btn { + appearance: none; + border: 0; + margin: 0; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 7px; + border-radius: 5px; + background: #ffffff; + color: #1a1a1a; + font-size: 11px; + font-weight: 800; + line-height: 1; + font-family: inherit; + box-shadow: + 0 1px 0 rgba( 255, 255, 255, 0.65 ) inset, + 0 2px 4px rgba( 0, 0, 0, 0.35 ); + pointer-events: auto; + cursor: pointer; + touch-action: manipulation; + } + .nos-hud__key-btn:hover { + filter: brightness( 1.04 ); + } + .nos-hud__key-btn:active { + transform: translateY( 1px ); + box-shadow: + 0 1px 0 rgba( 255, 255, 255, 0.4 ) inset, + 0 1px 2px rgba( 0, 0, 0, 0.4 ); + } + .nos-hud__key-btn--empty { + opacity: 0.4; + } + .nos-hud__text { + display: flex; + flex-direction: column; + gap: 1px; + } + .nos-hud__title { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + color: rgba( 255, 255, 255, 0.55 ); + text-shadow: 0 1px 2px rgba( 0, 0, 0, 0.5 ); + } + .nos-hud__pct { + font-size: 11px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: rgba( 255, 255, 255, 0.92 ); + text-shadow: 0 1px 2px rgba( 0, 0, 0, 0.55 ); + } + .nos-hud__track { + width: 112px; + height: 8px; + border-radius: 4px; + background: rgba( 0, 0, 0, 0.45 ); + box-shadow: inset 0 1px 2px rgba( 0, 0, 0, 0.45 ); + overflow: hidden; + position: relative; + } + .nos-hud__fill { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 100%; + transform-origin: left center; + transform: scaleX( 1 ); + border-radius: 4px; + background: linear-gradient( 180deg, #ff6b4a 0%, #c41e3a 55%, #8b1538 100% ); + box-shadow: 0 0 10px rgba( 255, 60, 80, 0.45 ); + transition: transform 0.08s ease-out; + } + `; + document.head.appendChild( css ); + + this.root = document.createElement( 'div' ); + this.root.className = 'nos-hud'; + this.root.innerHTML = ` +
+ +
+ NOS + 100% left +
+
+
+
+
+ `; + document.body.appendChild( this.root ); + + this.fill = this.root.querySelector( '.nos-hud__fill' ); + this.percentEl = this.root.querySelector( '.nos-hud__percent' ); + this.keyBtn = this.root.querySelector( '.nos-hud__key-btn' ); + + this.keyBtn.setAttribute( 'aria-label', 'Hold to use NOS boost' ); + + const setHeld = ( on ) => { + + this._controls.setNosUiHeld( on ); + + }; + + this.keyBtn.addEventListener( 'pointerdown', ( e ) => { + + e.preventDefault(); + this.keyBtn.setPointerCapture( e.pointerId ); + setHeld( true ); + + } ); + + this.keyBtn.addEventListener( 'pointerup', ( e ) => { + + e.preventDefault(); + setHeld( false ); + try { + + this.keyBtn.releasePointerCapture( e.pointerId ); + + } catch ( _ ) {} + + } ); + + this.keyBtn.addEventListener( 'pointercancel', () => setHeld( false ) ); + + this.keyBtn.addEventListener( 'lostpointercapture', () => setHeld( false ) ); + + this.keyBtn.addEventListener( 'keydown', ( e ) => { + + if ( e.code === 'Space' || e.code === 'Enter' ) { + + e.preventDefault(); + setHeld( true ); + + } + + } ); + + this.keyBtn.addEventListener( 'keyup', ( e ) => { + + if ( e.code === 'Space' || e.code === 'Enter' ) { + + e.preventDefault(); + setHeld( false ); + + } + + } ); + + } + + update( vehicle ) { + + const frac = Math.min( 1, Math.max( 0, vehicle.nosTankRemaining / NOS_DURATION ) ); + this.fill.style.transform = `scaleX( ${ frac } )`; + + const pct = Math.round( frac * 100 ); + this.percentEl.textContent = String( pct ); + + this.keyBtn.classList.toggle( 'nos-hud__key-btn--empty', pct <= 0 ); + this.keyBtn.setAttribute( 'aria-disabled', pct <= 0 ? 'true' : 'false' ); + + } + +} diff --git a/js/Particles.js b/js/Particles.js index 8880b97..7d8ebf5 100644 --- a/js/Particles.js +++ b/js/Particles.js @@ -10,6 +10,9 @@ const INV_MAX_LIFE = 1 / MAX_LIFE; const _blPos = new THREE.Vector3(); const _brPos = new THREE.Vector3(); +const _nosFwd = new THREE.Vector3(); +const _nosSpawn = new THREE.Vector3(); + export class SmokeTrails { constructor( scene ) { @@ -94,7 +97,10 @@ export class SmokeTrails { update( dt, vehicle ) { - const shouldEmit = vehicle.driftIntensity > 0.7; + const driftSmoke = vehicle.driftIntensity > 0.7; + const nosSmoke = vehicle.nosActive === true; + const shouldEmit = driftSmoke || nosSmoke; + const emitBatch = nosSmoke ? PARTICLES_PER_EMIT + 2 : PARTICLES_PER_EMIT; let aliveCount = 0; if ( shouldEmit ) { @@ -103,7 +109,7 @@ export class SmokeTrails { const bl = vehicle.wheelBL ? vehicle.wheelBL.getWorldPosition( _blPos ) : null; const br = vehicle.wheelBR ? vehicle.wheelBR.getWorldPosition( _brPos ) : null; - for ( let i = 0; i < PARTICLES_PER_EMIT; i ++ ) { + for ( let i = 0; i < emitBatch; i ++ ) { if ( bl ) this.emitAt( bl.x, roadY, bl.z ); if ( br ) this.emitAt( br.x, roadY, br.z ); @@ -180,3 +186,214 @@ export class SmokeTrails { } } + +const NOS_POOL_SIZE = 384; +const NOS_PARTICLES_PER_EMIT = 4; +const NOS_EMIT_JITTER = 0.08; +const NOS_BASE_SIZE = 0.65; +const NOS_MAX_LIFE = 0.52; +const INV_NOS_MAX_LIFE = 1 / NOS_MAX_LIFE; +const NOS_TRAIL_SPEED = 11; + +export class NosTaillightTrails { + + constructor( scene ) { + + const positions = new Float32Array( NOS_POOL_SIZE * 3 ); + const opacities = new Float32Array( NOS_POOL_SIZE ); + const sizes = new Float32Array( NOS_POOL_SIZE ); + + const geometry = new THREE.BufferGeometry(); + + const posAttr = new THREE.BufferAttribute( positions, 3 ); + posAttr.setUsage( THREE.DynamicDrawUsage ); + geometry.setAttribute( 'position', posAttr ); + + const opacityAttr = new THREE.BufferAttribute( opacities, 1 ); + opacityAttr.setUsage( THREE.DynamicDrawUsage ); + geometry.setAttribute( 'aOpacity', opacityAttr ); + + const sizeAttr = new THREE.BufferAttribute( sizes, 1 ); + sizeAttr.setUsage( THREE.DynamicDrawUsage ); + geometry.setAttribute( 'aSize', sizeAttr ); + + const map = new THREE.TextureLoader().load( 'sprites/smoke.png' ); + + const material = new THREE.PointsMaterial( { + map, + color: 0xff2838, + size: 1, + sizeAttenuation: true, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + opacity: 1, + } ); + + material.onBeforeCompile = ( shader ) => { + + shader.vertexShader = 'attribute float aSize;\nattribute float aOpacity;\nvarying float vOpacity;\n' + shader.vertexShader; + shader.vertexShader = shader.vertexShader.replace( + 'void main() {', + 'void main() {\n\tvOpacity = aOpacity;' + ); + shader.vertexShader = shader.vertexShader.replace( + 'gl_PointSize = size;', + 'gl_PointSize = size * aSize;' + ); + + shader.fragmentShader = 'varying float vOpacity;\n' + shader.fragmentShader; + shader.fragmentShader = shader.fragmentShader.replace( + 'vec4 diffuseColor = vec4( diffuse, opacity );', + 'vec4 diffuseColor = vec4( diffuse, opacity * vOpacity );' + ); + + }; + + const points = new THREE.Points( geometry, material ); + points.frustumCulled = false; + scene.add( points ); + + this.posAttr = posAttr; + this.opacityAttr = opacityAttr; + this.sizeAttr = sizeAttr; + this.positions = positions; + this.opacities = opacities; + this.sizes = sizes; + + this.particles = []; + + for ( let i = 0; i < NOS_POOL_SIZE; i ++ ) { + + this.particles.push( { + life: 0, + velocity: new THREE.Vector3(), + initialSize: 0, + } ); + + } + + this.emitIndex = 0; + + } + + update( dt, vehicle ) { + + const shouldEmit = vehicle.nosActive === true && vehicle.nosIntensity > 0.04; + let aliveCount = 0; + + if ( shouldEmit ) { + + _nosFwd.set( 0, 0, 1 ).applyQuaternion( vehicle.container.quaternion ); + _nosFwd.y = 0; + + if ( _nosFwd.lengthSq() > 1e-6 ) _nosFwd.normalize(); + + const yLift = vehicle.container.position.y + 0.1; + const bl = vehicle.wheelBL ? vehicle.wheelBL.getWorldPosition( _blPos ) : null; + const br = vehicle.wheelBR ? vehicle.wheelBR.getWorldPosition( _brPos ) : null; + + for ( let i = 0; i < NOS_PARTICLES_PER_EMIT; i ++ ) { + + if ( bl ) { + + _nosSpawn.copy( bl ).addScaledVector( _nosFwd, - 0.42 ); + _nosSpawn.y = yLift; + this.emitAt( + _nosSpawn.x + ( Math.random() - 0.5 ) * NOS_EMIT_JITTER, + _nosSpawn.y, + _nosSpawn.z + ( Math.random() - 0.5 ) * NOS_EMIT_JITTER, + _nosFwd + ); + + } + + if ( br ) { + + _nosSpawn.copy( br ).addScaledVector( _nosFwd, - 0.42 ); + _nosSpawn.y = yLift; + this.emitAt( + _nosSpawn.x + ( Math.random() - 0.5 ) * NOS_EMIT_JITTER, + _nosSpawn.y, + _nosSpawn.z + ( Math.random() - 0.5 ) * NOS_EMIT_JITTER, + _nosFwd + ); + + } + + } + + } + + const damping = 1 - dt * 0.85; + + for ( let i = 0; i < NOS_POOL_SIZE; i ++ ) { + + const p = this.particles[ i ]; + if ( p.life <= 0 ) continue; + + p.life -= dt; + + if ( p.life <= 0 ) { + + this.opacities[ i ] = 0; + aliveCount ++; + continue; + + } + + const t = 1 - p.life * INV_NOS_MAX_LIFE; + + p.velocity.multiplyScalar( damping ); + + const posIdx = i * 3; + this.positions[ posIdx ] += p.velocity.x * dt; + this.positions[ posIdx + 1 ] += p.velocity.y * dt; + this.positions[ posIdx + 2 ] += p.velocity.z * dt; + + this.opacities[ i ] = ( 1 - t ) * ( 0.28 + vehicle.nosIntensity * 0.22 ); + this.sizes[ i ] = p.initialSize * ( 0.45 + t * 1.9 ); + + aliveCount ++; + + } + + if ( shouldEmit || aliveCount > 0 ) { + + this.posAttr.needsUpdate = true; + this.opacityAttr.needsUpdate = true; + this.sizeAttr.needsUpdate = true; + + } + + } + + emitAt( x, y, z, fwdXZ ) { + + const i = this.emitIndex; + this.emitIndex = ( i + 1 ) % NOS_POOL_SIZE; + + const p = this.particles[ i ]; + + const posIdx = i * 3; + this.positions[ posIdx ] = x; + this.positions[ posIdx + 1 ] = y; + this.positions[ posIdx + 2 ] = z; + + p.initialSize = NOS_BASE_SIZE * ( 0.55 + Math.random() * 0.45 ); + + const bx = - fwdXZ.x * NOS_TRAIL_SPEED; + const bz = - fwdXZ.z * NOS_TRAIL_SPEED; + + p.velocity.set( + bx + ( Math.random() - 0.5 ) * 1.2, + 0.15 + Math.random() * 0.35, + bz + ( Math.random() - 0.5 ) * 1.2 + ); + + p.life = NOS_MAX_LIFE; + + } + +} + diff --git a/js/Vehicle.js b/js/Vehicle.js index 97eda6a..bcccb17 100644 --- a/js/Vehicle.js +++ b/js/Vehicle.js @@ -14,6 +14,15 @@ const SPEED_SCALE = 12.5; const LINEAR_DAMP = 0.1; export const MAX_SPEED = 1.5; +const NOS_MAX_MULT = 1.65; +const NOS_DRIVE_MULT = 1.5; +export const NOS_DURATION = 2.0; +const NOS_WHEEL_LIFT = 0.065; +/** Extra pitch (rad), subtracted from rotation.x target — nose-up during NOS. */ +const NOS_BODY_NOSE_UP = 0.24; +const NOS_DRIFT_THRESHOLD = 0.48; +const NOS_DRIFT_RECHARGE = 0.55; + function lerpAngle( a, b, t ) { let diff = b - a; @@ -53,6 +62,13 @@ export class Vehicle { this.driftIntensity = 0; + this.nosTankRemaining = NOS_DURATION; + this.nosActive = false; + this.nosIntensity = 0; + + this.wheelFLBaseY = 0; + this.wheelFRBaseY = 0; + } init( model ) { @@ -92,6 +108,9 @@ export class Vehicle { } ); + if ( this.wheelFL ) this.wheelFLBaseY = this.wheelFL.position.y; + if ( this.wheelFR ) this.wheelFRBaseY = this.wheelFR.position.y; + return this.container; } @@ -101,6 +120,36 @@ export class Vehicle { this.inputX = controlsInput.x; this.inputZ = controlsInput.z; + const nosInput = controlsInput.nos === true; + + const canBoost = this.nosTankRemaining > 0; + let nosOn = nosInput && canBoost; + + if ( nosOn ) { + + this.nosTankRemaining -= dt; + + if ( this.nosTankRemaining <= 0 ) { + + this.nosTankRemaining = 0; + nosOn = false; + + } + + } + + this.nosActive = nosOn; + + if ( this.nosActive ) { + + this.nosIntensity = THREE.MathUtils.lerp( this.nosIntensity, 1, Math.min( 1, dt * 14 ) ); + + } else { + + this.nosIntensity *= Math.exp( - dt * 9 ); + + } + if ( controlsInput.touchActive && ( this.inputX !== 0 || this.inputZ !== 0 ) ) { // Touch: joystick defines world-space direction, auto-gas @@ -112,7 +161,8 @@ export class Vehicle { const cross = _forward.x * this.inputZ - _forward.z * this.inputX; this.inputX = THREE.MathUtils.clamp( - cross * 2, - 1, 1 ); - this.linearSpeed = THREE.MathUtils.lerp( this.linearSpeed, MAX_SPEED, dt * 1.5 ); + const touchCap = this.nosActive ? MAX_SPEED * NOS_MAX_MULT : MAX_SPEED; + this.linearSpeed = THREE.MathUtils.lerp( this.linearSpeed, touchCap, dt * 1.5 ); } else { @@ -139,7 +189,8 @@ export class Vehicle { } else { - this.linearSpeed = THREE.MathUtils.lerp( this.linearSpeed, targetSpeed * MAX_SPEED, dt * 1.5 ); + const forwardCap = MAX_SPEED * ( this.nosActive ? NOS_MAX_MULT : 1 ); + this.linearSpeed = THREE.MathUtils.lerp( this.linearSpeed, targetSpeed * forwardCap, dt * 1.5 ); } @@ -167,7 +218,8 @@ export class Vehicle { _right.normalize(); const angvel = this.rigidBody.motionProperties.angularVelocity; - const drive = this.linearSpeed * 100 * dt; + let drive = this.linearSpeed * 100 * dt; + if ( this.nosActive ) drive *= NOS_DRIVE_MULT; rigidBody.setAngularVelocity( this.physicsWorld, this.rigidBody, [ angvel[ 0 ] + _right.x * drive, @@ -204,6 +256,9 @@ export class Vehicle { this.linearSpeed = 0; this.angularSpeed = 0; this.acceleration = 0; + this.nosTankRemaining = NOS_DURATION; + this.nosActive = false; + this.nosIntensity = 0; this.container.rotation.set( 0, 0, 0 ); this.container.quaternion.identity(); @@ -228,6 +283,20 @@ export class Vehicle { this.driftIntensity = Math.abs( this.linearSpeed - this.acceleration ) + ( this.bodyNode ? Math.abs( this.bodyNode.rotation.z ) * 2 : 0 ); + if ( this.driftIntensity > NOS_DRIFT_THRESHOLD ) { + + const driftFactor = THREE.MathUtils.clamp( + ( this.driftIntensity - NOS_DRIFT_THRESHOLD ) / ( 1.25 - NOS_DRIFT_THRESHOLD ), + 0, + 1 + ); + this.nosTankRemaining = Math.min( + NOS_DURATION, + this.nosTankRemaining + NOS_DRIFT_RECHARGE * dt * driftFactor + ); + + } + } alignWithY( quaternion, newY ) { @@ -245,9 +314,12 @@ export class Vehicle { if ( ! this.bodyNode ) return; + let pitchTarget = -( this.linearSpeed - this.acceleration ) / 6; + if ( this.nosActive ) pitchTarget -= NOS_BODY_NOSE_UP; + this.bodyNode.rotation.x = lerpAngle( this.bodyNode.rotation.x, - -( this.linearSpeed - this.acceleration ) / 6, + pitchTarget, dt * 10 ); @@ -281,6 +353,22 @@ export class Vehicle { } + const liftTarget = this.nosActive ? NOS_WHEEL_LIFT : 0; + + if ( this.wheelFL ) { + + const ty = this.wheelFLBaseY + liftTarget; + this.wheelFL.position.y = THREE.MathUtils.lerp( this.wheelFL.position.y, ty, Math.min( 1, dt * 14 ) ); + + } + + if ( this.wheelFR ) { + + const ty = this.wheelFRBaseY + liftTarget; + this.wheelFR.position.y = THREE.MathUtils.lerp( this.wheelFR.position.y, ty, Math.min( 1, dt * 14 ) ); + + } + } } diff --git a/js/main.js b/js/main.js index d87126a..2fa7073 100644 --- a/js/main.js +++ b/js/main.js @@ -8,10 +8,11 @@ import { Camera } from './Camera.js'; import { Controls } from './Controls.js'; import { buildTrack, decodeCells, computeSpawnPosition, computeTrackBounds } from './Track.js'; import { buildWallColliders, createSphereBody } from './Physics.js'; -import { SmokeTrails } from './Particles.js'; +import { SmokeTrails, NosTaillightTrails } from './Particles.js'; import { DriftMarks } from './DriftMarks.js'; import { GameAudio } from './Audio.js'; import { LapTimer } from './LapTimer.js'; +import { NosHud } from './NosHud.js'; import { ColorMapGLTFLoader } from './Loader.js'; @@ -226,12 +227,14 @@ async function init() { const controls = new Controls(); const particles = new SmokeTrails( scene ); + const nosTrails = new NosTaillightTrails( scene ); const driftMarks = new DriftMarks( scene, mapParam ); const audio = new GameAudio(); audio.init( cam.camera ); const lapTimer = new LapTimer( customCells, mapParam ); + const nosHud = new NosHud( controls ); const _forward = new THREE.Vector3(); const _camLead = new THREE.Vector3(); @@ -266,6 +269,8 @@ async function init() { vehicle.update( dt, input ); + nosHud.update( vehicle ); + dirLight.position.set( vehicle.spherePos.x + 11.4, 15, @@ -274,10 +279,11 @@ async function init() { const mv = vehicle.modelVelocity; _camLead.set( 0, 0, 1 ).applyQuaternion( vehicle.container.quaternion ).multiplyScalar( Math.sqrt( mv.x * mv.x + mv.z * mv.z ) ); - cam.update( dt, vehicle.spherePos, _camLead ); + cam.update( dt, vehicle.spherePos, _camLead, vehicle.nosIntensity ); particles.update( dt, vehicle ); + nosTrails.update( dt, vehicle ); driftMarks.update( dt, vehicle ); - audio.update( dt, vehicle.linearSpeed / MAX_SPEED, input.z, vehicle.driftIntensity ); + audio.update( dt, vehicle.linearSpeed / MAX_SPEED, input.z, vehicle.driftIntensity, vehicle.nosActive ); const hasInput = input.touchActive || Math.abs( input.x ) > 0.05 || Math.abs( input.z ) > 0.05; lapTimer.update( dt, vehicle.spherePos, hasInput );