export class GameRenderer { constructor() { this.scene = null; this.camera = null; this.renderer = null; this.canvas = null; this.tiles = new Map(); // Map of tile meshes this.buildings = new Map(); // Map of building meshes this.cursors = new Map(); // Map of player cursors this.labels = new Map(); // Map of building labels this.hoveredTile = null; this.cameraPos = { x: 0, y: 50, z: 50 }; this.cameraZoom = 1; // Re-introduced for proper orthographic zoom this.TILE_SIZE = 2; this.VIEW_DISTANCE = 50; } init() { this.canvas = document.getElementById('gameCanvas'); // Create scene this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x87CEEB); // Sky blue // Create camera this.camera = new THREE.OrthographicCamera( -40, 40, 30, -30, 0.1, 1000 ); this.camera.position.set(0, 50, 50); this.camera.lookAt(0, 0, 0); // Create renderer this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: true }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.shadowMap.enabled = true; // Add lights const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 10); directionalLight.castShadow = true; this.scene.add(directionalLight); // Create ground this.createGround(); // Handle window resize window.addEventListener('resize', () => this.onResize()); } createGround() { const geometry = new THREE.PlaneGeometry(1000, 1000); const material = new THREE.MeshLambertMaterial({ color: 0x228B22 }); // Forest green const ground = new THREE.Mesh(geometry, material); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground); } createTile(x, y, color = 0x90EE90) { const geometry = new THREE.PlaneGeometry(this.TILE_SIZE - 0.1, this.TILE_SIZE - 0.1); const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 }); const tile = new THREE.Mesh(geometry, material); tile.position.set(x * this.TILE_SIZE, 0.01, y * this.TILE_SIZE); tile.rotation.x = -Math.PI / 2; tile.userData = { x, y }; return tile; } createBuilding(buildingData) { const { type, x, y, owner_id, name } = buildingData; // Get player color for accents const playerColor = this.getPlayerColor(owner_id); // Create building group const buildingGroup = new THREE.Group(); buildingGroup.position.set(x * this.TILE_SIZE, 0, y * this.TILE_SIZE); buildingGroup.userData = { x, y, owner_id, type, name }; // Create building based on type let buildingMesh; if (type.includes('house')) { buildingMesh = this.createHouse(type, playerColor); } else if (type.includes('shop') || type === 'supermarket' || type === 'mall') { buildingMesh = this.createCommercialBuilding(type, playerColor); } else if (type.includes('factory')) { buildingMesh = this.createFactory(type, playerColor); } else if (type === 'road') { buildingMesh = this.createRoad(); } else if (type === 'park' || type === 'plaza') { buildingMesh = this.createPark(type, playerColor); } else if (type === 'town_hall') { buildingMesh = this.createTownHall(playerColor); } else if (type === 'power_plant') { buildingMesh = this.createPowerPlant(playerColor); } else { // Fallback to simple building buildingMesh = this.createSimpleBuilding(2, 0x808080); } buildingGroup.add(buildingMesh); return buildingGroup; } getPlayerColor(owner_id) { // Try to find the player color from game state or use default if (this.gameState && this.gameState.players && this.gameState.players[owner_id]) { const color = this.gameState.players[owner_id].color; return new THREE.Color(color); } return new THREE.Color(0xff6b6b); // Default reddish color } createSimpleBuilding(height, color) { const geometry = new THREE.BoxGeometry( this.TILE_SIZE - 0.2, height, this.TILE_SIZE - 0.2 ); const material = new THREE.MeshLambertMaterial({ color: color }); const building = new THREE.Mesh(geometry, material); building.position.y = height / 2; building.castShadow = true; building.receiveShadow = true; return building; } createHouse(type, playerColor) { const group = new THREE.Group(); // Determine house size and height let houseWidth = 1.4; let houseDepth = 1.2; let wallHeight = 1.5; let roofHeight = 0.8; if (type === 'medium_house') { houseWidth = 1.6; houseDepth = 1.4; wallHeight = 2.0; roofHeight = 1.0; } else if (type === 'large_house') { houseWidth = 1.7; houseDepth = 1.6; wallHeight = 2.5; roofHeight = 1.2; } // Main house body const wallGeometry = new THREE.BoxGeometry(houseWidth, wallHeight, houseDepth); const wallMaterial = new THREE.MeshLambertMaterial({ color: 0xf4f4f4 }); const walls = new THREE.Mesh(wallGeometry, wallMaterial); walls.position.y = wallHeight / 2; walls.castShadow = true; walls.receiveShadow = true; group.add(walls); // Roof (triangular prism) - player color const roofGeometry = new THREE.CylinderGeometry(0, houseWidth * 0.8, roofHeight, 4); const roofMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const roof = new THREE.Mesh(roofGeometry, roofMaterial); roof.position.y = wallHeight + roofHeight / 2; roof.rotation.y = Math.PI / 4; // Rotate 45 degrees to make it diamond-shaped roof.castShadow = true; group.add(roof); // Door const doorGeometry = new THREE.BoxGeometry(0.3, 0.6, 0.05); const doorMaterial = new THREE.MeshLambertMaterial({ color: 0x8b4513 }); const door = new THREE.Mesh(doorGeometry, doorMaterial); door.position.set(0, 0.3, houseDepth / 2 + 0.02); group.add(door); // Windows with player color frames const createWindow = (x, y, z) => { const windowGroup = new THREE.Group(); // Window frame - player color const frameGeometry = new THREE.BoxGeometry(0.35, 0.35, 0.05); const frameMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const frame = new THREE.Mesh(frameGeometry, frameMaterial); windowGroup.add(frame); // Window glass const glassGeometry = new THREE.BoxGeometry(0.25, 0.25, 0.02); const glassMaterial = new THREE.MeshLambertMaterial({ color: 0x87ceeb, transparent: true, opacity: 0.7 }); const glass = new THREE.Mesh(glassGeometry, glassMaterial); glass.position.z = 0.01; windowGroup.add(glass); windowGroup.position.set(x, y, z); return windowGroup; }; // Add windows group.add(createWindow(-houseWidth/3, wallHeight/2, houseDepth/2 + 0.02)); group.add(createWindow(houseWidth/3, wallHeight/2, houseDepth/2 + 0.02)); // Chimney for medium and large houses if (type !== 'small_house') { const chimneyGeometry = new THREE.BoxGeometry(0.2, 0.6, 0.2); const chimneyMaterial = new THREE.MeshLambertMaterial({ color: 0x696969 }); const chimney = new THREE.Mesh(chimneyGeometry, chimneyMaterial); chimney.position.set(houseWidth/3, wallHeight + roofHeight/2 + 0.3, -houseDepth/4); chimney.castShadow = true; group.add(chimney); } return group; } createCommercialBuilding(type, playerColor) { const group = new THREE.Group(); let width = 1.6; let height = 2.5; let depth = 1.4; let signColor = playerColor; if (type === 'supermarket') { width = 1.7; height = 3.0; depth = 1.6; } else if (type === 'mall') { width = 1.8; height = 3.5; depth = 1.7; } // Main building const buildingGeometry = new THREE.BoxGeometry(width, height, depth); const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0xe6e6e6 }); const building = new THREE.Mesh(buildingGeometry, buildingMaterial); building.position.y = height / 2; building.castShadow = true; building.receiveShadow = true; group.add(building); // Storefront sign - player color const signGeometry = new THREE.BoxGeometry(width * 0.9, 0.3, 0.05); const signMaterial = new THREE.MeshLambertMaterial({ color: signColor }); const sign = new THREE.Mesh(signGeometry, signMaterial); sign.position.set(0, height * 0.8, depth / 2 + 0.02); group.add(sign); // Large windows const windowGeometry = new THREE.BoxGeometry(width * 0.7, height * 0.4, 0.05); const windowMaterial = new THREE.MeshLambertMaterial({ color: 0x4169e1, transparent: true, opacity: 0.6 }); const windows = new THREE.Mesh(windowGeometry, windowMaterial); windows.position.set(0, height * 0.3, depth / 2 + 0.01); group.add(windows); // Entrance door const doorGeometry = new THREE.BoxGeometry(0.4, 0.8, 0.05); const doorMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const door = new THREE.Mesh(doorGeometry, doorMaterial); door.position.set(0, 0.4, depth / 2 + 0.02); group.add(door); // Air conditioning units on roof for larger buildings if (type === 'supermarket' || type === 'mall') { const acGeometry = new THREE.BoxGeometry(0.4, 0.2, 0.3); const acMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 }); const ac1 = new THREE.Mesh(acGeometry, acMaterial); ac1.position.set(-width/3, height + 0.1, 0); group.add(ac1); const ac2 = new THREE.Mesh(acGeometry, acMaterial); ac2.position.set(width/3, height + 0.1, 0); group.add(ac2); } return group; } createFactory(type, playerColor) { const group = new THREE.Group(); let width = 1.6; let height = 3.5; let depth = 1.5; if (type === 'large_factory') { width = 1.8; height = 4.5; depth = 1.7; } // Main factory building const buildingGeometry = new THREE.BoxGeometry(width, height, depth); const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x696969 }); const building = new THREE.Mesh(buildingGeometry, buildingMaterial); building.position.y = height / 2; building.castShadow = true; building.receiveShadow = true; group.add(building); // Smokestack - player color stripe const stackHeight = height + 1.5; const stackGeometry = new THREE.CylinderGeometry(0.1, 0.15, stackHeight); const stackMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 }); const stack = new THREE.Mesh(stackGeometry, stackMaterial); stack.position.set(width/3, stackHeight/2, -depth/4); stack.castShadow = true; group.add(stack); // Player color stripe on smokestack const stripeGeometry = new THREE.CylinderGeometry(0.12, 0.16, 0.3); const stripeMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const stripe = new THREE.Mesh(stripeGeometry, stripeMaterial); stripe.position.set(width/3, stackHeight * 0.7, -depth/4); group.add(stripe); // Factory windows const createFactoryWindow = (x, y, z) => { const windowGeometry = new THREE.BoxGeometry(0.3, 0.4, 0.05); const windowMaterial = new THREE.MeshLambertMaterial({ color: 0xffff88, transparent: true, opacity: 0.8 }); const window = new THREE.Mesh(windowGeometry, windowMaterial); window.position.set(x, y, z); return window; }; // Add multiple windows for (let i = -1; i <= 1; i++) { for (let j = 1; j <= 2; j++) { group.add(createFactoryWindow(i * width/3, j * height/3, depth/2 + 0.01)); } } // Loading dock - player color const dockGeometry = new THREE.BoxGeometry(0.6, 0.1, 0.4); const dockMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const dock = new THREE.Mesh(dockGeometry, dockMaterial); dock.position.set(-width/3, 0.05, depth/2 + 0.2); group.add(dock); return group; } createRoad() { const geometry = new THREE.BoxGeometry( this.TILE_SIZE, 0.1, this.TILE_SIZE ); const material = new THREE.MeshLambertMaterial({ color: 0x2F4F4F }); const road = new THREE.Mesh(geometry, material); road.position.y = 0.05; road.receiveShadow = true; return road; } createPark(type, playerColor) { const group = new THREE.Group(); // Base green area const baseGeometry = new THREE.BoxGeometry(this.TILE_SIZE - 0.1, 0.1, this.TILE_SIZE - 0.1); const baseMaterial = new THREE.MeshLambertMaterial({ color: 0x32cd32 }); const base = new THREE.Mesh(baseGeometry, baseMaterial); base.position.y = 0.05; base.receiveShadow = true; group.add(base); if (type === 'park') { // Add trees const createTree = (x, z) => { const treeGroup = new THREE.Group(); // Trunk const trunkGeometry = new THREE.CylinderGeometry(0.05, 0.08, 0.5); const trunkMaterial = new THREE.MeshLambertMaterial({ color: 0x8b4513 }); const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); trunk.position.y = 0.25; trunk.castShadow = true; treeGroup.add(trunk); // Leaves - player color accent const leavesGeometry = new THREE.SphereGeometry(0.3, 8, 6); const leavesMaterial = new THREE.MeshLambertMaterial({ color: playerColor.clone().lerp(new THREE.Color(0x228b22), 0.7) }); const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial); leaves.position.y = 0.6; leaves.castShadow = true; treeGroup.add(leaves); treeGroup.position.set(x, 0, z); return treeGroup; }; group.add(createTree(-0.4, -0.4)); group.add(createTree(0.4, 0.4)); } else { // plaza // Add fountain with player color accents const fountainGeometry = new THREE.CylinderGeometry(0.3, 0.4, 0.3); const fountainMaterial = new THREE.MeshLambertMaterial({ color: 0xd3d3d3 }); const fountain = new THREE.Mesh(fountainGeometry, fountainMaterial); fountain.position.y = 0.25; fountain.castShadow = true; group.add(fountain); // Water with player color tint const waterGeometry = new THREE.CylinderGeometry(0.25, 0.25, 0.05); const waterMaterial = new THREE.MeshLambertMaterial({ color: playerColor.clone().lerp(new THREE.Color(0x4169e1), 0.5), transparent: true, opacity: 0.7 }); const water = new THREE.Mesh(waterGeometry, waterMaterial); water.position.y = 0.35; group.add(water); } return group; } createTownHall(playerColor) { const group = new THREE.Group(); // Main building const mainGeometry = new THREE.BoxGeometry(1.7, 4.0, 1.6); const mainMaterial = new THREE.MeshLambertMaterial({ color: 0xf5f5dc }); const main = new THREE.Mesh(mainGeometry, mainMaterial); main.position.y = 2.0; main.castShadow = true; main.receiveShadow = true; group.add(main); // Clock tower const towerGeometry = new THREE.BoxGeometry(0.6, 1.5, 0.6); const towerMaterial = new THREE.MeshLambertMaterial({ color: 0xe6e6e6 }); const tower = new THREE.Mesh(towerGeometry, towerMaterial); tower.position.y = 4.75; tower.castShadow = true; group.add(tower); // Clock face - player color const clockGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.05); const clockMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const clock = new THREE.Mesh(clockGeometry, clockMaterial); clock.position.set(0, 4.5, 0.3); clock.rotation.x = Math.PI / 2; group.add(clock); // Flag pole with player color flag const poleGeometry = new THREE.CylinderGeometry(0.02, 0.02, 1.0); const poleMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 }); const pole = new THREE.Mesh(poleGeometry, poleMaterial); pole.position.set(0.6, 5.5, 0); group.add(pole); const flagGeometry = new THREE.PlaneGeometry(0.3, 0.2); const flagMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const flag = new THREE.Mesh(flagGeometry, flagMaterial); flag.position.set(0.75, 5.7, 0); group.add(flag); // Steps with player color carpet const stepsGeometry = new THREE.BoxGeometry(1.8, 0.2, 0.6); const stepsMaterial = new THREE.MeshLambertMaterial({ color: 0xd3d3d3 }); const steps = new THREE.Mesh(stepsGeometry, stepsMaterial); steps.position.set(0, 0.1, 0.8); group.add(steps); const carpetGeometry = new THREE.BoxGeometry(0.4, 0.01, 0.6); const carpetMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const carpet = new THREE.Mesh(carpetGeometry, carpetMaterial); carpet.position.set(0, 0.21, 0.8); group.add(carpet); return group; } createPowerPlant(playerColor) { const group = new THREE.Group(); // Main building const mainGeometry = new THREE.BoxGeometry(1.8, 3.0, 1.7); const mainMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 }); const main = new THREE.Mesh(mainGeometry, mainMaterial); main.position.y = 1.5; main.castShadow = true; main.receiveShadow = true; group.add(main); // Cooling towers const createCoolingTower = (x, z) => { const towerGeometry = new THREE.CylinderGeometry(0.2, 0.3, 4.0); const towerMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 }); const tower = new THREE.Mesh(towerGeometry, towerMaterial); tower.position.set(x, 2.0, z); tower.castShadow = true; // Player color stripe on tower const stripeGeometry = new THREE.CylinderGeometry(0.22, 0.32, 0.3); const stripeMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const stripe = new THREE.Mesh(stripeGeometry, stripeMaterial); stripe.position.set(x, 3.0, z); group.add(stripe); return tower; }; group.add(createCoolingTower(-0.5, -0.3)); group.add(createCoolingTower(0.5, -0.3)); // Control room with player color accents const controlGeometry = new THREE.BoxGeometry(0.8, 1.0, 0.6); const controlMaterial = new THREE.MeshLambertMaterial({ color: 0x777777 }); const control = new THREE.Mesh(controlGeometry, controlMaterial); control.position.set(0, 3.5, 0.4); control.castShadow = true; group.add(control); // Control room windows - player color frames const windowGeometry = new THREE.BoxGeometry(0.6, 0.3, 0.05); const windowMaterial = new THREE.MeshLambertMaterial({ color: playerColor }); const windows = new THREE.Mesh(windowGeometry, windowMaterial); windows.position.set(0, 3.5, 0.7); group.add(windows); // Power lines const lineGeometry = new THREE.CylinderGeometry(0.01, 0.01, 2.0); const lineMaterial = new THREE.MeshLambertMaterial({ color: 0x000000 }); const line = new THREE.Mesh(lineGeometry, lineMaterial); line.position.set(0.8, 4.0, 0); line.rotation.z = Math.PI / 6; group.add(line); return group; } createCursor(playerId, color) { const geometry = new THREE.RingGeometry(0.5, 0.7, 16); const material = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide }); const cursor = new THREE.Mesh(geometry, material); cursor.rotation.x = -Math.PI / 2; cursor.position.y = 0.02; return cursor; } updateGameState(gameState) { // Store game state for player color access this.gameState = gameState; // Clear existing buildings this.buildings.forEach(mesh => this.scene.remove(mesh)); this.buildings.clear(); // Add all buildings Object.values(gameState.buildings).forEach(building => { this.addBuilding(building); }); } addBuilding(buildingData) { const key = `${buildingData.x},${buildingData.y}`; // Remove existing building at this position if (this.buildings.has(key)) { this.scene.remove(this.buildings.get(key)); } // Create and add new building const building = this.createBuilding(buildingData); this.buildings.set(key, building); this.scene.add(building); } removeBuilding(x, y) { const key = `${x},${y}`; if (this.buildings.has(key)) { this.scene.remove(this.buildings.get(key)); this.buildings.delete(key); } } updateBuildingName(x, y, name) { const key = `${x},${y}`; const building = this.buildings.get(key); if (building) { building.userData.name = name; } } updateCursor(playerId, x, y) { if (!this.cursors.has(playerId)) { const cursor = this.createCursor(playerId, 0xff0000); this.cursors.set(playerId, cursor); this.scene.add(cursor); } const cursor = this.cursors.get(playerId); cursor.position.x = x * this.TILE_SIZE; cursor.position.z = y * this.TILE_SIZE; } removeCursor(playerId) { if (this.cursors.has(playerId)) { this.scene.remove(this.cursors.get(playerId)); this.cursors.delete(playerId); } } highlightTile(x, y) { // Remove previous highlight if (this.hoveredTile) { this.scene.remove(this.hoveredTile); this.hoveredTile = null; } // Create new highlight if (x !== null && y !== null) { this.hoveredTile = this.createTile(x, y, 0xFFFF00); this.scene.add(this.hoveredTile); } } screenToWorld(screenX, screenY) { const rect = this.canvas.getBoundingClientRect(); const x = ((screenX - rect.left) / rect.width) * 2 - 1; const y = -((screenY - rect.top) / rect.height) * 2 + 1; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera); // Raycast to ground plane const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const intersection = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, intersection); return { x: Math.floor(intersection.x / this.TILE_SIZE), y: Math.floor(intersection.z / this.TILE_SIZE) }; } moveCamera(dx, dy) { this.cameraPos.x += dx; this.cameraPos.z += dy; this.updateCameraPosition(); } zoomCamera(delta) { // Adjust the zoom property. A positive delta (scroll up) increases zoom. this.cameraZoom += delta; // Clamp the zoom level to a reasonable range this.cameraZoom = Math.max(0.5, Math.min(2.5, this.cameraZoom)); // Apply the zoom to the camera and update its projection matrix this.camera.zoom = this.cameraZoom; this.camera.updateProjectionMatrix(); } updateCameraPosition() { this.camera.position.set( this.cameraPos.x, this.cameraPos.y, this.cameraPos.z ); this.camera.lookAt(this.cameraPos.x, 0, this.cameraPos.z - 50); } startRenderLoop() { const animate = () => { requestAnimationFrame(animate); this.renderer.render(this.scene, this.camera); }; animate(); } onResize() { const aspect = window.innerWidth / window.innerHeight; this.camera.left = -40 * aspect; this.camera.right = 40 * aspect; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); } }