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>
62 lines
2.2 KiB
Rust
62 lines
2.2 KiB
Rust
//! Headless compositing: room buffers + depth-sorted screen objects → RGBA
|
|
//! pixels → optionally a PNG. No window required — this is what lets an LLM
|
|
//! *see* the frame it just authored.
|
|
|
|
use crate::framebuffer::{FrameBuffer, PIC_H, PIC_W};
|
|
use crate::palette;
|
|
use crate::sprite::{Prop, ScreenObj};
|
|
use crate::view;
|
|
|
|
/// The real AGI trick: draw each object bottom-up by priority, but test each
|
|
/// pixel against the room's priority buffer, so a high-priority column drawn
|
|
/// into the *room* still occludes a sprite standing behind it.
|
|
pub fn compose(room: &FrameBuffer, ego: Option<&ScreenObj>, props: &[Prop]) -> Vec<u8> {
|
|
let mut vis = room.visual.clone();
|
|
|
|
let mut draws: Vec<(u8, &view::Cel, i32, i32)> = Vec::with_capacity(props.len() + 1);
|
|
if let Some(ego) = ego {
|
|
draws.push((ego.priority(), ego.cel(), ego.x, ego.y));
|
|
}
|
|
for p in props {
|
|
draws.push((p.priority(), p.cel(), p.x, p.y));
|
|
}
|
|
draws.sort_by_key(|d| d.0);
|
|
|
|
for (pri, cel, bx, by) in draws {
|
|
let ox = bx - cel.w as i32 / 2;
|
|
let oy = by - cel.h as i32;
|
|
for cy in 0..cel.h {
|
|
for cx in 0..cel.w {
|
|
let c = cel.at(cx, cy);
|
|
if c == view::TRANSPARENT {
|
|
continue;
|
|
}
|
|
let sx = ox + cx as i32;
|
|
let sy = oy + cy as i32;
|
|
if sx < 0 || sy < 0 || sx as usize >= PIC_W || sy as usize >= PIC_H {
|
|
continue;
|
|
}
|
|
let i = sy as usize * PIC_W + sx as usize;
|
|
if pri >= room.priority[i] {
|
|
vis[i] = c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut rgba = Vec::with_capacity(PIC_W * PIC_H * 4);
|
|
for &v in &vis {
|
|
rgba.extend_from_slice(&palette::rgba(v));
|
|
}
|
|
rgba
|
|
}
|
|
|
|
/// Encode raw RGBA (PIC_W x PIC_H) to PNG bytes.
|
|
pub fn encode_png(rgba: &[u8]) -> Vec<u8> {
|
|
let img = image::RgbaImage::from_raw(PIC_W as u32, PIC_H as u32, rgba.to_vec())
|
|
.expect("buffer is always PIC_W*PIC_H*4");
|
|
let mut out = std::io::Cursor::new(Vec::new());
|
|
img.write_to(&mut out, image::ImageOutputFormat::Png).expect("png encode of a valid image");
|
|
out.into_inner()
|
|
}
|