DLC entry point for future player construction system. Adds Modification struct, ModificationType enum, and Modifications component. Wired into SaveStateV1 with #[serde(default)] for forward-compatible save format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
115 lines
4.3 KiB
Rust
115 lines
4.3 KiB
Rust
//! Modification data model stub (D-111/D-112, #567).
|
|
//!
|
|
//! Entry point for the future player construction system (DLC scope).
|
|
//! Tracks player-placed modifications to static and mobile chunks.
|
|
//! No construction logic ships in v0.1 — this is a data model stub only.
|
|
//!
|
|
//! The `Modifications` component can be attached to any entity that represents
|
|
//! a spatial chunk (static or mobile). Currently unused at runtime; the
|
|
//! `SaveStateV1` field ensures the data model round-trips through save/load
|
|
//! so future DLC can populate it without a save format migration.
|
|
|
|
use bevy_ecs::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::simulation::movement::TilePosition;
|
|
|
|
/// A single player-placed modification to a chunk.
|
|
///
|
|
/// Records what was placed, where, and when. The `modification_type` enum
|
|
/// will be extended by the construction DLC — the stub contains only a
|
|
/// `Placeholder` variant to keep the enum non-empty.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Modification {
|
|
/// Tile position of this modification within the chunk.
|
|
pub position: TilePosition,
|
|
/// What kind of modification was placed.
|
|
pub modification_type: ModificationType,
|
|
/// Simulation tick when this modification was placed.
|
|
pub placed_at_tick: u64,
|
|
}
|
|
|
|
/// Type of modification placed by the player.
|
|
///
|
|
/// Stub enum — will be extended by the construction DLC with variants
|
|
/// like `Wall`, `Floor`, `Furniture`, `Terminal`, etc.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ModificationType {
|
|
/// Placeholder variant. Prevents the enum from being uninhabited
|
|
/// and allows save format round-tripping before real variants ship.
|
|
Placeholder,
|
|
}
|
|
|
|
/// Component: player-placed modifications on a chunk entity.
|
|
///
|
|
/// Attach to any entity that represents a modifiable spatial region
|
|
/// (static chunk, mobile chunk, etc.). Initially empty for all entities.
|
|
#[derive(Component, Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct Modifications {
|
|
pub entries: Vec<Modification>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn modification_type_placeholder_serializes() {
|
|
let mod_type = ModificationType::Placeholder;
|
|
let bytes = rmp_serde::to_vec_named(&mod_type).expect("serialize");
|
|
let recovered: ModificationType = rmp_serde::from_slice(&bytes).expect("deserialize");
|
|
assert_eq!(mod_type, recovered);
|
|
}
|
|
|
|
#[test]
|
|
fn modification_roundtrips_via_messagepack() {
|
|
let modification = Modification {
|
|
position: TilePosition::new(5, 10, 0),
|
|
modification_type: ModificationType::Placeholder,
|
|
placed_at_tick: 42,
|
|
};
|
|
|
|
let bytes = rmp_serde::to_vec_named(&modification).expect("serialize");
|
|
let recovered: Modification = rmp_serde::from_slice(&bytes).expect("deserialize");
|
|
assert_eq!(modification, recovered);
|
|
}
|
|
|
|
#[test]
|
|
fn modifications_component_defaults_to_empty() {
|
|
let mods = Modifications::default();
|
|
assert!(mods.entries.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn modifications_with_entries_roundtrips() {
|
|
let mods = Modifications {
|
|
entries: vec![
|
|
Modification {
|
|
position: TilePosition::new(1, 2, 0),
|
|
modification_type: ModificationType::Placeholder,
|
|
placed_at_tick: 100,
|
|
},
|
|
Modification {
|
|
position: TilePosition::new(3, 4, 1),
|
|
modification_type: ModificationType::Placeholder,
|
|
placed_at_tick: 200,
|
|
},
|
|
],
|
|
};
|
|
|
|
let bytes = rmp_serde::to_vec_named(&mods).expect("serialize");
|
|
let recovered: Modifications = rmp_serde::from_slice(&bytes).expect("deserialize");
|
|
assert_eq!(mods.entries.len(), recovered.entries.len());
|
|
assert_eq!(mods.entries[0], recovered.entries[0]);
|
|
assert_eq!(mods.entries[1], recovered.entries[1]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_modifications_roundtrips() {
|
|
let mods = Modifications::default();
|
|
let bytes = rmp_serde::to_vec_named(&mods).expect("serialize");
|
|
let recovered: Modifications = rmp_serde::from_slice(&bytes).expect("deserialize");
|
|
assert!(recovered.entries.is_empty());
|
|
}
|
|
}
|