dreamwell-engine 1.0.0

Dreamwell pure-logic engine library — transforms, hierarchy, canon pipeline, spatial math, hashing, tile rules, validation, waymark schema, material/lighting descriptors. No SpacetimeDB dependency.
Documentation
use super::errors::{TopologyError, WaymarkError};
use super::topology::{TopologyLayer, TopologyTree};
use crate::physics::properties::PropertyValue;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TransitionMode {
    SeamlessStream,
    PhaseOverlay,
    CutScene,
    FadeBlack,
    Portal,
    Teleport,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransitionDef {
    pub id: String,
    pub name: String,
    pub from_layer: TopologyLayer,
    pub from_id: String,
    pub to_layer: TopologyLayer,
    pub to_id: String,
    pub mode: TransitionMode,
    pub properties: HashMap<String, PropertyValue>,
}

#[derive(Debug, Clone, Default)]
pub struct TransitionRegistry {
    transitions: Vec<TransitionDef>,
    id_index: HashMap<String, usize>,
}

impl TransitionRegistry {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn register(&mut self, def: TransitionDef) -> Result<(), WaymarkError> {
        if self.id_index.contains_key(&def.id) {
            return Err(WaymarkError::Topology(TopologyError::InvalidTransition {
                from: def.from_id.clone(),
                to: def.to_id.clone(),
            }));
        }
        let idx = self.transitions.len();
        self.id_index.insert(def.id.clone(), idx);
        self.transitions.push(def);
        Ok(())
    }

    pub fn find(&self, id: &str) -> Option<&TransitionDef> {
        self.id_index.get(id).map(|&i| &self.transitions[i])
    }

    pub fn from_node(&self, node_id: &str) -> Vec<&TransitionDef> {
        self.transitions.iter().filter(|t| t.from_id == node_id).collect()
    }

    pub fn to_node(&self, node_id: &str) -> Vec<&TransitionDef> {
        self.transitions.iter().filter(|t| t.to_id == node_id).collect()
    }

    pub fn validate(&self, tree: &TopologyTree) -> Vec<WaymarkError> {
        let mut errors = Vec::new();
        for t in &self.transitions {
            if tree.get(&t.from_id).is_none() {
                errors.push(WaymarkError::Topology(TopologyError::InvalidTransition {
                    from: t.from_id.clone(),
                    to: t.to_id.clone(),
                }));
            }
            if tree.get(&t.to_id).is_none() {
                errors.push(WaymarkError::Topology(TopologyError::InvalidTransition {
                    from: t.from_id.clone(),
                    to: t.to_id.clone(),
                }));
            }
            // Check adjacency or realm overlay
            let diff = (t.from_layer.depth() as i8 - t.to_layer.depth() as i8).unsigned_abs();
            if diff > 1 && t.from_layer != TopologyLayer::Realm && t.to_layer != TopologyLayer::Realm {
                errors.push(WaymarkError::Topology(TopologyError::InvalidTransition {
                    from: format!("{:?}:{}", t.from_layer, t.from_id),
                    to: format!("{:?}:{}", t.to_layer, t.to_id),
                }));
            }
        }
        errors
    }

    pub fn len(&self) -> usize {
        self.transitions.len()
    }
    pub fn is_empty(&self) -> bool {
        self.transitions.is_empty()
    }
}

impl TransitionMode {
    pub const ALL: &[TransitionMode] = &[
        Self::SeamlessStream,
        Self::PhaseOverlay,
        Self::CutScene,
        Self::FadeBlack,
        Self::Portal,
        Self::Teleport,
    ];

    pub fn name(&self) -> &'static str {
        match self {
            Self::SeamlessStream => "Seamless Stream",
            Self::PhaseOverlay => "Phase Overlay",
            Self::CutScene => "Cut Scene",
            Self::FadeBlack => "Fade Black",
            Self::Portal => "Portal",
            Self::Teleport => "Teleport",
        }
    }
}

#[cfg(test)]
mod tests {
    use super::super::topology::TopologyNode;
    use super::*;

    fn make_tree() -> TopologyTree {
        let mut tree = TopologyTree::new();
        tree.insert(TopologyNode {
            layer: TopologyLayer::Location,
            id: "loc1".into(),
            name: "L1".into(),
            parent_id: None,
            properties: HashMap::new(),
            tags: vec![],
        })
        .unwrap();
        tree.insert(TopologyNode {
            layer: TopologyLayer::Room,
            id: "room1".into(),
            name: "R1".into(),
            parent_id: Some("loc1".into()),
            properties: HashMap::new(),
            tags: vec![],
        })
        .unwrap();
        tree
    }

    #[test]
    fn valid_adjacent_transition() {
        let tree = make_tree();
        let mut reg = TransitionRegistry::new();
        reg.register(TransitionDef {
            id: "t1".into(),
            name: "T1".into(),
            from_layer: TopologyLayer::Location,
            from_id: "loc1".into(),
            to_layer: TopologyLayer::Room,
            to_id: "room1".into(),
            mode: TransitionMode::SeamlessStream,
            properties: HashMap::new(),
        })
        .unwrap();
        let errors = reg.validate(&tree);
        assert!(errors.is_empty());
    }

    #[test]
    fn invalid_missing_node() {
        let tree = make_tree();
        let mut reg = TransitionRegistry::new();
        reg.register(TransitionDef {
            id: "t2".into(),
            name: "T2".into(),
            from_layer: TopologyLayer::Location,
            from_id: "loc1".into(),
            to_layer: TopologyLayer::Room,
            to_id: "missing".into(),
            mode: TransitionMode::Portal,
            properties: HashMap::new(),
        })
        .unwrap();
        let errors = reg.validate(&tree);
        assert!(!errors.is_empty());
    }

    #[test]
    fn transition_mode_all() {
        assert_eq!(TransitionMode::ALL.len(), 6);
    }

    #[test]
    fn from_node_query() {
        let mut reg = TransitionRegistry::new();
        reg.register(TransitionDef {
            id: "t3".into(),
            name: "T3".into(),
            from_layer: TopologyLayer::Location,
            from_id: "loc1".into(),
            to_layer: TopologyLayer::Room,
            to_id: "room1".into(),
            mode: TransitionMode::FadeBlack,
            properties: HashMap::new(),
        })
        .unwrap();
        assert_eq!(reg.from_node("loc1").len(), 1);
        assert_eq!(reg.to_node("room1").len(), 1);
    }
}