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:
@@ -106,6 +106,28 @@ add them to a database that already has a `players` table from before this
|
||||
change. Run `docker compose down -v` once to pick them up on an existing local
|
||||
dev database.
|
||||
|
||||
## RAM (in-game currency)
|
||||
|
||||
See `overview/onfoot.md` for the full design — this is just the backend
|
||||
piece. `Player.ram_kb` is a single `BigInteger` balance, always stored and
|
||||
transmitted as kilobytes (the smallest denomination); the Godot client
|
||||
(`spacewar/autoload/currency.gd`) formats it up into KB/MB/GB/TB for
|
||||
display, 1000 per step. 100% separate from real-money monetization
|
||||
(`overview/money.md`) — this is purely an in-game economy.
|
||||
|
||||
`POST /matches/report` now also credits RAM to every reported player:
|
||||
`ram_payout_participation_kb` just for being in the match, plus
|
||||
`ram_payout_per_kill_kb` per kill, plus `ram_payout_win_bonus_kb` if they
|
||||
won (all three tunable in `app/config.py`, currently 50/15/200 — placeholder
|
||||
numbers, not balanced against anything). `GET /stats/{callsign}` returns the
|
||||
running total as `ram_kb`.
|
||||
|
||||
There is no spend endpoint yet — nothing in the game can spend RAM until the
|
||||
ship interior/hubs from `overview/onfoot.md` exist. `Player.ram_kb` is a new
|
||||
column on an existing table, same no-migrations caveat as above — covered by
|
||||
the same `docker compose down -v` if you're picking this up on an existing
|
||||
local dev database.
|
||||
|
||||
## Client integration
|
||||
|
||||
The Godot client is wired up (`spacewar/autoload/matchmaking_client.gd`):
|
||||
|
||||
@@ -15,5 +15,15 @@ class Settings(BaseSettings):
|
||||
# to "offline" and back.
|
||||
server_stale_seconds: int = 20
|
||||
|
||||
# RAM payout formula for POST /matches/report (see overview/onfoot.md) --
|
||||
# every reported player gets ram_payout_participation_kb just for being
|
||||
# in the match, plus ram_payout_per_kill_kb per kill, plus
|
||||
# ram_payout_win_bonus_kb if they won. Placeholder numbers, not balanced
|
||||
# against anything -- tune freely, nothing else in the schema depends on
|
||||
# the specific values.
|
||||
ram_payout_participation_kb: int = 50
|
||||
ram_payout_per_kill_kb: int = 15
|
||||
ram_payout_win_bonus_kb: int = 200
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -45,6 +45,8 @@ async def _seed_demo_servers() -> None:
|
||||
status=ServerStatus.available,
|
||||
player_count=player_count,
|
||||
max_players=50,
|
||||
game_mode="team_deathmatch",
|
||||
map_name="Sector Alpha",
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@@ -2,7 +2,7 @@ import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy import BigInteger, DateTime, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -38,6 +38,13 @@ class Player(Base):
|
||||
kills: Mapped[int] = mapped_column(Integer, default=0)
|
||||
deaths: Mapped[int] = mapped_column(Integer, default=0)
|
||||
hours_played: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
# In-game currency, see overview/onfoot.md -- named RAM, stored as a
|
||||
# single integer count of kilobytes (the smallest denomination); the
|
||||
# client formats it up into KB/MB/GB/TB (1000 per step, not 1024) for
|
||||
# display. BigInteger since a long-lived player's total is expected to
|
||||
# climb well past 32-bit Integer's ~2.1 billion ceiling (2.1 billion KB
|
||||
# is only ~2.1 TB).
|
||||
ram_kb: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
@@ -57,6 +64,16 @@ class GameServer(Base):
|
||||
# once that probe answers.
|
||||
player_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
max_players: Mapped[int] = mapped_column(Integer, default=50)
|
||||
# The actual in-match GameMode id (e.g. "team_deathmatch") and map
|
||||
# display name (e.g. "Sector Alpha") this server is running -- distinct
|
||||
# from `mode` above, which is only the matchmaking queue mode ("casual").
|
||||
# Plain strings, not a Mode-style enum: the set of GameMode ids lives in
|
||||
# the Godot client (spacewar/world/game_modes/, world.gd's MAPS), which
|
||||
# this API has no reason to duplicate/validate against. Reported by the
|
||||
# hosting server on every register/heartbeat call (see
|
||||
# spacewar/autoload/network_manager.gd's host_server()).
|
||||
game_mode: Mapped[str] = mapped_column(String(64), default="team_deathmatch")
|
||||
map_name: Mapped[str] = mapped_column(String(64), default="Sector Alpha")
|
||||
|
||||
|
||||
class Match(Base):
|
||||
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app import crud
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.models import GameServer, Match, MatchPlayer
|
||||
from app.schemas import MatchReportRequest
|
||||
@@ -48,6 +49,11 @@ async def report_match(req: MatchReportRequest, db: AsyncSession = Depends(get_d
|
||||
player.kills += result.kills
|
||||
player.deaths += result.deaths
|
||||
player.hours_played += result.seconds_played / 3600.0
|
||||
player.ram_kb += (
|
||||
settings.ram_payout_participation_kb
|
||||
+ result.kills * settings.ram_payout_per_kill_kb
|
||||
+ (settings.ram_payout_win_bonus_kb if result.is_winner else 0)
|
||||
)
|
||||
if result.is_winner:
|
||||
player.wins += 1
|
||||
elif req.winner_team is not None:
|
||||
|
||||
@@ -34,6 +34,8 @@ async def register_server(req: ServerRegisterRequest, db: AsyncSession = Depends
|
||||
existing.player_count = req.player_count
|
||||
existing.max_players = req.max_players
|
||||
existing.last_heartbeat = datetime.utcnow()
|
||||
existing.game_mode = req.game_mode
|
||||
existing.map_name = req.map_name
|
||||
else:
|
||||
db.add(
|
||||
GameServer(
|
||||
@@ -43,6 +45,8 @@ async def register_server(req: ServerRegisterRequest, db: AsyncSession = Depends
|
||||
status=status,
|
||||
player_count=req.player_count,
|
||||
max_players=req.max_players,
|
||||
game_mode=req.game_mode,
|
||||
map_name=req.map_name,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -80,6 +84,8 @@ async def list_servers(db: AsyncSession = Depends(get_db)) -> list[dict]:
|
||||
"player_count": s.player_count,
|
||||
"max_players": s.max_players,
|
||||
"last_heartbeat": s.last_heartbeat,
|
||||
"game_mode": s.game_mode,
|
||||
"map_name": s.map_name,
|
||||
}
|
||||
for s in servers
|
||||
]
|
||||
|
||||
@@ -22,4 +22,5 @@ async def get_stats(callsign: str, db: AsyncSession = Depends(get_db)) -> Player
|
||||
kills=player.kills,
|
||||
deaths=player.deaths,
|
||||
hours_played=player.hours_played,
|
||||
ram_kb=player.ram_kb,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,10 @@ class ServerRegisterRequest(BaseModel):
|
||||
mode: Mode
|
||||
player_count: int = 0
|
||||
max_players: int = 50
|
||||
# See GameServer.game_mode/map_name in app/models.py -- the in-match
|
||||
# GameMode id and map display name, distinct from `mode` (queue mode).
|
||||
game_mode: str = "team_deathmatch"
|
||||
map_name: str = "Sector Alpha"
|
||||
|
||||
|
||||
class PlayerStatsResponse(BaseModel):
|
||||
@@ -39,6 +43,7 @@ class PlayerStatsResponse(BaseModel):
|
||||
kills: int
|
||||
deaths: int
|
||||
hours_played: float
|
||||
ram_kb: int
|
||||
|
||||
|
||||
# Reported by the hosting game server (never a client directly -- see
|
||||
|
||||
Reference in New Issue
Block a user