tell_state reads KG for other-entity relationship state (MVP boundary per D-082). Contradiction monologue fires with pre-resolved entity names, shifts ToldBy source to PersonOfInterest. Full THE FRIEND arc event chain. Implements D-082 step 1, D-083 event chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::knowledge::graph::KnowledgeGraph;
|
||||
use crate::knowledge::types::RelationshipState;
|
||||
use crate::npc::mood::{MoodState, NpcMood};
|
||||
use crate::npc::{
|
||||
Contentment, Npc, Relationships, RoutineDeviation, Secret, SecretSeverity, ToleranceThreshold,
|
||||
@@ -77,6 +79,7 @@ fn derive_category(
|
||||
mood_state: &MoodState,
|
||||
relationships_opt: Option<&Relationships>,
|
||||
deviation_opt: Option<&RoutineDeviation>,
|
||||
kg_opt: Option<&KnowledgeGraph>,
|
||||
) -> Option<TellCategory> {
|
||||
// Priority 1: RoutineDeviation (primary detective mechanic, D-027 criterion 4)
|
||||
if deviation_opt.is_some() {
|
||||
@@ -102,10 +105,20 @@ fn derive_category(
|
||||
}
|
||||
|
||||
// Priority 5: Friendly — high contentment with at least one trusted relationship
|
||||
// D-082: prefer KG relationship state over ground-truth axis data.
|
||||
// Self-axis components (secret, stress, contentment, mood) remain ground-truth;
|
||||
// OTHER-entity relationship assessment uses the knowledge graph.
|
||||
if contentment.level > 20 {
|
||||
let has_positive_relationship = relationships_opt
|
||||
.map(|rels| rels.entries.iter().any(|r| r.trust_level > 3))
|
||||
.unwrap_or(false);
|
||||
let has_positive_relationship = if let Some(kg) = kg_opt {
|
||||
// D-082: use knowledge graph for other-entity relationship assessment
|
||||
kg.known_entities_iter()
|
||||
.any(|(_, ek)| ek.relationship == RelationshipState::Friendly)
|
||||
} else {
|
||||
// No KG: fall through to ground-truth axis data
|
||||
relationships_opt
|
||||
.map(|rels| rels.entries.iter().any(|r| r.trust_level > 3))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
if has_positive_relationship {
|
||||
return Some(TellCategory::Friendly);
|
||||
}
|
||||
@@ -135,12 +148,13 @@ pub fn derive_tell_state(
|
||||
&MoodState,
|
||||
Option<&Relationships>,
|
||||
Option<&RoutineDeviation>,
|
||||
Option<&KnowledgeGraph>,
|
||||
&mut DerivedTellState,
|
||||
),
|
||||
(With<Npc>, With<ActiveSim>),
|
||||
>,
|
||||
) {
|
||||
for (secret, tolerance, contentment, mood_state, relationships_opt, deviation_opt, mut tell) in
|
||||
for (secret, tolerance, contentment, mood_state, relationships_opt, deviation_opt, kg_opt, mut tell) in
|
||||
npcs.iter_mut()
|
||||
{
|
||||
tell.category = derive_category(
|
||||
@@ -150,6 +164,7 @@ pub fn derive_tell_state(
|
||||
mood_state,
|
||||
relationships_opt,
|
||||
deviation_opt,
|
||||
kg_opt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -248,6 +263,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
Some(&deviation()),
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::RoutineDeviation));
|
||||
}
|
||||
@@ -261,6 +277,7 @@ mod tests {
|
||||
&mood(NpcMood::Hostile),
|
||||
None,
|
||||
Some(&deviation()),
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::RoutineDeviation));
|
||||
}
|
||||
@@ -274,6 +291,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None, // No deviation
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::RoutineDeviation));
|
||||
}
|
||||
@@ -292,6 +310,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Nervous));
|
||||
}
|
||||
@@ -306,6 +325,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Guarded));
|
||||
}
|
||||
@@ -319,6 +339,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Nervous));
|
||||
}
|
||||
@@ -333,6 +354,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
// Still Guarded (Major secret, priority 4)
|
||||
assert_eq!(result, Some(TellCategory::Guarded));
|
||||
@@ -351,6 +373,7 @@ mod tests {
|
||||
&mood(NpcMood::Hostile),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Angry));
|
||||
}
|
||||
@@ -364,6 +387,7 @@ mod tests {
|
||||
&mood(NpcMood::Hostile),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Angry));
|
||||
}
|
||||
@@ -377,6 +401,7 @@ mod tests {
|
||||
&mood(NpcMood::Anxious), // Not Hostile
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Angry));
|
||||
}
|
||||
@@ -391,6 +416,7 @@ mod tests {
|
||||
&mood(NpcMood::Hostile),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Angry));
|
||||
}
|
||||
@@ -408,6 +434,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Guarded));
|
||||
}
|
||||
@@ -421,6 +448,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
// Moderate secret doesn't trigger Guarded
|
||||
assert_ne!(result, Some(TellCategory::Guarded));
|
||||
@@ -439,6 +467,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
Some(&positive_relationships()), // Trust > 3
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
@@ -452,6 +481,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
Some(&neutral_relationships()), // Trust = 0
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
@@ -465,6 +495,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None, // No relationships at all
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
@@ -479,6 +510,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
Some(&positive_relationships()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
@@ -496,6 +528,7 @@ mod tests {
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
@@ -509,6 +542,7 @@ mod tests {
|
||||
&mood(NpcMood::Anxious), // Not Hostile
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
@@ -572,4 +606,172 @@ mod tests {
|
||||
let state = world.get::<DerivedTellState>(entity).unwrap();
|
||||
assert_eq!(state.category, None);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// D-082: KG-aware Friendly tell
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
fn kg_with_friendly_entity() -> KnowledgeGraph {
|
||||
use crate::simulation::movement::TilePosition;
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(StableId(1), TilePosition::new(5, 5, 0), 10);
|
||||
kg.set_relationship(&StableId(1), RelationshipState::Friendly);
|
||||
kg
|
||||
}
|
||||
|
||||
fn kg_with_known_entity() -> KnowledgeGraph {
|
||||
use crate::simulation::movement::TilePosition;
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(StableId(1), TilePosition::new(5, 5, 0), 10);
|
||||
kg.set_relationship(&StableId(1), RelationshipState::Known);
|
||||
kg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kg_friendly_relationship_triggers_friendly_tell() {
|
||||
let kg = kg_with_friendly_entity();
|
||||
let result = derive_category(
|
||||
&neutral_secret(),
|
||||
&tolerance(0, 50),
|
||||
&contentment(30),
|
||||
&mood(NpcMood::Neutral),
|
||||
None, // Relationships component doesn't matter when KG exists
|
||||
None,
|
||||
Some(&kg),
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kg_known_relationship_does_not_trigger_friendly_tell() {
|
||||
// Known != Friendly — only Friendly relationship triggers the tell
|
||||
let kg = kg_with_known_entity();
|
||||
let result = derive_category(
|
||||
&neutral_secret(),
|
||||
&tolerance(0, 50),
|
||||
&contentment(30),
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
Some(&kg),
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kg_empty_does_not_trigger_friendly_tell() {
|
||||
let kg = KnowledgeGraph::new();
|
||||
let result = derive_category(
|
||||
&neutral_secret(),
|
||||
&tolerance(0, 50),
|
||||
&contentment(30),
|
||||
&mood(NpcMood::Neutral),
|
||||
None,
|
||||
None,
|
||||
Some(&kg),
|
||||
);
|
||||
assert_ne!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_kg_falls_through_to_relationships_component() {
|
||||
// Without KG, the old behavior (Relationships component) should work
|
||||
let result = derive_category(
|
||||
&neutral_secret(),
|
||||
&tolerance(0, 50),
|
||||
&contentment(30),
|
||||
&mood(NpcMood::Neutral),
|
||||
Some(&positive_relationships()),
|
||||
None,
|
||||
None, // No KG
|
||||
);
|
||||
assert_eq!(result, Some(TellCategory::Friendly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kg_overrides_relationships_component() {
|
||||
// KG says no Friendly relationships, even though Relationships
|
||||
// component has trust > 3 — KG wins (D-082).
|
||||
let kg = kg_with_known_entity(); // Known, not Friendly
|
||||
let result = derive_category(
|
||||
&neutral_secret(),
|
||||
&tolerance(0, 50),
|
||||
&contentment(30),
|
||||
&mood(NpcMood::Neutral),
|
||||
Some(&positive_relationships()), // ground-truth says Friendly
|
||||
None,
|
||||
Some(&kg), // KG says Known (not Friendly)
|
||||
);
|
||||
assert_ne!(
|
||||
result,
|
||||
Some(TellCategory::Friendly),
|
||||
"KG should override Relationships component for Friendly tell"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Priority chain: verify ordering at boundaries (QA gap closure)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn nervous_beats_angry_when_both_conditions_met() {
|
||||
// NPC has: Major secret, stress past midpoint (Nervous), AND
|
||||
// low contentment + Hostile mood (Angry).
|
||||
// Priority 2 (Nervous) must win over Priority 3 (Angry).
|
||||
let result = derive_category(
|
||||
&major_secret(),
|
||||
&tolerance(80, 100), // stress*2=160 > 100 → Nervous
|
||||
&contentment(-50), // < -20 → Angry condition met
|
||||
&mood(NpcMood::Hostile), // Angry condition met
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(TellCategory::Nervous),
|
||||
"Nervous (priority 2) must beat Angry (priority 3)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angry_beats_guarded_when_both_conditions_met() {
|
||||
// NPC has: Major secret (Guarded), AND low contentment + Hostile (Angry).
|
||||
// Priority 3 (Angry) must win over Priority 4 (Guarded).
|
||||
// Note: stress is LOW so Nervous does not trigger.
|
||||
let result = derive_category(
|
||||
&major_secret(),
|
||||
&tolerance(10, 100), // Low stress — not Nervous
|
||||
&contentment(-30), // < -20 → Angry
|
||||
&mood(NpcMood::Hostile),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(TellCategory::Angry),
|
||||
"Angry (priority 3) must beat Guarded (priority 4)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guarded_beats_friendly_when_both_conditions_met() {
|
||||
// NPC has: Major secret (Guarded), AND high contentment with positive
|
||||
// relationship (Friendly). Priority 4 (Guarded) must win over Priority 5.
|
||||
let result = derive_category(
|
||||
&major_secret(),
|
||||
&tolerance(0, 100), // Low stress — not Nervous
|
||||
&contentment(50), // > +20 → Friendly condition met
|
||||
&mood(NpcMood::Neutral),
|
||||
Some(&positive_relationships()), // Friendly condition met
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(TellCategory::Guarded),
|
||||
"Guarded (priority 4) must beat Friendly (priority 5)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user