273 lines
8.6 KiB
JavaScript
273 lines
8.6 KiB
JavaScript
|
|
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);
|
||
|
|
}
|
||
|
|
}
|