use crate::sky::Sky;
use crate::Camera;
pub const DEFAULT_KV6COL: u32 = 0x0080_8080;
#[derive(Debug, Clone, Copy)]
pub struct LightSrc {
pub pos: [f32; 3],
pub r2: f32,
pub sc: f32,
}
#[derive(Debug, Clone)]
pub struct Engine {
camera: Camera,
sky_color: u32,
fog_color: u32,
fog_max_scan_dist: i32,
side_shades: [i8; 6],
kv6col: u32,
lightmode: u32,
lights: Vec<LightSrc>,
sky: Option<Sky>,
}
impl Default for Engine {
fn default() -> Self {
Self {
camera: Camera::default(),
sky_color: 0x8087_ceeb,
fog_color: 0,
fog_max_scan_dist: 0,
side_shades: [0; 6],
kv6col: DEFAULT_KV6COL,
lightmode: 0,
lights: Vec::new(),
sky: None,
}
}
}
impl Engine {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_camera(&mut self, camera: Camera) {
self.camera = camera;
}
#[must_use]
pub fn camera(&self) -> Camera {
self.camera
}
pub fn set_sky_color(&mut self, color: u32) {
self.sky_color = color;
}
#[must_use]
pub fn sky_color(&self) -> u32 {
self.sky_color
}
pub fn set_fog(&mut self, color: u32, max_scan_dist: i32) {
self.fog_color = color;
self.fog_max_scan_dist = max_scan_dist.max(0);
}
#[must_use]
pub fn fog_color(&self) -> u32 {
self.fog_color
}
#[must_use]
pub fn fog_max_scan_dist(&self) -> i32 {
self.fog_max_scan_dist
}
pub fn set_side_shades(&mut self, top: i8, bot: i8, left: i8, right: i8, up: i8, down: i8) {
self.side_shades = [top, bot, left, right, up, down];
}
#[must_use]
pub fn side_shades(&self) -> [i8; 6] {
self.side_shades
}
pub fn set_kv6col(&mut self, color: u32) {
self.kv6col = color;
}
#[must_use]
pub fn kv6col(&self) -> u32 {
self.kv6col
}
pub fn set_lightmode(&mut self, mode: u32) {
self.lightmode = mode;
}
#[must_use]
pub fn lightmode(&self) -> u32 {
self.lightmode
}
pub fn add_light(&mut self, light: LightSrc) {
self.lights.push(light);
}
pub fn clear_lights(&mut self) {
self.lights.clear();
}
#[must_use]
pub fn lights(&self) -> &[LightSrc] {
&self.lights
}
pub fn set_sky(&mut self, sky: Option<Sky>) {
self.sky = sky;
}
#[must_use]
pub fn sky(&self) -> Option<&Sky> {
self.sky.as_ref()
}
pub fn render(&mut self, pixels: &mut [u32], width: u32, height: u32, pitch_pixels: u32) {
assert!(
width <= pitch_pixels,
"render: width {width} > pitch_pixels {pitch_pixels}"
);
let w = width as usize;
let h = height as usize;
let stride = pitch_pixels as usize;
assert!(
pixels.len() >= h * stride,
"render: buffer too small ({} pixels) for {h} × {stride}",
pixels.len(),
);
for y in 0..h {
let row_start = y * stride;
pixels[row_start..row_start + w].fill(self.sky_color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_fills_with_sky_color() {
let mut e = Engine::new();
e.set_sky_color(0xdead_beef);
let mut buf = vec![0u32; 64 * 32];
e.render(&mut buf, 64, 32, 64);
assert!(buf.iter().all(|&p| p == 0xdead_beef));
}
#[test]
fn render_respects_pitch() {
let mut e = Engine::new();
e.set_sky_color(0x1234_5678);
let stride: u32 = 80;
let width: u32 = 64;
let height: u32 = 32;
let mut buf = vec![0u32; (stride as usize) * (height as usize)];
e.render(&mut buf, width, height, stride);
for y in 0..height as usize {
let row = &buf[y * stride as usize..(y + 1) * stride as usize];
assert!(row[..width as usize].iter().all(|&p| p == 0x1234_5678));
assert!(row[width as usize..].iter().all(|&p| p == 0));
}
}
#[test]
fn fog_defaults_disabled() {
let e = Engine::new();
assert_eq!(e.fog_color(), 0);
assert_eq!(e.fog_max_scan_dist(), 0);
}
#[test]
fn set_fog_stores_color_and_distance() {
let mut e = Engine::new();
e.set_fog(0xFF_AA_BB_CC, 1024);
assert_eq!(e.fog_color(), 0xFF_AA_BB_CC);
assert_eq!(e.fog_max_scan_dist(), 1024);
}
#[test]
fn set_fog_clamps_negative_distance_to_zero() {
let mut e = Engine::new();
e.set_fog(0xFF, -100);
assert_eq!(e.fog_max_scan_dist(), 0);
}
#[test]
fn camera_default_matches_oracle_placeholders() {
let cam = Engine::new().camera();
let bits = |a: [f64; 3]| a.map(f64::to_bits);
assert_eq!(bits(cam.pos), bits([1024.0, 1024.0, 128.0]));
assert_eq!(bits(cam.right), bits([1.0, 0.0, 0.0]));
assert_eq!(bits(cam.down), bits([0.0, 0.0, 1.0]));
assert_eq!(bits(cam.forward), bits([0.0, 1.0, 0.0]));
}
}