fix(simulation): address PR #75 review — vision components, triangle validation, FNV-1a, KG gating
1. CRITICAL: spawn_template_npcs now inserts NpcVisionState, NpcMemory, PlayerAwareness on template-spawned NPCs — matches spawn_npc() pattern from PR #66. Without these, template NPCs were invisible to vision and awareness systems. 2. WARNING: generate_intra_template_triangles now calls validate_triangle_def before generating TriangleState — mirrors cross-template path. Updates test TriangleDefs and logistics-hub.yaml to pass all three quality checks (conflict viability, relationship coherence, interest divergence). 3. WARNING: state_hash in compute_observer_snapshot now uses FNV-1a instead of DefaultHasher — consistent with D-010 principle 4 and the pattern in TemplateId/TriangleId. Updates golden file for new hash value. 4. WARNING: TriangleCrisisEventWire.role_assignments now filtered against observer KnowledgeGraph — unknown NPCs redacted from wire event per D-010 principle 2 (information boundaries). 5. WARNING: ActiveTemplateInstances::insert now despawns previous instance entities before overwriting — prevents orphaned ECS entities. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -139,7 +139,24 @@ pub fn instantiate_template(
|
||||
};
|
||||
|
||||
// Register in ActiveTemplateInstances (init if absent).
|
||||
// If a previous instance with the same ID exists, unload it first to
|
||||
// prevent orphaned ECS entities (Hoshe review #2).
|
||||
world.init_resource::<ActiveTemplateInstances>();
|
||||
let previous = world
|
||||
.resource_mut::<ActiveTemplateInstances>()
|
||||
.remove(template_id);
|
||||
if let Some(prev) = previous {
|
||||
tracing::warn!(
|
||||
"instantiate_template: overwriting live TemplateId({}) — despawning {} entities",
|
||||
template_id.0,
|
||||
prev.npc_entities.len() + prev.triangle_entities.len(),
|
||||
);
|
||||
for entity in prev.npc_entities.iter().chain(prev.triangle_entities.iter()) {
|
||||
if world.get_entity(*entity).is_ok() {
|
||||
world.despawn(*entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
world
|
||||
.resource_mut::<ActiveTemplateInstances>()
|
||||
.insert(instance.clone());
|
||||
|
||||
@@ -732,6 +732,14 @@ pub fn spawn_template_npcs(
|
||||
|
||||
let entity = generate_npc(&role_def, world, rng);
|
||||
|
||||
// Vision + awareness components (#66) — must match spawn_npc().
|
||||
// Without these, vision/awareness systems silently skip template-spawned NPCs.
|
||||
world.entity_mut(entity).insert((
|
||||
crate::npc::vision::NpcVisionState::default(),
|
||||
crate::npc::vision::NpcMemory::default(),
|
||||
crate::npc::awareness::PlayerAwareness::default(),
|
||||
));
|
||||
|
||||
// Register the entity in EntityRegistry and attach stable identity.
|
||||
let stable_id = world.resource_mut::<EntityRegistry>().register(entity);
|
||||
world.entity_mut(entity).insert((
|
||||
|
||||
@@ -682,6 +682,16 @@ pub fn generate_intra_template_triangles(
|
||||
let role_to_npc: BTreeMap<RoleId, StableId> = npc_roles.into_iter().collect();
|
||||
|
||||
for def in defs {
|
||||
// Validate triangle definition before generating TriangleState (#109).
|
||||
// Mirrors the cross-template path in generate_cross_template_triangles.
|
||||
if let Err(e) = validate_triangle_def(def) {
|
||||
result.warnings.push(format!(
|
||||
"Triangle {:?}: skipped — validation failed: {}",
|
||||
def.triangle_id, e
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut role_assignments = BTreeMap::new();
|
||||
let mut assigned_npcs = BTreeSet::new();
|
||||
let mut assignment_ok = true;
|
||||
@@ -1363,7 +1373,11 @@ mod tests {
|
||||
],
|
||||
conflict_type: ConflictType::LoyaltyConflict,
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Relationships],
|
||||
relationship_constraints: vec![],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("waitstaff"),
|
||||
kind: crate::npc::RelationshipKind::Colleague,
|
||||
required_trust: TrustRange { min: 0, max: 5 },
|
||||
}],
|
||||
},
|
||||
TriangleDef {
|
||||
triangle_id: TriangleId::from_seed_and_roles(
|
||||
@@ -1381,7 +1395,11 @@ mod tests {
|
||||
],
|
||||
conflict_type: ConflictType::ResourceCompetition,
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Tolerance, NpcAxis::Contentment],
|
||||
relationship_constraints: vec![],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("bouncer"),
|
||||
kind: crate::npc::RelationshipKind::Colleague,
|
||||
required_trust: TrustRange { min: 0, max: 3 },
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1423,8 +1441,12 @@ mod tests {
|
||||
RoleId::new("missing_role"), // no NPC has this role
|
||||
],
|
||||
conflict_type: ConflictType::SecretExposure,
|
||||
interest_axes: [NpcAxis::Secret, NpcAxis::Secret, NpcAxis::Secret],
|
||||
relationship_constraints: vec![],
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Tolerance],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("bouncer"),
|
||||
kind: crate::npc::RelationshipKind::Colleague,
|
||||
required_trust: TrustRange { min: 0, max: 5 },
|
||||
}],
|
||||
},
|
||||
TriangleDef {
|
||||
triangle_id: TriangleId(101),
|
||||
@@ -1434,8 +1456,12 @@ mod tests {
|
||||
RoleId::new("also_missing"),
|
||||
],
|
||||
conflict_type: ConflictType::LatentTension,
|
||||
interest_axes: [NpcAxis::Contentment, NpcAxis::Contentment, NpcAxis::Tolerance],
|
||||
relationship_constraints: vec![],
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Contentment, NpcAxis::Tolerance],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("bouncer"),
|
||||
kind: crate::npc::RelationshipKind::Rival,
|
||||
required_trust: TrustRange { min: -5, max: 0 },
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1475,8 +1501,12 @@ mod tests {
|
||||
RoleId::new("regular"),
|
||||
],
|
||||
conflict_type: ConflictType::ResourceCompetition,
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Want, NpcAxis::Want],
|
||||
relationship_constraints: vec![],
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Tolerance],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("bouncer"),
|
||||
kind: crate::npc::RelationshipKind::Colleague,
|
||||
required_trust: TrustRange { min: 0, max: 5 },
|
||||
}],
|
||||
}];
|
||||
|
||||
// Should succeed with 1 triangle but log an error about < 2
|
||||
@@ -1498,7 +1528,11 @@ mod tests {
|
||||
],
|
||||
conflict_type: ConflictType::LoyaltyConflict,
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Relationships],
|
||||
relationship_constraints: vec![],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("b"),
|
||||
kind: crate::npc::RelationshipKind::Colleague,
|
||||
required_trust: TrustRange { min: 0, max: 5 },
|
||||
}],
|
||||
},
|
||||
TriangleDef {
|
||||
triangle_id: TriangleId(301),
|
||||
@@ -1508,8 +1542,12 @@ mod tests {
|
||||
RoleId::new("d"),
|
||||
],
|
||||
conflict_type: ConflictType::LatentTension,
|
||||
interest_axes: [NpcAxis::Contentment, NpcAxis::Contentment, NpcAxis::Tolerance],
|
||||
relationship_constraints: vec![],
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Contentment, NpcAxis::Tolerance],
|
||||
relationship_constraints: vec![RelationshipConstraint {
|
||||
with_role: RoleId::new("c"),
|
||||
kind: crate::npc::RelationshipKind::Rival,
|
||||
required_trust: TrustRange { min: -5, max: 0 },
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user