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-readyRect::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. Outputsdungeon_debug.pnganddungeon_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)
-
DungeonMapsent 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_bottomoverlay 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.pyandtile_picker.pytools 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/fb0mmap renderer; 180° framebuffer flip for Miyoo's rotated screen - Screen tearing eliminated: correct
FBIO_WAITFORVSYNCioctl + framebuffer write direction aligned with display scan direction blit_spriteoptimised: 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
1775796532140732734exhibits 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)
-
DungeonMapsent 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