//! 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, pub priority: Vec, pub control: Vec, pub hotspot: Vec, } 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() } }