feat(simulation): add modifications data model stub (#567, D-112)

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>
This commit is contained in:
2026-02-28 16:34:08 +01:00
co-authored by Claude Opus 4.6
parent 073c5a317c
commit 61eb40fcab
5 changed files with 182 additions and 0 deletions
+64
View File
@@ -43,6 +43,7 @@ use crate::content::template::{TemplateOwnership, TemplateReferenceMap, Triangle
use crate::knowledge::graph::KnowledgeGraph;
use crate::knowledge::registry::StableEntityId;
use crate::knowledge::types::StableId;
use crate::simulation::modification::Modification;
use crate::npc::{
CombatCapability, Contentment, DailyRoutine, InformationInventory, JobPerformance, Npc,
PersonalityTraits, Relationships, Secret, SecretSeverity, SkillSet, TellSystem,
@@ -99,6 +100,11 @@ pub struct SaveStateV1 {
/// for deterministic serialization (D-010).
#[serde(default)]
pub open_doors: Vec<StableId>,
/// Player-placed modifications to map chunks (D-111/D-112, #567).
/// DLC stub — empty in v0.1. The save slot exists so future construction
/// DLC can populate it without a save format migration.
#[serde(default)]
pub modifications: Vec<Modification>,
}
/// Per-NPC state snapshot for `SaveStateV1`.
@@ -411,6 +417,7 @@ mod tests {
template_references: TemplateReferenceMap::default(),
triangle_states: vec![],
open_doors: vec![],
modifications: vec![],
}
}
@@ -808,6 +815,7 @@ mod tests {
template_references: TemplateReferenceMap::default(),
triangle_states: vec![],
open_doors: vec![],
modifications: vec![],
};
let bytes = save.to_bytes().expect("serialize");
@@ -828,4 +836,60 @@ mod tests {
}));
assert!(result.is_err(), "must panic without StableEntityId");
}
// -----------------------------------------------------------------------
// Modifications stub round-trip (#567, D-111/D-112)
// -----------------------------------------------------------------------
#[test]
fn empty_modifications_roundtrips_in_save_state() {
// Acceptance (#567): empty modifications field survives save/load.
let state = minimal_save_state();
assert!(state.modifications.is_empty());
let bytes = state.to_bytes().expect("serialize");
let recovered = SaveStateV1::from_bytes(&bytes).expect("deserialize");
assert!(
recovered.modifications.is_empty(),
"empty modifications must roundtrip"
);
let bytes2 = recovered.to_bytes().expect("re-serialize");
assert_eq!(bytes, bytes2, "modifications roundtrip must be idempotent");
}
#[test]
fn populated_modifications_roundtrips_in_save_state() {
// Acceptance (#567): non-empty modifications field survives save/load.
use crate::simulation::modification::{ModificationType, Modification};
let mut state = minimal_save_state();
state.modifications = vec![
Modification {
position: TilePosition::new(10, 20, 0),
modification_type: ModificationType::Placeholder,
placed_at_tick: 500,
},
Modification {
position: TilePosition::new(3, 7, -1),
modification_type: ModificationType::Placeholder,
placed_at_tick: 1200,
},
];
let bytes = state.to_bytes().expect("serialize");
let recovered = SaveStateV1::from_bytes(&bytes).expect("deserialize");
assert_eq!(
recovered.modifications.len(),
2,
"two modifications must survive roundtrip"
);
assert_eq!(recovered.modifications[0].position, TilePosition::new(10, 20, 0));
assert_eq!(recovered.modifications[0].placed_at_tick, 500);
assert_eq!(recovered.modifications[1].position, TilePosition::new(3, 7, -1));
assert_eq!(recovered.modifications[1].placed_at_tick, 1200);
let bytes2 = recovered.to_bytes().expect("re-serialize");
assert_eq!(bytes, bytes2, "modifications roundtrip must be idempotent");
}
}