HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust. Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
hexwork docs player-interface-contract.md
7.8 kB
Markdown
at main

Approved player-interface recovery — implementation contract #

Authority: operator-approved plan, 2026-09-09. This contract coordinates implementation; it is not completion evidence. Preserve pre-existing light/scene/shade/Entry changes and playtest-data. No canonical save/replay tags or simulation lighting semantics change.

Ownership #

  • term worker: crates/hexwork-term/** and assets/data/keymap.toml only.
  • App worker: crates/hexwork-cli/src/app.rs and session.rs only.
  • renderer worker: crates/hexwork-render/** only (including benchmark).
  • PTY worker: crates/hexwork-cli/tests/pty/** and new interface PTY Rust wrapper only.
  • parent: entry.rs, other integration, README and documentation. Workers must not format/write other owners' files. Shared working tree; no commits/reversions.

Input boundary (fixed names) #

Add Input::TiltDown, TiltUp, SliceUp, SliceDown, Recenter, LookReset; optional remap actions tilt_down, tilt_up, slice_up, slice_down, recenter, look_reset. Shipped keys <, >, PageUp, PageDown, Home, 0 respectively. Legacy files MUST NOT gain defaults or require these actions. Add Input::Resize(u16,u16), MouseDown(u16,u16), MouseDrag(u16,u16), MouseUp, FocusLost. Backend preserves left-button coordinates; drag never toggles. Suppress Repeat for toggle actions; ordinary Press events remain presses. Backend capture optional with guard cleanup; expose actual enabled status. Existing poll_input API remains. Mouse state belongs to App, reset on release/focus/mode changes.

App boundary #

Keep existing canonical/headless input path world-relative. Add Application::physical_input(Input) with the same result/error signature as existing input method (report exact signature to parent), adapting physical directions before canonical dispatch. Presentation actions work through both adapters, but physical movement is camera-relative only. Parent routes terminal input to physical_input; old headless commands retain old direction meanings. Resize handled by Entry, never canonical.

Single coherent presentation state: saved perspective mode + separate FP yaw/pitch and chase azimuth/elevation, optional tactical selected Z, exact modal restoration. Tab toggles FP/chase, or leaves tactical to saved perspective; M toggles tactical; Esc dismisses look/modal/map before quit. x enters spatial look in Play, remains inventory detail in inventory. Look movement changes yaw ±15° / pitch or elevation ±10°, x/Esc exits. Tilt keys act outside prompts; 0 in look resets pitch/elevation and snaps current yaw to nearest octant. FP pitch ±75°, chase elevation 5..75°. Both azimuths initially -pi/2; FP level, chase default atan2(1.5,3). Preserve mode angles across toggles. Camera movement quantization: floor((angle + pi/8)/(pi/4)), modulo eight, exact ties toward increasing yaw. Tactical north-up world movement only on player floor, otherwise no tick/commit and explanatory Home hint. Coordinate clarification verified during execution: physical map Up means world -Y (top of north-up rendered map), Down +Y, independent of saved perspective. Legacy canonical Direction::North is named +Y and headless Input::Up retains that meaning. The fixed map adapter translates physical screen compass to those existing actions; it never rotates with saved camera yaw. Page keys only select slice; Home recenters. Story tactical pauses between accepted actions; Classic live. Camera-only actions never alter tick/hash/history. Expose look state/angle metadata for Entry diagnostics and truthful keymap hints.

Renderer boundary #

Preserve old scene::render wrapper for callers/tests; add typed production API scene::render_scene(grid, view, palette, framebuffer, lights, entities, bounds, scratch). Renderer defines and reports exact public EntityDescriptor, SceneBounds and RenderScratch types before parent integration; bounds use inclusive integer min/max consistent with Session tactical context (confirm actual context convention). Descriptor contains stable ID, centered world anchor, height, glyph, semantic role, positive local light; eligibility separately enforced using player-eye LOS + active location + 48m range. Provide reusable eligibility helper if practical. Old wrapper is compatibility only, not production evidence.

Camera helper APIs: camera::voxel_center([f32;3]) adds x/y 0.5 only when authoritative actor coords represent integer occupancy (verify); camera::chase_orbit_elevated(actor_pos,yaw,elevation) fixed spherical radius sqrt(11.25), target feet+1, actual pitch=-elevation. camera::collision_safe_chase(grid, actor_pos, yaw, elevation) sweeps target to eye with 0.15m skin and safe FP fallback. Existing chase_orbit remains compatible wrapper if needed. Shared basis/projection and ray generation use physical aspect W/(2H), near plane, finite parameters, FOV <=75.

Full-cell semantic packing policy: a glyph is an anchored symbol for a visible entity sample, not a claim that the entity occupies every pixel of the terminal glyph. Select a visible sample deterministically; retain its semantic role through ASCII/no-color. Preserve a nearer partner-half terrain color as the glyph background rather than painting black over it; half-sample geometry/depth remain independently available. In ASCII/no-color, terrain necessarily collapses but the visible semantic cue must remain. The player self-marker is explicitly always eligible on its selected floor (and included in chase), distinct from nonplayer light/LOS eligibility.

Reusable half samples retain hit/world/material/normal/ray distance/color/semantic kind. Unicode terrain upper block fg=upper/bg=lower; edges explicit full-cell policy with retained depths. Four-neighbor silhouettes and joins, no infinity subtraction. Typed entities projected by same basis, clipped not border-clamped, per-half depth, stable-ID tie break; no universal light floor. Actual semantic roles survive ASCII/no-color (parent conversion must recognize semantic glyphs; renderer report recommended API). FP excludes player, chase includes. Tactical has no saved-perspective frustum filter and only player marker on nonplayer floors. Local-light+player-eye LOS eligibility applies even in chase. Bounds prevent remote loaded location geometry leakage.

Correct ANSI256 actual cube and gray distance/ties; no-color bold and reset before chrome. Deterministic checked-in transfer LUT, documented generation and rounding; preserve current exposure references, zero light dark. Indexed light lookup and retained scratch; measured optimized render+encode trajectory benchmark 80x24 and 120x36, median/p95, allocation reporting (never infer zero allocation).

PTY/evidence boundary #

Independent Python VT decoder: incremental UTF8/CSI, cursor H/f, erase J/K, reset/bold/normal, truecolor/indexed fg/bg, alternate screen, wrap/resize. Unknown emitted controls fail. Handwritten fixtures split at every byte boundary. Per-cell glyph/fg/bg/bold and terminal defaults/cursor. Opt-in deterministic JSON/SVG exports (cell aspect recorded); metadata supplied by production adapters, no save changes. Physical-key executable tests cover full/diff, camera/look/map/modal, remaps including frozen legacy collisions, resize too-small suspension/restoration, chrome SGR resets. Do not call glyph-only tests color evidence.

Verification and limits #

Use docs/development-validation.md stable partition process. Focused named tests first; parent frozen integrated gates later. Report exact commands/outcomes, outstanding gaps and API changes. AC10 requires human normal-brightness route on identified release; never certify it with SVG or unit tests. No full HEXWORK completion claim. Deferred: lighting ownership/cache, warmth provenance, snapshot threading/adaptive quality, sound/animation/lantern fuel, full town art and human accessibility.