Files
client/CLAUDE.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

50 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spacewar
2D multiplayer space shooter inspired by Subspace Continuum. Built in **Godot 4.7** (GDScript). Target platform: Steam Deck + PC. Three races fight for galaxy control across casual (25v25) and ranked (5v5) modes.
## Design Docs (`overview/`)
| File | Contents |
|------|----------|
| `overview.md` | Concept, core loop, game flow, match sizes |
| `racesclasses.md` | 3 chosen races, 5 ship classes each |
| `multiplayer.md` | Maps, game modes, galaxy war meta |
| `bots.md` | Bot fill rules for casual matches |
| `tech.md` | Engine, networking architecture, checklist |
| `money.md` | Monetization strategy |
| `structure.md` | Project file tree, scene graph, input map |
Matchmaking API (`matchmaking-api/`, separate FastAPI + Postgres service, own
`README.md`) is a second codebase alongside the Godot project — see its
README for how to run/extend it.
## Completed
1. ~~Server browser~~ — superseded by item 14's `menu/server_select.gd`
2. ~~Main menu CASUAL/RANKED tiles~~ — superseded by item 14's HL2-style nav menu
3. **In-game pause menu** — ESC / controller Start button toggles overlay; game keeps running; RESUME, SETTINGS (stub), SELECT TEAM (live team swap — see item 12), QUIT TO MENU, QUIT TO DESKTOP
4. **Team & ship selection** — shows on world load before player spawns; 2 races randomly offered per match, decided once by the server so every player sees the same pair; ship grid with sprites; player spawns with chosen ship after selection
5. **Real race art wired in** — Terran Republic, Mechanos Sovereignty, and Vorg Swarm each have their own 5-ship roster (Interceptor/Gunship/Bomber/Support/Heavy) with real sprites cropped from uploaded concept sheets (`assets/images/ships/<race>/`); placeholder `example_ships` removed
6. **Multiplayer networking (movement + combat)** — ENet authoritative server; networked ship spawning with client-side prediction + server reconciliation for movement; server-authoritative bullets, health, death/respawn all broadcast to every peer (see `overview/tech.md`)
7. **Matchmaking API + client wiring** — FastAPI + Postgres service (`matchmaking-api/`) for casual/ranked queueing; main menu CASUAL/RANKED buttons queue via the API, poll for a match, then connect to the assigned server. Currently only one dev server is registered (auto-seeded on API startup) — see Current Tasks
8. **In-game chat** — T focuses "all" chat (whole match, both teams), Y focuses "team" chat (peers sharing the sender's race, since race doubles as team); Enter sends, Esc cancels; last 10 messages shown bottom-left, WoW-style translucent log + input line (`chat/chat_box.gd`); server-relayed and team-filtered via `ChatManager` autoload (`autoload/chat_manager.gd`)
9. **Bot fill for casual** — each of the match's 2 offered races is kept at a minimum of `GameConfig.bot_min_team_size` (7) total humans+bots; bots spawn/despawn reactively as players join/leave (`bots/bot_manager.gd`, server-authoritative autoload). Bots always fly their race's fighter/Interceptor (`race.ships[0]`) and use negative peer_ids, which lets them ride every existing networked-ship system (spawning, loadout replication, position/health sync, bullet attribution) with zero special-casing. AI (`bots/bot_ai.gd`, `class_name BotAI`) is a simple seek-nearest-enemy-and-shoot brain, deliberately isolated from ship networking code so future tuning/behavior changes stay contained to that one file. See `bots.md`. Casual matchmaking now forms with just 1 real player queued (bots pad the rest) — see `matchmaking-api/README.md`.
10. **Borderless fullscreen, no UI/world scaling** — launches at the real screen resolution with stretch mode disabled (1 game pixel = 1 screen pixel), instead of scaling the Steam-Deck-matched 1280×800 design canvas up to fill PC monitors (which looked zoomed in). PC monitors now just reveal more world/HUD instead. Every menu screen (`main_menu.gd`, `team_select.gd`, `chat/chat_box.gd`) was reworked to position itself via anchors relative to the real window instead of hardcoded 1280×800 pixel coordinates — see `structure.md`'s Display section, including a real `push_opposite_anchor` footgun hit along the way. `menu/server_browser.gd` was NOT updated at the time (still hardcoded, but unreachable in the live flow) — it has since been deleted, see item 14.
11. **Player list HUD** — top-left roster of every connected player, reactive to `PlayerRegistry`'s `loadout_updated`/`player_removed` signals rather than polled (`hud/player_list.gd`, added to `world.tscn`). White entries are the local player's team (same race), yellow are the enemy team; bots (negative peer_id) get a trailing " (b)". Teammates are sorted to the top.
12. **Select Team in pause menu (live team swap)** — pause menu's SELECT TEAM reopens `TeamSelect` mid-match instead of only offering it once pre-spawn; re-picking a race/ship reuses the existing `PlayerRegistry.submit_local_loadout``loadout_updated``Ship._init_player`/`_server_respawn` path, so the swap gets a fresh respawn + invincibility window for free. `GameConfig.team_select_focused` (mirrors `chat_focused`) blocks ship input while the picker is open, and Esc cancels back out without swapping (only once `TeamSelect` has already been used to reopen — the original mandatory pre-spawn pick still can't be cancelled). Fixed a bot-fill gap this surfaced: `PlayerRegistry.race_changed` now fires alongside `loadout_updated` so `BotManager` rebalances the race the player *left*, not just the one they joined.
13. **Live headcounts + roster + team-full lock on the race-selection screen** — each race box on `TeamSelect`'s "CHOOSE YOUR FACTION" screen now shows a live human player count and the roster of names below it, reactive to `PlayerRegistry.loadout_updated`/`player_removed` (bots excluded from both — they're padding, not a player's actual choice). If one race has more than 2 more humans than the other, the larger side is disabled and reads "TEAM FULL" (except for a player already on that race, so reopening the picker to pick a different ship never locks you out of your own team). This is one shared code path (`_show_race_selection()`/`_refresh_race_boxes()`), so it applies identically to the initial pre-spawn pick and to item 12's mid-match SELECT TEAM reopen — no separate implementation needed for the two.
14. **HL2-style main menu + real server select**`main_menu.gd` rebuilt as a plain white-on-space vertical nav (QUICK PLAY, SERVER SELECT, OPTIONS stub, PROFILE, QUIT) instead of the old big CASUAL/RANKED tiles; RANKED is gone entirely (not just disabled) until ranked matchmaking is real. Top-right shows callsign/rank/level (rank/level still placeholders, same as before). Callsign entry moved into a PROFILE overlay, auto-opened the first time QUICK PLAY is pressed with no callsign set. New `menu/server_select.gd`/`.tscn` replaces the retired `menu/server_browser.gd` — lists real servers from the matchmaking API's `GET /servers`, lets the player pick one and connect directly via `NetworkManager.join_server` (race/ship selection still happens in-world via TeamSelect, same as the matchmaking-queue path). `MatchmakingClient.list_servers()` added as the public wrapper other screens should use for this — don't call `_request` directly from outside the autoload.
15. **Ranked mode removed backend-side + server population/ping** — item 14 already dropped ranked from the menu UI, but the matchmaking API still had a `Mode.ranked` queue path (MMR-sorted matching, `RANKED_TEAM_SIZE`) with no client ever hitting it; that's now deleted (`app/models.py`/`config.py`/`queue_manager.py`/`schemas.py` in `matchmaking-api/`) — `Mode` only has `casual`. Note this is distinct from the CADET/PILOT/.../LEGEND rank-tier *display* system (`Player.mmr`, `/ranks`, `/stats`) backing the main menu's profile badge, which stays (still placeholder-driven, see Current Tasks). `GameServer` gained `player_count`/`max_players` columns, returned by `GET /servers` and settable via `POST /servers/register`; API startup now seeds 3 demo servers (`127.0.0.1:7777/7778/7779`, `player_count` 40/20/10 of 50) instead of one. `menu/server_select.gd` shows these as DB-fallback PLAYERS/PING columns, then actively overrides them per-row with a live probe: `NetworkManager` now runs a `UDPServer` on game-port+10000 (`_start_query_responder`) that answers a raw `"SPACEWAR_PING"` datagram with live `PlayerRegistry.players.size()`/`MAX_PLAYERS`, separate from the ENet game port since ENet won't answer arbitrary UDP itself — same game-port/query-port split classic server browsers use (offset is large, not +1, since local dev runs servers on sequential ports and a +1 query port would alias onto the next server's actual game port). Only `:7777` is realistically reachable without running extra local servers, so the other two demo rows show DB numbers with ping timing out until real servers register there. Post-connect, `hud/ping_display.gd` shows the local client's live RTT top-right using ENet's own `ENetPacketPeer.get_statistic(PEER_ROUND_TRIP_TIME)` on peer 1 (the server) — no custom protocol needed once already connected, unlike the pre-connect server-select probe.
16. **Real game-server pool (self-registration + heartbeat + stale sweep)**`NetworkManager.host_server()` now calls the new `MatchmakingClient.register_server()` once on boot and then every `HEARTBEAT_INTERVAL` (8s) via a `Timer`, reporting live `PlayerRegistry.players.size()`/`MAX_PLAYERS` — item 15's demo-seeded rows are no longer the only thing populating `GET /servers`; a real server registering on the same `(ip, port, mode)` as a demo row just takes it over in place, no special-casing needed. `--server-ip=` is a new cmdline arg for the IP a hosted server advertises (defaults to loopback for local dev — see the reachability note already in `matchmaking-api/README.md` about `DEV_SEED_SERVER_IP`, same constraint applies here). API-side, `POST /servers/register` now computes `status` from `player_count`/`max_players` (`full` vs `available`) instead of always writing `available`, and a new background loop (`sweep_stale_servers` in `app/routers/servers.py`, run every 5s from `main.py`'s lifespan) marks any server `offline` once its `last_heartbeat` exceeds `server_stale_seconds` (20s) — catches a crashed/killed server that never got to deregister cleanly, so a dead server doesn't sit in the list looking joinable forever. `menu/server_select.gd`'s connect button now also blocks (with a message) on `status == "full"`, not just `"offline"`.
17. **Race roster replaced (Terran/Mechanos/Vorg → Apex Dynamics/Inner Sphere Navy/Outer Rim Collective)**`team_select.gd`'s `RACES` now points at the 3 factions from `overview/racesclasses.md`'s pivot, each with a 3-ship Fighter/Gunner/Tank roster (Lancet/Pulsar/Sovereign, Patriot/Barrage/Behemoth, Rail-Jack/Scrap-Spitter/Iron-Clad) instead of the old 5-ship Interceptor/Gunship/Bomber/Support/Heavy set; `SPEED_BY_ROLE` shrunk to match. Art was extracted from 3 user-provided concept sheets (originally dropped at `assets/images/ship/`, now cropped per-ship into `assets/images/ships/apex|isn|orc/` with a `source/` copy of each sheet, same convention as the old race folders) — background removed via flood-fill + largest-connected-component matting (plain background for Apex, starfield for ORC, grid-lined UI panels for ISN, each needing different thresholding). ISN and ORC's source art was drawn nose-*sideways*; both were rotated 90° before saving since this project's ship rotation convention is nose-up at `rotation = 0` (`Vector2.UP.rotated(rotation)` in `ship_movement.gd`) — Apex's source art was already nose-up. `scale` values were computed with the same per-role target-on-screen-height normalization the old roster used (Fighter/Gunner/Tank targets reuse the old Interceptor/Gunship/Heavy heights, ~52.5/58.5/82.5px) so ship sizes read consistently across factions. Verified by hosting a real server and screenshotting bot fill flying with the new sprites — no load errors, transparency and nose-up orientation both correct. The old `terran/mech/vorg` asset folders are left on disk but unreferenced (no code points at them); not deleted since that wasn't asked for.
## Current Tasks
- [ ] Asteroids and environment hazards
- [ ] Sound effects (thrust, shoot, explosion, UI)
- [ ] Options screen — currently just a disabled stub button on the main menu
- [ ] Real rank/level backend — main menu's top-right badge and RANK_DATA are still placeholders (`CURRENT_RANK`/`CURRENT_LEVEL` constants in `main_menu.gd`)
- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`)
- [ ] Real account-linked identity — GodotSteam auth + VAC still not started; `callsign` is the only player identity today
- [ ] Lag compensation (basic rewind) — matters once testing moves beyond localhost