|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>City-Scale Pagani Racing Circuit</title> |
|
|
<style> |
|
|
html, body { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
height: 100%; |
|
|
overflow: hidden; |
|
|
background: linear-gradient(135deg, #1e3c72, #2a5298); |
|
|
font-family: Arial, sans-serif; |
|
|
} |
|
|
|
|
|
#canvas { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
display: block; |
|
|
touch-action: none; |
|
|
} |
|
|
|
|
|
#hud { |
|
|
position: absolute; |
|
|
top: 20px; |
|
|
left: 20px; |
|
|
background: rgba(0, 0, 0, 0.8); |
|
|
color: white; |
|
|
padding: 15px; |
|
|
border-radius: 10px; |
|
|
font-family: monospace; |
|
|
min-width: 250px; |
|
|
} |
|
|
|
|
|
#speedometer { |
|
|
font-size: 28px; |
|
|
font-weight: bold; |
|
|
color: #00ff00; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
#trackInfo { |
|
|
font-size: 16px; |
|
|
color: #ffff00; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
#lapInfo { |
|
|
font-size: 14px; |
|
|
color: #00ccff; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
#sectorInfo { |
|
|
font-size: 12px; |
|
|
color: #ff9900; |
|
|
} |
|
|
|
|
|
#controls { |
|
|
position: absolute; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
background: rgba(0, 0, 0, 0.7); |
|
|
color: white; |
|
|
padding: 15px; |
|
|
border-radius: 10px; |
|
|
font-size: 14px; |
|
|
max-width: 300px; |
|
|
} |
|
|
|
|
|
#mobileControls { |
|
|
position: absolute; |
|
|
bottom: 20px; |
|
|
left: 20px; |
|
|
display: grid; |
|
|
grid-template-columns: repeat(3, 60px); |
|
|
grid-template-rows: repeat(3, 60px); |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.control-btn { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
border: 2px solid white; |
|
|
border-radius: 10px; |
|
|
color: white; |
|
|
font-weight: bold; |
|
|
font-size: 18px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
touch-action: manipulation; |
|
|
user-select: none; |
|
|
transition: all 0.1s ease; |
|
|
} |
|
|
|
|
|
.control-btn:active { |
|
|
background: rgba(255, 255, 255, 0.4); |
|
|
transform: scale(0.95); |
|
|
} |
|
|
|
|
|
.control-btn.empty { |
|
|
opacity: 0; |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
#minimap { |
|
|
position: absolute; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
width: 200px; |
|
|
height: 200px; |
|
|
background: rgba(0, 0, 0, 0.8); |
|
|
border: 2px solid white; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
#controls { |
|
|
font-size: 12px; |
|
|
bottom: 100px; |
|
|
right: 10px; |
|
|
max-width: 200px; |
|
|
} |
|
|
|
|
|
#minimap { |
|
|
width: 150px; |
|
|
height: 150px; |
|
|
} |
|
|
|
|
|
#hud { |
|
|
min-width: 200px; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<canvas id="canvas"></canvas> |
|
|
|
|
|
<div id="hud"> |
|
|
<div id="speedometer">0 KM/H</div> |
|
|
<div id="trackInfo">SECTOR: Downtown</div> |
|
|
<div id="lapInfo">LAP: 1/3 | TIME: 0:00</div> |
|
|
<div id="sectorInfo">NEXT: Industrial Zone</div> |
|
|
</div> |
|
|
|
|
|
<canvas id="minimap"></canvas> |
|
|
|
|
|
<div id="controls"> |
|
|
<div><strong>City Racing Controls:</strong></div> |
|
|
<div>WASD / Arrows: Drive</div> |
|
|
<div>Space: Brake</div> |
|
|
<div>Shift: Boost</div> |
|
|
<div>R: Reset Position</div> |
|
|
<br> |
|
|
<div><strong>Track Sectors:</strong></div> |
|
|
<div>1. Downtown Streets</div> |
|
|
<div>2. Industrial Zone</div> |
|
|
<div>3. Harbor Bridge</div> |
|
|
<div>4. Airport Highway</div> |
|
|
</div> |
|
|
|
|
|
<div id="mobileControls"> |
|
|
<div class="control-btn empty"></div> |
|
|
<div class="control-btn" data-action="forward">↑</div> |
|
|
<div class="control-btn empty"></div> |
|
|
<div class="control-btn" data-action="left">←</div> |
|
|
<div class="control-btn" data-action="brake">■</div> |
|
|
<div class="control-btn" data-action="right">→</div> |
|
|
<div class="control-btn empty"></div> |
|
|
<div class="control-btn" data-action="backward">↓</div> |
|
|
<div class="control-btn" data-action="boost">★</div> |
|
|
</div> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
|
<script> |
|
|
class CityScaleRacingCircuit { |
|
|
constructor() { |
|
|
this.scene = new THREE.Scene(); |
|
|
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); |
|
|
this.renderer = new THREE.WebGLRenderer({ |
|
|
canvas: document.getElementById('canvas'), |
|
|
antialias: true |
|
|
}); |
|
|
|
|
|
|
|
|
this.CITY_SIZE = 2000; |
|
|
this.TRACK_WIDTH = 25; |
|
|
this.BUILDING_HEIGHT = 50; |
|
|
this.SECTOR_LENGTH = 500; |
|
|
|
|
|
|
|
|
this.car = null; |
|
|
this.carPosition = new THREE.Vector3(0, 2, 0); |
|
|
this.carRotation = 0; |
|
|
this.carSpeed = 0; |
|
|
this.maxSpeed = 250; |
|
|
this.acceleration = 1.2; |
|
|
this.turnSpeed = 0.025; |
|
|
this.gear = 1; |
|
|
this.rpm = 800; |
|
|
this.wheelRotation = 0; |
|
|
|
|
|
|
|
|
this.trackSectors = []; |
|
|
this.currentSector = 0; |
|
|
this.lapCount = 1; |
|
|
this.lapTime = 0; |
|
|
this.checkpoints = []; |
|
|
this.lastCheckpoint = -1; |
|
|
|
|
|
|
|
|
this.buildings = []; |
|
|
|
|
|
|
|
|
this.keys = {}; |
|
|
this.touchActive = {}; |
|
|
|
|
|
|
|
|
this.minimapRenderer = null; |
|
|
this.minimapCamera = null; |
|
|
this.minimapScene = null; |
|
|
|
|
|
this.init(); |
|
|
this.createDetailedPaganiZonda(); |
|
|
this.createCityScaleTrack(); |
|
|
this.createCityEnvironment(); |
|
|
this.setupLighting(); |
|
|
this.setupMinimap(); |
|
|
this.setupControls(); |
|
|
this.animate(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight); |
|
|
this.renderer.setClearColor(0x87CEEB); |
|
|
this.renderer.shadowMap.enabled = true; |
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
|
|
|
|
|
|
|
|
this.camera.position.set(0, 15, 30); |
|
|
this.camera.lookAt(0, 0, 0); |
|
|
|
|
|
|
|
|
this.scene.fog = new THREE.Fog(0x87CEEB, 100, 1500); |
|
|
} |
|
|
|
|
|
createDetailedPaganiZonda() { |
|
|
const carGroup = new THREE.Group(); |
|
|
|
|
|
|
|
|
const chassisGeometry = new THREE.BoxGeometry(4, 1, 7); |
|
|
const chassisMaterial = new THREE.MeshPhongMaterial({ |
|
|
color: 0x1a1a2e, |
|
|
shininess: 100 |
|
|
}); |
|
|
const chassis = new THREE.Mesh(chassisGeometry, chassisMaterial); |
|
|
chassis.position.y = 1; |
|
|
chassis.castShadow = true; |
|
|
carGroup.add(chassis); |
|
|
|
|
|
|
|
|
const hoodGeometry = new THREE.BoxGeometry(3.5, 0.6, 2.5); |
|
|
const hood = new THREE.Mesh(hoodGeometry, chassisMaterial); |
|
|
hood.position.set(0, 1.6, 2.2); |
|
|
hood.castShadow = true; |
|
|
carGroup.add(hood); |
|
|
|
|
|
|
|
|
const cabinGeometry = new THREE.BoxGeometry(3, 1.5, 3.5); |
|
|
const cabinMaterial = new THREE.MeshPhongMaterial({ |
|
|
color: 0x111111, |
|
|
transparent: true, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
const cabin = new THREE.Mesh(cabinGeometry, cabinMaterial); |
|
|
cabin.position.set(0, 2.2, 0); |
|
|
carGroup.add(cabin); |
|
|
|
|
|
|
|
|
const windshieldGeometry = new THREE.PlaneGeometry(3.2, 1.8); |
|
|
const windshieldMaterial = new THREE.MeshPhongMaterial({ |
|
|
color: 0x87CEEB, |
|
|
transparent: true, |
|
|
opacity: 0.2, |
|
|
side: THREE.DoubleSide |
|
|
}); |
|
|
const windshield = new THREE.Mesh(windshieldGeometry, windshieldMaterial); |
|
|
windshield.position.set(0, 2.8, 1.5); |
|
|
windshield.rotation.x = -0.3; |
|
|
carGroup.add(windshield); |
|
|
|
|
|
|
|
|
const dashGeometry = new THREE.BoxGeometry(2.8, 0.4, 1.2); |
|
|
const dashMaterial = new THREE.MeshPhongMaterial({ color: 0x333333 }); |
|
|
const dashboard = new THREE.Mesh(dashGeometry, dashMaterial); |
|
|
dashboard.position.set(0, 1.8, 1.5); |
|
|
carGroup.add(dashboard); |
|
|
|
|
|
|
|
|
const headlightGeometry = new THREE.CylinderGeometry(0.25, 0.25, 0.3, 12); |
|
|
const headlightMaterial = new THREE.MeshPhongMaterial({ |
|
|
color: 0xffffff, |
|
|
emissive: 0x444444 |
|
|
}); |
|
|
|
|
|
|
|
|
for (let i = 0; i < 3; i++) { |
|
|
const headlight = new THREE.Mesh(headlightGeometry, headlightMaterial); |
|
|
headlight.position.set(-1.0 + (i * 0.35), 1.2, 3.3); |
|
|
headlight.rotation.z = Math.PI / 2; |
|
|
carGroup.add(headlight); |
|
|
} |
|
|
|
|
|
|
|
|
for (let i = 0; i < 3; i++) { |
|
|
const headlight = new THREE.Mesh(headlightGeometry, headlightMaterial); |
|
|
headlight.position.set(1.0 - (i * 0.35), 1.2, 3.3); |
|
|
headlight.rotation.z = Math.PI / 2; |
|
|
carGroup.add(headlight); |
|
|
} |
|
|
|
|
|
|
|
|
const wingGeometry = new THREE.BoxGeometry(4.5, 0.3, 1.2); |
|
|
const wingMaterial = new THREE.MeshPhongMaterial({ |
|
|
color: 0x000080, |
|
|
shininess: 150 |
|
|
}); |
|
|
const wing = new THREE.Mesh(wingGeometry, wingMaterial); |
|
|
wing.position.set(0, 2.8, -3); |
|
|
carGroup.add(wing); |
|
|
|
|
|
|
|
|
const strutGeometry = new THREE.CylinderGeometry(0.08, 0.08, 1.2, 8); |
|
|
const leftStrut = new THREE.Mesh(strutGeometry, wingMaterial); |
|
|
leftStrut.position.set(-1.5, 2.2, -3); |
|
|
carGroup.add(leftStrut); |
|
|
|
|
|
const rightStrut = new THREE.Mesh(strutGeometry, wingMaterial); |
|
|
rightStrut.position.set(1.5, 2.2, -3); |
|
|
carGroup.add(rightStrut); |
|
|
|
|
|
|
|
|
const scoopGeometry = new THREE.BoxGeometry(0.6, 0.5, 1.2); |
|
|
const scoopMaterial = new THREE.MeshPhongMaterial({ color: 0x444444 }); |
|
|
|
|
|
const leftScoop = new THREE.Mesh(scoopGeometry, scoopMaterial); |
|
|
leftScoop.position.set(-1.8, 1.2, 0); |
|
|
carGroup.add(leftScoop); |
|
|
|
|
|
const rightScoop = new THREE.Mesh(scoopGeometry, scoopMaterial); |
|
|
rightScoop.position.set(1.8, 1.2, 0); |
|
|
carGroup.add(rightScoop); |
|
|
|
|
|
|
|
|
const wheelGeometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16); |
|
|
const wheelMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 }); |
|
|
const rimGeometry = new THREE.CylinderGeometry(0.65, 0.65, 0.55, 16); |
|
|
const rimMaterial = new THREE.MeshPhongMaterial({ color: 0x666666 }); |
|
|
|
|
|
this.wheels = []; |
|
|
const wheelPositions = [ |
|
|
[-1.8, 0.8, 2.2], |
|
|
[1.8, 0.8, 2.2], |
|
|
[-1.8, 0.8, -2.2], |
|
|
[1.8, 0.8, -2.2] |
|
|
]; |
|
|
|
|
|
wheelPositions.forEach((pos, index) => { |
|
|
const wheelGroup = new THREE.Group(); |
|
|
|
|
|
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); |
|
|
const rim = new THREE.Mesh(rimGeometry, rimMaterial); |
|
|
|
|
|
wheel.rotation.z = Math.PI / 2; |
|
|
rim.rotation.z = Math.PI / 2; |
|
|
|
|
|
wheelGroup.add(wheel); |
|
|
wheelGroup.add(rim); |
|
|
wheelGroup.position.set(pos[0], pos[1], pos[2]); |
|
|
wheelGroup.castShadow = true; |
|
|
|
|
|
this.wheels.push(wheelGroup); |
|
|
carGroup.add(wheelGroup); |
|
|
}); |
|
|
|
|
|
this.car = carGroup; |
|
|
this.scene.add(carGroup); |
|
|
} |
|
|
|
|
|
createCityScaleTrack() { |
|
|
|
|
|
const trackMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); |
|
|
const trackMarkingMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 }); |
|
|
const barrierMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 }); |
|
|
|
|
|
|
|
|
const sectors = [ |
|
|
{ |
|
|
name: "Downtown Streets", |
|
|
start: { x: 0, z: 0 }, |
|
|
path: [ |
|
|
{ x: 0, z: 0 }, |
|
|
{ x: 0, z: 400 }, |
|
|
{ x: 200, z: 600 }, |
|
|
{ x: 400, z: 600 } |
|
|
], |
|
|
width: this.TRACK_WIDTH, |
|
|
type: "street" |
|
|
}, |
|
|
{ |
|
|
name: "Industrial Zone", |
|
|
start: { x: 400, z: 600 }, |
|
|
path: [ |
|
|
{ x: 400, z: 600 }, |
|
|
{ x: 800, z: 800 }, |
|
|
{ x: 1200, z: 600 }, |
|
|
{ x: 1400, z: 400 } |
|
|
], |
|
|
width: this.TRACK_WIDTH * 1.2, |
|
|
type: "industrial" |
|
|
}, |
|
|
{ |
|
|
name: "Harbor Bridge", |
|
|
start: { x: 1400, z: 400 }, |
|
|
path: [ |
|
|
{ x: 1400, z: 400 }, |
|
|
{ x: 1600, z: 200 }, |
|
|
{ x: 1600, z: -200 }, |
|
|
{ x: 1400, z: -400 } |
|
|
], |
|
|
width: this.TRACK_WIDTH * 0.8, |
|
|
type: "bridge" |
|
|
}, |
|
|
{ |
|
|
name: "Airport Highway", |
|
|
start: { x: 1400, z: -400 }, |
|
|
path: [ |
|
|
{ x: 1400, z: -400 }, |
|
|
{ x: 1000, z: -600 }, |
|
|
{ x: 400, z: -600 }, |
|
|
{ x: 0, z: -400 }, |
|
|
{ x: 0, z: 0 } |
|
|
], |
|
|
width: this.TRACK_WIDTH * 1.5, |
|
|
type: "highway" |
|
|
} |
|
|
]; |
|
|
|
|
|
this.trackSectors = sectors; |
|
|
this.checkpoints = []; |
|
|
|
|
|
|
|
|
sectors.forEach((sector, sectorIndex) => { |
|
|
for (let i = 0; i < sector.path.length - 1; i++) { |
|
|
const start = sector.path[i]; |
|
|
const end = sector.path[i + 1]; |
|
|
|
|
|
|
|
|
const distance = Math.sqrt( |
|
|
Math.pow(end.x - start.x, 2) + Math.pow(end.z - start.z, 2) |
|
|
); |
|
|
const angle = Math.atan2(end.x - start.x, end.z - start.z); |
|
|
const midX = (start.x + end.x) / 2; |
|
|
const midZ = (start.z + end.z) / 2; |
|
|
|
|
|
|
|
|
const trackGeometry = new THREE.PlaneGeometry(sector.width, distance); |
|
|
const trackSegment = new THREE.Mesh(trackGeometry, trackMaterial); |
|
|
trackSegment.rotation.x = -Math.PI / 2; |
|
|
trackSegment.rotation.y = angle; |
|
|
trackSegment.position.set(midX, this.getTrackElevation(midX, midZ, sector.type), midZ); |
|
|
trackSegment.receiveShadow = true; |
|
|
this.scene.add(trackSegment); |
|
|
|
|
|
|
|
|
const markingGeometry = new THREE.PlaneGeometry(0.5, distance); |
|
|
const marking = new THREE.Mesh(markingGeometry, trackMarkingMaterial); |
|
|
marking.rotation.x = -Math.PI / 2; |
|
|
marking.rotation.y = angle; |
|
|
marking.position.set(midX, this.getTrackElevation(midX, midZ, sector.type) + 0.01, midZ); |
|
|
this.scene.add(marking); |
|
|
|
|
|
|
|
|
this.createTrackBarriers(start, end, sector.width, sector.type, barrierMaterial); |
|
|
|
|
|
|
|
|
if (i % 2 === 0) { |
|
|
this.checkpoints.push({ |
|
|
position: new THREE.Vector3(midX, this.getTrackElevation(midX, midZ, sector.type) + 2, midZ), |
|
|
sector: sectorIndex, |
|
|
index: this.checkpoints.length |
|
|
}); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
this.createStartFinishLine(); |
|
|
} |
|
|
|
|
|
getTrackElevation(x, z, type) { |
|
|
switch (type) { |
|
|
case "bridge": |
|
|
return 15; |
|
|
case "highway": |
|
|
return 5; |
|
|
case "industrial": |
|
|
return 2; |
|
|
default: |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
createTrackBarriers(start, end, width, type, material) { |
|
|
const distance = Math.sqrt( |
|
|
Math.pow(end.x - start.x, 2) + Math.pow(end.z - start.z, 2) |
|
|
); |
|
|
const angle = Math.atan2(end.x - start.x, end.z - start.z); |
|
|
const midX = (start.x + end.x) / 2; |
|
|
const midZ = (start.z + end.z) / 2; |
|
|
const elevation = this.getTrackElevation(midX, midZ, type); |
|
|
|
|
|
|
|
|
let barrierHeight = 3; |
|
|
if (type === "bridge") barrierHeight = 6; |
|
|
if (type === "highway") barrierHeight = 4; |
|
|
|
|
|
const barrierGeometry = new THREE.BoxGeometry(2, barrierHeight, distance); |
|
|
|
|
|
|
|
|
const leftBarrier = new THREE.Mesh(barrierGeometry, material); |
|
|
leftBarrier.position.set( |
|
|
midX + Math.cos(angle) * (width / 2 + 1), |
|
|
elevation + barrierHeight / 2, |
|
|
midZ - Math.sin(angle) * (width / 2 + 1) |
|
|
); |
|
|
leftBarrier.rotation.y = angle; |
|
|
leftBarrier.castShadow = true; |
|
|
this.scene.add(leftBarrier); |
|
|
|
|
|
|
|
|
const rightBarrier = new THREE.Mesh(barrierGeometry, material); |
|
|
rightBarrier.position.set( |
|
|
midX - Math.cos(angle) * (width / 2 + 1), |
|
|
elevation + barrierHeight / 2, |
|
|
midZ + Math.sin(angle) * (width / 2 + 1) |
|
|
); |
|
|
rightBarrier.rotation.y = angle; |
|
|
rightBarrier.castShadow = true; |
|
|
this.scene.add(rightBarrier); |
|
|
} |
|
|
|
|
|
createStartFinishLine() { |
|
|
|
|
|
const lineGeometry = new THREE.PlaneGeometry(this.TRACK_WIDTH, 4); |
|
|
const lineMaterial = new THREE.MeshLambertMaterial({ |
|
|
color: 0x000000, |
|
|
transparent: true, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
const startLine = new THREE.Mesh(lineGeometry, lineMaterial); |
|
|
startLine.rotation.x = -Math.PI / 2; |
|
|
startLine.position.set(0, 0.02, 0); |
|
|
this.scene.add(startLine); |
|
|
|
|
|
|
|
|
for (let i = 0; i < 8; i++) { |
|
|
for (let j = 0; j < 2; j++) { |
|
|
if ((i + j) % 2 === 0) { |
|
|
const checkerGeometry = new THREE.PlaneGeometry(this.TRACK_WIDTH / 8, 2); |
|
|
const checkerMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); |
|
|
const checker = new THREE.Mesh(checkerGeometry, checkerMaterial); |
|
|
checker.rotation.x = -Math.PI / 2; |
|
|
checker.position.set( |
|
|
-this.TRACK_WIDTH / 2 + (i + 0.5) * this.TRACK_WIDTH / 8, |
|
|
0.03, |
|
|
-2 + j * 4 |
|
|
); |
|
|
this.scene.add(checker); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
createCityEnvironment() { |
|
|
|
|
|
this.createBuildings(); |
|
|
|
|
|
|
|
|
const groundGeometry = new THREE.PlaneGeometry(this.CITY_SIZE * 2, this.CITY_SIZE * 2); |
|
|
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); |
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial); |
|
|
ground.rotation.x = -Math.PI / 2; |
|
|
ground.position.y = -1; |
|
|
ground.receiveShadow = true; |
|
|
this.scene.add(ground); |
|
|
|
|
|
|
|
|
this.createCityLandmarks(); |
|
|
} |
|
|
|
|
|
createBuildings() { |
|
|
const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 }); |
|
|
const windowMaterial = new THREE.MeshLambertMaterial({ color: 0xffff99 }); |
|
|
const columnMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 }); |
|
|
|
|
|
|
|
|
for (let x = -this.CITY_SIZE; x < this.CITY_SIZE; x += 120) { |
|
|
for (let z = -this.CITY_SIZE; z < this.CITY_SIZE; z += 120) { |
|
|
|
|
|
if (this.isTrackArea(x, z)) continue; |
|
|
|
|
|
|
|
|
const seed = (x * 1000 + z) * 0.001; |
|
|
const buildingHeight = 25 + (Math.sin(seed) * 0.5 + 0.5) * this.BUILDING_HEIGHT; |
|
|
const buildingWidth = 35 + (Math.sin(seed * 1.7) * 0.5 + 0.5) * 45; |
|
|
const buildingDepth = 35 + (Math.sin(seed * 2.3) * 0.5 + 0.5) * 45; |
|
|
const supportHeight = 15 + (Math.sin(seed * 3.1) * 0.5 + 0.5) * 10; |
|
|
const columnRadius = 2 + (Math.sin(seed * 4.7) * 0.5 + 0.5) * 1.5; |
|
|
|
|
|
const buildingX = x + (Math.sin(seed * 5.3) * 0.5) * 60; |
|
|
const buildingZ = z + (Math.sin(seed * 6.1) * 0.5) * 60; |
|
|
|
|
|
|
|
|
this.buildings.push({ |
|
|
x: buildingX, |
|
|
z: buildingZ, |
|
|
width: buildingWidth, |
|
|
depth: buildingDepth, |
|
|
supportHeight: supportHeight, |
|
|
totalHeight: supportHeight + buildingHeight |
|
|
}); |
|
|
|
|
|
|
|
|
const buildingGeometry = new THREE.BoxGeometry(buildingWidth, buildingHeight, buildingDepth); |
|
|
const building = new THREE.Mesh(buildingGeometry, buildingMaterial); |
|
|
building.position.set( |
|
|
buildingX, |
|
|
supportHeight + buildingHeight / 2, |
|
|
buildingZ |
|
|
); |
|
|
building.castShadow = true; |
|
|
this.scene.add(building); |
|
|
|
|
|
|
|
|
const columnGeometry = new THREE.CylinderGeometry(columnRadius, columnRadius * 1.2, supportHeight, 12); |
|
|
|
|
|
|
|
|
const cornerOffsets = [ |
|
|
[-buildingWidth/2 + columnRadius, -buildingDepth/2 + columnRadius], |
|
|
[buildingWidth/2 - columnRadius, -buildingDepth/2 + columnRadius], |
|
|
[-buildingWidth/2 + columnRadius, buildingDepth/2 - columnRadius], |
|
|
[buildingWidth/2 - columnRadius, buildingDepth/2 - columnRadius] |
|
|
]; |
|
|
|
|
|
cornerOffsets.forEach(offset => { |
|
|
const column = new THREE.Mesh(columnGeometry, columnMaterial); |
|
|
column.position.set( |
|
|
buildingX + offset[0], |
|
|
supportHeight / 2, |
|
|
buildingZ + offset[1] |
|
|
); |
|
|
column.castShadow = true; |
|
|
this.scene.add(column); |
|
|
|
|
|
|
|
|
const capGeometry = new THREE.SphereGeometry(columnRadius * 1.1, 8, 6); |
|
|
|
|
|
|
|
|
const topCap = new THREE.Mesh(capGeometry, columnMaterial); |
|
|
topCap.position.set( |
|
|
buildingX + offset[0], |
|
|
supportHeight - columnRadius * 0.3, |
|
|
buildingZ + offset[1] |
|
|
); |
|
|
topCap.scale.y = 0.5; |
|
|
this.scene.add(topCap); |
|
|
|
|
|
|
|
|
const bottomCap = new THREE.Mesh(capGeometry, columnMaterial); |
|
|
bottomCap.position.set( |
|
|
buildingX + offset[0], |
|
|
columnRadius * 0.3, |
|
|
buildingZ + offset[1] |
|
|
); |
|
|
bottomCap.scale.y = 0.5; |
|
|
this.scene.add(bottomCap); |
|
|
}); |
|
|
|
|
|
|
|
|
const braceGeometry = new THREE.CylinderGeometry(columnRadius * 0.3, columnRadius * 0.3, buildingWidth * 0.8, 8); |
|
|
|
|
|
|
|
|
const braceY = supportHeight * 0.6; |
|
|
|
|
|
|
|
|
for (let side = 0; side < 2; side++) { |
|
|
const braceZ = buildingZ + (side === 0 ? -buildingDepth/2 + columnRadius : buildingDepth/2 - columnRadius); |
|
|
const horizontalBrace = new THREE.Mesh(braceGeometry, columnMaterial); |
|
|
horizontalBrace.position.set(buildingX, braceY, braceZ); |
|
|
horizontalBrace.rotation.z = Math.PI / 2; |
|
|
this.scene.add(horizontalBrace); |
|
|
} |
|
|
|
|
|
|
|
|
const sideBraceGeometry = new THREE.CylinderGeometry(columnRadius * 0.3, columnRadius * 0.3, buildingDepth * 0.8, 8); |
|
|
for (let side = 0; side < 2; side++) { |
|
|
const braceX = buildingX + (side === 0 ? -buildingWidth/2 + columnRadius : buildingWidth/2 - columnRadius); |
|
|
const sideBrace = new THREE.Mesh(sideBraceGeometry, columnMaterial); |
|
|
sideBrace.position.set(braceX, braceY, buildingZ); |
|
|
sideBrace.rotation.x = Math.PI / 2; |
|
|
this.scene.add(sideBrace); |
|
|
} |
|
|
|
|
|
|
|
|
const windowsX = Math.floor(buildingWidth / 10); |
|
|
const windowsZ = Math.floor(buildingDepth / 10); |
|
|
const windowsY = Math.floor(buildingHeight / 8); |
|
|
|
|
|
|
|
|
for (let wx = 0; wx < windowsX; wx++) { |
|
|
for (let wy = 0; wy < windowsY; wy++) { |
|
|
if ((Math.sin(seed * 7.1 + wx * 0.5 + wy * 0.3) * 0.5 + 0.5) > 0.4) { |
|
|
const windowGeometry = new THREE.PlaneGeometry(4, 4); |
|
|
const window = new THREE.Mesh(windowGeometry, windowMaterial); |
|
|
|
|
|
|
|
|
window.position.set( |
|
|
buildingX + (wx - windowsX/2) * 10, |
|
|
supportHeight + (wy + 1) * 8, |
|
|
buildingZ + buildingDepth/2 + 0.1 |
|
|
); |
|
|
this.scene.add(window); |
|
|
|
|
|
|
|
|
if ((Math.sin(seed * 8.3 + wx * 0.7 + wy * 0.4) * 0.5 + 0.5) > 0.5) { |
|
|
const backWindow = window.clone(); |
|
|
backWindow.position.z = buildingZ - buildingDepth/2 - 0.1; |
|
|
backWindow.rotation.y = Math.PI; |
|
|
this.scene.add(backWindow); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
for (let wz = 0; wz < windowsZ; wz++) { |
|
|
for (let wy = 0; wy < windowsY; wy++) { |
|
|
if ((Math.sin(seed * 9.7 + wz * 0.6 + wy * 0.5) * 0.5 + 0.5) > 0.4) { |
|
|
const windowGeometry = new THREE.PlaneGeometry(4, 4); |
|
|
const sideWindow = new THREE.Mesh(windowGeometry, windowMaterial); |
|
|
|
|
|
|
|
|
sideWindow.position.set( |
|
|
buildingX - buildingWidth/2 - 0.1, |
|
|
supportHeight + (wy + 1) * 8, |
|
|
buildingZ + (wz - windowsZ/2) * 10 |
|
|
); |
|
|
sideWindow.rotation.y = Math.PI / 2; |
|
|
this.scene.add(sideWindow); |
|
|
|
|
|
|
|
|
if ((Math.sin(seed * 10.1 + wz * 0.8 + wy * 0.6) * 0.5 + 0.5) > 0.5) { |
|
|
const rightWindow = sideWindow.clone(); |
|
|
rightWindow.position.x = buildingX + buildingWidth/2 + 0.1; |
|
|
rightWindow.rotation.y = -Math.PI / 2; |
|
|
this.scene.add(rightWindow); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const underglowGeometry = new THREE.PlaneGeometry(buildingWidth * 0.8, buildingDepth * 0.8); |
|
|
const underglowMaterial = new THREE.MeshBasicMaterial({ |
|
|
color: 0x00aaff, |
|
|
transparent: true, |
|
|
opacity: 0.3, |
|
|
side: THREE.DoubleSide |
|
|
}); |
|
|
const underglow = new THREE.Mesh(underglowGeometry, underglowMaterial); |
|
|
underglow.rotation.x = -Math.PI / 2; |
|
|
underglow.position.set(buildingX, supportHeight * 0.1, buildingZ); |
|
|
this.scene.add(underglow); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
isTrackArea(x, z) { |
|
|
|
|
|
for (let sector of this.trackSectors) { |
|
|
for (let point of sector.path) { |
|
|
const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(z - point.z, 2)); |
|
|
if (distance < 100) return true; |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
createCityLandmarks() { |
|
|
|
|
|
const terminalGeometry = new THREE.BoxGeometry(200, 30, 100); |
|
|
const terminalMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc }); |
|
|
const terminal = new THREE.Mesh(terminalGeometry, terminalMaterial); |
|
|
terminal.position.set(800, 15, -800); |
|
|
terminal.castShadow = true; |
|
|
this.scene.add(terminal); |
|
|
|
|
|
|
|
|
for (let i = 0; i < 5; i++) { |
|
|
const craneGeometry = new THREE.BoxGeometry(5, 80, 5); |
|
|
const craneMaterial = new THREE.MeshLambertMaterial({ color: 0xff6600 }); |
|
|
const crane = new THREE.Mesh(craneGeometry, craneMaterial); |
|
|
crane.position.set(1500 + i * 30, 40, 200 + i * 20); |
|
|
crane.castShadow = true; |
|
|
this.scene.add(crane); |
|
|
} |
|
|
|
|
|
|
|
|
for (let i = 0; i < 3; i++) { |
|
|
const stackGeometry = new THREE.CylinderGeometry(8, 10, 120, 8); |
|
|
const stackMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 }); |
|
|
const stack = new THREE.Mesh(stackGeometry, stackMaterial); |
|
|
stack.position.set(600 + i * 50, 60, 700 + i * 30); |
|
|
stack.castShadow = true; |
|
|
this.scene.add(stack); |
|
|
} |
|
|
} |
|
|
|
|
|
setupLighting() { |
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.6); |
|
|
this.scene.add(ambientLight); |
|
|
|
|
|
|
|
|
const sun = new THREE.DirectionalLight(0xffffff, 0.8); |
|
|
sun.position.set(1000, 800, 500); |
|
|
sun.castShadow = true; |
|
|
sun.shadow.camera.left = -800; |
|
|
sun.shadow.camera.right = 800; |
|
|
sun.shadow.camera.top = 800; |
|
|
sun.shadow.camera.bottom = -800; |
|
|
sun.shadow.camera.near = 0.1; |
|
|
sun.shadow.camera.far = 2000; |
|
|
sun.shadow.mapSize.width = 2048; |
|
|
sun.shadow.mapSize.height = 2048; |
|
|
this.scene.add(sun); |
|
|
|
|
|
|
|
|
this.carHeadlight = new THREE.SpotLight(0xffffff, 1.2, 100, Math.PI / 5, 0.3, 1.5); |
|
|
this.carHeadlight.castShadow = true; |
|
|
this.carHeadlight.shadow.mapSize.width = 512; |
|
|
this.carHeadlight.shadow.mapSize.height = 512; |
|
|
this.scene.add(this.carHeadlight); |
|
|
|
|
|
|
|
|
this.createStreetLights(); |
|
|
} |
|
|
|
|
|
createStreetLights() { |
|
|
|
|
|
let lightCount = 0; |
|
|
const maxLights = 8; |
|
|
|
|
|
this.trackSectors.forEach(sector => { |
|
|
for (let i = 0; i < sector.path.length - 1; i += 4) { |
|
|
if (lightCount >= maxLights) break; |
|
|
|
|
|
const point = sector.path[i]; |
|
|
const streetLight = new THREE.PointLight(0xffaa00, 0.8, 80); |
|
|
streetLight.position.set(point.x, this.getTrackElevation(point.x, point.z, sector.type) + 12, point.z); |
|
|
this.scene.add(streetLight); |
|
|
lightCount++; |
|
|
|
|
|
|
|
|
const poleGeometry = new THREE.CylinderGeometry(0.3, 0.3, 12, 8); |
|
|
const poleMaterial = new THREE.MeshBasicMaterial({ |
|
|
color: 0x333333 |
|
|
}); |
|
|
const pole = new THREE.Mesh(poleGeometry, poleMaterial); |
|
|
pole.position.set(point.x, this.getTrackElevation(point.x, point.z, sector.type) + 6, point.z); |
|
|
this.scene.add(pole); |
|
|
|
|
|
|
|
|
const lampGeometry = new THREE.SphereGeometry(0.8, 8, 6); |
|
|
const lampMaterial = new THREE.MeshBasicMaterial({ |
|
|
color: 0xffaa00, |
|
|
transparent: true, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
const lamp = new THREE.Mesh(lampGeometry, lampMaterial); |
|
|
lamp.position.set(point.x, this.getTrackElevation(point.x, point.z, sector.type) + 12, point.z); |
|
|
this.scene.add(lamp); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
setupMinimap() { |
|
|
const minimapCanvas = document.getElementById('minimap'); |
|
|
this.minimapRenderer = new THREE.WebGLRenderer({ canvas: minimapCanvas }); |
|
|
this.minimapRenderer.setSize(200, 200); |
|
|
this.minimapRenderer.setClearColor(0x001122); |
|
|
|
|
|
this.minimapScene = new THREE.Scene(); |
|
|
this.minimapCamera = new THREE.OrthographicCamera(-800, 800, 800, -800, 1, 2000); |
|
|
this.minimapCamera.position.set(700, 1000, 0); |
|
|
this.minimapCamera.lookAt(700, 0, 0); |
|
|
|
|
|
|
|
|
const minimapTrackMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff }); |
|
|
this.trackSectors.forEach(sector => { |
|
|
for (let i = 0; i < sector.path.length - 1; i++) { |
|
|
const start = sector.path[i]; |
|
|
const end = sector.path[i + 1]; |
|
|
const distance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.z - start.z, 2)); |
|
|
const angle = Math.atan2(end.x - start.x, end.z - start.z); |
|
|
const midX = (start.x + end.x) / 2; |
|
|
const midZ = (start.z + end.z) / 2; |
|
|
|
|
|
const trackGeometry = new THREE.PlaneGeometry(8, distance); |
|
|
const trackSegment = new THREE.Mesh(trackGeometry, minimapTrackMaterial); |
|
|
trackSegment.rotation.x = -Math.PI / 2; |
|
|
trackSegment.rotation.y = angle; |
|
|
trackSegment.position.set(midX, 1, midZ); |
|
|
this.minimapScene.add(trackSegment); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const carDotGeometry = new THREE.SphereGeometry(5, 8, 8); |
|
|
const carDotMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); |
|
|
this.minimapCarDot = new THREE.Mesh(carDotGeometry, carDotMaterial); |
|
|
this.minimapScene.add(this.minimapCarDot); |
|
|
} |
|
|
|
|
|
setupControls() { |
|
|
document.addEventListener('keydown', (event) => { |
|
|
this.keys[event.key.toLowerCase()] = true; |
|
|
if (event.key.toLowerCase() === 'r') { |
|
|
this.resetCar(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.addEventListener('keyup', (event) => { |
|
|
this.keys[event.key.toLowerCase()] = false; |
|
|
}); |
|
|
|
|
|
const canvas = document.getElementById('canvas'); |
|
|
|
|
|
canvas.addEventListener('touchstart', (event) => { |
|
|
event.preventDefault(); |
|
|
this.handleTouch(event, true); |
|
|
}); |
|
|
|
|
|
canvas.addEventListener('touchend', (event) => { |
|
|
event.preventDefault(); |
|
|
this.handleTouch(event, false); |
|
|
}); |
|
|
|
|
|
document.querySelectorAll('.control-btn').forEach(btn => { |
|
|
const action = btn.dataset.action; |
|
|
if (action) { |
|
|
btn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
this.touchActive[action] = true; |
|
|
}); |
|
|
|
|
|
btn.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
this.touchActive[action] = false; |
|
|
}); |
|
|
|
|
|
btn.addEventListener('mousedown', (e) => { |
|
|
e.preventDefault(); |
|
|
this.touchActive[action] = true; |
|
|
}); |
|
|
|
|
|
btn.addEventListener('mouseup', (e) => { |
|
|
e.preventDefault(); |
|
|
this.touchActive[action] = false; |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
|
this.camera.aspect = window.innerWidth / window.innerHeight; |
|
|
this.camera.updateProjectionMatrix(); |
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight); |
|
|
}); |
|
|
} |
|
|
|
|
|
handleTouch(event, isStart) { |
|
|
const rect = event.target.getBoundingClientRect(); |
|
|
const touch = event.touches[0] || event.changedTouches[0]; |
|
|
|
|
|
if (!touch) return; |
|
|
|
|
|
const x = touch.clientX - rect.left; |
|
|
const y = touch.clientY - rect.top; |
|
|
const width = rect.width; |
|
|
const height = rect.height; |
|
|
|
|
|
const topArea = y < height * 0.3; |
|
|
const bottomArea = y > height * 0.7; |
|
|
const leftArea = x < width * 0.3; |
|
|
const rightArea = x > width * 0.7; |
|
|
|
|
|
if (topArea) this.touchActive.forward = isStart; |
|
|
if (bottomArea) this.touchActive.backward = isStart; |
|
|
if (leftArea) this.touchActive.left = isStart; |
|
|
if (rightArea) this.touchActive.right = isStart; |
|
|
} |
|
|
|
|
|
updateCarPhysics() { |
|
|
const forward = this.keys['w'] || this.keys['arrowup'] || this.touchActive.forward; |
|
|
const backward = this.keys['s'] || this.keys['arrowdown'] || this.touchActive.backward; |
|
|
const left = this.keys['a'] || this.keys['arrowleft'] || this.touchActive.left; |
|
|
const right = this.keys['d'] || this.keys['arrowright'] || this.touchActive.right; |
|
|
const brake = this.keys[' '] || this.touchActive.brake; |
|
|
const boost = this.keys['shift'] || this.touchActive.boost; |
|
|
|
|
|
|
|
|
const speedKmh = Math.abs(this.carSpeed * 4); |
|
|
if (speedKmh < 30) this.gear = 1; |
|
|
else if (speedKmh < 70) this.gear = 2; |
|
|
else if (speedKmh < 110) this.gear = 3; |
|
|
else if (speedKmh < 150) this.gear = 4; |
|
|
else if (speedKmh < 190) this.gear = 5; |
|
|
else this.gear = 6; |
|
|
|
|
|
this.rpm = 800 + (speedKmh * 35); |
|
|
|
|
|
|
|
|
if (forward) { |
|
|
this.carSpeed = Math.min(this.carSpeed + this.acceleration, this.maxSpeed * (boost ? 1.3 : 1)); |
|
|
} else if (backward) { |
|
|
this.carSpeed = Math.max(this.carSpeed - this.acceleration, -this.maxSpeed * 0.4); |
|
|
} else { |
|
|
if (this.carSpeed > 0) { |
|
|
this.carSpeed = Math.max(0, this.carSpeed - 0.8); |
|
|
} else { |
|
|
this.carSpeed = Math.min(0, this.carSpeed + 0.8); |
|
|
} |
|
|
} |
|
|
|
|
|
if (brake) { |
|
|
this.carSpeed *= 0.92; |
|
|
} |
|
|
|
|
|
|
|
|
if (Math.abs(this.carSpeed) > 0.5) { |
|
|
|
|
|
const speedRatio = Math.abs(this.carSpeed) / this.maxSpeed; |
|
|
const turnMultiplier = Math.max(0.3, 2.5 - (speedRatio * 2.2)); |
|
|
|
|
|
if (left) { |
|
|
this.carRotation += this.turnSpeed * turnMultiplier; |
|
|
} |
|
|
if (right) { |
|
|
this.carRotation -= this.turnSpeed * turnMultiplier; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.carPosition.x += Math.sin(this.carRotation) * this.carSpeed * 0.1; |
|
|
this.carPosition.z += Math.cos(this.carRotation) * this.carSpeed * 0.1; |
|
|
|
|
|
|
|
|
this.carPosition.y = this.getCurrentTrackElevation() + 2; |
|
|
|
|
|
|
|
|
if (this.car) { |
|
|
this.car.position.copy(this.carPosition); |
|
|
this.car.rotation.y = this.carRotation; |
|
|
|
|
|
|
|
|
this.wheelRotation += this.carSpeed * 0.02; |
|
|
this.wheels.forEach((wheel, index) => { |
|
|
wheel.children[0].rotation.x = this.wheelRotation; |
|
|
wheel.children[1].rotation.x = this.wheelRotation; |
|
|
|
|
|
|
|
|
if (index < 2) { |
|
|
let steeringAngle = 0; |
|
|
if (left) steeringAngle = 0.4; |
|
|
if (right) steeringAngle = -0.4; |
|
|
wheel.rotation.y = steeringAngle; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
let tiltAngle = 0; |
|
|
if (left) tiltAngle = -0.08; |
|
|
if (right) tiltAngle = 0.08; |
|
|
this.car.rotation.z = tiltAngle; |
|
|
} |
|
|
|
|
|
this.updateCarLights(); |
|
|
this.checkLapProgress(); |
|
|
this.updateHUD(); |
|
|
} |
|
|
|
|
|
getCurrentTrackElevation() { |
|
|
|
|
|
for (let sector of this.trackSectors) { |
|
|
for (let i = 0; i < sector.path.length - 1; i++) { |
|
|
const start = sector.path[i]; |
|
|
const end = sector.path[i + 1]; |
|
|
|
|
|
|
|
|
const distToStart = Math.sqrt( |
|
|
Math.pow(this.carPosition.x - start.x, 2) + |
|
|
Math.pow(this.carPosition.z - start.z, 2) |
|
|
); |
|
|
|
|
|
if (distToStart < 50) { |
|
|
return this.getTrackElevation(start.x, start.z, sector.type); |
|
|
} |
|
|
} |
|
|
} |
|
|
return 0; |
|
|
} |
|
|
|
|
|
updateCarLights() { |
|
|
if (this.carHeadlight && this.car) { |
|
|
|
|
|
this.carHeadlight.position.copy(this.carPosition); |
|
|
this.carHeadlight.position.y += 1.5; |
|
|
this.carHeadlight.position.x += Math.sin(this.carRotation) * 2; |
|
|
this.carHeadlight.position.z += Math.cos(this.carRotation) * 2; |
|
|
|
|
|
const target = this.carPosition.clone(); |
|
|
target.x += Math.sin(this.carRotation) * 40; |
|
|
target.z += Math.cos(this.carRotation) * 40; |
|
|
this.carHeadlight.target.position.copy(target); |
|
|
this.carHeadlight.target.updateMatrixWorld(); |
|
|
} |
|
|
} |
|
|
|
|
|
checkLapProgress() { |
|
|
|
|
|
this.checkpoints.forEach((checkpoint, index) => { |
|
|
const distance = this.carPosition.distanceTo(checkpoint.position); |
|
|
if (distance < 25 && index === (this.lastCheckpoint + 1) % this.checkpoints.length) { |
|
|
this.lastCheckpoint = index; |
|
|
this.currentSector = checkpoint.sector; |
|
|
|
|
|
if (index === 0) { |
|
|
this.lapCount++; |
|
|
this.lapTime = 0; |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
updateHUD() { |
|
|
const speedKmh = Math.round(Math.abs(this.carSpeed * 4)); |
|
|
document.getElementById('speedometer').textContent = `${speedKmh} KM/H`; |
|
|
|
|
|
const sectorNames = ["Downtown", "Industrial", "Harbor", "Highway"]; |
|
|
const nextSector = (this.currentSector + 1) % sectorNames.length; |
|
|
|
|
|
|
|
|
const underBuilding = this.isCarUnderBuilding(); |
|
|
const sectorText = underBuilding ? |
|
|
`SECTOR: ${sectorNames[this.currentSector]} (Under Building)` : |
|
|
`SECTOR: ${sectorNames[this.currentSector]}`; |
|
|
|
|
|
document.getElementById('trackInfo').textContent = sectorText; |
|
|
document.getElementById('sectorInfo').textContent = `NEXT: ${sectorNames[nextSector]}`; |
|
|
|
|
|
this.lapTime += 1/60; |
|
|
const minutes = Math.floor(this.lapTime / 60); |
|
|
const seconds = Math.floor(this.lapTime % 60); |
|
|
document.getElementById('lapInfo').textContent = |
|
|
`LAP: ${this.lapCount}/3 | TIME: ${minutes}:${seconds.toString().padStart(2, '0')}`; |
|
|
} |
|
|
|
|
|
updateCamera() { |
|
|
if (this.car) { |
|
|
const speedFactor = Math.min(Math.abs(this.carSpeed) / this.maxSpeed, 1); |
|
|
const cameraDistance = 25 + speedFactor * 20; |
|
|
let cameraHeight = 12 + speedFactor * 8; |
|
|
|
|
|
|
|
|
const isUnderBuilding = this.isCarUnderBuilding(); |
|
|
if (isUnderBuilding) { |
|
|
cameraHeight = Math.min(cameraHeight, 8); |
|
|
} |
|
|
|
|
|
const targetPosition = this.carPosition.clone(); |
|
|
targetPosition.y += cameraHeight; |
|
|
targetPosition.x -= Math.sin(this.carRotation) * cameraDistance; |
|
|
targetPosition.z -= Math.cos(this.carRotation) * cameraDistance; |
|
|
|
|
|
|
|
|
this.camera.position.lerp(targetPosition, 0.08); |
|
|
|
|
|
const lookTarget = this.carPosition.clone(); |
|
|
lookTarget.x += Math.sin(this.carRotation) * 10; |
|
|
lookTarget.z += Math.cos(this.carRotation) * 10; |
|
|
lookTarget.y += 2; |
|
|
|
|
|
this.camera.lookAt(lookTarget); |
|
|
|
|
|
|
|
|
if (speedFactor > 0.7 && !isUnderBuilding) { |
|
|
const shake = (speedFactor - 0.7) * 0.8; |
|
|
this.camera.position.x += (Math.random() - 0.5) * shake; |
|
|
this.camera.position.y += (Math.random() - 0.5) * shake * 0.5; |
|
|
this.camera.position.z += (Math.random() - 0.5) * shake; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
isCarUnderBuilding() { |
|
|
|
|
|
const carX = this.carPosition.x; |
|
|
const carZ = this.carPosition.z; |
|
|
const carY = this.carPosition.y; |
|
|
|
|
|
for (let building of this.buildings) { |
|
|
|
|
|
if (carX > building.x - building.width/2 && carX < building.x + building.width/2 && |
|
|
carZ > building.z - building.depth/2 && carZ < building.z + building.depth/2 && |
|
|
carY < building.supportHeight + 5) { |
|
|
return true; |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
updateMinimap() { |
|
|
if (this.minimapCarDot) { |
|
|
this.minimapCarDot.position.copy(this.carPosition); |
|
|
this.minimapCarDot.position.y = 5; |
|
|
} |
|
|
|
|
|
this.minimapRenderer.render(this.minimapScene, this.minimapCamera); |
|
|
} |
|
|
|
|
|
resetCar() { |
|
|
this.carPosition.set(0, 2, 0); |
|
|
this.carRotation = 0; |
|
|
this.carSpeed = 0; |
|
|
this.gear = 1; |
|
|
this.rpm = 800; |
|
|
this.currentSector = 0; |
|
|
this.lastCheckpoint = -1; |
|
|
} |
|
|
|
|
|
animate() { |
|
|
requestAnimationFrame(() => this.animate()); |
|
|
|
|
|
this.updateCarPhysics(); |
|
|
this.updateCamera(); |
|
|
this.updateMinimap(); |
|
|
|
|
|
this.renderer.render(this.scene, this.camera); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
new CityScaleRacingCircuit(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |