Add ENet multiplayer (movement/combat sync) and matchmaking API
Networked play, end to end: connect, get matched, spawn, fight, respawn. Godot side: - Replace the single hardcoded Player with per-peer networked ships (MultiplayerSpawner + custom spawn_function), driven by client-side prediction with server reconciliation for movement. - Server-authoritative combat: bullets, health, death/respawn all decided by the server and broadcast to every peer over RPC. - Fix a Camera2D bug where Godot auto-promotes the first camera to ever enter the scene tree as the active one regardless of its own `current` value — with MultiplayerSpawner catch-up replication that was almost never the local player's own ship. `make_current()` on the owner's camera fixes it; property assignment doesn't reliably override the auto-claim. - Fix two "late joiner never learns already-established state" gaps (health, visibility) by folding both into the existing per-tick position broadcast instead of relying on one-shot RPCs that only reach peers already connected when they fire. - Make the 2 offered factions in team select match for every player in a match: the server decides once and clients either read it directly (server's own instance) or request it over RPC, rather than each client rolling its own random pair. - New MatchmakingClient autoload wires the main menu's CASUAL/RANKED buttons to the matchmaking API instead of connecting directly. New matchmaking-api/ (FastAPI + Postgres, Docker Compose): - Queue/match/stats/ranks endpoints. In-memory matchmaking queue in a single API process — no Redis until there's an actual reason to shard the queue across instances. - Background loop forms a match once enough players are queued for a mode, assigns the first available registered game server. - No real server pool yet: one dev server is auto-seeded from DEV_SEED_SERVER_IP/PORT; POST /servers/register exists for real servers to self-register later but nothing calls it yet. Docs: CLAUDE.md and overview/tech.md updated to match — networking checklist mostly checked off, matchmaking backend section rewritten to describe what was actually built vs. the original Go/Redis plan, and Current Tasks reordered around what's actually left (bot fill, real server pool, ranked/MMR refinement, lag compensation). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
extends CanvasLayer
|
||||
class_name TeamSelect
|
||||
|
||||
# Movement speed factor by role, shared across all 3 races so e.g. every
|
||||
# race's Interceptor moves at the same speed as every other race's.
|
||||
@@ -116,21 +117,41 @@ const ROW_IMG := 95.0
|
||||
|
||||
var _root: Control
|
||||
var _content: Control
|
||||
var _loading_lbl: Label
|
||||
var _offered: Array = []
|
||||
var _chosen_race: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 20
|
||||
_offered = _pick_two_races()
|
||||
_build_root()
|
||||
|
||||
# The 2 offered races must be the same for every player in the match, not
|
||||
# independently randomized per client — the server decides once (see
|
||||
# World.decide_offered_races()) and every client either asks for it
|
||||
# (real remote peers) or reads it directly (the server's own instance,
|
||||
# no round-trip needed since it's the same process).
|
||||
var world := get_node("/root/World")
|
||||
if multiplayer.is_server():
|
||||
offer_races(world.decide_offered_races())
|
||||
else:
|
||||
world.request_offered_races.rpc_id(1)
|
||||
|
||||
|
||||
# Called once this client knows which 2 races (by id) the match is offering.
|
||||
func offer_races(race_ids: Array) -> void:
|
||||
if _loading_lbl:
|
||||
_loading_lbl.queue_free()
|
||||
_loading_lbl = null
|
||||
_offered = race_ids.map(func(id): return _race_by_id(id))
|
||||
_show_race_selection()
|
||||
|
||||
|
||||
func _pick_two_races() -> Array:
|
||||
var pool := RACES.duplicate()
|
||||
pool.shuffle()
|
||||
return [pool[0], pool[1]]
|
||||
func _race_by_id(id: int) -> Dictionary:
|
||||
for race in RACES:
|
||||
if race.id == id:
|
||||
return race
|
||||
return RACES[0]
|
||||
|
||||
|
||||
func _build_root() -> void:
|
||||
@@ -144,6 +165,9 @@ func _build_root() -> void:
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_root.add_child(overlay)
|
||||
|
||||
_loading_lbl = _label(_root, "LOADING MATCH...", 20, Color(0.5, 0.6, 0.75),
|
||||
0, 380, 1280, 420, HORIZONTAL_ALIGNMENT_CENTER)
|
||||
|
||||
|
||||
func _swap_content() -> Control:
|
||||
if _content:
|
||||
@@ -179,7 +203,6 @@ func _show_race_selection() -> void:
|
||||
|
||||
func _on_race_chosen(race: Dictionary) -> void:
|
||||
_chosen_race = race
|
||||
GameConfig.player_race = race.id
|
||||
_show_ship_selection()
|
||||
|
||||
|
||||
@@ -203,10 +226,9 @@ func _show_ship_selection() -> void:
|
||||
|
||||
|
||||
func _on_ship_chosen(ship: Dictionary) -> void:
|
||||
GameConfig.player_ship_path = ship.path
|
||||
GameConfig.player_ship_scale = ship.scale
|
||||
GameConfig.player_ship_speed_factor = ship.speed_factor
|
||||
GameConfig.team_selected.emit()
|
||||
PlayerRegistry.submit_local_loadout(
|
||||
GameConfig.player_name, _chosen_race.id, ship.path, ship.scale, ship.speed_factor
|
||||
)
|
||||
queue_free()
|
||||
|
||||
|
||||
@@ -346,7 +368,7 @@ func _make_ship_row(ship: Dictionary, accent: Color) -> Button:
|
||||
|
||||
func _label(parent: Control, text: String, font_size: int, color: Color,
|
||||
x1: float, y1: float, x2: float, y2: float,
|
||||
align: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT) -> void:
|
||||
align: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT) -> Label:
|
||||
var lbl := Label.new()
|
||||
lbl.text = text
|
||||
lbl.horizontal_alignment = align
|
||||
@@ -355,6 +377,7 @@ func _label(parent: Control, text: String, font_size: int, color: Color,
|
||||
lbl.add_theme_color_override("font_color", color)
|
||||
_place(lbl, x1, y1, x2, y2)
|
||||
parent.add_child(lbl)
|
||||
return lbl
|
||||
|
||||
|
||||
func _place(node: Control, x1: float, y1: float, x2: float, y2: float) -> void:
|
||||
|
||||
Reference in New Issue
Block a user