Macroquad Graphical Client — Gfx Play Client
Summary
Ship flatland3-gfx as the only interactive play client. It connects through flatland-client-lib, implements full play behavior (movement, combat, inventory, quests, NPC chat, rotation editor, etc.), and renders the world with Macroquad + egui HUD/overlays.
Retired: Ratatui play (tools/flatland default play, make dev-play), flatland-dev embedded play. flatland3 CLI is kept for auth and agent subcommands (inventory --json, etc.) — not for human play.
Decisions (locked for v1)
| Decision | Choice |
|---|---|
| Delivery | New binary flatland3-gfx (tools/flatland-gfx) — not a subcommand on flatland3 |
| World rendering | Macroquad — glyph + YAML color per cell (1 m = 16 px) |
| HUD / overlays | egui-miniquad for all non-map UI |
| Art pipeline | YAML sprite sheets under assets/gfx/sprites/; installed clients sync bundles via flatland3 assets sync |
| Server / protocol | No changes — client presentation only |
Current architecture (what we're cloning)
Pain points to fix while porting:
- ~1,700-line play loop in
tools/flatland/src/play.rsduplicated intools/flatland-dev/src/play.rs crates/tui/src/map_presentation.rsandcrates/tui/src/world.rsare engine-agnostic but depend onratatui::style::Color- Input in
crates/tui/src/app.rsis tied tocrossterm::event::KeyEvent
Target architecture
Phase 0 — Extract flatland-client-ui
New crate: crates/client-ui/
Move engine-agnostic code out of flatland-tui (TUI becomes thin adapters):
| Module | Source | Changes |
|---|---|---|
color.rs |
map_presentation::parse_color |
RgbColor { r,g,b } + conversion helpers |
map_presentation.rs |
existing | Replace ratatui::Color with RgbColor |
world.rs |
existing ~1,400 lines | Same grid logic; cell_fg: Vec<Option<RgbColor>> |
app.rs |
existing | MovementState, MapTargetState, InputAction, ActiveOverlay |
keymap.rs |
existing | Rebind to UiKeyEvent instead of crossterm |
keybindings.rs |
existing | Plain-text help (unchanged) |
layout.rs |
extract from render.rs hud_layout |
HudLayout { world, combat, status, sidebar, log } as normalized fractions |
hud.rs |
new | HudViewMode, PlayHud structs (no Frame) |
New input abstraction in client-ui/src/input.rs:
pub enum UiKeyEventKind { Press, Release, Repeat }
pub struct UiKeyEvent { pub kind, pub code: UiKeyCode, pub modifiers }
pub enum UiPointerEvent { Move { x, y }, Click { x, y, button }, Scroll { .. } }
crates/tui/src/input.rs: mapcrossterm::Event→UiKeyEvent/UiPointerEvent- Gfx crate maps macroquad keys/mouse → same types
MovementState::apply_keybecomesapply_ui_key(&UiKeyEvent, ...)
Refactor flatland-tui: re-export from client-ui where needed; render.rs converts RgbColor → ratatui::Color at draw time only.
Gate: cargo test -p flatland-client-ui -p flatland-tui must stay green before gfx work.
Phase 1 — Extract flatland-play-loop
New crate: crates/play-loop/
Single async session driver extracted from tools/flatland/src/play.rs:
pub struct PlaySessionConfig { pub keys, pub auto_reconnect, ... }
pub struct PlaySession<S: PlayConnection> { client, movement, overlays, chat, ... }
impl<S> PlaySession<S> {
pub async fn tick(&mut self, input: &[PlayInput], renderer: &mut impl PlayRenderer) -> PlayTickResult;
}
Responsibilities (lifted verbatim from play.rs):
client.drain_events()+ stall logging- 100 ms intent interval: movement / auto-nav /
move_by/stop InputAction→GameClientmethod dispatch (83 actions today)- Chat mode buffer, reconnect/backoff
- Builds
PlayHudsnapshot for renderer
Trait for front ends:
pub trait PlayRenderer {
fn draw(&mut self, state: &GameState, hud: &PlayHud, ui: &PlayUiState);
fn world_rect(&self) -> ScreenRect; // for mouse_to_world
}
Refactor tools/flatland/src/play.rs and tools/flatland-dev/src/play.rs to call PlaySession — behavior must not change (regression: manual make dev-play smoke).
Phase 2 — flatland-gfx shell (world map + connect)
New crate: crates/gfx/
Dependencies: macroquad, egui-miniquad (or egui_macroquad), flatland-client-ui, flatland-client-lib, flatland-play-loop
Window / loop (crates/gfx/src/lib.rs):
#[macroquad::main("Flatland3")]
async fn run_gfx(connect_opts: ConnectOptions) {
loop {
let input = poll_macroquad_input();
session.tick(&input, &mut renderer).await;
clear_background(BLACK);
renderer.draw_world(); // macroquad
renderer.draw_egui(&egui_ctx); // panels
next_frame().await;
}
}
World rendering (Macroquad, not egui):
- 1 world meter =
CELL_PX(start 16px, configurable) WorldView::build_with_targetunchanged; draw each cell withdraw_text_exusing bundled monospace TTF- Square cells fix TUI's tall-font stretch noted in
crates/tui/src/world.rs - Target rings: colored cell background rects (red T1, blue T2) matching TUI logic in
render.rsdraw_world - Hover cursor:
+glyph in dark gray - Camera: center on player; optional zoom later
Mouse: port mouse_to_world from render.rs using renderer.world_rect() + same grid math from grid_to_world.
Milestone: connect, walk with WASD, click-to-walk, map-target mode, see AOI terrain/entities with YAML colors.
Phase 3 — egui HUD panels
Use egui for all non-map UI. Implement panel modules mirroring TUI:
| Panel | TUI source | egui approach |
|---|---|---|
| Status bar | draw_status_* |
TopBottomPanel::bottom — HP/stamina gauges, coords, sprint |
| Combat | combat.rs |
Window or docked panel when show_combat_panel |
| Sidebar | draw_sidebar_panels |
SidePanel::right — location, quest snippet |
| Log | draw_log |
ScrollArea + monospace font |
| Help | draw_keybindings_overlay |
modal Window with format_keybindings_plain() |
| Stats | draw_stats_overlay |
modal |
| Inventory | draw_inventory_overlay |
scrollable list + sections |
| Craft / destroy / move pickers | render overlays | modal windows |
| Keychain | keychain.rs |
modal |
| Loadout | loadout.rs |
modal |
| Rotation editor | rotation_editor.rs |
multi-step modal (hardest UI) |
| NPC chat | npc_chat.rs |
full-screen egui when active (blocks map like TUI) |
| Shop | shop.rs |
modal |
Layout: reuse HudLayout from client-ui to position egui panels per HudViewMode (Normal / Compact / Map).
Input routing: when egui wants keyboard (egui_ctx.wants_keyboard_input()), skip movement; otherwise forward to MovementState.
Phase 4 — Ship flatland3-gfx binary
New package: tools/flatland-gfx/
- Clap args mirror
PlayArgsintools/flatland/src/play.rs:--name,--server,--no-reconnect - Reuse auth path from
tools/flatland/src/game_session.rs(prepare_remote_play) - Workspace wiring in root
Cargo.toml:
members = [ ..., "crates/client-ui", "crates/play-loop", "crates/gfx", "tools/flatland-gfx" ]
macroquad = "0.4"
egui = "0.29"
egui-miniquad = "..." # pin compatible pair at implementation time
Makefile / docs:
make dev-play-gfx→./scripts/dev-play-gfx.sh(parallel toscripts/dev-play.sh)- Update
specificationbinaries table - Short
docs/graphical-client.md: build, run, keybind parity note
No content-admin changes for v1 — same YAML catalogs; gfx reads map-glyphs.yaml via existing client-ui loader.
Phase 5 — Parity checklist and tests
Automated:
cargo test -p flatland-client-ui -p flatland-tui -p flatland-play-loop -p flatland-gfx- Headless gfx smoke:
PlaySession+ mock renderer assertingWorldViewdimensions andInputActionmapping (no GPU in CI) - Keep existing
flatland-client-libconnect tests
Manual parity matrix (must pass before calling v1 done):
- Movement: WASD (chord diagonals), sprint toggle, vertical axis, Space stop, Shift+Space lunge
- Map: click walk,
mtarget mode, auto-nav cancel - Combat: Tab targets, hotbar 1–9, dodge (
c)/block (q)/lunge, rotations - Social: say/whisper chat, NPC interact (
f) - Economy: inventory, craft
[, harvest,, pickupp, shop - Meta:
?help,.HUD cycle, reconnect
Suggested implementation order (PR slices)
- client-ui extraction + tui refactor (no gfx yet) — largest refactor, zero user-visible change
- play-loop extraction + flatland/flatland-dev thin wrappers
- gfx world-only +
flatland3-gfxconnects and walks - egui status + log + help
- egui combat + inventory + overlays (batch by related actions)
- rotation editor + NPC chat + shop (last — most UI complexity)
- docs + makefile + smoke test
Implementation todos
| ID | Task | Status |
|---|---|---|
extract-client-ui |
Create crates/client-ui: move world, map_presentation, app, keymap, layout; add RgbColor + UiKeyEvent; refactor flatland-tui to adapter |
done |
extract-play-loop |
Create crates/play-loop with PlaySession + InputAction dispatch; thin tools/flatland and tools/flatland-dev play.rs wrappers |
→ 29 §4.7a |
gfx-world-shell |
Create crates/gfx: macroquad async main, WorldView glyph rendering, mouse_to_world, wire PlaySession |
done |
egui-panels |
Implement egui HUD/overlays mirroring TUI panels (status, combat, inventory, modals, NPC chat, rotation editor, shop) | partial — → 29 §4.7b |
ship-binary |
Add tools/flatland-gfx (flatland3-gfx), workspace deps, make dev-play-gfx, docs/graphical-client.md |
done |
parity-tests |
Add client-ui + play-loop unit tests; manual parity checklist; cargo test workspace |
partial (47 client-ui tests) |
Risk notes
| Risk | Mitigation |
|---|---|
| macroquad + tokio async | Use #[macroquad::main] async; keep GameClient calls on same runtime; avoid nested runtimes |
| egui vs macroquad input fight | Central input_router — egui priority when pointer over panel or modal open |
| 9k lines → drift | play-loop owns dispatch; gfx/tui only render |
| Font/glyph alignment | Fixed CELL_PX; test with goblin @ and multi-codepoint glyphs from YAML |
| CI without GPU | Unit-test client-ui + mock PlayRenderer; gfx crate cfg(test) only |
Out of scope (later)
Removing or freezing TUI development— done: TUI play retired; no Tier 4+ work oncrates/tuiplay path— retired; use gfx + dev gatewayflatland-devembedded play
Next: client foundation (specification)
After gfx sprite / world-sheet work (4.7.0), prioritize in order:
- Extract
crates/play-loop— singlePlaySessionforflatland3-gfxonly - Gfx UX completeness — combat HUD, shop, rotation editor, remaining modals
- Presentation pipeline —
crates/presentation+ YAMLsprite_modes+ admin validate - WASM — deferred (browser play not a goal)
Gate: Tier 4.7 exit in specification before 4.5b directional jump or new gameplay features.
Local play / testing: make dev-restart + make dev-play-gfx (dev gateway + worker stack) — not flatland-dev.
Out of scope (historical v1 notes)
- YAML-driven NPC/terrain sprite catalogs + content-admin art pipeline → moved to 4.7c in
29 - Authoritative server player yaw / rotate keys
- Extracting
crates/play-loop→ 4.7a in29(was deferred; gfx had local loop) - Web/WASM build → 4.7d deferred
Foundation pass (2026-07-15) — primary client polish
Shipped on gfx/macroquad-client:
- UI toolkit: theme, list/tree/overlay helpers, input_router
- Smooth camera + client facing yaw + player sprite atlas (
assets/gfx/player/hero.png) - Hierarchical inventory browser (shared
inventory_browser_lines/format_inventory_row) - Interactive rotation editor overlay; reconnect loop; docs mark gfx as primary