# Project Structure ## File Tree ``` spacewar/ ← repo root └── spacewar/ ← Godot project root (open this in Godot editor) ├── project.godot ├── autoload/ │ └── game_config.gd ← autoload singleton (tuning values, player state, signals) ├── menu/ │ ├── main_menu.tscn/gd ← entry point / main scene; HL2-style vertical nav │ ├── server_select.tscn/gd ← server list UI, backed by matchmaking-api's GET /servers │ ├── pause_menu.tscn/gd ← in-game pause overlay (ESC / Start button) │ └── team_select.tscn/gd ← race + ship selection overlay (shown on world load) ├── ships/ │ ├── ship.tscn ← player ship scene (formerly node_2d.tscn) │ ├── ship_movement.gd ← player ship logic │ └── bullet.tscn/gd ← projectile ├── chat/ │ └── chat_box.tscn/gd ← WoW-style chat overlay (T=all, Y=team), added to world.tscn ├── hud/ │ └── player_list.tscn/gd ← top-left player roster (white=team, yellow=enemy, "(b)"=bot), added to world.tscn ├── bots/ │ ├── bot_manager.gd ← autoload; server-only bot fill (join/leave reconciliation) │ ├── bot_ai.gd ← class_name BotAI; seek-nearest-enemy-and-shoot brain │ └── bot_names.gd ← class_name BotNames; procedural callsign pool ├── world/ │ ├── world.tscn ← game world; world.gd loads the active map into MapContainer │ ├── world.gd ← picks/instances a map scene from world/maps/ │ ├── world_tileset.tres ← shared TileSet resource (walls.png + asteroids.png atlas sources) │ └── maps/ │ └── map_01.tscn ← hand-painted map: Walls + Asteroids TileMapLayers, team spawn Marker2Ds └── assets/ ├── icon.svg └── images/ ├── background/skybox/ ← 6 space background PNGs (1.png – 6.png) ├── effects/ ← explosion1.png ├── ships/example_ships/ ← 4 placeholder ship sprites (1.png, 1B.png, 2a.png, 3b.png) └── tiles/ ← walls.png, asteroids.png, used by world_tileset.tres ``` > **Note:** `asteroid_movement.gd` referenced in an earlier version of this doc does not exist yet — it's still an open task (see `CLAUDE.md`). ## Scene Graph ### `menu/main_menu.tscn` — entry point ``` MainMenu (Control) ← main_menu.gd builds all UI in _ready() ``` ### `menu/server_select.tscn` — server list ``` ServerSelect (Control) ← server_select.gd builds all UI in _ready(), fetches GET /servers ``` ### `world/world.tscn` — game world ``` World (Node2D) ← world.gd instances MAP_SCENE into MapContainer on _ready() ├── MapContainer (Node2D) ← holds the instanced map (world/maps/map_01.tscn), background included ├── Player ← instance of ships/ship.tscn ├── HUD (CanvasLayer) │ └── HealthLabel ├── PauseMenu (CanvasLayer, layer=10) ← menu/pause_menu.tscn ├── TeamSelect (CanvasLayer, layer=20) ← menu/team_select.tscn; queue_free()s after selection ├── ChatBox (CanvasLayer, layer=5) ← chat/chat_box.tscn └── PlayerList (CanvasLayer, layer=4) ← hud/player_list.tscn ``` ### `world/maps/map_01.tscn` — hand-painted map ``` Map01 (Node2D) ├── Background (TextureRect) ← per-map space background (skybox/1.png); visible while editing this scene ├── Walls (TileMapLayer) ← painted by hand in the Godot Tile Editor, uses world_tileset.tres ├── Asteroids (TileMapLayer) ← painted by hand, same shared tileset └── SpawnPoints (Node2D) ├── TeamASpawn1 (Marker2D) ← group "team_a_spawn" └── TeamBSpawn1 (Marker2D) ← group "team_b_spawn" ``` ### `ships/ship.tscn` — player ship ``` Player (CharacterBody2D) ← ship_movement.gd ├── CollisionShape2D ← CircleShape2D ├── Sprite2D ← texture set at runtime from GameConfig.player_ship_path └── VisibleOnScreenNotifier2D ``` ## Input Map | Action | Key | Controller | |--------|-----|------------| | `move_up` | W | — | | `move_left` | A | — | | `move_right` | D | — | | `move_down` | S | — | | `shoot` | Space | — | | `toggle_pause` | Esc | Start / Options button (JoyButton 6) | | `chat_all` | T | — | | `chat_team` | Y | — | ## Game Flow ``` main_menu.tscn ├── QUICK PLAY → matchmaking queue (casual) → world.tscn │ ├── TeamSelect overlay: pick race (both offered every match) │ ├── TeamSelect overlay: pick ship │ └── Player spawns → game live ├── SERVER SELECT → server_select.tscn → pick a server → world.tscn (same TeamSelect flow) ├── OPTIONS → stub, coming soon ├── PROFILE → callsign-edit overlay (also auto-opens from QUICK PLAY if no callsign is set) └── QUIT ``` Ranked matchmaking is removed from the menu entirely (not just disabled) until it's real — see `CLAUDE.md` Current Tasks. ## Display Launches borderless fullscreen at the real screen resolution (`window/size/mode=3` in `project.godot`) with stretch mode **disabled** — 1 game pixel = 1 screen pixel everywhere, no UI/world scaling. Chosen over Godot's default `canvas_items`+`expand` stretch (which scales the whole 2D canvas to fill the window) because the game's design canvas is deliberately sized to the Steam Deck's native 1280×800; scaling that up to fill a PC monitor made everything look zoomed in. With stretch disabled, PC monitors just reveal more of the world/HUD at native size instead — the actual map (`world/maps/map_01.tscn`) is already a fixed-size 14016×6000 arena, not viewport-sized, so there's more world to reveal. This means every menu Control has to position itself relative to the *real* window size, not a fixed 1280×800 design canvas — anchors (0.0=edge, 0.5=center, 1.0=opposite edge) plus fixed pixel offsets from that anchor, not raw absolute pixel coordinates. `main_menu.gd` and `team_select.gd` both have a `_place()`/direct-property-assignment helper for this — **always set anchor properties directly (`node.anchor_left = ...`) rather than through sequential `set_anchor_and_offset()` calls**: that method's default `push_opposite_anchor=true` drags the opposite side's anchor/offset along whenever two sequential calls momentarily disagree (e.g. left set to a 0.5 anchor while right is still its 0.0 default), corrupting layout — hit this exact bug converting the play-mode buttons to be centered. ## Key Autoload — `GameConfig` | Property | Type | Set by | |----------|------|--------| | `player_name` | String | Main menu callsign input | | `player_race` | int | TeamSelect race pick | | `player_ship_path` | String | TeamSelect ship pick | | `team_selected` | signal | Emitted by TeamSelect when done | | `ship_thrust` | float | Tuning constant | | `ship_max_speed` | float | Tuning constant | | `ship_rotation_speed` | float | Tuning constant | | `ship_fire_rate` | float | Tuning constant | | `bullet_speed` | float | Tuning constant | | `ship_max_health` | int | Tuning constant | | `ship_respawn_delay` | float | Tuning constant | | `ship_invincibility_time` | float | Tuning constant | | `bullet_damage` | int | Tuning constant | | `ship_wall_damage` | int | Tuning constant | | `chat_focused` | bool | Set by ChatBox while its input line has keyboard focus; gates ship movement/fire input | | `bot_min_team_size` | int | Tuning constant — min humans+bots per race, see `bots/bot_manager.gd` | | `bot_engage_range` | float | Tuning constant — bot max shoot distance | | `bot_stop_distance` | float | Tuning constant — bot stops closing distance below this | | `bot_aim_tolerance_deg` | float | Tuning constant — how precisely a bot must face a target to fire | ## Key Autoload — `BotManager` Server-authoritative bot fill for casual (`bots/bot_manager.gd`). Keeps each of the match's 2 offered races at `GameConfig.bot_min_team_size` total humans+bots, reacting to `PlayerRegistry.loadout_updated`/`player_removed` and `World.decide_offered_races()`. See `overview/bots.md`. ## Key Autoload — `ChatManager` Server-relayed chat (`autoload/chat_manager.gd`). `send_chat(text, team_only)` sends; `message_received(sender_name, sender_race, team_only, text)` signal delivers incoming messages to `ChatBox`. "Team" messages are filtered server-side to peers sharing the sender's race (race doubles as team — see `PlayerRegistry`).