Sister Brawl — Architecture

Pure system architecture. File inventory → Reference. API contracts → Reference.


System Diagram

graph TB
    subgraph Browser["Browser Client"]
        Game["Game.svelte<br/>(2794 LOC)"]
        Game --> Canvas3D["Threlte [Canvas]<br/>3D Arena + Entities"]
        Game --> Canvas2D["2D Canvas<br/>Solo/Headless Fallback"]
        Game --> Store["gameStore.svelte.ts<br/>(895 LOC)"]
        Store --> Prediction["Client Prediction<br/>60Hz"]
        Store --> Reconciliation["Server Reconciliation<br/>10-frame correction"]
        Store --> Interpolation["Entity Interpolation<br/>Remote lerp"]
        Store --> HitStop["Hit-Stop<br/>Freeze frames"]
        Game --> Audio["audio.ts<br/>Web Audio API"]
        Game --> Particles["particleSystem.ts<br/>Pool: 200"]
        Game --> Shake["screenShake.ts<br/>Stackable, quadratic"]
        Game --> UI["UI Components<br/>CharacterSelect, HUD, VictoryScreen, Tutorial"]
    end

    Browser <--->|WebSocket /ws| Nakama["Nakama Server<br/>(TypeScript Runtime)"]

    subgraph Platform["Funday Platform (K3s)"]
        Nakama --> Handler["match_handler.ts<br/>(3539 LOC)"]
        Handler --> Physics["60Hz Tick<br/>Input → Physics → Broadcast"]
        Handler --> Specials["6 Character Specials<br/>Server-authoritative"]
        Handler --> Hazards["Arena Hazards<br/>Lava, Ice, Trees"]
        Handler --> Progression["XP/Level/Elo<br/>Nakama Storage"]
        Handler --> Replay["Replay Recording<br/>10Hz snapshots"]
        Handler --> Spectator["Free Camera<br/>Spectator Mode"]
        Nakama --> Metrics["metrics.ts<br/>199 LOC"]
        Metrics --> Sidecar["Metrics Sidecar :9101<br/>Python → Prometheus"]
    end

    Sidecar --> Prometheus["Prometheus"]
    Prometheus --> Grafana["Grafana Dashboard<br/>:9119/d/sisterbrawl"]

Integration Points

ComponentTechnologyInterface
Frontend ↔ NakamaWebSocket/ws with Nakama socket protocol
Frontend ↔ BFFHTTP + Cookie/api/plugins/rpc/ (game RPCs)
Nakama ↔ MetricsRPC + HTTPsisterbrawl_metrics_plain → Prometheus
Nakama ↔ K8sAgones SDKFleet allocation, health checks
Frontend ↔ CDNHTTPSStatic assets via Traefik → Nginx

Data Flow

Match Lifecycle

sequenceDiagram
    participant C as Client
    participant N as Nakama
    participant MH as Match Handler
    participant S as Spectator

    C->>N: find_match_v3 RPC
    N->>MH: matchCreate -> matchInit()
    C->>MH: joinMatch() -> matchJoinAttempt()/matchJoin()
    Note over C,MH: Both players READY
    MH->>C: countdown -> matchLoop() starts (60Hz)
    loop 60Hz loop
        C->>MH: send INPUT
        MH->>C: physics update -> STATE_UPDATE broadcast
    end
    Note over C,MH: Match end (kills/timeout)
    MH->>C: GAME_OVER + progression
    C->>C: ResultsOverlay (XP/levels)
    S->>N: sisterbrawl_spectate RPC

Solo Practice Flow

flowchart TD
    Mount["Game.svelte mounts"] --> onMount["onMount() check matchId"]
    onMount -->|matchId is null| Solo["Solo Practice Mode"]
    Solo --> Timeout["2s timeout -> startSoloPractice()"]
    Timeout --> Spawn["Spawn player (Ember) & bot (Frost) locally"]
    Spawn --> Phase["Set phase = 'playing' & isSoloMode = true"]
    Phase --> Render["2D canvas render loop at 60fps"]
    Render --> AI["Bot AI runs client-side (chase, attack, jump)"]

Key Constants (Single Source of Truth)

ConstantValueAuthority
TICK_RATE60 HzServer & Client
MATCH_TIMEOUT300s (18,000 ticks)Server
KILL_THRESHOLD10 killsServer
ARENA_SIZE56×56 (±28 units)Server & Client
GRAVITY-20 m/s²Server & Client
ATTACK_RANGE1.5 unitsServer
ATTACK_COOLDOWN15 ticks (250ms)Server
JUMP_VELOCITY10 m/sServer & Client
KNOCKBACK8 m/sServer
HITSTUN20 ticks (333ms)Server & Client
BLOCK_REDUCTION80%Server
BASE_K (Elo)32Server
MIN_RATING (Elo)100Server
PROVISIONAL_K64 (first 10 matches)Server
PARTICLE_POOL_MAX200Client
DAMAGE_NUMBER_POOL50Client

File Structure (Logical)

games/sisterbrawl/
├── funday-plugin.json          # Game manifest
├── src/                        # Frontend (Svelte 5 runes)
│   ├── Game.svelte             # Main component, dual renderer
│   ├── stores/                 # Reactive stores (.svelte.ts)
│   ├── components/             # UI components
│   ├── lib/                    # Core systems (audio, particles, shake, themes)
│   └── types/                  # Shared TypeScript types
└── server/                     # Nakama TypeScript runtime
    ├── match_handler.ts        # Authoritative match logic
    └── metrics.ts              # 199 LOC

Nakama Module Registration

// nakama-modules/index.ts
import {
  matchInit,
  matchJoinAttempt,
  matchJoin,
  matchLeave,
  matchLoop,
  matchTerminate,
} from "@games/sisterbrawl/server/match_handler"
 
initializer.registerMatch("sisterbrawl_match", {
  matchInit,
  matchJoinAttempt,
  matchJoin,
  matchLeave,
  matchLoop,
  matchTerminate,
})
 
// RPCs (11 total)
initializer.registerRpc("sisterbrawl_find_spectatable", rpcFindSpectatable)
initializer.registerRpc("sisterbrawl_spectate", rpcSpectate)
initializer.registerRpc("sisterbrawl_get_replay", rpcGetReplay)
initializer.registerRpc("sisterbrawl_get_leaderboard", rpcGetLeaderboard)
initializer.registerRpc("sisterbrawl_get_player_stats", rpcGetPlayerStats)
initializer.registerRpc("sisterbrawl_record_referral", rpcRecordReferral)
initializer.registerRpc("sisterbrawl_metrics_plain", rpcMetricsPlain)
initializer.registerRpc("sisterbrawl_get_active_counts", rpcGetActiveCounts)
initializer.registerRpc("sisterbrawl_create_match", rpcCreateMatch)

Match Label Schema (Nakama)

interface MatchLabel {
  open: "true" | "false" // true = joinable in lobby
  mode: "ranked" | "casual" | "custom"
  arenaTheme: "volcanic" | "frozen" | "forest" | "auto"
  hostId: string // Nakama user ID of host
  playerCount: number // Current players
  maxPlayers: number // 2-8
  version: string // Game manifest version
}

Arena Themes (Deterministic + Host Override)

ThemePaletteHazards
VolcanicRed/orange/black4 lava pools (3 dmg + 2 DPS burn/2s) + moving platform
FrozenBlue/white/cyan4 ice crystals (2 dmg + 1 DPS slow/1s) + slippery center
ForestGreen/brown/gold5 trees (block movement/projectiles) + moving platform

Sync Contract: Host selects theme via opcode 8 → all clients & spectators converge via STATE_UPDATE payload containing matchId + arenaTheme.


Client-Side Prediction + Reconciliation

flowchart TD
    Input["Local Input (60Hz)"] --> Apply["applyInput() -> predicted state"]
    Apply --> Send["sendOpcode(0, INPUT) -> Nakama"]
    Send --> Server["Server matchLoop() -> authoritative physics"]
    Server --> Broadcast["STATE_UPDATE broadcast (60Hz)"]
    Broadcast --> Reconcile["reconcile() -> replay unacked inputs -> smooth correct (10 frames if drift > 0.2)"]
    Broadcast --> Interpolate["interpolateRemotes() -> lerp between snapshots"]

Platform Bridge (Injected Props)

interface PlatformProps {
  matchId: string | null
  userId: string
  username: string
  sessionToken: string
  isHost: boolean
  matchLabel: MatchLabel
  onMatchState: (state: MatchState) => void
  onMatchEnd: (results: MatchResults) => void
  onPlayerJoin: (player: PlayerInfo) => void
  onPlayerLeave: (userId: string) => void
  onError: (error: string) => void
  sendOpcode: (opcode: number, data: any) => void
  rpc: (id: string, payload: any) => Promise<any>
}

Injected by NativeGameHost.svelte via platformBus — NOT postMessage.



Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send