Drop Apex Dynamics from the playable roster (2 races now: Inner Sphere Navy, Outer Rim Collective) to simplify the 2D art pipeline; World's race-offer logic just shuffles both remaining races instead of picking 2 of N. Also fix bullets using a constant muzzle velocity instead of adding the shooter's own velocity, and shrink the bullet hitbox/sprite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.7 KiB
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.gdreferenced in an earlier version of this doc does not exist yet — it's still an open task (seeCLAUDE.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).