fix(simulation): Clippy cleanup and CI enforcement (#635)

Fix all Clippy warnings across the server codebase (2411 insertions, 1341
deletions). Raise type-complexity-threshold to 750 and too-many-arguments
to 12 in .clippy.toml for idiomatic Bevy ECS system signatures. The server
now passes `cargo clippy -- --deny warnings` cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 10:33:15 +01:00
co-authored by Claude Opus 4.6
parent 7f7706442a
commit aa79dd97e7
84 changed files with 2409 additions and 1339 deletions
+7 -4
View File
@@ -28,8 +28,8 @@ use serde::{Deserialize, Serialize};
use crate::npc::vision::NpcVisionState;
use crate::npc::{Npc, ToleranceThreshold};
use crate::simulation::time::SimulationTime;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::SimulationTime;
// ---------------------------------------------------------------------------
// Constants
@@ -117,7 +117,7 @@ pub fn detect_player_awareness(
awareness.consecutive_los_ticks = 0;
// Decay suspicion slowly when player is not visible
if time.tick % AWARENESS_DECAY_INTERVAL == 0 && awareness.suspicion_level > 0 {
if time.tick.is_multiple_of(AWARENESS_DECAY_INTERVAL) && awareness.suspicion_level > 0 {
awareness.suspicion_level =
(awareness.suspicion_level - AWARENESS_DECAY_AMOUNT).max(0);
}
@@ -134,8 +134,8 @@ mod tests {
use super::*;
use crate::npc::vision::NpcVisionState;
use crate::npc::{Npc, ToleranceThreshold};
use crate::simulation::time::SimulationTime;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::SimulationTime;
use bevy_ecs::world::World;
fn setup_world() -> World {
@@ -372,7 +372,10 @@ mod tests {
run_system(&mut world);
let awareness = world.get::<PlayerAwareness>(npc).unwrap();
assert_eq!(awareness.suspicion_level, 100, "suspicion should cap at 100");
assert_eq!(
awareness.suspicion_level, 100,
"suspicion should cap at 100"
);
}
#[test]
+68 -24
View File
@@ -16,11 +16,11 @@
use bevy_ecs::prelude::*;
use crate::npc::mood::{MoodState, NpcMood};
use crate::npc::routine::ActivityState;
use crate::npc::{
Contentment, DailyRoutine, JobPerformance, Npc, Relationships, ToleranceThreshold,
};
use crate::npc::mood::{MoodState, NpcMood};
use crate::npc::routine::ActivityState;
use crate::simulation::tier::BackgroundSim;
use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE};
@@ -110,7 +110,7 @@ pub fn background_tick(
>,
) {
// Fire once per game-minute (D-031: 10 ticks/minute)
if time.tick % TICKS_PER_GAME_MINUTE != 0 {
if !time.tick.is_multiple_of(TICKS_PER_GAME_MINUTE) {
return;
}
@@ -185,16 +185,16 @@ pub fn background_tick(
#[cfg(test)]
mod tests {
use super::*;
use crate::knowledge::types::StableId;
use crate::npc::mood::{MoodState, NpcMood};
use crate::npc::routine::ActivityState;
use crate::npc::{
Contentment, DailyRoutine, JobPerformance, Npc, Relationship, RelationshipKind,
Relationships, RoutineEntry, ToleranceThreshold,
};
use crate::npc::mood::{MoodState, NpcMood};
use crate::npc::routine::ActivityState;
use crate::simulation::movement::TilePosition;
use crate::simulation::tier::{ActiveSim, BackgroundSim};
use crate::simulation::time::{DayPhase, SimulationTime, TICKS_PER_GAME_MINUTE};
use crate::knowledge::types::StableId;
use bevy_ecs::world::World;
// --- derive_background_mood ---
@@ -311,7 +311,10 @@ mod tests {
Npc,
BackgroundSim,
MoodState::default(),
ToleranceThreshold { current_stress: 60, threshold: 50 },
ToleranceThreshold {
current_stress: 60,
threshold: 50,
},
))
.id();
@@ -333,7 +336,10 @@ mod tests {
Npc,
BackgroundSim,
MoodState::default(),
ToleranceThreshold { current_stress: 60, threshold: 50 },
ToleranceThreshold {
current_stress: 60,
threshold: 50,
},
))
.id();
@@ -353,7 +359,10 @@ mod tests {
Npc,
BackgroundSim,
MoodState::default(),
ToleranceThreshold { current_stress: 60, threshold: 50 },
ToleranceThreshold {
current_stress: 60,
threshold: 50,
},
))
.id();
@@ -379,14 +388,21 @@ mod tests {
mood: NpcMood::Warm,
changed_tick: 0,
},
ToleranceThreshold { current_stress: 60, threshold: 50 },
ToleranceThreshold {
current_stress: 60,
threshold: 50,
},
))
.id();
run_system(&mut world);
let mood = world.get::<MoodState>(npc).unwrap();
assert_eq!(mood.mood, NpcMood::Warm, "ActiveSim NPC must not be updated by background_tick");
assert_eq!(
mood.mood,
NpcMood::Warm,
"ActiveSim NPC must not be updated by background_tick"
);
}
// --- Mood state machine ---
@@ -401,7 +417,10 @@ mod tests {
Npc,
BackgroundSim,
MoodState::default(),
ToleranceThreshold { current_stress: 50, threshold: 50 },
ToleranceThreshold {
current_stress: 50,
threshold: 50,
},
))
.id();
@@ -416,9 +435,7 @@ mod tests {
world.resource_mut::<SimulationTime>().tick = 0;
// No ToleranceThreshold → defaults (0, 50) → Content (0 < 20)
let npc = world
.spawn((Npc, BackgroundSim, MoodState::default()))
.id();
let npc = world.spawn((Npc, BackgroundSim, MoodState::default())).id();
run_system(&mut world);
@@ -434,8 +451,14 @@ mod tests {
.spawn((
Npc,
BackgroundSim,
MoodState { mood: NpcMood::Warm, changed_tick: 0 },
ToleranceThreshold { current_stress: 55, threshold: 50 },
MoodState {
mood: NpcMood::Warm,
changed_tick: 0,
},
ToleranceThreshold {
current_stress: 55,
threshold: 50,
},
))
.id();
@@ -456,7 +479,10 @@ mod tests {
.spawn((
Npc,
BackgroundSim,
MoodState { mood: NpcMood::Content, changed_tick: 42 },
MoodState {
mood: NpcMood::Content,
changed_tick: 42,
},
))
.id();
@@ -464,7 +490,10 @@ mod tests {
let mood = world.get::<MoodState>(npc).unwrap();
assert_eq!(mood.mood, NpcMood::Content);
assert_eq!(mood.changed_tick, 42, "changed_tick must not update when mood unchanged");
assert_eq!(
mood.changed_tick, 42,
"changed_tick must not update when mood unchanged"
);
}
// --- Schedule state machine ---
@@ -564,7 +593,10 @@ mod tests {
run_system(&mut world);
let rels = world.get::<Relationships>(npc).unwrap();
assert_eq!(rels.entries[0].trust_level, 4, "positive trust decrements by 1");
assert_eq!(
rels.entries[0].trust_level, 4,
"positive trust decrements by 1"
);
}
#[test]
@@ -591,7 +623,10 @@ mod tests {
run_system(&mut world);
let rels = world.get::<Relationships>(npc).unwrap();
assert_eq!(rels.entries[0].trust_level, -3, "negative trust increments by 1");
assert_eq!(
rels.entries[0].trust_level, -3,
"negative trust increments by 1"
);
}
#[test]
@@ -709,7 +744,10 @@ mod tests {
Npc,
BackgroundSim,
MoodState::default(),
ToleranceThreshold { current_stress: 5, threshold: 50 },
ToleranceThreshold {
current_stress: 5,
threshold: 50,
},
))
.id();
@@ -718,13 +756,19 @@ mod tests {
Npc,
BackgroundSim,
MoodState::default(),
ToleranceThreshold { current_stress: 55, threshold: 50 },
ToleranceThreshold {
current_stress: 55,
threshold: 50,
},
))
.id();
run_system(&mut world);
assert_eq!(world.get::<MoodState>(calm).unwrap().mood, NpcMood::Content);
assert_eq!(world.get::<MoodState>(hostile).unwrap().mood, NpcMood::Hostile);
assert_eq!(
world.get::<MoodState>(hostile).unwrap().mood,
NpcMood::Hostile
);
}
}
+94 -15
View File
@@ -16,7 +16,9 @@ use crate::bridge::types::MonologueEvent;
use crate::knowledge::events::{
KnowledgeEvent, KnowledgeEventType, ProcessedFactGrant, ProcessedKnowledgeGrant,
};
use crate::knowledge::types::{FactId, KnowledgeConfidence, KnowledgeSource, KnowledgeState, RelationshipState, StableId};
use crate::knowledge::types::{
FactId, KnowledgeConfidence, KnowledgeSource, KnowledgeState, RelationshipState, StableId,
};
use crate::knowledge::{KnowledgeEventQueue, KnowledgeGraph, StableEntityId};
use crate::npc::mood::{MoodState, NpcMood};
use crate::npc::relationships::RelationshipGraph;
@@ -513,12 +515,23 @@ mod tests {
let fact_id = FactId("investigation.clue".to_string());
let kg = make_kg(vec![(
fact_id.clone(),
make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, 5, false),
make_fact(
KnowledgeConfidence::KnowsOf,
KnowledgeState::Active,
5,
false,
),
)]);
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default()))
.spawn((
Npc,
ActiveSim,
kg,
DisclosureCooldown::default(),
DisclosureCandidates::default(),
))
.id();
app.update();
@@ -537,12 +550,23 @@ mod tests {
let fact_id = FactId("secret.dangerous".to_string());
let kg = make_kg(vec![(
fact_id.clone(),
make_fact(KnowledgeConfidence::KnowsDetails, KnowledgeState::Active, 5, true),
make_fact(
KnowledgeConfidence::KnowsDetails,
KnowledgeState::Active,
5,
true,
),
)]);
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default()))
.spawn((
Npc,
ActiveSim,
kg,
DisclosureCooldown::default(),
DisclosureCandidates::default(),
))
.id();
app.update();
@@ -561,12 +585,23 @@ mod tests {
let fact_id = FactId("cargo.manifest".to_string());
let kg = make_kg(vec![(
fact_id.clone(),
make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Stale, 5, false),
make_fact(
KnowledgeConfidence::KnowsOf,
KnowledgeState::Stale,
5,
false,
),
)]);
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default()))
.spawn((
Npc,
ActiveSim,
kg,
DisclosureCooldown::default(),
DisclosureCandidates::default(),
))
.id();
app.update();
@@ -585,7 +620,12 @@ mod tests {
let fact_id = FactId("dock.schedule".to_string());
let kg = make_kg(vec![(
fact_id.clone(),
make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, 5, false),
make_fact(
KnowledgeConfidence::KnowsOf,
KnowledgeState::Active,
5,
false,
),
)]);
let mut cooldown = DisclosureCooldown::default();
@@ -593,7 +633,13 @@ mod tests {
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, cooldown, DisclosureCandidates::default()))
.spawn((
Npc,
ActiveSim,
kg,
cooldown,
DisclosureCandidates::default(),
))
.id();
app.update();
@@ -614,7 +660,12 @@ mod tests {
.map(|i| {
(
FactId(format!("fact.{:02}", i)),
make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, i as u64, false),
make_fact(
KnowledgeConfidence::KnowsOf,
KnowledgeState::Active,
i as u64,
false,
),
)
})
.collect();
@@ -622,7 +673,13 @@ mod tests {
let kg = make_kg(facts);
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default()))
.spawn((
Npc,
ActiveSim,
kg,
DisclosureCooldown::default(),
DisclosureCandidates::default(),
))
.id();
app.update();
@@ -642,7 +699,12 @@ mod tests {
let fact_id = FactId("investigation.clue".to_string());
let kg = make_kg(vec![(
fact_id.clone(),
make_fact(KnowledgeConfidence::KnowsOf, KnowledgeState::Active, 5, false),
make_fact(
KnowledgeConfidence::KnowsOf,
KnowledgeState::Active,
5,
false,
),
)]);
// Set computed_tick = 1 (non-zero). Tick 5 is within the 30-tick refresh window.
@@ -651,7 +713,13 @@ mod tests {
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), candidates))
.spawn((
Npc,
ActiveSim,
kg,
DisclosureCooldown::default(),
candidates,
))
.id();
// Advance tick to 5 (within CANDIDATE_REFRESH_TICKS = 30).
@@ -677,12 +745,23 @@ mod tests {
let fact_id = FactId("rumour.vague".to_string());
let kg = make_kg(vec![(
fact_id.clone(),
make_fact(KnowledgeConfidence::Suspects, KnowledgeState::Active, 5, false),
make_fact(
KnowledgeConfidence::Suspects,
KnowledgeState::Active,
5,
false,
),
)]);
let npc = app
.world_mut()
.spawn((Npc, ActiveSim, kg, DisclosureCooldown::default(), DisclosureCandidates::default()))
.spawn((
Npc,
ActiveSim,
kg,
DisclosureCooldown::default(),
DisclosureCandidates::default(),
))
.id();
app.update();
+55 -14
View File
@@ -29,17 +29,17 @@ use std::collections::BTreeMap;
use bevy_ecs::prelude::*;
use crate::knowledge::graph::KnowledgeGraph;
use crate::knowledge::types::{FactId, KnowledgeConfidence, StableId};
use crate::npc::awareness::PlayerAwareness;
use crate::npc::mood::MoodState;
use crate::npc::vision::{NpcMemory, NpcVisionState};
use crate::npc::{
CombatCapability, CombatStyle, Contentment, DailyRoutine, InformationInventory, JobPerformance,
KnownFact, Npc, PersonalityTrait, PersonalityTraits, Relationship, RelationshipKind,
Relationships, RoutineEntry, Secret, SecretSeverity, Skill, SkillSet, Tell, TellSystem,
TellTrigger, ToleranceThreshold, Want, WantKind, MAX_KEY_RELATIONSHIPS,
};
use crate::knowledge::graph::KnowledgeGraph;
use crate::npc::mood::MoodState;
use crate::npc::awareness::PlayerAwareness;
use crate::npc::vision::{NpcMemory, NpcVisionState};
use crate::simulation::movement::TilePosition;
use crate::simulation::rng::SimRng;
use crate::simulation::tier::ActiveSim;
@@ -116,15 +116,30 @@ fn pick_secret_severity(idx: usize) -> SecretSeverity {
fn pick_relationship_kind(idx: usize) -> RelationshipKind {
use RelationshipKind::*;
const VARIANTS: [RelationshipKind; 7] =
[Colleague, Friend, Rival, Romantic, Family, Superior, Subordinate];
const VARIANTS: [RelationshipKind; 7] = [
Colleague,
Friend,
Rival,
Romantic,
Family,
Superior,
Subordinate,
];
VARIANTS[idx % VARIANTS.len()]
}
fn pick_personality_trait(idx: usize) -> PersonalityTrait {
use PersonalityTrait::*;
const VARIANTS: [PersonalityTrait; 10] = [
Cautious, Bold, Honest, Deceptive, Compassionate, Ruthless, Curious, Incurious, Social,
Cautious,
Bold,
Honest,
Deceptive,
Compassionate,
Ruthless,
Curious,
Incurious,
Social,
Reclusive,
];
VARIANTS[idx % VARIANTS.len()]
@@ -132,8 +147,16 @@ fn pick_personality_trait(idx: usize) -> PersonalityTrait {
fn pick_skill(idx: usize) -> Skill {
use Skill::*;
const VARIANTS: [Skill; 8] =
[Combat, Intimidation, Medical, Observation, Persuasion, Piloting, Stealth, Technical];
const VARIANTS: [Skill; 8] = [
Combat,
Intimidation,
Medical,
Observation,
Persuasion,
Piloting,
Stealth,
Technical,
];
VARIANTS[idx % VARIANTS.len()]
}
@@ -257,7 +280,14 @@ fn gen_routine(rng: &mut SimRng, location_pool: &[(DayPhase, TilePosition)]) ->
.iter()
.map(|&i| {
let (phase, location) = available[i];
let activity_names = ["Work", "Patrol", "Rest", "Meeting", "Training", "Maintenance"];
let activity_names = [
"Work",
"Patrol",
"Rest",
"Meeting",
"Training",
"Maintenance",
];
let act_idx = rng.rng.random_range(0..activity_names.len());
RoutineEntry {
phase,
@@ -391,7 +421,9 @@ fn gen_skills(rng: &mut SimRng, role: &RoleDefinition) -> (SkillSet, Option<Comb
extra_attempts += 1;
let idx = rng.rng.random_range(0..8_usize);
let skill = pick_skill(idx);
skills.entry(skill).or_insert_with(|| rng.rng.random_range(2_u8..=5));
skills
.entry(skill)
.or_insert_with(|| rng.rng.random_range(2_u8..=5));
}
let combat_trained = role.combat_enabled && rng.rng.random_range(0..3_u32) < 2;
@@ -560,7 +592,10 @@ mod tests {
let entity = generate_npc(&role, &mut world, &mut rng);
assert!(world.get_entity(entity).is_ok(), "spawned entity must exist");
assert!(
world.get_entity(entity).is_ok(),
"spawned entity must exist"
);
}
#[test]
@@ -602,7 +637,10 @@ mod tests {
world.get::<TellSystem>(entity).is_some(),
"must have TellSystem"
);
assert!(world.get::<SkillSet>(entity).is_some(), "must have SkillSet");
assert!(
world.get::<SkillSet>(entity).is_some(),
"must have SkillSet"
);
assert!(
world.get::<MoodState>(entity).is_some(),
"must have MoodState"
@@ -629,7 +667,10 @@ mod tests {
let want_a = world_a.get::<Want>(ea).unwrap();
let want_b = world_b.get::<Want>(eb).unwrap();
assert_eq!(want_a.primary, want_b.primary, "Want.primary must match");
assert_eq!(want_a.intensity, want_b.intensity, "Want.intensity must match");
assert_eq!(
want_a.intensity, want_b.intensity,
"Want.intensity must match"
);
let tol_a = world_a.get::<ToleranceThreshold>(ea).unwrap();
let tol_b = world_b.get::<ToleranceThreshold>(eb).unwrap();
+4 -1
View File
@@ -82,7 +82,10 @@ impl InteractionMemory {
/// Count notable events of a given kind.
pub fn count_events(&self, kind: InteractionEventKind) -> usize {
self.notable_events.iter().filter(|e| e.kind == kind).count()
self.notable_events
.iter()
.filter(|e| e.kind == kind)
.count()
}
}
-1
View File
@@ -126,7 +126,6 @@ pub struct NpcVoiceProfile {
pub culture_id: String,
}
/// NPC animation tier (D-047).
///
/// Tier 1 (clear): public daily activities — instantly readable.
+10 -16
View File
@@ -18,10 +18,10 @@
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
use crate::simulation::line_pool::Mood as ContentMood;
use crate::npc::interaction::InteractionMemory;
use crate::npc::{Npc, ToleranceThreshold};
use crate::simulation::dialogue::CurrentMood;
use crate::simulation::line_pool::Mood as ContentMood;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::{DayPhase, SimulationTime};
@@ -293,10 +293,7 @@ mod tests {
#[test]
fn mood_warm_when_positive_interaction() {
assert_eq!(
derive_mood(0, 50, DayPhase::Morning, true),
NpcMood::Warm
);
assert_eq!(derive_mood(0, 50, DayPhase::Morning, true), NpcMood::Warm);
}
#[test]
@@ -381,10 +378,7 @@ mod tests {
#[test]
fn mood_priority_warm_over_frustrated() {
// Warm takes priority over Frustrated (checked before Evening test)
assert_eq!(
derive_mood(25, 50, DayPhase::Evening, true),
NpcMood::Warm
);
assert_eq!(derive_mood(25, 50, DayPhase::Evening, true), NpcMood::Warm);
}
#[test]
@@ -489,12 +483,7 @@ mod tests {
// No ToleranceThreshold → defaults (stress=0, threshold=50) → Content
let npc = world
.spawn((
Npc,
ActiveSim,
MoodState::default(),
CurrentMood::default(),
))
.spawn((Npc, ActiveSim, MoodState::default(), CurrentMood::default()))
.id();
let mut schedule = bevy_ecs::schedule::Schedule::default();
@@ -669,7 +658,12 @@ mod tests {
// This test documents the invariant: derive_mood never emits these states.
use std::collections::HashSet;
let phases = [DayPhase::Morning, DayPhase::Afternoon, DayPhase::Evening, DayPhase::Night];
let phases = [
DayPhase::Morning,
DayPhase::Afternoon,
DayPhase::Evening,
DayPhase::Night,
];
let stresses: &[i16] = &[-50, -1, 0, 1, 19, 20, 29, 30, 49, 50, 51, 100];
let thresholds: &[i16] = &[0, 1, 50, 100];
let warm_flags = [false, true];
+86 -57
View File
@@ -40,20 +40,11 @@ pub const CONFRONTATION_TRUST_DELTA: i8 = -2;
#[derive(Debug, Clone)]
pub enum TrustEvent {
/// Player completed a Talk exchange with an NPC.
TalkCompleted {
npc: Entity,
player: Entity,
},
TalkCompleted { npc: Entity, player: Entity },
/// Player walked away during active dialogue (D-064).
WalkAway {
npc: Entity,
player: Entity,
},
WalkAway { npc: Entity, player: Entity },
/// Player delivered a confrontation (D-063).
ConfrontationDelivered {
npc: Entity,
player: Entity,
},
ConfrontationDelivered { npc: Entity, player: Entity },
}
/// Resource: queue of pending trust events.
@@ -300,7 +291,11 @@ fn scale_delta(delta: i8, factor_tenths: i8) -> i8 {
// Multiply by factor, round up (ceiling of absolute value)
let scaled_abs = (delta.unsigned_abs() as i16 * factor_tenths as i16 + 9) / 10;
let scaled = scaled_abs.min(10) as i8;
if delta < 0 { -(scaled as i8) } else { scaled as i8 }
if delta < 0 {
-scaled
} else {
scaled
}
}
/// Drain pending trust events and apply deltas to the RelationshipGraph.
@@ -489,9 +484,12 @@ const DECAY_DELTA: i8 = 1;
/// 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>) {
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 {
if !time.tick.is_multiple_of(DECAY_INTERVAL_TICKS) {
return;
}
@@ -1065,11 +1063,13 @@ mod tests {
);
// Queue propagation from A
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: 2,
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: 2,
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
@@ -1077,7 +1077,9 @@ mod tests {
// B should now have a trust edge toward the player (positive)
let graph = world.resource::<RelationshipGraph>();
let edge = graph.get_relationship(&npc_b, &player).expect("B should have edge to player");
let edge = graph
.get_relationship(&npc_b, &player)
.expect("B should have edge to player");
assert!(
edge.trust > 0,
"Second-order NPC should trust player more after positive first-order event"
@@ -1104,11 +1106,13 @@ mod tests {
make_edge(RelationshipKind::Colleague, 2),
);
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
@@ -1141,11 +1145,13 @@ mod tests {
graph.set_relationship(npc_a, npc_b, make_edge(RelationshipKind::Friend, 5));
graph.set_relationship(npc_b, npc_c, make_edge(RelationshipKind::Friend, 5));
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
@@ -1181,11 +1187,13 @@ mod tests {
graph.set_relationship(npc_b, npc_c, make_edge(RelationshipKind::Friend, 5));
// First run: queue the third-order change
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
@@ -1197,9 +1205,13 @@ mod tests {
// C should now have an edge with positive trust
let graph = world.resource::<RelationshipGraph>();
let edge = graph.get_relationship(&npc_c, &player)
let edge = graph
.get_relationship(&npc_c, &player)
.expect("Delayed change should have been applied by now");
assert!(edge.trust > 0, "Third-order trust should be positive after delayed application");
assert!(
edge.trust > 0,
"Third-order trust should be positive after delayed application"
);
}
#[test]
@@ -1227,11 +1239,13 @@ mod tests {
make_edge(RelationshipKind::Friend, 5),
);
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: 2,
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: 2,
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
@@ -1269,11 +1283,13 @@ mod tests {
make_edge(RelationshipKind::Friend, 5),
);
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: -2, // confrontation
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: -2, // confrontation
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
@@ -1281,7 +1297,8 @@ mod tests {
// B should trust player LESS after A was confronted
let graph = world.resource::<RelationshipGraph>();
let edge = graph.get_relationship(&npc_b, &player)
let edge = graph
.get_relationship(&npc_b, &player)
.expect("B should have edge to player");
assert!(
edge.trust < 0,
@@ -1298,21 +1315,30 @@ mod tests {
let npc_a = StableId(1);
let player = StableId(99);
world.resource_mut::<PropagationQueue>().push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
world
.resource_mut::<PropagationQueue>()
.push(PropagationEvent {
npc: npc_a,
player,
delta: 5,
});
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(propagate_social_actions);
schedule.run(&mut world);
let graph = world.resource::<RelationshipGraph>();
assert!(graph.is_empty(), "No relationships → no propagation edges created");
assert!(
graph.is_empty(),
"No relationships → no propagation edges created"
);
let delay_queue = world.resource::<DelayedTrustQueue>();
assert_eq!(delay_queue.pending_count(), 0, "No delay queue entries either");
assert_eq!(
delay_queue.pending_count(),
0,
"No delay queue entries either"
);
}
#[test]
@@ -1357,6 +1383,9 @@ mod tests {
schedule.run(&mut world); // Should not panic
let graph = world.resource::<RelationshipGraph>();
assert!(graph.is_empty(), "no edge should be created for unregistered entity");
assert!(
graph.is_empty(),
"no edge should be created for unregistered entity"
);
}
}
+11 -11
View File
@@ -132,12 +132,7 @@ pub fn enter_activity(
mut commands: Commands,
time: Res<SimulationTime>,
npcs: Query<
(
Entity,
&TilePosition,
&DailyRoutine,
Option<&ActivityState>,
),
(Entity, &TilePosition, &DailyRoutine, Option<&ActivityState>),
(
With<Npc>,
With<ActiveSim>,
@@ -287,8 +282,7 @@ pub fn detect_routine_deviation(
let phase = time.day_phase();
let tick = time.tick;
for (entity, pos, routine, activity_opt, path_req, computed_path, deviating_opt) in
npcs.iter()
for (entity, pos, routine, activity_opt, path_req, computed_path, deviating_opt) in npcs.iter()
{
let Some(entry) = routine.entry_for_phase(phase) else {
// No routine entry for this phase — nothing to deviate from.
@@ -388,7 +382,7 @@ mod tests {
.spawn((
Npc,
crate::simulation::tier::ActiveSim, // system requires With<ActiveSim> (#94)
TilePosition::new(5, 5, 0), // Not at afternoon location
TilePosition::new(5, 5, 0), // Not at afternoon location
DailyRoutine {
entries: vec![RoutineEntry {
phase: DayPhase::Afternoon,
@@ -570,7 +564,10 @@ mod tests {
let state = world.get::<ActivityState>(entity).unwrap();
assert_eq!(state.activity, "Work");
assert_eq!(state.phase, DayPhase::Afternoon);
assert_eq!(state.started_tick, MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE);
assert_eq!(
state.started_tick,
MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE
);
}
#[test]
@@ -933,7 +930,10 @@ mod tests {
run_deviation_system(&mut world);
let queue = world.resource::<RoutineDeviationEventQueue>();
assert!(queue.is_empty(), "on-schedule NPC should not emit a deviation event");
assert!(
queue.is_empty(),
"on-schedule NPC should not emit a deviation event"
);
}
#[test]
+23 -12
View File
@@ -154,8 +154,16 @@ pub fn derive_tell_state(
(With<Npc>, With<ActiveSim>),
>,
) {
for (secret, tolerance, contentment, mood_state, relationships_opt, deviation_opt, kg_opt, mut tell) in
npcs.iter_mut()
for (
secret,
tolerance,
contentment,
mood_state,
relationships_opt,
deviation_opt,
kg_opt,
mut tell,
) in npcs.iter_mut()
{
tell.category = derive_category(
secret,
@@ -177,10 +185,10 @@ pub fn derive_tell_state(
mod tests {
use super::*;
use crate::knowledge::types::StableId;
use crate::npc::{Relationship, RelationshipKind, RoutineDeviation as NpcRoutineDeviation};
use crate::npc::{SecretSeverity, ToleranceThreshold};
use crate::npc::mood::NpcMood;
use crate::npc::DeviationTrigger;
use crate::npc::{Relationship, RelationshipKind, RoutineDeviation as NpcRoutineDeviation};
use crate::npc::{SecretSeverity, ToleranceThreshold};
fn neutral_secret() -> Secret {
Secret {
@@ -218,7 +226,10 @@ mod tests {
}
fn mood(m: NpcMood) -> MoodState {
MoodState { mood: m, changed_tick: 0 }
MoodState {
mood: m,
changed_tick: 0,
}
}
fn positive_relationships() -> Relationships {
@@ -539,7 +550,7 @@ mod tests {
let result = derive_category(
&neutral_secret(),
&tolerance(0, 50),
&contentment(-5), // Not low enough for angry
&contentment(-5), // Not low enough for angry
&mood(NpcMood::Anxious), // Not Hostile
None,
None,
@@ -554,8 +565,8 @@ mod tests {
#[test]
fn system_updates_derived_tell_state() {
use bevy_ecs::world::World;
use crate::npc::Npc;
use bevy_ecs::world::World;
let mut world = World::new();
@@ -583,8 +594,8 @@ mod tests {
#[test]
fn system_sets_none_for_neutral_npc() {
use bevy_ecs::world::World;
use crate::npc::Npc;
use bevy_ecs::world::World;
let mut world = World::new();
@@ -721,8 +732,8 @@ mod tests {
// 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
&tolerance(80, 100), // stress*2=160 > 100 → Nervous
&contentment(-50), // < -20 → Angry condition met
&mood(NpcMood::Hostile), // Angry condition met
None,
None,
@@ -762,8 +773,8 @@ mod tests {
// 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
&tolerance(0, 100), // Low stress — not Nervous
&contentment(50), // > +20 → Friendly condition met
&mood(NpcMood::Neutral),
Some(&positive_relationships()), // Friendly condition met
None,
+9 -4
View File
@@ -239,7 +239,10 @@ mod tests {
run_system(&mut world);
let queue = world.resource::<ToleranceBreachEventQueue>();
assert!(queue.is_empty(), "stress=49 < threshold=50 should not breach");
assert!(
queue.is_empty(),
"stress=49 < threshold=50 should not breach"
);
}
#[test]
@@ -402,8 +405,7 @@ mod tests {
// Should not crash — query requires ToleranceThreshold, so entity is simply skipped
let mut world = setup_world();
world.spawn((
Npc,
ActiveSim,
Npc, ActiveSim,
// No ToleranceThreshold component
));
@@ -529,7 +531,10 @@ mod tests {
run_system(&mut world);
let queue = world.resource::<ToleranceBreachEventQueue>();
assert!(queue.is_empty(), "stress=99 < threshold=100 should not breach");
assert!(
queue.is_empty(),
"stress=99 < threshold=100 should not breach"
);
}
#[test]
+39 -15
View File
@@ -110,10 +110,8 @@ impl Stage1Filter {
}
// Exclude ToldBy-source facts (Cautious behavior)
if self.exclude_told_by {
if matches!(fact.source, KnowledgeSource::ToldBy { .. }) {
return false;
}
if self.exclude_told_by && matches!(fact.source, KnowledgeSource::ToldBy { .. }) {
return false;
}
true
@@ -192,9 +190,11 @@ impl TraitModifierConfig {
/// Delegates to `KnowledgeConfidence::try_from` (which accepts both
/// camelCase and underscore forms) rather than duplicating the match.
fn parse_confidence(s: &str) -> Option<KnowledgeConfidence> {
KnowledgeConfidence::try_from(s).map_err(|e| {
tracing::warn!("Unknown confidence level in trait config: {}", e);
}).ok()
KnowledgeConfidence::try_from(s)
.map_err(|e| {
tracing::warn!("Unknown confidence level in trait config: {}", e);
})
.ok()
}
/// Convert a `PersonalityTrait` to its lowercase YAML key.
@@ -286,8 +286,14 @@ modifiers:
KnowledgeSource::DirectObservation { tick: 50 },
);
assert!(!cautious.allows_fact(&suspects_fact), "Cautious excludes Suspects");
assert!(cautious.allows_fact(&details_fact), "Cautious allows KnowsDetails");
assert!(
!cautious.allows_fact(&suspects_fact),
"Cautious excludes Suspects"
);
assert!(
cautious.allows_fact(&details_fact),
"Cautious allows KnowsDetails"
);
}
#[test]
@@ -302,7 +308,10 @@ modifiers:
tick: 50,
},
);
assert!(!cautious.allows_fact(&told_fact), "Cautious excludes ToldBy");
assert!(
!cautious.allows_fact(&told_fact),
"Cautious excludes ToldBy"
);
}
#[test]
@@ -314,7 +323,10 @@ modifiers:
KnowledgeConfidence::Suspects,
KnowledgeSource::DirectObservation { tick: 50 },
);
assert!(gossipy.allows_fact(&suspects_fact), "Gossipy includes Suspects");
assert!(
gossipy.allows_fact(&suspects_fact),
"Gossipy includes Suspects"
);
}
#[test]
@@ -417,10 +429,22 @@ modifiers:
#[test]
fn parse_confidence_values() {
assert_eq!(parse_confidence("suspects"), Some(KnowledgeConfidence::Suspects));
assert_eq!(parse_confidence("knows_of"), Some(KnowledgeConfidence::KnowsOf));
assert_eq!(parse_confidence("knows_details"), Some(KnowledgeConfidence::KnowsDetails));
assert_eq!(parse_confidence("direct"), Some(KnowledgeConfidence::Direct));
assert_eq!(
parse_confidence("suspects"),
Some(KnowledgeConfidence::Suspects)
);
assert_eq!(
parse_confidence("knows_of"),
Some(KnowledgeConfidence::KnowsOf)
);
assert_eq!(
parse_confidence("knows_details"),
Some(KnowledgeConfidence::KnowsDetails)
);
assert_eq!(
parse_confidence("direct"),
Some(KnowledgeConfidence::Direct)
);
assert_eq!(parse_confidence("invalid"), None);
}
+15 -7
View File
@@ -30,8 +30,8 @@ use crate::perception::shadowcast::compute_fov;
use crate::perception::vision_cone::{apply_vision_cone, Facing, VisionConeConfig};
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
use crate::simulation::spatial::{NaiveSpatialIndex, SpatialIndex};
use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE};
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE};
/// NPC vision range in tiles (matches player forward range from VisionConeConfig).
pub const NPC_VISION_RANGE: i32 = 20;
@@ -128,8 +128,7 @@ pub fn compute_npc_vision(
// Collect visible tile positions — apply vision cone if NPC has facing
let visible_positions: BTreeSet<(i32, i32)> = if let Some(facing_comp) = facing {
let cone_tiles =
apply_vision_cone(&fov, npc_pos.x, npc_pos.y, facing_comp.0, &config);
let cone_tiles = apply_vision_cone(&fov, npc_pos.x, npc_pos.y, facing_comp.0, &config);
cone_tiles.into_iter().map(|(x, y, _)| (x, y)).collect()
} else {
// No facing → omnidirectional vision (full FOV)
@@ -261,7 +260,7 @@ pub fn degrade_npc_inferences(
time: Res<SimulationTime>,
mut npc_query: Query<&mut NpcMemory, (With<Npc>, With<ActiveSim>)>,
) {
if time.tick % TICKS_PER_GAME_MINUTE != 0 {
if !time.tick.is_multiple_of(TICKS_PER_GAME_MINUTE) {
return;
}
@@ -595,7 +594,10 @@ mod tests {
schedule.run(&mut world);
let queue = world.resource::<KnowledgeEventQueue>();
assert!(!queue.is_empty(), "should emit DirectObservation for player");
assert!(
!queue.is_empty(),
"should emit DirectObservation for player"
);
let event = &queue.events[0];
assert_eq!(event.observer, npc);
@@ -653,7 +655,10 @@ mod tests {
KnowledgeEventType::LeftLOS { target } if target == player
)
});
assert!(has_left_los, "should emit LeftLOS when player leaves NPC LOS");
assert!(
has_left_los,
"should emit LeftLOS when player leaves NPC LOS"
);
// Check zone inference was created
let npc_memory = world.get::<NpcMemory>(npc).unwrap();
@@ -705,7 +710,10 @@ mod tests {
let entry = memory.last_known.get(&player_sid).unwrap();
assert_eq!(entry.position, pos(10, 10));
assert_eq!(entry.observed_tick, 100);
assert!(entry.zone_inference.is_none(), "active observation = no inference");
assert!(
entry.zone_inference.is_none(),
"active observation = no inference"
);
}
// --- degrade_npc_inferences tests ---