MRPGI/src/picture.rs
type-two d0caf983a0 Initial commit: MRPGI — Apple-Silicon AGI adventure engine
A new-school reimagining of Sierra's 1980s AGI interpreter in Rust + macroquad:

- Dual-buffer (visual + priority) room model with hand-painted walkability
- In-engine room editor: brush/line/rect/fill/pick/erase/oval/image/spawn/object tools
- Sprite pipeline (PNG -> EGA quantize), multi-room worlds with edge exits
- Objects with look/use/needs/keys/win conditions (no code)
- Forgiving text parser + optional local-LLM lane (Ollama / OpenRouter)
- Branching NPC dialogue trees, chiptune SFX, per-room ambient music
- Content kit: 61 archetypes across fantasy/scifi/monsters/saucy/horror
- Full docs: README, EDITOR_GUIDE, MANUAL, art manifesto + Gemini prompts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:26:55 +10:00

284 lines
8.5 KiB
Rust

//! Vector-style room painting.
//!
//! In real AGI a PICTURE resource is a stream of drawing opcodes. MRPGI keeps
//! that spirit but in a clean, hand-editable form: a room is just a list of
//! [`PicOp`]s that paint into the visual and/or priority buffers. Leaving a
//! buffer as `None` lets you, say, set depth without drawing anything visible.
use crate::framebuffer::{band, FrameBuffer, PIC_H, PIC_W};
use crate::view::{Cel, TRANSPARENT};
#[derive(Clone)]
pub enum PicOp {
/// Filled rectangle.
Rect {
x: i32,
y: i32,
w: i32,
h: i32,
vis: Option<u8>,
pri: Option<u8>,
},
/// Connected polyline through the given points.
Line {
points: Vec<(i32, i32)>,
vis: u8,
pri: Option<u8>,
},
/// Single pixel.
Pixel {
x: i32,
y: i32,
vis: Option<u8>,
pri: Option<u8>,
},
}
pub fn draw(fb: &mut FrameBuffer, ops: &[PicOp]) {
for op in ops {
match op {
PicOp::Rect { x, y, w, h, vis, pri } => {
for yy in *y..(*y + *h) {
for xx in *x..(*x + *w) {
put(fb, xx, yy, *vis, *pri);
}
}
}
PicOp::Pixel { x, y, vis, pri } => put(fb, *x, *y, *vis, *pri),
PicOp::Line { points, vis, pri } => {
for pair in points.windows(2) {
line(fb, pair[0], pair[1], *vis, *pri);
}
}
}
}
}
#[inline]
fn put(fb: &mut FrameBuffer, x: i32, y: i32, vis: Option<u8>, pri: Option<u8>) {
if x < 0 || y < 0 || x as usize >= PIC_W || y as usize >= PIC_H {
return;
}
let i = y as usize * PIC_W + x as usize;
if let Some(c) = vis {
fb.visual[i] = c;
}
if let Some(p) = pri {
fb.priority[i] = p;
}
}
/// Bresenham line into the visual buffer (and priority, if given).
fn line(fb: &mut FrameBuffer, a: (i32, i32), b: (i32, i32), vis: u8, pri: Option<u8>) {
let (mut x0, mut y0) = a;
let (x1, y1) = b;
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
put(fb, x0, y0, Some(vis), pri);
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
}
/// How an image maps into a region when its size doesn't match.
#[derive(Clone, Copy)]
pub enum Fit {
/// Scale to fill the region exactly (nearest-neighbor, keeps pixels crisp).
Stretch,
/// Repeat the image across the region.
Tile,
/// Place at original size, centered, clipped to the region.
Center,
}
/// Stamp a cel (a quantized image or sprite) into the buffers with its
/// top-left at (dx, dy). Transparent pixels are skipped; `pri` optionally
/// bakes depth/control under the painted pixels (AGI's `add.to.pic`).
pub fn stamp_cel(fb: &mut FrameBuffer, cel: &Cel, dx: i32, dy: i32, pri: Option<u8>) {
for cy in 0..cel.h {
for cx in 0..cel.w {
let c = cel.at(cx, cy);
if c == TRANSPARENT {
continue;
}
put(fb, dx + cx as i32, dy + cy as i32, Some(c), pri);
}
}
}
/// The "draw a shape, pour an image into it" primitive: fill a rectangular
/// region with an image using a [`Fit`] mode. The region is the shape (and can
/// also stamp depth via `pri`); the image supplies the look.
pub fn stamp_cel_fit(
fb: &mut FrameBuffer,
region: (i32, i32, i32, i32),
cel: &Cel,
fit: Fit,
pri: Option<u8>,
) {
let (rx, ry, rw, rh) = region;
if rw <= 0 || rh <= 0 || cel.w == 0 || cel.h == 0 {
return;
}
for dyk in 0..rh {
for dxk in 0..rw {
let (sx, sy) = match fit {
Fit::Stretch => (
(dxk * cel.w as i32 / rw) as usize,
(dyk * cel.h as i32 / rh) as usize,
),
Fit::Tile => ((dxk as usize) % cel.w, (dyk as usize) % cel.h),
Fit::Center => {
let ox = (rw - cel.w as i32) / 2;
let oy = (rh - cel.h as i32) / 2;
let sx = dxk - ox;
let sy = dyk - oy;
if sx < 0 || sy < 0 || sx >= cel.w as i32 || sy >= cel.h as i32 {
continue;
}
(sx as usize, sy as usize)
}
};
let c = cel.at(sx.min(cel.w - 1), sy.min(cel.h - 1));
if c == TRANSPARENT {
continue;
}
put(fb, rx + dxk, ry + dyk, Some(c), pri);
}
}
}
// ---------------------------------------------------------------------------
// Editor primitives — painting into the LOOK (visual) and MEANING (priority)
// buffers. `PriFill` is how a paint stroke writes the priority/control layer:
// leave it (Keep), stamp a fixed control value (Set), or restore the default
// walkable depth band for that row (Band).
// ---------------------------------------------------------------------------
#[derive(Clone, Copy)]
pub enum PriFill {
Keep,
Set(u8),
Band,
}
#[inline]
fn put_pri(fb: &mut FrameBuffer, x: i32, y: i32, p: PriFill) {
if x < 0 || y < 0 || x as usize >= PIC_W || y as usize >= PIC_H {
return;
}
let i = y as usize * PIC_W + x as usize;
match p {
PriFill::Keep => {}
PriFill::Set(v) => fb.priority[i] = v,
PriFill::Band => fb.priority[i] = band(y as usize),
}
}
/// Paint one pixel: optional visual colour + a priority write.
pub fn px(fb: &mut FrameBuffer, x: i32, y: i32, color: Option<u8>, pri: PriFill) {
if let Some(c) = color {
put(fb, x, y, Some(c), None);
}
put_pri(fb, x, y, pri);
}
/// Bresenham line writing visual colour (+ optional priority).
pub fn line_into(fb: &mut FrameBuffer, a: (i32, i32), b: (i32, i32), color: u8, pri: PriFill) {
let (mut x0, mut y0) = a;
let (x1, y1) = b;
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
px(fb, x0, y0, Some(color), pri);
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
}
/// Filled rectangle into visual (+ optional priority).
pub fn rect_into(fb: &mut FrameBuffer, x: i32, y: i32, w: i32, h: i32, color: Option<u8>, pri: PriFill) {
for yy in y..(y + h) {
for xx in x..(x + w) {
px(fb, xx, yy, color, pri);
}
}
}
/// Flood fill the connected region sharing the start pixel's visual colour.
/// Writes `color` to the visual buffer and `pri` to the priority buffer for
/// every filled pixel. Returns the number of pixels filled (for leak checks).
pub fn flood_fill(fb: &mut FrameBuffer, sx: i32, sy: i32, color: Option<u8>, pri: PriFill) -> usize {
if sx < 0 || sy < 0 || sx as usize >= PIC_W || sy as usize >= PIC_H {
return 0;
}
let target = fb.visual[sy as usize * PIC_W + sx as usize];
let mut visited = vec![false; PIC_W * PIC_H];
let mut stack = vec![(sx, sy)];
let mut count = 0usize;
while let Some((x, y)) = stack.pop() {
if x < 0 || y < 0 || x as usize >= PIC_W || y as usize >= PIC_H {
continue;
}
let i = y as usize * PIC_W + x as usize;
if visited[i] || fb.visual[i] != target {
continue;
}
visited[i] = true;
px(fb, x, y, color, pri);
count += 1;
stack.push((x + 1, y));
stack.push((x - 1, y));
stack.push((x, y + 1));
stack.push((x, y - 1));
}
count
}
/// Filled ellipse inscribed in the rect (x, y, w, h).
pub fn ellipse_into(fb: &mut FrameBuffer, x: i32, y: i32, w: i32, h: i32, color: Option<u8>, pri: PriFill) {
if w <= 0 || h <= 0 {
return;
}
let rx = w as f32 / 2.0;
let ry = h as f32 / 2.0;
let cx = x as f32 + rx - 0.5;
let cy = y as f32 + ry - 0.5;
for yy in y..(y + h) {
for xx in x..(x + w) {
let nx = (xx as f32 - cx) / rx;
let ny = (yy as f32 - cy) / ry;
if nx * nx + ny * ny <= 1.0 {
px(fb, xx, yy, color, pri);
}
}
}
}