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
// Zone runtime type — spatial containers for scene content, entry points,
// physics defaults, and LOD/culling configuration.

use serde::{Deserialize, Serialize};

/// A runtime zone within the 9-layer topology.
/// Zones are loaded/unloaded as the player traverses the world.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Zone {
    pub id: String,
    pub name: String,
    #[serde(default = "default_topology_layer")]
    pub topology_layer: u8,
    #[serde(default)]
    pub bounds: ZoneBounds,
    #[serde(default)]
    pub physics: ZonePhysicsDefaults,
    #[serde(default)]
    pub entry_points: Vec<EntryPoint>,
    #[serde(default)]
    pub poi_bindings: Vec<PoiBinding>,
    #[serde(default)]
    pub lod_profile: LodSuperpositionProfile,
    #[serde(default)]
    pub cull_profile: ZoneCullingProfile,
    #[serde(default)]
    pub parent_zone_id: Option<String>,
}

fn default_topology_layer() -> u8 {
    6
}

/// Spawn/transition entry point within a zone.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntryPoint {
    pub id: String,
    pub position: [f32; 3],
    #[serde(default)]
    pub facing: [f32; 3],
    #[serde(default)]
    pub kind: EntryPointKind,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum EntryPointKind {
    #[default]
    Spawn,
    Portal,
    Waypoint,
    Checkpoint,
}

/// Axis-aligned bounding region for a zone.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZoneBounds {
    #[serde(default)]
    pub min: [f32; 3],
    #[serde(default = "default_bounds_max")]
    pub max: [f32; 3],
}

fn default_bounds_max() -> [f32; 3] {
    [256.0, 128.0, 256.0]
}

impl Default for ZoneBounds {
    fn default() -> Self {
        Self {
            min: [0.0; 3],
            max: default_bounds_max(),
        }
    }
}

/// Per-zone physics overrides.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZonePhysicsDefaults {
    #[serde(default = "default_gravity")]
    pub gravity: [f32; 3],
    #[serde(default = "default_friction")]
    pub friction: f32,
    #[serde(default)]
    pub atmosphere_density: f32,
    #[serde(default = "default_step_height")]
    pub step_height: f32,
    #[serde(default = "default_slope_limit")]
    pub slope_limit_degrees: f32,
}

fn default_gravity() -> [f32; 3] {
    [0.0, -9.81, 0.0]
}
fn default_friction() -> f32 {
    0.5
}
fn default_step_height() -> f32 {
    0.35
}
fn default_slope_limit() -> f32 {
    45.0
}

impl Default for ZonePhysicsDefaults {
    fn default() -> Self {
        Self {
            gravity: default_gravity(),
            friction: default_friction(),
            atmosphere_density: 0.0,
            step_height: default_step_height(),
            slope_limit_degrees: default_slope_limit(),
        }
    }
}

/// Binds a POI definition to a zone location.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoiBinding {
    pub poi_id: String,
    pub position: [f32; 3],
    #[serde(default = "default_interaction_radius")]
    pub interaction_radius: f32,
    #[serde(default)]
    pub property_tag: Option<String>,
}

fn default_interaction_radius() -> f32 {
    2.0
}

/// Per-zone LOD distance thresholds.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LodSuperpositionProfile {
    #[serde(default = "default_lod1_distance")]
    pub lod1_distance: f32,
    #[serde(default = "default_lod2_distance")]
    pub lod2_distance: f32,
    #[serde(default = "default_cull_distance")]
    pub cull_distance: f32,
}

fn default_lod1_distance() -> f32 {
    30.0
}
fn default_lod2_distance() -> f32 {
    80.0
}
fn default_cull_distance() -> f32 {
    200.0
}

impl Default for LodSuperpositionProfile {
    fn default() -> Self {
        Self {
            lod1_distance: default_lod1_distance(),
            lod2_distance: default_lod2_distance(),
            cull_distance: default_cull_distance(),
        }
    }
}

/// Per-zone visibility culling configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZoneCullingProfile {
    /// Bitmask of visible topology layers (default: all 10 visible = 0x3FF).
    #[serde(default = "default_layer_mask")]
    pub layer_visibility_mask: u32,
    #[serde(default = "default_max_visible")]
    pub max_visible_objects: u32,
}

fn default_layer_mask() -> u32 {
    0x3FF
}
fn default_max_visible() -> u32 {
    65536
}

impl Default for ZoneCullingProfile {
    fn default() -> Self {
        Self {
            layer_visibility_mask: default_layer_mask(),
            max_visible_objects: default_max_visible(),
        }
    }
}

impl ZoneCullingProfile {
    /// Convert layer mask to per-layer visibility array.
    pub fn is_layer_visible(&self, layer: u8) -> bool {
        layer < 10 && (self.layer_visibility_mask & (1 << layer)) != 0
    }

    /// Create a mask from a list of visible layer indices.
    pub fn from_visible_layers(layers: &[u8]) -> Self {
        let mut mask = 0u32;
        for &l in layers {
            if l < 10 {
                mask |= 1 << l;
            }
        }
        Self {
            layer_visibility_mask: mask,
            max_visible_objects: default_max_visible(),
        }
    }
}

/// Scene transition state machine.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SceneTransitionState {
    pub phase: TransitionPhase,
    pub source_zone_id: Option<String>,
    pub target_zone_id: Option<String>,
    #[serde(default)]
    pub progress: f32,
}

impl Default for SceneTransitionState {
    fn default() -> Self {
        Self {
            phase: TransitionPhase::Idle,
            source_zone_id: None,
            target_zone_id: None,
            progress: 0.0,
        }
    }
}

impl SceneTransitionState {
    /// Begin a transition from source to target zone.
    pub fn begin(&mut self, source: &str, target: &str) {
        self.phase = TransitionPhase::FadeOut;
        self.source_zone_id = Some(source.to_string());
        self.target_zone_id = Some(target.to_string());
        self.progress = 0.0;
    }

    /// Advance transition by dt seconds (assumes 1.0s per phase).
    pub fn advance(&mut self, dt: f32) {
        self.progress += dt;
        if self.progress >= 1.0 {
            self.progress = 0.0;
            self.phase = match self.phase {
                TransitionPhase::Idle => TransitionPhase::Idle,
                TransitionPhase::FadeOut => TransitionPhase::Loading,
                TransitionPhase::Loading => TransitionPhase::FadeIn,
                TransitionPhase::FadeIn => TransitionPhase::Idle,
            };
        }
    }

    pub fn is_active(&self) -> bool {
        self.phase != TransitionPhase::Idle
    }
}

/// Phases of a scene transition.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TransitionPhase {
    #[default]
    Idle,
    FadeOut,
    Loading,
    FadeIn,
}

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

    #[test]
    fn zone_serde_roundtrip() {
        let zone = Zone {
            id: "zone_01".into(),
            name: "Test Zone".into(),
            topology_layer: 6,
            bounds: ZoneBounds::default(),
            physics: ZonePhysicsDefaults::default(),
            entry_points: vec![EntryPoint {
                id: "spawn_a".into(),
                position: [10.0, 0.0, 5.0],
                facing: [0.0, 0.0, 1.0],
                kind: EntryPointKind::Spawn,
            }],
            poi_bindings: vec![],
            lod_profile: LodSuperpositionProfile::default(),
            cull_profile: ZoneCullingProfile::default(),
            parent_zone_id: None,
        };
        let json = serde_json::to_string(&zone).unwrap();
        let back: Zone = serde_json::from_str(&json).unwrap();
        assert_eq!(back.id, "zone_01");
        assert_eq!(back.topology_layer, 6);
        assert_eq!(back.entry_points.len(), 1);
    }

    #[test]
    fn default_gravity() {
        let phys = ZonePhysicsDefaults::default();
        assert!((phys.gravity[1] - (-9.81)).abs() < 0.001);
        assert!((phys.step_height - 0.35).abs() < 0.001);
    }

    #[test]
    fn transition_state_cycle() {
        let mut ts = SceneTransitionState::default();
        assert!(!ts.is_active());
        ts.begin("zone_a", "zone_b");
        assert!(ts.is_active());
        assert_eq!(ts.phase, TransitionPhase::FadeOut);
        ts.advance(1.0);
        assert_eq!(ts.phase, TransitionPhase::Loading);
        ts.advance(1.0);
        assert_eq!(ts.phase, TransitionPhase::FadeIn);
        ts.advance(1.0);
        assert_eq!(ts.phase, TransitionPhase::Idle);
        assert!(!ts.is_active());
    }

    #[test]
    fn cull_profile_layer_mask() {
        let profile = ZoneCullingProfile::from_visible_layers(&[0, 3, 6, 9]);
        assert!(profile.is_layer_visible(0));
        assert!(profile.is_layer_visible(3));
        assert!(profile.is_layer_visible(6));
        assert!(profile.is_layer_visible(9));
        assert!(!profile.is_layer_visible(1));
        assert!(!profile.is_layer_visible(10));
    }
}