edcda92812
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>
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
import enum
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String
|
|
from sqlalchemy import Enum as SAEnum
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class Mode(str, enum.Enum):
|
|
casual = "casual"
|
|
ranked = "ranked"
|
|
|
|
|
|
class ServerStatus(str, enum.Enum):
|
|
available = "available"
|
|
full = "full"
|
|
offline = "offline"
|
|
|
|
|
|
# No account/auth system yet (GodotSteam auth is still on the networking
|
|
# checklist), so callsign is the identity key for now — same as the game
|
|
# client itself, which only has a free-text name field today.
|
|
class Player(Base):
|
|
__tablename__ = "players"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
callsign: Mapped[str] = mapped_column(String(32), unique=True, index=True)
|
|
mmr: Mapped[int] = mapped_column(Integer, default=1000)
|
|
wins: Mapped[int] = mapped_column(Integer, default=0)
|
|
losses: Mapped[int] = mapped_column(Integer, default=0)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
class GameServer(Base):
|
|
__tablename__ = "game_servers"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
ip: Mapped[str] = mapped_column(String(255))
|
|
port: Mapped[int] = mapped_column(Integer)
|
|
mode: Mapped[Mode] = mapped_column(SAEnum(Mode))
|
|
status: Mapped[ServerStatus] = mapped_column(SAEnum(ServerStatus), default=ServerStatus.available)
|
|
last_heartbeat: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
class Match(Base):
|
|
__tablename__ = "matches"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
mode: Mapped[Mode] = mapped_column(SAEnum(Mode))
|
|
server_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("game_servers.id"))
|
|
started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
|
|
players: Mapped[list["MatchPlayer"]] = relationship(back_populates="match")
|
|
|
|
|
|
class MatchPlayer(Base):
|
|
__tablename__ = "match_players"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
match_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("matches.id"))
|
|
player_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("players.id"))
|
|
team: Mapped[int] = mapped_column(Integer) # 0 or 1
|
|
|
|
match: Mapped["Match"] = relationship(back_populates="players")
|