Files
client/overview/structure.md
T
anekdotin 7b44b2f2dc Add real server pool, race roster overhaul, and ship energy system
Server browsing & matchmaking:
- HL2-style main menu (QUICK PLAY/SERVER SELECT/OPTIONS/PROFILE/QUIT)
  replaces the old CASUAL/RANKED tiles; RANKED removed end-to-end
  (menu, matchmaking-api's Mode.ranked queue path, MMR matching) until
  ranked is real.
- New menu/server_select.gd lists real servers from the matchmaking
  API's GET /servers and connects directly, retiring the old
  unreachable menu/server_browser.gd.
- Real game-server pool: NetworkManager.host_server() self-registers on
  boot and heartbeats every 8s with live player counts; API computes
  available/full status from player_count, and a background sweep marks
  any server offline once its heartbeat goes stale (catches a crashed
  server that never deregistered).
- Server-select rows get a live UDP ping probe (query port = game port
  + 10000) instead of trusting stale DB numbers; post-connect HUD shows
  live RTT off ENet's own peer stats.

Race roster overhaul:
- Swapped Terran/Mechanos/Vorg for the pivoted roster — Apex Dynamics,
  Inner Sphere Navy, Outer Rim Collective — each with a 3-ship
  Fighter/Gunner/Tank lineup, art cropped from concept sheets with
  background removal + orientation fixes per sheet.
- Live headcount + roster + "TEAM FULL" lock on the race-select screen,
  shared between the initial pre-spawn pick and the pause menu's live
  SELECT TEAM swap.
- Bot personalities (bots/bot_personality.gd): aggression/caution/
  accuracy/reaction/awareness traits rolled per bot instead of one
  fixed AI profile.

HUD additions:
- Player list (roster, teammates white/enemies yellow, bots flagged),
  kill feed, minimap, and explosion VFX on death.
- Health and energy now render as bars (hud/stat_bar.gd) instead of
  text in the top-left HUD.

Ship energy system:
- Per-role max energy (Fighter 100 / Gunner 150 / Tank 250), 75 energy
  per shot, flat regen (100 per 2.5s), fully server-authoritative and
  piggybacked on the existing per-tick state broadcast alongside health.
- New blue "mirrored" bar top-middle of the screen (hud/energy_bar.gd)
  whose fill drains from both edges toward the center instead of
  left-to-right.

Ship handling tuning:
- Turn rate reduced (4.0 -> 1.0 rad/s) so a quick tap no longer
  over-rotates; holding past 0.15s ramps to double speed (2.0 rad/s) for
  fast full turns, gated the same way damage already is so replay during
  reconciliation can't double-count the hold timer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 17:56:33 -04:00

8.7 KiB
Raw Blame History

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 (2 random of 3 offered)
  │     ├── 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).