From e9585f8570f9ac7213c9e8595607c0d1c3b1ff5a Mon Sep 17 00:00:00 2001 From: retoor Date: Sun, 5 Oct 2025 03:31:45 +0200 Subject: [PATCH] update --- server/main.py | 10 +- static/js/GameRenderer.js | 304 ++++++++++++++++++++++++++++++++++---- static/js/InputHandler.js | 8 +- 3 files changed, 287 insertions(+), 35 deletions(-) diff --git a/server/main.py b/server/main.py index 8480c27..cddc009 100644 --- a/server/main.py +++ b/server/main.py @@ -113,9 +113,17 @@ async def websocket_endpoint(websocket: WebSocket, nickname: str): player_id = await ws_manager.get_player_id(websocket) if player_id: player = game_state.get_or_create_player(nickname, player_id) + + # Trigger economy update on login to ensure player stats are current + # This is especially important after server restarts + logger.info(f"Player {player_id} connected, triggering economy update to sync stats.") + await trigger_economy_update_and_save() + + # Send updated player data after economy sync + updated_player = game_state.players.get(player_id, player) await websocket.send_json({ "type": "init", - "player": player.to_dict(), + "player": updated_player.to_dict(), "game_state": game_state.get_state() }) while True: diff --git a/static/js/GameRenderer.js b/static/js/GameRenderer.js index 521f367..5b37cc6 100644 --- a/static/js/GameRenderer.js +++ b/static/js/GameRenderer.js @@ -15,7 +15,7 @@ export class GameRenderer { // Map of building labels this.hoveredTile = null; - this.cameraPos = { x: 0, y: 50, z: 50 }; + this.cameraPos = { x: 0, y: 0, z: 0 }; this.cameraZoom = 1; // Re-introduced for proper orthographic zoom this.TILE_SIZE = 2; @@ -33,7 +33,8 @@ export class GameRenderer { this.camera = new THREE.OrthographicCamera( -40, 40, 30, -30, 0.1, 1000 ); - this.camera.position.set(0, 50, 50); + // Transport Tycoon style isometric view + this.camera.position.set(50, 50, 50); this.camera.lookAt(0, 0, 0); // Create renderer @@ -43,17 +44,36 @@ export class GameRenderer { }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadows // Add lights - const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); + const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); // Reduced ambient for better shadows this.scene.add(ambientLight); - const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); - directionalLight.position.set(10, 20, 10); + const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); // Increased intensity + directionalLight.position.set(30, 40, 30); // Higher position for better shadows directionalLight.castShadow = true; + + // 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 + this.scene.add(directionalLight); // Create ground this.createGround(); + // Create grid + this.createGrid(); // Handle window resize window.addEventListener('resize', () => this.onResize()); } @@ -67,6 +87,49 @@ export class GameRenderer { this.scene.add(ground); } + 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); + } + 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({ @@ -87,27 +150,36 @@ export class GameRenderer { // Get player color for accents const playerColor = this.getPlayerColor(owner_id); + // Determine building orientation based on adjacent roads + const orientation = this.getBuildingOrientation(x, y, type); + // 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 }; + // Add foundation/base for all non-road buildings + if (type !== 'road') { + const foundation = this.createTileFoundation(); + buildingGroup.add(foundation); + } + // Create building based on type let buildingMesh; if (type.includes('house')) { - buildingMesh = this.createHouse(type, playerColor); + buildingMesh = this.createHouse(type, playerColor, orientation); } else if (type.includes('shop') || type === 'supermarket' || type === 'mall') { - buildingMesh = this.createCommercialBuilding(type, playerColor); + buildingMesh = this.createCommercialBuilding(type, playerColor, orientation); } else if (type.includes('factory')) { - buildingMesh = this.createFactory(type, playerColor); + buildingMesh = this.createFactory(type, playerColor, orientation); } else if (type === 'road') { buildingMesh = this.createRoad(); } else if (type === 'park' || type === 'plaza') { buildingMesh = this.createPark(type, playerColor); } else if (type === 'town_hall') { - buildingMesh = this.createTownHall(playerColor); + buildingMesh = this.createTownHall(playerColor, orientation); } else if (type === 'power_plant') { - buildingMesh = this.createPowerPlant(playerColor); + buildingMesh = this.createPowerPlant(playerColor, orientation); } else { // Fallback to simple building buildingMesh = this.createSimpleBuilding(2, 0x808080); @@ -126,6 +198,78 @@ export class GameRenderer { return new THREE.Color(0xff6b6b); // Default reddish color } + 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; + } + createSimpleBuilding(height, color) { const geometry = new THREE.BoxGeometry( this.TILE_SIZE - 0.2, @@ -140,7 +284,7 @@ export class GameRenderer { return building; } - createHouse(type, playerColor) { + createHouse(type, playerColor, orientation = 0) { const group = new THREE.Group(); // Determine house size and height @@ -179,11 +323,29 @@ export class GameRenderer { roof.castShadow = true; group.add(roof); - // Door + // Door - position based on orientation (always faces road) 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); + + // 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; + } group.add(door); // Windows with player color frames @@ -211,9 +373,25 @@ export class GameRenderer { 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)); + // 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; + } // Chimney for medium and large houses if (type !== 'small_house') { @@ -228,7 +406,7 @@ export class GameRenderer { return group; } - createCommercialBuilding(type, playerColor) { + createCommercialBuilding(type, playerColor, orientation = 0) { const group = new THREE.Group(); let width = 1.6; @@ -255,14 +433,11 @@ export class GameRenderer { building.receiveShadow = true; group.add(building); - // Storefront sign - player color + // Storefront elements - position based on orientation 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, @@ -270,14 +445,46 @@ export class GameRenderer { 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); + + // 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); group.add(door); // Air conditioning units on roof for larger buildings @@ -296,7 +503,7 @@ export class GameRenderer { return group; } - createFactory(type, playerColor) { + createFactory(type, playerColor, orientation = 0) { const group = new THREE.Group(); let width = 1.6; @@ -373,6 +580,7 @@ export class GameRenderer { const material = new THREE.MeshLambertMaterial({ color: 0x2F4F4F }); const road = new THREE.Mesh(geometry, material); road.position.y = 0.05; + road.castShadow = true; road.receiveShadow = true; return road; @@ -442,7 +650,7 @@ export class GameRenderer { return group; } - createTownHall(playerColor) { + createTownHall(playerColor, orientation = 0) { const group = new THREE.Group(); // Main building @@ -499,7 +707,7 @@ export class GameRenderer { return group; } - createPowerPlant(playerColor) { + createPowerPlant(playerColor, orientation = 0) { const group = new THREE.Group(); // Main building @@ -595,6 +803,35 @@ export class GameRenderer { const building = this.createBuilding(buildingData); this.buildings.set(key, building); this.scene.add(building); + + // 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); + } + } } removeBuilding(x, y) { @@ -683,10 +920,17 @@ export class GameRenderer { } updateCameraPosition() { + // Transport Tycoon style isometric positioning this.camera.position.set( - this.cameraPos.x, this.cameraPos.y, this.cameraPos.z + this.cameraPos.x + 50, + 50, + this.cameraPos.z + 50 + ); + this.camera.lookAt( + this.cameraPos.x, + 0, + this.cameraPos.z ); - this.camera.lookAt(this.cameraPos.x, 0, this.cameraPos.z - 50); } startRenderLoop() { diff --git a/static/js/InputHandler.js b/static/js/InputHandler.js index ba22d2a..7625dc6 100644 --- a/static/js/InputHandler.js +++ b/static/js/InputHandler.js @@ -119,19 +119,19 @@ export class InputHandler { switch (event.key) { case 'ArrowUp': - this.app.renderer.moveCamera(s, -s); // Move diagonally to pan straight up + this.app.renderer.moveCamera(0, -s); // Move straight up in world coordinates moved = true; break; case 'ArrowDown': - this.app.renderer.moveCamera(-s, s); // Move diagonally to pan straight down + this.app.renderer.moveCamera(0, s); // Move straight down in world coordinates moved = true; break; case 'ArrowLeft': - this.app.renderer.moveCamera(-s, -s); // Move diagonally to pan straight left + this.app.renderer.moveCamera(-s, 0); // Move straight left in world coordinates moved = true; break; case 'ArrowRight': - this.app.renderer.moveCamera(s, s); // Move diagonally to pan straight right + this.app.renderer.moveCamera(s, 0); // Move straight right in world coordinates moved = true; break; }