Phase 3 — Combat & Classes
Version: v0.3.0 Status: ✅ Complete
Goal
Build the foundational systems that make combat possible: a proper game state machine, a UDP network channel, a server tick loop, three playable classes, enemy AI, and a complete basic combat loop. By the end, the game has a real beginning, middle, and end — pick a class, fight enemies, extract or die.
Deliverable
A complete combat loop on real Miyoo hardware: title screen → class select → fight enemies → use your starter ability → die or extract. State transitions feel clean and the combat is responsive over WiFi.
Planned
1. State Machine (Client)
The client currently has one state: connect and render the dungeon. Phase 3 introduces a proper state machine:
| State | Description |
|---|---|
| Boot | Load core assets, display title screen, auto-advance to Main Menu |
| Main Menu | Login / connect, quit. Transitions to Class Select on success |
| Class Select | Pick Warrior, Skirmisher, or Arcanist. Sends class to server |
| Loading | Receive map data, show progress text ("The void stirs…") |
| Dungeon | Active run — movement, combat, looting |
| Death | Brief screen, return to Main Menu |
| Extraction Success | Brief screen listing extracted items, return to Main Menu |
| System Menu | Start button overlay — accessible from Dungeon state. Logout / quit |
State transitions are server-driven for dungeon events (death, extraction). Menu navigation is purely client-side.
2. Networking: UDP Channel
Add a UDP socket alongside the existing TCP connection. Used for high-frequency, loss-tolerant messages:
- Client → Server:
PlayerInput(position, facing, action flags) sent every tick - Server → Client:
WorldSnapshot(all entity states) broadcast every tick (~33ms at 30Hz)
TCP remains for reliable messages: connect, class select, loot pickup, extraction, death confirmation.
3. Server: 30Hz Tick Loop
The server runs a Tokio interval at 30Hz independently of client connections. Each tick:
- Process pending
PlayerInputpackets - Run enemy AI (move, attack)
- Resolve hit detection and damage
- Broadcast
WorldSnapshotvia UDP to all clients in the dungeon
4. The Three Classes
Defined in shared/src/lib.rs. All classes are solo-viable — differences are in playstyle, not raw power.
| Class | Identity | HP | Starter Ability |
|---|---|---|---|
| Warrior | Tanky frontline fighter | High | Second Wind — burst heal + brief damage reduction |
| Skirmisher | Mobile damage dealer | Medium | Shadow Mend — quick self-heal + brief speed boost |
| Arcanist | Ranged spellcaster | Low | Arcane Restore — mana refill + minor damage shield |
Starter abilities are on Y, permanent, and never lost on death.
pub enum Class { Warrior, Skirmisher, Arcanist }
pub struct ClassStats {
pub max_health: f32,
pub damage_multiplier: f32,
pub move_speed: f32,
}
pub struct EntityState {
pub id: u32,
pub x: f32,
pub y: f32,
pub health: f32,
pub max_health: f32,
pub class: Option<Class>,
}
WorldSnapshot carries a Vec<EntityState> covering players and enemies.
5. Enemy AI
Enemies are seeded at dungeon generation time. Their state lives entirely on the server.
- Patrol: wander within a small radius when no player is nearby
- Chase: move toward nearest player when in detection range
- Attack: melee hit when within attack range; brief back-off after each swing to prevent stunlocking
- Enemy health, damage, and detection range scale with dungeon tier (placeholder values in v0.3.0)
6. Basic Combat
- A (tap) — Basic attack. Sends
PlayerInputwith attack flag; server resolves hit detection - Y — Starter ability. Server validates cooldown and applies effect
- Damage numbers already built (floating, alpha-fade) — wired to server-confirmed hits only
- HP bars rendered above all entities (thin rect, green → red)
- Player death: server sends
Deathmessage → client transitions to Death state - Enemy death: entity removed from next
WorldSnapshot
7. Potions (Basic)
- R1 (tap) — Use health potion. Server validates and applies heal
- 3 potions per run (server-tracked count)
- No vendor or UI yet — potions are granted automatically at run start as a placeholder
- Full potion purchasing and quick-use menu comes in Phase 4
8. Client: Prediction & Reconciliation
- Movement inputs play locally immediately (prediction)
- Server sends authoritative
WorldSnapshotevery ~33ms - If server position disagrees, client lerps to correct position over 2–3 frames
- No prediction on damage — damage numbers and HP changes only appear on server confirmation
Deferred to v0.3.x Patches
These are combat mechanics from the design doc that aren't needed for the basic loop but will be added before Phase 4:
- Sprint (B hold) + stamina system
- Charged / heavy attack (A long press)
- Shield blocking (R1 hold) + shield-walk animations
- Class special movement abilities (B quick-tap + hold)
- Target cycling (L1)
- Loading screen with progress bar and baked static background (Page 11 optimization)
Controls Reference (v0.3.0)
All input via evdev (key codes, not SDL2 scancodes). See client/src/input.rs for the authoritative mapping.
| Button | Dungeon Action | Menu Action |
|---|---|---|
| D-pad | Move (8-directional) | Navigate |
| A | Basic attack | Confirm |
| B | (reserved for sprint — v0.3.x) | Back / Cancel |
| Y | Starter ability | — |
| X | (reserved for Ability Tome — Phase 4) | — |
| R1 | Use health potion | — |
| Start | System menu | — |
| Select | (reserved for inventory — Phase 4) | — |
Deploy & Test Loop
# Server
cargo run -p server
# Build + deploy to Miyoo Mini Plus
./deploy.sh plus
# Build + deploy to Miyoo Mini Flip
./deploy.sh flip
Key things to test on hardware:
- Each class feels distinct in movement speed and survivability
- Combat is responsive — no rubberbanding on LAN
- Enemy AI chases and attacks without getting stuck
- Starter ability cooldown is correct server-side
- Death and extraction transitions are clean
- 30+ FPS with 6+ enemies on screen
Completion Checklist (Planned)
State Machine
- Boot → Main Menu → Class Select → Loading → Dungeon → Death / Extraction flow
- System Menu overlay from Dungeon state (Start button)
- Server-driven death and extraction triggers
Networking
- UDP socket open alongside TCP
-
PlayerInputsent via UDP every tick -
WorldSnapshotbroadcast via UDP from server every tick - Client prediction + reconciliation for movement
Server
- 30Hz Tokio tick loop
- Enemy spawn at dungeon generation
- Enemy patrol / chase / attack AI
- Hit detection and damage calculation (server-side only)
- Ability cooldown tracking per player
- Potion count tracking per player (3 per run, server-authoritative)
Classes & Combat
- Warrior, Skirmisher, Arcanist defined in shared crate with distinct base stats
- Class transmitted to server at Class Select
- Basic attack (A) wired to server hit detection
- Starter ability (Y) for each class with cooldown
- Potions (R1) with server-validated heal
- HP bars on all entities
- Floating damage / heal numbers on server-confirmed hits
- Enemy sprites rendered from monster sprite sheet
- Player death → Death state transition
- Enemy death → removed from WorldSnapshot
Hardware
- 30+ FPS on Miyoo Mini Plus with multiple enemies
Actual
What Was Built
- State machine: Boot → MainMenu → ClassSelect → Loading → Dungeon → Death → ExtractionSuccess — confirmed working on Miyoo Mini Plus hardware
- System Menu overlay (Start button) with grid/hitbox toggles and exit
- Class selection screen (Warrior / Skirmisher / Arcanist) with D-pad navigation
- UDP game channel: client sends
PlayerInputevery frame, server 30Hz tick broadcastsWorldSnapshot - Async server rewrite:
#[tokio::main],GameWorldshared state, per-connection tasks, UDP recv loop deploy.sh: unified deploy script for both Miyoo devices- Full combat loop: server-authoritative hit detection (A), damage calc by class, enemy melee on contact
- Starter abilities (Y) for all 3 classes with distinct effects and server-side cooldown tracking
- Potions (R1): 3 per run, 40% heal, server-validated
- Enemy sprites rendered per kind+variant from
assets/monster_sprites/ - Player HUD: HP bar, ability state indicator, potion count
- Tile-locked A* enemy chase AI with aggro/leash, reactive repathing, animation hysteresis
- Enemy patrol state (wander in radius when no player in range)
- Server sends
PlayerDied/PlayerExtractedvia TCP; client transitions to correct outcome screen - Extraction triggered server-side when player center enters portal tile AABB
Deviations & Discoveries
- System Menu already existed from Phase 2; carried forward into the state machine as-is
- UDP registration is implicit: server learns the client's UDP address from the first
PlayerInputdatagram received (no explicit UDP handshake needed) - Wall-adjacency A* inflation removed — caused pathfinding failure in corridors; tile-locked movement made it unnecessary
What Was Deferred
- Client prediction + reconciliation — deferred to v0.3.x; position snapshots arrive but corrections aren't applied yet
- Sprint, charged attack, shield blocking, target cycling — deferred to v0.3.x patches per plan
Completion Checklist (Actual)
- Boot → Main Menu → Class Select → Loading → Dungeon → Death / Extraction flow
- System Menu overlay from Dungeon state (Start button)
- Server-driven death and extraction triggers
- UDP socket open alongside TCP
-
PlayerInputsent via UDP every tick -
WorldSnapshotbroadcast via UDP from server every tick - Client prediction + reconciliation for movement (deferred to v0.3.x)
- 30Hz Tokio tick loop
- Enemy spawn at dungeon generation
- Enemy patrol / chase / attack AI
- Hit detection and damage calculation (server-side only)
- Ability cooldown tracking per player
- Potion count tracking per player
- Warrior, Skirmisher, Arcanist defined in shared crate with distinct base stats
- Class transmitted to server at Class Select
- Basic attack (A) wired to server hit detection
- Starter ability (Y) for each class with cooldown
- Potions (R1) with server-validated heal
- HP bars on all entities
- Floating damage / heal numbers on server-confirmed hits
- Enemy sprites rendered from monster sprite sheet
- Player death → Death state transition
- Enemy death → removed from WorldSnapshot
- 30+ FPS on Miyoo Mini Plus with multiple enemies
Previous: Phase 2 — World & Movement Next: Phase 4 — Loot, Extraction & Hub