diff --git a/server/main.py b/server/main.py index 668f19a..e429a3d 100644 --- a/server/main.py +++ b/server/main.py @@ -18,38 +18,50 @@ ws_manager = WebSocketManager(game_state) economy_engine = EconomyEngine(game_state) database = Database() +# --- HYBRID MODEL RE-IMPLEMENTED --- +last_economy_tick_time = time.time() TICK_INTERVAL = 10 # seconds -# --- FIX: Reverting to a simple, reliable 10-second loop --- -async def economy_loop(): - """A simple loop that runs the economy tick every 10 seconds.""" - while True: - await asyncio.sleep(TICK_INTERVAL) - - logger.info("Triggering scheduled economy tick.") - start_time = time.perf_counter() +async def trigger_economy_update_and_save(): + """Triggers an economy tick, broadcasts updates concurrently, saves, and resets the timer.""" + global last_economy_tick_time + + logger.debug("Triggering full economy update and save cycle...") + start_time = time.perf_counter() - # 1. Process one economy tick - economy_engine.tick() + economy_engine.tick() + + update_tasks = [] + for player_id, player in game_state.players.items(): + if player_id in ws_manager.active_connections: + task = ws_manager.send_to_player(player_id, { + "type": "player_stats_update", + "player": player.to_dict() + }) + update_tasks.append(task) + + if update_tasks: + await asyncio.gather(*update_tasks) + + database.save_game_state(game_state) + + # Reset the global tick timer after any update + last_economy_tick_time = time.time() + + duration = time.perf_counter() - start_time + logger.info(f"Full economy update cycle completed in {duration:.4f} seconds.") + +async def economy_loop(): + """Runs periodically to check if a passive economy update is needed.""" + global last_economy_tick_time + while True: + # Check if 10 seconds have passed since the last tick (from any source) + if time.time() - last_economy_tick_time > TICK_INTERVAL: + logger.info("Triggering timed economy update for idle players.") + await trigger_economy_update_and_save() - # 2. Broadcast updates concurrently - update_tasks = [] - for player_id, player in game_state.players.items(): - if player_id in ws_manager.active_connections: - task = ws_manager.send_to_player(player_id, { - "type": "player_stats_update", - "player": player.to_dict() - }) - update_tasks.append(task) - - if update_tasks: - await asyncio.gather(*update_tasks) - - # 3. Save the new state - database.save_game_state(game_state) - - duration = time.perf_counter() - start_time - logger.info(f"Economy tick cycle completed in {duration:.4f} seconds.") + # Check frequently for responsiveness + await asyncio.sleep(1) @asynccontextmanager async def lifespan(app: FastAPI): @@ -58,9 +70,9 @@ async def lifespan(app: FastAPI): database.init_db() game_state.load_state(database.load_game_state()) - # Start the simple economy loop + # Start the hybrid economy loop task = asyncio.create_task(economy_loop()) - logger.info(f"Economy loop started with a {TICK_INTERVAL}-second interval.") + logger.info("Hybrid economy loop started.") yield @@ -117,8 +129,9 @@ async def handle_message(websocket: WebSocket, data: dict): result = game_state.place_building(player_id, data["building_type"], data["x"], data["y"]) if result["success"]: await ws_manager.broadcast({"type": "building_placed", "building": result["building"]}) - # --- CHANGE: Action now only saves, economy is handled by the loop --- - database.save_game_state(game_state) + # --- CHANGE: Trigger an INSTANT economy update on financial action --- + logger.info(f"Player {player_id} action triggered immediate economy update.") + await trigger_economy_update_and_save() else: await websocket.send_json({"type": "error", "message": result["error"]}) @@ -126,8 +139,9 @@ async def handle_message(websocket: WebSocket, data: dict): result = game_state.remove_building(player_id, data["x"], data["y"]) if result["success"]: await ws_manager.broadcast({"type": "building_removed", "x": data["x"], "y": data["y"]}) - # --- CHANGE: Action now only saves, economy is handled by the loop --- - database.save_game_state(game_state) + # --- CHANGE: Trigger an INSTANT economy update on financial action --- + logger.info(f"Player {player_id} action triggered immediate economy update.") + await trigger_economy_update_and_save() else: await websocket.send_json({"type": "error", "message": result["error"]}) diff --git a/static/css/style.css b/static/css/style.css index d721f29..0f2a60b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -63,7 +63,7 @@ stats-display { /* Building Toolbox */ building-toolbox { position: absolute; - left: 10px; + right: 10px; top: 120px; background: var(--bg-medium); border: 2px solid var(--border-color); diff --git a/static/js/components/BuildingToolbox.js b/static/js/components/BuildingToolbox.js index 8647c74..351a923 100644 --- a/static/js/components/BuildingToolbox.js +++ b/static/js/components/BuildingToolbox.js @@ -6,6 +6,7 @@ class BuildingToolbox extends HTMLElement { constructor() { super(); this.app = null; + this.buildingItems = []; // A cache for the building DOM elements this.buildings = [ { type: 'small_house', name: 'Small House', cost: 5000, income: -50, pop: 10 }, { type: 'medium_house', name: 'Medium House', cost: 12000, income: -120, pop: 25 }, @@ -24,18 +25,30 @@ class BuildingToolbox extends HTMLElement { } connectedCallback() { - this.render(); + // 1. Initial full render happens only once + this.innerHTML = this.renderHTML(); + + // 2. Cache the DOM elements for efficient future updates + this.buildingItems = this.querySelectorAll('.building-item'); + + // 3. Add click handlers only once + this.addClickHandlers(); } attributeChangedCallback() { - this.render(); + // When player stats change, run the lightweight update function + // instead of a full re-render. + if (this.buildingItems.length > 0) { + this.updateItemStates(); + } } - - render() { + + renderHTML() { + // This function generates the initial HTML string. const money = parseInt(this.getAttribute('player-money') || '0'); const population = parseInt(this.getAttribute('player-population') || '0'); - - this.innerHTML = ` + + return `
Buildings
@@ -44,7 +57,6 @@ class BuildingToolbox extends HTMLElement { const canAfford = money >= building.cost; const meetsReq = !building.req || population >= building.req; const enabled = canAfford && meetsReq; - return `
${building.name} @@ -74,12 +85,15 @@ class BuildingToolbox extends HTMLElement { }).join('')}
`; - - // Add click handlers - this.querySelectorAll('.building-item').forEach(item => { + } + + addClickHandlers() { + this.buildingItems.forEach(item => { item.addEventListener('click', () => { const type = item.dataset.type; const building = this.buildings.find(b => b.type === type); + const money = parseInt(this.getAttribute('player-money') || '0'); + const population = parseInt(this.getAttribute('player-population') || '0'); if (money >= building.cost && (!building.req || population >= building.req)) { if (this.app) { @@ -89,6 +103,26 @@ class BuildingToolbox extends HTMLElement { }); }); } + + updateItemStates() { + // This lightweight function only updates styles, preserving the DOM and scroll position. + const money = parseInt(this.getAttribute('player-money') || '0'); + const population = parseInt(this.getAttribute('player-population') || '0'); + + this.buildingItems.forEach(item => { + const type = item.dataset.type; + const building = this.buildings.find(b => b.type === type); + if (!building) return; + + const canAfford = money >= building.cost; + const meetsReq = !building.req || population >= building.req; + const enabled = canAfford && meetsReq; + + // Directly update only the styles that change + item.style.opacity = enabled ? '1' : '0.5'; + item.style.cursor = enabled ? 'pointer' : 'not-allowed'; + }); + } } customElements.define('building-toolbox', BuildingToolbox);