Sister Brawl — Core Systems Deep-Dive
Pure systems implementation reference. Character definitions → Characters. Architecture → Architecture.
🎯 Client-Side Prediction + Server Reconciliation
Local player movement predicted immediately at 60Hz. When server state arrives, unacked inputs are replayed. Smooth correction over 10 frames when drift > 0.2 units. Remote entities interpolate between snapshots.
Implementation: gameStore.svelte.ts — applyInput(), reconcile(), interpolateRemotes()
⚡ Character Ability System (Server-Authoritative)
All 6 characters have unique special abilities simulated server-side on the Nakama match handler (ice walls, burn DOTs, stun shockwaves, teleporting logic, reflections). AbilityDef on each CharacterDef provides UI metadata and tooltips.
export interface AbilityDef {
cooldown: number
duration: number
type: string // 'burn' | 'ice-wall' | 'dash-strike' | 'teleport-crit' | 'stun' | 'shield'
dmgMod?: number
tooltip?: string
}Character specials implemented in match_handler.ts: Characters → Special Ability Types
🔊 Procedural Audio (Web Audio API)
Zero external files. All sounds generated procedurally.
SFX (14+)
| SFX | Trigger | Description |
|---|---|---|
attack_swing | Attack initiation | Swing whoosh |
attack_hit | Hit connect | Impact thud |
jump | Jump initiation | Jump grunt |
land | Landing | Ground impact |
block_raise | Block initiation | Shield raise |
block_hit | Block impact | Deflection ping |
special_cast | Special activation | Magic charge |
special_ember | Fireball | Fire whoosh |
special_frost | Ice Wall | Ice crack |
special_volt | Dash Strike | Electric zap |
special_shade | Teleport | Shadow pop |
special_terra | Ground Pound | Ground rumble |
special_aqua | Water Shield | Water splash |
ko | Knockout | Heavy impact |
countdown | 3-2-1-FIGHT | Countdown beeps |
victory | Match win | Victory fanfare |
Music
- Lobby track: C minor 9 pad + slow arpeggio (8-12s loop variation)
- Battle track: Layered kick/bass/lead with 100→140 BPM tempo ramp
- Auto-switches on phase change (
character-select/lobby→playing)
Volume Controls
- Master / SFX / Music gain nodes (independent)
- Persisted to
localStorage prefers-reduced-motionrespected
Implementation: src/lib/audio.ts
💥 Particle VFX (Data-Only Pool)
Data-only particle pool (200 max). No Three.js objects — plain TypeScript objects rendered in 2D canvas draw loop.
interface Particle {
x: number
y: number
vx: number
vy: number
life: number
maxLife: number
color: string
size: number
type: "spark" | "burst" | "dust" | "trail" | "special"
}Effects
| Trigger | Particle Type | Description |
|---|---|---|
| Hit connect | spark | 8-12 yellow/orange sparks |
| KO | burst | 30+ character-color particles expanding |
| Attack swing | trail | Character-color trail behind weapon |
| Jump/Land | dust | Small puff at feet |
| Special cast | special | Character-specific (fire, ice, electric, etc.) |
| Block | spark | Blue shield sparks on impact |
Implementation: src/lib/particleSystem.ts
📳 Screen Shake
Impact-proportional camera shake. Character-specific intensities.
interface ShakeProfile {
intensity: number // 0.3 (light) → 2.0 (KO)
duration: number
number
number // frames
decay: "quadratic" | "linear"
directional: boolean // shake toward impact
}Shake Profiles
| Event | Intensity | Duration | Decay |
|---|---|---|---|
| Light attack hit | 0.3 | 8f | Quadratic |
| Heavy attack hit | 0.8 | 12f | Quadratic |
| Special hit | 1.2 | 15f | Quadratic |
| KO | 2.0 | 20f | Quadratic |
| Super finisher | 2.5 | 30f | Quadratic |
Features: Stackable (multiple shakes add), respects prefers-reduced-motion, quadratic decay feels natural.
Implementation: src/lib/screenShake.ts
🎯 Hit-Stop System
Frame-freeze on impact for weight/feedback.
| Event | Frames (at 60Hz) | Duration |
|---|---|---|
| Regular hit | 2 | ~33ms |
| Special hit | 2 | ~33ms |
| Combo completion | 5 | ~83ms |
| KO / dramatic finish | 3 | ~50ms |
Implementation: gameStore.svelte.ts — hitStopTicks counter, checked in render loop.
💨 Floating Damage Numbers
Damage numbers appear on 2D canvas when entities take damage.
interface DamageNumber {
x: number
y: number
value: number
color: string // white=normal, red=crit, green=heal, orange=burn
life: number // frames remaining
vy: number // upward velocity
}Throttling
- Max 1 damage number per entity per 10 frames
- Pool of 50 numbers (reused)
- Fade out over 60 frames
🏔️ Arena Themes + Hazards (Server-Side)
Arena theme selection uses selectArenaTheme(matchId, arenaTheme): host-selected arenaTheme overrides deterministic matchId hash. Server, players, and spectators all consume the same shared selector.
interface ArenaTheme {
name: string
bg: string
floor: string
grid: string
wall: string
accent: string
hazards: HazardDef[]
}Themes
| Theme | Palette | Hazards |
|---|---|---|
| Volcanic | Red/orange/black | 4 lava pools (3 dmg + 2 DPS burn/2s) + moving platform |
| Frozen | Blue/white/cyan | 4 ice crystals (2 dmg + 1 DPS slow/1s) + slippery center |
| Forest | Green/brown/gold | 5 trees (block movement/projectiles) + moving platform |
Synchronization Contract
- Host stage select sends
{ arenaTheme }through opcode8during character select - Opcode
8lobby updates may arrive under native bridge’sstate/dataenvelope — game normalizes before updating local selection UI - Recurring
STATE_UPDATEpayloads includematchIdandarenaThemeso player clients converge throughMatchState - Spectator join snapshots and
SpectatorStateincludematchIdandarenaThemeso spectators render same stage as players - If no host selection exists, server and clients pass actual Nakama
matchIdtoselectArenaThemeand derive same deterministic default
Hazard Application (Server-Side)
In match_handler.ts stepPhysics():
- Lava: instant damage + burn DOT
- Ice: damage + slow DOT + slippery center
- Trees: push-out collision (blocks movement/projectiles)
- Moving platform: entity transport (placeholder)
🔄 Replay System
Snapshots recorded at 10Hz (every 6 ticks). Stored in Nakama storage.
Data Structure
interface ReplaySnapshot {
tick: number
timestamp: number
entities: EntitySnapshot[] // position, state, hp, facing
projectiles: ProjectileSnapshot[]
inputs: InputSnapshot[] // per-player input at this tick
}Storage
- Collection:
replays - Key:
{matchId}/snap/{tick} - TTL: 7 days (auto-cleanup via cron)
Client Playback
ReplayControls.svelte provides:
- Play/pause
- Speed: 0.25x, 0.5x, 1x, 2x
- Frame step (←/→)
- Timeline scrubber
- Ghost overlay toggle (compare runs)
👁️ Spectator Mode
SpectatorViewport.svelte + spectatorStore.svelte.ts. Free camera for watching live matches.
Join Flow
// 1. Find spectatable match
const { matchId } = await rpc("sisterbrawl_find_spectatable")
// 2. Get spectate token
const { spectateToken } = await rpc("sisterbrawl_spectate", { matchId })
// 3. Join as spectator
await socket.joinMatch(undefined, spectateToken, { spectator: "true" })
// 4. Receive STATE_UPDATE broadcasts (same as players)Features
- Free camera (WASD + mouse drag)
- Entity interpolation (same as players)
- HP bars above entities
- Team colors
- Kill feed
- Match timer
📊 Progression System
XP Formula
- Base: 15 XP per match
- +8 XP per kill
- +2 XP per death
- +1 XP per 10 damage dealt
- +20 XP win bonus
Streak Bonuses
- 3+ win streak: 2× XP multiplier
- 5+ win streak: 3× XP multiplier
Levels
Thresholds: [0, 100, 250, 500, 1000, 2000, 3500, 5500, 8000, 11000]
Storage
- Nakama storage: collection=
progression, key={userId}/profile - Included in
GAME_OVERpayload for client display
🏆 Elo Rating System (Ranked)
Leaderboard-based (NOT storage-based). N-player via average opponent rating.
Constants
| Constant | Value |
|---|---|
BASE_K | 32 |
MIN_RATING | 100 |
PROVISIONAL_K | 64 (first 10 matches) |
PROVISIONAL_MATCHES | 10 |
Calculation
// 2-player: standard Elo
// N-player FFA: average opponent rating
delta = K * (actualScore - expectedScore)Rank Tiers
| Tier | Rating Range | Color |
|---|---|---|
| Iron | 100-799 | 9ca3af |
| Bronze | 800-1399 | cd7f32 |
| Silver | 1400-1999 | c0c0c0 |
| Gold | 2000-2599 | ffd700 |
| Platinum | 2600-2999 | e5e4e2 |
| Emerald | 3000-3399 | 50c878 |
| Diamond | 3400-3799 | b9f2ff |
| Master | 3800-4199 | ff6b6b |
| Grandmaster | 4200-4599 | ff4757 |
| Divine | 4600+ | a855f7 |
File References
| File | Purpose |
|---|---|
src/lib/audio.ts | Web Audio API (14+ SFX, BGM, volumes) |
src/lib/particleSystem.ts | Data-only particle pool (200 max) |
src/lib/screenShake.ts | Stackable screen shake profiles |
src/lib/arena-themes.ts | 3 themes + hazards, deterministic selection |
src/stores/gameStore.svelte.ts | Prediction, reconciliation, hit-stop, damage numbers, progression |
src/stores/spectatorStore.svelte.ts | Spectator state management |
src/components/ReplayControls.svelte | Playback UI (speed, scrub, ghost) |
src/components/SpectatorViewport.svelte | Free camera spectator view |
server/match_handler.ts | Server-authoritative abilities, hazards, progression, Elo, replay recording |
server/metrics.ts | 4 Prometheus metrics |
Related Pages
- Characters → Special Ability Types — Server-side special implementations
- Architecture → Constants — Tick rate, gravity, arena size, etc.
- Reference → API Contracts — Opcode definitions, RPC payloads
- Roadmap → Phase 1: Juice Foundation — Hitstop, shake, particles, hitflash, audio initiation