Roadmap

RPG98 Roadmap Phase 2 — World & Movement

Phase 2 — World & Movement

Version: v0.2.0 → v0.2.4 Status: 🔄 In Progress


Goal

Build the dungeon world the player moves through. The server generates procedural dungeons and sends them to the client. The client renders them correctly using contextual tile selection and follows the player with a camera. Movement feels smooth and responsive on the Miyoo.

Deliverable

A playable Miyoo build where you connect to the server, receive a procedurally generated dungeon, walk around it freely with responsive controls, and see correctly rendered walls, corners, floors, and doors — all at 30+ FPS on real hardware.


Planned

Server: Dungeon Generation

The server generates a dungeon on each new connection using a seeded LCG RNG (no external crate). The same seed always produces the same dungeon — useful for debugging.

Algorithm:

  • Place 8–15 non-overlapping rectangular rooms on a 64×64 tile grid
  • Connect each room to the previous with an L-shaped corridor (2 tiles wide — wide enough for the 32×32 player sprite)
  • Scatter doors at corridor pinch points (walls on both N+S or both E+W sides) with a 1-in-4 random chance
  • Spawn point = centre of first room

Shared crate types (in shared/src/lib.rs):

pub const DUNGEON_W: usize = 64;
pub const DUNGEON_H: usize = 64;

pub enum Tile { Wall, Floor, Door }

pub struct DungeonMap {
    pub tiles: Vec<Tile>,
    pub width: u32, pub height: u32,
    pub spawn_x: u32, pub spawn_y: u32,
}

The full map is serialized via bincode and sent in the initial ServerMessage::Connected response. For a 64×64 map this is small enough to send in one TCP message without chunking.

Client: Contextual Tile Rendering

A flat tile map is not enough — walls need to know what's around them to pick the right sprite from the tileset. The client runs neighbour detection on every wall tile every frame (cheap at 64×64).

Detection logic (8 roles):

  • Straight faces — wall with exactly one open orthogonal neighbour (N/S/E/W)
  • Inner corners — wall with two open orthogonal neighbours (e.g. floor south + floor east = inner-NW corner)
  • Outer corners — wall with no open orthogonal neighbours but one open diagonal (e.g. floor SE diagonal = outer-NW corner)
  • Wall fill — all other cases (solid interior wall mass)
  • Floor — plain floor, with 1-in-12 deterministic variant based on tile position hash (no RNG, same every frame)
  • Door — 2 tiles wide; right half detected by checking if left neighbour is also a door tile

All tile pixel coordinates are stored in a lookup table and were assigned using the tools/tile_picker.py interactive tool.

Client: Camera & Viewport

  • Viewport: 20×15 tiles at 2× scale (matches 640×480 screen)
  • Camera locked to player centre — no smoothing for now (can be added later)
  • Only tiles within the visible viewport are drawn each frame
  • Out-of-bounds tile positions clamp to Tile::Wall

Client: Player Movement

  • Position tracked in game pixels (f32), not tile coords — allows sub-tile movement
  • Speed: 60 px/sec (tunable constant)
  • D-pad / WASD moves the player; diagonal movement normalized by 1/√2
  • Collision: 4-corner hitbox check using a 16×16 box centred in the 32×32 sprite (8px margin each side)
  • Axis-separated movement: try X and Y independently so the player slides along walls instead of stopping dead

Debug Tooling

Two Python tools in tools/ support tile assignment and visual debugging:

  • tile_picker.py — interactive tileset viewer; click a tile, press a role key to assign it. Ctrl+S outputs Rust-ready Rect::new() constants. Saves assignments to a JSON sidecar automatically.
  • dungeon_debug.py — generates the same dungeon as the server (SEED=42), renders a colour-coded role map (left) and the actual tileset tiles (right) side by side. Outputs dungeon_debug.png and dungeon_debug_zoom.png (3× version).

Deploy & Test Loop

# Run server on Pi
cargo run -p server

# Build and deploy to Miyoo (replace 192.0.0.1 with your Miyoo's local IP)
cargo build -p client --target armv7-unknown-linux-musleabihf
scp target/armv7-unknown-linux-musleabihf/debug/client root@192.0.0.1:/tmp/rpg98_client
ssh root@192.0.0.1 "/tmp/rpg98_client"

# Debug tile rendering on dev machine
python3 tools/dungeon_debug.py
# open tools/dungeon_debug_zoom.png

Completion Checklist (Planned)

  • Server generates seeded dungeon (rooms + 2-wide corridors + doors)
  • DungeonMap sent to client on connect
  • Client renders all wall roles correctly (faces, inner corners, outer corners, fill)
  • Floor tiles render with deterministic 1-in-12 variant
  • Doors render as 2-tile-wide pairs (left/right halves)
  • wall_face_n_bottom overlay on floor tiles directly south of north walls
  • Camera follows player, viewport clips correctly at dungeon edges
  • Player moves smoothly with wall sliding (axis-separated collision)
  • 30+ FPS on real Miyoo hardware
  • dungeon_debug.py and tile_picker.py tools working

Actual

What Was Built

Dungeon generation (server):

  • Seeded LCG RNG; 8–15 non-overlapping rooms on a 64×64 tile grid; L-shaped 3-tile-wide corridors
  • 5-pass cleanup pipeline: sliver removal → excess intersection + peninsula (shared stability loop) → interior island flood-fill → boundary seal
  • Random seed per connection (nanosecond timestamp)

Autotile engine (client):

  • Full contextual tile selection based on north depth, face rows (3 depths), side-wall strips, cap tiles, and diagonal edge detection — all derived from a manually mapped tileset ruleset
  • Config-driven: tile roles loaded from a JSON sidecar at startup (TilesetConfig); falls back to hardcoded defaults if missing
  • Floor variation using a Murmur-style position hash — same algorithm in both client and tile mapper tool, so previews match exactly

Rendering & performance:

  • Direct /dev/fb0 mmap renderer; 180° framebuffer flip for Miyoo's rotated screen
  • Screen tearing eliminated: correct FBIO_WAITFORVSYNC ioctl + framebuffer write direction aligned with display scan direction
  • blit_sprite optimised: row-invariant values hoisted out of inner loop; per-blit column clip replaces per-pixel bounds check
  • 30 FPS framelock (hybrid sleep + spin); confirmed ~49–50 FPS uncapped on hardware with plenty of headroom

Camera & collision:

  • Camera clamped to dungeon bounds — viewport never shows void beyond the map edge; player moves off-centre near boundaries
  • Collision rule derived from manual mapping: border walls (any cardinal floor neighbour) are passable; only pure void walls block movement
  • Player hitbox tuned to 18×22 px source (7 px inset each side, 8–30 px vertical)

Tile mapper tool (tools/tile_mapper.py):

  • Multiple modes (paint → preview → gen → collision): P cycles between them
  • Config panel: assign all 14 wall roles + floor variations interactively; Ctrl+S saves JSON
  • Collision mode: yellow overlay shows solid cells; left/right click paints overrides; saves collision override JSON
  • Outer stability loop mirrors the server's dungeon cleanup so previews are accurate

Prototype systems:

  • Floating damage numbers: world-space, random upward drift, alpha fade — prototype for Phase 3 combat feedback

Deviations & Discoveries

  • Corridors widened to 3 tiles (was 2): 2-tile corridors felt cramped for a 32×32 sprite; doors were removed entirely as a result (they don't fit naturally in 3-wide corridors)
  • Tile rendering far more complex than planned: the initial "8 roles" plan was replaced by a full autotile system derived by manually mapping a real tileset — north depth, face rows, side walls, and cap tiles all required separate rules
  • 5-pass cleanup instead of simple generation: the LCG RNG produced wall stubs and excess intersection tiles that needed iterative cleanup passes to resolve; peninsula cleanup and excess intersection interact and require a shared stability outer loop
  • Config-driven tile roles: hardcoding tile coordinates per tileset wasn't viable for multiple tilesets; moved to JSON config with a tool to edit it interactively
  • Collision derived empirically: the collision rule (border wall = passable, void wall = solid) was confirmed by manually mapping 257 tiles and verifying the rule matched

Still In Progress

  • Portal generation: exit portal needs to be placed in the dungeon; tiles TBD (still evaluating tileset options)
  • Dungeon structures: rooms are plain rectangular spaces; need interior structures/props to make them feel inhabited
  • Generation bugs: seed 1775796532140732734 exhibits generation issues — under investigation

What Was Deferred

  • Enemy pathfinding and combat → Phase 3
  • Multiple tileset support (currently one tileset wired up) → Phase 3+
  • Minimap → Phase 4
  • Multiplayer / second player → Phase 3

Completion Checklist (Actual)

  • Server generates seeded dungeon (rooms + 3-wide corridors)
  • DungeonMap sent to client on connect
  • Client renders all wall roles correctly (faces, caps, side walls, face rows 1–3)
  • Floor tiles render with deterministic variation (Murmur hash)
  • Camera follows player, viewport clamped correctly at dungeon edges
  • Player moves smoothly with wall sliding (axis-separated collision)
  • Smooth walking animation: 4-step cycle (0→1→2→1) at 8 FPS, direction-aware with correct horizontal frame order
  • 30+ FPS on real Miyoo hardware (locks at 30, ~50 FPS uncapped)
  • Tile mapper tool working with config panel, preview, and collision modes
  • Portal generation — exit portal placed in dungeon (tiles TBD)
  • Dungeon structures — interior props/structures in rooms
  • Generation bug — seed 1775796532140732734 (known issue, under investigation)
  • Doors — removed (incompatible with 3-wide corridors; reconsidered for Phase 4)
  • dungeon_debug.py — replaced by tile mapper's built-in gen mode

Previous: Phase 1 — Setup & Foundations Next: Phase 3 — Combat & Classes