feat(simulation): NPC-NPC relationship dynamics with passive decay (#103, D-024)
update_relationship_dynamics runs once per game-minute (10 ticks). Edges inactive for 1+ game-hours (600 ticks) decay trust 1 point toward 0 per minute. Creates social texture: NPCs who haven't interacted drift to neutral without active maintenance. Blocks #249 (social propagation, Sprint 15). Constants: DECAY_INTERVAL_TICKS=10, DECAY_INACTIVITY_THRESHOLD_TICKS=600, DECAY_DELTA=1. Integer arithmetic only (D-010 determinism). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,90 @@
|
||||
//! Global relationship graph resource (D-024).
|
||||
//! Global relationship graph resource (D-024) and trust progression (#324).
|
||||
//!
|
||||
//! Tracks how entities feel about each other. Separate from KnowledgeGraph
|
||||
//! (what entities know) — this is what entities feel.
|
||||
//! BTreeMap with tuple key (subject, target) for deterministic iteration
|
||||
//! and efficient prefix queries via range().
|
||||
//!
|
||||
//! Trust progression: interaction events (talk, walk-away, confrontation)
|
||||
//! adjust the per-edge `trust: i8` value via the `update_trust` system.
|
||||
//! Trust maps to D-028 TrustTier via `relationship_to_trust()` in dialogue.rs.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::knowledge::types::StableId;
|
||||
use crate::knowledge::EntityRegistry;
|
||||
use crate::simulation::time::SimulationTime;
|
||||
|
||||
use super::{RelationshipEvent, RelationshipKind};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trust event types (#324)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Trust delta for a completed Talk interaction: NPC warms to the player.
|
||||
pub const TALK_TRUST_DELTA: i8 = 1;
|
||||
|
||||
/// Trust delta when the player walks away mid-dialogue: NPC feels slighted.
|
||||
pub const WALK_AWAY_TRUST_DELTA: i8 = -1;
|
||||
|
||||
/// Trust delta when the player delivers a confrontation: NPC feels threatened.
|
||||
pub const CONFRONTATION_TRUST_DELTA: i8 = -2;
|
||||
|
||||
/// Events that modify trust on the RelationshipGraph.
|
||||
///
|
||||
/// Produced by dialogue systems, consumed by `update_trust` each tick.
|
||||
/// Direction: always (NPC → player), tracking how the NPC feels about
|
||||
/// the player after an interaction.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TrustEvent {
|
||||
/// Player completed a Talk exchange with an NPC.
|
||||
TalkCompleted {
|
||||
npc: Entity,
|
||||
player: Entity,
|
||||
},
|
||||
/// Player walked away during active dialogue (D-064).
|
||||
WalkAway {
|
||||
npc: Entity,
|
||||
player: Entity,
|
||||
},
|
||||
/// Player delivered a confrontation (D-063).
|
||||
ConfrontationDelivered {
|
||||
npc: Entity,
|
||||
player: Entity,
|
||||
},
|
||||
}
|
||||
|
||||
/// Resource: queue of pending trust events.
|
||||
/// Drained once per tick by the `update_trust` system.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct TrustEventQueue {
|
||||
events: Vec<TrustEvent>,
|
||||
}
|
||||
|
||||
impl TrustEventQueue {
|
||||
/// Push a trust event into the queue.
|
||||
pub fn push(&mut self, event: TrustEvent) {
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
/// Drain all pending events.
|
||||
pub fn drain(&mut self) -> Vec<TrustEvent> {
|
||||
std::mem::take(&mut self.events)
|
||||
}
|
||||
|
||||
/// Number of pending events.
|
||||
pub fn len(&self) -> usize {
|
||||
self.events.len()
|
||||
}
|
||||
|
||||
/// Whether the queue is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.events.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge in the relationship graph. Directed: A's feelings about B.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RelationshipEdge {
|
||||
@@ -98,6 +170,137 @@ impl RelationshipGraph {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.edges.is_empty()
|
||||
}
|
||||
|
||||
/// Iterate over all edges mutably (for decay system).
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut RelationshipEdge> {
|
||||
self.edges.values_mut()
|
||||
}
|
||||
|
||||
/// Get or create an edge between subject and target.
|
||||
///
|
||||
/// If no edge exists, inserts a default Colleague edge with trust 0.
|
||||
/// Returns a mutable reference for direct field modification.
|
||||
pub fn ensure_edge(
|
||||
&mut self,
|
||||
subject: StableId,
|
||||
target: StableId,
|
||||
tick: u64,
|
||||
) -> &mut RelationshipEdge {
|
||||
self.edges
|
||||
.entry((subject, target))
|
||||
.or_insert_with(|| RelationshipEdge {
|
||||
kind: RelationshipKind::Colleague,
|
||||
trust: 0,
|
||||
history: vec![],
|
||||
last_interaction_tick: tick,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System: update_trust (#324)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Drain pending trust events and apply deltas to the RelationshipGraph.
|
||||
///
|
||||
/// Each event adjusts the NPC→player trust edge. If no edge exists,
|
||||
/// one is created with default Colleague kind and trust 0 before applying
|
||||
/// the delta. Trust is clamped to [-10, +10] per D-010.
|
||||
///
|
||||
/// System ordering: after dialogue systems (which emit the events),
|
||||
/// before advance_tick.
|
||||
pub fn update_trust(
|
||||
mut queue: ResMut<TrustEventQueue>,
|
||||
mut graph: ResMut<RelationshipGraph>,
|
||||
registry: Res<EntityRegistry>,
|
||||
time: Res<SimulationTime>,
|
||||
) {
|
||||
for event in queue.drain() {
|
||||
let (npc, player, delta) = match event {
|
||||
TrustEvent::TalkCompleted { npc, player } => (npc, player, TALK_TRUST_DELTA),
|
||||
TrustEvent::WalkAway { npc, player } => (npc, player, WALK_AWAY_TRUST_DELTA),
|
||||
TrustEvent::ConfrontationDelivered { npc, player } => {
|
||||
(npc, player, CONFRONTATION_TRUST_DELTA)
|
||||
}
|
||||
};
|
||||
|
||||
let Some(npc_sid) = registry.to_stable(npc) else {
|
||||
tracing::warn!("Trust event for unregistered NPC {:?}", npc);
|
||||
continue;
|
||||
};
|
||||
let Some(player_sid) = registry.to_stable(player) else {
|
||||
tracing::warn!("Trust event for unregistered player {:?}", player);
|
||||
continue;
|
||||
};
|
||||
|
||||
let edge = graph.ensure_edge(npc_sid, player_sid, time.tick);
|
||||
edge.trust = edge.trust.saturating_add(delta).clamp(-10, 10);
|
||||
edge.last_interaction_tick = time.tick;
|
||||
|
||||
tracing::debug!(
|
||||
npc = npc_sid.0,
|
||||
player = player_sid.0,
|
||||
delta,
|
||||
new_trust = edge.trust,
|
||||
"Trust updated"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System: update_relationship_dynamics (#103)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Ticks between decay evaluations — 1 game-minute (D-031: 10 ticks/minute).
|
||||
const DECAY_INTERVAL_TICKS: u64 = 10;
|
||||
|
||||
/// Ticks without interaction before trust decay begins — 1 game-hour
|
||||
/// (10 ticks/minute × 60 minutes = 600 ticks).
|
||||
const DECAY_INACTIVITY_THRESHOLD_TICKS: u64 = 600;
|
||||
|
||||
/// Passive trust decay applied per decay interval.
|
||||
/// Trust drifts toward 0 at 1 point per hour of inactivity.
|
||||
const DECAY_DELTA: i8 = 1;
|
||||
|
||||
/// Apply passive trust decay to NPC-NPC relationships (#103, D-024).
|
||||
///
|
||||
/// Runs once per game-minute (every 10 ticks). For each relationship edge
|
||||
/// inactive for more than one game-hour, decays trust 1 point toward 0.
|
||||
/// Positive trust decreases; negative trust increases; zero trust is stable.
|
||||
///
|
||||
/// This creates the social texture over time: NPCs who haven't interacted
|
||||
/// recently drift back to neutral, making active relationship maintenance
|
||||
/// meaningful. Blocks #249 (player-action social propagation, Sprint 15).
|
||||
///
|
||||
/// System ordering: after update_trust, before advance_tick.
|
||||
pub fn update_relationship_dynamics(time: Res<SimulationTime>, mut graph: ResMut<RelationshipGraph>) {
|
||||
// Lightweight: evaluate once per game-minute
|
||||
if time.tick % DECAY_INTERVAL_TICKS != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for edge in graph.values_mut() {
|
||||
let ticks_since = time.tick.saturating_sub(edge.last_interaction_tick);
|
||||
if ticks_since < DECAY_INACTIVITY_THRESHOLD_TICKS {
|
||||
continue; // Recent interaction — no decay
|
||||
}
|
||||
|
||||
let old_trust = edge.trust;
|
||||
edge.trust = match edge.trust.cmp(&0) {
|
||||
std::cmp::Ordering::Greater => (edge.trust - DECAY_DELTA).max(0),
|
||||
std::cmp::Ordering::Less => (edge.trust + DECAY_DELTA).min(0),
|
||||
std::cmp::Ordering::Equal => 0,
|
||||
};
|
||||
|
||||
if edge.trust != old_trust {
|
||||
tracing::trace!(
|
||||
old_trust,
|
||||
new_trust = edge.trust,
|
||||
ticks_inactive = ticks_since,
|
||||
"NPC relationship trust decayed toward neutral"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -230,4 +433,339 @@ mod tests {
|
||||
assert_eq!(*keys[1], (StableId(2), StableId(3)));
|
||||
assert_eq!(*keys[2], (StableId(3), StableId(1)));
|
||||
}
|
||||
|
||||
// -- ensure_edge tests (#324) -------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn ensure_edge_creates_default_when_missing() {
|
||||
let mut graph = RelationshipGraph::new();
|
||||
let a = StableId(1);
|
||||
let b = StableId(2);
|
||||
|
||||
let edge = graph.ensure_edge(a, b, 100);
|
||||
assert_eq!(edge.kind, RelationshipKind::Colleague);
|
||||
assert_eq!(edge.trust, 0);
|
||||
assert_eq!(edge.last_interaction_tick, 100);
|
||||
assert_eq!(graph.edge_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_edge_returns_existing_edge() {
|
||||
let mut graph = RelationshipGraph::new();
|
||||
let a = StableId(1);
|
||||
let b = StableId(2);
|
||||
graph.set_relationship(a, b, make_edge(RelationshipKind::Friend, 7));
|
||||
|
||||
let edge = graph.ensure_edge(a, b, 200);
|
||||
// Should return existing edge, not overwrite
|
||||
assert_eq!(edge.kind, RelationshipKind::Friend);
|
||||
assert_eq!(edge.trust, 7);
|
||||
assert_eq!(graph.edge_count(), 1);
|
||||
}
|
||||
|
||||
// -- TrustEventQueue tests (#324) ----------------------------------------
|
||||
|
||||
#[test]
|
||||
fn trust_queue_push_and_drain() {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
let e2 = world.spawn_empty().id();
|
||||
|
||||
let mut queue = TrustEventQueue::default();
|
||||
assert!(queue.is_empty());
|
||||
|
||||
queue.push(TrustEvent::TalkCompleted {
|
||||
npc: e1,
|
||||
player: e2,
|
||||
});
|
||||
assert_eq!(queue.len(), 1);
|
||||
|
||||
let events = queue.drain();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(queue.is_empty());
|
||||
}
|
||||
|
||||
// -- update_trust system tests (#324) ------------------------------------
|
||||
|
||||
fn setup_trust_world() -> bevy_ecs::world::World {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.init_resource::<SimulationTime>();
|
||||
world.init_resource::<EntityRegistry>();
|
||||
world.init_resource::<RelationshipGraph>();
|
||||
world.init_resource::<TrustEventQueue>();
|
||||
world
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn talk_completed_increments_trust() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::TalkCompleted { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, TALK_TRUST_DELTA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_away_decrements_trust() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::WalkAway { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, WALK_AWAY_TRUST_DELTA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confrontation_decrements_trust_more() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::ConfrontationDelivered { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, CONFRONTATION_TRUST_DELTA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_talks_accumulate_trust() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
// Push 5 talk events
|
||||
for _ in 0..5 {
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::TalkCompleted { npc, player });
|
||||
}
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, 5); // 5 * TALK_TRUST_DELTA(1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_clamps_at_positive_ten() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
// Push 15 talk events — should clamp at 10
|
||||
for _ in 0..15 {
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::TalkCompleted { npc, player });
|
||||
}
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_clamps_at_negative_ten() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
// Push 8 confrontation events — 8 * -2 = -16, should clamp at -10
|
||||
for _ in 0..8 {
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::ConfrontationDelivered { npc, player });
|
||||
}
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, -10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_events_net_correctly() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
// 3 talks (+3) then 1 walk-away (-1) then 1 confrontation (-2) = net 0
|
||||
let mut queue = world.resource_mut::<TrustEventQueue>();
|
||||
queue.push(TrustEvent::TalkCompleted { npc, player });
|
||||
queue.push(TrustEvent::TalkCompleted { npc, player });
|
||||
queue.push(TrustEvent::TalkCompleted { npc, player });
|
||||
queue.push(TrustEvent::WalkAway { npc, player });
|
||||
queue.push(TrustEvent::ConfrontationDelivered { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.trust, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_trust_updates_last_interaction_tick() {
|
||||
let mut world = setup_trust_world();
|
||||
world.resource_mut::<SimulationTime>().tick = 42;
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::TalkCompleted { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let registry = world.resource::<EntityRegistry>();
|
||||
let npc_sid = registry.to_stable(npc).unwrap();
|
||||
let player_sid = registry.to_stable(player).unwrap();
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.last_interaction_tick, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_trust_preserves_existing_edge_kind() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
let npc_sid = world.resource_mut::<EntityRegistry>().register(npc);
|
||||
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
|
||||
|
||||
// Pre-populate with a Friend edge at trust 5
|
||||
world.resource_mut::<RelationshipGraph>().set_relationship(
|
||||
npc_sid,
|
||||
player_sid,
|
||||
make_edge(RelationshipKind::Friend, 5),
|
||||
);
|
||||
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::TalkCompleted { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
let edge = graph.get_relationship(&npc_sid, &player_sid).unwrap();
|
||||
assert_eq!(edge.kind, RelationshipKind::Friend); // Kind preserved
|
||||
assert_eq!(edge.trust, 6); // 5 + 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregistered_entity_event_is_skipped() {
|
||||
let mut world = setup_trust_world();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let player = world.spawn_empty().id();
|
||||
// Only register npc, not player
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
|
||||
world
|
||||
.resource_mut::<TrustEventQueue>()
|
||||
.push(TrustEvent::TalkCompleted { npc, player });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(update_trust);
|
||||
schedule.run(&mut world); // Should not panic
|
||||
|
||||
let graph = world.resource::<RelationshipGraph>();
|
||||
assert!(graph.is_empty(), "no edge should be created for unregistered entity");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user