Add match structure (countdown, timer, scoreboard, banner, persistent stats) and capital-ship fleet polish

Match phases (autoload/match_manager.gd, world/game_modes/):
- New server-authoritative MatchManager loops PRE_MATCH (30s countdown,
  ships held invisible/uncollidable, flagships creep into formation) ->
  IN_PROGRESS (active GameMode's clock runs) -> POST_MATCH (winner
  banner, result reported to matchmaking-api) -> back to a fresh
  PRE_MATCH forever, matching the always-on server-pool model instead
  of kicking players to the menu at match end.
- Win-condition logic lives in a new GameMode abstraction (game_mode.gd
  base + team_deathmatch_mode.gd, the only mode so far) so a future
  mode is a new subclass plus one factory branch, no timer/scoreboard/
  banner code changes needed.
- Three new HUD pieces: match_timer.gd (countdown/clock), scoreboard.gd
  (hold-Tab two-team panel), match_banner.gd (winner banner).
- New MatchStats autoload tracks per-match kills/deaths by peer_id
  (including bots), hooked into kill_feed_manager's existing
  report_kill() call site.

Persistent stats (matchmaking-api/):
- Player gains kills/deaths/hours_played columns; new
  POST /matches/report endpoint (server-only, called once at
  POST_MATCH) upserts each real player's totals by callsign via a
  shared app/crud.py helper also used by the matchmaking-queue join
  path. GET /stats/{callsign} returns the new fields alongside mmr/
  wins/losses.

Capital-ship fleet polish (world/flagship.gd, world/world.gd,
ships/ship_movement.gd, autoload/game_config.gd):
- Flagship formations are now a clean vertical line (no per-ship
  position/rotation jitter) so play_creep_in()'s rigid-group tween
  reads as one disciplined fleet arriving together, rising from
  directly below (not a random compass direction) over the full 30s
  countdown.
- Fixed a real bug where the creep-in tween only ever played on the
  server -- MatchManager._run_pre_match() called straight into World,
  server-only code a remote client's own process never runs, leaving
  their flagships static all match. World now triggers it off
  MatchManager.phase_changed instead, which fires identically on every
  peer.
- Fixed a second bug (only reachable on a fresh server boot's very
  first spawn): a phase==PRE_MATCH check that's true even before the
  match loop has genuinely started that phase for the first time fired
  play_creep_in() with a bogus zero-duration tween, corrupting the
  target the real 30s tween read moments later -- flagships would
  settle 4000 units off from their intended formation slot and fire
  from there instead. Guarded on get_remaining_seconds() > 0 too.
- The held ship's camera now actively tracks its own team's flagship
  centroid every tick during the countdown (position_smoothing
  disabled for the hold, since it fights a manually-driven target and
  was the reason the fleet read as invisible) instead of sitting fixed
  and wide-angle; local offset/zoom/smoothing are explicitly reset on
  release so control handback doesn't inherit a stale camera transform.
- _apply_pre_match_hold()/_apply_respawn() now set _dead/
  _held_for_pre_match inside the RPC itself, not just in the
  server-only caller -- those flags never reached remote clients
  before, so WASD wasn't actually blocked for them during the hold.
- Ships now launch from a narrow point directly beneath their own
  flagship formation instead of a full-circle scatter around the spawn
  marker (which could land a spawn behind/inside a hull). Bumped
  flagship_defense_radius so it still comfortably reaches a player who
  flies a straight line to the enemy side without correcting for that
  new offset.

ISN gets a Stealth Corvette hull mixed into its flagship formation
(assets/images/ships/isn/), banner art renamed off opaque UUID
filenames to isn_banner.jpeg/orc_banner.jpeg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 11:40:15 -04:00
parent a94bccc57b
commit be5c41f82f
59 changed files with 1793 additions and 207 deletions
+4
View File
@@ -0,0 +1,4 @@
I wanted to work on making the matches seem more official with winners and losers . Each player will have a history of total kills, total wins, total losses, hours in a match.
I wanted the current match to have a timer. Each match is 7 minutes long. The way the match works is all
players are at the startpoint where the ships are on there perspective side. They dont see there ships yet. The timer starts counting down from 30 seconds. the 3 capital ships slowly start creeping to there spots. WHen the countdown reaches 0 the players or bots ships appear in the spawn. The match will be 7 minutes long. In that time there will be stats for each player. Total kills, total deaths. At the end of the match the team with the most kills wins. There will be a banner of the winner displayhed on the map saying ORC or ISN wins . A user can press tab to see a current scoreboard similiar to csgo.
. Banners are located here /mnt/code/spacewar/spacewar/assets/images/banners/.
+28
View File
@@ -0,0 +1,28 @@
# Match Framework Implementation Progress
Tracking checklist for the plan in `overview/map1.md` (pre-match countdown, 7-minute
timer, kills/deaths, scoreboard, winner banner, persistent stats, reusable
GameMode/map framework). Full design plan: see the approved plan this session
(sections referenced below). Check items off as they're completed.
- [x] 1. GameMode abstraction — `world/game_modes/game_mode.gd` (base) + `team_deathmatch_mode.gd`
- [x] 2. `MatchStats` autoload — per-match kills/deaths, hook into `kill_feed_manager.gd`
- [x] 3. `MatchManager` autoload — PRE_MATCH/IN_PROGRESS/POST_MATCH phase state machine
- [x] 4. Flagship creep-in tween — `flagship.gd:play_creep_in()`, `world.gd:play_flagship_creep_in()`, `GameConfig.flagship_creep_in_distance`
- [x] 5. Ship-spawn gating — `ship_movement.gd` pre-match hold (`_server_respawn` phase gate, `server_hold_for_match_start`/`server_release_from_hold`/`_apply_pre_match_hold`), `world.gd` hold/release helpers
- [x] 6. Map abstraction — `world.gd` `MAPS` array + `_pick_map()` replacing hardcoded `MAP_SCENE`
- [x] 7. Scoreboard HUD — `hud/scoreboard.gd`+`.tscn`, hold-Tab, `scoreboard` input action in `project.godot`
- [x] 8. Winner banner HUD — `hud/match_banner.gd`+`.tscn`, `banner_path` in `team_select.gd` RACES, rename ISN/ORC banner assets
- [x] 9. Match timer HUD — `hud/match_timer.gd`+`.tscn`, top-center MM:SS readout
- [x] 10. Wiring — new autoloads in `project.godot`, new HUD nodes in `world.tscn`, `MatchManager.start(self)` + late-joiner pull RPCs in `world.gd:_ready()`
- [x] 11. Backend schema + endpoint — `matchmaking-api` `Player.kills/deaths/hours_played`, `app/crud.py`, `app/schemas.py`, `app/routers/matches.py` (`POST /matches/report`), `app/routers/stats.py`, `app/main.py` (verified end-to-end via docker compose)
- [x] 12. Godot-side backend call site — `network_manager.gd:get_registered_address()`, `matchmaking_client.gd:report_match_result()`, `MatchManager._report_match_result()`
- [x] 13. Docs update — `CLAUDE.md` new numbered item, `overview/multiplayer.md`, `overview/structure.md`
- [x] 14. Verification — headless server smoke test: clean compile, full PRE_MATCH→IN_PROGRESS→POST_MATCH→loop cycle with a temporarily-sped-up clock, bot-vs-bot kills flowing through MatchStats and correctly deciding the winning race, backend /matches/report round-trip via a real docker compose run. Additionally ran a REAL two-process ENet test (separate server + client, real network handshake, not single-process offline mode): confirmed real peer-connect, correct late-joiner phase/timer pull mid-countdown, and every ship (bots, server's own ghost, the real second client's ship) held/released in perfect lockstep across both processes over two full match loops. Also incidentally confirmed the pre-existing bot-rebalancing logic (item 9) correctly reacted to the real human joining. NOT verified: visual rendering (hidden ships, flagship creep-in tween, Tab scoreboard, winner banner) and the Tab keybind itself, since this sandbox has no display — needs a real client pass on the user's machine.
## All 14 items complete.
Remaining follow-up recommended: a real (non-headless) client session on the user's machine to
visually confirm the countdown/creep-in/scoreboard/banner and to exercise Tab's input-map binding
(see hud/scoreboard.gd's comment about verifying the hand-typed Tab keycode in the Godot editor's
Input Map UI).
+16
View File
@@ -17,6 +17,11 @@
## Ranked Mode — 5v5
> **Status:** design ideas only — not implemented. The matchmaking API's ranked
> queue path was built and then deleted (`CLAUDE.md` item 15) once it became
> clear no client would hit it before casual was solid; it's absent from the
> main menu entirely (not just disabled) until it's built for real.
Competitive, skill-based, MMR/ELO ladder. Penalty for leaving mid-match. Smallmedium maps only.
### Ranked Mode Ideas
@@ -36,10 +41,21 @@ Competitive, skill-based, MMR/ELO ladder. Penalty for leaving mid-match. Small
No penalty for leaving. Larger maps. Chaotic and fun — you can drop in mid-match.
> **Status:** **Team Deathmatch** (the first row below) is implemented — a
> 7-minute timed version of Annihilation's "pure kills" idea (most kills when
> the clock runs out wins, not last-team-standing), plus a 30s pre-match
> countdown, live kill/death tracking, a Tab scoreboard, and a winner banner
> (see `overview/map1.md`, `CLAUDE.md` item 26). It's built through a
> `GameMode` abstraction (`spacewar/world/game_modes/`) specifically so the
> other rows here — Sector Control, King of the Hill, Base Assault, Convoy
> Escort — can each become a new `GameMode` subclass later without changing
> the match-phase/timer/scoreboard/banner machinery around them.
### Casual Mode Ideas
| Mode | Description |
|------|-------------|
| **Team Deathmatch** *(implemented)* | Most kills when the match timer runs out wins. Timed variant of the Annihilation idea below. |
| **Sector Control** | Map divided into sectors. Hold more sectors at time limit. Ties to galactic war meta. |
| **King of the Hill** | One contested zone in the center. Hold it to rack up points. |
| **Annihilation** | Pure kills. Last team standing or first to X kills. Chaos mode. |
+10 -2
View File
@@ -1,13 +1,21 @@
# Networking Implementation Plan
> **Status: complete.** This was the ordered checklist that took Spacewar from
> zero networking code to the real ENet authoritative-server implementation —
> all 10 phases below were executed (see `CLAUDE.md` item 6 and the many items
> after it that build on top of this foundation). Kept as a historical record
> of the approach and ordering; the checkboxes are left unchecked as
> originally written rather than retroactively edited. For current
> architecture, see `tech.md`; for the present file layout, see `structure.md`.
Step-by-step breakdown for taking Spacewar from single-player-only to working
multiplayer. `tech.md` describes the target architecture; this doc is the
ordered checklist for getting there from the current codebase.
**Current state (as of this doc):** zero networking code exists anywhere in
**Original starting state:** zero networking code existed anywhere in
the project. The server browser's JOIN button and the main menu's CASUAL
button just set local `GameConfig` fields and load `world.tscn` — no
connection is ever made. Everything downstream assumes exactly one player.
connection was ever made. Everything downstream assumed exactly one player.
**Guiding principle:** get two peers moving/shooting/dying correctly over
ENet *first*, with ugly/no prediction. Polish feel (prediction,
+21 -21
View File
@@ -2,13 +2,13 @@
## Concept
A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built for **Steam Deck and PC**. Two alien races fight for control of a galaxy. Fast matchmaking like Rocket League — quick in, quick out.
A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built for **Steam Deck and PC**. Two human factions — Inner Sphere Navy (disciplined military) and Outer Rim Collective (scrappy belters), not aliens — fight for control of a galaxy inside the tight corridors of factories, refineries, and shipyards. Fast matchmaking like Rocket League — quick in, quick out.
## Core Loop
1. Launch → Main Menu
2. Enter callsign → click CASUAL
3. World loads → pick your race (both offered every match) → pick your ship
2. Enter callsign (PROFILE overlay, auto-opens first time) → click QUICK PLAY
3. World loads → pick your faction (both offered every match) → pick your ship
4. Fight in a 25v25 battle
5. Return to main menu
@@ -16,45 +16,45 @@ A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built
| Mode | Size | Status |
|------|------|--------|
| Casual | 25v25 (bots fill to 7v7 minimum) | In progress |
| Ranked | 5v5, competitive MMR | Disabled — coming later |
| Casual | 25v25 (bots fill to 7v7 minimum) | Live |
| Ranked | 5v5, competitive MMR | Removed (not just disabled) until it's real — see `CLAUDE.md` Current Tasks |
## Current Game Flow (as built)
```
Main Menu
└─ CASUAL ──► World loads immediately (game active)
Main Menu (HL2-style vertical nav: QUICK PLAY / SERVER SELECT / OPTIONS / PROFILE / QUIT)
└─ QUICK PLAY ──► matchmaking queue (casual) ──► World loads (game active)
(SERVER SELECT skips the queue, connects to a picked server directly)
└─ TeamSelect overlay appears
├─ Pick race (both offered every match)
└─ Pick ship (4 available, sprites shown)
└─ Player spawns centred, invincible briefly
├─ Pick faction (both offered every match)
└─ Pick ship (3 available, sprites shown)
└─ Player spawns centred, invincible briefly, flies in from a random direction
└─ ESC / Start ──► Pause menu overlay
├─ RESUME
├─ SETTINGS (stub)
├─ SELECT TEAM (stub)
├─ SETTINGS (live — audio, frame-rate cap, vsync, window mode, colorblind mode, key rebinding)
├─ SELECT TEAM (live — reopens TeamSelect mid-match, swaps faction/ship with a fresh respawn)
├─ QUIT TO MENU
└─ QUIT TO DESKTOP
```
## Races
## Factions
2 playable races (a third, Apex Dynamics, was cut to simplify the art pipeline) — see `racesclasses.md`. Each race has 5 ship classes:
2 playable factions (a third, Apex Dynamics, was cut to simplify the art pipeline) — see `racesclasses.md`. Each faction has 3 ship classes:
| Class | Role |
|-------|------|
| Interceptor | Fast, agile generalist |
| Bomber | Area damage, slow |
| Support | Heal/rally; allies can attach |
| Stealth | Assassin, hit-and-run |
| Heavy | Tank, massive firepower |
| Fighter | Single, high-precision, high-damage shot |
| Gunner | Rapid-fire spray/burst, more shots for less damage each |
| Tank | Bigger and slower; fires both bullets and missiles |
Races are visually distinct — critical for reading a 25v25 battlefield at a glance.
Factions are visually distinct — critical for reading a 25v25 battlefield at a glance.
## Match Setup
- Both races are fielded every match (order they're presented in is randomized)
- Both factions are fielded every match (order they're presented in is randomized)
- Player picks which of the 2 they fight for, then picks their ship
- Bot fill ensures a minimum of **7v7** in casual; no bots added beyond that
- Each ship has an energy pool spent on firing (Fighter 100/Gunner 150/Tank 250 max, regenerating over time) alongside health — see `CLAUDE.md` item 23
## Design Philosophy
+156 -57
View File
@@ -7,40 +7,67 @@ 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)
── game_config.gd match-wide tuning constants, input-focus flags, persisted user settings (see GameConfig table below)
│ ├── network_manager.gd ← owns the ENetMultiplayerPeer; host_server()/join_server(); server self-registration+heartbeat with matchmaking-api; UDP query responder for pre-connect server-select pings
│ ├── player_registry.gd ← per-peer loadout registry (name/race/ship/role/flame/sound paths), keyed by peer_id; server-relayed to every peer
│ ├── matchmaking_client.gd ← HTTP wrapper around matchmaking-api (queue, list_servers, register_server); don't call _request() directly from outside this file
│ ├── chat_manager.gd ← server-relayed all/team chat
│ ├── kill_feed_manager.gd ← server-authoritative kill-feed broadcast (victim/killer name+race per death); also feeds MatchStats
│ ├── match_stats.gd ← per-match (resets every loop) kills/deaths by peer_id, incl. bots; hooked from kill_feed_manager.gd
│ ├── match_manager.gd ← server-authoritative match-phase state machine (PRE_MATCH/IN_PROGRESS/POST_MATCH, loops forever); owns the active GameMode
│ └── music_manager.gd ← menu background music player, routed through the Music audio bus
├── 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)
│ ├── main_menu.tscn/gd ← entry point / main scene; HL2-style vertical nav (QUICK PLAY, SERVER SELECT, OPTIONS, PROFILE, QUIT)
│ ├── server_select.tscn/gd ← server list UI, backed by matchmaking-api's GET /servers + a live UDP ping probe
│ ├── pause_menu.tscn/gd ← in-game pause overlay (ESC / Start button)
── settings_panel.tscn/gd ← Options screen; one instance shared by main menu (OPTIONS) and pause menu (SETTINGS); audio/frame-rate/vsync/window-mode/colorblind/key-rebind sections
│ └── team_select.tscn/gd ← faction + ship selection overlay (shown on world load and on pause-menu SELECT TEAM)
├── ships/
│ ├── ship.tscn ← player ship scene (formerly node_2d.tscn)
│ ├── ship_movement.gd ← player ship logic
│ ├── ship.tscn ← player ship scene
│ ├── ship_movement.gd ← player ship logic (movement, prediction/reconciliation, health/energy, nameplate interpolation)
│ └── 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
── player_list.tscn/gd ← top-left player roster (white=team, yellow/orange=enemy per colorblind mode, "(b)"=bot)
│ ├── kill_feed.tscn/gd ← last 4 deaths, stacked above ChatBox's history panel
│ ├── mini_map.tscn/gd ← polls game state (hazards, players, flagships), pokes MiniMapView, calls queue_redraw()
│ ├── mini_map_view.gd ← pure Control _draw() surface for the minimap (split out since CanvasLayer can't override _draw())
│ ├── ping_display.tscn/gd ← top-right live RTT via ENetPacketPeer.get_statistic(PEER_ROUND_TRIP_TIME)
│ ├── energy_bar.tscn/gd ← blue center-out mirrored bar; also instanced for the health bar (top of world.tscn)
│ ├── match_timer.tscn/gd ← top-center match clock (PRE_MATCH countdown / IN_PROGRESS 7:00 countdown), driven by MatchManager
│ ├── scoreboard.tscn/gd ← hold-Tab CS:GO-style scoreboard, two team panels (name/role/K/D), driven by MatchStats + the active GameMode
│ └── match_banner.tscn/gd ← full-screen winner banner (ISN/ORC art + "<RACE> WINS"/"DRAW"), shown during MatchManager's POST_MATCH phase
├── 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_ai.gd ← class_name BotAI; seek-nearest-enemy-and-shoot brain, tuned per-bot by BotPersonality
│ ├── bot_personality.gd ← class_name BotPersonality; 5 independent 0-1 traits (aggression/caution/accuracy/reaction/awareness) rolled once per bot-slot
│ └── 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.tscn ← game world; world.gd loads the active map, spawns flagships, brokers bullets/hazard colliders
│ ├── world.gd ← picks/instances a map scene from world/maps/ (MAPS array), decides offered races, spawns flagship formations, hosts MatchManager's ship-hold/creep-in hooks
│ ├── world_tileset.tres ← shared TileSet resource (walls.png + asteroids.png atlas sources)
│ ├── flagship.tscn/gd ← capital-ship point-defense formation (class_name Flagship), see CLAUDE.md item 24; also plays the PRE_MATCH creep-in tween
│ ├── starfield.gdshader ← procedural starfield background
│ ├── game_modes/
│ │ ├── game_mode.gd ← class_name GameMode; base ruleset (win condition, score label, match duration) MatchManager drives every mode through
│ │ └── team_deathmatch_mode.gd ← class_name TeamDeathmatchMode; most-kills-when-clock-runs-out, the only implemented mode today (see overview/map1.md)
│ └── maps/
│ └── map_01.tscn ← hand-painted map: Walls + Asteroids TileMapLayers, team spawn Marker2Ds
│ └── map_01.tscn ← hand-painted map: Walls + Asteroids TileMapLayers (asteroids deal impact damage, not just bounce), team spawn Marker2Ds
└── assets/
├── icon.svg
├── default_bus_layout.tres ← Master/Music/SFX audio buses
└── 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
├── background/skybox/ ← space background PNGs
├── effects/
│ ├── explosion/ ← ship-death explosion animation frames
└── bullets/ ← per-race/role bullet sprites, incl. flagship turret fire
├── banners/ ← faction banner art
├── ships/isn/, ships/orc/ ← live faction ship art (idle + _flame sprites), each with a source/ concept-sheet copy
└── 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`).
> `assets/images/ships/apex/` and the older `terran/mech/vorg` folders are left on disk but unreferenced by any code — not deleted, see CLAUDE.md items 17/19.
## Scene Graph
@@ -56,33 +83,43 @@ ServerSelect (Control) ← server_select.gd builds all UI in _ready(), fetches
### `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 (Node2D) ← world.gd instances a map into MapContainer, spawns flagships, brokers bullet/hazard colliders
├── MapContainer (Node2D) ← holds the instanced map (world/maps/map_01.tscn), background included
├── Players (Node2D)per-peer ship.tscn instances live here
├── MultiplayerSpawner ← spawns/despawns ships into Players
├── Bullets (Node2D) ← server-spawned bullet.tscn instances
├── BulletSpawner (MultiplayerSpawner)
├── PauseMenu (CanvasLayer, layer=10) ← menu/pause_menu.tscn
├── TeamSelect (CanvasLayer, layer=20)menu/team_select.tscn; also reopened live from the pause menu's SELECT TEAM
── SettingsPanel (CanvasLayer, layer=25) ← menu/settings_panel.tscn; opened by pause menu's SETTINGS
├── ChatBox (CanvasLayer, layer=5) ← chat/chat_box.tscn
├── PlayerList (CanvasLayer) ← hud/player_list.tscn
├── KillFeed (CanvasLayer, layer=5) ← hud/kill_feed.tscn
├── MiniMap ← hud/mini_map.tscn
├── PingDisplay ← hud/ping_display.tscn
├── HealthBar ← hud/energy_bar.tscn instance, reused for health
├── EnergyBar ← hud/energy_bar.tscn instance
├── MatchTimer (CanvasLayer, layer=4) ← hud/match_timer.tscn
├── Scoreboard (CanvasLayer, layer=6) ← hud/scoreboard.tscn; visible only while Tab is held
└── MatchBanner (CanvasLayer, layer=30) ← hud/match_banner.tscn; visible only during MatchManager's POST_MATCH phase
```
### `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
├── Background (TextureRect) ← per-map space background; procedural starfield.gdshader, not a tiled skybox
├── Walls (TileMapLayer) ← painted by hand in the Godot Tile Editor, uses world_tileset.tres; bounces ships (environment_wall group)
├── Asteroids (TileMapLayer) ← painted by hand, same shared tileset; bounces AND deals impact damage (environment_hazard group, GameConfig.ship_wall_damage)
└── SpawnPoints (Node2D)
├── TeamASpawn1 (Marker2D) ← group "team_a_spawn"
└── TeamBSpawn1 (Marker2D) ← group "team_b_spawn"
├── TeamASpawn1 (Marker2D) ← group "team_a_spawn"; also where world.gd spawns Team A's flagship formation
└── TeamBSpawn1 (Marker2D) ← group "team_b_spawn"; also where world.gd spawns Team B's flagship formation
```
### `ships/ship.tscn` — player ship
```
Player (CharacterBody2D) ← ship_movement.gd
├── CollisionShape2D ← CircleShape2D
├── Sprite2D ← texture set at runtime from GameConfig.player_ship_path
├── Sprite2D ← texture set at runtime from PlayerRegistry's per-peer ship_path
└── VisibleOnScreenNotifier2D
```
@@ -98,6 +135,7 @@ Player (CharacterBody2D) ← ship_movement.gd
| `toggle_pause` | Esc | Start / Options button (JoyButton 6) |
| `chat_all` | T | — |
| `chat_team` | Y | — |
| `scoreboard` | Tab | — |
## Game Flow
@@ -108,7 +146,7 @@ main_menu.tscn
│ ├── 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
├── OPTIONS → settings_panel.tscn (audio, frame-rate cap, vsync, window mode, colorblind mode, key rebinding) — same panel the in-game pause menu's SETTINGS opens
├── PROFILE → callsign-edit overlay (also auto-opens from QUICK PLAY if no callsign is set)
└── QUIT
```
@@ -142,34 +180,61 @@ 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 |
Per-peer identity (`player_race`, `player_ship_path`) moved out of `GameConfig`
early on, per the networking plan's Phase 2 — that's `PlayerRegistry`'s job
now (see below). `GameConfig` today holds match-wide tuning constants plus
persisted user settings. Grouped by area rather than listed exhaustively
(~70 properties) — see `autoload/game_config.gd` for the full list:
| Area | Examples | Notes |
|------|----------|-------|
| Persisted settings | `max_fps`, `vsync_mode`, `window_mode`, `master_volume`/`music_volume`/`sfx_volume`, `colorblind_mode`, `keybinds` | Loaded/applied in `_ready()`, written to `user://settings.cfg` by `save_settings()`; each has a `set_*()` that applies live |
| Ship movement | `ship_thrust`, `ship_max_speed`, `ship_rotation_speed`, `ship_rotation_ramp_delay` | Tuning constants |
| Combat | `ship_fire_rate`, `bullet_speed`, `ship_bullet_damage_by_role`, `ship_bullet_count_by_role`, `bullet_max_range` | Per-role dictionaries keyed `"Fighter"/"Gunner"/"Tank"` |
| Energy | `ship_max_energy_by_role`, `ship_bullet_energy_cost_by_role`, `ship_energy_regen_rate` | See CLAUDE.md item 23 |
| Health/respawn | `ship_max_health`, `ship_respawn_delay`, `ship_invincibility_time`, `ship_wall_damage` | `ship_wall_damage` applies on both wall bounces and asteroid hazard hits |
| Collision/visuals | `ship_bounce_restitution`, `ship_collision_damage_scale`, `ship_scale_factor`, `ship_hitbox_scale`, `explosion_scale`, `damage_number_*` | |
| Colors | `team_color`, `enemy_color`, `colorblind_mode` (swaps `enemy_color` between `ENEMY_COLOR_DEFAULT`/`ENEMY_COLOR_COLORBLIND`) | Read by nameplates, `hud/player_list.gd`, `hud/mini_map.gd` |
| Camera/world | `camera_zoom`, `world_bounds`, `spawn_area_inner_radius`/`outer_radius` | |
| Flagships | `flagship_count_per_spawn`, `flagship_defense_radius`, `flagship_fire_rate`, `flagship_missile_*`, `flagship_bullet_*`, `flagship_aim_jitter_px`, `flagship_creep_in_distance` | See CLAUDE.md item 24; `flagship_creep_in_distance` is the PRE_MATCH cosmetic tween's start offset |
| Match structure | `default_game_mode_id`, `match_pre_match_duration`, `match_duration`, `match_post_match_duration` | Read by `MatchManager`'s phase state machine — see item 26 |
| Input-focus gates | `chat_focused`, `team_select_focused`, `settings_focused` | Each blocks ship movement/fire input while its overlay owns keyboard focus |
| Bot tuning | `bot_min_team_size`, `bot_engage_range`, `bot_stop_distance_min`/`max`, `bot_retreat_health_frac_min`/`max`, `bot_aim_tolerance_best_deg`/`worst_deg`, `bot_aim_jitter_max_px`, `bot_reaction_update_best_sec`/`worst_sec`, `bot_awareness_range_min`/`max` | Min/max pairs are interpolated per-bot by that bot's rolled `BotPersonality` (0=worst trait, 1=best) |
## Key Autoload — `NetworkManager`
Owns the `ENetMultiplayerPeer` (`autoload/network_manager.gd`). `host_server(port)` /
`join_server(ip, port)`. A hosted server self-registers with matchmaking-api on boot
and re-heartbeats every 8s via `MatchmakingClient.register_server()` (reusing one
persistent `HTTPRequest` node — a fresh one per call caused a periodic stutter, see
CLAUDE.md item 20/21). Also runs a `UDPServer` on game-port+10000 (`_start_query_responder`)
that answers a raw `"SPACEWAR_PING"` datagram with live player count, used by
`menu/server_select.gd`'s pre-connect probe (post-connect ping uses ENet's own RTT stat
instead, via `hud/ping_display.gd`).
## Key Autoload — `PlayerRegistry`
Per-peer loadout registry (`autoload/player_registry.gd`), `Dictionary[peer_id, info]`
where `info` holds `name`/`race`/`ship_path`/`ship_scale`/`ship_speed_factor`/`role`/
`ship_flame_path`/`ship_sound_path`. `submit_local_loadout()` (real players) and
`register_bot()` (bots, negative peer_ids) both funnel through the same server-relayed
storage, so every system that reads a loadout (spawning, HUD, bots, bullets) treats
players and bots identically. `loadout_updated`/`race_changed`/`player_removed` signals
drive `BotManager`, `hud/player_list.gd`, and TeamSelect's live headcounts.
## Key Autoload — `MatchmakingClient`
HTTP wrapper around `matchmaking-api` (`autoload/matchmaking_client.gd`) — queueing,
`list_servers()` (backs `menu/server_select.gd`'s `GET /servers`), and
`register_server()`/heartbeat. Don't call the internal `_request()` directly from
outside this file; use the public wrapper methods.
## 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`.
match's 2 offered factions at `GameConfig.bot_min_team_size` total humans+bots,
reacting to `PlayerRegistry.loadout_updated`/`race_changed`/`player_removed` and
`World.decide_offered_races()`. Also rolls a `BotPersonality` per bot-slot. See `overview/bots.md`.
## Key Autoload — `ChatManager`
@@ -177,3 +242,37 @@ Server-relayed chat (`autoload/chat_manager.gd`). `send_chat(text, team_only)` s
`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`).
## Key Autoload — `KillFeedManager`
Server-authoritative kill-feed broadcast (`autoload/kill_feed_manager.gd`). One RPC per
death, called from `ship_movement.gd`'s `_die()` — no client → server leg, unlike chat.
`kill_reported` signal feeds `hud/kill_feed.gd`'s last-4-lines display, and
`report_kill()` also calls `MatchStats.record_kill()` — the one hook point both
systems share.
## Key Autoload — `MatchStats`
Per-match (resets every `MatchManager` loop) kills/deaths by `peer_id`, including
bots (`autoload/match_stats.gd`). `get_team_kills(race_id)` sums a whole team via
`PlayerRegistry`. `stats_updated`/`stats_reset` signals drive `hud/scoreboard.gd`.
## Key Autoload — `MatchManager`
Server-authoritative match-phase state machine (`autoload/match_manager.gd`):
`PRE_MATCH` (30s countdown, ships held via `ship_movement.gd`'s
`server_hold_for_match_start()`, flagships cosmetically creep into formation) →
`IN_PROGRESS` (the active `GameMode`'s clock runs, ships released) → `POST_MATCH`
(winner banner, match result reported to matchmaking-api) → loops back into a
fresh `PRE_MATCH` indefinitely — no menu kick, matching this project's always-on
server-pool model (see item 16). Follows the same "decide once on the server,
RPC-broadcast the value, late joiners pull it" pattern `world.gd`'s
`decide_offered_races()` established. Owns the active `GameMode`
(`world/game_modes/`) instance, built identically on every peer off the shared
`GameConfig.default_game_mode_id` constant — adding a new mode is a new
`GameMode` subclass plus one factory branch, no changes to this state machine.
## Key Autoload — `MusicManager`
Menu background-music player (`autoload/music_manager.gd`), routed through the Music
audio bus so `GameConfig.music_volume` controls it live.
-39
View File
@@ -1,39 +0,0 @@
# Task Log
Task 1
I want to build an energy system for each ship. Below the health put an energy of 100/100 for the fighter/ 150/150 for middle ship and big ship 250 energy. Each bullet takes 75 to shoot. Have a blue energy bar that is mirrord top middle appear at the top . Have it take 2.5 seconds to refill 100
## Completed
| # | Task | Notes |
|---|------|-------|
| 0 | Format all design docs as Markdown | Done |
| 24 | Energy system (Fighter 100 / Gunner 150 / Tank 250, 75 per shot, blue center-out mirrored bar top-middle, +100 per 2.5s regen) | `GameConfig.ship_max_energy_by_role`/`bullet_energy_cost`/`ship_energy_regen_rate`; server-authoritative in `ships/ship_movement.gd` (broadcast via `_receive_state`, same pattern as health); role threaded through `PlayerRegistry` loadout + bots; text readout `HUD/EnergyLabel` below HP, visual bar `hud/energy_bar.gd`/`.tscn` |
| 1 | Design 10 races, pick 3 | Races 1, 4, 5 chosen — see `racesclasses.md` |
| 2 | Ranked + casual mode ideas | See `multiplayer.md` |
| 3 | Multiplayer engineering doc | See `tech.md` |
| 4 | ~~Server browser screen~~ | Superseded by #21's `menu/server_select.gd` |
| 5 | ~~Main menu (CASUAL/RANKED tiles)~~ | Superseded by #21's HL2-style nav menu |
| 6 | In-game pause menu | ESC / controller Start; game runs behind it; working: RESUME, QUIT TO MENU, QUIT TO DESKTOP |
| 7 | Team & ship selection | On world load; 2 random races offered; ship grid with real sprites; player spawns after |
| 8 | Replace placeholder races with chosen 3 (Terran Republic, Mechanos Sovereignty, Vorg Swarm) | Done |
| 9 | Real ship sprites for all 3 races (5 ships each) | Done |
| 13 | Multiplayer networking — ENet authoritative server | Done |
| 18 | In-game chat (T=all, Y=team, last 10 messages, bottom-left) | `chat/chat_box.gd` + `autoload/chat_manager.gd` — see `chat.md` |
| 11 | Bot fill for casual (min 7v7) | `bots/bot_manager.gd` + `bots/bot_ai.gd` — see `bots.md` |
| 17 | Select Team in pause menu (live team swap) | Reopens `TeamSelect` mid-match via `PlayerRegistry.submit_local_loadout`'s existing respawn path; bot fill rebalances the vacated race too (`PlayerRegistry.race_changed`) |
| 20 | Live headcount/roster + TEAM FULL lock on race select | Shared by the initial pick and #17's reopen; humans only (bots excluded), blocks joining a race >2 humans ahead of the other, exempts your own current race |
| 21 | HL2-style main menu + real server select | `main_menu.gd` rebuilt as plain white vertical nav (QUICK PLAY/SERVER SELECT/OPTIONS/PROFILE/QUIT); RANKED removed entirely; new `menu/server_select.gd` lists real servers via `MatchmakingClient.list_servers()` (`GET /servers`) and connects directly, retiring `menu/server_browser.gd` |
## Up Next
| # | Task | Priority |
|---|------|----------|
| 10 | Asteroids + environment hazards | Medium |
| 12 | Sound effects (thrust, shoot, explosion, UI clicks) | Medium |
| 14 | Galaxy war meta + sector control | Low (post-networking) |
| 15 | Ranked matchmaking + MMR | Low (post-networking) |
| 16 | Settings screen (audio, controls, display) | Low |
| 22 | Options screen (currently a disabled stub on the main menu) | Low |
| 23 | Real rank/level backend for the main menu's top-right badge | Low |
+8 -4
View File
@@ -75,10 +75,14 @@ diverged from the original plan below:
then connects directly to the returned server IP/port
Not yet built: session tokens (server IP/port are handed back unauthenticated
— fine for local dev, not for a real deployment), and a real server pool —
only one dev server is registered right now (auto-seeded on API startup);
`POST /servers/register` exists for real servers to self-register/heartbeat
but nothing calls it yet.
— fine for local dev, not for a real deployment).
Real server pool is now live: `NetworkManager.host_server()` calls
`POST /servers/register` once on boot and re-heartbeats every 8s, reporting
live player counts; a real server registering on the same (ip, port, mode)
as one of the 3 demo-seeded rows just takes it over in place. A background
sweep marks any server `offline` once its heartbeat goes stale (crash/kill
without clean deregister). See `CLAUDE.md` item 16.
### Steam Integration