dreamwell-matter 1.0.0

DreamMatter benchmark — GPU physics materialization demo and profiler
Documentation
// POI system — points of interest with interaction, cooldown, and visual feedback.
//
// Each POI has a position, interaction radius, state machine (Available/Triggered/Cooldown),
// and generates visual dreamlets (emissive ring + interaction effect).

/// A point of interest in the scene.
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>,
}

/// POI interaction state machine.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoiState {
    Available,
    Triggered,
    Cooldown,
}

/// Events emitted by the POI system.
#[derive(Debug, Clone)]
pub enum PoiEvent {
    /// Player entered interaction radius.
    Entered { poi_id: u32 },
    /// Player triggered interaction.
    Triggered { poi_id: u32 },
    /// Cooldown completed.
    Ready { poi_id: u32 },
}

/// Manages all POIs in a scene.
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,
        }
    }

    /// Add a POI at the given position.
    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
    }

    /// Add a POI with a semantic tag.
    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
    }

    /// Check if the player can interact with any POI and trigger it.
    /// Returns the POI ID if an interaction occurred.
    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
    }

    /// Update POI states: transition Triggered → Cooldown → Available.
    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 => {}
            }
        }
    }

    /// Find the nearest available POI to a position. Returns (poi_id, distance).
    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())
    }

    /// Drain pending events. Clean Compute: returns a draining iterator, no allocation.
    pub fn drain_events(&mut self) -> std::vec::Drain<'_, PoiEvent> {
        self.events.drain(..)
    }

    /// Number of POIs.
    pub fn len(&self) -> usize {
        self.pois.len()
    }

    pub fn is_empty(&self) -> bool {
        self.pois.is_empty()
    }

    /// Iterate all POIs.
    pub fn iter(&self) -> impl Iterator<Item = &Poi> {
        self.pois.iter()
    }

    /// Generate visual dreamlet data for POI rings into a caller-provided buffer.
    /// Appends (position, color, scale) tuples for each ring segment.
    /// Clean Compute: caller owns the buffer, no per-frame allocation.
    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));
        }

    }

    /// Convenience wrapper (allocating). Prefer `ring_dreamlets_into` on hot paths.
    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);

        // Player within radius.
        let id = sys.check_interaction([5.5, 0.0, 5.5]);
        assert!(id.is_some());

        // Player outside radius.
        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);

        // First trigger succeeds.
        let id1 = sys.check_interaction([0.0, 0.0, 0.0]);
        assert!(id1.is_some());

        // Transition to cooldown.
        sys.update(0.0);
        assert_eq!(sys.pois[0].state, PoiState::Cooldown);

        // Second trigger fails (cooldown).
        let id2 = sys.check_interaction([0.0, 0.0, 0.0]);
        assert!(id2.is_none());

        // After cooldown expires.
        sys.update(2.1);
        assert_eq!(sys.pois[0].state, PoiState::Available);

        // Can trigger again.
        let id3 = sys.check_interaction([0.0, 0.0, 0.0]);
        assert!(id3.is_some());
    }
}