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/A/S/D to move, Mouse to look, Space to jump, Shift for speed boost</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> | |
// Game variables | |
let score = 0, timeRemaining = 60, gameActive = true; | |
// Scene setup | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 5, 15); | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
document.body.appendChild(renderer.domElement); | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2); | |
scene.add(ambientLight); | |
// Sun and Moon | |
const sunLight = new THREE.DirectionalLight(0xffddaa, 0.8); | |
sunLight.castShadow = true; | |
sunLight.shadow.mapSize.width = 2048; | |
sunLight.shadow.mapSize.height = 2048; | |
sunLight.shadow.camera.near = 1; | |
sunLight.shadow.camera.far = 500; | |
sunLight.shadow.camera.left = -100; | |
sunLight.shadow.camera.right = 100; | |
sunLight.shadow.camera.top = 100; | |
sunLight.shadow.camera.bottom = -100; | |
scene.add(sunLight); | |
const moonLight = new THREE.DirectionalLight(0xaabbff, 0.4); | |
moonLight.castShadow = true; | |
moonLight.shadow.mapSize.width = 2048; | |
moonLight.shadow.mapSize.height = 2048; | |
scene.add(moonLight); | |
// Ground with bump mapping | |
const textureLoader = new THREE.TextureLoader(); | |
const groundGeometry = new THREE.PlaneGeometry(200, 200); | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x1a5e1a, | |
roughness: 0.8, | |
metalness: 0.2, | |
bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'), | |
bumpScale: 0.1 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
// Buildings and collectibles arrays | |
const buildings = [], collectibles = []; | |
const buildingColors = [0x888888, 0x666666, 0x999999, 0xaaaaaa, 0x555555, 0x334455, 0x445566, 0x223344, 0x556677, 0x667788, 0x993333, 0x884422, 0x553333, 0x772222, 0x664433]; | |
// Building rules (unchanged) | |
const buildingRules = [ | |
{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}, | |
{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}, | |
{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}, | |
{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}, | |
{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 (modified for better window positioning) | |
function interpretLSystem(rule, position, rotation) { | |
let currentString = rule.axiom; | |
for (let i = 0; i < rule.iterations; i++) { | |
let newString = ""; | |
for (let j = 0; j < currentString.length; j++) { | |
newString += rule.rules[currentString[j]] || currentString[j]; | |
} | |
currentString = newString; | |
} | |
let buildingGroup = new THREE.Group(); | |
buildingGroup.position.copy(position); | |
const stack = []; | |
let currentPosition = new THREE.Vector3(0, 0, 0); | |
let currentRotation = rotation || new THREE.Euler(); | |
let scale = new THREE.Vector3(1, 1, 1); | |
const color = buildingColors[Math.floor(Math.random() * buildingColors.length)]; | |
const material = new THREE.MeshStandardMaterial({color: color, roughness: 0.7, metalness: 0.2}); | |
for (let i = 0; i < currentString.length; i++) { | |
const char = currentString[i]; | |
switch (char) { | |
case 'F': | |
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; | |
const geometry = new THREE.BoxGeometry(width, height, depth); | |
const buildingPart = new THREE.Mesh(geometry, material); | |
buildingPart.position.copy(currentPosition); | |
buildingPart.position.y += height / 2; // Center properly | |
buildingPart.rotation.copy(currentRotation); | |
buildingPart.castShadow = true; | |
buildingPart.receiveShadow = true; | |
if (height > 5 && width > 2 && depth > 2) { | |
addWindowsToBuilding(buildingPart, width, height, depth); | |
} | |
buildingGroup.add(buildingPart); | |
const direction = new THREE.Vector3(0, height, 0); | |
direction.applyEuler(currentRotation); | |
currentPosition.add(direction); | |
break; | |
case '+': currentRotation.y += rule.angle; break; | |
case '-': currentRotation.y -= rule.angle; break; | |
case '/': currentRotation.x += rule.angle; break; | |
case '\\': currentRotation.x -= rule.angle; break; | |
case '^': currentRotation.z += rule.angle; break; | |
case '&': currentRotation.z -= rule.angle; break; | |
case '[': stack.push({position: currentPosition.clone(), rotation: currentRotation.clone(), scale: scale.clone()}); break; | |
case ']': if (stack.length > 0) { const state = stack.pop(); currentPosition = state.position; currentRotation = state.rotation; scale = state.scale; } break; | |
case '>': scale.multiplyScalar(1.2); break; | |
case '<': scale.multiplyScalar(0.8); break; | |
} | |
} | |
return buildingGroup; | |
} | |
// Create city (unchanged) | |
function createCity() { | |
const citySize = 5, spacing = 15; | |
for (let x = -citySize; x <= citySize; x++) { | |
for (let z = -citySize; z <= citySize; z++) { | |
if (Math.random() < 0.2) continue; | |
const position = new THREE.Vector3(x * spacing + (Math.random() * 2 - 1), 0, z * spacing + (Math.random() * 2 - 1)); | |
let selectedRule, random = Math.random(), cumulativeProbability = 0; | |
for (const rule of buildingRules) { | |
cumulativeProbability += rule.probability; | |
if (random <= cumulativeProbability) { selectedRule = rule; break; } | |
} | |
if (!selectedRule) selectedRule = buildingRules[0]; | |
const building = interpretLSystem(selectedRule, position, new THREE.Euler()); | |
scene.add(building); | |
buildings.push(building); | |
} | |
} | |
// Roads (unchanged) | |
const roadWidth = 8, roadColor = 0x333333; | |
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); | |
scene.add(road); | |
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); | |
} | |
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); | |
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 building (fixed positioning) | |
function addWindowsToBuilding(building, width, height, depth) { | |
const windowSize = 0.5, 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 | |
}); | |
const numLevels = Math.floor((height - 2) / windowSpacing); | |
// Front and back | |
for (let level = 0; level < numLevels; level++) { | |
const y = -height/2 + 1 + level * windowSpacing; | |
for (let x = -width/2 + windowSpacing; x < width/2 - windowSpacing/2; x += windowSpacing) { | |
if (Math.random() < 0.3) continue; | |
const frontWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
frontWindow.position.set(x, y, depth/2 + 0.01); | |
building.add(frontWindow); | |
const backWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
backWindow.position.set(x, y, -depth/2 - 0.01); | |
backWindow.rotation.y = Math.PI; | |
building.add(backWindow); | |
} | |
} | |
// Sides | |
for (let level = 0; level < numLevels; level++) { | |
const y = -height/2 + 1 + level * windowSpacing; | |
for (let z = -depth/2 + windowSpacing; z < depth/2 - windowSpacing/2; z += windowSpacing) { | |
if (Math.random() < 0.3) continue; | |
const rightWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
rightWindow.position.set(width/2 + 0.01, y, z); | |
rightWindow.rotation.y = -Math.PI/2; | |
building.add(rightWindow); | |
const leftWindow = new THREE.Mesh(windowGeometry, windowMaterial); | |
leftWindow.position.set(-width/2 - 0.01, y, z); | |
leftWindow.rotation.y = Math.PI/2; | |
building.add(leftWindow); | |
} | |
} | |
} | |
// Collectibles, skybox, player setup (unchanged) | |
function createCollectibles() { | |
const citySize = 5, spacing = 15; | |
for (let i = 0; i < 20; i++) { | |
const collectible = new THREE.Mesh( | |
new THREE.BoxGeometry(1, 1, 1), | |
new THREE.MeshStandardMaterial({color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5, transparent: true, opacity: 0.8}) | |
); | |
const x = (Math.random() * 2 - 1) * citySize * spacing; | |
const z = (Math.random() * 2 - 1) * citySize * spacing; | |
const y = 1 + Math.random() * 20; | |
collectible.position.set(x, y, z); | |
collectible.userData = { | |
id: i, rotationSpeed: 0.01 + Math.random() * 0.02, | |
floatSpeed: 0.5 + Math.random() * 0.5, floatRange: 0.5 + Math.random() * 0.5, | |
initialY: y | |
}; | |
scene.add(collectible); | |
collectibles.push(collectible); | |
} | |
} | |
function createSkybox() { | |
const sky = new THREE.Mesh( | |
new THREE.SphereGeometry(400, 32, 32), | |
new THREE.MeshBasicMaterial({color: 0x87CEEB, side: THREE.BackSide}) | |
); | |
scene.add(sky); | |
for (let i = 0; i < 50; i++) { | |
const radius = 350, phi = Math.random() * Math.PI, theta = Math.random() * Math.PI * 2; | |
const cloud = new THREE.Mesh( | |
new THREE.SphereGeometry(10 + Math.random() * 20, 8, 8), | |
new THREE.MeshStandardMaterial({color: 0xffffff, roughness: 1, metalness: 0, transparent: true, opacity: 0.8}) | |
); | |
cloud.position.set( | |
radius * Math.sin(phi) * Math.cos(theta), | |
radius * Math.cos(phi) + 50, | |
radius * Math.sin(phi) * Math.sin(theta) | |
); | |
cloud.userData.rotationSpeed = 0.0001 + Math.random() * 0.0002; | |
scene.add(cloud); | |
} | |
} | |
const player = new THREE.Mesh( | |
new THREE.CylinderGeometry(0.5, 0.5, 2, 8), | |
new THREE.MeshStandardMaterial({color: 0x0000ff}) | |
); | |
player.position.set(0, 1, 0); | |
player.castShadow = true; | |
scene.add(player); | |
// Player physics and controls | |
const playerVelocity = new THREE.Vector3(); | |
const playerDirection = new THREE.Vector3(); | |
let isJumping = false; | |
const GRAVITY = 0.2, JUMP_FORCE = 0.7, BASE_SPEED = 0.2; | |
let moveSpeed = BASE_SPEED; | |
let speedBoostActive = false, speedBoostCooldown = false; | |
let speedBoostTimer = 0, cooldownTimer = 0; | |
const keys = {w: false, a: false, s: false, d: false, space: false, shift: false}; | |
let cameraRotation = 0, 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; | |
case 'shift': keys.shift = 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; | |
case 'shift': keys.shift = false; break; | |
} | |
}); | |
document.addEventListener('mousemove', (event) => { | |
if (document.pointerLockElement === renderer.domElement) { | |
cameraRotation -= event.movementX * 0.002; | |
cameraPitch -= event.movementY * 0.002; | |
cameraPitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, cameraPitch)); | |
} | |
}); | |
renderer.domElement.addEventListener('click', () => { | |
if (!document.pointerLockElement) renderer.domElement.requestPointerLock(); | |
}); | |
function updatePlayer(delta) { | |
// Speed boost handling | |
if (keys.shift && !speedBoostActive && !speedBoostCooldown) { | |
speedBoostActive = true; | |
speedBoostTimer = 10; | |
moveSpeed = BASE_SPEED * 2; | |
} | |
if (speedBoostActive) { | |
speedBoostTimer -= delta; | |
if (speedBoostTimer <= 0) { | |
speedBoostActive = false; | |
speedBoostCooldown = true; | |
cooldownTimer = 10; | |
moveSpeed = BASE_SPEED; | |
} | |
} | |
if (speedBoostCooldown) { | |
cooldownTimer -= delta; | |
if (cooldownTimer <= 0) speedBoostCooldown = false; | |
} | |
playerVelocity.y -= GRAVITY; | |
if (keys.space && !isJumping) { | |
playerVelocity.y = JUMP_FORCE; | |
isJumping = true; | |
} | |
playerDirection.z = Number(keys.s) - Number(keys.w); | |
playerDirection.x = Number(keys.d) - Number(keys.a); | |
playerDirection.normalize(); | |
playerDirection.applyAxisAngle(new THREE.Vector3(0, 1, 0), cameraRotation); | |
player.position.x += playerDirection.x * moveSpeed; | |
player.position.z += playerDirection.z * moveSpeed; | |
player.position.y += playerVelocity.y; | |
camera.position.set(player.position.x, player.position.y + 1.5, player.position.z); | |
camera.rotation.order = 'YXZ'; | |
camera.rotation.y = cameraRotation; | |
camera.rotation.x = cameraPitch; | |
checkPlayerCollisions(); | |
} | |
function checkPlayerCollisions() { | |
for (let i = collectibles.length - 1; i >= 0; i--) { | |
const collectible = collectibles[i]; | |
if (player.position.distanceTo(collectible.position) < 1) { | |
scene.remove(collectible); | |
collectibles.splice(i, 1); | |
score += 10; | |
document.getElementById('score').textContent = `Score: ${score}`; | |
} | |
} | |
for (const building of buildings) { | |
building.traverse((child) => { | |
if (child.isMesh) { | |
const buildingBox = new THREE.Box3().setFromObject(child); | |
const playerPos = player.position.clone(); | |
if (playerPos.x + 0.5 > buildingBox.min.x && playerPos.x - 0.5 < buildingBox.max.x && | |
playerPos.z + 0.5 > buildingBox.min.z && playerPos.z - 0.5 < buildingBox.max.z) { | |
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)} | |
].sort((a, b) => a.dist - b.dist); | |
switch (edgeDistances[0].edge) { | |
case 'left': player.position.x = buildingBox.min.x - 0.5; break; | |
case 'right': player.position.x = buildingBox.max.x + 0.5; break; | |
case 'front': player.position.z = buildingBox.min.z - 0.5; break; | |
case 'back': player.position.z = buildingBox.max.z + 0.5; break; | |
} | |
} | |
} | |
}); | |
} | |
if (player.position.y <= 1) { | |
player.position.y = 1; | |
playerVelocity.y = 0; | |
isJumping = false; | |
} | |
} | |
// Sun and Moon cycle | |
let cycleTime = 0; | |
function updateLighting(delta) { | |
cycleTime += delta; | |
const cycleDuration = 120; // 2 minutes in seconds | |
const angle = (cycleTime / cycleDuration) * Math.PI * 2; | |
// Sun position and intensity | |
sunLight.position.set( | |
Math.cos(angle) * 100, | |
Math.sin(angle) * 100, | |
Math.sin(angle) * 50 | |
); | |
sunLight.intensity = Math.max(0, Math.sin(angle)) * 0.8; | |
// Moon position and intensity | |
moonLight.position.set( | |
Math.cos(angle + Math.PI) * 100, | |
Math.sin(angle + Math.PI) * 100, | |
Math.sin(angle + Math.PI) * 50 | |
); | |
moonLight.intensity = Math.max(0, Math.sin(angle + Math.PI)) * 0.4; | |
// Sky color transition | |
const dayColor = new THREE.Color(0x87CEEB); | |
const nightColor = new THREE.Color(0x001133); | |
scene.background = dayColor.clone().lerp(nightColor, Math.max(0, -Math.sin(angle))); | |
} | |
// Game loop | |
let lastTime = performance.now(); | |
function animate() { | |
requestAnimationFrame(animate); | |
const currentTime = performance.now(); | |
const delta = (currentTime - lastTime) / 1000; | |
lastTime = currentTime; | |
if (gameActive) { | |
updatePlayer(delta); | |
updateLighting(delta); | |
for (const collectible of collectibles) { | |
collectible.rotation.x += collectible.userData.rotationSpeed; | |
collectible.rotation.y += collectible.userData.rotationSpeed * 1.5; | |
collectible.position.y = collectible.userData.initialY + | |
Math.sin(Date.now() * 0.001 * collectible.userData.floatSpeed) * collectible.userData.floatRange; | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
// Timer and end game (unchanged) | |
function updateTimer() { | |
if (gameActive && timeRemaining > 0) { | |
timeRemaining--; | |
document.getElementById('time').textContent = `Time: ${timeRemaining}`; | |
if (timeRemaining === 0) { | |
gameActive = false; | |
endGame(); | |
} | |
} | |
} | |
function endGame() { | |
const endScreen = document.createElement('div'); | |
endScreen.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.8);color:white;padding:20px;border-radius:10px;text-align: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}`; | |
player.position.set(0, 1, 0); | |
playerVelocity.set(0, 0, 0); | |
cameraRotation = 0; cameraPitch = 0; | |
for (const collectible of collectibles) scene.remove(collectible); | |
collectibles.length = 0; | |
createCollectibles(); | |
}); | |
} | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// Initialize | |
createCity(); | |
createCollectibles(); | |
createSkybox(); | |
setInterval(updateTimer, 1000); | |
animate(); | |
</script> | |
</body> | |
</html> |