use std::collections::{HashMap, VecDeque};
use crate::coord::CoordSystem;
use crate::octree::OctreeNode;
use crate::store::ZoneStore;
use crate::zone::{Zone, ZoneEntry};
#[derive(Clone, Copy, Debug, Default)]
pub struct DirectCartesian;
impl CoordSystem for DirectCartesian {
fn to_internal(&self, p: [f64; 3]) -> [f64; 3] {
p
}
fn from_internal(&self, p: [f64; 3]) -> [f64; 3] {
p
}
}
#[derive(Clone, Copy, Debug)]
pub struct ScaledCartesian {
pub scale_to_meters: f64,
pub origin: [f64; 3],
}
impl CoordSystem for ScaledCartesian {
fn to_internal(&self, p: [f64; 3]) -> [f64; 3] {
std::array::from_fn(|i| (p[i] - self.origin[i]) * self.scale_to_meters)
}
fn from_internal(&self, p: [f64; 3]) -> [f64; 3] {
std::array::from_fn(|i| p[i] / self.scale_to_meters + self.origin[i])
}
}
#[derive(Clone, Copy, Debug)]
pub struct YUpCartesian {
pub scale: f64,
}
impl CoordSystem for YUpCartesian {
fn to_internal(&self, p: [f64; 3]) -> [f64; 3] {
[p[0] * self.scale, p[2] * self.scale, p[1] * self.scale]
}
fn from_internal(&self, p: [f64; 3]) -> [f64; 3] {
[p[0] / self.scale, p[2] / self.scale, p[1] / self.scale]
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cartesian2D {
pub scale: f64,
pub layer: f64,
}
impl CoordSystem for Cartesian2D {
fn to_internal(&self, p: [f64; 3]) -> [f64; 3] {
[p[0] * self.scale, p[1] * self.scale, self.layer]
}
fn from_internal(&self, p: [f64; 3]) -> [f64; 3] {
[p[0] / self.scale, p[1] / self.scale, 0.0]
}
}
pub struct GameInstance {
pub instance_id: u32,
pub store: ZoneStore,
pub octree: OctreeNode,
pub coord: Box<dyn CoordSystem>,
}
pub struct GameWorld {
instances: HashMap<u32, GameInstance>,
zone_templates: Vec<ZoneEntry>,
}
impl GameWorld {
pub fn new(templates: Vec<ZoneEntry>) -> Self {
Self { instances: HashMap::new(), zone_templates: templates }
}
pub fn spawn_instance(&mut self, id: u32, coord: Box<dyn CoordSystem>, world_half: f64) {
let store = ZoneStore::from_entries(&self.zone_templates, coord.as_ref());
self.instances.insert(
id,
GameInstance {
instance_id: id,
store,
octree: OctreeNode::new([0.0; 3], world_half),
coord,
},
);
}
pub fn despawn_instance(&mut self, id: u32) {
self.instances.remove(&id);
}
pub fn instance_count(&self) -> usize {
self.instances.len()
}
pub fn query(&self, instance_id: u32, pos: [f64; 3]) -> Option<Vec<u32>> {
let inst = self.instances.get(&instance_id)?;
Some(inst.store.query_enu(inst.coord.to_internal(pos)).to_vec())
}
pub fn add_zone_to_instance(&mut self, instance_id: u32, entry: ZoneEntry) -> bool {
let Some(inst) = self.instances.get_mut(&instance_id) else {
return false;
};
inst.store.add_zone(entry.id, &entry.zone, inst.coord.as_ref());
true
}
}
pub struct LayeredMap {
pub layers: HashMap<i32, ZoneStore>,
pub coord: ScaledCartesian,
pub floor_height: f64,
}
impl LayeredMap {
pub fn new(coord: ScaledCartesian, floor_height: f64) -> Self {
Self { layers: HashMap::new(), coord, floor_height }
}
pub fn floor_of(&self, z_game: f64) -> i32 {
let z_m = z_game * self.coord.scale_to_meters;
(z_m / self.floor_height).floor() as i32
}
pub fn add_zone(&mut self, floor: i32, entry: ZoneEntry) {
let coord = self.coord;
self.layers
.entry(floor)
.or_insert_with(|| ZoneStore::from_entries(&[], &coord))
.add_zone(entry.id, &entry.zone, &coord);
}
pub fn query(&self, pos: [f64; 3]) -> Vec<(i32, Vec<u32>)> {
let floor = self.floor_of(pos[2]);
let p = self.coord.to_internal(pos);
[floor - 1, floor, floor + 1]
.iter()
.filter_map(|&f| {
let hits = self.layers.get(&f)?.query_enu(p);
(!hits.is_empty()).then_some((f, hits.to_vec()))
})
.collect()
}
}
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
pub struct ChunkKey {
pub cx: i32,
pub cy: i32,
pub cz: i32,
}
impl ChunkKey {
pub fn from_pos(pos: [f64; 3], chunk_size: f64) -> Self {
Self {
cx: (pos[0] / chunk_size).floor() as i32,
cy: (pos[1] / chunk_size).floor() as i32,
cz: (pos[2] / chunk_size).floor() as i32,
}
}
pub fn neighbors(&self) -> Vec<ChunkKey> {
let mut v = Vec::with_capacity(27);
for dz in -1i32..=1 {
for dy in -1i32..=1 {
for dx in -1i32..=1 {
v.push(ChunkKey { cx: self.cx + dx, cy: self.cy + dy, cz: self.cz + dz });
}
}
}
v
}
}
pub struct GameChunk {
pub store: ZoneStore,
pub octree: OctreeNode,
}
pub struct ChunkedGameWorld {
pub chunk_size: f64,
pub coord: Box<dyn CoordSystem>,
pub max_chunks: usize,
chunks: HashMap<ChunkKey, GameChunk>,
access_order: VecDeque<ChunkKey>,
}
impl ChunkedGameWorld {
pub fn new(chunk_size: f64, coord: Box<dyn CoordSystem>, max_chunks: usize) -> Self {
Self {
chunk_size,
coord,
max_chunks: max_chunks.max(1),
chunks: HashMap::new(),
access_order: VecDeque::new(),
}
}
pub fn loaded_chunks(&self) -> usize {
self.chunks.len()
}
fn touch(&mut self, key: ChunkKey) {
if let Some(pos) = self.access_order.iter().position(|&k| k == key) {
self.access_order.remove(pos);
}
self.access_order.push_back(key);
}
fn ensure_loaded(&mut self, key: ChunkKey) {
if self.chunks.contains_key(&key) {
self.touch(key);
return;
}
if self.chunks.len() >= self.max_chunks
&& let Some(old) = self.access_order.pop_front()
{
self.chunks.remove(&old);
}
let h = self.chunk_size / 2.0;
let origin =
std::array::from_fn(|i| [key.cx, key.cy, key.cz][i] as f64 * self.chunk_size + h);
self.chunks.insert(
key,
GameChunk {
store: ZoneStore::from_entries(&[], self.coord.as_ref()),
octree: OctreeNode::new(origin, h),
},
);
self.access_order.push_back(key);
}
pub fn query(&mut self, pos: [f64; 3]) -> Vec<u32> {
let p = self.coord.to_internal(pos);
let key = ChunkKey::from_pos(p, self.chunk_size);
key.neighbors()
.into_iter()
.flat_map(|k| {
self.ensure_loaded(k);
self.chunks.get(&k).unwrap().store.query_enu(p)
})
.collect()
}
pub fn insert_point(&mut self, pos: [f64; 3]) {
let p = self.coord.to_internal(pos);
let key = ChunkKey::from_pos(p, self.chunk_size);
self.ensure_loaded(key);
self.chunks.get_mut(&key).unwrap().octree.insert(p, 8);
}
pub fn add_zone_at(&mut self, ref_pos: [f64; 3], entry: ZoneEntry) {
let p = self.coord.to_internal(ref_pos);
let key = ChunkKey::from_pos(p, self.chunk_size);
self.ensure_loaded(key);
let coord = &self.coord;
self.chunks
.get_mut(&key)
.unwrap()
.store
.add_zone(entry.id, &entry.zone, coord.as_ref());
}
}
#[derive(Debug, Clone)]
pub struct Portal {
pub id: u32,
pub trigger_zone: Zone,
pub dest_instance: u32,
pub dest_pos: [f64; 3],
pub dest_yaw_deg: f32,
}
pub struct PortalSystem {
portals: Vec<Portal>,
portal_store: ZoneStore,
}
impl PortalSystem {
pub fn build<C: CoordSystem + ?Sized>(portals: Vec<Portal>, coord: &C) -> Self {
let entries: Vec<ZoneEntry> = portals
.iter()
.map(|p| ZoneEntry::new(p.id, p.trigger_zone.clone()))
.collect();
Self { portal_store: ZoneStore::from_entries(&entries, coord), portals }
}
pub fn check<C: CoordSystem + ?Sized>(&self, pos: [f64; 3], coord: &C) -> Option<&Portal> {
let p = coord.to_internal(pos);
let hits = self.portal_store.query_enu(p);
hits.first().and_then(|&id| self.portals.iter().find(|p| p.id == id))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaled_cartesian_round_trip() {
let c = ScaledCartesian { scale_to_meters: 0.01, origin: [100.0, 0.0, 0.0] };
assert_eq!(c.to_internal([100.0, 0.0, 0.0]), [0.0, 0.0, 0.0]);
assert_eq!(c.to_internal([200.0, 0.0, 0.0])[0], 1.0);
let back = c.from_internal(c.to_internal([350.0, 40.0, -8.0]));
for i in 0..3 {
assert!((back[i] - [350.0, 40.0, -8.0][i]).abs() < 1e-9);
}
}
#[test]
fn yup_remaps_axes() {
let c = YUpCartesian { scale: 1.0 };
assert_eq!(c.to_internal([1.0, 2.0, 3.0]), [1.0, 3.0, 2.0]);
assert_eq!(c.from_internal([1.0, 3.0, 2.0]), [1.0, 2.0, 3.0]);
}
#[test]
fn cartesian2d_pins_layer() {
let c = Cartesian2D { scale: 2.0, layer: 7.0 };
assert_eq!(c.to_internal([3.0, 4.0, 999.0]), [6.0, 8.0, 7.0]);
}
fn unit_square_zone(id: u32) -> ZoneEntry {
ZoneEntry::new(id, Zone::Aabb { min: [-5.0, -5.0, -100.0], max: [5.0, 5.0, 100.0] })
}
#[test]
fn game_world_instances_are_isolated() {
let mut world = GameWorld::new(vec![unit_square_zone(1)]);
world.spawn_instance(10, Box::new(DirectCartesian), 1000.0);
world.spawn_instance(11, Box::new(DirectCartesian), 1000.0);
assert_eq!(world.instance_count(), 2);
assert_eq!(world.query(10, [0.0, 0.0, 0.0]), Some(vec![1]));
assert_eq!(world.query(11, [0.0, 0.0, 0.0]), Some(vec![1]));
world.add_zone_to_instance(10, unit_square_zone(2));
let mut a = world.query(10, [0.0, 0.0, 0.0]).unwrap();
a.sort();
assert_eq!(a, vec![1, 2]);
assert_eq!(world.query(11, [0.0, 0.0, 0.0]), Some(vec![1]));
world.despawn_instance(11);
assert_eq!(world.instance_count(), 1);
assert_eq!(world.query(11, [0.0, 0.0, 0.0]), None);
}
#[test]
fn scaled_query_respects_units() {
let mut world = GameWorld::new(vec![]);
let coord = ScaledCartesian { scale_to_meters: 0.01, origin: [0.0; 3] };
world.spawn_instance(1, Box::new(coord), 100_000.0);
world.add_zone_to_instance(
1,
ZoneEntry::new(
9,
Zone::Aabb { min: [-500.0, -500.0, -500.0], max: [500.0, 500.0, 500.0] },
),
);
assert_eq!(world.query(1, [100.0, 0.0, 0.0]), Some(vec![9]));
assert_eq!(world.query(1, [1000.0, 0.0, 0.0]), Some(vec![]));
}
#[test]
fn layered_map_separates_floors() {
let coord = ScaledCartesian { scale_to_meters: 1.0, origin: [0.0; 3] };
let mut map = LayeredMap::new(coord, 3.0); map.add_zone(0, unit_square_zone(100));
map.add_zone(1, unit_square_zone(101));
assert_eq!(map.floor_of(1.0), 0);
assert_eq!(map.floor_of(4.0), 1);
let hits = map.query([0.0, 0.0, 1.0]);
let floors: Vec<i32> = hits.iter().map(|(f, _)| *f).collect();
assert!(floors.contains(&0));
assert!(floors.contains(&1));
assert!(!floors.contains(&-1), "no zones two floors away");
}
#[test]
fn chunked_world_loads_and_evicts() {
let mut world =
ChunkedGameWorld::new(16.0, Box::new(DirectCartesian), 4); for i in 0..10 {
world.insert_point([i as f64 * 64.0, 0.0, 0.0]);
}
assert!(world.loaded_chunks() <= 4, "LRU cap honoured");
}
#[test]
fn chunked_world_query_finds_zone() {
let mut world = ChunkedGameWorld::new(32.0, Box::new(DirectCartesian), 64);
world.add_zone_at([0.0, 0.0, 0.0], unit_square_zone(5));
assert!(world.query([0.0, 0.0, 0.0]).contains(&5));
}
#[test]
fn portal_triggers_inside_zone() {
let coord = DirectCartesian;
let portals = vec![Portal {
id: 1,
trigger_zone: Zone::Aabb { min: [-1.0, -1.0, -1.0], max: [1.0, 1.0, 1.0] },
dest_instance: 99,
dest_pos: [50.0, 0.0, 0.0],
dest_yaw_deg: 90.0,
}];
let sys = PortalSystem::build(portals, &coord);
let hit = sys.check([0.0, 0.0, 0.0], &coord).expect("inside trigger");
assert_eq!(hit.dest_instance, 99);
assert!(sys.check([10.0, 10.0, 10.0], &coord).is_none());
}
}