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:
@@ -55,13 +55,15 @@ use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use rand::Rng;
|
||||
|
||||
use crate::simulation::triangle::{TriangleClassification, TriangleId, TrianglePhase, TriangleState};
|
||||
use crate::knowledge::EntityRegistry;
|
||||
use crate::npc::Npc;
|
||||
use crate::simulation::movement::{PlayerCharacter, TilePosition};
|
||||
use crate::simulation::rng::SimRng;
|
||||
use crate::simulation::tier::ActiveSim;
|
||||
use crate::simulation::time::{SimulationTime, TICKS_PER_GAME_MINUTE};
|
||||
use crate::simulation::triangle::{
|
||||
TriangleClassification, TriangleId, TrianglePhase, TriangleState,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
@@ -151,7 +153,7 @@ pub struct ContaminationActive(pub bool);
|
||||
/// Tracks how many triangles have been activated this session. For v0.1 this
|
||||
/// is a simple boolean gate — once `activated_count >= MAX_CONCURRENT_ACTIVATIONS`,
|
||||
/// the activation pass no-ops. Persisted in `SaveStateV1`.
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
pub struct ActivationState {
|
||||
/// Number of triangles activated this session.
|
||||
pub activated_count: u32,
|
||||
@@ -159,15 +161,6 @@ pub struct ActivationState {
|
||||
pub last_activation_tick: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for ActivationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
activated_count: 0,
|
||||
last_activation_tick: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActivationState {
|
||||
/// Whether the activation pass should attempt to activate a new triangle.
|
||||
///
|
||||
@@ -177,15 +170,9 @@ impl ActivationState {
|
||||
if self.activated_count >= MAX_CONCURRENT_ACTIVATIONS {
|
||||
return false;
|
||||
}
|
||||
// v0.1: cooldown is 0, so no cooldown check needed.
|
||||
// Future: check (current_tick - last_activation_tick) >= ACTIVATION_COOLDOWN_TICKS
|
||||
if ACTIVATION_COOLDOWN_TICKS > 0 {
|
||||
if let Some(last) = self.last_activation_tick {
|
||||
if _current_tick.saturating_sub(last) < ACTIVATION_COOLDOWN_TICKS {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// v0.1: ACTIVATION_COOLDOWN_TICKS = 0, so no cooldown check needed.
|
||||
// Future (v0.3+): when ACTIVATION_COOLDOWN_TICKS > 0, gate on
|
||||
// (_current_tick - last_activation_tick) >= ACTIVATION_COOLDOWN_TICKS.
|
||||
true
|
||||
}
|
||||
|
||||
@@ -375,8 +362,7 @@ impl Plugin for StorytellerPlugin {
|
||||
.add_systems(Update, tick_contamination_activation)
|
||||
.add_systems(
|
||||
Update,
|
||||
append_player_history
|
||||
.after(crate::simulation::movement::validate_movement),
|
||||
append_player_history.after(crate::simulation::movement::validate_movement),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -393,8 +379,7 @@ impl Plugin for StorytellerPlugin {
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
expire_routine_deviations
|
||||
.before(crate::npc::tell_state::derive_tell_state),
|
||||
expire_routine_deviations.before(crate::npc::tell_state::derive_tell_state),
|
||||
);
|
||||
|
||||
tracing::debug!("StorytellerPlugin initialized");
|
||||
@@ -458,11 +443,12 @@ pub fn tick_contamination_activation(
|
||||
/// Formula: capped conversations × weight + observation ticks × weight + monologue triggers × weight.
|
||||
/// Implements #162 step 3 scoring.
|
||||
fn compute_engagement_score(record: &EngagementRecord) -> f32 {
|
||||
let conv_score = record.conversation_count.min(CONVERSATION_CAP) as f32
|
||||
* ENGAGEMENT_WEIGHT_CONVERSATION;
|
||||
let conv_score =
|
||||
record.conversation_count.min(CONVERSATION_CAP) as f32 * ENGAGEMENT_WEIGHT_CONVERSATION;
|
||||
// Use f64 intermediate to avoid precision loss beyond ~16.8M ticks (2^24),
|
||||
// where f32 can no longer distinguish adjacent integers.
|
||||
let obs_score = (record.observation_time_ticks as f64 * ENGAGEMENT_WEIGHT_OBSERVATION as f64) as f32;
|
||||
let obs_score =
|
||||
(record.observation_time_ticks as f64 * ENGAGEMENT_WEIGHT_OBSERVATION as f64) as f32;
|
||||
let mono_score = record.monologue_trigger_count as f32 * ENGAGEMENT_WEIGHT_MONOLOGUE;
|
||||
conv_score + obs_score + mono_score
|
||||
}
|
||||
@@ -475,9 +461,8 @@ fn compute_engagement_score(record: &EngagementRecord) -> f32 {
|
||||
/// 1. Gate: contamination active + activation limit not reached + cadence check.
|
||||
/// 2. Co-presence query: find NPCs near any recorded player position (MovementHistoryBuffer).
|
||||
/// 3. Engagement scoring: score each co-present NPC via EngagementRecord.
|
||||
/// 4+5. Triangle routing: find Simmering triangle containing highest-scoring co-present NPC.
|
||||
/// v0.1: NPCs not assigned to any triangle are excluded (D-025 reference link routing deferred to v0.3+).
|
||||
/// Holds (no activation) if no triangle-assigned NPC is co-present.
|
||||
/// 4. Find Simmering triangle containing highest-scoring co-present NPC.
|
||||
/// 5. Routing note: NPCs not in any triangle excluded (D-025, deferred to v0.3+). Holds if none.
|
||||
/// 6. Activation event: emit TriangleActivatedEvent, record in ActivationState.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn activation_pass(
|
||||
@@ -502,14 +487,15 @@ pub fn activation_pass(
|
||||
// Gate 3: poll cadence — only run every ACTIVATION_CADENCE_TICKS.
|
||||
// Guard: at tick 0 all engagement scores are 0.0 and selection is random
|
||||
// (e.g. contamination pre-set in a save file). Skip tick 0.
|
||||
if time.tick == 0 || time.tick % ACTIVATION_CADENCE_TICKS != 0 {
|
||||
if time.tick == 0 || !time.tick.is_multiple_of(ACTIVATION_CADENCE_TICKS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: co-presence query
|
||||
let npc_positions: Vec<(Entity, TilePosition)> =
|
||||
npcs.iter().map(|(e, pos, _)| (e, *pos)).collect();
|
||||
let mut copresent = history.npcs_copresent_in_window(npc_positions.into_iter(), COPRESENCE_THRESHOLD);
|
||||
let mut copresent =
|
||||
history.npcs_copresent_in_window(npc_positions.into_iter(), COPRESENCE_THRESHOLD);
|
||||
// Deduplicate: the co-presence query can return the same entity more than once
|
||||
// if multiple player positions fall near the same NPC. Without dedup, the NPC
|
||||
// gets added to candidates twice, doubling its RNG weight.
|
||||
@@ -679,7 +665,9 @@ pub fn expire_routine_deviations(
|
||||
) {
|
||||
for (entity, deviation) in deviations.iter() {
|
||||
if deviation.expires_at_tick > 0 && time.tick >= deviation.expires_at_tick {
|
||||
commands.entity(entity).remove::<crate::npc::RoutineDeviation>();
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<crate::npc::RoutineDeviation>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,10 +679,10 @@ pub fn expire_routine_deviations(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge::types::StableId;
|
||||
use crate::simulation::triangle::{
|
||||
RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState,
|
||||
};
|
||||
use crate::knowledge::types::StableId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn make_triangle(classification: TriangleClassification) -> TriangleState {
|
||||
@@ -922,19 +910,19 @@ mod tests {
|
||||
let mut world = World::new();
|
||||
let entity_old = world.spawn_empty().id();
|
||||
let entity_new = world.spawn_empty().id();
|
||||
let copresent = buf.npcs_copresent_in_window(
|
||||
[(entity_old, TilePosition::new(0, 0, 0))].into_iter(),
|
||||
0,
|
||||
);
|
||||
let copresent =
|
||||
buf.npcs_copresent_in_window([(entity_old, TilePosition::new(0, 0, 0))].into_iter(), 0);
|
||||
assert!(
|
||||
copresent.is_empty(),
|
||||
"NPC at evicted position x=0 should not be copresent"
|
||||
);
|
||||
let copresent_new = buf.npcs_copresent_in_window(
|
||||
[(entity_new, TilePosition::new(9999, 0, 0))].into_iter(),
|
||||
0,
|
||||
let copresent_new = buf
|
||||
.npcs_copresent_in_window([(entity_new, TilePosition::new(9999, 0, 0))].into_iter(), 0);
|
||||
assert_eq!(
|
||||
copresent_new.len(),
|
||||
1,
|
||||
"NPC at newest position should be copresent"
|
||||
);
|
||||
assert_eq!(copresent_new.len(), 1, "NPC at newest position should be copresent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -958,7 +946,10 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(result.contains(&nearby), "nearby NPC should be copresent");
|
||||
assert!(!result.contains(&distant), "distant NPC should not be copresent");
|
||||
assert!(
|
||||
!result.contains(&distant),
|
||||
"distant NPC should not be copresent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -970,10 +961,8 @@ mod tests {
|
||||
let npc = world.spawn_empty().id();
|
||||
|
||||
// Same x/y but different z
|
||||
let result = buf.npcs_copresent_in_window(
|
||||
[(npc, TilePosition::new(10, 10, 1))].into_iter(),
|
||||
0,
|
||||
);
|
||||
let result =
|
||||
buf.npcs_copresent_in_window([(npc, TilePosition::new(10, 10, 1))].into_iter(), 0);
|
||||
assert!(
|
||||
result.is_empty(),
|
||||
"NPC on different z-level must not be copresent"
|
||||
@@ -1085,7 +1074,9 @@ mod tests {
|
||||
world.resource_mut::<SimulationTime>().tick = tick;
|
||||
|
||||
// Register NPC entity in EntityRegistry to get a StableId
|
||||
let npc_entity = world.spawn((crate::npc::Npc, ActiveSim, TilePosition::new(5, 5, 0))).id();
|
||||
let npc_entity = world
|
||||
.spawn((crate::npc::Npc, ActiveSim, TilePosition::new(5, 5, 0)))
|
||||
.id();
|
||||
let stable_id = world.resource_mut::<EntityRegistry>().register(npc_entity);
|
||||
|
||||
// Spawn a Simmering triangle with the NPC in a role
|
||||
@@ -1094,14 +1085,20 @@ mod tests {
|
||||
let tri_entity = world.spawn((tri, ActiveSim)).id();
|
||||
|
||||
// Put the player nearby in the history buffer
|
||||
world.resource_mut::<MovementHistoryBuffer>().append(TilePosition::new(5, 5, 0));
|
||||
world
|
||||
.resource_mut::<MovementHistoryBuffer>()
|
||||
.append(TilePosition::new(5, 5, 0));
|
||||
|
||||
schedule.run(&mut world);
|
||||
|
||||
let events = world.resource_mut::<TriangleActivatedQueue>().drain();
|
||||
assert_eq!(events.len(), 1);
|
||||
let tri_state = world.get::<TriangleState>(tri_entity).unwrap();
|
||||
assert_eq!(tri_state.phase, TrianglePhase::Active, "triangle containing copresent NPC should be Active");
|
||||
assert_eq!(
|
||||
tri_state.phase,
|
||||
TrianglePhase::Active,
|
||||
"triangle containing copresent NPC should be Active"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1111,7 +1108,9 @@ mod tests {
|
||||
let (mut world, mut schedule) = setup_activation_world();
|
||||
|
||||
// Register an NPC and assign it to a triangle role so candidates are non-empty
|
||||
let npc = world.spawn((crate::npc::Npc, ActiveSim, TilePosition::new(1, 1, 0))).id();
|
||||
let npc = world
|
||||
.spawn((crate::npc::Npc, ActiveSim, TilePosition::new(1, 1, 0)))
|
||||
.id();
|
||||
let stable_id = world.resource_mut::<EntityRegistry>().register(npc);
|
||||
|
||||
let mut tri = make_triangle(TriangleClassification::ActiveFork);
|
||||
@@ -1119,15 +1118,22 @@ mod tests {
|
||||
world.spawn((tri, ActiveSim));
|
||||
|
||||
// Put the player nearby so the NPC is copresent
|
||||
world.resource_mut::<MovementHistoryBuffer>().append(TilePosition::new(1, 1, 0));
|
||||
world
|
||||
.resource_mut::<MovementHistoryBuffer>()
|
||||
.append(TilePosition::new(1, 1, 0));
|
||||
|
||||
// First run — should activate
|
||||
world.resource_mut::<SimulationTime>().tick = ACTIVATION_CADENCE_TICKS;
|
||||
schedule.run(&mut world);
|
||||
let first_events = world.resource_mut::<TriangleActivatedQueue>().drain();
|
||||
assert_eq!(first_events.len(), 1, "first pass must activate the triangle");
|
||||
assert_eq!(
|
||||
world.resource::<ActivationState>().activated_count, 1,
|
||||
first_events.len(),
|
||||
1,
|
||||
"first pass must activate the triangle"
|
||||
);
|
||||
assert_eq!(
|
||||
world.resource::<ActivationState>().activated_count,
|
||||
1,
|
||||
"ActivationState must record the first activation"
|
||||
);
|
||||
|
||||
@@ -1209,12 +1215,14 @@ mod tests {
|
||||
));
|
||||
|
||||
// Populate the activation queue
|
||||
world.resource_mut::<TriangleActivatedQueue>().push(TriangleActivatedEvent {
|
||||
triangle_id,
|
||||
tick: 100,
|
||||
anchor_entity: npc_a,
|
||||
anchor_score: 15.0,
|
||||
});
|
||||
world
|
||||
.resource_mut::<TriangleActivatedQueue>()
|
||||
.push(TriangleActivatedEvent {
|
||||
triangle_id,
|
||||
tick: 100,
|
||||
anchor_entity: npc_a,
|
||||
anchor_score: 15.0,
|
||||
});
|
||||
|
||||
// Run system
|
||||
let mut schedule = Schedule::default();
|
||||
@@ -1222,11 +1230,23 @@ mod tests {
|
||||
schedule.run(&mut world);
|
||||
|
||||
// Both NPCs should have RoutineDeviation
|
||||
assert!(world.get::<RoutineDeviation>(npc_a).is_some(), "NPC A should have RoutineDeviation");
|
||||
assert!(world.get::<RoutineDeviation>(npc_b).is_some(), "NPC B should have RoutineDeviation");
|
||||
assert!(
|
||||
world.get::<RoutineDeviation>(npc_a).is_some(),
|
||||
"NPC A should have RoutineDeviation"
|
||||
);
|
||||
assert!(
|
||||
world.get::<RoutineDeviation>(npc_b).is_some(),
|
||||
"NPC B should have RoutineDeviation"
|
||||
);
|
||||
let dev = world.get::<RoutineDeviation>(npc_a).unwrap();
|
||||
assert_eq!(dev.trigger, crate::npc::DeviationTrigger::TriangleEscalation);
|
||||
assert_eq!(dev.expires_at_tick, crate::npc::TELL_ESCALATION_DURATION_TICKS);
|
||||
assert_eq!(
|
||||
dev.trigger,
|
||||
crate::npc::DeviationTrigger::TriangleEscalation
|
||||
);
|
||||
assert_eq!(
|
||||
dev.expires_at_tick,
|
||||
crate::npc::TELL_ESCALATION_DURATION_TICKS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1239,11 +1259,13 @@ mod tests {
|
||||
world.insert_resource(time);
|
||||
|
||||
// Spawn entity with expired deviation
|
||||
let entity = world.spawn(RoutineDeviation {
|
||||
trigger: DeviationTrigger::TriangleEscalation,
|
||||
tick: 100,
|
||||
expires_at_tick: 400, // expired at tick 500
|
||||
}).id();
|
||||
let entity = world
|
||||
.spawn(RoutineDeviation {
|
||||
trigger: DeviationTrigger::TriangleEscalation,
|
||||
tick: 100,
|
||||
expires_at_tick: 400, // expired at tick 500
|
||||
})
|
||||
.id();
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(expire_routine_deviations);
|
||||
@@ -1265,11 +1287,13 @@ mod tests {
|
||||
world.insert_resource(time);
|
||||
|
||||
// Deviation that expires at tick 500 — should survive at tick 200
|
||||
let entity = world.spawn(RoutineDeviation {
|
||||
trigger: DeviationTrigger::TriangleEscalation,
|
||||
tick: 100,
|
||||
expires_at_tick: 500,
|
||||
}).id();
|
||||
let entity = world
|
||||
.spawn(RoutineDeviation {
|
||||
trigger: DeviationTrigger::TriangleEscalation,
|
||||
tick: 100,
|
||||
expires_at_tick: 500,
|
||||
})
|
||||
.id();
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(expire_routine_deviations);
|
||||
|
||||
Reference in New Issue
Block a user