Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Simple SimCity</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> | |
| <style> | |
| #game-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| #ui-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| .building-option { | |
| pointer-events: all; | |
| transition: all 0.2s; | |
| } | |
| .building-option:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 15px rgba(255, 255, 255, 0.5); | |
| } | |
| .selected { | |
| border: 2px solid white; | |
| box-shadow: 0 0 15px rgba(255, 255, 255, 0.8); | |
| } | |
| #grid-highlight { | |
| position: absolute; | |
| background-color: rgba(255, 255, 255, 0.3); | |
| border: 1px dashed white; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white"> | |
| <div id="game-container"> | |
| <div id="ui-overlay" class="p-4"> | |
| <div class="flex justify-between items-start"> | |
| <!-- Building selection panel --> | |
| <div class="bg-gray-800 bg-opacity-80 rounded-lg p-4 shadow-lg"> | |
| <h2 class="text-xl font-bold mb-3">Buildings</h2> | |
| <div class="grid grid-cols-3 gap-3"> | |
| <div class="building-option bg-blue-600 rounded p-2 cursor-pointer text-center" data-type="house"> | |
| <div class="h-12 w-12 bg-blue-400 mx-auto mb-1"></div> | |
| <span>House</span> | |
| </div> | |
| <div class="building-option bg-green-600 rounded p-2 cursor-pointer text-center" data-type="office"> | |
| <div class="h-12 w-12 bg-green-400 mx-auto mb-1"></div> | |
| <span>Office</span> | |
| </div> | |
| <div class="building-option bg-yellow-600 rounded p-2 cursor-pointer text-center" data-type="park"> | |
| <div class="h-12 w-12 bg-yellow-400 mx-auto mb-1"></div> | |
| <span>Park</span> | |
| </div> | |
| <div class="building-option bg-red-600 rounded p-2 cursor-pointer text-center" data-type="hospital"> | |
| <div class="h-12 w-12 bg-red-400 mx-auto mb-1"></div> | |
| <span>Hospital</span> | |
| </div> | |
| <div class="building-option bg-purple-600 rounded p-2 cursor-pointer text-center" data-type="school"> | |
| <div class="h-12 w-12 bg-purple-400 mx-auto mb-1"></div> | |
| <span>School</span> | |
| </div> | |
| <div class="building-option bg-gray-600 rounded p-2 cursor-pointer text-center" data-type="road"> | |
| <div class="h-12 w-12 bg-gray-400 mx-auto mb-1"></div> | |
| <span>Road</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats panel --> | |
| <div class="bg-gray-800 bg-opacity-80 rounded-lg p-4 shadow-lg"> | |
| <h2 class="text-xl font-bold mb-3">City Stats</h2> | |
| <div class="space-y-2"> | |
| <div>Population: <span id="population">0</span></div> | |
| <div>Buildings: <span id="building-count">0</span></div> | |
| <div>Money: $<span id="money">10000</span></div> | |
| </div> | |
| <button id="demolish-btn" class="mt-4 bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded pointer-events-all"> | |
| Demolish Mode | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Bottom panel --> | |
| <div class="absolute bottom-4 left-0 w-full flex justify-center"> | |
| <div class="bg-gray-800 bg-opacity-80 rounded-lg p-3 shadow-lg"> | |
| <div id="message" class="text-center">Select a building to place</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="grid-highlight"></div> | |
| </div> | |
| <script> | |
| // Game variables | |
| let selectedBuildingType = null; | |
| let demolishMode = false; | |
| let money = 10000; | |
| let population = 0; | |
| let buildingCount = 0; | |
| // Building costs | |
| const buildingCosts = { | |
| house: 500, | |
| office: 1000, | |
| park: 300, | |
| hospital: 1500, | |
| school: 1200, | |
| road: 100 | |
| }; | |
| // Building population values | |
| const buildingPopulation = { | |
| house: 10, | |
| office: -5, // Offices reduce population (workers come from houses) | |
| park: 0, | |
| hospital: 0, | |
| school: 0, | |
| road: 0 | |
| }; | |
| // Three.js variables | |
| let scene, camera, renderer, controls; | |
| let grid = []; | |
| const gridSize = 20; | |
| const cellSize = 2; | |
| const buildings = []; | |
| // Initialize the game | |
| init(); | |
| function init() { | |
| // Set up Three.js scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87CEEB); // Sky blue | |
| // Set up camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(20, 30, 20); | |
| camera.lookAt(0, 0, 0); | |
| // Set up renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.getElementById('game-container').appendChild(renderer.domElement); | |
| // Add orbit controls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0x404040); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| // Create ground grid | |
| createGround(); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| // Set up UI event listeners | |
| setupUI(); | |
| // Start animation loop | |
| animate(); | |
| } | |
| function createGround() { | |
| // Create ground plane | |
| const groundGeometry = new THREE.PlaneGeometry(gridSize * cellSize, gridSize * cellSize); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x3a5f0b, | |
| side: THREE.DoubleSide | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| scene.add(ground); | |
| // Create grid lines | |
| const gridHelper = new THREE.GridHelper(gridSize * cellSize, gridSize); | |
| scene.add(gridHelper); | |
| // Initialize grid array | |
| for (let i = 0; i < gridSize; i++) { | |
| grid[i] = []; | |
| for (let j = 0; j < gridSize; j++) { | |
| grid[i][j] = null; // null means empty cell | |
| } | |
| } | |
| } | |
| function setupUI() { | |
| // Building selection | |
| document.querySelectorAll('.building-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| selectedBuildingType = this.getAttribute('data-type'); | |
| demolishMode = false; | |
| // Update UI | |
| document.querySelectorAll('.building-option').forEach(el => el.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| document.getElementById('demolish-btn').classList.remove('bg-red-700'); | |
| document.getElementById('demolish-btn').classList.add('bg-red-600'); | |
| document.getElementById('message').textContent = `Selected: ${selectedBuildingType.charAt(0).toUpperCase() + selectedBuildingType.slice(1)} (Cost: $${buildingCosts[selectedBuildingType]})`; | |
| }); | |
| }); | |
| // Demolish mode button | |
| document.getElementById('demolish-btn').addEventListener('click', function() { | |
| demolishMode = !demolishMode; | |
| selectedBuildingType = null; | |
| // Update UI | |
| document.querySelectorAll('.building-option').forEach(el => el.classList.remove('selected')); | |
| if (demolishMode) { | |
| this.classList.remove('bg-red-600'); | |
| this.classList.add('bg-red-700'); | |
| document.getElementById('message').textContent = 'Demolish mode active - Click buildings to remove them'; | |
| } else { | |
| this.classList.remove('bg-red-700'); | |
| this.classList.add('bg-red-600'); | |
| document.getElementById('message').textContent = 'Select a building to place'; | |
| } | |
| }); | |
| // Mouse move for grid highlight | |
| document.addEventListener('mousemove', onMouseMove); | |
| // Click to place building | |
| document.addEventListener('click', onClick); | |
| } | |
| function onMouseMove(event) { | |
| if (!selectedBuildingType && !demolishMode) { | |
| document.getElementById('grid-highlight').style.display = 'none'; | |
| return; | |
| } | |
| // Get mouse position in normalized device coordinates (-1 to +1) | |
| const mouse = new THREE.Vector2(); | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| // Raycast to find intersection with ground | |
| const raycaster = new THREE.Raycaster(); | |
| raycaster.setFromCamera(mouse, camera); | |
| const intersects = raycaster.intersectObjects(scene.children); | |
| if (intersects.length > 0) { | |
| const point = intersects[0].point; | |
| // Snap to grid | |
| const gridX = Math.round(point.x / cellSize) * cellSize; | |
| const gridZ = Math.round(point.z / cellSize) * cellSize; | |
| // Convert to grid coordinates | |
| const gridI = Math.round(point.x / cellSize) + gridSize / 2; | |
| const gridJ = Math.round(point.z / cellSize) + gridSize / 2; | |
| // Check if within grid bounds | |
| if (gridI >= 0 && gridI < gridSize && gridJ >= 0 && gridJ < gridSize) { | |
| // Show highlight | |
| const highlight = document.getElementById('grid-highlight'); | |
| highlight.style.width = `${cellSize * 50}px`; | |
| highlight.style.height = `${cellSize * 50}px`; | |
| highlight.style.left = `${event.clientX - cellSize * 25}px`; | |
| highlight.style.top = `${event.clientY - cellSize * 25}px`; | |
| highlight.style.display = 'block'; | |
| // Change color based on availability | |
| if (grid[gridI][gridJ] === null || demolishMode) { | |
| highlight.style.backgroundColor = 'rgba(255, 255, 255, 0.3)'; | |
| } else { | |
| highlight.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'; | |
| } | |
| } else { | |
| document.getElementById('grid-highlight').style.display = 'none'; | |
| } | |
| } else { | |
| document.getElementById('grid-highlight').style.display = 'none'; | |
| } | |
| } | |
| function onClick(event) { | |
| if (!selectedBuildingType && !demolishMode) return; | |
| // Get mouse position in normalized device coordinates (-1 to +1) | |
| const mouse = new THREE.Vector2(); | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| // Raycast to find intersection with ground | |
| const raycaster = new THREE.Raycaster(); | |
| raycaster.setFromCamera(mouse, camera); | |
| const intersects = raycaster.intersectObjects(scene.children); | |
| if (intersects.length > 0) { | |
| const point = intersects[0].point; | |
| // Convert to grid coordinates | |
| const gridI = Math.round(point.x / cellSize) + gridSize / 2; | |
| const gridJ = Math.round(point.z / cellSize) + gridSize / 2; | |
| // Check if within grid bounds | |
| if (gridI >= 0 && gridI < gridSize && gridJ >= 0 && gridJ < gridSize) { | |
| if (demolishMode) { | |
| // Demolish building | |
| if (grid[gridI][gridJ] !== null) { | |
| removeBuilding(gridI, gridJ); | |
| } | |
| } else { | |
| // Place building | |
| if (grid[gridI][gridJ] === null) { | |
| if (money >= buildingCosts[selectedBuildingType]) { | |
| placeBuilding(selectedBuildingType, gridI, gridJ); | |
| } else { | |
| document.getElementById('message').textContent = `Not enough money! Need $${buildingCosts[selectedBuildingType]}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function placeBuilding(type, gridI, gridJ) { | |
| // Calculate world position | |
| const x = (gridI - gridSize / 2) * cellSize; | |
| const z = (gridJ - gridSize / 2) * cellSize; | |
| let buildingMesh; | |
| let height = 1; | |
| // Create different building types | |
| switch (type) { | |
| case 'house': | |
| height = 1.5; | |
| const houseGeometry = new THREE.BoxGeometry(1.8, height, 1.8); | |
| const houseMaterial = new THREE.MeshStandardMaterial({ color: 0x4682B4 }); | |
| buildingMesh = new THREE.Mesh(houseGeometry, houseMaterial); | |
| break; | |
| case 'office': | |
| height = 3; | |
| const officeGeometry = new THREE.BoxGeometry(1.8, height, 1.8); | |
| const officeMaterial = new THREE.MeshStandardMaterial({ color: 0x708090 }); | |
| buildingMesh = new THREE.Mesh(officeGeometry, officeMaterial); | |
| break; | |
| case 'park': | |
| height = 0.2; | |
| const parkGeometry = new THREE.BoxGeometry(1.8, height, 1.8); | |
| const parkMaterial = new THREE.MeshStandardMaterial({ color: 0x32CD32 }); | |
| buildingMesh = new THREE.Mesh(parkGeometry, parkMaterial); | |
| break; | |
| case 'hospital': | |
| height = 2.5; | |
| const hospitalGeometry = new THREE.BoxGeometry(1.8, height, 1.8); | |
| const hospitalMaterial = new THREE.MeshStandardMaterial({ color: 0xFFFAFA }); | |
| buildingMesh = new THREE.Mesh(hospitalGeometry, hospitalMaterial); | |
| // Add red cross | |
| const crossGeometry = new THREE.BoxGeometry(1.8, 0.2, 0.2); | |
| const crossMaterial = new THREE.MeshStandardMaterial({ color: 0xFF0000 }); | |
| const cross1 = new THREE.Mesh(crossGeometry, crossMaterial); | |
| cross1.position.y = height + 0.1; | |
| const cross2 = new THREE.Mesh(crossGeometry, crossMaterial); | |
| cross2.position.y = height + 0.1; | |
| cross2.rotation.y = Math.PI / 2; | |
| scene.add(cross1); | |
| scene.add(cross2); | |
| break; | |
| case 'school': | |
| height = 2; | |
| const schoolGeometry = new THREE.BoxGeometry(1.8, height, 1.8); | |
| const schoolMaterial = new THREE.MeshStandardMaterial({ color: 0xF5DEB3 }); | |
| buildingMesh = new THREE.Mesh(schoolGeometry, schoolMaterial); | |
| break; | |
| case 'road': | |
| height = 0.1; | |
| const roadGeometry = new THREE.BoxGeometry(1.8, height, 1.8); | |
| const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x696969 }); | |
| buildingMesh = new THREE.Mesh(roadGeometry, roadMaterial); | |
| break; | |
| } | |
| // Position the building | |
| buildingMesh.position.set(x, height / 2, z); | |
| scene.add(buildingMesh); | |
| // Add to grid | |
| grid[gridI][gridJ] = { type, mesh: buildingMesh }; | |
| </html> |