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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user