use serde::{Deserialize, Serialize};
#[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
}
#[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,
}
#[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(),
}
}
}
#[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(),
}
}
}
#[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
}
#[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(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZoneCullingProfile {
#[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 {
pub fn is_layer_visible(&self, layer: u8) -> bool {
layer < 10 && (self.layer_visibility_mask & (1 << layer)) != 0
}
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(),
}
}
}
#[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 {
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;
}
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
}
}
#[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));
}
}