use flatland_protocol::{InteriorDoorView, InteriorMapView, InteriorRoomView};
#[derive(Debug, Clone, Copy)]
struct SharedWallEdge {
horizontal: bool,
fixed: f32,
along_min: f32,
along_max: f32,
}
fn room_bounds(room: &InteriorRoomView) -> (f32, f32, f32, f32) {
(
room.x0.min(room.x1),
room.x0.max(room.x1),
room.y0.min(room.y1),
room.y0.max(room.y1),
)
}
pub fn interior_wall_glyph_line(fixed: f32) -> i32 {
fixed.round() as i32
}
fn rect_perimeter_edges(room: &InteriorRoomView) -> [SharedWallEdge; 4] {
let (w, e, s, n) = room_bounds(room);
[
SharedWallEdge {
horizontal: true,
fixed: s,
along_min: w,
along_max: e,
},
SharedWallEdge {
horizontal: true,
fixed: n,
along_min: w,
along_max: e,
},
SharedWallEdge {
horizontal: false,
fixed: w,
along_min: s,
along_max: n,
},
SharedWallEdge {
horizontal: false,
fixed: e,
along_min: s,
along_max: n,
},
]
}
fn nearest_room_edge<'a>(
rooms: impl Iterator<Item = &'a InteriorRoomView>,
x: f32,
y: f32,
) -> Option<SharedWallEdge> {
let mut best: Option<(SharedWallEdge, f32, bool)> = None;
for room in rooms {
for edge in rect_perimeter_edges(room) {
let dist = if edge.horizontal {
(y - edge.fixed).abs()
} else {
(x - edge.fixed).abs()
};
let along = if edge.horizontal { x } else { y };
let contains = along >= edge.along_min - 0.6 && along <= edge.along_max + 0.6;
let replace = match best {
None => true,
Some((_, bd, b_contains)) => {
dist < bd - 0.01
|| ((dist - bd).abs() <= 0.01 && contains && !b_contains)
}
};
if replace {
best = Some((edge, dist, contains));
}
}
}
best.map(|(edge, _, _)| edge)
}
fn display_on_wall_edge(edge: &SharedWallEdge, x: f32, y: f32) -> (f32, f32) {
let line = interior_wall_glyph_line(edge.fixed) as f32;
if edge.horizontal {
let along = x.clamp(edge.along_min, edge.along_max);
(along, line)
} else {
let along = y.clamp(edge.along_min, edge.along_max);
(line, along)
}
}
fn perimeter_on_floor(interior: &InteriorMapView, floor: i32, x: f32, y: f32) -> (f32, f32) {
let rooms = interior.rooms.iter().filter(|r| r.floor == floor);
match nearest_room_edge(rooms, x, y) {
Some(edge) => display_on_wall_edge(&edge, x, y),
None => (x, y),
}
}
pub fn interior_room_door_display_xy(
door: &InteriorDoorView,
interior: &InteriorMapView,
floor: i32,
) -> (f32, f32) {
if door.kind == "stairs" {
let room_a = interior.rooms.iter().find(|r| r.id == door.room_a);
let room_b = interior.rooms.iter().find(|r| r.id == door.room_b);
if room_a.map(|r| r.floor) == Some(floor) {
return (door.x_a.unwrap_or(door.x), door.y_a.unwrap_or(door.y));
}
if room_b.map(|r| r.floor) == Some(floor) {
return (door.x_b.unwrap_or(door.x), door.y_b.unwrap_or(door.y));
}
return (door.x_a.unwrap_or(door.x), door.y_a.unwrap_or(door.y));
}
let rooms = interior
.rooms
.iter()
.filter(|r| r.id == door.room_a || r.id == door.room_b);
match nearest_room_edge(rooms, door.x, door.y) {
Some(edge) => display_on_wall_edge(&edge, door.x, door.y),
None => (door.x, door.y),
}
}
pub fn interior_door_display_xy(
interior: &InteriorMapView,
floor: i32,
door_id: &str,
x: f32,
y: f32,
) -> (f32, f32) {
if let Some(rd) = interior.room_doors.iter().find(|d| d.id == door_id) {
return interior_room_door_display_xy(rd, interior, floor);
}
perimeter_on_floor(interior, floor, x, y)
}
#[cfg(test)]
mod tests {
use super::*;
use flatland_protocol::{InteriorDoorView, InteriorMapView, InteriorRoomView};
fn room(id: &str, x0: f32, y0: f32, x1: f32, y1: f32, floor: i32) -> InteriorRoomView {
InteriorRoomView {
id: id.into(),
label: id.into(),
floor,
x0,
y0,
x1,
y1,
floor_color: None,
floor_glyph: None,
}
}
fn town_hall() -> InteriorMapView {
InteriorMapView {
building_id: "town_hall".into(),
blueprint_id: "town_hall".into(),
background_color: "#000".into(),
default_floor_color: Some("#2a2a2a".into()),
floor_height_m: 3.0,
z_platforms: vec![],
z_transitions: vec![],
rooms: vec![
room("main", -3.5, -8.0, 18.5, 6.0, 0),
room("kitchen", 18.5, -8.0, 23.5, 0.0, 0),
room("weapon", 18.5, 0.0, 25.5, 12.5, 0),
room("hall_n", -3.5, 6.0, 12.0, 12.5, 0),
room("meeting", -3.5, 12.5, 12.0, 23.5, 0),
room("office", 12.0, 6.0, 18.5, 12.5, 0),
],
room_doors: vec![],
}
}
fn door(id: &str, a: &str, b: &str, x: f32, y: f32, kind: &str) -> InteriorDoorView {
InteriorDoorView {
id: id.into(),
room_a: a.into(),
room_b: b.into(),
x,
y,
kind: kind.into(),
x_a: None,
y_a: None,
x_b: None,
y_b: None,
}
}
#[test]
fn glyph_line_rounds_to_blocking_cell() {
assert_eq!(interior_wall_glyph_line(-8.0), -8);
assert_eq!(interior_wall_glyph_line(0.0), 0);
assert_eq!(interior_wall_glyph_line(6.0), 6);
assert_eq!(interior_wall_glyph_line(12.5), 13);
assert_eq!(interior_wall_glyph_line(18.5), 19);
assert_eq!(interior_wall_glyph_line(-3.5), -4);
assert_eq!(interior_wall_glyph_line(23.5), 24);
assert_eq!(interior_wall_glyph_line(25.5), 26);
}
#[test]
fn door_snaps_to_nearest_wall_line() {
let interior = town_hall();
let d = door("d_office", "main", "office", 12.0, 9.0, "door");
assert_eq!(interior_room_door_display_xy(&d, &interior, 0), (12.0, 9.0));
let d = door("d_kitchen2", "weapon", "kitchen", 18.5, 8.5, "door");
assert_eq!(interior_room_door_display_xy(&d, &interior, 0), (19.0, 8.5));
let d = door("d_meeting", "main", "meeting", 5.5, 12.5, "door");
assert_eq!(interior_room_door_display_xy(&d, &interior, 0), (5.5, 13.0));
let d = door("d_hall_n", "main", "hall_n", 6.0, 6.0, "door");
assert_eq!(interior_room_door_display_xy(&d, &interior, 0), (6.0, 6.0));
}
#[test]
fn stairs_keep_authored_position() {
let interior = town_hall();
let d = door("stairs", "main", "upper", -2.0, 4.5, "stairs");
assert_eq!(interior_room_door_display_xy(&d, &interior, 0), (-2.0, 4.5));
}
#[test]
fn perimeter_fallback_projects_mid_room_portal_to_nearest_wall() {
let interior = town_hall();
let (x, y) = interior_door_display_xy(&interior, 0, "front", 7.5, -6.0);
assert_eq!((x, y), (7.5, -8.0));
}
}