drive / index.html
kimhyunwoo's picture
Update index.html
cb562cc verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tesla 3D 무한맵 운전 게임</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #000;
}
#gameContainer {
position: relative;
width: 100vw;
height: 100vh;
}
#minimap {
position: absolute;
top: 20px;
right: 20px;
width: 200px;
height: 200px;
background: rgba(0, 0, 0, 0.8);
border: 2px solid #00ff00;
border-radius: 15px;
z-index: 100;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
}
#speedometer {
position: absolute;
bottom: 30px;
right: 30px;
font-size: 20px;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
z-index: 100;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.8));
padding: 20px;
border-radius: 15px;
min-width: 200px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
#speedometer .speed-display {
font-size: 48px;
font-weight: 300;
color: #00ff88;
text-align: center;
margin: 10px 0;
}
#controls {
position: absolute;
top: 20px;
left: 20px;
color: white;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.8));
padding: 20px;
border-radius: 15px;
z-index: 100;
font-size: 14px;
line-height: 1.8;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
#score {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 24px;
font-weight: 300;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
z-index: 100;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.8));
padding: 15px 30px;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.gear-indicator {
text-align: center;
font-size: 32px;
margin-top: 10px;
font-weight: bold;
}
.gear-p { color: #ff4444; }
.gear-r { color: #ffaa44; }
.gear-n { color: #ffffff; }
.gear-d { color: #00ff88; }
canvas {
display: block;
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="controls">
<div><strong>🎮 Tesla 운전 시뮬레이터</strong></div>
<div>W/↑ - 가속</div>
<div>S/↓ - 브레이크/후진</div>
<div>A/← - 좌회전</div>
<div>D/→ - 우회전</div>
<div>Space - 핸드브레이크</div>
<div>C - 카메라 변경</div>
<div>L - 헤드라이트</div>
</div>
<div id="score">주행 거리: 0 km</div>
<canvas id="minimap"></canvas>
<div id="speedometer">
<div style="text-align: center; color: #888; font-size: 12px;">TESLA</div>
<div class="speed-display"><span id="speed">0</span></div>
<div style="text-align: center; color: #888; font-size: 14px;">km/h</div>
<div class="gear-indicator gear-n" id="gear">P</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Game variables
let scene, camera, renderer;
let car, carSpeed = 0, carRotation = 0;
let keys = {};
let buildings = [];
let roads = [];
let props = [];
let trees = [];
let distance = 0;
let cameraMode = 0;
let minimapCanvas, minimapCtx;
let clock = new THREE.Clock();
let headlightsOn = false;
let leftHeadlight, rightHeadlight;
let wheels = [];
const MAX_SPEED = 3;
const ACCELERATION = 0.08;
const DECELERATION = 0.04;
const TURN_SPEED = 0.04;
const BRAKE_DECELERATION = 0.12;
const WORLD_SIZE = 1000;
const CHUNK_SIZE = 150;
// Initialize game
function init() {
// Scene setup
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x8EAEC0, 200, 800);
// Camera setup
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
2000
);
// Renderer setup
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.8;
document.getElementById('gameContainer').appendChild(renderer.domElement);
// Sky
createSky();
// Lighting
setupLighting();
// Create Tesla car
createTeslaCar();
// Create initial world
createWorld();
// Setup minimap
setupMinimap();
// Event listeners
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('resize', onWindowResize);
// Start game loop
animate();
}
function createSky() {
// Create gradient sky
const skyGeometry = new THREE.SphereGeometry(1500, 32, 15);
const skyMaterial = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0077be) },
bottomColor: { value: new THREE.Color(0xffffff) },
offset: { value: 33 },
exponent: { value: 0.6 }
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`,
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(sky);
// Add clouds
const cloudGeometry = new THREE.PlaneGeometry(100, 100);
const cloudMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.4,
side: THREE.DoubleSide
});
for(let i = 0; i < 20; i++) {
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
cloud.position.set(
(Math.random() - 0.5) * 1000,
200 + Math.random() * 200,
(Math.random() - 0.5) * 1000
);
cloud.rotation.x = Math.PI / 2;
cloud.scale.setScalar(Math.random() * 2 + 1);
scene.add(cloud);
}
}
function setupLighting() {
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
// Sun light
const sunLight = new THREE.DirectionalLight(0xffd4a3, 1);
sunLight.position.set(100, 200, 100);
sunLight.castShadow = true;
sunLight.shadow.camera.left = -150;
sunLight.shadow.camera.right = 150;
sunLight.shadow.camera.top = 150;
sunLight.shadow.camera.bottom = -150;
sunLight.shadow.camera.near = 0.1;
sunLight.shadow.camera.far = 500;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
scene.add(sunLight);
// Hemisphere light for better ambient
const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x8B7355, 0.3);
scene.add(hemiLight);
}
function createTeslaCar() {
car = new THREE.Group();
// Tesla-like car body
const bodyGroup = new THREE.Group();
// Main body
const bodyGeometry = new THREE.BoxGeometry(2.2, 0.8, 4.8);
const bodyMaterial = new THREE.MeshPhongMaterial({
color: 0x1a1a1a,
metalness: 0.8,
roughness: 0.2
});
const carBody = new THREE.Mesh(bodyGeometry, bodyMaterial);
carBody.position.y = 0.6;
carBody.castShadow = true;
bodyGroup.add(carBody);
// Sleek roof (Tesla-like curve)
const roofGeometry = new THREE.BoxGeometry(2, 0.6, 3);
const roofMaterial = new THREE.MeshPhongMaterial({
color: 0x0a0a0a,
metalness: 0.9,
roughness: 0.1
});
const carRoof = new THREE.Mesh(roofGeometry, roofMaterial);
carRoof.position.y = 1.2;
carRoof.position.z = -0.3;
// Round the edges
carRoof.scale.set(0.95, 1, 1);
bodyGroup.add(carRoof);
// Glass windows
const glassGeometry = new THREE.BoxGeometry(1.9, 0.5, 2.8);
const glassMaterial = new THREE.MeshPhongMaterial({
color: 0x222244,
transparent: true,
opacity: 0.3,
metalness: 0.9,
roughness: 0.1
});
const glass = new THREE.Mesh(glassGeometry, glassMaterial);
glass.position.y = 1.2;
glass.position.z = -0.3;
bodyGroup.add(glass);
// Front hood
const hoodGeometry = new THREE.BoxGeometry(2.1, 0.2, 1.5);
const hood = new THREE.Mesh(hoodGeometry, bodyMaterial);
hood.position.y = 0.8;
hood.position.z = 2.2;
hood.rotation.x = -0.1;
bodyGroup.add(hood);
// Wheels - Tesla style
const wheelGeometry = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 32);
const wheelMaterial = new THREE.MeshPhongMaterial({
color: 0x1a1a1a,
metalness: 0.8,
roughness: 0.3
});
// Wheel rims
const rimGeometry = new THREE.CylinderGeometry(0.32, 0.32, 0.26, 8);
const rimMaterial = new THREE.MeshPhongMaterial({
color: 0x888888,
metalness: 0.9,
roughness: 0.2
});
const wheelPositions = [
{ x: -0.95, y: 0.1, z: 1.6 },
{ x: 0.95, y: 0.1, z: 1.6 },
{ x: -0.95, y: 0.1, z: -1.6 },
{ x: 0.95, y: 0.1, z: -1.6 }
];
wheelPositions.forEach(pos => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.rotation.z = Math.PI / 2;
wheel.position.set(pos.x, pos.y, pos.z);
wheel.castShadow = true;
bodyGroup.add(wheel);
wheels.push(wheel);
const rim = new THREE.Mesh(rimGeometry, rimMaterial);
rim.rotation.z = Math.PI / 2;
rim.position.set(pos.x * 1.01, pos.y, pos.z);
bodyGroup.add(rim);
});
// Headlights - Tesla style LED
const headlightGeometry = new THREE.BoxGeometry(0.4, 0.2, 0.1);
const headlightMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 0.2
});
const headlight1 = new THREE.Mesh(headlightGeometry, headlightMaterial);
headlight1.position.set(-0.7, 0.6, 2.4);
bodyGroup.add(headlight1);
const headlight2 = new THREE.Mesh(headlightGeometry, headlightMaterial);
headlight2.position.set(0.7, 0.6, 2.4);
bodyGroup.add(headlight2);
// LED tail lights
const taillightGeometry = new THREE.BoxGeometry(0.5, 0.15, 0.1);
const taillightMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 0.3
});
const taillight1 = new THREE.Mesh(taillightGeometry, taillightMaterial);
taillight1.position.set(-0.8, 0.7, -2.4);
bodyGroup.add(taillight1);
const taillight2 = new THREE.Mesh(taillightGeometry, taillightMaterial);
taillight2.position.set(0.8, 0.7, -2.4);
bodyGroup.add(taillight2);
// Door handles (flush like Tesla)
const handleGeometry = new THREE.BoxGeometry(0.02, 0.1, 0.4);
const handleMaterial = new THREE.MeshPhongMaterial({
color: 0x444444,
metalness: 0.9
});
[-1.1, 1.1].forEach(x => {
[-0.5, 0.8].forEach(z => {
const handle = new THREE.Mesh(handleGeometry, handleMaterial);
handle.position.set(x, 0.8, z);
bodyGroup.add(handle);
});
});
// Tesla logo/emblem
const logoGeometry = new THREE.PlaneGeometry(0.3, 0.3);
const logoMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 0.2
});
const logo = new THREE.Mesh(logoGeometry, logoMaterial);
logo.position.set(0, 0.8, 2.41);
bodyGroup.add(logo);
car.add(bodyGroup);
// Add headlight spotlights
leftHeadlight = new THREE.SpotLight(0xffffff, 0, 50, Math.PI / 4, 0.5, 2);
leftHeadlight.position.set(-0.7, 0.6, 2.4);
leftHeadlight.target.position.set(-0.7, 0, 10);
car.add(leftHeadlight);
car.add(leftHeadlight.target);
rightHeadlight = new THREE.SpotLight(0xffffff, 0, 50, Math.PI / 4, 0.5, 2);
rightHeadlight.position.set(0.7, 0.6, 2.4);
rightHeadlight.target.position.set(0.7, 0, 10);
car.add(rightHeadlight);
car.add(rightHeadlight.target);
car.position.y = 0.3;
scene.add(car);
}
function createWorld() {
// Better ground texture
const groundGeometry = new THREE.PlaneGeometry(WORLD_SIZE * 4, WORLD_SIZE * 4);
const groundMaterial = new THREE.MeshLambertMaterial({
color: 0x4a5d3a
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
scene.add(ground);
// Create initial city blocks
for (let x = -WORLD_SIZE/2; x < WORLD_SIZE/2; x += CHUNK_SIZE) {
for (let z = -WORLD_SIZE/2; z < WORLD_SIZE/2; z += CHUNK_SIZE) {
createCityBlock(x, z);
}
}
}
function createCityBlock(centerX, centerZ) {
// Asphalt roads with better textures
const roadWidth = 12;
const roadMaterial = new THREE.MeshLambertMaterial({
color: 0x2a2a2a
});
// Main roads
const hRoadGeometry = new THREE.PlaneGeometry(CHUNK_SIZE, roadWidth);
const hRoad = new THREE.Mesh(hRoadGeometry, roadMaterial);
hRoad.rotation.x = -Math.PI / 2;
hRoad.position.set(centerX, 0.01, centerZ);
hRoad.receiveShadow = true;
scene.add(hRoad);
roads.push(hRoad);
const vRoadGeometry = new THREE.PlaneGeometry(roadWidth, CHUNK_SIZE);
const vRoad = new THREE.Mesh(vRoadGeometry, roadMaterial);
vRoad.rotation.x = -Math.PI / 2;
vRoad.position.set(centerX, 0.01, centerZ);
vRoad.receiveShadow = true;
scene.add(vRoad);
roads.push(vRoad);
// Road lines
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
for(let i = -CHUNK_SIZE/2; i < CHUNK_SIZE/2; i += 10) {
const lineGeometry = new THREE.PlaneGeometry(0.2, 4);
const line = new THREE.Mesh(lineGeometry, lineMaterial);
line.rotation.x = -Math.PI / 2;
line.position.set(centerX, 0.02, centerZ + i);
scene.add(line);
}
// Sidewalks
const sidewalkMaterial = new THREE.MeshLambertMaterial({ color: 0x808080 });
const sidewalkGeometry = new THREE.BoxGeometry(CHUNK_SIZE, 0.2, 2);
[-7, 7].forEach(offset => {
const sidewalk = new THREE.Mesh(sidewalkGeometry, sidewalkMaterial);
sidewalk.position.set(centerX, 0.1, centerZ + offset);
scene.add(sidewalk);
});
// Buildings in quadrants
const quadrants = [
{ x: centerX - CHUNK_SIZE/3, z: centerZ - CHUNK_SIZE/3 },
{ x: centerX + CHUNK_SIZE/3, z: centerZ - CHUNK_SIZE/3 },
{ x: centerX - CHUNK_SIZE/3, z: centerZ + CHUNK_SIZE/3 },
{ x: centerX + CHUNK_SIZE/3, z: centerZ + CHUNK_SIZE/3 }
];
quadrants.forEach(quad => {
const rand = Math.random();
if (rand > 0.3) {
createModernBuildings(quad.x, quad.z, CHUNK_SIZE/4);
} else {
createPark(quad.x, quad.z, CHUNK_SIZE/4);
}
});
// Street furniture
createStreetFurniture(centerX, centerZ);
}
function createModernBuildings(centerX, centerZ, size) {
const buildingCount = Math.floor(Math.random() * 3) + 2;
for (let i = 0; i < buildingCount; i++) {
const width = Math.random() * 20 + 15;
const depth = Math.random() * 20 + 15;
const height = Math.random() * 60 + 30;
const floors = Math.floor(height / 3);
const buildingGroup = new THREE.Group();
// Building base
const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
const buildingColor = new THREE.Color(
0.3 + Math.random() * 0.3,
0.3 + Math.random() * 0.3,
0.3 + Math.random() * 0.4
);
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColor,
metalness: 0.5,
roughness: 0.5
});
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
building.position.y = height / 2;
building.castShadow = true;
building.receiveShadow = true;
buildingGroup.add(building);
// Windows
for(let floor = 0; floor < floors; floor++) {
for(let side = 0; side < 4; side++) {
const windowCount = Math.floor(width / 3);
for(let w = 0; w < windowCount; w++) {
const windowGeometry = new THREE.BoxGeometry(1.5, 2, 0.1);
const windowMaterial = new THREE.MeshPhongMaterial({
color: 0x87CEEB,
emissive: 0x444466,
emissiveIntensity: 0.3,
metalness: 0.9,
roughness: 0.1
});
const window = new THREE.Mesh(windowGeometry, windowMaterial);
const angle = (side * Math.PI) / 2;
const radius = (side % 2 === 0) ? depth/2 : width/2;
window.position.set(
Math.sin(angle) * radius * 1.01,
floor * 3 + 2,
Math.cos(angle) * radius * 1.01
);
window.rotation.y = angle;
if(side < 2) {
window.position.x += (w - windowCount/2) * 2.5;
} else {
window.position.z += (w - windowCount/2) * 2.5;
}
buildingGroup.add(window);
}
}
}
buildingGroup.position.set(
centerX + (Math.random() - 0.5) * size,
0,
centerZ + (Math.random() - 0.5) * size
);
scene.add(buildingGroup);
buildings.push(buildingGroup);
}
}
function createPark(centerX, centerZ, size) {
// Grass ground
const parkGeometry = new THREE.PlaneGeometry(size, size);
const parkMaterial = new THREE.MeshLambertMaterial({
color: 0x3a7d3a
});
const park = new THREE.Mesh(parkGeometry, parkMaterial);
park.rotation.x = -Math.PI / 2;
park.position.set(centerX, 0.02, centerZ);
park.receiveShadow = true;
scene.add(park);
// Walking paths
const pathMaterial = new THREE.MeshLambertMaterial({ color: 0x8B7355 });
const pathGeometry = new THREE.PlaneGeometry(2, size);
const path1 = new THREE.Mesh(pathGeometry, pathMaterial);
path1.rotation.x = -Math.PI / 2;
path1.position.set(centerX, 0.03, centerZ);
scene.add(path1);
const path2 = new THREE.Mesh(new THREE.PlaneGeometry(size, 2), pathMaterial);
path2.rotation.x = -Math.PI / 2;
path2.position.set(centerX, 0.03, centerZ);
scene.add(path2);
// Trees
const treeCount = Math.floor(Math.random() * 8) + 5;
for (let i = 0; i < treeCount; i++) {
const x = centerX + (Math.random() - 0.5) * size * 0.8;
const z = centerZ + (Math.random() - 0.5) * size * 0.8;
// Avoid paths
if(Math.abs(x - centerX) > 2 && Math.abs(z - centerZ) > 2) {
createRealisticTree(x, z);
}
}
// Park benches
for(let i = 0; i < 3; i++) {
createBench(
centerX + (Math.random() - 0.5) * size * 0.6,
centerZ + (Math.random() - 0.5) * size * 0.6
);
}
}
function createRealisticTree(x, z) {
const tree = new THREE.Group();
// Trunk
const trunkGeometry = new THREE.CylinderGeometry(0.8, 1, 6, 8);
const trunkMaterial = new THREE.MeshPhongMaterial({
color: 0x4a3c28,
roughness: 0.8
});
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
trunk.position.y = 3;
trunk.castShadow = true;
tree.add(trunk);
// Foliage layers
const foliageColors = [0x2d5a2d, 0x3a6b3a, 0x4a7c4a];
const foliageSizes = [5, 4, 3];
const foliageHeights = [6, 8, 10];
foliageColors.forEach((color, i) => {
const foliageGeometry = new THREE.SphereGeometry(foliageSizes[i], 8, 6);
const foliageMaterial = new THREE.MeshPhongMaterial({
color: color,
roughness: 0.8
});
const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial);
foliage.position.y = foliageHeights[i];
foliage.castShadow = true;
tree.add(foliage);
});
tree.position.set(x, 0, z);
scene.add(tree);
trees.push(tree);
}
function createStreetFurniture(centerX, centerZ) {
// Modern street lights
const positions = [
{ x: centerX - 20, z: centerZ - 20 },
{ x: centerX + 20, z: centerZ - 20 },
{ x: centerX - 20, z: centerZ + 20 },
{ x: centerX + 20, z: centerZ + 20 }
];
positions.forEach(pos => {
const pole = new THREE.Group();
// Pole
const poleGeometry = new THREE.CylinderGeometry(0.15, 0.2, 10);
const poleMaterial = new THREE.MeshPhongMaterial({
color: 0x333333,
metalness: 0.8,
roughness: 0.3
});
const poleMesh = new THREE.Mesh(poleGeometry, poleMaterial);
poleMesh.position.y = 5;
poleMesh.castShadow = true;
pole.add(poleMesh);
// LED Light fixture
const lightGeometry = new THREE.BoxGeometry(2, 0.3, 0.8);
const lightMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
emissive: 0xffffaa,
emissiveIntensity: 0.5
});
const lightMesh = new THREE.Mesh(lightGeometry, lightMaterial);
lightMesh.position.y = 10;
pole.add(lightMesh);
// Add actual light
const streetLight = new THREE.PointLight(0xffffaa, 0.5, 30);
streetLight.position.y = 9.5;
pole.add(streetLight);
pole.position.set(pos.x, 0, pos.z);
scene.add(pole);
props.push(pole);
});
// Traffic lights
if(Math.random() > 0.5) {
createTrafficLight(centerX + 8, centerZ + 8);
}
// Fire hydrants
if(Math.random() > 0.6) {
createFireHydrant(centerX + 15, centerZ - 15);
}
}
function createTrafficLight(x, z) {
const trafficLight = new THREE.Group();
// Pole
const poleGeometry = new THREE.CylinderGeometry(0.1, 0.1, 8);
const poleMaterial = new THREE.MeshPhongMaterial({ color: 0x333333 });
const pole = new THREE.Mesh(poleGeometry, poleMaterial);
pole.position.y = 4;
trafficLight.add(pole);
// Light box
const boxGeometry = new THREE.BoxGeometry(0.8, 2.4, 0.8);
const boxMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a });
const box = new THREE.Mesh(boxGeometry, boxMaterial);
box.position.y = 8;
trafficLight.add(box);
// Lights
const colors = [0xff0000, 0xffff00, 0x00ff00];
colors.forEach((color, i) => {
const lightGeometry = new THREE.SphereGeometry(0.3);
const lightMaterial = new THREE.MeshPhongMaterial({
color: color,
emissive: color,
emissiveIntensity: i === 2 ? 0.8 : 0.1
});
const light = new THREE.Mesh(lightGeometry, lightMaterial);
light.position.y = 8.8 - i * 0.8;
light.position.z = 0.41;
trafficLight.add(light);
});
trafficLight.position.set(x, 0, z);
scene.add(trafficLight);
props.push(trafficLight);
}
function createFireHydrant(x, z) {
const hydrant = new THREE.Group();
const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.35, 1.5);
const bodyMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
metalness: 0.5,
roughness: 0.4
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
hydrant.add(body);
const topGeometry = new THREE.SphereGeometry(0.35, 8, 4);
const top = new THREE.Mesh(topGeometry, bodyMaterial);
top.position.y = 1.5;
hydrant.add(top);
hydrant.position.set(x, 0, z);
hydrant.castShadow = true;
scene.add(hydrant);
props.push(hydrant);
}
function createBench(x, z) {
const bench = new THREE.Group();
// Seat
const seatGeometry = new THREE.BoxGeometry(4, 0.2, 1.5);
const woodMaterial = new THREE.MeshPhongMaterial({
color: 0x8B4513,
roughness: 0.8
});
const seat = new THREE.Mesh(seatGeometry, woodMaterial);
seat.position.y = 0.6;
bench.add(seat);
// Back
const backGeometry = new THREE.BoxGeometry(4, 1.2, 0.2);
const back = new THREE.Mesh(backGeometry, woodMaterial);
back.position.y = 1.2;
back.position.z = -0.6;
back.rotation.x = -0.1;
bench.add(back);
// Metal legs
const legMaterial = new THREE.MeshPhongMaterial({
color: 0x444444,
metalness: 0.8
});
const legGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.6);
[-1.8, 1.8].forEach(xOffset => {
[-0.5, 0.5].forEach(zOffset => {
const leg = new THREE.Mesh(legGeometry, legMaterial);
leg.position.set(xOffset, 0.3, zOffset);
bench.add(leg);
});
});
bench.position.set(x, 0, z);
bench.rotation.y = Math.random() * Math.PI;
bench.castShadow = true;
scene.add(bench);
props.push(bench);
}
function setupMinimap() {
minimapCanvas = document.getElementById('minimap');
minimapCanvas.width = 200;
minimapCanvas.height = 200;
minimapCtx = minimapCanvas.getContext('2d');
}
function updateMinimap() {
// Clear
minimapCtx.fillStyle = 'rgba(0, 0, 0, 0.9)';
minimapCtx.fillRect(0, 0, 200, 200);
// Draw grid
minimapCtx.strokeStyle = '#2a2a2a';
minimapCtx.lineWidth = 1;
const scale = 0.4;
const offsetX = 100 - car.position.x * scale;
const offsetZ = 100 - car.position.z * scale;
// Grid lines
for (let x = -WORLD_SIZE; x <= WORLD_SIZE; x += CHUNK_SIZE) {
minimapCtx.beginPath();
minimapCtx.moveTo(x * scale + offsetX, 0);
minimapCtx.lineTo(x * scale + offsetX, 200);
minimapCtx.stroke();
}
for (let z = -WORLD_SIZE; z <= WORLD_SIZE; z += CHUNK_SIZE) {
minimapCtx.beginPath();
minimapCtx.moveTo(0, z * scale + offsetZ);
minimapCtx.lineTo(200, z * scale + offsetZ);
minimapCtx.stroke();
}
// Draw buildings
buildings.forEach(building => {
const bx = building.position.x * scale + offsetX;
const bz = building.position.z * scale + offsetZ;
if (bx > -20 && bx < 220 && bz > -20 && bz < 220) {
minimapCtx.fillStyle = '#444';
minimapCtx.fillRect(bx - 4, bz - 4, 8, 8);
}
});
// Draw trees
trees.forEach(tree => {
const tx = tree.position.x * scale + offsetX;
const tz = tree.position.z * scale + offsetZ;
if (tx > -10 && tx < 210 && tz > -10 && tz < 210) {
minimapCtx.fillStyle = '#2a5a2a';
minimapCtx.beginPath();
minimapCtx.arc(tx, tz, 2, 0, Math.PI * 2);
minimapCtx.fill();
}
});
// Draw car
minimapCtx.save();
minimapCtx.translate(100, 100);
minimapCtx.rotate(-car.rotation.y);
// Car body
minimapCtx.fillStyle = '#00ff88';
minimapCtx.fillRect(-4, -7, 8, 14);
// Direction indicator
minimapCtx.strokeStyle = '#00ffff';
minimapCtx.lineWidth = 2;
minimapCtx.beginPath();
minimapCtx.moveTo(0, 0);
minimapCtx.lineTo(0, -15);
minimapCtx.stroke();
minimapCtx.restore();
}
function handleKeyDown(e) {
keys[e.key.toLowerCase()] = true;
if (e.key.toLowerCase() === 'l') {
headlightsOn = !headlightsOn;
leftHeadlight.intensity = headlightsOn ? 2 : 0;
rightHeadlight.intensity = headlightsOn ? 2 : 0;
}
}
function handleKeyUp(e) {
keys[e.key.toLowerCase()] = false;
}
function updateCar() {
const delta = clock.getDelta();
// Handle input
if (keys['w'] || keys['arrowup']) {
carSpeed = Math.min(carSpeed + ACCELERATION, MAX_SPEED);
} else if (keys['s'] || keys['arrowdown']) {
if (carSpeed > 0) {
carSpeed = Math.max(carSpeed - BRAKE_DECELERATION, 0);
} else {
carSpeed = Math.max(carSpeed - ACCELERATION, -MAX_SPEED / 2);
}
} else {
if (carSpeed > 0) {
carSpeed = Math.max(carSpeed - DECELERATION, 0);
} else if (carSpeed < 0) {
carSpeed = Math.min(carSpeed + DECELERATION, 0);
}
}
// Turning
if (Math.abs(carSpeed) > 0.01) {
let turnAmount = TURN_SPEED;
if(Math.abs(carSpeed) < 0.5) turnAmount *= Math.abs(carSpeed) * 2;
if (keys['a'] || keys['arrowleft']) {
carRotation += turnAmount * (carSpeed > 0 ? 1 : -1);
}
if (keys['d'] || keys['arrowright']) {
carRotation -= turnAmount * (carSpeed > 0 ? 1 : -1);
}
}
// Handbrake
if (keys[' '] && Math.abs(carSpeed) > 0.5) {
carSpeed *= 0.92;
if (keys['a'] || keys['arrowleft']) {
carRotation += TURN_SPEED * 2;
}
if (keys['d'] || keys['arrowright']) {
carRotation -= TURN_SPEED * 2;
}
}
// Update position
car.rotation.y = carRotation;
car.position.x += Math.sin(carRotation) * carSpeed;
car.position.z += Math.cos(carRotation) * carSpeed;
// Rotate wheels
wheels.forEach(wheel => {
wheel.rotation.x += carSpeed * 0.5;
});
// Update camera
updateCamera();
// Generate world
checkWorldGeneration();
// Update UI
updateUI();
// Update distance
distance += Math.abs(carSpeed) * 0.05;
document.getElementById('score').textContent = `주행 거리: ${distance.toFixed(1)} km`;
}
function updateCamera() {
if (keys['c']) {
keys['c'] = false;
cameraMode = (cameraMode + 1) % 3;
}
const smoothness = 0.1;
let targetX, targetY, targetZ;
let lookX, lookY, lookZ;
switch(cameraMode) {
case 0: // Third person
targetX = car.position.x - Math.sin(carRotation) * 20;
targetY = car.position.y + 10;
targetZ = car.position.z - Math.cos(carRotation) * 20;
lookX = car.position.x;
lookY = car.position.y + 2;
lookZ = car.position.z;
break;
case 1: // Hood cam
targetX = car.position.x + Math.sin(carRotation) * 2;
targetY = car.position.y + 3;
targetZ = car.position.z + Math.cos(carRotation) * 2;
lookX = car.position.x + Math.sin(carRotation) * 20;
lookY = car.position.y + 2;
lookZ = car.position.z + Math.cos(carRotation) * 20;
break;
case 2: // Drone view
targetX = car.position.x;
targetY = car.position.y + 50;
targetZ = car.position.z - 10;
lookX = car.position.x;
lookY = car.position.y;
lookZ = car.position.z;
break;
}
camera.position.x += (targetX - camera.position.x) * smoothness;
camera.position.y += (targetY - camera.position.y) * smoothness;
camera.position.z += (targetZ - camera.position.z) * smoothness;
camera.lookAt(lookX, lookY, lookZ);
}
function checkWorldGeneration() {
const carChunkX = Math.floor(car.position.x / CHUNK_SIZE) * CHUNK_SIZE;
const carChunkZ = Math.floor(car.position.z / CHUNK_SIZE) * CHUNK_SIZE;
// Generate new chunks
for (let x = carChunkX - CHUNK_SIZE * 3; x <= carChunkX + CHUNK_SIZE * 3; x += CHUNK_SIZE) {
for (let z = carChunkZ - CHUNK_SIZE * 3; z <= carChunkZ + CHUNK_SIZE * 3; z += CHUNK_SIZE) {
const chunkKey = `${x}_${z}`;
if (!window.generatedChunks) window.generatedChunks = new Set();
if (!window.generatedChunks.has(chunkKey)) {
createCityBlock(x, z);
window.generatedChunks.add(chunkKey);
}
}
}
// Remove far objects
const maxDistance = CHUNK_SIZE * 4;
[...buildings, ...props, ...roads, ...trees].forEach(obj => {
if (obj.position) {
const distance = obj.position.distanceTo(car.position);
if (distance > maxDistance) {
scene.remove(obj);
buildings = buildings.filter(b => b !== obj);
props = props.filter(p => p !== obj);
roads = roads.filter(r => r !== obj);
trees = trees.filter(t => t !== obj);
}
}
});
}
function updateUI() {
const speedKmh = Math.floor(Math.abs(carSpeed) * 60);
document.getElementById('speed').textContent = speedKmh;
const gearElement = document.getElementById('gear');
let gear = 'P';
let gearClass = 'gear-p';
if (Math.abs(carSpeed) < 0.01) {
gear = 'P';
gearClass = 'gear-p';
} else if (carSpeed > 0.01) {
gear = 'D';
gearClass = 'gear-d';
} else if (carSpeed < -0.01) {
gear = 'R';
gearClass = 'gear-r';
}
gearElement.textContent = gear;
gearElement.className = `gear-indicator ${gearClass}`;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
updateCar();
updateMinimap();
renderer.render(scene, camera);
}
// Start the game
init();
</script>
</body>
</html>