Add game mode categories, character creation, on-foot ship/hub mode, RAM currency, and in-ship navigation

Bundles several sessions' worth of previously uncommitted work: map
categories with Domination/Conquest/King of the Hill mode stubs and a
server-list mode filter; a procedurally-drawn character-creation screen
replacing the old callsign-only PROFILE overlay; the on-foot groundwork
(walkable station hub, ship interior, character controller) plus the RAM
currency backend; and today's addition, an in-ship Helldivers-2-style
navigation table that QUICK PLAY's queue/connect flow and the Belters/
Military hub travel now live behind, with hub-and-ship return paths and a
context-aware pause menu usable both in-match and inside the ship.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 12:30:22 -04:00
parent be5c41f82f
commit d3bed34c6c
124 changed files with 3347 additions and 159 deletions
+14 -3
View File
@@ -8,7 +8,7 @@
|------|----------|
| `overview.md` | Concept, core loop, game flow, match sizes |
| `racesclasses.md` | 2 chosen factions, 3 ship classes each |
| `multiplayer.md` | Maps, game modes (design ideas — casual's Sector Control etc. not yet built), galaxy war meta |
| `multiplayer.md` | Maps, game modes (4 casual categories exist as `GameMode` subclasses — Team Deathmatch is real, Domination/Conquest/King of the Hill are scoring stubs, see `CLAUDE.md` item 27), galaxy war meta |
| `bots.md` | Bot fill rules for casual matches |
| `tech.md` | Engine, networking architecture, checklist |
| `money.md` | Monetization strategy |
@@ -71,13 +71,24 @@ Added the other half of that same knob: a VSYNC dropdown (Enabled/Adaptive/Disab
25. **ISN flagship formation mixes in a Stealth Corvette hull** — item 24's 3-ship flagship formation was 3 identical `flagship_colossus.png` copies per faction; ISN's formation is now 2 Colossus + 1 `stealth_corvette.png` (a new hull processed from a user-supplied render: background matted out via border flood-fill, a disconnected decorative sparkle dropped, rotated 90° to this project's nose-up convention — see `assets/images/ships/isn/source/isn_stealth_corvette_obsidian_source.jpeg` for the original). `team_select.gd`'s ISN entry gained a `flagship_paths` array (list of `{path, scale}`, cycled by formation-slot index in `world.gd`'s `_spawn_flagship()`); a race with no `flagship_paths` (ORC, so far) falls through to the original singular `flagship_path`/`flagship_scale` for every slot, so this is additive and backward compatible. The Corvette's `scale: 0.68` was calibrated against its native resolution (516×1019, notably taller than Colossus's 535×692) so all 3 formation members read at the same on-screen size despite the size mismatch in source art.
26. **Match structure — pre-match countdown, timer, kills/deaths, scoreboard, winner banner, persistent stats, reusable GameMode/map framework** (see `overview/map1.md`) — matches now have real phases instead of ships spawning the instant a peer connects. New `MatchManager` autoload (`autoload/match_manager.gd`) runs a server-authoritative `PRE_MATCH` (30s countdown, ships held invisible/uncollidable via `ship_movement.gd`'s `server_hold_for_match_start()`/`server_release_from_hold()` — reuses the existing dead-ship machinery rather than a parallel state, so prediction/reconciliation are untouched; flagships cosmetically tween in from a random offset via `flagship.gd`'s `play_creep_in()`) → `IN_PROGRESS` (the active `GameMode`'s 7-minute clock runs, ships released) → `POST_MATCH` (winner banner, result reported to matchmaking-api) → loops back into a fresh `PRE_MATCH` forever, matching the always-on server-pool model from item 16 rather than kicking players to the menu. Phase state is replicated with the same "decide once on the server, RPC-broadcast, late joiners pull it" pattern `world.gd`'s `decide_offered_races()` already established. Win-condition logic lives entirely in a new `GameMode` abstraction (`world/game_modes/game_mode.gd` base + `team_deathmatch_mode.gd`, the only mode implemented so far — most kills when the clock runs out wins, draw on a tie) built identically on every peer off a shared `GameConfig.default_game_mode_id` constant, so a future mode (`multiplayer.md`'s Flagship Assault/Last Ship Standing/Capture and Hold ideas) is a new subclass plus one factory branch in `MatchManager`, with zero changes to the timer/scoreboard/banner code around it — none of that ever hardcodes "kills," only ever calling `GameMode.get_score_for_team()`/`get_score_label()`. `world.gd`'s hardcoded `MAP_SCENE` const became a `MAPS` array + `_pick_map()` (same static-array convention as `TeamSelect.RACES`) so a second map is an append, not a rewrite — still only `map_01` exists today. New `MatchStats` autoload (`autoload/match_stats.gd`) tracks per-match (not persistent) kills/deaths by peer_id, including bots, hooked into the single existing death-detection call site (`kill_feed_manager.gd`'s `report_kill()`) rather than duplicating it in `ship_movement.gd`. Three new signal-driven HUD `CanvasLayer`s follow every existing HUD file's autonomous-`_build_ui()` convention: `hud/match_timer.gd` (top-center countdown/clock), `hud/scoreboard.gd` (hold-Tab CS:GO-style two-team panel, new `scoreboard` input action bound to Tab), `hud/match_banner.gd` (full-screen winner banner, `layer=30` — highest in the project). Banner art (`assets/images/banners/`) was renamed from opaque UUID filenames to `isn_banner.jpeg`/`orc_banner.jpeg` (the third, Apex Dynamics, stays orphaned per item 19's precedent of leaving unused assets on disk) and wired via a new `banner_path` key on `team_select.gd`'s ISN/ORC `RACES` entries, same pattern as the existing `flagship_path` key. Persistent per-player history (total kills, deaths, wins, losses, hours played) is a new `matchmaking-api` capability: `Player` gained `kills`/`deaths`/`hours_played` columns, a new `POST /matches/report` endpoint (`app/routers/matches.py`) is called once by the hosting server's `MatchManager` (never a client, same server-only trust model as `/servers/register`) at `POST_MATCH`, upserting each real (non-bot) player's totals by callsign via a shared `app/crud.py` helper also now used by the matchmaking-queue join path; `GET /stats/{callsign}` returns the 3 new fields alongside the existing `mmr`/`wins`/`losses`. No migrations tooling exists in this API (schema changes just land in `models.py` for `create_all()` to pick up on a fresh DB) — a `docker compose down -v` is needed once to add the new columns to an already-existing local `players` table, verified end-to-end via a real `docker compose up` round-trip during implementation.
27. **Map categories + 3 new GameMode stubs (Domination, Conquest, King of the Hill)** — maps now declare which game modes they support, the way CS's de_/cs_ prefixes tie a map to a mode, except a map can list more than one category rather than being locked to exactly one. `world.gd`'s `MAPS` entries gained a `"categories"` array; `World.pick_map()` (static, public) filters `MAPS` down to whichever list a given mode id and picks the first match, falling back to the full list if nothing matches (documented as *not* randomized among ties — every caller runs this independently with no shared seed, unlike `_spawn_flagships()`'s `_match_seed`, so a random pick here would desync which map each peer loads the moment a second candidate exists; real map rotation needs that seed-sync treatment first). Three new `GameMode` subclasses (`world/game_modes/domination_mode.gd`/`conquest_mode.gd`/`king_of_the_hill_mode.gd`) round out `MatchManager._create_mode()`'s factory alongside `team_deathmatch_mode.gd` — each only overrides `get_mode_name()` for now and shares a new `GameMode._kills_based_win_condition()` helper (factored out of `TeamDeathmatchMode.check_win_condition()`, which now just calls it) for scoring, since none of the three have real zone/capture-point mechanics yet. `overview/multiplayer.md`'s casual mode-ideas table was renamed to match this session's chosen names (Domination/Conquest/King of the Hill), replacing the old Sector Control/Annihilation/Base Assault/Convoy Escort ideas that had no code behind them either way. **map_01 only lists `"team_deathmatch"`** in its `categories` — Domination/Conquest/King of the Hill exist as `GameMode` code but no map is actually built/tuned for them yet, so map_01 doesn't claim to support modes it can't really run.
**Server list now reports the real game mode + map**`menu/server_select.gd` was previously showing only the matchmaking *queue* mode ("CASUAL", the only value `Mode` has since item 15) with no indication of what's actually being played. `GameServer` (matchmaking-api) gained `game_mode`/`map_name` string columns (plain strings, not a `Mode`-style enum — the API has no reason to know the client's `GameMode` id catalog), set via new fields on `POST /servers/register`'s `ServerRegisterRequest` and returned by `GET /servers`; `NetworkManager._register_with_matchmaking_api()` now calls the static `World.pick_map(GameConfig.default_game_mode_id)` to get these before every heartbeat — safe to call before `world.tscn` even loads (the heartbeat starts on server boot) since it's a pure function of shared static data, same "decide independently, same result on every caller" pattern as `TeamSelect.RACES`. `world.gd` gained a `class_name World` so this static call is reachable from `NetworkManager`. Each server row now reads e.g. `"TEAM DEATHMATCH · Sector Alpha 127.0.0.1:7777"` (`_format_mode_name()` mirrors `GameMode.get_mode_name()`'s formatting so the two never spell a mode differently). Verified end-to-end: rebuilt the local matchmaking-api DB (`docker compose down -v && up -d --build`, needed for the new columns per this API's no-migrations convention — see item 26/`matchmaking-api/README.md`), hosted a real headless server, and confirmed `GET /servers` showed `game_mode: "team_deathmatch"`, `map_name: "Sector Alpha"` for the live row.
**Game mode became its own toggleable filter, not just row text**`server_select.gd` gained an "ALL MODES" + one-per-`GameConfig.GAME_MODE_IDS` toggle bar (radio-style via a `ButtonGroup`, same pattern the server rows below already use) above the list; picking one narrows the rendered rows to that `game_mode` without re-hitting the API. `GameConfig.GAME_MODE_IDS` is a new canonical ordered array (`["team_deathmatch", "domination", "conquest", "king_of_the_hill"]`) backing both this filter bar and `default_game_mode_id`'s valid values. Required splitting what was one `_refresh_servers()` (fetch + render) into `_refresh_servers()` (network fetch only, sets `_all_servers`) and `_apply_filter()` (narrows to `_selected_category`, rebuilds rows into `_servers` — the array rows/ping-probes/CONNECT actually index into) so toggling a filter doesn't need a fresh network round-trip.
28. **Character creation screen (Stardew-Valley-style callsign + portrait picker)** — the main menu's PROFILE overlay (item 14) was just a callsign `LineEdit`; it now also offers a portrait pick, over the existing space-themed starfield background (`main_menu.gd`'s `_add_bg()`, unchanged, already reused here). No portrait art exists in the project (no `assets/images/portraits/` or similar), so portraits are procedurally drawn rather than sourced as new images: new `menu/pilot_portrait.gd` (`class_name PilotPortrait`, `extends Control`) `_draw()`s a simple pilot bust (shoulders + helmet + visor) from a `PRESETS` array of grayscale suit shades paired with a colored visor accent (Ash/Slate/Steel/Iron/Fog/Graphite) — kept monochrome/gray per this feature's art direction, with the visor as the only pop of color per preset. `_add_profile_overlay()`'s panel grew (400×240 → 420×450) to fit a centered portrait frame flanked by `<`/`>` `Button`s (`_on_portrait_cycle()`, cycling `PilotPortrait.preset_index` which wraps via `wrapi()`) above the existing callsign field; title changed from "PROFILE" to "CREATE YOUR PILOT". New `GameConfig.player_portrait_index: int` stores the choice with the same lifetime as the existing (not-persisted-to-disk, resets on relaunch) `player_name` — only written on SAVE, matching the callsign's existing commit-on-save behavior rather than live-updating as the player cycles. The chosen portrait is also now shown as a small icon in the main menu's top-right badge (`_badge_portrait`, refreshed in `_refresh_profile_badge()`) next to the callsign/rank text, so the pick is visible outside the creation screen too, not just while editing it.
29. **On-foot station hub — first sprite import + walkable room (new gameplay mode, groundwork only)** — kicks off a Stardew-Valley-style on-foot mode alongside the ship combat, using 4 AI-generated reference sheets dropped at `assets/images/worldsprites/orc/` (a multi-NPC character roster, a messy "concept + swatches" corridor sheet, a clean labeled tileset reference sheet, and a furniture/props sheet). No `godot4` binary was assumed available per prior session notes, but this sandbox does have one at the `godot4`-aliased path — used here to actually headless-import and screenshot-verify everything below, not just hand-write `.import` files blind. Individual sprites were hand-cropped out of the sheets with ImageMagick (no PIL/pip in this sandbox) since none of the 4 sheets are uniform game-ready spritesheets — grid boundaries were found by overlaying a labeled pixel grid and reading it back with the Read tool, iterating per-crop; backgrounds matted to transparent via corner-seeded flood fill (`-fuzz`/`-draw "alpha X,Y floodfill"`), same technique as the existing ship sprites. New assets: `assets/images/worldsprites/character/soldier_{idle,walk}.png` (one NPC variant's front-idle + side-walking pose — the sheet has no full 4-directional walk cycle, just these two poses per outfit, so `on_foot_character.gd` swaps between them by movement state rather than animating frames, and flips `Sprite2D.flip_h` for left/right, mirroring `ship_movement.gd`'s thrust-sprite-swap pattern from item 18); `assets/images/worldtiles/station_{floor,walls,door}.png` (3 floor variants + 2 wall variants cropped to a uniform 72×72 grid from the clean labeled sheet, plus one door graphic used as a decorative `Sprite2D`, not a tile); `assets/images/worldsprites/furniture/{locker,cabinet,plant,server_rack}.png`. Each folder keeps a `source/` copy of its origin sheet, same convention as the race art in item 17. New `world/station_tileset.tres` (2 `TileSetAtlasSource`s, `tile_size = Vector2i(72, 72)`) follows `world/world_tileset.tres`'s existing walls+asteroids split pattern. New `world/levels/station_hub.tscn` + `station_hub.gd` (`class_name StationHub`) procedurally builds a 12×9 room via `TileMapLayer.set_cell()` in `_ready()` (hand-authoring the raw `tile_data` `PackedInt32Array` format was judged too risky to get right blind, so the layout is code-generated instead of painted) with a door-sized gap in the wall ring, spawns `StaticBody2D` wall colliders per used cell (same "tiles have no collision shapes of their own" approach as `world.gd`'s ship-map collider builder, but a separate implementation local to this scene, not a shared one) and places the 4 furniture sprites as static decoration. New `world/levels/on_foot_character.gd` (`class_name OnFootCharacter`, `CharacterBody2D`) is a 4-directional top-down walker (reuses the existing `move_up/down/left/right` input actions literally instead of the ships' rotate+thrust scheme) with idle/walk sprite swapping and a `Camera2D` using the same `make_current()` fix from the networking memory notes. Verified end-to-end, not just import-clean: launched the real scene headlessly under a throwaway Xvfb display (`:133`, distinct from the user's actual running editor/display — left both untouched), screenshotted the room (floor variety, both wall types, door gap, all 4 furniture pieces, and the player all render correctly), then drove movement via a temporary `HubAutopilot` autoload (removed after) simulating a held `move_right` and confirmed the walk sprite/camera-follow render correctly and that the character stops cleanly at the rock wall's collider instead of clipping through. **This is groundwork only** — no main-menu entry point, no NPC interaction/dialogue, no second room/transition, and the character roster/tileset/furniture sheets still have far more content un-cropped than used (only 1 of ~10 character variants, 5 of ~40 tileset tiles, 4 of ~30 furniture items) — see Current Tasks.
## Current Tasks
- [x] ~~Asteroids and environment hazards~~ — done, undocumented until now: `world.gd`'s collider builder gives the map's `Asteroids` TileMapLayer round hitboxes in an `environment_hazard` group distinct from `environment_wall`, and deals impact damage (`GameConfig.ship_wall_damage`) on top of the bounce every wall/asteroid tile already causes
- [ ] Sound effects — shoot (per-ship, `world.gd`'s `_play_shoot_sound()`) and menu music (`autoload/music_manager.gd`) exist; thrust, explosion, and UI-click sounds are still missing
- [ ] Graphics quality presets / resolution scale in Options (audio, window mode, colorblind, and key rebinding are now done — see item 21)
- [ ] 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`)
- [ ] Additional game modes (Flagship Assault, Last Ship Standing, Capture and Hold — see `multiplayer.md`) via item 26's new `GameMode` framework (`world/game_modes/`); only Team Deathmatch is implemented so far
- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`) — Sector Control specifically would also need its own `GameMode` subclass, see above
- [ ] Real scoring for Domination/Conquest/King of the Hill (see item 27) — all 3 exist as `GameMode` subclasses with map-category tags wired up, but still score by kills like Team Deathmatch until each grows its own zone/point/capture mechanic
- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`) — ties most naturally to Domination once it has real per-sector scoring, see above
- [ ] 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
- [ ] On-foot station hub (see item 29) — currently a standalone unlinked scene (`world/levels/station_hub.tscn`) with one hand-built room; needs a main-menu entry point, decide whether it's multiplayer (networked like ship combat) or single-player/local, NPC interaction, and cropping more of the still-mostly-unused character/tileset/furniture sheets