use super::constants::*;
#[derive(Debug, Clone)]
pub struct Face {
pub vertex_indices: Vec<usize>,
pub normal: [f32; 3],
pub dist: f32,
pub color_index: u8,
pub is_sky: bool,
pub light_level: f32,
pub tex_type: TexType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TexType {
Floor,
Ceiling,
Wall,
Sky,
Metal,
Lava,
}
#[derive(Debug, Clone, Copy)]
pub struct WallSeg {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
pub floor_z: f32,
pub ceil_z: f32,
}
#[derive(Debug, Clone)]
pub struct Room {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub floor_z: f32,
pub ceil_z: f32,
pub light: f32,
}
#[derive(Debug, Clone)]
pub struct QuakeMap {
pub vertices: Vec<[f32; 3]>,
pub faces: Vec<Face>,
pub walls: Vec<WallSeg>,
pub rooms: Vec<Room>,
pub player_start: [f32; 4], }
impl Default for QuakeMap {
fn default() -> Self {
Self {
vertices: Vec::new(),
faces: Vec::new(),
walls: Vec::new(),
rooms: Vec::new(),
player_start: [0.0, 0.0, 0.0, 0.0],
}
}
}
impl QuakeMap {
pub fn new() -> Self {
Self::default()
}
pub fn floor_height_at(&self, x: f32, y: f32) -> f32 {
let mut best = None;
for room in &self.rooms {
if x >= room.x && x <= room.x + room.width && y >= room.y && y <= room.y + room.height {
best = Some(match best {
Some(prev) => f32::max(prev, room.floor_z),
None => room.floor_z,
});
}
}
best.unwrap_or_else(|| {
self.rooms
.iter()
.map(|r| r.floor_z)
.fold(f32::MAX, f32::min)
})
}
pub fn ceiling_height_at(&self, x: f32, y: f32) -> f32 {
let mut best = None;
for room in &self.rooms {
if x >= room.x && x <= room.x + room.width && y >= room.y && y <= room.y + room.height {
best = Some(match best {
Some(prev) => f32::min(prev, room.ceil_z),
None => room.ceil_z,
});
}
}
best.unwrap_or(1000.0)
}
pub fn point_in_solid(&self, x: f32, y: f32, z: f32, radius: f32) -> bool {
let player_top = z + super::constants::PLAYER_HEIGHT;
for wall in &self.walls {
if wall.ceil_z <= z || wall.floor_z >= player_top {
continue;
}
if circle_intersects_segment(x, y, radius, wall.x1, wall.y1, wall.x2, wall.y2) {
return true;
}
}
false
}
pub fn supportive_floor_at(&self, x: f32, y: f32, z: f32) -> f32 {
let fall_tolerance = super::constants::STEPSIZE;
let mut best: Option<f32> = None;
let mut lowest_match: Option<f32> = None;
for room in &self.rooms {
if x >= room.x && x <= room.x + room.width && y >= room.y && y <= room.y + room.height {
lowest_match = Some(match lowest_match {
Some(prev) => f32::min(prev, room.floor_z),
None => room.floor_z,
});
if room.floor_z <= z + fall_tolerance {
best = Some(match best {
Some(prev) => f32::max(prev, room.floor_z),
None => room.floor_z,
});
}
}
}
best.or(lowest_match).unwrap_or_else(|| {
self.rooms
.iter()
.map(|r| r.floor_z)
.fold(f32::MAX, f32::min)
})
}
pub fn player_start(&self) -> (f32, f32, f32, f32) {
(
self.player_start[0],
self.player_start[1],
self.player_start[2],
self.player_start[3],
)
}
}
fn circle_intersects_segment(cx: f32, cy: f32, r: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
let dx = x2 - x1;
let dy = y2 - y1;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-8 {
let dist = ((cx - x1) * (cx - x1) + (cy - y1) * (cy - y1)).sqrt();
return dist < r;
}
let t = ((cx - x1) * dx + (cy - y1) * dy) / len_sq;
let t = t.clamp(0.0, 1.0);
let nearest_x = x1 + t * dx;
let nearest_y = y1 + t * dy;
let dist = ((cx - nearest_x) * (cx - nearest_x) + (cy - nearest_y) * (cy - nearest_y)).sqrt();
dist < r
}
pub fn generate_e1m1() -> QuakeMap {
let mut map = QuakeMap::new();
let rooms_def: Vec<(f32, f32, f32, f32, f32, f32, f32)> = vec![
(0.0, 0.0, 256.0, 256.0, 0.0, 192.0, 0.9),
(256.0, 64.0, 192.0, 128.0, 0.0, 128.0, 0.6),
(448.0, -128.0, 384.0, 384.0, -32.0, 256.0, 0.8),
(832.0, -32.0, 192.0, 128.0, 32.0, 160.0, 0.5),
(1024.0, -192.0, 320.0, 320.0, -64.0, 224.0, 0.7),
(544.0, 256.0, 128.0, 192.0, 0.0, 128.0, 0.4),
(480.0, 448.0, 256.0, 256.0, -16.0, 192.0, 0.3),
(544.0, -320.0, 128.0, 192.0, -32.0, 128.0, 0.5),
(480.0, -576.0, 256.0, 256.0, 48.0, 256.0, 0.7),
(0.0, 256.0, 64.0, 256.0, 0.0, 96.0, 0.2),
(0.0, 512.0, 128.0, 128.0, 16.0, 128.0, 0.5),
];
for (i, &(rx, ry, rw, rh, fz, cz, light)) in rooms_def.iter().enumerate() {
let room = Room {
x: rx,
y: ry,
width: rw,
height: rh,
floor_z: fz,
ceil_z: cz,
light,
};
map.rooms.push(room);
let color_idx = (i % WALL_COLORS.len()) as u8;
add_room_geometry(&mut map, rx, ry, rw, rh, fz, cz, light, color_idx);
}
map.player_start = [128.0, 128.0, 0.0, 0.0];
add_pillar(&mut map, 576.0, 0.0, 32.0, -32.0, 192.0, 0.7, 2);
add_pillar(&mut map, 704.0, 0.0, 32.0, -32.0, 192.0, 0.7, 2);
add_pillar(&mut map, 576.0, 128.0, 32.0, -32.0, 192.0, 0.7, 2);
add_pillar(&mut map, 704.0, 128.0, 32.0, -32.0, 192.0, 0.7, 2);
add_platform(&mut map, 608.0, 16.0, 96.0, 96.0, 32.0, 0.9, 4);
map
}
#[allow(clippy::too_many_arguments)]
fn add_room_geometry(
map: &mut QuakeMap,
x: f32,
y: f32,
w: f32,
h: f32,
fz: f32,
cz: f32,
light: f32,
color_idx: u8,
) {
let base = map.vertices.len();
map.vertices.push([x, y, fz]); map.vertices.push([x + w, y, fz]); map.vertices.push([x + w, y + h, fz]); map.vertices.push([x, y + h, fz]); map.vertices.push([x, y, cz]); map.vertices.push([x + w, y, cz]); map.vertices.push([x + w, y + h, cz]); map.vertices.push([x, y + h, cz]);
map.faces.push(Face {
vertex_indices: vec![base, base + 1, base + 2, base + 3],
normal: [0.0, 0.0, 1.0],
dist: fz,
color_index: color_idx,
is_sky: false,
light_level: light,
tex_type: TexType::Floor,
});
map.faces.push(Face {
vertex_indices: vec![base + 7, base + 6, base + 5, base + 4],
normal: [0.0, 0.0, -1.0],
dist: -cz,
color_index: color_idx,
is_sky: false,
light_level: light * 0.7,
tex_type: TexType::Ceiling,
});
map.faces.push(Face {
vertex_indices: vec![base, base + 4, base + 5, base + 1],
normal: [0.0, -1.0, 0.0],
dist: -y,
color_index: color_idx,
is_sky: false,
light_level: light * 0.8,
tex_type: TexType::Wall,
});
map.walls.push(WallSeg {
x1: x,
y1: y,
x2: x + w,
y2: y,
floor_z: fz,
ceil_z: cz,
});
map.faces.push(Face {
vertex_indices: vec![base + 2, base + 6, base + 7, base + 3],
normal: [0.0, 1.0, 0.0],
dist: y + h,
color_index: color_idx,
is_sky: false,
light_level: light * 0.8,
tex_type: TexType::Wall,
});
map.walls.push(WallSeg {
x1: x,
y1: y + h,
x2: x + w,
y2: y + h,
floor_z: fz,
ceil_z: cz,
});
map.faces.push(Face {
vertex_indices: vec![base + 3, base + 7, base + 4, base],
normal: [-1.0, 0.0, 0.0],
dist: -x,
color_index: color_idx,
is_sky: false,
light_level: light * 0.6,
tex_type: TexType::Wall,
});
map.walls.push(WallSeg {
x1: x,
y1: y,
x2: x,
y2: y + h,
floor_z: fz,
ceil_z: cz,
});
map.faces.push(Face {
vertex_indices: vec![base + 1, base + 5, base + 6, base + 2],
normal: [1.0, 0.0, 0.0],
dist: x + w,
color_index: color_idx,
is_sky: false,
light_level: light * 0.6,
tex_type: TexType::Wall,
});
map.walls.push(WallSeg {
x1: x + w,
y1: y,
x2: x + w,
y2: y + h,
floor_z: fz,
ceil_z: cz,
});
}
#[allow(clippy::too_many_arguments)]
fn add_pillar(
map: &mut QuakeMap,
cx: f32,
cy: f32,
size: f32,
fz: f32,
cz: f32,
light: f32,
color_idx: u8,
) {
let half = size / 2.0;
let base = map.vertices.len();
map.vertices.push([cx - half, cy - half, fz]);
map.vertices.push([cx + half, cy - half, fz]);
map.vertices.push([cx + half, cy + half, fz]);
map.vertices.push([cx - half, cy + half, fz]);
map.vertices.push([cx - half, cy - half, cz]);
map.vertices.push([cx + half, cy - half, cz]);
map.vertices.push([cx + half, cy + half, cz]);
map.vertices.push([cx - half, cy + half, cz]);
let normals = [
[0.0, -1.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[-1.0, 0.0, 0.0],
];
let indices = [
[base, base + 4, base + 5, base + 1],
[base + 1, base + 5, base + 6, base + 2],
[base + 2, base + 6, base + 7, base + 3],
[base + 3, base + 7, base + 4, base],
];
for (i, (norm, idx)) in normals.iter().zip(indices.iter()).enumerate() {
map.faces.push(Face {
vertex_indices: idx.to_vec(),
normal: *norm,
dist: 0.0,
color_index: color_idx,
is_sky: false,
light_level: light * if i % 2 == 0 { 0.8 } else { 0.6 },
tex_type: TexType::Metal,
});
}
map.walls.push(WallSeg {
x1: cx - half,
y1: cy - half,
x2: cx + half,
y2: cy - half,
floor_z: fz,
ceil_z: cz,
});
map.walls.push(WallSeg {
x1: cx + half,
y1: cy - half,
x2: cx + half,
y2: cy + half,
floor_z: fz,
ceil_z: cz,
});
map.walls.push(WallSeg {
x1: cx + half,
y1: cy + half,
x2: cx - half,
y2: cy + half,
floor_z: fz,
ceil_z: cz,
});
map.walls.push(WallSeg {
x1: cx - half,
y1: cy + half,
x2: cx - half,
y2: cy - half,
floor_z: fz,
ceil_z: cz,
});
}
#[allow(clippy::too_many_arguments)]
fn add_platform(
map: &mut QuakeMap,
x: f32,
y: f32,
w: f32,
h: f32,
height: f32,
light: f32,
color_idx: u8,
) {
let base = map.vertices.len();
map.vertices.push([x, y, height]);
map.vertices.push([x + w, y, height]);
map.vertices.push([x + w, y + h, height]);
map.vertices.push([x, y + h, height]);
map.faces.push(Face {
vertex_indices: vec![base, base + 1, base + 2, base + 3],
normal: [0.0, 0.0, 1.0],
dist: height,
color_index: color_idx,
is_sky: false,
light_level: light,
tex_type: TexType::Metal,
});
let side_base = map.vertices.len();
map.vertices.push([x, y, 0.0]); map.vertices.push([x + w, y, 0.0]);
map.vertices.push([x + w, y + h, 0.0]);
map.vertices.push([x, y + h, 0.0]);
map.faces.push(Face {
vertex_indices: vec![side_base, base, base + 1, side_base + 1],
normal: [0.0, -1.0, 0.0],
dist: -y,
color_index: color_idx,
is_sky: false,
light_level: light * 0.6,
tex_type: TexType::Metal,
});
map.rooms.push(Room {
x,
y,
width: w,
height: h,
floor_z: height,
ceil_z: height + 160.0,
light,
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_e1m1_creates_geometry() {
let map = generate_e1m1();
assert!(!map.vertices.is_empty());
assert!(!map.faces.is_empty());
assert!(!map.walls.is_empty());
assert!(!map.rooms.is_empty());
}
#[test]
fn floor_height_in_spawn_room() {
let map = generate_e1m1();
let h = map.floor_height_at(128.0, 128.0);
assert!((h - 0.0).abs() < 0.01);
}
#[test]
fn ceiling_in_spawn_room() {
let map = generate_e1m1();
let h = map.ceiling_height_at(128.0, 128.0);
assert!((h - 192.0).abs() < 0.01);
}
#[test]
fn collision_detects_walls() {
let map = generate_e1m1();
assert!(map.point_in_solid(-5.0, 128.0, 10.0, 16.0));
}
#[test]
fn platform_floor_height_returns_highest() {
let map = generate_e1m1();
let h = map.floor_height_at(656.0, 64.0);
assert!((h - 32.0).abs() < 0.01, "expected 32.0, got {h}");
}
#[test]
fn ceiling_height_returns_lowest() {
let map = generate_e1m1();
let h = map.ceiling_height_at(656.0, 64.0);
assert!((h - 192.0).abs() < 0.01, "expected 192.0, got {h}");
}
#[test]
fn collision_respects_z_bounds() {
let map = generate_e1m1();
assert!(map.point_in_solid(-5.0, 128.0, 10.0, 16.0));
assert!(!map.point_in_solid(-5.0, 128.0, 500.0, 16.0));
}
#[test]
fn supportive_floor_prevents_platform_teleport() {
let map = generate_e1m1();
let h = map.supportive_floor_at(656.0, 64.0, -32.0);
assert!((h - (-32.0)).abs() < 0.01, "expected -32.0, got {h}");
}
#[test]
fn supportive_floor_on_platform() {
let map = generate_e1m1();
let h = map.supportive_floor_at(656.0, 64.0, 32.0);
assert!((h - 32.0).abs() < 0.01, "expected 32.0, got {h}");
}
#[test]
fn circle_misses_distant_segment() {
assert!(!circle_intersects_segment(
100.0, 100.0, 5.0, 0.0, 0.0, 10.0, 0.0
));
}
#[test]
fn circle_hits_segment_center() {
assert!(circle_intersects_segment(
5.0, 1.0, 2.0, 0.0, 0.0, 10.0, 0.0
));
}
#[test]
fn circle_hits_segment_endpoint() {
assert!(circle_intersects_segment(
0.5, 0.0, 1.0, 1.0, 0.0, 10.0, 0.0
));
}
#[test]
fn circle_tangent_just_misses() {
assert!(!circle_intersects_segment(
5.0, 5.1, 5.0, 0.0, 0.0, 10.0, 0.0
));
}
#[test]
fn circle_zero_length_segment() {
assert!(circle_intersects_segment(0.0, 0.0, 1.0, 0.5, 0.0, 0.5, 0.0));
assert!(!circle_intersects_segment(
10.0, 10.0, 1.0, 0.5, 0.0, 0.5, 0.0
));
}
#[test]
fn empty_map_defaults() {
let map = QuakeMap::new();
assert!(map.vertices.is_empty());
assert!(map.faces.is_empty());
assert!(map.walls.is_empty());
assert!(map.rooms.is_empty());
assert_eq!(map.player_start, [0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn floor_height_outside_all_rooms() {
let map = generate_e1m1();
let h = map.floor_height_at(99999.0, 99999.0);
let lowest = map.rooms.iter().map(|r| r.floor_z).fold(f32::MAX, f32::min);
assert!(
(h - lowest).abs() < 0.01,
"outside rooms should return lowest floor, got {h}"
);
}
#[test]
fn ceiling_height_outside_all_rooms() {
let map = generate_e1m1();
let h = map.ceiling_height_at(99999.0, 99999.0);
assert!(
(h - 1000.0).abs() < 0.01,
"outside rooms should return 1000.0, got {h}"
);
}
#[test]
fn no_collision_in_open_area() {
let map = generate_e1m1();
assert!(!map.point_in_solid(128.0, 128.0, 10.0, 16.0));
}
#[test]
fn player_start_tuple_matches_array() {
let map = generate_e1m1();
let (x, y, z, a) = map.player_start();
assert_eq!(x, map.player_start[0]);
assert_eq!(y, map.player_start[1]);
assert_eq!(z, map.player_start[2]);
assert_eq!(a, map.player_start[3]);
}
#[test]
fn e1m1_has_multiple_rooms() {
let map = generate_e1m1();
assert!(
map.rooms.len() >= 4,
"expected at least 4 rooms, got {}",
map.rooms.len()
);
}
#[test]
fn supportive_floor_outside_all_rooms() {
let map = generate_e1m1();
let h = map.supportive_floor_at(99999.0, 99999.0, 0.0);
let lowest = map.rooms.iter().map(|r| r.floor_z).fold(f32::MAX, f32::min);
assert!(
(h - lowest).abs() < 0.01,
"outside rooms should return lowest floor, got {h}"
);
}
fn single_room_map() -> QuakeMap {
let mut map = QuakeMap::new();
add_room_geometry(&mut map, 0.0, 0.0, 100.0, 200.0, 0.0, 128.0, 0.8, 1);
map
}
#[test]
fn room_geometry_adds_eight_vertices() {
let map = single_room_map();
assert_eq!(
map.vertices.len(),
8,
"box room should have 8 corner vertices"
);
}
#[test]
fn room_geometry_adds_six_faces() {
let map = single_room_map();
assert_eq!(map.faces.len(), 6, "box room should have 6 faces");
}
#[test]
fn room_geometry_adds_four_wall_segments() {
let map = single_room_map();
assert_eq!(
map.walls.len(),
4,
"box room should have 4 collision wall segments"
);
}
#[test]
fn room_floor_face_normal_points_up() {
let map = single_room_map();
let floor = &map.faces[0];
assert_eq!(floor.normal, [0.0, 0.0, 1.0]);
assert_eq!(floor.tex_type, TexType::Floor);
}
#[test]
fn room_ceiling_face_normal_points_down() {
let map = single_room_map();
let ceil = &map.faces[1];
assert_eq!(ceil.normal, [0.0, 0.0, -1.0]);
assert_eq!(ceil.tex_type, TexType::Ceiling);
}
#[test]
fn room_ceiling_light_is_dimmed() {
let map = single_room_map();
let floor_light = map.faces[0].light_level;
let ceil_light = map.faces[1].light_level;
assert!(
ceil_light < floor_light,
"ceiling should be dimmer than floor: ceil={ceil_light} floor={floor_light}"
);
assert!((ceil_light - 0.8 * 0.7).abs() < 0.01);
}
#[test]
fn room_wall_normals_are_axis_aligned() {
let map = single_room_map();
let expected_normals = [
[0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], ];
for (i, expected) in expected_normals.iter().enumerate() {
assert_eq!(
map.faces[2 + i].normal,
*expected,
"wall {i} normal mismatch"
);
}
}
#[test]
fn room_ns_walls_brighter_than_ew_walls() {
let map = single_room_map();
let south_light = map.faces[2].light_level;
let west_light = map.faces[4].light_level;
assert!(
south_light > west_light,
"N/S walls ({south_light}) should be brighter than E/W walls ({west_light})"
);
assert!((south_light - 0.8 * 0.8).abs() < 0.01);
assert!((west_light - 0.8 * 0.6).abs() < 0.01);
}
#[test]
fn room_wall_segments_match_room_bounds() {
let map = single_room_map();
let s = &map.walls[0];
assert!((s.x1 - 0.0).abs() < 0.01);
assert!((s.y1 - 0.0).abs() < 0.01);
assert!((s.x2 - 100.0).abs() < 0.01);
assert!((s.y2 - 0.0).abs() < 0.01);
assert!((s.floor_z - 0.0).abs() < 0.01);
assert!((s.ceil_z - 128.0).abs() < 0.01);
}
#[test]
fn room_vertices_span_correct_bounds() {
let map = single_room_map();
let xs: Vec<f32> = map.vertices.iter().map(|v| v[0]).collect();
let ys: Vec<f32> = map.vertices.iter().map(|v| v[1]).collect();
let zs: Vec<f32> = map.vertices.iter().map(|v| v[2]).collect();
assert!((*xs.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() - 0.0).abs() < 0.01);
assert!(
(*xs.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() - 100.0).abs() < 0.01
);
assert!((*ys.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() - 0.0).abs() < 0.01);
assert!(
(*ys.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() - 200.0).abs() < 0.01
);
assert!((*zs.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() - 0.0).abs() < 0.01);
assert!(
(*zs.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() - 128.0).abs() < 0.01
);
}
#[test]
fn room_all_wall_faces_are_wall_type() {
let map = single_room_map();
for i in 2..6 {
assert_eq!(
map.faces[i].tex_type,
TexType::Wall,
"face {i} should be Wall type"
);
}
}
#[test]
fn pillar_adds_eight_vertices() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 50.0, 50.0, 20.0, 0.0, 100.0, 0.7, 2);
assert_eq!(map.vertices.len(), 8);
}
#[test]
fn pillar_adds_four_faces() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 50.0, 50.0, 20.0, 0.0, 100.0, 0.7, 2);
assert_eq!(
map.faces.len(),
4,
"pillar should have 4 wall faces (no floor/ceil)"
);
}
#[test]
fn pillar_adds_four_wall_segments() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 50.0, 50.0, 20.0, 0.0, 100.0, 0.7, 2);
assert_eq!(map.walls.len(), 4);
}
#[test]
fn pillar_faces_use_metal_texture() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 50.0, 50.0, 20.0, 0.0, 100.0, 0.7, 2);
for face in &map.faces {
assert_eq!(face.tex_type, TexType::Metal);
}
}
#[test]
fn pillar_walls_form_square() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 100.0, 100.0, 40.0, 0.0, 200.0, 0.7, 2);
let wall_xs: Vec<f32> = map.walls.iter().flat_map(|w| [w.x1, w.x2]).collect();
let wall_ys: Vec<f32> = map.walls.iter().flat_map(|w| [w.y1, w.y2]).collect();
let x_min = wall_xs.iter().cloned().fold(f32::MAX, f32::min);
let x_max = wall_xs.iter().cloned().fold(f32::MIN, f32::max);
let y_min = wall_ys.iter().cloned().fold(f32::MAX, f32::min);
let y_max = wall_ys.iter().cloned().fold(f32::MIN, f32::max);
assert!((x_min - 80.0).abs() < 0.01, "pillar x_min={x_min}");
assert!((x_max - 120.0).abs() < 0.01, "pillar x_max={x_max}");
assert!((y_min - 80.0).abs() < 0.01, "pillar y_min={y_min}");
assert!((y_max - 120.0).abs() < 0.01, "pillar y_max={y_max}");
}
#[test]
fn pillar_wall_z_ranges_match() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 50.0, 50.0, 20.0, -10.0, 150.0, 0.7, 2);
for wall in &map.walls {
assert!((wall.floor_z - (-10.0)).abs() < 0.01);
assert!((wall.ceil_z - 150.0).abs() < 0.01);
}
}
#[test]
fn pillar_alternating_light_levels() {
let mut map = QuakeMap::new();
add_pillar(&mut map, 50.0, 50.0, 20.0, 0.0, 100.0, 1.0, 2);
assert!((map.faces[0].light_level - 0.8).abs() < 0.01);
assert!((map.faces[1].light_level - 0.6).abs() < 0.01);
assert!((map.faces[2].light_level - 0.8).abs() < 0.01);
assert!((map.faces[3].light_level - 0.6).abs() < 0.01);
}
#[test]
fn platform_adds_room_entry() {
let mut map = QuakeMap::new();
add_platform(&mut map, 10.0, 20.0, 50.0, 60.0, 32.0, 0.9, 4);
assert_eq!(map.rooms.len(), 1);
let room = &map.rooms[0];
assert!((room.x - 10.0).abs() < 0.01);
assert!((room.y - 20.0).abs() < 0.01);
assert!((room.width - 50.0).abs() < 0.01);
assert!((room.height - 60.0).abs() < 0.01);
assert!((room.floor_z - 32.0).abs() < 0.01);
assert!((room.ceil_z - 192.0).abs() < 0.01); }
#[test]
fn platform_adds_top_face() {
let mut map = QuakeMap::new();
add_platform(&mut map, 0.0, 0.0, 64.0, 64.0, 48.0, 0.9, 4);
assert!(!map.faces.is_empty());
let top = &map.faces[0];
assert_eq!(top.normal, [0.0, 0.0, 1.0]);
assert!((top.dist - 48.0).abs() < 0.01);
assert_eq!(top.tex_type, TexType::Metal);
}
#[test]
fn platform_adds_south_step_face() {
let mut map = QuakeMap::new();
add_platform(&mut map, 0.0, 0.0, 64.0, 64.0, 48.0, 0.9, 4);
assert!(map.faces.len() >= 2);
let step = &map.faces[1];
assert_eq!(step.normal, [0.0, -1.0, 0.0]);
assert_eq!(step.tex_type, TexType::Metal);
}
#[test]
fn platform_adds_eight_vertices() {
let mut map = QuakeMap::new();
add_platform(&mut map, 0.0, 0.0, 64.0, 64.0, 48.0, 0.9, 4);
assert_eq!(map.vertices.len(), 8);
}
#[test]
fn platform_top_vertices_at_correct_height() {
let mut map = QuakeMap::new();
add_platform(&mut map, 10.0, 20.0, 30.0, 40.0, 50.0, 0.9, 4);
for i in 0..4 {
assert!(
(map.vertices[i][2] - 50.0).abs() < 0.01,
"top vertex {i} z={}",
map.vertices[i][2]
);
}
for i in 4..8 {
assert!(
(map.vertices[i][2] - 0.0).abs() < 0.01,
"bottom vertex {i} z={}",
map.vertices[i][2]
);
}
}
#[test]
fn circle_intersects_diagonal_segment() {
assert!(circle_intersects_segment(
0.0, 0.0, 1.0, -10.0, -10.0, 10.0, 10.0
));
}
#[test]
fn circle_misses_parallel_segment() {
assert!(!circle_intersects_segment(
0.0, 5.0, 4.0, -100.0, 0.0, 100.0, 0.0
));
}
#[test]
fn floor_height_single_room() {
let mut map = QuakeMap::new();
map.rooms.push(Room {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
floor_z: 10.0,
ceil_z: 200.0,
light: 1.0,
});
assert!((map.floor_height_at(50.0, 50.0) - 10.0).abs() < 0.01);
}
#[test]
fn point_in_solid_zero_radius() {
let mut map = QuakeMap::new();
map.walls.push(WallSeg {
x1: 0.0,
y1: 0.0,
x2: 100.0,
y2: 0.0,
floor_z: 0.0,
ceil_z: 100.0,
});
assert!(!map.point_in_solid(50.0, 10.0, 50.0, 0.0));
}
#[test]
fn supportive_floor_slightly_above_platform() {
let mut map = QuakeMap::new();
map.rooms.push(Room {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
floor_z: 0.0,
ceil_z: 200.0,
light: 1.0,
});
map.rooms.push(Room {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
floor_z: 50.0,
ceil_z: 200.0,
light: 1.0,
});
let h = map.supportive_floor_at(50.0, 50.0, 52.0);
assert!((h - 50.0).abs() < 0.01, "expected 50.0, got {h}");
}
#[test]
fn floor_height_at_room_boundary() {
let mut map = QuakeMap::new();
map.rooms.push(Room {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
floor_z: 5.0,
ceil_z: 200.0,
light: 1.0,
});
assert!((map.floor_height_at(0.0, 0.0) - 5.0).abs() < 0.01);
assert!((map.floor_height_at(100.0, 100.0) - 5.0).abs() < 0.01);
assert!((map.floor_height_at(100.0, 0.0) - 5.0).abs() < 0.01);
assert!((map.floor_height_at(0.0, 100.0) - 5.0).abs() < 0.01);
}
#[test]
fn empty_map_floor_height_fallback() {
let map = QuakeMap::new();
let h = map.floor_height_at(0.0, 0.0);
assert_eq!(h, f32::MAX);
}
}