pub struct Poi {
pub id: u32,
pub position: [f32; 3],
pub interaction_radius: f32,
pub state: PoiState,
pub cooldown_duration: f32,
pub cooldown_remaining: f32,
pub tag: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoiState {
Available,
Triggered,
Cooldown,
}
#[derive(Debug, Clone)]
pub enum PoiEvent {
Entered { poi_id: u32 },
Triggered { poi_id: u32 },
Ready { poi_id: u32 },
}
pub struct PoiSystem {
pois: Vec<Poi>,
events: Vec<PoiEvent>,
next_id: u32,
}
impl Default for PoiSystem {
fn default() -> Self {
Self::new()
}
}
impl PoiSystem {
pub fn new() -> Self {
Self {
pois: Vec::new(),
events: Vec::new(),
next_id: 1,
}
}
pub fn add_poi(&mut self, position: [f32; 3], radius: f32, cooldown: f32) -> u32 {
let id = self.next_id;
self.next_id += 1;
self.pois.push(Poi {
id,
position,
interaction_radius: radius,
state: PoiState::Available,
cooldown_duration: cooldown,
cooldown_remaining: 0.0,
tag: None,
});
id
}
pub fn add_poi_tagged(&mut self, position: [f32; 3], radius: f32, cooldown: f32, tag: &str) -> u32 {
let id = self.add_poi(position, radius, cooldown);
if let Some(poi) = self.pois.last_mut() {
poi.tag = Some(tag.to_string());
}
id
}
pub fn check_interaction(&mut self, player_pos: [f32; 3]) -> Option<u32> {
for poi in &mut self.pois {
if poi.state != PoiState::Available {
continue;
}
let dx = poi.position[0] - player_pos[0];
let dy = poi.position[1] - player_pos[1];
let dz = poi.position[2] - player_pos[2];
let dist2 = dx * dx + dy * dy + dz * dz;
if dist2 <= poi.interaction_radius * poi.interaction_radius {
poi.state = PoiState::Triggered;
poi.cooldown_remaining = poi.cooldown_duration;
self.events.push(PoiEvent::Triggered { poi_id: poi.id });
return Some(poi.id);
}
}
None
}
pub fn update(&mut self, dt: f32) {
for poi in &mut self.pois {
match poi.state {
PoiState::Triggered => {
poi.state = PoiState::Cooldown;
}
PoiState::Cooldown => {
poi.cooldown_remaining -= dt;
if poi.cooldown_remaining <= 0.0 {
poi.state = PoiState::Available;
self.events.push(PoiEvent::Ready { poi_id: poi.id });
}
}
PoiState::Available => {}
}
}
}
pub fn nearest_available(&self, position: [f32; 3]) -> Option<(u32, f32)> {
self.pois
.iter()
.filter(|p| p.state == PoiState::Available)
.map(|p| {
let dx = p.position[0] - position[0];
let dy = p.position[1] - position[1];
let dz = p.position[2] - position[2];
(p.id, (dx * dx + dy * dy + dz * dz).sqrt())
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
}
pub fn drain_events(&mut self) -> std::vec::Drain<'_, PoiEvent> {
self.events.drain(..)
}
pub fn len(&self) -> usize {
self.pois.len()
}
pub fn is_empty(&self) -> bool {
self.pois.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Poi> {
self.pois.iter()
}
pub fn ring_dreamlets_into(&self, poi_id: u32, time: f32, result: &mut Vec<([f32; 3], [f32; 4], f32)>) {
let Some(poi) = self.pois.iter().find(|p| p.id == poi_id) else {
return;
};
let segments = 8;
let pulse = (time * 2.0).sin() * 0.5 + 0.5;
let color = match poi.state {
PoiState::Available => [0.2, 1.0, 0.4, 0.6 + pulse * 0.4],
PoiState::Triggered => [1.0, 0.8, 0.2, 1.0],
PoiState::Cooldown => [0.5, 0.5, 0.5, 0.3],
};
for i in 0..segments {
let angle = (i as f32 / segments as f32) * std::f32::consts::TAU;
let r = poi.interaction_radius;
let pos = [
poi.position[0] + r * angle.cos(),
poi.position[1] + 0.1,
poi.position[2] + r * angle.sin(),
];
result.push((pos, color, 0.15));
}
}
pub fn ring_dreamlets(&self, poi_id: u32, time: f32) -> Vec<([f32; 3], [f32; 4], f32)> {
let mut result = Vec::with_capacity(8);
self.ring_dreamlets_into(poi_id, time, &mut result);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn triggers_within_radius() {
let mut sys = PoiSystem::new();
sys.add_poi([5.0, 0.0, 5.0], 2.0, 3.0);
let id = sys.check_interaction([5.5, 0.0, 5.5]);
assert!(id.is_some());
let mut sys2 = PoiSystem::new();
sys2.add_poi([5.0, 0.0, 5.0], 2.0, 3.0);
let id = sys2.check_interaction([100.0, 0.0, 100.0]);
assert!(id.is_none());
}
#[test]
fn cooldown_prevents_retrigger() {
let mut sys = PoiSystem::new();
sys.add_poi([0.0, 0.0, 0.0], 5.0, 2.0);
let id1 = sys.check_interaction([0.0, 0.0, 0.0]);
assert!(id1.is_some());
sys.update(0.0);
assert_eq!(sys.pois[0].state, PoiState::Cooldown);
let id2 = sys.check_interaction([0.0, 0.0, 0.0]);
assert!(id2.is_none());
sys.update(2.1);
assert_eq!(sys.pois[0].state, PoiState::Available);
let id3 = sys.check_interaction([0.0, 0.0, 0.0]);
assert!(id3.is_some());
}
}