Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>3D City Scene</title> | |
<style> | |
body { | |
margin: 0; | |
overflow: hidden; | |
font-family: Arial, sans-serif; | |
} | |
canvas { | |
display: block; | |
} | |
.ui-container { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
color: white; | |
background-color: rgba(0, 0, 0, 0.5); | |
padding: 10px; | |
border-radius: 5px; | |
user-select: none; | |
} | |
.controls { | |
position: absolute; | |
bottom: 10px; | |
left: 10px; | |
color: white; | |
background-color: rgba(0, 0, 0, 0.5); | |
padding: 10px; | |
border-radius: 5px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="ui-container"> | |
<h2>3D City Explorer</h2> | |
<div id="score">Score: 0</div> | |
<div id="time">Time: 60</div> | |
</div> | |
<div class="controls"> | |
<p>Controls: W to move backward, S to move forward, A/D to move left/right, Mouse to look, Space to jump</p> | |
<p>Collect the floating cubes to score points!</p> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
// Polyfill for tqdm since it's not defined | |
const tqdm = { | |
tqdm: function(desc, total) { | |
return { | |
update: function() {}, | |
// Other methods as needed | |
}; | |
} | |
}; | |
</script> | |
<script> | |
// Game variables | |
let score = 0; | |
let timeRemaining = 60; | |
let gameActive = true; | |
// Set up scene | |
const scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87CEEB); // Sky blue background | |
// Set up camera | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 5, 15); | |
// Set up renderer | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
document.body.appendChild(renderer.domElement); | |
// Add lights | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(50, 50, 50); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
directionalLight.shadow.camera.near = 1; | |
directionalLight.shadow.camera.far = 500; | |
directionalLight.shadow.camera.left = -100; | |
directionalLight.shadow.camera.right = 100; | |
directionalLight.shadow.camera.top = 100; | |
directionalLight.shadow.camera.bottom = -100; | |
scene.add(directionalLight); | |
// Create ground | |
const groundGeometry = new THREE.PlaneGeometry(200, 200); | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x1a5e1a, // Green | |
roughness: 0.8, | |
metalness: 0.2 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
// Buildings array | |
const buildings = []; | |
const collectibles = []; | |
// Building color palette | |
const buildingColors = [ | |
0x888888, 0x666666, 0x999999, 0xaaaaaa, 0x555555, | |
0x334455, 0x445566, 0x223344, 0x556677, 0x667788, | |
0x993333, 0x884422, 0x553333, 0x772222, 0x664433 | |
]; | |
// L-system grammar rules for buildings | |
const buildingRules = [ | |
// Colonial style - symmetrical with central features | |
{ | |
name: "Colonial", | |
axiom: "A", | |
rules: { | |
"A": "B[+F][-F]", | |
"B": "F[-C][+C]F", | |
"C": "D[-E][+E]", | |
"D": "F[+F][-F]F", | |
"E": "F[-F][+F]" | |
}, | |
iterations: 2, | |
baseHeight: 10, | |
baseWidth: 6, | |
baseDepth: 6, | |
angle: Math.PI/6, | |
probability: 0.2 | |
}, | |
// Victorian style - complex with many decorative elements | |
{ | |
name: "Victorian", | |
axiom: "A", | |
rules: { | |
"A": "B[+C][-C][/D][\\D]", | |
"B": "F[+F][-F][/F][\\F]", | |
"C": "F[++F][--F]", | |
"D": "F[+\F][-/F]" | |
}, | |
iterations: 3, | |
baseHeight: 15, | |
baseWidth: 5, | |
baseDepth: 5, | |
angle: Math.PI/5, | |
probability: 0.15 | |
}, | |
// Modern style - clean lines, boxy but with variations | |
{ | |
name: "Modern", | |
axiom: "A", | |
rules: { | |
"A": "B[+B][-B]", | |
"B": "F[/C][\\C]", | |
"C": "F" | |
}, | |
iterations: 2, | |
baseHeight: 20, | |
baseWidth: 8, | |
baseDepth: 8, | |
angle: Math.PI/2, | |
probability: 0.25 | |
}, | |
// Skyscraper - tall vertical structures | |
{ | |
name: "Skyscraper", | |
axiom: "A", | |
rules: { | |
"A": "FB[+C][-C]", | |
"B": "FB", | |
"C": "F[+F][-F]" | |
}, | |
iterations: 4, | |
baseHeight: 30, | |
baseWidth: 10, | |
baseDepth: 10, | |
angle: Math.PI/8, | |
probability: 0.15 | |
}, | |
// Simple box building - for variety and filling space | |
{ | |
name: "Simple", | |
axiom: "F", | |
rules: { | |
"F": "F[+F][-F]" | |
}, | |
iterations: 1, | |
baseHeight: 8, | |
baseWidth: 6, | |
baseDepth: 6, | |
angle: Math.PI/4, | |
probability: 0.25 | |
} | |
]; | |
// L-system interpreter | |
function interpretLSystem(rule, position, rotation) { | |
// Start with the axiom | |
let currentString = rule.axiom; | |
// Apply rules for the specified number of iterations | |
for (let i = 0; i < rule.iterations; i++) { | |
let newString = ""; | |
// Apply rules to each character | |
for (let j = 0; j < currentString.length; j++) { | |
const char = currentString[j]; | |
newString += rule.rules[char] || char; | |
} | |
currentString = newString; | |
} | |
// Now interpret the L-system string to create building parts | |
let buildingGroup = new THREE.Group(); | |
buildingGroup.position.copy(position); | |
// Stack to keep track of transformations | |
const stack = []; | |
let currentPosition = new THREE.Vector3(0, 0, 0); | |
let currentRotation = rotation || new THREE.Euler(); | |
let scale = new THREE.Vector3(1, 1, 1); | |
// Select a material for this building | |
const color = buildingColors[Math.floor(Math.random() * buildingColors.length)]; | |
const material = new THREE.MeshStandardMaterial({ | |
color: color, | |
roughness: 0.7, | |
metalness: 0.2 | |
}); | |
// Interpret each character in the final string | |
for (let i = 0; i < currentString.length; i++) { | |
const char = currentString[i]; | |
switch (char) { | |
case 'F': // Forward and create a building part | |
// Randomize dimensions with constraints based on rule | |
const width = rule.baseWidth * (0.5 + Math.random() * 0.5) * scale.x; | |
const height = rule.baseHeight * (0.5 + Math.random() * 0.5) * scale.y; | |
const depth = rule.baseDepth * (0.5 + Math.random() * 0.5) * scale.z; | |
// Create geometry | |
const geometry = new THREE.BoxGeometry(width, height, depth); | |
const buildingPart = new THREE.Mesh(geometry, material); | |
// Position and add to group | |
buildingPart.position.copy(currentPosition); | |
buildingPart.rotation.copy(currentRotation); | |
buildingPart.castShadow = true; | |
buildingPart.receiveShadow = true; | |
// Add windows if part is large enough | |
if (height > 5 && width > 2 && depth > 2) { | |
addWindowsToBuilding(buildingPart, width, height, depth); | |
} | |
buildingGroup.add(buildingPart); | |
// Move forward in the direction of current rotation | |
const direction = new THREE.Vector3(0, height/2, 0); | |
direction.applyEuler(currentRotation); | |
currentPosition.add(direction); | |
break; | |
case '+': // Rotate right around Y axis | |
currentRotation.y += rule.angle; | |
break; | |
case '-': // Rotate left around Y axis | |
currentRotation.y -= rule.angle; | |
break; | |
case '/': // Rotate around X axis | |
currentRotation.x += rule.angle; | |
break; | |
case '\\': // Rotate around X axis (opposite) | |
currentRotation.x -= rule.angle; | |
break; | |
case '^': // Rotate around Z axis | |
currentRotation.z += rule.angle; | |
break; | |
case '&': // Rotate around Z axis (opposite) | |
currentRotation.z -= rule.angle; | |
break; | |
case '[': // Push state | |
stack.push({ | |
position: currentPosition.clone(), | |
rotation: currentRotation.clone(), | |
scale: scale.clone() | |
}); | |
break; | |
case ']': // Pop state | |
if (stack.length > 0) { | |
const state = stack.pop(); | |
currentPosition = state.position; | |
currentRotation = state.rotation; | |
scale = state.scale; | |
} | |
break; | |
case '>': // Scale up | |
scale.multiplyScalar(1.2); | |
break; | |
case '<': // Scale down | |
scale.multiplyScalar(0.8); | |
break; | |
} | |
} | |
return buildingGroup; | |
} | |
// Create a city | |
function createCity() { | |
// Create grid of buildings | |
const citySize = 5; // Size of the city grid | |
const spacing = 15; // Spacing between building centers | |
for (let x = -citySize; x <= citySize; x++) { | |
for (let z = -citySize; z <= citySize; z++) { | |
// Skip sometimes to create spaces | |
if (Math.random() < 0.2) continue; | |
// Position with slight randomization | |
const position = new THREE.Vector3( | |
x * spacing + (Math.random() * 2 - 1), // Add slight randomness | |
0, // Will be adjusted by the L-system | |
z * spacing + (Math.random() * 2 - 1) | |
); | |
// Select a building style based on probability | |
let selectedRule = null; | |
let random = Math.random(); | |
let cumulativeProbability = 0; | |
for (const rule of buildingRules) { | |
cumulativeProbability += rule.probability; | |
if (random <= cumulativeProbability) { | |
selectedRule = rule; | |
break; | |
} | |
} | |
if (!selectedRule) { | |
selectedRule = buildingRules[0]; // Default to first rule if somehow none selected | |
} | |
// Create building using L-system | |
const building = interpretLSystem(selectedRule, position, new THREE.Euler()); | |
scene.add(building); | |
buildings.push(building); | |
} | |
} | |
// Create streets | |
const roadWidth = 8; | |
const roadColor = 0x333333; | |
// X-axis roads | |
for (let x = -citySize; x <= citySize; x++) { | |
const roadGeometry = new THREE.PlaneGeometry(roadWidth, citySize * 2 * spacing + roadWidth); | |
const roadMaterial = new THREE.MeshStandardMaterial({ | |
color: roadColor, | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
const road = new THREE.Mesh(roadGeometry, roadMaterial); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set(x * spacing, 0.01, 0); // Slightly above ground to prevent z-fighting | |
scene.add(road); | |
// Add road markings | |
const markingGeometry = new THREE.PlaneGeometry(0.5, citySize * 2 * spacing + roadWidth); | |
const markingMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); | |
const marking = new THREE.Mesh(markingGeometry, markingMaterial); | |
marking.rotation.x = -Math.PI / 2; | |
marking.position.set(x * spacing, 0.02, 0); | |
scene.add(marking); | |
} | |
// Z-axis roads | |
for (let z = -citySize; z <= citySize; z++) { | |
const roadGeometry = new THREE.PlaneGeometry(citySize * 2 * spacing + roadWidth, roadWidth); | |
const roadMaterial = new THREE.MeshStandardMaterial({ | |
color: roadColor, | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
const road = new THREE.Mesh(roadGeometry, roadMaterial); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set(0, 0.01, z * spacing); | |
scene.add(road); | |
// Add road markings | |
const markingGeometry = new THREE.PlaneGeometry(citySize * 2 * spacing + roadWidth, 0.5); | |
const markingMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); | |
const marking = new THREE.Mesh(markingGeometry, markingMaterial); | |
marking.rotation.x = -Math.PI / 2; | |
marking.position.set(0, 0.02, z * spacing); | |
scene.add(marking); | |
} | |
} | |
// Add windows to a building | |
function addWindowsToBuilding(building, width, height, depth) { | |
const windowSize = 0.5; | |
const windowSpacing = 1.5; | |
const windowGeometry = new THREE.PlaneGeometry(windowSize, windowSize); | |
const windowMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xffffcc, | |
emissive: 0xffffcc, | |
emissiveIntensity: 0.5, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
// Calculate how many levels of windows to add based on building height | |
const numLevels = Math.floor((height - 2) / windowSpacing); | |
// Front and back windows | |
const frontZ = depth / 2 + 0.01; | |
const backZ = -depth / 2 - 0.01; | |
for (let level = 0; level < numLevels; level++) { | |
// Calculate y position for this level, starting from near the bottom | |
const y = 1 + level * windowSpacing; | |
for (let x = -width / 2 + windowSpacing; x < width / 2 - windowSpacing / 2; x += windowSpacing) { | |
// Only add some windows randomly | |
if (Math.random() < 0.3) continue; | |
// Front window | |
const frontWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
frontWindow.position.set(x, y, frontZ); | |
frontWindow.rotation.y = Math.PI; | |
building.add(frontWindow); | |
// Back window | |
const backWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
backWindow.position.set(x, y, backZ); | |
building.add(backWindow); | |
} | |
} | |
// Side windows | |
const rightX = width / 2 + 0.01; | |
const leftX = -width / 2 - 0.01; | |
for (let level = 0; level < numLevels; level++) { | |
// Calculate y position for this level, starting from near the bottom | |
const y = 1 + level * windowSpacing; | |
for (let z = -depth / 2 + windowSpacing; z < depth / 2 - windowSpacing / 2; z += windowSpacing) { | |
// Only add some windows randomly | |
if (Math.random() < 0.3) continue; | |
// Right window | |
const rightWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
rightWindow.position.set(rightX, y, z); | |
rightWindow.rotation.y = Math.PI / 2; | |
building.add(rightWindow); | |
// Left window | |
const leftWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
leftWindow.position.set(leftX, y, z); | |
leftWindow.rotation.y = -Math.PI / 2; | |
building.add(leftWindow); | |
} | |
} | |
} | |
// Create collectible items | |
function createCollectibles() { | |
const citySize = 5; | |
const spacing = 15; | |
for (let i = 0; i < 20; i++) { | |
const x = (Math.random() * 2 - 1) * citySize * spacing; | |
const z = (Math.random() * 2 - 1) * citySize * spacing; | |
const y = 1 + Math.random() * 20; | |
const collectibleGeometry = new THREE.BoxGeometry(1, 1, 1); | |
const collectibleMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xffff00, | |
emissive: 0xffff00, | |
emissiveIntensity: 0.5, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const collectible = new THREE.Mesh(collectibleGeometry, collectibleMaterial); | |
collectible.position.set(x, y, z); | |
collectible.userData.id = i; | |
collectible.userData.rotationSpeed = 0.01 + Math.random() * 0.02; | |
collectible.userData.floatSpeed = 0.5 + Math.random() * 0.5; | |
collectible.userData.floatRange = 0.5 + Math.random() * 0.5; | |
collectible.userData.initialY = y; | |
scene.add(collectible); | |
collectibles.push(collectible); | |
} | |
} | |
// Create skybox with clouds | |
function createSkybox() { | |
const skyGeometry = new THREE.SphereGeometry(400, 32, 32); | |
const skyMaterial = new THREE.MeshBasicMaterial({ | |
color: 0x87CEEB, | |
side: THREE.BackSide | |
}); | |
const sky = new THREE.Mesh(skyGeometry, skyMaterial); | |
scene.add(sky); | |
// Add clouds | |
for (let i = 0; i < 50; i++) { | |
const radius = 350; | |
const phi = Math.random() * Math.PI; | |
const theta = Math.random() * Math.PI * 2; | |
const x = radius * Math.sin(phi) * Math.cos(theta); | |
const y = radius * Math.cos(phi) + 50; // Keep clouds higher in the sky | |
const z = radius * Math.sin(phi) * Math.sin(theta); | |
const cloudSize = 10 + Math.random() * 20; | |
const cloudGeometry = new THREE.SphereGeometry(cloudSize, 8, 8); | |
const cloudMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xffffff, | |
roughness: 1, | |
metalness: 0, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial); | |
cloud.position.set(x, y, z); | |
cloud.userData.rotationSpeed = 0.0001 + Math.random() * 0.0002; | |
scene.add(cloud); | |
} | |
} | |
// Player object | |
const playerHeight = 2; | |
const playerRadius = 0.5; | |
// Use cylinder instead of capsule for compatibility with r128 | |
const playerGeometry = new THREE.CylinderGeometry(playerRadius, playerRadius, playerHeight, 8); | |
const playerMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff }); | |
const player = new THREE.Mesh(playerGeometry, playerMaterial); | |
player.position.set(0, playerHeight / 2, 0); | |
player.castShadow = true; | |
scene.add(player); | |
// Player physics | |
const playerVelocity = new THREE.Vector3(); | |
const playerDirection = new THREE.Vector3(); | |
let isJumping = false; | |
const GRAVITY = 0.2; | |
const JUMP_FORCE = 0.7; | |
const MOVE_SPEED = 0.2; | |
// Player collision detection | |
function checkPlayerCollisions() { | |
// Collectible collisions | |
for (let i = collectibles.length - 1; i >= 0; i--) { | |
const collectible = collectibles[i]; | |
const distance = player.position.distanceTo(collectible.position); | |
if (distance < playerRadius + 1) { | |
scene.remove(collectible); | |
collectibles.splice(i, 1); | |
score += 10; | |
document.getElementById('score').textContent = `Score: ${score}`; | |
} | |
} | |
// Building collisions - updated for composite buildings | |
for (const building of buildings) { | |
// For each building group, we need to check collision with each child | |
building.traverse((child) => { | |
if (child.isMesh) { | |
const buildingBox = new THREE.Box3().setFromObject(child); | |
const playerPos = player.position.clone(); | |
// Check if player is inside building bounds but add some margin for the radius | |
if (playerPos.x + playerRadius > buildingBox.min.x && | |
playerPos.x - playerRadius < buildingBox.max.x && | |
playerPos.z + playerRadius > buildingBox.min.z && | |
playerPos.z - playerRadius < buildingBox.max.z) { | |
// Find the closest edge to push the player away from | |
const edgeDistances = [ | |
{edge: 'left', dist: Math.abs(playerPos.x - buildingBox.min.x)}, | |
{edge: 'right', dist: Math.abs(playerPos.x - buildingBox.max.x)}, | |
{edge: 'front', dist: Math.abs(playerPos.z - buildingBox.min.z)}, | |
{edge: 'back', dist: Math.abs(playerPos.z - buildingBox.max.z)} | |
]; | |
edgeDistances.sort((a, b) => a.dist - b.dist); | |
const closestEdge = edgeDistances[0].edge; | |
// Push player away from the closest edge | |
switch (closestEdge) { | |
case 'left': | |
player.position.x = buildingBox.min.x - playerRadius; | |
break; | |
case 'right': | |
player.position.x = buildingBox.max.x + playerRadius; | |
break; | |
case 'front': | |
player.position.z = buildingBox.min.z - playerRadius; | |
break; | |
case 'back': | |
player.position.z = buildingBox.max.z + playerRadius; | |
break; | |
} | |
} | |
} | |
}); | |
} | |
// Floor collision and jumping physics | |
if (player.position.y <= playerHeight / 2) { | |
player.position.y = playerHeight / 2; | |
playerVelocity.y = 0; | |
isJumping = false; | |
} | |
} | |
// Controls | |
const keys = { | |
w: false, | |
a: false, | |
s: false, | |
d: false, | |
space: false | |
}; | |
// Mouse controls for looking around | |
const mousePosition = { | |
x: 0, | |
y: 0 | |
}; | |
let cameraRotation = 0; | |
let cameraPitch = 0; | |
document.addEventListener('keydown', (event) => { | |
switch (event.key.toLowerCase()) { | |
case 'w': keys.w = true; break; | |
case 'a': keys.a = true; break; | |
case 's': keys.s = true; break; | |
case 'd': keys.d = true; break; | |
case ' ': keys.space = true; break; | |
} | |
}); | |
document.addEventListener('keyup', (event) => { | |
switch (event.key.toLowerCase()) { | |
case 'w': keys.w = false; break; | |
case 'a': keys.a = false; break; | |
case 's': keys.s = false; break; | |
case 'd': keys.d = false; break; | |
case ' ': keys.space = false; break; | |
} | |
}); | |
document.addEventListener('mousemove', (event) => { | |
// Only capture mouse if pointer is locked | |
if (document.pointerLockElement === renderer.domElement) { | |
cameraRotation -= event.movementX * 0.002; | |
cameraPitch -= event.movementY * 0.002; | |
// Limit pitch to prevent camera flipping | |
cameraPitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, cameraPitch)); | |
} | |
}); | |
// Lock pointer when clicking on the game | |
renderer.domElement.addEventListener('click', () => { | |
if (!document.pointerLockElement) { | |
renderer.domElement.requestPointerLock(); | |
} | |
}); | |
// Update player position based on input | |
function updatePlayer() { | |
// Apply gravity | |
playerVelocity.y -= GRAVITY; | |
// Handle jumping | |
if (keys.space && !isJumping) { | |
playerVelocity.y = JUMP_FORCE; | |
isJumping = true; | |
} | |
// Get movement direction based on camera rotation | |
// Switching W and S keys (W now moves backward, S moves forward) | |
playerDirection.z = Number(keys.s) - Number(keys.w); | |
playerDirection.x = Number(keys.d) - Number(keys.a); | |
playerDirection.normalize(); | |
// Rotate direction based on camera rotation | |
playerDirection.applyAxisAngle(new THREE.Vector3(0, 1, 0), cameraRotation); | |
// Apply movement | |
player.position.x += playerDirection.x * MOVE_SPEED; | |
player.position.z += playerDirection.z * MOVE_SPEED; | |
player.position.y += playerVelocity.y; | |
// Update camera position to follow player | |
camera.position.x = player.position.x; | |
camera.position.z = player.position.z; | |
camera.position.y = player.position.y + 1.5; // Eye level | |
// Update camera rotation | |
camera.rotation.order = 'YXZ'; // Important for proper rotation | |
camera.rotation.y = cameraRotation; | |
camera.rotation.x = cameraPitch; | |
// Collision detection | |
checkPlayerCollisions(); | |
} | |
// Timer function | |
function updateTimer() { | |
if (gameActive && timeRemaining > 0) { | |
timeRemaining--; | |
document.getElementById('time').textContent = `Time: ${timeRemaining}`; | |
if (timeRemaining === 0) { | |
gameActive = false; | |
endGame(); | |
} | |
} | |
} | |
// End game | |
function endGame() { | |
const endScreen = document.createElement('div'); | |
endScreen.style.position = 'absolute'; | |
endScreen.style.top = '50%'; | |
endScreen.style.left = '50%'; | |
endScreen.style.transform = 'translate(-50%, -50%)'; | |
endScreen.style.background = 'rgba(0, 0, 0, 0.8)'; | |
endScreen.style.color = 'white'; | |
endScreen.style.padding = '20px'; | |
endScreen.style.borderRadius = '10px'; | |
endScreen.style.textAlign = 'center'; | |
endScreen.innerHTML = ` | |
<h2>Game Over!</h2> | |
<p>Your final score: ${score}</p> | |
<button id="restart-btn" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px;">Play Again</button> | |
`; | |
document.body.appendChild(endScreen); | |
document.getElementById('restart-btn').addEventListener('click', () => { | |
document.body.removeChild(endScreen); | |
score = 0; | |
timeRemaining = 60; | |
gameActive = true; | |
document.getElementById('score').textContent = `Score: ${score}`; | |
document.getElementById('time').textContent = `Time: ${timeRemaining}`; | |
// Reset player position | |
player.position.set(0, playerHeight / 2, 0); | |
playerVelocity.set(0, 0, 0); | |
// Reset camera | |
cameraRotation = 0; | |
cameraPitch = 0; | |
// Reset collectibles | |
for (const collectible of collectibles) { | |
scene.remove(collectible); | |
} | |
collectibles.length = 0; | |
createCollectibles(); | |
}); | |
} | |
// Animation and game loop | |
function animate() { | |
requestAnimationFrame(animate); | |
if (gameActive) { | |
updatePlayer(); | |
// Animate collectibles | |
for (const collectible of collectibles) { | |
collectible.rotation.x += collectible.userData.rotationSpeed; | |
collectible.rotation.y += collectible.userData.rotationSpeed * 1.5; | |
// Float up and down | |
const floatOffset = Math.sin(Date.now() * 0.001 * collectible.userData.floatSpeed) * collectible.userData.floatRange; | |
collectible.position.y = collectible.userData.initialY + floatOffset; | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// Initialize | |
createCity(); | |
createCollectibles(); | |
createSkybox(); | |
// Start game timer | |
setInterval(updateTimer, 1000); | |
// Start animation loop | |
animate(); | |
</script> | |
</body> | |
</html> |