# 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`).
