Home Macroquad Graphical Client

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)

flowchart TB subgraph today [Today] PlayBin[tools/flatland play.rs] DevPlay[tools/flatland-dev play.rs] TuiCrate[crates/tui] ClientLib[crates/client-lib] PlayBin --> TuiCrate DevPlay --> TuiCrate PlayBin --> ClientLib DevPlay --> ClientLib TuiCrate --> WorldView[world.rs WorldView] TuiCrate --> MapPres[map_presentation.rs] TuiCrate --> AppState[app.rs MovementState InputAction] TuiCrate --> Render[render.rs ratatui draw] end

Pain points to fix while porting:

  • ~1,700-line play loop in tools/flatland/src/play.rs duplicated in tools/flatland-dev/src/play.rs
  • crates/tui/src/map_presentation.rs and crates/tui/src/world.rs are engine-agnostic but depend on ratatui::style::Color
  • Input in crates/tui/src/app.rs is tied to crossterm::event::KeyEvent

Target architecture

flowchart TB subgraph shared [New shared layer] ClientUi[crates/client-ui] PlayLoop[crates/play-loop] ClientUi --> WorldView ClientUi --> MapPres ClientUi --> AppState ClientUi --> Layout[hud_layout rects] end subgraph clients [Front ends] Tui[crates/tui ratatui adapter] Gfx[crates/gfx macroquad + egui] end subgraph bins [Binaries] Flatland3[tools/flatland flatland3] GfxBin[tools/flatland-gfx flatland3-gfx] DevPlay[tools/flatland-dev] end PlayLoop --> ClientLib[crates/client-lib] Tui --> ClientUi Gfx --> ClientUi Flatland3 --> Tui Flatland3 --> PlayLoop GfxBin --> Gfx GfxBin --> PlayLoop DevPlay --> Tui DevPlay --> PlayLoop

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: map crossterm::EventUiKeyEvent / UiPointerEvent
  • Gfx crate maps macroquad keys/mouse → same types
  • MovementState::apply_key becomes apply_ui_key(&UiKeyEvent, ...)

Refactor flatland-tui: re-export from client-ui where needed; render.rs converts RgbColorratatui::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
  • InputActionGameClient method dispatch (83 actions today)
  • Chat mode buffer, reconnect/backoff
  • Builds PlayHud snapshot 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_target unchanged; draw each cell with draw_text_ex using 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.rs draw_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 PlayArgs in tools/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 to scripts/dev-play.sh)
  • Update specification binaries 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 asserting WorldView dimensions and InputAction mapping (no GPU in CI)
  • Keep existing flatland-client-lib connect 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, m target 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 ,, pickup p, shop
  • Meta: ? help, . HUD cycle, reconnect

Suggested implementation order (PR slices)

  1. client-ui extraction + tui refactor (no gfx yet) — largest refactor, zero user-visible change
  2. play-loop extraction + flatland/flatland-dev thin wrappers
  3. gfx world-only + flatland3-gfx connects and walks
  4. egui status + log + help
  5. egui combat + inventory + overlays (batch by related actions)
  6. rotation editor + NPC chat + shop (last — most UI complexity)
  7. 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 developmentdone: TUI play retired; no Tier 4+ work on crates/tui play path
  • flatland-dev embedded playretired; use gfx + dev gateway

Next: client foundation (specification)

After gfx sprite / world-sheet work (4.7.0), prioritize in order:

  1. Extract crates/play-loop — single PlaySession for flatland3-gfx only
  2. Gfx UX completeness — combat HUD, shop, rotation editor, remaining modals
  3. Presentation pipelinecrates/presentation + YAML sprite_modes + admin validate
  4. 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-loop4.7a in 29 (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