- In-game chat: T=all, Y=team (race doubles as team), server-relayed and team-filtered via new ChatManager autoload; last 10 messages shown bottom-left (chat/chat_box.gd). - Bot fill for casual (bots/): keeps each of the match's 2 offered races at a minimum of 7 total humans+bots, spawning/despawning reactively as players join/leave. Bots always fly their race's fighter and use negative peer_ids so they ride the existing networked-ship stack (spawning, loadout sync, health/position sync, bullet attribution) with no special-casing. Casual matchmaking now forms with just 1 real player queued instead of waiting for a second (matchmaking-api/). - Borderless fullscreen with stretch scaling disabled instead of scaling the Steam-Deck-matched 1280x800 canvas up to fill PC monitors (which read as zoomed in) — PC monitors now reveal more world/HUD at native size instead. Reworked main_menu/team_select/chat_box to position via anchors relative to the real window instead of hardcoded coordinates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.3 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
│ ├── server_browser.tscn/gd ← server browser UI (built, not in active flow yet)
│ ├── 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
├── 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()
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
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
├── CASUAL → world.tscn
│ ├── TeamSelect overlay: pick race (2 random of 3 offered)
│ ├── TeamSelect overlay: pick ship
│ └── Player spawns → game live
└── RANKED → disabled (coming soon)
Note:
server_browser.tscnwas built (Task 1) and is functional, but the current flow bypasses it — CASUAL goes directly to the world and TeamSelect handles name/race/ship. The server browser will be re-integrated when real multiplayer server listing is built. It still uses the old hardcoded-1280px layout style (see Display below) — not yet updated since it isn't reachable in the live flow.
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).