952 lines
36 KiB
JavaScript
Raw Normal View History

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;
2025-10-05 03:31:45 +02:00
this.cameraPos = { x: 0, y: 0, z: 0 };
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
);
2025-10-05 03:31:45 +02:00
// Transport Tycoon style isometric view
this.camera.position.set(50, 50, 50);
2025-10-04 20:40:44 +02:00
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;
2025-10-05 03:31:45 +02:00
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadows
2025-10-04 20:40:44 +02:00
// Add lights
2025-10-05 03:31:45 +02:00
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); // Reduced ambient for better shadows
2025-10-04 20:40:44 +02:00
this.scene.add(ambientLight);
2025-10-05 03:31:45 +02:00
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); // Increased intensity
directionalLight.position.set(30, 40, 30); // Higher position for better shadows
2025-10-04 20:40:44 +02:00
directionalLight.castShadow = true;
2025-10-05 03:31:45 +02:00
// Configure shadow properties for better quality
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 200;
// Set shadow camera bounds to cover the play area
const shadowDistance = 50;
directionalLight.shadow.camera.left = -shadowDistance;
directionalLight.shadow.camera.right = shadowDistance;
directionalLight.shadow.camera.top = shadowDistance;
directionalLight.shadow.camera.bottom = -shadowDistance;
directionalLight.shadow.bias = -0.0001; // Reduce shadow acne
2025-10-04 20:40:44 +02:00
this.scene.add(directionalLight);
// Create ground
this.createGround();
2025-10-05 03:31:45 +02:00
// Create grid
this.createGrid();
2025-10-04 20:40:44 +02:00
// 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);
}
2025-10-05 03:31:45 +02:00
createGrid() {
const gridGroup = new THREE.Group();
const gridSize = 200; // Grid extends from -100 to +100
const divisions = gridSize / this.TILE_SIZE; // Number of divisions
// Create grid lines material - subtle gray
const gridMaterial = new THREE.LineBasicMaterial({
color: 0x666666,
transparent: true,
opacity: 0.3
});
// Create vertical lines (parallel to Z axis)
// Offset by half tile size so grid squares are centered on buildings
for (let i = -divisions/2; i <= divisions/2; i++) {
const geometry = new THREE.BufferGeometry();
const x = (i * this.TILE_SIZE) - (this.TILE_SIZE / 2);
const vertices = new Float32Array([
x, 0.02, -gridSize/2, // Start point
x, 0.02, gridSize/2 // End point
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const line = new THREE.Line(geometry, gridMaterial);
gridGroup.add(line);
}
// Create horizontal lines (parallel to X axis)
// Offset by half tile size so grid squares are centered on buildings
for (let i = -divisions/2; i <= divisions/2; i++) {
const geometry = new THREE.BufferGeometry();
const z = (i * this.TILE_SIZE) - (this.TILE_SIZE / 2);
const vertices = new Float32Array([
-gridSize/2, 0.02, z, // Start point
gridSize/2, 0.02, z // End point
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const line = new THREE.Line(geometry, gridMaterial);
gridGroup.add(line);
}
this.scene.add(gridGroup);
}
2025-10-04 20:40:44 +02:00
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);
2025-10-05 03:31:45 +02:00
// Determine building orientation based on adjacent roads
const orientation = this.getBuildingOrientation(x, y, type);
2025-10-05 02:53:26 +02:00
// 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 };
2025-10-05 03:31:45 +02:00
// Add foundation/base for all non-road buildings
if (type !== 'road') {
const foundation = this.createTileFoundation();
buildingGroup.add(foundation);
}
2025-10-05 02:53:26 +02:00
// Create building based on type
let buildingMesh;
2025-10-04 20:40:44 +02:00
if (type.includes('house')) {
2025-10-05 03:31:45 +02:00
buildingMesh = this.createHouse(type, playerColor, orientation);
2025-10-04 20:40:44 +02:00
} else if (type.includes('shop') || type === 'supermarket' || type === 'mall') {
2025-10-05 03:31:45 +02:00
buildingMesh = this.createCommercialBuilding(type, playerColor, orientation);
2025-10-04 20:40:44 +02:00
} else if (type.includes('factory')) {
2025-10-05 03:31:45 +02:00
buildingMesh = this.createFactory(type, playerColor, orientation);
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 03:31:45 +02:00
buildingMesh = this.createTownHall(playerColor, orientation);
2025-10-04 20:40:44 +02:00
} else if (type === 'power_plant') {
2025-10-05 03:31:45 +02:00
buildingMesh = this.createPowerPlant(playerColor, orientation);
2025-10-05 02:53:26 +02:00
} 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
}
2025-10-05 03:31:45 +02:00
getBuildingOrientation(x, y, buildingType) {
// Roads and parks don't need orientation
if (buildingType === 'road' || buildingType === 'park' || buildingType === 'plaza') {
return 0;
}
// Check adjacent tiles for roads
const adjacentRoads = this.getAdjacentRoads(x, y);
// If no roads, face south (default)
if (adjacentRoads.length === 0) {
return 0; // 0 = south, 90 = west, 180 = north, 270 = east
}
// Priority order: south, east, north, west
// Buildings prefer to face south, then east, etc.
const priorities = ['south', 'east', 'north', 'west'];
for (const direction of priorities) {
if (adjacentRoads.includes(direction)) {
switch (direction) {
case 'south': return 0; // Face south (positive Z)
case 'east': return 270; // Face east (positive X)
case 'north': return 180; // Face north (negative Z)
case 'west': return 90; // Face west (negative X)
}
}
}
return 0; // Default to south
}
getAdjacentRoads(x, y) {
const roads = [];
if (!this.gameState || !this.gameState.buildings) return roads;
const directions = [
{ dir: 'north', dx: 0, dy: -1 },
{ dir: 'south', dx: 0, dy: 1 },
{ dir: 'east', dx: 1, dy: 0 },
{ dir: 'west', dx: -1, dy: 0 }
];
for (const { dir, dx, dy } of directions) {
const checkX = x + dx;
const checkY = y + dy;
const key = `${checkX},${checkY}`;
const building = this.gameState.buildings[key];
if (building && building.type === 'road') {
roads.push(dir);
}
}
return roads;
}
createTileFoundation() {
// Create a full tile-sized foundation with road color
const geometry = new THREE.BoxGeometry(
this.TILE_SIZE,
0.05,
this.TILE_SIZE
);
const material = new THREE.MeshLambertMaterial({ color: 0x2F4F4F }); // Same as road color
const foundation = new THREE.Mesh(geometry, material);
foundation.position.y = 0.025; // Half the foundation height
foundation.castShadow = true;
foundation.receiveShadow = true;
return foundation;
}
2025-10-05 02:53:26 +02:00
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 03:31:45 +02:00
createHouse(type, playerColor, orientation = 0) {
2025-10-05 02:53:26 +02:00
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);
2025-10-05 03:31:45 +02:00
// Door - position based on orientation (always faces road)
2025-10-05 02:53:26 +02:00
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);
2025-10-05 03:31:45 +02:00
// Position door based on orientation
switch (orientation) {
case 0: // Face south (positive Z)
door.position.set(0, 0.3, houseDepth / 2 + 0.02);
break;
case 90: // Face west (negative X)
door.position.set(-houseWidth / 2 - 0.02, 0.3, 0);
door.rotation.y = Math.PI / 2;
break;
case 180: // Face north (negative Z)
door.position.set(0, 0.3, -houseDepth / 2 - 0.02);
door.rotation.y = Math.PI;
break;
case 270: // Face east (positive X)
door.position.set(houseWidth / 2 + 0.02, 0.3, 0);
door.rotation.y = -Math.PI / 2;
break;
}
2025-10-05 02:53:26 +02:00
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;
};
2025-10-05 03:31:45 +02:00
// Add windows - position based on orientation (front-facing)
switch (orientation) {
case 0: // Face south (positive Z)
group.add(createWindow(-houseWidth/3, wallHeight/2, houseDepth/2 + 0.02));
group.add(createWindow(houseWidth/3, wallHeight/2, houseDepth/2 + 0.02));
break;
case 90: // Face west (negative X)
group.add(createWindow(-houseWidth/2 - 0.02, wallHeight/2, -houseDepth/3));
group.add(createWindow(-houseWidth/2 - 0.02, wallHeight/2, houseDepth/3));
break;
case 180: // Face north (negative Z)
group.add(createWindow(-houseWidth/3, wallHeight/2, -houseDepth/2 - 0.02));
group.add(createWindow(houseWidth/3, wallHeight/2, -houseDepth/2 - 0.02));
break;
case 270: // Face east (positive X)
group.add(createWindow(houseWidth/2 + 0.02, wallHeight/2, -houseDepth/3));
group.add(createWindow(houseWidth/2 + 0.02, wallHeight/2, houseDepth/3));
break;
}
2025-10-05 02:53:26 +02:00
// 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;
}
2025-10-05 03:31:45 +02:00
createCommercialBuilding(type, playerColor, orientation = 0) {
2025-10-05 02:53:26 +02:00
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);
2025-10-05 03:31:45 +02:00
// Storefront elements - position based on orientation
2025-10-05 02:53:26 +02:00
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);
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);
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);
2025-10-05 03:31:45 +02:00
// Position storefront based on orientation
switch (orientation) {
case 0: // Face south (positive Z)
sign.position.set(0, height * 0.8, depth / 2 + 0.02);
windows.position.set(0, height * 0.3, depth / 2 + 0.01);
door.position.set(0, 0.4, depth / 2 + 0.02);
break;
case 90: // Face west (negative X)
sign.position.set(-width / 2 - 0.02, height * 0.8, 0);
sign.rotation.y = Math.PI / 2;
windows.position.set(-width / 2 - 0.01, height * 0.3, 0);
windows.rotation.y = Math.PI / 2;
door.position.set(-width / 2 - 0.02, 0.4, 0);
door.rotation.y = Math.PI / 2;
break;
case 180: // Face north (negative Z)
sign.position.set(0, height * 0.8, -depth / 2 - 0.02);
sign.rotation.y = Math.PI;
windows.position.set(0, height * 0.3, -depth / 2 - 0.01);
windows.rotation.y = Math.PI;
door.position.set(0, 0.4, -depth / 2 - 0.02);
door.rotation.y = Math.PI;
break;
case 270: // Face east (positive X)
sign.position.set(width / 2 + 0.02, height * 0.8, 0);
sign.rotation.y = -Math.PI / 2;
windows.position.set(width / 2 + 0.01, height * 0.3, 0);
windows.rotation.y = -Math.PI / 2;
door.position.set(width / 2 + 0.02, 0.4, 0);
door.rotation.y = -Math.PI / 2;
break;
}
group.add(sign);
group.add(windows);
2025-10-05 02:53:26 +02:00
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;
}
2025-10-05 03:31:45 +02:00
createFactory(type, playerColor, orientation = 0) {
2025-10-05 02:53:26 +02:00
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;
2025-10-05 03:31:45 +02:00
road.castShadow = true;
2025-10-05 02:53:26 +02:00
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;
}
2025-10-05 03:31:45 +02:00
createTownHall(playerColor, orientation = 0) {
2025-10-05 02:53:26 +02:00
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;
}
2025-10-05 03:31:45 +02:00
createPowerPlant(playerColor, orientation = 0) {
2025-10-05 02:53:26 +02:00
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);
2025-10-05 03:31:45 +02:00
// If this is a road, update adjacent building orientations
if (buildingData.type === 'road') {
this.updateAdjacentBuildingOrientations(buildingData.x, buildingData.y);
}
}
updateAdjacentBuildingOrientations(roadX, roadY) {
if (!this.gameState || !this.gameState.buildings) return;
const directions = [
{ dx: 0, dy: -1 }, // North
{ dx: 0, dy: 1 }, // South
{ dx: 1, dy: 0 }, // East
{ dx: -1, dy: 0 } // West
];
for (const { dx, dy } of directions) {
const checkX = roadX + dx;
const checkY = roadY + dy;
const key = `${checkX},${checkY}`;
const buildingData = this.gameState.buildings[key];
if (buildingData && buildingData.type !== 'road') {
// Rebuild this building with new orientation
this.removeBuilding(checkX, checkY);
this.addBuilding(buildingData);
}
}
2025-10-04 20:40:44 +02:00
}
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() {
2025-10-05 03:31:45 +02:00
// Transport Tycoon style isometric positioning
2025-10-04 20:40:44 +02:00
this.camera.position.set(
2025-10-05 03:31:45 +02:00
this.cameraPos.x + 50,
50,
this.cameraPos.z + 50
);
this.camera.lookAt(
this.cameraPos.x,
0,
this.cameraPos.z
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);
}
}