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 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) {
// 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);
}
}