3DCityScene / index.html
awacke1's picture
Update index.html
47aab95 verified
raw
history blame
25.8 kB
<!DOCTYPE html>
<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>