be5c41f82f
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>
105 lines
4.1 KiB
Markdown
105 lines
4.1 KiB
Markdown
# Tech Stack
|
||
|
||
## Engine & Language
|
||
|
||
- **Engine:** Godot 4.7 (Forward Plus renderer)
|
||
- **Language:** GDScript
|
||
- **Project path:** `/mnt/code/spacewar/spacewar/` (inner folder is the Godot project root)
|
||
- **Platform target:** Linux (Steam Deck native, 1280×800), Windows PC
|
||
- **Art style:** Pixel art (placeholder sprites in use currently)
|
||
|
||
See `structure.md` for the full file tree and scene graph.
|
||
|
||
---
|
||
|
||
## Multiplayer Architecture (Planned)
|
||
|
||
```
|
||
[Client] ──── [Matchmaking Server] ──── [Dedicated Game Server]
|
||
│
|
||
[Backend API]
|
||
(queue, ranks, accounts)
|
||
```
|
||
|
||
### Authoritative Server Model
|
||
|
||
- Dedicated game servers run the authoritative simulation (not player-hosted)
|
||
- Clients send **inputs** (movement, fire, ability) — server validates and broadcasts results
|
||
- No client-side cheating possible on critical state (position, HP, kills)
|
||
- Player position simulated server-side; clients predict locally and reconcile
|
||
|
||
### Godot Networking Layer
|
||
|
||
```gdscript
|
||
# Server: move a player
|
||
@rpc("authority", "call_local", "reliable")
|
||
func set_position(new_pos: Vector2) -> void:
|
||
position = new_pos
|
||
|
||
# Client: send input to server
|
||
@rpc("any_peer", "call_remote", "unreliable")
|
||
func send_input(dir: Vector2, firing: bool) -> void:
|
||
pass
|
||
```
|
||
|
||
- **Transport:** ENet (UDP, built into Godot) for low-latency game data
|
||
- **Reliable channel:** critical state (HP, death, respawn)
|
||
- **Unreliable channel:** high-frequency position/velocity updates
|
||
|
||
### Client-Side Prediction & Lag Compensation
|
||
|
||
1. Client predicts own movement locally (feels instant)
|
||
2. Server confirms or corrects (reconciliation)
|
||
3. Other players interpolated between last two known positions
|
||
4. Lag compensation: server rewinds state slightly to validate hitscan shots
|
||
|
||
### Matchmaking Backend
|
||
|
||
Built — see `matchmaking-api/README.md` for how to run it. Actual stack
|
||
diverged from the original plan below:
|
||
|
||
| Component | Planned | Actual |
|
||
|-----------|---------|--------|
|
||
| API Server | Go or Node.js | **FastAPI (Python)** — I/O-bound queue/CRUD logic, not a latency-critical hot path, so dev velocity won over raw throughput |
|
||
| Queue / State | Redis | **In-memory in the API process** — no Redis; revisit only if the API needs to run as more than one instance |
|
||
| Database (accounts, ranks) | PostgreSQL | PostgreSQL, as planned |
|
||
| Hosting | VPS (Hetzner / DigitalOcean) or self-hosted | Not deployed — local Docker Compose only so far |
|
||
|
||
**Flow (as built):**
|
||
1. Client sends `POST /matchmaking/queue/join` with callsign + mode
|
||
2. Backend queues the ticket; a background loop forms a match once enough
|
||
players are queued for that mode
|
||
3. Match forms → backend assigns the first available registered `GameServer`
|
||
for that mode
|
||
4. Client polls `GET /matchmaking/queue/status/{ticket}` until `matched`,
|
||
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).
|
||
|
||
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
|
||
|
||
- **GodotSteam** plugin (open source, wraps Steamworks SDK)
|
||
- Handles: Steam auth, VAC anti-cheat, Steam lobbies, achievements
|
||
- No separate account system needed at launch
|
||
|
||
---
|
||
|
||
## Networking Checklist
|
||
|
||
- [x] Godot ENet server/client setup
|
||
- [x] Player input RPC structure
|
||
- [x] Position sync with interpolation
|
||
- [x] Ship class registration per peer
|
||
- [x] Server-authoritative health/death
|
||
- [x] Matchmaking API (queue + lobby assignment) — client wired end-to-end for casual; ranked mode implementation removed until it's real (see `CLAUDE.md`); real server pool still open, see Current Tasks in `CLAUDE.md`
|
||
- [ ] GodotSteam auth + VAC
|
||
- [ ] Lag compensation (basic rewind)
|