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; 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 building height and color based on type let height = 1; let color = 0x808080; if (type.includes('house')) { height = type === 'small_house' ? 2 : type === 'medium_house' ? 3 : 4; color = 0xD2691E; } else if (type.includes('shop') || type === 'supermarket' || type === 'mall') { height = 3; color = 0x4169E1; } else if (type.includes('factory')) { height = 5; color = 0x696969; } else if (type === 'road') { height = 0.1; color = 0x2F4F4F; } else if (type === 'park' || type === 'plaza') { height = 0.5; color = 0x32CD32; } else if (type === 'town_hall') { height = 6; color = 0xFFD700; } else if (type === 'power_plant') { height = 8; color = 0xFF4500; } // Create building mesh 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.set( x * this.TILE_SIZE, height / 2, y * this.TILE_SIZE ); building.castShadow = true; building.receiveShadow = true; building.userData = { x, y, owner_id, type, name }; return building; } 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) { // 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) { this.cameraZoom = Math.max(0.5, Math.min(2, this.cameraZoom + delta)); this.updateCameraPosition(); } updateCameraPosition() { this.camera.position.set( this.cameraPos.x, this.cameraPos.y * this.cameraZoom, this.cameraPos.z * this.cameraZoom ); this.camera.lookAt(this.cameraPos.x, 0, 0); } 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); } }