2025-10-04 20:40:44 +02:00
|
|
|
export class GameRenderer {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.scene = null;
|
|
|
|
|
this.camera = null;
|
|
|
|
|
this.renderer = null;
|
|
|
|
|
this.canvas = null;
|
|
|
|
|
|
2025-10-05 00:08:32 +02:00
|
|
|
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
|
2025-10-04 20:40:44 +02:00
|
|
|
|
|
|
|
|
this.hoveredTile = null;
|
|
|
|
|
this.cameraPos = { x: 0, y: 50, z: 50 };
|
2025-10-05 00:18:06 +02:00
|
|
|
this.cameraZoom = 1; // Re-introduced for proper orthographic zoom
|
2025-10-04 20:40:44 +02:00
|
|
|
|
|
|
|
|
this.TILE_SIZE = 2;
|
|
|
|
|
this.VIEW_DISTANCE = 50;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
this.canvas = document.getElementById('gameCanvas');
|
|
|
|
|
// Create scene
|
|
|
|
|
this.scene = new THREE.Scene();
|
2025-10-05 00:08:32 +02:00
|
|
|
this.scene.background = new THREE.Color(0x87CEEB);
|
|
|
|
|
// Sky blue
|
2025-10-04 20:40:44 +02:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
2025-10-05 02:53:26 +02:00
|
|
|
// 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;
|
2025-10-04 20:40:44 +02:00
|
|
|
if (type.includes('house')) {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createHouse(type, playerColor);
|
2025-10-04 20:40:44 +02:00
|
|
|
} else if (type.includes('shop') || type === 'supermarket' || type === 'mall') {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createCommercialBuilding(type, playerColor);
|
2025-10-04 20:40:44 +02:00
|
|
|
} else if (type.includes('factory')) {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createFactory(type, playerColor);
|
2025-10-04 20:40:44 +02:00
|
|
|
} else if (type === 'road') {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createRoad();
|
2025-10-04 20:40:44 +02:00
|
|
|
} else if (type === 'park' || type === 'plaza') {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createPark(type, playerColor);
|
2025-10-04 20:40:44 +02:00
|
|
|
} else if (type === 'town_hall') {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createTownHall(playerColor);
|
2025-10-04 20:40:44 +02:00
|
|
|
} else if (type === 'power_plant') {
|
2025-10-05 02:53:26 +02:00
|
|
|
buildingMesh = this.createPowerPlant(playerColor);
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to simple building
|
|
|
|
|
buildingMesh = this.createSimpleBuilding(2, 0x808080);
|
2025-10-04 20:40:44 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 02:53:26 +02:00
|
|
|
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) {
|
2025-10-04 20:40:44 +02:00
|
|
|
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);
|
2025-10-05 02:53:26 +02:00
|
|
|
building.position.y = height / 2;
|
2025-10-04 20:40:44 +02:00
|
|
|
building.castShadow = true;
|
|
|
|
|
building.receiveShadow = true;
|
|
|
|
|
return building;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 02:53:26 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 20:40:44 +02:00
|
|
|
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) {
|
2025-10-05 02:53:26 +02:00
|
|
|
// Store game state for player color access
|
|
|
|
|
this.gameState = gameState;
|
|
|
|
|
|
2025-10-04 20:40:44 +02:00
|
|
|
// 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) {
|
2025-10-05 00:18:06 +02:00
|
|
|
// 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();
|
2025-10-04 20:40:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateCameraPosition() {
|
|
|
|
|
this.camera.position.set(
|
2025-10-05 00:18:06 +02:00
|
|
|
this.cameraPos.x, this.cameraPos.y, this.cameraPos.z
|
2025-10-04 20:40:44 +02:00
|
|
|
);
|
2025-10-05 00:08:32 +02:00
|
|
|
this.camera.lookAt(this.cameraPos.x, 0, this.cameraPos.z - 50);
|
2025-10-04 20:40:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|