Files
web-adventure/.idea/copilotDiffState.xml
T
2026-05-29 11:11:44 +02:00

27 lines
150 KiB
XML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CopilotDiffPersistence">
<option name="pendingDiffs">
<map>
<entry key="$PROJECT_DIR$/backend.py">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/backend.py" />
<option name="originalContent" value="import os&#10;import json&#10;import asyncio&#10;from datetime import datetime, timedelta&#10;from typing import Dict, List, Set&#10;from dataclasses import dataclass, field&#10;import random&#10;import hashlib&#10;from fastapi import FastAPI, WebSocket, HTTPException, Request&#10;from fastapi.middleware.cors import CORSMiddleware&#10;&#10;# Configuration&#10;GAME_DURATION = 30 * 60 # 30 minutes in seconds&#10;GRID_SIZE = 500 # Grid size in tiles&#10;PLAYER_START_SPAWN_RANGE = 50&#10;BOSS_SPAWN_DISTANCE = 150&#10;ACTION_POINTS_MAX = 20&#10;ACTION_POINTS_REGEN_INTERVAL = 60 # 1 minute&#10;MONSTER_SPAWN_RATE = 0.1 # Probability per game tick&#10;MAX_MONSTERS = 50&#10;BOSS_HEALTH_BASE = 1000&#10;MIN_LEVEL_FOR_BOSS = 10&#10;BOSS_STRUCTURE_BONUS_MULTIPLIER = 0.1&#10;&#10;# Data Models&#10;@dataclass&#10;class Position:&#10; x: float&#10; y: float&#10; z: float = 0&#10;&#10; def to_dict(self):&#10; return {&quot;x&quot;: self.x, &quot;y&quot;: self.y, &quot;z&quot;: self.z}&#10;&#10; @classmethod&#10; def from_dict(cls, d):&#10; return cls(x=d[&quot;x&quot;], y=d[&quot;y&quot;], z=d.get(&quot;z&quot;, 0))&#10;&#10;@dataclass&#10;class Player:&#10; id: str&#10; username: str&#10; color: str&#10; level: int = 1&#10; exp: int = 0&#10; position: Position = field(default_factory=lambda: Position(0, 0, 0))&#10; health: int = 100&#10; max_health: int = 100&#10; action_points: int = ACTION_POINTS_MAX&#10; max_action_points: int = ACTION_POINTS_MAX&#10; last_action_regen: float = 0&#10; inventory: Dict[str, int] = field(default_factory=lambda: {&quot;wood&quot;: 0, &quot;stone&quot;: 0})&#10; attack: int = 5&#10; defense: int = 2&#10; movement_capacity: int = 1&#10; gathering_capacity: int = 10&#10; active: bool = True&#10; session_id: str = &quot;&quot;&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;username&quot;: self.username,&#10; &quot;color&quot;: self.color,&#10; &quot;level&quot;: self.level,&#10; &quot;exp&quot;: self.exp,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;health&quot;: self.health,&#10; &quot;max_health&quot;: self.max_health,&#10; &quot;action_points&quot;: self.action_points,&#10; &quot;max_action_points&quot;: self.max_action_points,&#10; &quot;inventory&quot;: self.inventory,&#10; &quot;attack&quot;: self.attack,&#10; &quot;defense&quot;: self.defense,&#10; &quot;movement_capacity&quot;: self.movement_capacity,&#10; &quot;gathering_capacity&quot;: self.gathering_capacity,&#10; &quot;active&quot;: self.active,&#10; }&#10;&#10;@dataclass&#10;class Monster:&#10; id: str&#10; position: Position&#10; health: int&#10; max_health: int&#10; level: int&#10; attack: int&#10; is_boss: bool = False&#10; boss_progress: float = 0.0 # Percentage of damage done&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;health&quot;: self.health,&#10; &quot;max_health&quot;: self.max_health,&#10; &quot;level&quot;: self.level,&#10; &quot;attack&quot;: self.attack,&#10; &quot;is_boss&quot;: self.is_boss,&#10; &quot;boss_progress&quot;: self.boss_progress,&#10; }&#10;&#10;@dataclass&#10;class Structure:&#10; id: str&#10; position: Position&#10; structure_type: str # &quot;house&quot;, &quot;farm&quot;, &quot;guard_tower&quot;&#10; owner_id: str&#10; health: int&#10; bonuses: Dict = field(default_factory=dict)&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;structure_type&quot;: self.structure_type,&#10; &quot;owner_id&quot;: self.owner_id,&#10; &quot;health&quot;: self.health,&#10; &quot;bonuses&quot;: self.bonuses,&#10; }&#10;&#10;@dataclass&#10;class Resource:&#10; id: str&#10; position: Position&#10; resource_type: str # &quot;tree&quot;, &quot;mountain&quot;&#10; amount: int&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;resource_type&quot;: self.resource_type,&#10; &quot;amount&quot;: self.amount,&#10; }&#10;&#10;class GameWorld:&#10; def __init__(self):&#10; self.players: Dict[str, Player] = {}&#10; self.monsters: Dict[str, Monster] = {}&#10; self.structures: Dict[str, Structure] = {}&#10; self.resources: Dict[str, Resource] = {}&#10; self.boss: Monster = None&#10; self.game_start_time: float = 0&#10; self.is_game_active: bool = False&#10; self.connected_sessions: Dict[str, str] = {} # session_id -&gt; player_id&#10; self.generation = 0&#10; self._init_resources()&#10;&#10; def _init_resources(self):&#10; &quot;&quot;&quot;Initialize static resources on the map&quot;&quot;&quot;&#10; for _ in range(50):&#10; resource_id = f&quot;resource_{len(self.resources)}&quot;&#10; x = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; y = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; resource_type = random.choice([&quot;tree&quot;, &quot;mountain&quot;])&#10; self.resources[resource_id] = Resource(&#10; id=resource_id,&#10; position=Position(x, y, 0),&#10; resource_type=resource_type,&#10; amount=random.randint(50, 200),&#10; )&#10;&#10; def start_game(self):&#10; &quot;&quot;&quot;Start or restart the game&quot;&quot;&quot;&#10; self.game_start_time = datetime.now().timestamp()&#10; self.is_game_active = True&#10; self.boss = None&#10; self.monsters.clear()&#10; self.generation += 1&#10; # Keep players but reset their state&#10; for player in self.players.values():&#10; if not player.active:&#10; continue&#10; player.level = 1&#10; player.exp = 0&#10; player.health = player.max_health&#10; player.action_points = ACTION_POINTS_MAX&#10; player.inventory = {&quot;wood&quot;: 0, &quot;stone&quot;: 0}&#10; player.position = Position(&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; 0,&#10; )&#10;&#10; def get_elapsed_time(self) -&gt; float:&#10; if not self.is_game_active:&#10; return 0&#10; return datetime.now().timestamp() - self.game_start_time&#10;&#10; def check_game_over(self) -&gt; bool:&#10; if not self.is_game_active:&#10; return False&#10; elapsed = self.get_elapsed_time()&#10; if elapsed &gt;= GAME_DURATION:&#10; self.is_game_active = False&#10; return True&#10; return False&#10;&#10; def update_tick(self):&#10; &quot;&quot;&quot;Called periodically to update game state&quot;&quot;&quot;&#10; current_time = datetime.now().timestamp()&#10;&#10; # Spawn boss if conditions are met&#10; if self.boss is None and len(self.players) &gt; 0:&#10; active_players = [p for p in self.players.values() if p.active]&#10; if active_players and any(p.level &gt;= MIN_LEVEL_FOR_BOSS for p in active_players):&#10; self._spawn_boss()&#10;&#10; # Spawn monsters&#10; if random.random() &lt; MONSTER_SPAWN_RATE and len(self.monsters) &lt; MAX_MONSTERS:&#10; self._spawn_monster()&#10;&#10; # Regenerate player action points&#10; for player in self.players.values():&#10; if not player.active:&#10; continue&#10; if current_time - player.last_action_regen &gt;= ACTION_POINTS_REGEN_INTERVAL:&#10; regen_amount = 1&#10; nearby_bonuses = self._get_nearby_structure_bonuses(player.position)&#10; if &quot;action_regen&quot; in nearby_bonuses:&#10; regen_amount = nearby_bonuses[&quot;action_regen&quot;]&#10; player.action_points = min(&#10; player.max_action_points,&#10; player.action_points + regen_amount,&#10; )&#10; player.last_action_regen = current_time&#10;&#10; def _spawn_monster(self):&#10; &quot;&quot;&quot;Spawn a random monster on the map&quot;&quot;&quot;&#10; monster_id = f&quot;monster_{len(self.monsters)}_{self.generation}&quot;&#10; level = random.randint(1, 5)&#10; x = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; y = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; health = 20 + level * 10&#10; self.monsters[monster_id] = Monster(&#10; id=monster_id,&#10; position=Position(x, y, 0),&#10; health=health,&#10; max_health=health,&#10; level=level,&#10; attack=3 + level,&#10; is_boss=False,&#10; )&#10;&#10; def _spawn_boss(self):&#10; &quot;&quot;&quot;Spawn the boss monster&quot;&quot;&quot;&#10; boss_id = f&quot;boss_{self.generation}&quot;&#10; active_players = [p for p in self.players.values() if p.active]&#10; avg_level = sum(p.level for p in active_players) / len(active_players)&#10; health = int(BOSS_HEALTH_BASE + avg_level * 500)&#10;&#10; # Spawn boss at a distance from players&#10; if active_players:&#10; player_pos = active_players[0].position&#10; angle = random.random() * 2 * 3.14159&#10; x = player_pos.x + BOSS_SPAWN_DISTANCE * (angle ** 0.5) * (1 if random.random() &gt; 0.5 else -1)&#10; y = player_pos.y + BOSS_SPAWN_DISTANCE * (1 - angle ** 0.5) * (1 if random.random() &gt; 0.5 else -1)&#10; else:&#10; x = y = 0&#10;&#10; self.boss = Monster(&#10; id=boss_id,&#10; position=Position(x, y, 0),&#10; health=health,&#10; max_health=health,&#10; level=int(avg_level) + 5,&#10; attack=15 + int(avg_level),&#10; is_boss=True,&#10; )&#10;&#10; def _get_nearby_structure_bonuses(self, position: Position, radius: int = 10) -&gt; Dict:&#10; bonuses = {}&#10; for structure in self.structures.values():&#10; dist = ((structure.position.x - position.x) ** 2 + (structure.position.y - position.y) ** 2) ** 0.5&#10; if dist &lt;= radius:&#10; for key, value in structure.bonuses.items():&#10; bonuses[key] = bonuses.get(key, 0) + value&#10; return bonuses&#10;&#10;# Use plain dicts instead of Pydantic models&#10;&#10;# Global game state&#10;game_world = GameWorld()&#10;print(&quot;DEBUG: GameWorld initialized&quot;)&#10;&#10;app = FastAPI()&#10;print(&quot;DEBUG: FastAPI app created&quot;)&#10;&#10;# CORS&#10;app.add_middleware(&#10; CORSMiddleware,&#10; allow_origins=[&quot;*&quot;],&#10; allow_credentials=True,&#10; allow_methods=[&quot;*&quot;],&#10; allow_headers=[&quot;*&quot;],&#10;)&#10;&#10;# ==================== API ENDPOINTS ====================&#10;&#10;@app.post(&quot;/api/login&quot;)&#10;async def login(request: Request):&#10; &quot;&quot;&quot;Login user and create/get player&quot;&quot;&quot;&#10; print(&quot;===== LOGIN START =====&quot;, flush=True)&#10; try:&#10; print(&quot;[1] Parsing JSON...&quot;, flush=True)&#10; body = await request.json()&#10; print(f&quot;[2] Got body: {body}&quot;, flush=True)&#10; except Exception as e:&#10; print(f&quot;[X] Failed to parse JSON: {e}&quot;, flush=True)&#10; raise HTTPException(status_code=400, detail=&quot;Invalid JSON&quot;)&#10; &#10; print(&quot;[3] Extracting fields...&quot;, flush=True)&#10; username = body.get(&quot;username&quot;, &quot;&quot;).strip()&#10; color = body.get(&quot;color&quot;, &quot;&quot;)&#10; print(f&quot;[4] Got username={username}, color={color}&quot;, flush=True)&#10; &#10; if len(username) &lt; 1 or len(username) &gt; 30:&#10; print(&quot;[5a] Invalid username length&quot;, flush=True)&#10; raise HTTPException(status_code=400, detail=&quot;Invalid username length&quot;)&#10;&#10; print(&quot;[5b] Generating player ID...&quot;, flush=True)&#10; player_id = hashlib.md5(f&quot;{username}_{game_world.generation}&quot;.encode()).hexdigest()[:12]&#10; print(f&quot;[6] Player_id={player_id}&quot;, flush=True)&#10;&#10; print(&quot;[7] Checking if player exists...&quot;, flush=True)&#10; if player_id not in game_world.players:&#10; print(&quot;[8] Creating new player...&quot;, flush=True)&#10; player = Player(&#10; id=player_id,&#10; username=username,&#10; color=color,&#10; position=Position(&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; 0,&#10; ),&#10; )&#10; print(&quot;[9] Adding player to world...&quot;, flush=True)&#10; game_world.players[player_id] = player&#10; print(&quot;[10] Player added&quot;, flush=True)&#10;&#10; print(&quot;[11] Getting player from world...&quot;, flush=True)&#10; player = game_world.players[player_id]&#10; player.active = True&#10; print(&quot;[12] Creating session...&quot;, flush=True)&#10; session_id = hashlib.md5(f&quot;{player_id}_{datetime.now().timestamp()}&quot;.encode()).hexdigest()[:16]&#10; player.session_id = session_id&#10; game_world.connected_sessions[session_id] = player_id&#10; print(&quot;[13] Session created&quot;, flush=True)&#10;&#10; print(&quot;[14] Converting player to dict...&quot;, flush=True)&#10; player_dict = player.to_dict()&#10; print(f&quot;[15] Player dict has {len(player_dict)} keys&quot;, flush=True)&#10; &#10; print(&quot;[16] Building response...&quot;, flush=True)&#10; response = {&#10; &quot;player_id&quot;: player_id,&#10; &quot;session_id&quot;: session_id,&#10; &quot;player&quot;: player_dict,&#10; &quot;game_active&quot;: game_world.is_game_active,&#10; &quot;time_remaining&quot;: max(0, GAME_DURATION - game_world.get_elapsed_time()),&#10; }&#10; print(&quot;[17] Response built, returning...&quot;, flush=True)&#10; return response&#10;&#10;@app.get(&quot;/api/game/state&quot;)&#10;async def get_game_state():&#10; &quot;&quot;&quot;Get full game state&quot;&quot;&quot;&#10; return {&#10; &quot;grid_size&quot;: GRID_SIZE,&#10; &quot;players&quot;: {pid: p.to_dict() for pid, p in game_world.players.items()},&#10; &quot;monsters&quot;: {mid: m.to_dict() for mid, m in game_world.monsters.items()},&#10; &quot;boss&quot;: game_world.boss.to_dict() if game_world.boss else None,&#10; &quot;structures&quot;: {sid: s.to_dict() for sid, s in game_world.structures.items()},&#10; &quot;resources&quot;: {rid: r.to_dict() for rid, r in game_world.resources.items()},&#10; &quot;game_active&quot;: game_world.is_game_active,&#10; &quot;time_remaining&quot;: max(0, GAME_DURATION - game_world.get_elapsed_time()),&#10; }&#10;&#10;@app.post(&quot;/api/player/{player_id}/move&quot;)&#10;async def move_player(player_id: str, request: Request):&#10; &quot;&quot;&quot;Move player&quot;&quot;&quot;&#10; if player_id not in game_world.players:&#10; raise HTTPException(status_code=404, detail=&quot;Player not found&quot;)&#10;&#10; body = await request.json()&#10; dx = body.get(&quot;dx&quot;, 0)&#10; dy = body.get(&quot;dy&quot;, 0)&#10;&#10; player = game_world.players[player_id]&#10; if not player.active:&#10; raise HTTPException(status_code=400, detail=&quot;Player not active&quot;)&#10;&#10; if player.action_points &lt; 1:&#10; raise HTTPException(status_code=400, detail=&quot;Insufficient action points&quot;)&#10;&#10; # Calculate distance&#10; distance = (dx ** 2 + dy ** 2) ** 0.5&#10; if distance &gt; player.movement_capacity:&#10; raise HTTPException(status_code=400, detail=&quot;Movement exceeds capacity&quot;)&#10;&#10; player.position.x += dx&#10; player.position.y += dy&#10; player.position.x = max(-GRID_SIZE // 2, min(GRID_SIZE // 2, player.position.x))&#10; player.position.y = max(-GRID_SIZE // 2, min(GRID_SIZE // 2, player.position.y))&#10; player.action_points -= 1&#10;&#10; return {&quot;position&quot;: player.position.to_dict(), &quot;action_points&quot;: player.action_points}&#10;&#10;@app.post(&quot;/api/player/{player_id}/action&quot;)&#10;async def player_action(player_id: str, request: Request):&#10; &quot;&quot;&quot;Player performs an action&quot;&quot;&quot;&#10; if player_id not in game_world.players:&#10; raise HTTPException(status_code=404, detail=&quot;Player not found&quot;)&#10;&#10; body = await request.json()&#10; player = game_world.players[player_id]&#10; if player.action_points &lt; 1:&#10; raise HTTPException(status_code=400, detail=&quot;Insufficient action points&quot;)&#10;&#10; action_type = body.get(&quot;action_type&quot;)&#10; if action_type == &quot;attack&quot;:&#10; result = _handle_attack(player, body.get(&quot;target_id&quot;))&#10; elif action_type == &quot;gather&quot;:&#10; result = _handle_gather(player, body.get(&quot;target_id&quot;))&#10; elif action_type == &quot;build&quot;:&#10; result = _handle_build(player, body.get(&quot;tx&quot;), body.get(&quot;ty&quot;), body.get(&quot;structure_type&quot;))&#10; else:&#10; return {&quot;action&quot;: action_type, &quot;success&quot;: False, &quot;reason&quot;: &quot;Unknown action&quot;}&#10;&#10; # Only consume AP if the action actually succeeded&#10; if result.get(&quot;success&quot;):&#10; player.action_points -= 1&#10;&#10; return result&#10;&#10;def _handle_attack(player: Player, target_id: str) -&gt; Dict:&#10; &quot;&quot;&quot;Handle player attack&quot;&quot;&quot;&#10; if target_id in game_world.monsters:&#10; monster = game_world.monsters[target_id]&#10; dist = ((monster.position.x - player.position.x) ** 2 + (monster.position.y - player.position.y) ** 2) ** 0.5&#10;&#10; if dist &gt; 5:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Target too far&quot;}&#10;&#10; damage = max(1, player.attack + random.randint(-2, 2) - monster.defense)&#10; monster.health -= damage&#10;&#10; if monster.health &lt;= 0:&#10; del game_world.monsters[target_id]&#10; player.exp += monster.level * 10&#10; player.level = 1 + int(player.exp / 100)&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;exp&quot;: monster.level * 10, &quot;monster_killed&quot;: True}&#10;&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;monster_health_remaining&quot;: monster.health}&#10;&#10; elif game_world.boss and target_id == game_world.boss.id:&#10; monster = game_world.boss&#10; dist = ((monster.position.x - player.position.x) ** 2 + (monster.position.y - player.position.y) ** 2) ** 0.5&#10;&#10; if dist &gt; 5:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Boss too far&quot;}&#10;&#10; damage = max(1, player.attack + random.randint(-2, 2) - monster.defense)&#10; monster.health -= damage&#10; old_progress = monster.boss_progress&#10; monster.boss_progress = (monster.max_health - monster.health) / monster.max_health * 100&#10;&#10; if monster.health &lt;= 0:&#10; # Victory!&#10; for p in game_world.players.values():&#10; if p.active:&#10; p.level += 5&#10; p.exp += 500&#10; game_world.boss = None&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;boss_killed&quot;: True}&#10;&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;boss_health_remaining&quot;: monster.health}&#10;&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Target not found&quot;}&#10;&#10;def _handle_gather(player: Player, target_id: str) -&gt; Dict:&#10; &quot;&quot;&quot;Gather from all resources within GATHER_RADIUS of the player (target_id is ignored).&quot;&quot;&quot;&#10; GATHER_RADIUS = 5&#10; gathered_total = {&quot;wood&quot;: 0, &quot;stone&quot;: 0}&#10; depleted = []&#10; remaining_capacity = player.gathering_capacity&#10;&#10; for rid, resource in list(game_world.resources.items()):&#10; if remaining_capacity &lt;= 0:&#10; break&#10; dist = ((resource.position.x - player.position.x) ** 2 +&#10; (resource.position.y - player.position.y) ** 2) ** 0.5&#10; if dist &lt;= GATHER_RADIUS:&#10; amount = min(remaining_capacity, resource.amount)&#10; resource.amount -= amount&#10; gathered_total[resource.resource_type] = gathered_total.get(resource.resource_type, 0) + amount&#10; remaining_capacity -= amount&#10; if resource.amount &lt;= 0:&#10; depleted.append(rid)&#10;&#10; for rid in depleted:&#10; del game_world.resources[rid]&#10;&#10; total_gathered = sum(gathered_total.values())&#10; if total_gathered == 0:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;No resources within reach (radius 5)&quot;}&#10;&#10; for rtype, amt in gathered_total.items():&#10; player.inventory[rtype] = player.inventory.get(rtype, 0) + amt&#10;&#10; return {&quot;success&quot;: True, &quot;gathered&quot;: gathered_total, &quot;inventory&quot;: player.inventory}&#10;&#10;def _handle_build(player: Player, tx: float, ty: float, structure_type: str) -&gt; Dict:&#10; &quot;&quot;&quot;Handle structure building&quot;&quot;&quot;&#10; if structure_type not in [&quot;house&quot;, &quot;farm&quot;, &quot;guard_tower&quot;]:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Invalid structure type&quot;}&#10;&#10; costs = {&quot;house&quot;: {&quot;wood&quot;: 20, &quot;stone&quot;: 10}, &quot;farm&quot;: {&quot;wood&quot;: 15, &quot;stone&quot;: 5}, &quot;guard_tower&quot;: {&quot;wood&quot;: 30, &quot;stone&quot;: 20}}&#10; cost = costs[structure_type]&#10;&#10; for material, amount in cost.items():&#10; if player.inventory.get(material, 0) &lt; amount:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: f&quot;Insufficient {material}&quot;}&#10;&#10; # Deduct cost&#10; for material, amount in cost.items():&#10; player.inventory[material] -= amount&#10;&#10; # Create structure&#10; structure_id = f&quot;struct_{len(game_world.structures)}&quot;&#10; bonuses = {}&#10; if structure_type == &quot;farm&quot;:&#10; bonuses[&quot;action_regen&quot;] = 2&#10; elif structure_type == &quot;guard_tower&quot;:&#10; bonuses[&quot;defense&quot;] = 2&#10; elif structure_type == &quot;house&quot;:&#10; bonuses[&quot;max_health&quot;] = 20&#10;&#10; game_world.structures[structure_id] = Structure(&#10; id=structure_id,&#10; position=Position(tx, ty, 0),&#10; structure_type=structure_type,&#10; owner_id=player.id,&#10; health=100,&#10; bonuses=bonuses,&#10; )&#10;&#10; return {&quot;success&quot;: True, &quot;structure_id&quot;: structure_id, &quot;inventory&quot;: player.inventory}&#10;&#10;@app.websocket(&quot;/ws/{player_id}&quot;)&#10;async def websocket_endpoint(websocket: WebSocket, player_id: str):&#10; &quot;&quot;&quot;WebSocket for real-time game updates&quot;&quot;&quot;&#10; if player_id not in game_world.players:&#10; await websocket.close(code=4004, reason=&quot;Player not found&quot;)&#10; return&#10;&#10; player = game_world.players[player_id]&#10; await websocket.accept()&#10;&#10; try:&#10; while True:&#10; # Send game state updates every 100ms&#10; await asyncio.sleep(0.1)&#10; game_world.update_tick()&#10;&#10; if game_world.check_game_over():&#10; await websocket.send_json({&quot;type&quot;: &quot;game_over&quot;, &quot;time_remaining&quot;: 0})&#10; break&#10;&#10; state = {&#10; &quot;type&quot;: &quot;state_update&quot;,&#10; &quot;players&quot;: {pid: p.to_dict() for pid, p in game_world.players.items()},&#10; &quot;monsters&quot;: {mid: m.to_dict() for mid, m in game_world.monsters.items()},&#10; &quot;boss&quot;: game_world.boss.to_dict() if game_world.boss else None,&#10; &quot;structures&quot;: {sid: s.to_dict() for sid, s in game_world.structures.items()},&#10; &quot;resources&quot;: {rid: r.to_dict() for rid, r in game_world.resources.items()},&#10; &quot;time_remaining&quot;: max(0, GAME_DURATION - game_world.get_elapsed_time()),&#10; &quot;elapsed_time&quot;: game_world.get_elapsed_time(),&#10; }&#10; await websocket.send_json(state)&#10; except Exception as e:&#10; print(f&quot;WebSocket error: {e}&quot;)&#10; finally:&#10; player.active = False&#10;&#10;@app.post(&quot;/api/game/start&quot;)&#10;async def start_game():&#10; &quot;&quot;&quot;Start a new game&quot;&quot;&quot;&#10; game_world.start_game()&#10; return {&quot;game_active&quot;: True, &quot;time_remaining&quot;: GAME_DURATION}&#10;&#10;@app.get(&quot;/api/health&quot;)&#10;async def health():&#10; &quot;&quot;&quot;Health check&quot;&quot;&quot;&#10; return {&quot;status&quot;: &quot;ok&quot;}&#10;&#10;&#10;" />
<option name="updatedContent" value="import os&#10;import json&#10;import asyncio&#10;from datetime import datetime, timedelta&#10;from typing import Dict, List, Set&#10;from dataclasses import dataclass, field&#10;import random&#10;import hashlib&#10;from fastapi import FastAPI, WebSocket, HTTPException, Request&#10;from fastapi.middleware.cors import CORSMiddleware&#10;&#10;# Configuration&#10;DEBUG_MODE = os.getenv(&quot;DEBUG&quot;, &quot;0&quot;).strip().lower() in {&quot;1&quot;, &quot;true&quot;, &quot;yes&quot;, &quot;on&quot;}&#10;&#10;GAME_DURATION = 30 * 60 # 30 minutes in seconds&#10;GRID_SIZE = 500 # Grid size in tiles&#10;PLAYER_START_SPAWN_RANGE = 50&#10;BOSS_SPAWN_DISTANCE = 150&#10;ACTION_POINTS_MAX = 20&#10;# In debug mode AP regenerates much faster for rapid testing.&#10;ACTION_POINTS_REGEN_INTERVAL = float(os.getenv(&quot;ACTION_REGEN_INTERVAL&quot;, &quot;1&quot; if DEBUG_MODE else &quot;60&quot;))&#10;MONSTER_SPAWN_RATE = 0.1 # Probability per game tick&#10;MAX_MONSTERS = 50&#10;BOSS_HEALTH_BASE = 1000&#10;MIN_LEVEL_FOR_BOSS = 10&#10;BOSS_STRUCTURE_BONUS_MULTIPLIER = 0.1&#10;&#10;# Data Models&#10;@dataclass&#10;class Position:&#10; x: float&#10; y: float&#10; z: float = 0&#10;&#10; def to_dict(self):&#10; return {&quot;x&quot;: self.x, &quot;y&quot;: self.y, &quot;z&quot;: self.z}&#10;&#10; @classmethod&#10; def from_dict(cls, d):&#10; return cls(x=d[&quot;x&quot;], y=d[&quot;y&quot;], z=d.get(&quot;z&quot;, 0))&#10;&#10;@dataclass&#10;class Player:&#10; id: str&#10; username: str&#10; color: str&#10; level: int = 1&#10; exp: int = 0&#10; position: Position = field(default_factory=lambda: Position(0, 0, 0))&#10; health: int = 100&#10; max_health: int = 100&#10; action_points: int = ACTION_POINTS_MAX&#10; max_action_points: int = ACTION_POINTS_MAX&#10; last_action_regen: float = 0&#10; inventory: Dict[str, int] = field(default_factory=lambda: {&quot;wood&quot;: 0, &quot;stone&quot;: 0})&#10; attack: int = 5&#10; defense: int = 2&#10; movement_capacity: int = 1&#10; gathering_capacity: int = 10&#10; active: bool = True&#10; session_id: str = &quot;&quot;&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;username&quot;: self.username,&#10; &quot;color&quot;: self.color,&#10; &quot;level&quot;: self.level,&#10; &quot;exp&quot;: self.exp,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;health&quot;: self.health,&#10; &quot;max_health&quot;: self.max_health,&#10; &quot;action_points&quot;: self.action_points,&#10; &quot;max_action_points&quot;: self.max_action_points,&#10; &quot;inventory&quot;: self.inventory,&#10; &quot;attack&quot;: self.attack,&#10; &quot;defense&quot;: self.defense,&#10; &quot;movement_capacity&quot;: self.movement_capacity,&#10; &quot;gathering_capacity&quot;: self.gathering_capacity,&#10; &quot;active&quot;: self.active,&#10; }&#10;&#10;@dataclass&#10;class Monster:&#10; id: str&#10; position: Position&#10; health: int&#10; max_health: int&#10; level: int&#10; attack: int&#10; is_boss: bool = False&#10; boss_progress: float = 0.0 # Percentage of damage done&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;health&quot;: self.health,&#10; &quot;max_health&quot;: self.max_health,&#10; &quot;level&quot;: self.level,&#10; &quot;attack&quot;: self.attack,&#10; &quot;is_boss&quot;: self.is_boss,&#10; &quot;boss_progress&quot;: self.boss_progress,&#10; }&#10;&#10;@dataclass&#10;class Structure:&#10; id: str&#10; position: Position&#10; structure_type: str # &quot;house&quot;, &quot;farm&quot;, &quot;guard_tower&quot;&#10; owner_id: str&#10; health: int&#10; bonuses: Dict = field(default_factory=dict)&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;structure_type&quot;: self.structure_type,&#10; &quot;owner_id&quot;: self.owner_id,&#10; &quot;health&quot;: self.health,&#10; &quot;bonuses&quot;: self.bonuses,&#10; }&#10;&#10;@dataclass&#10;class Resource:&#10; id: str&#10; position: Position&#10; resource_type: str # &quot;tree&quot;, &quot;mountain&quot;&#10; amount: int&#10;&#10; def to_dict(self):&#10; return {&#10; &quot;id&quot;: self.id,&#10; &quot;position&quot;: self.position.to_dict(),&#10; &quot;resource_type&quot;: self.resource_type,&#10; &quot;amount&quot;: self.amount,&#10; }&#10;&#10;class GameWorld:&#10; def __init__(self):&#10; self.players: Dict[str, Player] = {}&#10; self.monsters: Dict[str, Monster] = {}&#10; self.structures: Dict[str, Structure] = {}&#10; self.resources: Dict[str, Resource] = {}&#10; self.boss: Monster = None&#10; self.game_start_time: float = 0&#10; self.is_game_active: bool = False&#10; self.connected_sessions: Dict[str, str] = {} # session_id -&gt; player_id&#10; self.generation = 0&#10; self._init_resources()&#10;&#10; def _init_resources(self):&#10; &quot;&quot;&quot;Initialize static resources on the map&quot;&quot;&quot;&#10; for _ in range(50):&#10; resource_id = f&quot;resource_{len(self.resources)}&quot;&#10; x = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; y = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; resource_type = random.choice([&quot;tree&quot;, &quot;mountain&quot;])&#10; self.resources[resource_id] = Resource(&#10; id=resource_id,&#10; position=Position(x, y, 0),&#10; resource_type=resource_type,&#10; amount=random.randint(50, 200),&#10; )&#10;&#10; def start_game(self):&#10; &quot;&quot;&quot;Start or restart the game&quot;&quot;&quot;&#10; self.game_start_time = datetime.now().timestamp()&#10; self.is_game_active = True&#10; self.boss = None&#10; self.monsters.clear()&#10; self.generation += 1&#10; # Keep players but reset their state&#10; for player in self.players.values():&#10; if not player.active:&#10; continue&#10; player.level = 1&#10; player.exp = 0&#10; player.health = player.max_health&#10; player.action_points = ACTION_POINTS_MAX&#10; player.inventory = {&quot;wood&quot;: 0, &quot;stone&quot;: 0}&#10; player.position = Position(&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; 0,&#10; )&#10;&#10; def get_elapsed_time(self) -&gt; float:&#10; if not self.is_game_active:&#10; return 0&#10; return datetime.now().timestamp() - self.game_start_time&#10;&#10; def check_game_over(self) -&gt; bool:&#10; if not self.is_game_active:&#10; return False&#10; elapsed = self.get_elapsed_time()&#10; if elapsed &gt;= GAME_DURATION:&#10; self.is_game_active = False&#10; return True&#10; return False&#10;&#10; def update_tick(self):&#10; &quot;&quot;&quot;Called periodically to update game state&quot;&quot;&quot;&#10; current_time = datetime.now().timestamp()&#10;&#10; # Spawn boss if conditions are met&#10; if self.boss is None and len(self.players) &gt; 0:&#10; active_players = [p for p in self.players.values() if p.active]&#10; if active_players and any(p.level &gt;= MIN_LEVEL_FOR_BOSS for p in active_players):&#10; self._spawn_boss()&#10;&#10; # Spawn monsters&#10; if random.random() &lt; MONSTER_SPAWN_RATE and len(self.monsters) &lt; MAX_MONSTERS:&#10; self._spawn_monster()&#10;&#10; # Regenerate player action points&#10; for player in self.players.values():&#10; if not player.active:&#10; continue&#10; if current_time - player.last_action_regen &gt;= ACTION_POINTS_REGEN_INTERVAL:&#10; regen_amount = 1&#10; nearby_bonuses = self._get_nearby_structure_bonuses(player.position)&#10; if &quot;action_regen&quot; in nearby_bonuses:&#10; regen_amount = nearby_bonuses[&quot;action_regen&quot;]&#10; player.action_points = min(&#10; player.max_action_points,&#10; player.action_points + regen_amount,&#10; )&#10; player.last_action_regen = current_time&#10;&#10; def _spawn_monster(self):&#10; &quot;&quot;&quot;Spawn a random monster on the map&quot;&quot;&quot;&#10; monster_id = f&quot;monster_{len(self.monsters)}_{self.generation}&quot;&#10; level = random.randint(1, 5)&#10; x = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; y = random.randint(-GRID_SIZE // 2, GRID_SIZE // 2)&#10; health = 20 + level * 10&#10; self.monsters[monster_id] = Monster(&#10; id=monster_id,&#10; position=Position(x, y, 0),&#10; health=health,&#10; max_health=health,&#10; level=level,&#10; attack=3 + level,&#10; is_boss=False,&#10; )&#10;&#10; def _spawn_boss(self):&#10; &quot;&quot;&quot;Spawn the boss monster&quot;&quot;&quot;&#10; boss_id = f&quot;boss_{self.generation}&quot;&#10; active_players = [p for p in self.players.values() if p.active]&#10; avg_level = sum(p.level for p in active_players) / len(active_players)&#10; health = int(BOSS_HEALTH_BASE + avg_level * 500)&#10;&#10; # Spawn boss at a distance from players&#10; if active_players:&#10; player_pos = active_players[0].position&#10; angle = random.random() * 2 * 3.14159&#10; x = player_pos.x + BOSS_SPAWN_DISTANCE * (angle ** 0.5) * (1 if random.random() &gt; 0.5 else -1)&#10; y = player_pos.y + BOSS_SPAWN_DISTANCE * (1 - angle ** 0.5) * (1 if random.random() &gt; 0.5 else -1)&#10; else:&#10; x = y = 0&#10;&#10; self.boss = Monster(&#10; id=boss_id,&#10; position=Position(x, y, 0),&#10; health=health,&#10; max_health=health,&#10; level=int(avg_level) + 5,&#10; attack=15 + int(avg_level),&#10; is_boss=True,&#10; )&#10;&#10; def _get_nearby_structure_bonuses(self, position: Position, radius: int = 10) -&gt; Dict:&#10; bonuses = {}&#10; for structure in self.structures.values():&#10; dist = ((structure.position.x - position.x) ** 2 + (structure.position.y - position.y) ** 2) ** 0.5&#10; if dist &lt;= radius:&#10; for key, value in structure.bonuses.items():&#10; bonuses[key] = bonuses.get(key, 0) + value&#10; return bonuses&#10;&#10;# Use plain dicts instead of Pydantic models&#10;&#10;# Global game state&#10;game_world = GameWorld()&#10;print(&quot;DEBUG: GameWorld initialized&quot;)&#10;&#10;app = FastAPI()&#10;print(&quot;DEBUG: FastAPI app created&quot;)&#10;&#10;# CORS&#10;app.add_middleware(&#10; CORSMiddleware,&#10; allow_origins=[&quot;*&quot;],&#10; allow_credentials=True,&#10; allow_methods=[&quot;*&quot;],&#10; allow_headers=[&quot;*&quot;],&#10;)&#10;&#10;# ==================== API ENDPOINTS ====================&#10;&#10;@app.post(&quot;/api/login&quot;)&#10;async def login(request: Request):&#10; &quot;&quot;&quot;Login user and create/get player&quot;&quot;&quot;&#10; print(&quot;===== LOGIN START =====&quot;, flush=True)&#10; try:&#10; print(&quot;[1] Parsing JSON...&quot;, flush=True)&#10; body = await request.json()&#10; print(f&quot;[2] Got body: {body}&quot;, flush=True)&#10; except Exception as e:&#10; print(f&quot;[X] Failed to parse JSON: {e}&quot;, flush=True)&#10; raise HTTPException(status_code=400, detail=&quot;Invalid JSON&quot;)&#10; &#10; print(&quot;[3] Extracting fields...&quot;, flush=True)&#10; username = body.get(&quot;username&quot;, &quot;&quot;).strip()&#10; color = body.get(&quot;color&quot;, &quot;&quot;)&#10; print(f&quot;[4] Got username={username}, color={color}&quot;, flush=True)&#10; &#10; if len(username) &lt; 1 or len(username) &gt; 30:&#10; print(&quot;[5a] Invalid username length&quot;, flush=True)&#10; raise HTTPException(status_code=400, detail=&quot;Invalid username length&quot;)&#10;&#10; print(&quot;[5b] Generating player ID...&quot;, flush=True)&#10; player_id = hashlib.md5(f&quot;{username}_{game_world.generation}&quot;.encode()).hexdigest()[:12]&#10; print(f&quot;[6] Player_id={player_id}&quot;, flush=True)&#10;&#10; print(&quot;[7] Checking if player exists...&quot;, flush=True)&#10; if player_id not in game_world.players:&#10; print(&quot;[8] Creating new player...&quot;, flush=True)&#10; player = Player(&#10; id=player_id,&#10; username=username,&#10; color=color,&#10; position=Position(&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; random.randint(-PLAYER_START_SPAWN_RANGE, PLAYER_START_SPAWN_RANGE),&#10; 0,&#10; ),&#10; )&#10; print(&quot;[9] Adding player to world...&quot;, flush=True)&#10; game_world.players[player_id] = player&#10; print(&quot;[10] Player added&quot;, flush=True)&#10;&#10; print(&quot;[11] Getting player from world...&quot;, flush=True)&#10; player = game_world.players[player_id]&#10; player.active = True&#10; print(&quot;[12] Creating session...&quot;, flush=True)&#10; session_id = hashlib.md5(f&quot;{player_id}_{datetime.now().timestamp()}&quot;.encode()).hexdigest()[:16]&#10; player.session_id = session_id&#10; game_world.connected_sessions[session_id] = player_id&#10; print(&quot;[13] Session created&quot;, flush=True)&#10;&#10; print(&quot;[14] Converting player to dict...&quot;, flush=True)&#10; player_dict = player.to_dict()&#10; print(f&quot;[15] Player dict has {len(player_dict)} keys&quot;, flush=True)&#10; &#10; print(&quot;[16] Building response...&quot;, flush=True)&#10; response = {&#10; &quot;player_id&quot;: player_id,&#10; &quot;session_id&quot;: session_id,&#10; &quot;player&quot;: player_dict,&#10; &quot;game_active&quot;: game_world.is_game_active,&#10; &quot;time_remaining&quot;: max(0, GAME_DURATION - game_world.get_elapsed_time()),&#10; }&#10; print(&quot;[17] Response built, returning...&quot;, flush=True)&#10; return response&#10;&#10;@app.get(&quot;/api/game/state&quot;)&#10;async def get_game_state():&#10; &quot;&quot;&quot;Get full game state&quot;&quot;&quot;&#10; return {&#10; &quot;grid_size&quot;: GRID_SIZE,&#10; &quot;players&quot;: {pid: p.to_dict() for pid, p in game_world.players.items()},&#10; &quot;monsters&quot;: {mid: m.to_dict() for mid, m in game_world.monsters.items()},&#10; &quot;boss&quot;: game_world.boss.to_dict() if game_world.boss else None,&#10; &quot;structures&quot;: {sid: s.to_dict() for sid, s in game_world.structures.items()},&#10; &quot;resources&quot;: {rid: r.to_dict() for rid, r in game_world.resources.items()},&#10; &quot;game_active&quot;: game_world.is_game_active,&#10; &quot;time_remaining&quot;: max(0, GAME_DURATION - game_world.get_elapsed_time()),&#10; }&#10;&#10;@app.post(&quot;/api/player/{player_id}/move&quot;)&#10;async def move_player(player_id: str, request: Request):&#10; &quot;&quot;&quot;Move player&quot;&quot;&quot;&#10; if player_id not in game_world.players:&#10; raise HTTPException(status_code=404, detail=&quot;Player not found&quot;)&#10;&#10; body = await request.json()&#10; dx = body.get(&quot;dx&quot;, 0)&#10; dy = body.get(&quot;dy&quot;, 0)&#10;&#10; player = game_world.players[player_id]&#10; if not player.active:&#10; raise HTTPException(status_code=400, detail=&quot;Player not active&quot;)&#10;&#10; if player.action_points &lt; 1:&#10; raise HTTPException(status_code=400, detail=&quot;Insufficient action points&quot;)&#10;&#10; # Calculate distance&#10; distance = (dx ** 2 + dy ** 2) ** 0.5&#10; if distance &gt; player.movement_capacity:&#10; raise HTTPException(status_code=400, detail=&quot;Movement exceeds capacity&quot;)&#10;&#10; player.position.x += dx&#10; player.position.y += dy&#10; player.position.x = max(-GRID_SIZE // 2, min(GRID_SIZE // 2, player.position.x))&#10; player.position.y = max(-GRID_SIZE // 2, min(GRID_SIZE // 2, player.position.y))&#10; player.action_points -= 1&#10;&#10; return {&quot;position&quot;: player.position.to_dict(), &quot;action_points&quot;: player.action_points}&#10;&#10;@app.post(&quot;/api/player/{player_id}/action&quot;)&#10;async def player_action(player_id: str, request: Request):&#10; &quot;&quot;&quot;Player performs an action&quot;&quot;&quot;&#10; if player_id not in game_world.players:&#10; raise HTTPException(status_code=404, detail=&quot;Player not found&quot;)&#10;&#10; body = await request.json()&#10; player = game_world.players[player_id]&#10; if player.action_points &lt; 1:&#10; raise HTTPException(status_code=400, detail=&quot;Insufficient action points&quot;)&#10;&#10; action_type = body.get(&quot;action_type&quot;)&#10; if action_type == &quot;attack&quot;:&#10; result = _handle_attack(player, body.get(&quot;target_id&quot;))&#10; elif action_type == &quot;gather&quot;:&#10; result = _handle_gather(player, body.get(&quot;target_id&quot;))&#10; elif action_type == &quot;build&quot;:&#10; result = _handle_build(player, body.get(&quot;tx&quot;), body.get(&quot;ty&quot;), body.get(&quot;structure_type&quot;))&#10; else:&#10; return {&quot;action&quot;: action_type, &quot;success&quot;: False, &quot;reason&quot;: &quot;Unknown action&quot;}&#10;&#10; # Only consume AP if the action actually succeeded&#10; if result.get(&quot;success&quot;):&#10; player.action_points -= 1&#10;&#10; return result&#10;&#10;def _handle_attack(player: Player, target_id: str) -&gt; Dict:&#10; &quot;&quot;&quot;Handle player attack&quot;&quot;&quot;&#10; if target_id in game_world.monsters:&#10; monster = game_world.monsters[target_id]&#10; dist = ((monster.position.x - player.position.x) ** 2 + (monster.position.y - player.position.y) ** 2) ** 0.5&#10;&#10; if dist &gt; 5:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Target too far&quot;}&#10;&#10; damage = max(1, player.attack + random.randint(-2, 2) - monster.defense)&#10; monster.health -= damage&#10;&#10; if monster.health &lt;= 0:&#10; del game_world.monsters[target_id]&#10; player.exp += monster.level * 10&#10; player.level = 1 + int(player.exp / 100)&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;exp&quot;: monster.level * 10, &quot;monster_killed&quot;: True}&#10;&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;monster_health_remaining&quot;: monster.health}&#10;&#10; elif game_world.boss and target_id == game_world.boss.id:&#10; monster = game_world.boss&#10; dist = ((monster.position.x - player.position.x) ** 2 + (monster.position.y - player.position.y) ** 2) ** 0.5&#10;&#10; if dist &gt; 5:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Boss too far&quot;}&#10;&#10; damage = max(1, player.attack + random.randint(-2, 2) - monster.defense)&#10; monster.health -= damage&#10; old_progress = monster.boss_progress&#10; monster.boss_progress = (monster.max_health - monster.health) / monster.max_health * 100&#10;&#10; if monster.health &lt;= 0:&#10; # Victory!&#10; for p in game_world.players.values():&#10; if p.active:&#10; p.level += 5&#10; p.exp += 500&#10; game_world.boss = None&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;boss_killed&quot;: True}&#10;&#10; return {&quot;success&quot;: True, &quot;damage&quot;: damage, &quot;boss_health_remaining&quot;: monster.health}&#10;&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Target not found&quot;}&#10;&#10;def _handle_gather(player: Player, target_id: str) -&gt; Dict:&#10; &quot;&quot;&quot;Gather from all resources within GATHER_RADIUS of the player (target_id is ignored).&quot;&quot;&quot;&#10; GATHER_RADIUS = 5&#10; gathered_total = {&quot;wood&quot;: 0, &quot;stone&quot;: 0}&#10; depleted = []&#10; remaining_capacity = player.gathering_capacity&#10;&#10; for rid, resource in list(game_world.resources.items()):&#10; if remaining_capacity &lt;= 0:&#10; break&#10; dist = ((resource.position.x - player.position.x) ** 2 +&#10; (resource.position.y - player.position.y) ** 2) ** 0.5&#10; if dist &lt;= GATHER_RADIUS:&#10; amount = min(remaining_capacity, resource.amount)&#10; resource.amount -= amount&#10; gathered_total[resource.resource_type] = gathered_total.get(resource.resource_type, 0) + amount&#10; remaining_capacity -= amount&#10; if resource.amount &lt;= 0:&#10; depleted.append(rid)&#10;&#10; for rid in depleted:&#10; del game_world.resources[rid]&#10;&#10; total_gathered = sum(gathered_total.values())&#10; if total_gathered == 0:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;No resources within reach (radius 5)&quot;}&#10;&#10; for rtype, amt in gathered_total.items():&#10; player.inventory[rtype] = player.inventory.get(rtype, 0) + amt&#10;&#10; return {&quot;success&quot;: True, &quot;gathered&quot;: gathered_total, &quot;inventory&quot;: player.inventory}&#10;&#10;def _handle_build(player: Player, tx: float, ty: float, structure_type: str) -&gt; Dict:&#10; &quot;&quot;&quot;Handle structure building&quot;&quot;&quot;&#10; if structure_type not in [&quot;house&quot;, &quot;farm&quot;, &quot;guard_tower&quot;]:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: &quot;Invalid structure type&quot;}&#10;&#10; costs = {&quot;house&quot;: {&quot;wood&quot;: 20, &quot;stone&quot;: 10}, &quot;farm&quot;: {&quot;wood&quot;: 15, &quot;stone&quot;: 5}, &quot;guard_tower&quot;: {&quot;wood&quot;: 30, &quot;stone&quot;: 20}}&#10; cost = costs[structure_type]&#10;&#10; for material, amount in cost.items():&#10; if player.inventory.get(material, 0) &lt; amount:&#10; return {&quot;success&quot;: False, &quot;reason&quot;: f&quot;Insufficient {material}&quot;}&#10;&#10; # Deduct cost&#10; for material, amount in cost.items():&#10; player.inventory[material] -= amount&#10;&#10; # Create structure&#10; structure_id = f&quot;struct_{len(game_world.structures)}&quot;&#10; bonuses = {}&#10; if structure_type == &quot;farm&quot;:&#10; bonuses[&quot;action_regen&quot;] = 2&#10; elif structure_type == &quot;guard_tower&quot;:&#10; bonuses[&quot;defense&quot;] = 2&#10; elif structure_type == &quot;house&quot;:&#10; bonuses[&quot;max_health&quot;] = 20&#10;&#10; game_world.structures[structure_id] = Structure(&#10; id=structure_id,&#10; position=Position(tx, ty, 0),&#10; structure_type=structure_type,&#10; owner_id=player.id,&#10; health=100,&#10; bonuses=bonuses,&#10; )&#10;&#10; return {&quot;success&quot;: True, &quot;structure_id&quot;: structure_id, &quot;inventory&quot;: player.inventory}&#10;&#10;@app.websocket(&quot;/ws/{player_id}&quot;)&#10;async def websocket_endpoint(websocket: WebSocket, player_id: str):&#10; &quot;&quot;&quot;WebSocket for real-time game updates&quot;&quot;&quot;&#10; if player_id not in game_world.players:&#10; await websocket.close(code=4004, reason=&quot;Player not found&quot;)&#10; return&#10;&#10; player = game_world.players[player_id]&#10; await websocket.accept()&#10;&#10; try:&#10; while True:&#10; # Send game state updates every 100ms&#10; await asyncio.sleep(0.1)&#10; game_world.update_tick()&#10;&#10; if game_world.check_game_over():&#10; await websocket.send_json({&quot;type&quot;: &quot;game_over&quot;, &quot;time_remaining&quot;: 0})&#10; break&#10;&#10; state = {&#10; &quot;type&quot;: &quot;state_update&quot;,&#10; &quot;players&quot;: {pid: p.to_dict() for pid, p in game_world.players.items()},&#10; &quot;monsters&quot;: {mid: m.to_dict() for mid, m in game_world.monsters.items()},&#10; &quot;boss&quot;: game_world.boss.to_dict() if game_world.boss else None,&#10; &quot;structures&quot;: {sid: s.to_dict() for sid, s in game_world.structures.items()},&#10; &quot;resources&quot;: {rid: r.to_dict() for rid, r in game_world.resources.items()},&#10; &quot;time_remaining&quot;: max(0, GAME_DURATION - game_world.get_elapsed_time()),&#10; &quot;elapsed_time&quot;: game_world.get_elapsed_time(),&#10; }&#10; await websocket.send_json(state)&#10; except Exception as e:&#10; print(f&quot;WebSocket error: {e}&quot;)&#10; finally:&#10; player.active = False&#10;&#10;@app.post(&quot;/api/game/start&quot;)&#10;async def start_game():&#10; &quot;&quot;&quot;Start a new game&quot;&quot;&quot;&#10; game_world.start_game()&#10; return {&quot;game_active&quot;: True, &quot;time_remaining&quot;: GAME_DURATION}&#10;&#10;@app.get(&quot;/api/health&quot;)&#10;async def health():&#10; &quot;&quot;&quot;Health check&quot;&quot;&quot;&#10; return {&quot;status&quot;: &quot;ok&quot;}&#10;&#10;&#10;" />
</PendingDiffInfo>
</value>
</entry>
<entry key="$PROJECT_DIR$/index.html">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/index.html" />
<option name="originalContent" value="&lt;!DOCTYPE html&gt;&#10;&lt;html lang=&quot;en&quot;&gt;&#10;&lt;head&gt;&#10; &lt;meta charset=&quot;UTF-8&quot;&gt;&#10; &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;&#10; &lt;title&gt;Web Adventure - Community RPG&lt;/title&gt;&#10; &lt;style&gt;&#10; * {&#10; margin: 0;&#10; padding: 0;&#10; box-sizing: border-box;&#10; }&#10;&#10; body {&#10; font-family: 'Arial', sans-serif;&#10; background: #1a1a1a;&#10; color: #fff;&#10; overflow: hidden;&#10; }&#10;&#10; .login-screen {&#10; display: flex;&#10; align-items: center;&#10; justify-content: center;&#10; min-height: 100vh;&#10; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&#10; }&#10;&#10; .login-form {&#10; background: #2a2a2a;&#10; padding: 40px;&#10; border-radius: 10px;&#10; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);&#10; text-align: center;&#10; }&#10;&#10; .login-form h1 {&#10; margin-bottom: 30px;&#10; color: #667eea;&#10; font-size: 2.5em;&#10; }&#10;&#10; .login-form input,&#10; .login-form select {&#10; display: block;&#10; width: 100%;&#10; padding: 12px;&#10; margin: 15px 0;&#10; border: none;&#10; border-radius: 5px;&#10; background: #3a3a3a;&#10; color: #fff;&#10; font-size: 1em;&#10; }&#10;&#10; .login-form input::placeholder {&#10; color: #999;&#10; }&#10;&#10; .color-selector {&#10; display: flex;&#10; gap: 10px;&#10; margin: 20px 0;&#10; flex-wrap: wrap;&#10; }&#10;&#10; .color-option {&#10; width: 40px;&#10; height: 40px;&#10; border-radius: 5px;&#10; cursor: pointer;&#10; border: 3px solid transparent;&#10; transition: all 0.3s;&#10; }&#10;&#10; .color-option:hover {&#10; transform: scale(1.1);&#10; }&#10;&#10; .color-option.selected {&#10; border-color: #fff;&#10; transform: scale(1.2);&#10; }&#10;&#10; .login-form button {&#10; width: 100%;&#10; padding: 12px;&#10; background: #667eea;&#10; color: white;&#10; border: none;&#10; border-radius: 5px;&#10; font-size: 1.1em;&#10; cursor: pointer;&#10; margin-top: 20px;&#10; transition: background 0.3s;&#10; }&#10;&#10; .login-form button:hover {&#10; background: #764ba2;&#10; }&#10;&#10; .game-screen {&#10; display: none;&#10; width: 100%;&#10; height: 100vh;&#10; position: relative;&#10; }&#10;&#10; .game-screen.active {&#10; display: flex;&#10; }&#10;&#10; #gameCanvas {&#10; flex: 1;&#10; background: #000;&#10; }&#10;&#10; .ui-panel {&#10; position: absolute;&#10; background: rgba(0, 0, 0, 0.9);&#10; color: #fff;&#10; border: 2px solid #667eea;&#10; border-radius: 5px;&#10; padding: 15px;&#10; font-size: 0.9em;&#10; font-family: monospace;&#10; }&#10;&#10; .ui-top-left {&#10; top: 10px;&#10; left: 10px;&#10; max-width: 300px;&#10; }&#10;&#10; .ui-top-right {&#10; top: 10px;&#10; right: 10px;&#10; text-align: right;&#10; max-width: 300px;&#10; }&#10;&#10; .stats {&#10; margin-bottom: 15px;&#10; }&#10;&#10; .stat-row {&#10; display: flex;&#10; justify-content: space-between;&#10; margin: 5px 0;&#10; }&#10;&#10; .stat-label {&#10; color: #aaa;&#10; }&#10;&#10; .stat-value {&#10; color: #667eea;&#10; font-weight: bold;&#10; }&#10;&#10; .stat-bar {&#10; width: 100%;&#10; height: 20px;&#10; background: #333;&#10; border-radius: 3px;&#10; margin-top: 3px;&#10; overflow: hidden;&#10; }&#10;&#10; .stat-bar-fill {&#10; height: 100%;&#10; background: #667eea;&#10; transition: width 0.3s;&#10; }&#10;&#10; .stat-bar-fill.health {&#10; background: #ff4444;&#10; }&#10;&#10; .stat-bar-fill.action {&#10; background: #44ff44;&#10; }&#10;&#10; .inventory {&#10; margin-top: 20px;&#10; border-top: 1px solid #667eea;&#10; padding-top: 10px;&#10; }&#10;&#10; .inventory-item {&#10; display: flex;&#10; justify-content: space-between;&#10; margin: 5px 0;&#10; }&#10;&#10; .bottom-panel {&#10; position: absolute;&#10; bottom: 10px;&#10; left: 10px;&#10; right: 10px;&#10; background: rgba(0, 0, 0, 0.9);&#10; border: 2px solid #667eea;&#10; border-radius: 5px;&#10; padding: 15px;&#10; }&#10;&#10; .button-group {&#10; display: grid;&#10; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));&#10; gap: 10px;&#10; }&#10;&#10; .action-button {&#10; padding: 10px;&#10; background: #667eea;&#10; color: white;&#10; border: none;&#10; border-radius: 5px;&#10; cursor: pointer;&#10; transition: all 0.3s;&#10; font-size: 0.9em;&#10; }&#10;&#10; .action-button:hover {&#10; background: #764ba2;&#10; transform: scale(1.05);&#10; }&#10;&#10; .action-button:disabled {&#10; background: #444;&#10; cursor: not-allowed;&#10; opacity: 0.5;&#10; }&#10;&#10; .status-message {&#10; margin-top: 10px;&#10; padding: 10px;&#10; background: #333;&#10; border-radius: 5px;&#10; min-height: 30px;&#10; }&#10;&#10; .boss-bar-container {&#10; position: absolute;&#10; top: 50%;&#10; left: 50%;&#10; transform: translate(-50%, -50%);&#10; background: rgba(0, 0, 0, 0.9);&#10; border: 2px solid #ff4444;&#10; border-radius: 5px;&#10; padding: 20px;&#10; text-align: center;&#10; display: none;&#10; z-index: 10;&#10; }&#10;&#10; .boss-bar-container.active {&#10; display: block;&#10; }&#10;&#10; .game-over-screen {&#10; display: none;&#10; position: fixed;&#10; top: 0;&#10; left: 0;&#10; right: 0;&#10; bottom: 0;&#10; background: rgba(0, 0, 0, 0.95);&#10; z-index: 1000;&#10; align-items: center;&#10; justify-content: center;&#10; }&#10;&#10; .game-over-screen.active {&#10; display: flex;&#10; }&#10;&#10; .game-over-content {&#10; background: rgba(102, 126, 234, 0.1);&#10; border: 2px solid #667eea;&#10; border-radius: 10px;&#10; padding: 40px;&#10; text-align: center;&#10; max-width: 500px;&#10; }&#10;&#10; .game-over-content h1 {&#10; font-size: 2.5em;&#10; margin-bottom: 20px;&#10; }&#10;&#10; .game-over-content p {&#10; margin: 10px 0;&#10; font-size: 1.1em;&#10; }&#10;&#10; .game-over-content button {&#10; margin-top: 20px;&#10; padding: 12px 30px;&#10; background: #667eea;&#10; color: white;&#10; border: none;&#10; border-radius: 5px;&#10; font-size: 1.1em;&#10; cursor: pointer;&#10; }&#10;&#10; /* Camera controls widget */&#10; .camera-controls {&#10; position: absolute;&#10; bottom: 180px;&#10; right: 10px;&#10; background: rgba(0,0,0,0.85);&#10; border: 2px solid #667eea;&#10; border-radius: 8px;&#10; padding: 10px;&#10; display: none;&#10; flex-direction: column;&#10; align-items: center;&#10; gap: 4px;&#10; z-index: 20;&#10; user-select: none;&#10; }&#10; .camera-controls.active { display: flex; }&#10; .camera-controls .cam-label {&#10; color: #667eea;&#10; font-size: 0.75em;&#10; font-weight: bold;&#10; margin-bottom: 4px;&#10; letter-spacing: 0.05em;&#10; }&#10; .cam-row { display: flex; gap: 4px; }&#10; .cam-btn {&#10; width: 36px; height: 36px;&#10; background: #667eea;&#10; color: #fff;&#10; border: none;&#10; border-radius: 5px;&#10; font-size: 1.1em;&#10; cursor: pointer;&#10; display: flex; align-items: center; justify-content: center;&#10; transition: background 0.15s;&#10; }&#10; .cam-btn:hover { background: #764ba2; }&#10; .cam-btn:active { background: #4a3a8a; transform: scale(0.95); }&#10; .cam-btn.wide { width: 78px; font-size: 0.75em; }&#10; &lt;/style&gt;&#10;&lt;/head&gt;&#10;&lt;body&gt;&#10; &lt;div class=&quot;login-screen&quot; id=&quot;loginScreen&quot;&gt;&#10; &lt;div class=&quot;login-form&quot;&gt;&#10; &lt;h1&gt; Web Adventure&lt;/h1&gt;&#10; &lt;form id=&quot;loginForm&quot;&gt;&#10; &lt;input type=&quot;text&quot; id=&quot;username&quot; placeholder=&quot;Enter your username&quot; required&gt;&#10; &lt;div style=&quot;margin: 20px 0; color: #aaa;&quot;&gt;Choose your color:&lt;/div&gt;&#10; &lt;div class=&quot;color-selector&quot; id=&quot;colorSelector&quot;&gt;&lt;/div&gt;&#10; &lt;button type=&quot;submit&quot;&gt;Enter the World&lt;/button&gt;&#10; &lt;/form&gt;&#10; &lt;p style=&quot;margin-top: 20px; color: #999; font-size: 0.9em;&quot;&gt;&#10; Welcome to Web Adventure! Log in to join the multiplayer game world.&#10; &lt;/p&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;game-screen&quot; id=&quot;gameScreen&quot;&gt;&#10; &lt;canvas id=&quot;gameCanvas&quot;&gt;&lt;/canvas&gt;&#10;&#10; &lt;div class=&quot;ui-panel ui-top-left&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Player Stats&lt;/div&gt;&#10; &lt;div class=&quot;stats&quot;&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Level:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statLevel&quot;&gt;1&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Experience:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statExp&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Health:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statHealth&quot;&gt;100/100&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-bar&quot;&gt;&#10; &lt;div class=&quot;stat-bar-fill health&quot; id=&quot;healthBar&quot; style=&quot;width: 100%&quot;&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Action Points:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statActionPoints&quot;&gt;20&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-bar&quot;&gt;&#10; &lt;div class=&quot;stat-bar-fill action&quot; id=&quot;actionBar&quot; style=&quot;width: 100%&quot;&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Attack:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statAttack&quot;&gt;5&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Defense:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statDefense&quot;&gt;2&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Move Range:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statMove&quot;&gt;5&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;inventory&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Inventory&lt;/div&gt;&#10; &lt;div class=&quot;inventory-item&quot;&gt;&#10; &lt;span&gt;Wood:&lt;/span&gt;&#10; &lt;span id=&quot;invWood&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;inventory-item&quot;&gt;&#10; &lt;span&gt;Stone:&lt;/span&gt;&#10; &lt;span id=&quot;invStone&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;ui-panel ui-top-right&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Game Status&lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Time Remaining:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;timeRemaining&quot;&gt;30:00&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Players Online:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;playersOnline&quot;&gt;1&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Monsters:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;monsterCount&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;boss-bar-container&quot; id=&quot;bossBar&quot;&gt;&#10; &lt;div style=&quot;color: #ff4444; font-weight: bold; margin-bottom: 10px;&quot;&gt;⚔️ BOSS APPEARED ⚔️&lt;/div&gt;&#10; &lt;div class=&quot;stat-bar&quot;&gt;&#10; &lt;div class=&quot;stat-bar-fill&quot; id=&quot;bossHealthBar&quot; style=&quot;width: 100%; background: #ff4444;&quot;&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span id=&quot;bossHealth&quot;&gt;Loading...&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;!-- Camera orbit / zoom controls (top-right, shown after login) --&gt;&#10; &lt;div class=&quot;camera-controls&quot; id=&quot;cameraControls&quot;&gt;&#10; &lt;div class=&quot;cam-label&quot;&gt; CAMERA&lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot;&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camUp&quot; title=&quot;Tilt up&quot;&gt;▲&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot;&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camLeft&quot; title=&quot;Orbit left&quot;&gt;◀&lt;/button&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camCenter&quot; title=&quot;Re-center on player&quot;&gt;⊙&lt;/button&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camRight&quot; title=&quot;Orbit right&quot;&gt;▶&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot;&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camDown&quot; title=&quot;Tilt down&quot;&gt;▼&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot; style=&quot;margin-top:4px; gap:4px;&quot;&gt;&#10; &lt;button class=&quot;cam-btn wide&quot; id=&quot;camZoomIn&quot; title=&quot;Zoom in&quot;&gt; +&lt;/button&gt;&#10; &lt;button class=&quot;cam-btn wide&quot; id=&quot;camZoomOut&quot; title=&quot;Zoom out&quot;&gt; −&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;bottom-panel&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Controls&lt;/div&gt;&#10; &lt;div class=&quot;button-group&quot;&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveUpBtn&quot;&gt;⬆ Move&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveDownBtn&quot;&gt;⬇ Move&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveLeftBtn&quot;&gt;⬅ Move&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveRightBtn&quot;&gt;➡ Move&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;button-group&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;gatherBtn&quot;&gt; Gather&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;attackBtn&quot;&gt;⚔️ Attack&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;button-group&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;buildHouseBtn&quot;&gt; House&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;buildFarmBtn&quot;&gt; Farm&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;buildTowerBtn&quot;&gt;️ Tower&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;status-message&quot; id=&quot;statusMessage&quot;&gt;Ready for adventure!&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;game-over-screen&quot; id=&quot;gameOverScreen&quot;&gt;&#10; &lt;div class=&quot;game-over-content&quot;&gt;&#10; &lt;h1 id=&quot;gameOverTitle&quot;&gt;GAME OVER&lt;/h1&gt;&#10; &lt;p id=&quot;gameOverMessage&quot;&gt;The timer has run out!&lt;/p&gt;&#10; &lt;p id=&quot;gameOverStats&quot;&gt;&lt;/p&gt;&#10; &lt;button onclick=&quot;location.reload()&quot;&gt;Return to Login&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js&quot;&gt;&lt;/script&gt;&#10; &lt;script src=&quot;https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js&quot;&gt;&lt;/script&gt;&#10; &lt;script&gt;&#10; // ==================== GLOBAL STATE ====================&#10; let currentPlayer = null;&#10; let currentPlayerId = null;&#10; let gameState = null;&#10; let gameActive = true;&#10; let ws = null;&#10;&#10; const API_BASE = 'http://localhost:8000/api';&#10;&#10; // ==================== UTILITIES ====================&#10; function showMessage(msg) {&#10; document.getElementById('statusMessage').textContent = msg;&#10; }&#10;&#10; function updateUI() {&#10; if (!currentPlayer) return;&#10;&#10; document.getElementById('statLevel').textContent = currentPlayer.level;&#10; document.getElementById('statExp').textContent = currentPlayer.exp;&#10; document.getElementById('statHealth').textContent = `${currentPlayer.health}/${currentPlayer.max_health}`;&#10; document.getElementById('healthBar').style.width = `${(currentPlayer.health / currentPlayer.max_health) * 100}%`;&#10; document.getElementById('statActionPoints').textContent = currentPlayer.action_points;&#10; document.getElementById('actionBar').style.width = `${(currentPlayer.action_points / currentPlayer.max_action_points) * 100}%`;&#10; document.getElementById('statAttack').textContent = currentPlayer.attack;&#10; document.getElementById('statDefense').textContent = currentPlayer.defense;&#10; document.getElementById('statMove').textContent = currentPlayer.movement_capacity;&#10;&#10; document.getElementById('invWood').textContent = currentPlayer.inventory.wood || 0;&#10; document.getElementById('invStone').textContent = currentPlayer.inventory.stone || 0;&#10;&#10; if (gameState) {&#10; const activePlayerCount = Object.values(gameState.players).filter(p =&gt; p.active).length;&#10; document.getElementById('playersOnline').textContent = activePlayerCount;&#10; document.getElementById('monsterCount').textContent = Object.keys(gameState.monsters).length;&#10; }&#10; }&#10;&#10; function formatTime(seconds) {&#10; const mins = Math.floor(seconds / 60);&#10; const secs = Math.floor(seconds % 60);&#10; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;&#10; }&#10;&#10; // ==================== THREE.JS SCENE ====================&#10; let scene, camera, renderer, controls;&#10; const playerSpheres = {};&#10; const monsterCubes = {};&#10; let ossBossObject = null;&#10; const structureObjects = {};&#10; const resourceObjects = {};&#10;&#10; function initScene() {&#10; const canvas = document.getElementById('gameCanvas');&#10; scene = new THREE.Scene();&#10; scene.background = new THREE.Color(0x0a0a0a);&#10;&#10; camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 10000);&#10; camera.position.set(0, 50, 50);&#10; camera.lookAt(0, 0, 0);&#10;&#10; renderer = new THREE.WebGLRenderer({ canvas, antialias: true });&#10; renderer.setSize(canvas.clientWidth, canvas.clientHeight);&#10; renderer.shadowMap.enabled = true;&#10;&#10; // Mouse camera controls: left drag to orbit, wheel to zoom, right drag to pan.&#10; controls = new THREE.OrbitControls(camera, renderer.domElement);&#10; controls.enableDamping = true;&#10; controls.dampingFactor = 0.08;&#10; controls.target.set(0, 0, 0);&#10; controls.maxPolarAngle = Math.PI * 0.49;&#10; controls.minDistance = 10;&#10; controls.maxDistance = 200;&#10; controls.enableKeys = false; // We handle arrow keys ourselves.&#10;&#10; // Lighting&#10; const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);&#10; scene.add(ambientLight);&#10;&#10; const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);&#10; directionalLight.position.set(100, 100, 100);&#10; directionalLight.shadow.mapSize.width = 2048;&#10; directionalLight.shadow.mapSize.height = 2048;&#10; directionalLight.castShadow = true;&#10; scene.add(directionalLight);&#10;&#10; // Ground plane&#10; const groundGeometry = new THREE.PlaneGeometry(500, 500);&#10; const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x1a3a1a });&#10; const ground = new THREE.Mesh(groundGeometry, groundMaterial);&#10; ground.rotation.x = -Math.PI / 2;&#10; ground.receiveShadow = true;&#10; scene.add(ground);&#10;&#10; // Grid helper&#10; const gridHelper = new THREE.GridHelper(500, 50, 0x444444, 0x222222);&#10; gridHelper.position.y = 0.1;&#10; scene.add(gridHelper);&#10;&#10; window.addEventListener('resize', () =&gt; {&#10; camera.aspect = canvas.clientWidth / canvas.clientHeight;&#10; camera.updateProjectionMatrix();&#10; renderer.setSize(canvas.clientWidth, canvas.clientHeight);&#10; });&#10;&#10; animate();&#10; }&#10;&#10; function focusCameraOnPlayer(force = false) {&#10; if (!currentPlayer || !camera) return;&#10;&#10; const targetX = currentPlayer.position.x;&#10; const targetZ = currentPlayer.position.y;&#10;&#10; if (force) {&#10; camera.position.set(targetX + 4, 8, targetZ + 6);&#10; if (controls) {&#10; controls.target.set(targetX, 0, targetZ);&#10; controls.minDistance = 3;&#10; controls.maxDistance = 80;&#10; controls.update();&#10; } else {&#10; camera.lookAt(targetX, 0, targetZ);&#10; }&#10; return;&#10; }&#10;&#10; if (controls) {&#10; const dx = targetX - controls.target.x;&#10; const dz = targetZ - controls.target.z;&#10; controls.target.x += dx * 0.12;&#10; controls.target.z += dz * 0.12;&#10; } else {&#10; camera.position.x = targetX;&#10; camera.position.z = targetZ + 15;&#10; camera.lookAt(targetX, 0, targetZ);&#10; }&#10; }&#10;&#10; function rotateCameraBy(deltaAzimuth, deltaPolar, deltaZoom = 0) {&#10; if (!camera) return;&#10; const target = controls ? controls.target.clone() : new THREE.Vector3(0, 0, 0);&#10; const offset = camera.position.clone().sub(target);&#10; const spherical = new THREE.Spherical().setFromVector3(offset);&#10;&#10; spherical.theta -= deltaAzimuth;&#10; spherical.phi = Math.max(0.15, Math.min(Math.PI * 0.48, spherical.phi + deltaPolar));&#10; spherical.radius = Math.max(10, Math.min(200, spherical.radius + deltaZoom));&#10;&#10; offset.setFromSpherical(spherical);&#10; camera.position.copy(target).add(offset);&#10; camera.lookAt(target.x, target.y, target.z);&#10;&#10; if (controls) {&#10; controls.target.copy(target);&#10; const damp = controls.enableDamping;&#10; controls.enableDamping = false;&#10; controls.update();&#10; controls.enableDamping = damp;&#10; }&#10; }&#10;&#10; function createPlayerSphere(player) {&#10; const geometry = new THREE.SphereGeometry(0.5, 16, 16);&#10; const isCurrentPlayer = player.id === currentPlayerId;&#10; const material = new THREE.MeshPhongMaterial({&#10; color: player.color,&#10; emissive: isCurrentPlayer ? 0x222222 : 0x000000,&#10; shininess: isCurrentPlayer ? 80 : 30,&#10; });&#10; const sphere = new THREE.Mesh(geometry, material);&#10; sphere.position.set(player.position.x, 0.5, player.position.y);&#10; if (isCurrentPlayer) {&#10; sphere.scale.set(1.15, 1.15, 1.15);&#10; }&#10; sphere.castShadow = true;&#10; sphere.receiveShadow = true;&#10; scene.add(sphere);&#10;&#10; // Add label&#10; const canvas = document.createElement('canvas');&#10; const ctx = canvas.getContext('2d');&#10; canvas.width = 256;&#10; canvas.height = 128;&#10; ctx.fillStyle = 'white';&#10; ctx.font = '32px Arial';&#10; ctx.textAlign = 'center';&#10; ctx.textBaseline = 'middle';&#10; ctx.fillText(player.username, 128, 64);&#10;&#10; const texture = new THREE.CanvasTexture(canvas);&#10; const spriteMaterial = new THREE.SpriteMaterial({ map: texture });&#10; const sprite = new THREE.Sprite(spriteMaterial);&#10; sprite.scale.set(4, 2, 1);&#10; sprite.position.set(player.position.x, 2.5, player.position.y);&#10; scene.add(sprite);&#10;&#10; playerSpheres[player.id] = { mesh: sphere, sprite };&#10; }&#10;&#10; function updatePlayerSphere(player) {&#10; if (playerSpheres[player.id]) {&#10; playerSpheres[player.id].mesh.position.set(player.position.x, 0.5, player.position.y);&#10; playerSpheres[player.id].sprite.position.set(player.position.x, 2.5, player.position.y);&#10; }&#10; }&#10;&#10; function createMonsterCube(monster) {&#10; const geometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);&#10; const material = new THREE.MeshPhongMaterial({ color: 0xff4444 });&#10; const cube = new THREE.Mesh(geometry, material);&#10; cube.position.set(monster.position.x, 0.3, monster.position.y);&#10; cube.castShadow = true;&#10; scene.add(cube);&#10; monsterCubes[monster.id] = cube;&#10; }&#10;&#10; function createBossMesh(monster) {&#10; const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);&#10; const material = new THREE.MeshPhongMaterial({ color: 0xff0000 });&#10; const cube = new THREE.Mesh(geometry, material);&#10; cube.position.set(monster.position.x, 0.75, monster.position.y);&#10; cube.castShadow = true;&#10; scene.add(cube);&#10; ossBossObject = cube;&#10; }&#10;&#10; function createStructureMesh(structure) {&#10; let geometry, color;&#10; if (structure.structure_type === 'house') {&#10; geometry = new THREE.ConeGeometry(0.5, 1, 4);&#10; color = 0xc0a080;&#10; } else if (structure.structure_type === 'farm') {&#10; geometry = new THREE.ConeGeometry(0.5, 1, 4);&#10; color = 0x90ee90;&#10; } else if (structure.structure_type === 'guard_tower') {&#10; geometry = new THREE.ConeGeometry(0.35, 1.4, 4);&#10; color = 0x808080;&#10; }&#10; const material = new THREE.MeshPhongMaterial({ color });&#10; const mesh = new THREE.Mesh(geometry, material);&#10; mesh.position.set(structure.position.x, 0.5, structure.position.y);&#10; mesh.castShadow = true;&#10; scene.add(mesh);&#10; structureObjects[structure.id] = mesh;&#10; }&#10;&#10; function createResourceMesh(resource) {&#10; let mesh;&#10; if (resource.resource_type === 'tree') {&#10; const trunkGeo = new THREE.CylinderGeometry(0.12, 0.18, 0.8, 8);&#10; const trunkMat = new THREE.MeshPhongMaterial({ color: 0x8B4513 });&#10; const trunk = new THREE.Mesh(trunkGeo, trunkMat);&#10; trunk.position.y = 0.4;&#10; const foliageMat = new THREE.MeshPhongMaterial({ color: 0x228b22 });&#10; const foliage1 = new THREE.Mesh(new THREE.ConeGeometry(0.85, 1.3, 8), foliageMat);&#10; foliage1.position.y = 1.3;&#10; const foliage2 = new THREE.Mesh(new THREE.ConeGeometry(0.6, 1.0, 8), foliageMat);&#10; foliage2.position.y = 2.1;&#10; mesh = new THREE.Group();&#10; mesh.add(trunk, foliage1, foliage2);&#10; } else {&#10; const rockMat = new THREE.MeshPhongMaterial({ color: 0x8a8a8a, flatShading: true });&#10; const main = new THREE.Mesh(new THREE.DodecahedronGeometry(0.65, 0), rockMat);&#10; main.position.y = 0.5;&#10; main.rotation.y = Math.random() * Math.PI;&#10; const side1 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.38, 0), rockMat);&#10; side1.position.set(0.6, 0.25, 0.12);&#10; side1.rotation.y = Math.random() * Math.PI;&#10; const side2 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.28, 0), rockMat);&#10; side2.position.set(-0.5, 0.2, 0.25);&#10; side2.rotation.y = Math.random() * Math.PI;&#10; mesh = new THREE.Group();&#10; mesh.add(main, side1, side2);&#10; }&#10; mesh.position.set(resource.position.x, 0, resource.position.y);&#10; mesh.castShadow = true;&#10; scene.add(mesh);&#10; resourceObjects[resource.id] = mesh;&#10; }&#10;&#10; function updateGameScene() {&#10; if (!gameState) return;&#10;&#10; // Update players&#10; for (const player of Object.values(gameState.players || {})) {&#10; if (!playerSpheres[player.id]) {&#10; createPlayerSphere(player);&#10; } else {&#10; updatePlayerSphere(player);&#10; }&#10; }&#10;&#10; // Remove deleted players&#10; for (const pid in playerSpheres) {&#10; if (!(gameState.players || {})[pid]) {&#10; scene.remove(playerSpheres[pid].mesh);&#10; scene.remove(playerSpheres[pid].sprite);&#10; delete playerSpheres[pid];&#10; }&#10; }&#10;&#10; // Update monsters&#10; for (const monster of Object.values(gameState.monsters || {})) {&#10; if (!monsterCubes[monster.id]) {&#10; createMonsterCube(monster);&#10; } else {&#10; monsterCubes[monster.id].position.set(monster.position.x, 0.3, monster.position.y);&#10; }&#10; }&#10;&#10; // Remove deleted monsters&#10; for (const mid in monsterCubes) {&#10; if (!(gameState.monsters || {})[mid]) {&#10; scene.remove(monsterCubes[mid]);&#10; delete monsterCubes[mid];&#10; }&#10; }&#10;&#10; // Update boss&#10; if (gameState.boss) {&#10; if (!ossBossObject) {&#10; createBossMesh(gameState.boss);&#10; document.getElementById('bossBar').classList.add('active');&#10; } else {&#10; ossBossObject.position.set(gameState.boss.position.x, 0.75, gameState.boss.position.y);&#10; ossBossObject.rotation.y += 0.01;&#10; }&#10; document.getElementById('bossHealthBar').style.width = `${gameState.boss.boss_progress}%`;&#10; document.getElementById('bossHealth').textContent = `${gameState.boss.health} / ${gameState.boss.max_health}`;&#10; } else {&#10; if (ossBossObject) { scene.remove(ossBossObject); ossBossObject = null; }&#10; document.getElementById('bossBar').classList.remove('active');&#10; }&#10;&#10; // Update structures&#10; for (const structure of Object.values(gameState.structures || {})) {&#10; if (!structureObjects[structure.id]) createStructureMesh(structure);&#10; }&#10;&#10; // Update resources — guard against missing field&#10; const resources = gameState.resources || {};&#10; for (const resource of Object.values(resources)) {&#10; if (!resourceObjects[resource.id]) createResourceMesh(resource);&#10; }&#10; for (const rid in resourceObjects) {&#10; if (!resources[rid]) { scene.remove(resourceObjects[rid]); delete resourceObjects[rid]; }&#10; }&#10;&#10; focusCameraOnPlayer(false);&#10; }&#10;&#10; function animate() {&#10; requestAnimationFrame(animate);&#10; updateGameScene();&#10; if (controls) controls.update();&#10; renderer.render(scene, camera);&#10; }&#10;&#10; // ==================== API FUNCTIONS ====================&#10; async function login(username, color) {&#10; try {&#10; const response = await fetch(`${API_BASE}/login`, {&#10; method: 'POST',&#10; headers: { 'Content-Type': 'application/json' },&#10; body: JSON.stringify({ username, color }),&#10; });&#10; const data = await response.json();&#10; currentPlayer = data.player;&#10; currentPlayerId = data.player_id;&#10; gameActive = data.game_active;&#10;&#10; document.getElementById('loginScreen').style.display = 'none';&#10; document.getElementById('gameScreen').classList.add('active');&#10;&#10; initScene();&#10; createPlayerSphere(currentPlayer);&#10; focusCameraOnPlayer(true);&#10; connectWebSocket();&#10; setupControls();&#10;&#10; return data;&#10; } catch (error) {&#10; console.error('Login failed:', error);&#10; showMessage('Login failed. Try again.');&#10; }&#10; }&#10;&#10; function connectWebSocket() {&#10; ws = new WebSocket(`ws://localhost:8000/ws/${currentPlayerId}`);&#10;&#10; ws.onmessage = (event) =&gt; {&#10; const data = JSON.parse(event.data);&#10;&#10; if (data.type === 'state_update') {&#10; gameState = data;&#10; currentPlayer = gameState.players[currentPlayerId] || currentPlayer;&#10;&#10; document.getElementById('timeRemaining').textContent = formatTime(data.time_remaining);&#10;&#10; if (data.time_remaining &lt;= 0) {&#10; showGameOver();&#10; }&#10;&#10; updateUI();&#10; } else if (data.type === 'game_over') {&#10; showGameOver();&#10; }&#10; };&#10;&#10; ws.onerror = (error) =&gt; {&#10; console.error('WebSocket error:', error);&#10; };&#10;&#10; ws.onclose = () =&gt; {&#10; console.log('WebSocket closed');&#10; };&#10; }&#10;&#10; function showGameOver() {&#10; gameActive = false;&#10; document.getElementById('gameOverScreen').classList.add('active');&#10; document.getElementById('gameOverMessage').textContent =&#10; 'The world resets... Come back when you\'re stronger!';&#10; document.getElementById('gameOverStats').textContent =&#10; `Final Level: ${currentPlayer.level} | Final Experience: ${currentPlayer.exp}`;&#10; }&#10;&#10; async function movePlayer(dx, dy) {&#10; try {&#10; const response = await fetch(`${API_BASE}/player/${currentPlayerId}/move`, {&#10; method: 'POST',&#10; headers: { 'Content-Type': 'application/json' },&#10; body: JSON.stringify({ dx, dy }),&#10; });&#10; const data = await response.json();&#10;&#10; if (!response.ok) {&#10; showMessage(`Move failed: ${data.detail || data.reason || 'Unknown error'}`);&#10; return data;&#10; }&#10;&#10; // Apply local update immediately so movement feels responsive even before WS tick arrives.&#10; if (response.ok &amp;&amp; data.position &amp;&amp; currentPlayer) {&#10; currentPlayer.position = data.position;&#10; currentPlayer.action_points = data.action_points ?? currentPlayer.action_points;&#10; updatePlayerSphere(currentPlayer);&#10; focusCameraOnPlayer(false);&#10; updateUI();&#10; }&#10; return data;&#10; } catch (error) {&#10; console.error('Move failed:', error);&#10; showMessage('Move failed!');&#10; }&#10; }&#10;&#10; async function performAction(actionType, targetId = null, tx = null, ty = null, structureType = null) {&#10; try {&#10; const response = await fetch(`${API_BASE}/player/${currentPlayerId}/action`, {&#10; method: 'POST',&#10; headers: { 'Content-Type': 'application/json' },&#10; body: JSON.stringify({ action_type: actionType, target_id: targetId, tx, ty, structure_type: structureType }),&#10; });&#10; const data = await response.json();&#10;&#10; if (data.success) {&#10; showMessage(`Action successful: ${actionType}`);&#10; } else {&#10; showMessage(`Action failed: ${data.reason || 'Unknown error'}`);&#10; }&#10; return data;&#10; } catch (error) {&#10; console.error('Action failed:', error);&#10; showMessage('Action failed!');&#10; }&#10; }&#10;&#10; // ==================== UI CONTROLS ====================&#10; function setupControls() {&#10; document.getElementById('moveUpBtn').onclick = () =&gt; movePlayer(0, -1);&#10; document.getElementById('moveDownBtn').onclick = () =&gt; movePlayer(0, 1);&#10; document.getElementById('moveLeftBtn').onclick = () =&gt; movePlayer(-1, 0);&#10; document.getElementById('moveRightBtn').onclick = () =&gt; movePlayer( 1, 0);&#10;&#10; // Show and wire camera widget buttons.&#10; document.getElementById('cameraControls').classList.add('active');&#10;&#10; const AZ = 0.25, PL = 0.12, ZM = 8;&#10; // Support both click and held-down via a repeating interval.&#10; function bindCamBtn(id, az, pl, zm = 0) {&#10; const btn = document.getElementById(id);&#10; let interval = null;&#10; const fire = () =&gt; rotateCameraBy(az, pl, zm);&#10; btn.addEventListener('mousedown', () =&gt; { fire(); interval = setInterval(fire, 80); });&#10; btn.addEventListener('touchstart', (e) =&gt; { e.preventDefault(); fire(); interval = setInterval(fire, 80); }, { passive: false });&#10; const stop = () =&gt; clearInterval(interval);&#10; btn.addEventListener('mouseup', stop);&#10; btn.addEventListener('mouseleave', stop);&#10; btn.addEventListener('touchend', stop);&#10; }&#10; bindCamBtn('camLeft', AZ, 0);&#10; bindCamBtn('camRight', -AZ, 0);&#10; bindCamBtn('camUp', 0, -PL);&#10; bindCamBtn('camDown', 0, PL);&#10; bindCamBtn('camZoomIn', 0, 0, -ZM);&#10; bindCamBtn('camZoomOut', 0, 0, ZM);&#10; document.getElementById('camCenter').onclick = () =&gt; focusCameraOnPlayer(true);&#10;&#10; document.getElementById('attackBtn').onclick = () =&gt; {&#10; if (gameState &amp;&amp; gameState.boss) {&#10; performAction('attack', gameState.boss.id);&#10; } else if (gameState &amp;&amp; Object.keys(gameState.monsters).length &gt; 0) {&#10; const targetId = Object.keys(gameState.monsters)[0];&#10; performAction('attack', targetId);&#10; } else {&#10; showMessage('No targets available');&#10; }&#10; };&#10;&#10; document.getElementById('gatherBtn').onclick = () =&gt; {&#10; performAction('gather', null);&#10; };&#10;&#10; document.getElementById('buildHouseBtn').onclick = () =&gt; {&#10; performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'house');&#10; };&#10;&#10; document.getElementById('buildFarmBtn').onclick = () =&gt; {&#10; performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'farm');&#10; };&#10;&#10; document.getElementById('buildTowerBtn').onclick = () =&gt; {&#10; performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'guard_tower');&#10; };&#10; }&#10;&#10; // ==================== LOGIN FORM ====================&#10; document.getElementById('loginForm').addEventListener('submit', async (e) =&gt; {&#10; e.preventDefault();&#10; const username = document.getElementById('username').value;&#10; const color = document.querySelector('.color-option.selected');&#10; if (!color) {&#10; showMessage('Please select a color');&#10; return;&#10; }&#10; await login(username, color.dataset.color);&#10; });&#10;&#10; // Color selector&#10; const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2'];&#10; const colorSelector = document.getElementById('colorSelector');&#10; colors.forEach(color =&gt; {&#10; const option = document.createElement('div');&#10; option.className = 'color-option';&#10; option.style.backgroundColor = color;&#10; option.dataset.color = color;&#10; option.onclick = () =&gt; {&#10; document.querySelectorAll('.color-option').forEach(c =&gt; c.classList.remove('selected'));&#10; option.classList.add('selected');&#10; };&#10; colorSelector.appendChild(option);&#10; });&#10;&#10; // Select first color by default&#10; document.querySelector('.color-option').classList.add('selected');&#10; &lt;/script&gt;&#10;&lt;/body&gt;&#10;&lt;/html&gt;&#10;" />
<option name="updatedContent" value="&lt;!DOCTYPE html&gt;&#10;&lt;html lang=&quot;en&quot;&gt;&#10;&lt;head&gt;&#10; &lt;meta charset=&quot;UTF-8&quot;&gt;&#10; &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;&#10; &lt;title&gt;Web Adventure - Community RPG&lt;/title&gt;&#10; &lt;style&gt;&#10; * {&#10; margin: 0;&#10; padding: 0;&#10; box-sizing: border-box;&#10; }&#10;&#10; body {&#10; font-family: 'Arial', sans-serif;&#10; background: #1a1a1a;&#10; color: #fff;&#10; overflow: hidden;&#10; }&#10;&#10; .login-screen {&#10; display: flex;&#10; align-items: center;&#10; justify-content: center;&#10; min-height: 100vh;&#10; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&#10; }&#10;&#10; .login-form {&#10; background: #2a2a2a;&#10; padding: 40px;&#10; border-radius: 10px;&#10; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);&#10; text-align: center;&#10; }&#10;&#10; .login-form h1 {&#10; margin-bottom: 30px;&#10; color: #667eea;&#10; font-size: 2.5em;&#10; }&#10;&#10; .login-form input,&#10; .login-form select {&#10; display: block;&#10; width: 100%;&#10; padding: 12px;&#10; margin: 15px 0;&#10; border: none;&#10; border-radius: 5px;&#10; background: #3a3a3a;&#10; color: #fff;&#10; font-size: 1em;&#10; }&#10;&#10; .login-form input::placeholder {&#10; color: #999;&#10; }&#10;&#10; .color-selector {&#10; display: flex;&#10; gap: 10px;&#10; margin: 20px 0;&#10; flex-wrap: wrap;&#10; }&#10;&#10; .color-option {&#10; width: 40px;&#10; height: 40px;&#10; border-radius: 5px;&#10; cursor: pointer;&#10; border: 3px solid transparent;&#10; transition: all 0.3s;&#10; }&#10;&#10; .color-option:hover {&#10; transform: scale(1.1);&#10; }&#10;&#10; .color-option.selected {&#10; border-color: #fff;&#10; transform: scale(1.2);&#10; }&#10;&#10; .login-form button {&#10; width: 100%;&#10; padding: 12px;&#10; background: #667eea;&#10; color: white;&#10; border: none;&#10; border-radius: 5px;&#10; font-size: 1.1em;&#10; cursor: pointer;&#10; margin-top: 20px;&#10; transition: background 0.3s;&#10; }&#10;&#10; .login-form button:hover {&#10; background: #764ba2;&#10; }&#10;&#10; .game-screen {&#10; display: none;&#10; width: 100%;&#10; height: 100vh;&#10; position: relative;&#10; }&#10;&#10; .game-screen.active {&#10; display: flex;&#10; }&#10;&#10; #gameCanvas {&#10; flex: 1;&#10; background: #000;&#10; }&#10;&#10; .ui-panel {&#10; position: absolute;&#10; background: rgba(0, 0, 0, 0.9);&#10; color: #fff;&#10; border: 2px solid #667eea;&#10; border-radius: 5px;&#10; padding: 15px;&#10; font-size: 0.9em;&#10; font-family: monospace;&#10; }&#10;&#10; .ui-top-left {&#10; top: 10px;&#10; left: 10px;&#10; max-width: 300px;&#10; }&#10;&#10; .ui-top-right {&#10; top: 10px;&#10; right: 10px;&#10; text-align: right;&#10; max-width: 300px;&#10; }&#10;&#10; .stats {&#10; margin-bottom: 15px;&#10; }&#10;&#10; .stat-row {&#10; display: flex;&#10; justify-content: space-between;&#10; margin: 5px 0;&#10; }&#10;&#10; .stat-label {&#10; color: #aaa;&#10; }&#10;&#10; .stat-value {&#10; color: #667eea;&#10; font-weight: bold;&#10; }&#10;&#10; .stat-bar {&#10; width: 100%;&#10; height: 20px;&#10; background: #333;&#10; border-radius: 3px;&#10; margin-top: 3px;&#10; overflow: hidden;&#10; }&#10;&#10; .stat-bar-fill {&#10; height: 100%;&#10; background: #667eea;&#10; transition: width 0.3s;&#10; }&#10;&#10; .stat-bar-fill.health {&#10; background: #ff4444;&#10; }&#10;&#10; .stat-bar-fill.action {&#10; background: #44ff44;&#10; }&#10;&#10; .inventory {&#10; margin-top: 20px;&#10; border-top: 1px solid #667eea;&#10; padding-top: 10px;&#10; }&#10;&#10; .inventory-item {&#10; display: flex;&#10; justify-content: space-between;&#10; margin: 5px 0;&#10; }&#10;&#10; .bottom-panel {&#10; position: absolute;&#10; bottom: 10px;&#10; left: 50%;&#10; transform: translateX(-50%);&#10; width: min(760px, calc(100% - 20px));&#10; background: rgba(0, 0, 0, 0.9);&#10; border: 2px solid #667eea;&#10; border-radius: 5px;&#10; padding: 10px;&#10; }&#10;&#10; .button-group {&#10; display: grid;&#10; grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));&#10; gap: 6px;&#10; }&#10;&#10; .action-button {&#10; padding: 6px 8px;&#10; background: #667eea;&#10; color: white;&#10; border: none;&#10; border-radius: 5px;&#10; cursor: pointer;&#10; transition: all 0.3s;&#10; font-size: 0.78em;&#10; line-height: 1.1;&#10; }&#10;&#10; .action-button:hover {&#10; background: #764ba2;&#10; transform: scale(1.05);&#10; }&#10;&#10; .action-button:disabled {&#10; background: #444;&#10; cursor: not-allowed;&#10; opacity: 0.5;&#10; }&#10;&#10; .status-message {&#10; margin-top: 8px;&#10; padding: 8px;&#10; background: #333;&#10; border-radius: 5px;&#10; min-height: 24px;&#10; font-size: 0.85em;&#10; }&#10;&#10; .controls-help {&#10; color: #aaa;&#10; font-size: 0.72em;&#10; margin-bottom: 8px;&#10; }&#10;&#10; .boss-bar-container {&#10; position: absolute;&#10; top: 50%;&#10; left: 50%;&#10; transform: translate(-50%, -50%);&#10; background: rgba(0, 0, 0, 0.9);&#10; border: 2px solid #ff4444;&#10; border-radius: 5px;&#10; padding: 20px;&#10; text-align: center;&#10; display: none;&#10; z-index: 10;&#10; }&#10;&#10; .boss-bar-container.active {&#10; display: block;&#10; }&#10;&#10; .game-over-screen {&#10; display: none;&#10; position: fixed;&#10; top: 0;&#10; left: 0;&#10; right: 0;&#10; bottom: 0;&#10; background: rgba(0, 0, 0, 0.95);&#10; z-index: 1000;&#10; align-items: center;&#10; justify-content: center;&#10; }&#10;&#10; .game-over-screen.active {&#10; display: flex;&#10; }&#10;&#10; .game-over-content {&#10; background: rgba(102, 126, 234, 0.1);&#10; border: 2px solid #667eea;&#10; border-radius: 10px;&#10; padding: 40px;&#10; text-align: center;&#10; max-width: 500px;&#10; }&#10;&#10; .game-over-content h1 {&#10; font-size: 2.5em;&#10; margin-bottom: 20px;&#10; }&#10;&#10; .game-over-content p {&#10; margin: 10px 0;&#10; font-size: 1.1em;&#10; }&#10;&#10; .game-over-content button {&#10; margin-top: 20px;&#10; padding: 12px 30px;&#10; background: #667eea;&#10; color: white;&#10; border: none;&#10; border-radius: 5px;&#10; font-size: 1.1em;&#10; cursor: pointer;&#10; }&#10;&#10; /* Camera controls widget */&#10; .camera-controls {&#10; position: absolute;&#10; bottom: 180px;&#10; right: 10px;&#10; background: rgba(0,0,0,0.85);&#10; border: 2px solid #667eea;&#10; border-radius: 8px;&#10; padding: 10px;&#10; display: none;&#10; flex-direction: column;&#10; align-items: center;&#10; gap: 4px;&#10; z-index: 20;&#10; user-select: none;&#10; }&#10; .camera-controls.active { display: flex; }&#10; .camera-controls .cam-label {&#10; color: #667eea;&#10; font-size: 0.75em;&#10; font-weight: bold;&#10; margin-bottom: 4px;&#10; letter-spacing: 0.05em;&#10; }&#10; .cam-row { display: flex; gap: 4px; }&#10; .cam-btn {&#10; width: 36px; height: 36px;&#10; background: #667eea;&#10; color: #fff;&#10; border: none;&#10; border-radius: 5px;&#10; font-size: 1.1em;&#10; cursor: pointer;&#10; display: flex; align-items: center; justify-content: center;&#10; transition: background 0.15s;&#10; }&#10; .cam-btn:hover { background: #764ba2; }&#10; .cam-btn:active { background: #4a3a8a; transform: scale(0.95); }&#10; .cam-btn.wide { width: 78px; font-size: 0.75em; }&#10; &lt;/style&gt;&#10;&lt;/head&gt;&#10;&lt;body&gt;&#10; &lt;div class=&quot;login-screen&quot; id=&quot;loginScreen&quot;&gt;&#10; &lt;div class=&quot;login-form&quot;&gt;&#10; &lt;h1&gt; Web Adventure&lt;/h1&gt;&#10; &lt;form id=&quot;loginForm&quot;&gt;&#10; &lt;input type=&quot;text&quot; id=&quot;username&quot; placeholder=&quot;Enter your username&quot; required&gt;&#10; &lt;div style=&quot;margin: 20px 0; color: #aaa;&quot;&gt;Choose your color:&lt;/div&gt;&#10; &lt;div class=&quot;color-selector&quot; id=&quot;colorSelector&quot;&gt;&lt;/div&gt;&#10; &lt;button type=&quot;submit&quot;&gt;Enter the World&lt;/button&gt;&#10; &lt;/form&gt;&#10; &lt;p style=&quot;margin-top: 20px; color: #999; font-size: 0.9em;&quot;&gt;&#10; Welcome to Web Adventure! Log in to join the multiplayer game world.&#10; &lt;/p&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;game-screen&quot; id=&quot;gameScreen&quot;&gt;&#10; &lt;canvas id=&quot;gameCanvas&quot;&gt;&lt;/canvas&gt;&#10;&#10; &lt;div class=&quot;ui-panel ui-top-left&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Player Stats&lt;/div&gt;&#10; &lt;div class=&quot;stats&quot;&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Level:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statLevel&quot;&gt;1&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Experience:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statExp&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Health:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statHealth&quot;&gt;100/100&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-bar&quot;&gt;&#10; &lt;div class=&quot;stat-bar-fill health&quot; id=&quot;healthBar&quot; style=&quot;width: 100%&quot;&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Action Points:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statActionPoints&quot;&gt;20&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-bar&quot;&gt;&#10; &lt;div class=&quot;stat-bar-fill action&quot; id=&quot;actionBar&quot; style=&quot;width: 100%&quot;&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Attack:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statAttack&quot;&gt;5&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Defense:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statDefense&quot;&gt;2&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Move Range:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;statMove&quot;&gt;5&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;inventory&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Inventory&lt;/div&gt;&#10; &lt;div class=&quot;inventory-item&quot;&gt;&#10; &lt;span&gt;Wood:&lt;/span&gt;&#10; &lt;span id=&quot;invWood&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;inventory-item&quot;&gt;&#10; &lt;span&gt;Stone:&lt;/span&gt;&#10; &lt;span id=&quot;invStone&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;ui-panel ui-top-right&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Game Status&lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Time Remaining:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;timeRemaining&quot;&gt;30:00&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Players Online:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;playersOnline&quot;&gt;1&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;stat-row&quot;&gt;&#10; &lt;span class=&quot;stat-label&quot;&gt;Monsters:&lt;/span&gt;&#10; &lt;span class=&quot;stat-value&quot; id=&quot;monsterCount&quot;&gt;0&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;boss-bar-container&quot; id=&quot;bossBar&quot;&gt;&#10; &lt;div style=&quot;color: #ff4444; font-weight: bold; margin-bottom: 10px;&quot;&gt;⚔️ BOSS APPEARED ⚔️&lt;/div&gt;&#10; &lt;div class=&quot;stat-bar&quot;&gt;&#10; &lt;div class=&quot;stat-bar-fill&quot; id=&quot;bossHealthBar&quot; style=&quot;width: 100%; background: #ff4444;&quot;&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;span id=&quot;bossHealth&quot;&gt;Loading...&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;!-- Camera orbit / zoom controls (top-right, shown after login) --&gt;&#10; &lt;div class=&quot;camera-controls&quot; id=&quot;cameraControls&quot;&gt;&#10; &lt;div class=&quot;cam-label&quot;&gt; CAMERA&lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot;&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camUp&quot; title=&quot;Tilt up&quot;&gt;▲&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot;&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camLeft&quot; title=&quot;Orbit left&quot;&gt;◀&lt;/button&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camCenter&quot; title=&quot;Re-center on player&quot;&gt;⊙&lt;/button&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camRight&quot; title=&quot;Orbit right&quot;&gt;▶&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot;&gt;&#10; &lt;button class=&quot;cam-btn&quot; id=&quot;camDown&quot; title=&quot;Tilt down&quot;&gt;▼&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;cam-row&quot; style=&quot;margin-top:4px; gap:4px;&quot;&gt;&#10; &lt;button class=&quot;cam-btn wide&quot; id=&quot;camZoomIn&quot; title=&quot;Zoom in&quot;&gt; +&lt;/button&gt;&#10; &lt;button class=&quot;cam-btn wide&quot; id=&quot;camZoomOut&quot; title=&quot;Zoom out&quot;&gt; −&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;bottom-panel&quot;&gt;&#10; &lt;div style=&quot;color: #667eea; font-weight: bold; margin-bottom: 10px;&quot;&gt;Controls&lt;/div&gt;&#10; &lt;div class=&quot;controls-help&quot;&gt;Each action costs 1 AP. Hover buttons to see details.&lt;/div&gt;&#10; &lt;div class=&quot;button-group&quot;&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveUpBtn&quot; title=&quot;Move north by 1 tile (cost: 1 AP)&quot;&gt;⬆ N&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveDownBtn&quot; title=&quot;Move south by 1 tile (cost: 1 AP)&quot;&gt;⬇ S&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveLeftBtn&quot; title=&quot;Move west by 1 tile (cost: 1 AP)&quot;&gt;⬅ W&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;moveRightBtn&quot; title=&quot;Move east by 1 tile (cost: 1 AP)&quot;&gt;➡ E&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;button-group&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;gatherBtn&quot; title=&quot;Gather nearby trees/mountains within radius 5 (cost: 1 AP)&quot;&gt; Gather&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;attackBtn&quot; title=&quot;Attack nearest selected enemy target (cost: 1 AP on success)&quot;&gt;⚔️ Attack&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;button-group&quot; style=&quot;margin-top: 10px;&quot;&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;buildHouseBtn&quot; title=&quot;Build House (20 wood, 10 stone)&quot;&gt; House&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;buildFarmBtn&quot; title=&quot;Build Farm (15 wood, 5 stone)&quot;&gt; Farm&lt;/button&gt;&#10; &lt;button class=&quot;action-button&quot; id=&quot;buildTowerBtn&quot; title=&quot;Build Guard Tower (30 wood, 20 stone)&quot;&gt;️ Tower&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class=&quot;status-message&quot; id=&quot;statusMessage&quot;&gt;Ready for adventure!&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class=&quot;game-over-screen&quot; id=&quot;gameOverScreen&quot;&gt;&#10; &lt;div class=&quot;game-over-content&quot;&gt;&#10; &lt;h1 id=&quot;gameOverTitle&quot;&gt;GAME OVER&lt;/h1&gt;&#10; &lt;p id=&quot;gameOverMessage&quot;&gt;The timer has run out!&lt;/p&gt;&#10; &lt;p id=&quot;gameOverStats&quot;&gt;&lt;/p&gt;&#10; &lt;button onclick=&quot;location.reload()&quot;&gt;Return to Login&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js&quot;&gt;&lt;/script&gt;&#10; &lt;script src=&quot;https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js&quot;&gt;&lt;/script&gt;&#10; &lt;script&gt;&#10; // ==================== GLOBAL STATE ====================&#10; let currentPlayer = null;&#10; let currentPlayerId = null;&#10; let gameState = null;&#10; let gameActive = true;&#10; let ws = null;&#10; let cameraKeyBindingsAttached = false;&#10;&#10; const API_BASE = 'http://localhost:8000/api';&#10;&#10; // ==================== UTILITIES ====================&#10; function showMessage(msg) {&#10; document.getElementById('statusMessage').textContent = msg;&#10; }&#10;&#10; function updateUI() {&#10; if (!currentPlayer) return;&#10;&#10; document.getElementById('statLevel').textContent = currentPlayer.level;&#10; document.getElementById('statExp').textContent = currentPlayer.exp;&#10; document.getElementById('statHealth').textContent = `${currentPlayer.health}/${currentPlayer.max_health}`;&#10; document.getElementById('healthBar').style.width = `${(currentPlayer.health / currentPlayer.max_health) * 100}%`;&#10; document.getElementById('statActionPoints').textContent = currentPlayer.action_points;&#10; document.getElementById('actionBar').style.width = `${(currentPlayer.action_points / currentPlayer.max_action_points) * 100}%`;&#10; document.getElementById('statAttack').textContent = currentPlayer.attack;&#10; document.getElementById('statDefense').textContent = currentPlayer.defense;&#10; document.getElementById('statMove').textContent = currentPlayer.movement_capacity;&#10;&#10; document.getElementById('invWood').textContent = currentPlayer.inventory.wood || 0;&#10; document.getElementById('invStone').textContent = currentPlayer.inventory.stone || 0;&#10;&#10; if (gameState) {&#10; const activePlayerCount = Object.values(gameState.players).filter(p =&gt; p.active).length;&#10; document.getElementById('playersOnline').textContent = activePlayerCount;&#10; document.getElementById('monsterCount').textContent = Object.keys(gameState.monsters).length;&#10; }&#10; }&#10;&#10; function formatTime(seconds) {&#10; const mins = Math.floor(seconds / 60);&#10; const secs = Math.floor(seconds % 60);&#10; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;&#10; }&#10;&#10; // ==================== THREE.JS SCENE ====================&#10; let scene, camera, renderer, controls;&#10; const playerSpheres = {};&#10; const monsterCubes = {};&#10; let ossBossObject = null;&#10; const structureObjects = {};&#10; const resourceObjects = {};&#10;&#10; function initScene() {&#10; const canvas = document.getElementById('gameCanvas');&#10; scene = new THREE.Scene();&#10; scene.background = new THREE.Color(0x0a0a0a);&#10;&#10; camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 10000);&#10; camera.position.set(0, 50, 50);&#10; camera.lookAt(0, 0, 0);&#10;&#10; renderer = new THREE.WebGLRenderer({ canvas, antialias: true });&#10; renderer.setSize(canvas.clientWidth, canvas.clientHeight);&#10; renderer.shadowMap.enabled = true;&#10;&#10; // Mouse camera controls: left drag to orbit, wheel to zoom, right drag to pan.&#10; controls = new THREE.OrbitControls(camera, renderer.domElement);&#10; controls.enableDamping = true;&#10; controls.dampingFactor = 0.08;&#10; controls.target.set(0, 0, 0);&#10; controls.maxPolarAngle = Math.PI * 0.49;&#10; controls.minDistance = 10;&#10; controls.maxDistance = 200;&#10; controls.enableKeys = false; // We handle arrow keys ourselves.&#10;&#10; // Lighting&#10; const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);&#10; scene.add(ambientLight);&#10;&#10; const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);&#10; directionalLight.position.set(100, 100, 100);&#10; directionalLight.shadow.mapSize.width = 2048;&#10; directionalLight.shadow.mapSize.height = 2048;&#10; directionalLight.castShadow = true;&#10; scene.add(directionalLight);&#10;&#10; // Ground plane&#10; const groundGeometry = new THREE.PlaneGeometry(500, 500);&#10; const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x1a3a1a });&#10; const ground = new THREE.Mesh(groundGeometry, groundMaterial);&#10; ground.rotation.x = -Math.PI / 2;&#10; ground.receiveShadow = true;&#10; scene.add(ground);&#10;&#10; // Grid helper&#10; const gridHelper = new THREE.GridHelper(500, 50, 0x444444, 0x222222);&#10; gridHelper.position.y = 0.1;&#10; scene.add(gridHelper);&#10;&#10; window.addEventListener('resize', () =&gt; {&#10; camera.aspect = canvas.clientWidth / canvas.clientHeight;&#10; camera.updateProjectionMatrix();&#10; renderer.setSize(canvas.clientWidth, canvas.clientHeight);&#10; });&#10;&#10; animate();&#10; }&#10;&#10; function focusCameraOnPlayer(force = false) {&#10; if (!currentPlayer || !camera) return;&#10;&#10; const targetX = currentPlayer.position.x;&#10; const targetZ = currentPlayer.position.y;&#10;&#10; if (force) {&#10; camera.position.set(targetX + 4, 8, targetZ + 6);&#10; if (controls) {&#10; controls.target.set(targetX, 0, targetZ);&#10; controls.minDistance = 3;&#10; controls.maxDistance = 80;&#10; controls.update();&#10; } else {&#10; camera.lookAt(targetX, 0, targetZ);&#10; }&#10; return;&#10; }&#10;&#10; if (controls) {&#10; const dx = targetX - controls.target.x;&#10; const dz = targetZ - controls.target.z;&#10; controls.target.x += dx * 0.12;&#10; controls.target.z += dz * 0.12;&#10; } else {&#10; camera.position.x = targetX;&#10; camera.position.z = targetZ + 15;&#10; camera.lookAt(targetX, 0, targetZ);&#10; }&#10; }&#10;&#10; function rotateCameraBy(deltaAzimuth, deltaPolar, deltaZoom = 0) {&#10; if (!camera) return;&#10; const target = controls ? controls.target.clone() : new THREE.Vector3(0, 0, 0);&#10; const offset = camera.position.clone().sub(target);&#10; const spherical = new THREE.Spherical().setFromVector3(offset);&#10;&#10; spherical.theta -= deltaAzimuth;&#10; spherical.phi = Math.max(0.15, Math.min(Math.PI * 0.48, spherical.phi + deltaPolar));&#10; spherical.radius = Math.max(10, Math.min(200, spherical.radius + deltaZoom));&#10;&#10; offset.setFromSpherical(spherical);&#10; camera.position.copy(target).add(offset);&#10; camera.lookAt(target.x, target.y, target.z);&#10;&#10; if (controls) {&#10; controls.target.copy(target);&#10; const damp = controls.enableDamping;&#10; controls.enableDamping = false;&#10; controls.update();&#10; controls.enableDamping = damp;&#10; }&#10; }&#10;&#10; function panCameraBy(deltaRight, deltaForward) {&#10; if (!camera || !controls) return;&#10;&#10; const forward = new THREE.Vector3();&#10; camera.getWorldDirection(forward);&#10; forward.y = 0;&#10; if (forward.lengthSq() === 0) return;&#10; forward.normalize();&#10;&#10; const right = new THREE.Vector3(forward.z, 0, -forward.x).normalize();&#10; const move = new THREE.Vector3();&#10; move.addScaledVector(right, deltaRight);&#10; move.addScaledVector(forward, deltaForward);&#10;&#10; controls.target.add(move);&#10; camera.position.add(move);&#10; controls.update();&#10; }&#10;&#10; function createPlayerSphere(player) {&#10; const geometry = new THREE.SphereGeometry(0.5, 16, 16);&#10; const isCurrentPlayer = player.id === currentPlayerId;&#10; const material = new THREE.MeshPhongMaterial({&#10; color: player.color,&#10; emissive: isCurrentPlayer ? 0x222222 : 0x000000,&#10; shininess: isCurrentPlayer ? 80 : 30,&#10; });&#10; const sphere = new THREE.Mesh(geometry, material);&#10; sphere.position.set(player.position.x, 0.5, player.position.y);&#10; if (isCurrentPlayer) {&#10; sphere.scale.set(1.15, 1.15, 1.15);&#10; }&#10; sphere.castShadow = true;&#10; sphere.receiveShadow = true;&#10; scene.add(sphere);&#10;&#10; // Add label&#10; const canvas = document.createElement('canvas');&#10; const ctx = canvas.getContext('2d');&#10; canvas.width = 256;&#10; canvas.height = 128;&#10; ctx.fillStyle = 'white';&#10; ctx.font = '32px Arial';&#10; ctx.textAlign = 'center';&#10; ctx.textBaseline = 'middle';&#10; ctx.fillText(player.username, 128, 64);&#10;&#10; const texture = new THREE.CanvasTexture(canvas);&#10; const spriteMaterial = new THREE.SpriteMaterial({ map: texture });&#10; const sprite = new THREE.Sprite(spriteMaterial);&#10; sprite.scale.set(4, 2, 1);&#10; sprite.position.set(player.position.x, 2.5, player.position.y);&#10; scene.add(sprite);&#10;&#10; playerSpheres[player.id] = { mesh: sphere, sprite };&#10; }&#10;&#10; function updatePlayerSphere(player) {&#10; if (playerSpheres[player.id]) {&#10; playerSpheres[player.id].mesh.position.set(player.position.x, 0.5, player.position.y);&#10; playerSpheres[player.id].sprite.position.set(player.position.x, 2.5, player.position.y);&#10; }&#10; }&#10;&#10; function createMonsterCube(monster) {&#10; const geometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);&#10; const material = new THREE.MeshPhongMaterial({ color: 0xff4444 });&#10; const cube = new THREE.Mesh(geometry, material);&#10; cube.position.set(monster.position.x, 0.3, monster.position.y);&#10; cube.castShadow = true;&#10; scene.add(cube);&#10; monsterCubes[monster.id] = cube;&#10; }&#10;&#10; function createBossMesh(monster) {&#10; const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);&#10; const material = new THREE.MeshPhongMaterial({ color: 0xff0000 });&#10; const cube = new THREE.Mesh(geometry, material);&#10; cube.position.set(monster.position.x, 0.75, monster.position.y);&#10; cube.castShadow = true;&#10; scene.add(cube);&#10; ossBossObject = cube;&#10; }&#10;&#10; function createStructureMesh(structure) {&#10; let geometry, color;&#10; if (structure.structure_type === 'house') {&#10; geometry = new THREE.ConeGeometry(0.5, 1, 4);&#10; color = 0xc0a080;&#10; } else if (structure.structure_type === 'farm') {&#10; geometry = new THREE.ConeGeometry(0.5, 1, 4);&#10; color = 0x90ee90;&#10; } else if (structure.structure_type === 'guard_tower') {&#10; geometry = new THREE.ConeGeometry(0.35, 1.4, 4);&#10; color = 0x808080;&#10; }&#10; const material = new THREE.MeshPhongMaterial({ color });&#10; const mesh = new THREE.Mesh(geometry, material);&#10; mesh.position.set(structure.position.x, 0.5, structure.position.y);&#10; mesh.castShadow = true;&#10; scene.add(mesh);&#10; structureObjects[structure.id] = mesh;&#10; }&#10;&#10; function createResourceMesh(resource) {&#10; let mesh;&#10; if (resource.resource_type === 'tree') {&#10; const trunkGeo = new THREE.CylinderGeometry(0.12, 0.18, 0.8, 8);&#10; const trunkMat = new THREE.MeshPhongMaterial({ color: 0x8B4513 });&#10; const trunk = new THREE.Mesh(trunkGeo, trunkMat);&#10; trunk.position.y = 0.4;&#10; const foliageMat = new THREE.MeshPhongMaterial({ color: 0x228b22 });&#10; const foliage1 = new THREE.Mesh(new THREE.ConeGeometry(0.85, 1.3, 8), foliageMat);&#10; foliage1.position.y = 1.3;&#10; const foliage2 = new THREE.Mesh(new THREE.ConeGeometry(0.6, 1.0, 8), foliageMat);&#10; foliage2.position.y = 2.1;&#10; mesh = new THREE.Group();&#10; mesh.add(trunk, foliage1, foliage2);&#10; } else {&#10; const rockMat = new THREE.MeshPhongMaterial({ color: 0x8a8a8a, flatShading: true });&#10; const main = new THREE.Mesh(new THREE.DodecahedronGeometry(0.65, 0), rockMat);&#10; main.position.y = 0.5;&#10; main.rotation.y = Math.random() * Math.PI;&#10; const side1 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.38, 0), rockMat);&#10; side1.position.set(0.6, 0.25, 0.12);&#10; side1.rotation.y = Math.random() * Math.PI;&#10; const side2 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.28, 0), rockMat);&#10; side2.position.set(-0.5, 0.2, 0.25);&#10; side2.rotation.y = Math.random() * Math.PI;&#10; mesh = new THREE.Group();&#10; mesh.add(main, side1, side2);&#10; }&#10; mesh.position.set(resource.position.x, 0, resource.position.y);&#10; mesh.castShadow = true;&#10; scene.add(mesh);&#10; resourceObjects[resource.id] = mesh;&#10; }&#10;&#10; function updateGameScene() {&#10; if (!gameState) return;&#10;&#10; // Update players&#10; for (const player of Object.values(gameState.players || {})) {&#10; if (!playerSpheres[player.id]) {&#10; createPlayerSphere(player);&#10; } else {&#10; updatePlayerSphere(player);&#10; }&#10; }&#10;&#10; // Remove deleted players&#10; for (const pid in playerSpheres) {&#10; if (!(gameState.players || {})[pid]) {&#10; scene.remove(playerSpheres[pid].mesh);&#10; scene.remove(playerSpheres[pid].sprite);&#10; delete playerSpheres[pid];&#10; }&#10; }&#10;&#10; // Update monsters&#10; for (const monster of Object.values(gameState.monsters || {})) {&#10; if (!monsterCubes[monster.id]) {&#10; createMonsterCube(monster);&#10; } else {&#10; monsterCubes[monster.id].position.set(monster.position.x, 0.3, monster.position.y);&#10; }&#10; }&#10;&#10; // Remove deleted monsters&#10; for (const mid in monsterCubes) {&#10; if (!(gameState.monsters || {})[mid]) {&#10; scene.remove(monsterCubes[mid]);&#10; delete monsterCubes[mid];&#10; }&#10; }&#10;&#10; // Update boss&#10; if (gameState.boss) {&#10; if (!ossBossObject) {&#10; createBossMesh(gameState.boss);&#10; document.getElementById('bossBar').classList.add('active');&#10; } else {&#10; ossBossObject.position.set(gameState.boss.position.x, 0.75, gameState.boss.position.y);&#10; ossBossObject.rotation.y += 0.01;&#10; }&#10; document.getElementById('bossHealthBar').style.width = `${gameState.boss.boss_progress}%`;&#10; document.getElementById('bossHealth').textContent = `${gameState.boss.health} / ${gameState.boss.max_health}`;&#10; } else {&#10; if (ossBossObject) { scene.remove(ossBossObject); ossBossObject = null; }&#10; document.getElementById('bossBar').classList.remove('active');&#10; }&#10;&#10; // Update structures&#10; for (const structure of Object.values(gameState.structures || {})) {&#10; if (!structureObjects[structure.id]) createStructureMesh(structure);&#10; }&#10;&#10; // Update resources — guard against missing field&#10; const resources = gameState.resources || {};&#10; for (const resource of Object.values(resources)) {&#10; if (!resourceObjects[resource.id]) createResourceMesh(resource);&#10; }&#10; for (const rid in resourceObjects) {&#10; if (!resources[rid]) { scene.remove(resourceObjects[rid]); delete resourceObjects[rid]; }&#10; }&#10;&#10; focusCameraOnPlayer(false);&#10; }&#10;&#10; function animate() {&#10; requestAnimationFrame(animate);&#10; updateGameScene();&#10; if (controls) controls.update();&#10; renderer.render(scene, camera);&#10; }&#10;&#10; // ==================== API FUNCTIONS ====================&#10; async function login(username, color) {&#10; try {&#10; const response = await fetch(`${API_BASE}/login`, {&#10; method: 'POST',&#10; headers: { 'Content-Type': 'application/json' },&#10; body: JSON.stringify({ username, color }),&#10; });&#10; const data = await response.json();&#10; currentPlayer = data.player;&#10; currentPlayerId = data.player_id;&#10; gameActive = data.game_active;&#10;&#10; document.getElementById('loginScreen').style.display = 'none';&#10; document.getElementById('gameScreen').classList.add('active');&#10;&#10; initScene();&#10; createPlayerSphere(currentPlayer);&#10; focusCameraOnPlayer(true);&#10; connectWebSocket();&#10; setupControls();&#10;&#10; return data;&#10; } catch (error) {&#10; console.error('Login failed:', error);&#10; showMessage('Login failed. Try again.');&#10; }&#10; }&#10;&#10; function connectWebSocket() {&#10; ws = new WebSocket(`ws://localhost:8000/ws/${currentPlayerId}`);&#10;&#10; ws.onmessage = (event) =&gt; {&#10; const data = JSON.parse(event.data);&#10;&#10; if (data.type === 'state_update') {&#10; gameState = data;&#10; currentPlayer = gameState.players[currentPlayerId] || currentPlayer;&#10;&#10; document.getElementById('timeRemaining').textContent = formatTime(data.time_remaining);&#10;&#10; if (data.time_remaining &lt;= 0) {&#10; showGameOver();&#10; }&#10;&#10; updateUI();&#10; } else if (data.type === 'game_over') {&#10; showGameOver();&#10; }&#10; };&#10;&#10; ws.onerror = (error) =&gt; {&#10; console.error('WebSocket error:', error);&#10; };&#10;&#10; ws.onclose = () =&gt; {&#10; console.log('WebSocket closed');&#10; };&#10; }&#10;&#10; function showGameOver() {&#10; gameActive = false;&#10; document.getElementById('gameOverScreen').classList.add('active');&#10; document.getElementById('gameOverMessage').textContent =&#10; 'The world resets... Come back when you\'re stronger!';&#10; document.getElementById('gameOverStats').textContent =&#10; `Final Level: ${currentPlayer.level} | Final Experience: ${currentPlayer.exp}`;&#10; }&#10;&#10; async function movePlayer(dx, dy) {&#10; try {&#10; const response = await fetch(`${API_BASE}/player/${currentPlayerId}/move`, {&#10; method: 'POST',&#10; headers: { 'Content-Type': 'application/json' },&#10; body: JSON.stringify({ dx, dy }),&#10; });&#10; const data = await response.json();&#10;&#10; if (!response.ok) {&#10; showMessage(`Move failed: ${data.detail || data.reason || 'Unknown error'}`);&#10; return data;&#10; }&#10;&#10; // Apply local update immediately so movement feels responsive even before WS tick arrives.&#10; if (response.ok &amp;&amp; data.position &amp;&amp; currentPlayer) {&#10; currentPlayer.position = data.position;&#10; currentPlayer.action_points = data.action_points ?? currentPlayer.action_points;&#10; updatePlayerSphere(currentPlayer);&#10; focusCameraOnPlayer(false);&#10; updateUI();&#10; }&#10; return data;&#10; } catch (error) {&#10; console.error('Move failed:', error);&#10; showMessage('Move failed!');&#10; }&#10; }&#10;&#10; async function performAction(actionType, targetId = null, tx = null, ty = null, structureType = null) {&#10; try {&#10; const response = await fetch(`${API_BASE}/player/${currentPlayerId}/action`, {&#10; method: 'POST',&#10; headers: { 'Content-Type': 'application/json' },&#10; body: JSON.stringify({ action_type: actionType, target_id: targetId, tx, ty, structure_type: structureType }),&#10; });&#10; const data = await response.json();&#10;&#10; if (data.success) {&#10; showMessage(`Action successful: ${actionType}`);&#10; } else {&#10; showMessage(`Action failed: ${data.reason || 'Unknown error'}`);&#10; }&#10; return data;&#10; } catch (error) {&#10; console.error('Action failed:', error);&#10; showMessage('Action failed!');&#10; }&#10; }&#10;&#10; // ==================== UI CONTROLS ====================&#10; function setupControls() {&#10; document.getElementById('moveUpBtn').onclick = () =&gt; movePlayer(0, -1);&#10; document.getElementById('moveDownBtn').onclick = () =&gt; movePlayer(0, 1);&#10; document.getElementById('moveLeftBtn').onclick = () =&gt; movePlayer(-1, 0);&#10; document.getElementById('moveRightBtn').onclick = () =&gt; movePlayer( 1, 0);&#10;&#10; // Show and wire camera widget buttons.&#10; document.getElementById('cameraControls').classList.add('active');&#10;&#10; const AZ = 0.25, PL = 0.12, ZM = 8;&#10; // Support both click and held-down via a repeating interval.&#10; function bindCamBtn(id, az, pl, zm = 0) {&#10; const btn = document.getElementById(id);&#10; let interval = null;&#10; const fire = () =&gt; rotateCameraBy(az, pl, zm);&#10; btn.addEventListener('mousedown', () =&gt; { fire(); interval = setInterval(fire, 80); });&#10; btn.addEventListener('touchstart', (e) =&gt; { e.preventDefault(); fire(); interval = setInterval(fire, 80); }, { passive: false });&#10; const stop = () =&gt; clearInterval(interval);&#10; btn.addEventListener('mouseup', stop);&#10; btn.addEventListener('mouseleave', stop);&#10; btn.addEventListener('touchend', stop);&#10; }&#10; bindCamBtn('camLeft', AZ, 0);&#10; bindCamBtn('camRight', -AZ, 0);&#10; bindCamBtn('camUp', 0, -PL);&#10; bindCamBtn('camDown', 0, PL);&#10; bindCamBtn('camZoomIn', 0, 0, -ZM);&#10; bindCamBtn('camZoomOut', 0, 0, ZM);&#10; document.getElementById('camCenter').onclick = () =&gt; focusCameraOnPlayer(true);&#10;&#10; if (!cameraKeyBindingsAttached) {&#10; cameraKeyBindingsAttached = true;&#10; document.addEventListener('keydown', (event) =&gt; {&#10; const gameVisible = document.getElementById('gameScreen').classList.contains('active');&#10; if (!gameVisible || !camera) return;&#10;&#10; const key = event.key.toLowerCase();&#10; const rotationStep = 0.12;&#10; const panStep = 1.0;&#10;&#10; if ([&quot;arrowleft&quot;, &quot;arrowright&quot;, &quot;arrowup&quot;, &quot;arrowdown&quot;, &quot;z&quot;, &quot;q&quot;, &quot;s&quot;, &quot;d&quot;].includes(key)) {&#10; event.preventDefault();&#10; }&#10;&#10; if (key === 'arrowleft') rotateCameraBy(rotationStep, 0, 0);&#10; else if (key === 'arrowright') rotateCameraBy(-rotationStep, 0, 0);&#10; else if (key === 'arrowup') rotateCameraBy(0, -rotationStep * 0.45, 0);&#10; else if (key === 'arrowdown') rotateCameraBy(0, rotationStep * 0.45, 0);&#10; else if (key === 'z') panCameraBy(0, panStep);&#10; else if (key === 's') panCameraBy(0, -panStep);&#10; else if (key === 'q') panCameraBy(-panStep, 0);&#10; else if (key === 'd') panCameraBy(panStep, 0);&#10; });&#10; }&#10;&#10; document.getElementById('attackBtn').onclick = () =&gt; {&#10; if (gameState &amp;&amp; gameState.boss) {&#10; performAction('attack', gameState.boss.id);&#10; } else if (gameState &amp;&amp; Object.keys(gameState.monsters).length &gt; 0) {&#10; const targetId = Object.keys(gameState.monsters)[0];&#10; performAction('attack', targetId);&#10; } else {&#10; showMessage('No targets available');&#10; }&#10; };&#10;&#10; document.getElementById('gatherBtn').onclick = () =&gt; {&#10; performAction('gather', null);&#10; };&#10;&#10; document.getElementById('buildHouseBtn').onclick = () =&gt; {&#10; performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'house');&#10; };&#10;&#10; document.getElementById('buildFarmBtn').onclick = () =&gt; {&#10; performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'farm');&#10; };&#10;&#10; document.getElementById('buildTowerBtn').onclick = () =&gt; {&#10; performAction('build', null, currentPlayer.position.x, currentPlayer.position.y, 'guard_tower');&#10; };&#10; }&#10;&#10; // ==================== LOGIN FORM ====================&#10; document.getElementById('loginForm').addEventListener('submit', async (e) =&gt; {&#10; e.preventDefault();&#10; const username = document.getElementById('username').value;&#10; const color = document.querySelector('.color-option.selected');&#10; if (!color) {&#10; showMessage('Please select a color');&#10; return;&#10; }&#10; await login(username, color.dataset.color);&#10; });&#10;&#10; // Color selector&#10; const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2'];&#10; const colorSelector = document.getElementById('colorSelector');&#10; colors.forEach(color =&gt; {&#10; const option = document.createElement('div');&#10; option.className = 'color-option';&#10; option.style.backgroundColor = color;&#10; option.dataset.color = color;&#10; option.onclick = () =&gt; {&#10; document.querySelectorAll('.color-option').forEach(c =&gt; c.classList.remove('selected'));&#10; option.classList.add('selected');&#10; };&#10; colorSelector.appendChild(option);&#10; });&#10;&#10; // Select first color by default&#10; document.querySelector('.color-option').classList.add('selected');&#10; &lt;/script&gt;&#10;&lt;/body&gt;&#10;&lt;/html&gt;&#10;" />
</PendingDiffInfo>
</value>
</entry>
</map>
</option>
</component>
</project>