The handover plan (docs/HANDOVER.md), P0-P6, in one pass. One insight powers it: CLI/HTTP/MCP/Python is one feature, not four — a headless core driven by Commands in, Events out, with every surface a thin adapter. - mrpgi-core: the whole sim as a library with zero macroquad (the workspace boundary is the guardrail). GameState::apply/tick, fixed 1/20s cycles, deterministic replays, headless RGBA/PNG rendering. - mrpgi-headless: JSONL stdio REPL, --script replay (AI lane off = byte-identical), --render-room PNG, --serve HTTP (state/command/ events cursor/frame.png), --mcp JSON-RPC stdio server whose render_frame returns a real PNG so a model sees its own moves. - Games are folders: game.json manifest (all fields default — legacy dirs still load), data-driven verb table feeding both the parser and the AI whitelist, flags with requires_flag/sets_flag gating on objects and dialogue choices, live UpsertRoom lore ingestion with validation. - mrpgi (GUI): now one consumer of the bus; editor commits through the same commands MCP uses; audio became core events with per-game sfx/ music/ file overrides beating the chiptune synth. - Tests: parser goldens + a full win-path playthrough asserting byte-identical transcripts across runs. Deleted: the old fused src/ (root is now a virtual workspace), the dead demo Scene/Assets path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
2.7 KiB
Rust
88 lines
2.7 KiB
Rust
//! AGI's core rendering trick: every room is drawn into TWO buffers at once.
|
|
//!
|
|
//! * the **visual** buffer is what the player sees (palette indices), and
|
|
//! * the **priority** buffer is invisible — it encodes depth bands and
|
|
//! control lines so the ego can walk *behind* a column and be blocked by a
|
|
//! wall, all without any per-object Z bookkeeping.
|
|
//!
|
|
//! Values 4..=15 in the priority buffer are depth bands (higher = nearer the
|
|
//! camera = drawn on top). Values 0..=3 are "control" lines with special
|
|
//! meaning to the movement system.
|
|
|
|
pub const PIC_W: usize = 160;
|
|
pub const PIC_H: usize = 168;
|
|
|
|
/// The "open" color the visual buffer clears to (white, like AGI fills).
|
|
pub const BG_VISUAL: u8 = 15;
|
|
|
|
// --- Control-line values (priority buffer 0..=3) ---------------------------
|
|
/// Unconditional barrier — nothing may step here.
|
|
pub const CTRL_BLOCK: u8 = 0;
|
|
/// Water: sets the on-water state for the ego. (reserved for later)
|
|
pub const CTRL_WATER: u8 = 1;
|
|
/// Signal line: stepping on it can trigger room logic. (reserved for later)
|
|
pub const CTRL_TRIGGER: u8 = 2;
|
|
/// Conditional barrier. (reserved for later)
|
|
pub const CTRL_COND: u8 = 3;
|
|
|
|
/// Depth bands run from here up to 15.
|
|
pub const FIRST_BAND: u8 = 4;
|
|
|
|
/// Map a screen row to its default depth priority: 4 at the top of the play
|
|
/// area down to 15 at the very bottom. This is what makes a flat floor sort
|
|
/// correctly with no extra work from the room author.
|
|
#[inline]
|
|
pub fn band(y: usize) -> u8 {
|
|
let b = FIRST_BAND as usize + (y * 12) / PIC_H;
|
|
b.min(15) as u8
|
|
}
|
|
|
|
pub struct FrameBuffer {
|
|
pub visual: Vec<u8>,
|
|
pub priority: Vec<u8>,
|
|
}
|
|
|
|
impl FrameBuffer {
|
|
pub fn new() -> Self {
|
|
let mut fb = FrameBuffer {
|
|
visual: vec![BG_VISUAL; PIC_W * PIC_H],
|
|
priority: vec![FIRST_BAND; PIC_W * PIC_H],
|
|
};
|
|
fb.clear();
|
|
fb
|
|
}
|
|
|
|
/// Reset to a blank room: white visual, default depth bands on priority.
|
|
pub fn clear(&mut self) {
|
|
for y in 0..PIC_H {
|
|
let p = band(y);
|
|
for x in 0..PIC_W {
|
|
let i = y * PIC_W + x;
|
|
self.visual[i] = BG_VISUAL;
|
|
self.priority[i] = p;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn in_bounds(x: i32, y: i32) -> bool {
|
|
x >= 0 && y >= 0 && (x as usize) < PIC_W && (y as usize) < PIC_H
|
|
}
|
|
|
|
/// Priority at a point; off-screen reads as a solid wall.
|
|
#[inline]
|
|
pub fn priority_at(&self, x: i32, y: i32) -> u8 {
|
|
if Self::in_bounds(x, y) {
|
|
self.priority[y as usize * PIC_W + x as usize]
|
|
} else {
|
|
CTRL_BLOCK
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for FrameBuffer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|