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>
125 lines
3.6 KiB
Rust
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()
|
|
}
|
|
}
|