MRPCI/mrpci-core/src/screens.rs
m3ultra c83c90711d MRPCI v0.1 — the SCI-generation Monster Robot Party engine
Headless core (mrpci-core) + macroquad GUI (mrpci), inheriting MRPGI's
Command/Event bus architecture and jumping a Sierra generation:

- 256-color palettes with live median-cut quantization + palette cycling
- four screens: visual / priority / control / hotspot
- A* pathfinding with LOS-verified edges and line-exact path following
- perspective scale tables (actors shrink toward the horizon)
- point-and-click verb bar AND a text parser, one shared verb pipeline
- rhai room scripts (on_enter/on_verb/on_trigger + after() tick timers)
- deterministic 30Hz cycles, seeded RNG — byte-identical script replays
- checkpoint at room entry; death rewinds instead of restarting
- save-anywhere slots; multi-voice chip synth; JSONL/HTTP/MCP surfaces
- demo game: Neon Precinct (3 rooms, 25 points, one merciful fusebox)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 22:52:39 +10:00

125 lines
3.6 KiB
Rust

//! SCI's core rendering model: every room is FOUR aligned screens.
//!
//! AGI squeezed depth and walls into one buffer (values 0-3 were walls, 4-15
//! were depth) — which is why AGI rooms can't have a wall you also walk
//! behind. SCI split them, and so do we:
//!
//! * **visual** — palette indices (0..=255), what the player sees
//! * **priority** — depth bands 0..=15 (higher = nearer the camera)
//! * **control** — walkability bitflags (block / water), independent of depth
//! * **hotspot** — a click/step map: each pixel names the hotspot it belongs to
//!
//! The ego is drawn a pixel at a time, only where its priority >= the
//! scenery's — the whole "walk behind the column" trick with zero per-object
//! Z bookkeeping — while walls live on a *different* screen, so a barrier can
//! sit at any depth.
pub const PIC_W: usize = 320;
pub const PIC_H: usize = 190;
/// The "open" color the visual buffer clears to.
pub const BG_VISUAL: u8 = 0;
// --- control bitflags -------------------------------------------------------
/// Nothing may step here.
pub const CTL_BLOCK: u8 = 1;
/// Water: actors flagged `on_land_only` refuse it; scripts can ask `on_water()`.
pub const CTL_WATER: u8 = 2;
/// Number of depth bands.
pub const BANDS: u8 = 16;
/// Map a screen row to its default depth band: 0 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 {
((y * BANDS as usize) / PIC_H).min(15) as u8
}
pub struct Screens {
pub visual: Vec<u8>,
pub priority: Vec<u8>,
pub control: Vec<u8>,
pub hotspot: Vec<u8>,
}
impl Screens {
pub fn new() -> Self {
let mut s = Screens {
visual: vec![BG_VISUAL; PIC_W * PIC_H],
priority: vec![0; PIC_W * PIC_H],
control: vec![0; PIC_W * PIC_H],
hotspot: vec![0; PIC_W * PIC_H],
};
s.clear();
s
}
/// Reset to a blank room: black visual, default depth bands, all-open
/// control, no hotspots.
pub fn clear(&mut self) {
for y in 0..PIC_H {
let p = band(y);
let row = y * PIC_W;
for x in 0..PIC_W {
self.visual[row + x] = BG_VISUAL;
self.priority[row + x] = p;
self.control[row + x] = 0;
self.hotspot[row + x] = 0;
}
}
}
#[inline]
pub fn in_bounds(x: i32, y: i32) -> bool {
x >= 0 && y >= 0 && (x as usize) < PIC_W && (y as usize) < PIC_H
}
#[inline]
pub fn idx(x: i32, y: i32) -> usize {
y as usize * PIC_W + x as usize
}
/// Control flags at a point; off-screen reads as a solid wall.
#[inline]
pub fn control_at(&self, x: i32, y: i32) -> u8 {
if Self::in_bounds(x, y) {
self.control[Self::idx(x, y)]
} else {
CTL_BLOCK
}
}
/// Is this point free to stand on?
#[inline]
pub fn walkable(&self, x: i32, y: i32) -> bool {
self.control_at(x, y) & CTL_BLOCK == 0
}
#[inline]
pub fn priority_at(&self, x: i32, y: i32) -> u8 {
if Self::in_bounds(x, y) {
self.priority[Self::idx(x, y)]
} else {
15
}
}
/// Which hotspot (0 = none) owns this pixel?
#[inline]
pub fn hotspot_at(&self, x: i32, y: i32) -> u8 {
if Self::in_bounds(x, y) {
self.hotspot[Self::idx(x, y)]
} else {
0
}
}
}
impl Default for Screens {
fn default() -> Self {
Self::new()
}
}