refactor(server): apply rustfmt and clippy suggestions
Formatting pass across simulation, perception, knowledge, NPC, and test modules. Includes two clippy fixes in monologue.rs (.values() instead of for (_, v) pattern). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,11 @@ pub fn receive_bridge_inputs(
|
|||||||
match bridge.receive_inputs() {
|
match bridge.receive_inputs() {
|
||||||
Ok(inputs) => {
|
Ok(inputs) => {
|
||||||
for input in &inputs {
|
for input in &inputs {
|
||||||
tracing::debug!("Received input: tick={} action={:?}", input.tick, input.action);
|
tracing::debug!(
|
||||||
|
"Received input: tick={} action={:?}",
|
||||||
|
input.tick,
|
||||||
|
input.action
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for input in inputs {
|
for input in inputs {
|
||||||
input_queue.push(input);
|
input_queue.push(input);
|
||||||
@@ -153,8 +157,7 @@ impl Plugin for BridgePlugin {
|
|||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
receive_bridge_inputs
|
receive_bridge_inputs.before(crate::simulation::input::process_player_input),
|
||||||
.before(crate::simulation::input::process_player_input),
|
|
||||||
crate::perception::observer::compute_visibility_geometry
|
crate::perception::observer::compute_visibility_geometry
|
||||||
.after(crate::simulation::movement::validate_movement),
|
.after(crate::simulation::movement::validate_movement),
|
||||||
crate::simulation::interaction::compute_nearby_interactions
|
crate::simulation::interaction::compute_nearby_interactions
|
||||||
|
|||||||
+56
-25
@@ -184,11 +184,11 @@ fn spawn_npc(world: &mut World, profile: &types::NpcProfile, result: &mut SpawnR
|
|||||||
|
|
||||||
// Register in EntityRegistry for StableId mapping
|
// Register in EntityRegistry for StableId mapping
|
||||||
let stable_id = world.resource_mut::<EntityRegistry>().register(entity);
|
let stable_id = world.resource_mut::<EntityRegistry>().register(entity);
|
||||||
world
|
world.entity_mut(entity).insert(StableEntityId(stable_id));
|
||||||
.entity_mut(entity)
|
|
||||||
.insert(StableEntityId(stable_id));
|
|
||||||
|
|
||||||
result.npc_ids.insert(profile.canonical_id.clone(), stable_id);
|
result
|
||||||
|
.npc_ids
|
||||||
|
.insert(profile.canonical_id.clone(), stable_id);
|
||||||
result.npcs_spawned += 1;
|
result.npcs_spawned += 1;
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -418,10 +418,7 @@ fn resolve_routines(
|
|||||||
|
|
||||||
for schedule in &routine_file.schedules {
|
for schedule in &routine_file.schedules {
|
||||||
let Some(&stable_id) = npc_ids.get(&schedule.npc) else {
|
let Some(&stable_id) = npc_ids.get(&schedule.npc) else {
|
||||||
tracing::debug!(
|
tracing::debug!("Skipping routine for {}: not in npc_ids map", schedule.npc);
|
||||||
"Skipping routine for {}: not in npc_ids map",
|
|
||||||
schedule.npc
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(entity) = world.resource::<EntityRegistry>().to_entity(&stable_id) else {
|
let Some(entity) = world.resource::<EntityRegistry>().to_entity(&stable_id) else {
|
||||||
@@ -523,7 +520,10 @@ fn parse_relationship_kind(s: &str) -> npc::RelationshipKind {
|
|||||||
"superior" => npc::RelationshipKind::Superior,
|
"superior" => npc::RelationshipKind::Superior,
|
||||||
"subordinate" => npc::RelationshipKind::Subordinate,
|
"subordinate" => npc::RelationshipKind::Subordinate,
|
||||||
other => {
|
other => {
|
||||||
tracing::warn!("Unknown relationship kind '{}', defaulting to Colleague", other);
|
tracing::warn!(
|
||||||
|
"Unknown relationship kind '{}', defaulting to Colleague",
|
||||||
|
other
|
||||||
|
);
|
||||||
npc::RelationshipKind::Colleague
|
npc::RelationshipKind::Colleague
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,10 +594,7 @@ fn parse_secret_severity(description: &str) -> npc::SecretSeverity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Minor: social embarrassment, mild secrets, ambiguous situations
|
// Minor: social embarrassment, mild secrets, ambiguous situations
|
||||||
if lower.contains("ambiguous")
|
if lower.contains("ambiguous") || lower.contains("embarrassment") || lower.contains("gossip") {
|
||||||
|| lower.contains("embarrassment")
|
|
||||||
|| lower.contains("gossip")
|
|
||||||
{
|
|
||||||
return npc::SecretSeverity::Minor;
|
return npc::SecretSeverity::Minor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,7 +712,10 @@ mod tests {
|
|||||||
|
|
||||||
let tells = world.get::<npc::TellSystem>(entity).unwrap();
|
let tells = world.get::<npc::TellSystem>(entity).unwrap();
|
||||||
assert_eq!(tells.tells.len(), 1);
|
assert_eq!(tells.tells.len(), 1);
|
||||||
assert_eq!(tells.tells[0].trigger, npc::TellTrigger::StressAboveThreshold);
|
assert_eq!(
|
||||||
|
tells.tells[0].trigger,
|
||||||
|
npc::TellTrigger::StressAboveThreshold
|
||||||
|
);
|
||||||
|
|
||||||
let skills = world.get::<npc::SkillSet>(entity).unwrap();
|
let skills = world.get::<npc::SkillSet>(entity).unwrap();
|
||||||
assert_eq!(skills.skills.len(), 2);
|
assert_eq!(skills.skills.len(), 2);
|
||||||
@@ -771,7 +771,10 @@ mod tests {
|
|||||||
assert_eq!(parse_want_kind("Wealth"), Some(npc::WantKind::Wealth));
|
assert_eq!(parse_want_kind("Wealth"), Some(npc::WantKind::Wealth));
|
||||||
assert_eq!(parse_want_kind("safety"), Some(npc::WantKind::Safety));
|
assert_eq!(parse_want_kind("safety"), Some(npc::WantKind::Safety));
|
||||||
assert_eq!(parse_want_kind("KNOWLEDGE"), Some(npc::WantKind::Knowledge));
|
assert_eq!(parse_want_kind("KNOWLEDGE"), Some(npc::WantKind::Knowledge));
|
||||||
assert_eq!(parse_want_kind("Connection"), Some(npc::WantKind::Connection));
|
assert_eq!(
|
||||||
|
parse_want_kind("Connection"),
|
||||||
|
Some(npc::WantKind::Connection)
|
||||||
|
);
|
||||||
assert_eq!(parse_want_kind("Power"), Some(npc::WantKind::Power));
|
assert_eq!(parse_want_kind("Power"), Some(npc::WantKind::Power));
|
||||||
assert_eq!(parse_want_kind("Freedom"), Some(npc::WantKind::Freedom));
|
assert_eq!(parse_want_kind("Freedom"), Some(npc::WantKind::Freedom));
|
||||||
assert_eq!(parse_want_kind("Justice"), Some(npc::WantKind::Justice));
|
assert_eq!(parse_want_kind("Justice"), Some(npc::WantKind::Justice));
|
||||||
@@ -851,7 +854,9 @@ mod tests {
|
|||||||
.to_entity(&stable_id)
|
.to_entity(&stable_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let kg = world.get::<KnowledgeGraph>(entity).expect("KnowledgeGraph should be attached");
|
let kg = world
|
||||||
|
.get::<KnowledgeGraph>(entity)
|
||||||
|
.expect("KnowledgeGraph should be attached");
|
||||||
assert!(kg.knows_fact(&FactId("contraband.ring_exists".to_string())));
|
assert!(kg.knows_fact(&FactId("contraband.ring_exists".to_string())));
|
||||||
assert!(kg.knows_fact(&FactId("relationship.kael_trust".to_string())));
|
assert!(kg.knows_fact(&FactId("relationship.kael_trust".to_string())));
|
||||||
assert!(kg.fact_at_least(
|
assert!(kg.fact_at_least(
|
||||||
@@ -886,7 +891,9 @@ mod tests {
|
|||||||
.to_entity(&stable_id)
|
.to_entity(&stable_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let kg = world.get::<KnowledgeGraph>(entity).expect("Phase 2 should attach KnowledgeGraph");
|
let kg = world
|
||||||
|
.get::<KnowledgeGraph>(entity)
|
||||||
|
.expect("Phase 2 should attach KnowledgeGraph");
|
||||||
assert!(kg.knows_fact(&FactId("investigation.inspection_lapses".to_string())));
|
assert!(kg.knows_fact(&FactId("investigation.inspection_lapses".to_string())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,15 +946,39 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_relationship_kinds() {
|
fn parse_relationship_kinds() {
|
||||||
assert_eq!(parse_relationship_kind("friend"), npc::RelationshipKind::Friend);
|
assert_eq!(
|
||||||
assert_eq!(parse_relationship_kind("colleague"), npc::RelationshipKind::Colleague);
|
parse_relationship_kind("friend"),
|
||||||
assert_eq!(parse_relationship_kind("family"), npc::RelationshipKind::Family);
|
npc::RelationshipKind::Friend
|
||||||
assert_eq!(parse_relationship_kind("romantic"), npc::RelationshipKind::Romantic);
|
);
|
||||||
assert_eq!(parse_relationship_kind("superior"), npc::RelationshipKind::Superior);
|
assert_eq!(
|
||||||
assert_eq!(parse_relationship_kind("subordinate"), npc::RelationshipKind::Subordinate);
|
parse_relationship_kind("colleague"),
|
||||||
assert_eq!(parse_relationship_kind("rival"), npc::RelationshipKind::Rival);
|
npc::RelationshipKind::Colleague
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_relationship_kind("family"),
|
||||||
|
npc::RelationshipKind::Family
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_relationship_kind("romantic"),
|
||||||
|
npc::RelationshipKind::Romantic
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_relationship_kind("superior"),
|
||||||
|
npc::RelationshipKind::Superior
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_relationship_kind("subordinate"),
|
||||||
|
npc::RelationshipKind::Subordinate
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_relationship_kind("rival"),
|
||||||
|
npc::RelationshipKind::Rival
|
||||||
|
);
|
||||||
// Unknown defaults to Colleague
|
// Unknown defaults to Colleague
|
||||||
assert_eq!(parse_relationship_kind("acquaintance"), npc::RelationshipKind::Colleague);
|
assert_eq!(
|
||||||
|
parse_relationship_kind("acquaintance"),
|
||||||
|
npc::RelationshipKind::Colleague
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -108,22 +108,20 @@ impl KnowledgeGraph {
|
|||||||
// --- Write Operations ---
|
// --- Write Operations ---
|
||||||
|
|
||||||
/// Record a direct observation of another entity (entity is in LOS).
|
/// Record a direct observation of another entity (entity is in LOS).
|
||||||
pub fn observe_entity(
|
pub fn observe_entity(&mut self, target: StableId, position: TilePosition, tick: u64) {
|
||||||
&mut self,
|
let entry = self
|
||||||
target: StableId,
|
.entities
|
||||||
position: TilePosition,
|
.entry(target)
|
||||||
tick: u64,
|
.or_insert_with(|| EntityKnowledge {
|
||||||
) {
|
last_known_position: None,
|
||||||
let entry = self.entities.entry(target).or_insert_with(|| EntityKnowledge {
|
last_observed_tick: 0,
|
||||||
last_known_position: None,
|
last_updated_tick: 0,
|
||||||
last_observed_tick: 0,
|
confidence: KnowledgeConfidence::Direct,
|
||||||
last_updated_tick: 0,
|
source: KnowledgeSource::DirectObservation { tick },
|
||||||
confidence: KnowledgeConfidence::Direct,
|
state: KnowledgeState::Active,
|
||||||
source: KnowledgeSource::DirectObservation { tick },
|
relationship: RelationshipState::Unknown,
|
||||||
state: KnowledgeState::Active,
|
known_attributes: BTreeMap::new(),
|
||||||
relationship: RelationshipState::Unknown,
|
});
|
||||||
known_attributes: BTreeMap::new(),
|
|
||||||
});
|
|
||||||
entry.last_known_position = Some(position);
|
entry.last_known_position = Some(position);
|
||||||
entry.last_observed_tick = tick;
|
entry.last_observed_tick = tick;
|
||||||
entry.last_updated_tick = tick;
|
entry.last_updated_tick = tick;
|
||||||
@@ -251,10 +249,7 @@ mod tests {
|
|||||||
g.observe_entity(target, make_position(5, 10), 100);
|
g.observe_entity(target, make_position(5, 10), 100);
|
||||||
g.set_relationship(&target, RelationshipState::Hostile);
|
g.set_relationship(&target, RelationshipState::Hostile);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(g.relationship_with(&target), RelationshipState::Hostile);
|
||||||
g.relationship_with(&target),
|
|
||||||
RelationshipState::Hostile
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -330,10 +325,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
// Age = 200 - 100 = 100, which is > decay_after (10)
|
// Age = 200 - 100 = 100, which is > decay_after (10)
|
||||||
g.decay(200, &thresholds);
|
g.decay(200, &thresholds);
|
||||||
assert_eq!(
|
assert_eq!(g.confidence_of(&target), Some(KnowledgeConfidence::KnowsOf));
|
||||||
g.confidence_of(&target),
|
|
||||||
Some(KnowledgeConfidence::KnowsOf)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ pub mod graph;
|
|||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub use events::{
|
pub use events::{KnowledgeEvent, KnowledgeEventQueue, KnowledgeEventType};
|
||||||
KnowledgeEvent, KnowledgeEventQueue, KnowledgeEventType,
|
|
||||||
};
|
|
||||||
pub use graph::KnowledgeGraph;
|
pub use graph::KnowledgeGraph;
|
||||||
pub use registry::{EntityRegistry, StableEntityId};
|
pub use registry::{EntityRegistry, StableEntityId};
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
@@ -33,8 +31,7 @@ impl Plugin for KnowledgePlugin {
|
|||||||
(
|
(
|
||||||
events::process_knowledge_events
|
events::process_knowledge_events
|
||||||
.after(crate::perception::observer::compute_observer_snapshot),
|
.after(crate::perception::observer::compute_observer_snapshot),
|
||||||
events::decay_knowledge
|
events::decay_knowledge.after(events::process_knowledge_events),
|
||||||
.after(events::process_knowledge_events),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
tracing::debug!("KnowledgePlugin initialized");
|
tracing::debug!("KnowledgePlugin initialized");
|
||||||
|
|||||||
@@ -141,7 +141,11 @@ mod tests {
|
|||||||
graph.set_relationship(a, StableId(20), make_edge(RelationshipKind::Colleague, 2));
|
graph.set_relationship(a, StableId(20), make_edge(RelationshipKind::Colleague, 2));
|
||||||
graph.set_relationship(a, StableId(30), make_edge(RelationshipKind::Rival, -3));
|
graph.set_relationship(a, StableId(30), make_edge(RelationshipKind::Rival, -3));
|
||||||
// Different subject — should not appear
|
// Different subject — should not appear
|
||||||
graph.set_relationship(StableId(2), StableId(10), make_edge(RelationshipKind::Family, 8));
|
graph.set_relationship(
|
||||||
|
StableId(2),
|
||||||
|
StableId(10),
|
||||||
|
make_edge(RelationshipKind::Family, 8),
|
||||||
|
);
|
||||||
|
|
||||||
let rels = graph.relationships_of(&a);
|
let rels = graph.relationships_of(&a);
|
||||||
assert_eq!(rels.len(), 3);
|
assert_eq!(rels.len(), 3);
|
||||||
@@ -158,9 +162,17 @@ mod tests {
|
|||||||
|
|
||||||
graph.set_relationship(StableId(1), target, make_edge(RelationshipKind::Friend, 5));
|
graph.set_relationship(StableId(1), target, make_edge(RelationshipKind::Friend, 5));
|
||||||
graph.set_relationship(StableId(2), target, make_edge(RelationshipKind::Rival, -2));
|
graph.set_relationship(StableId(2), target, make_edge(RelationshipKind::Rival, -2));
|
||||||
graph.set_relationship(StableId(3), target, make_edge(RelationshipKind::Colleague, 0));
|
graph.set_relationship(
|
||||||
|
StableId(3),
|
||||||
|
target,
|
||||||
|
make_edge(RelationshipKind::Colleague, 0),
|
||||||
|
);
|
||||||
// Edge to different target — should not appear
|
// Edge to different target — should not appear
|
||||||
graph.set_relationship(StableId(1), StableId(99), make_edge(RelationshipKind::Family, 8));
|
graph.set_relationship(
|
||||||
|
StableId(1),
|
||||||
|
StableId(99),
|
||||||
|
make_edge(RelationshipKind::Family, 8),
|
||||||
|
);
|
||||||
|
|
||||||
let knowers = graph.who_knows(&target);
|
let knowers = graph.who_knows(&target);
|
||||||
assert_eq!(knowers.len(), 3);
|
assert_eq!(knowers.len(), 3);
|
||||||
@@ -199,9 +211,21 @@ mod tests {
|
|||||||
fn deterministic_iteration() {
|
fn deterministic_iteration() {
|
||||||
let mut graph = RelationshipGraph::new();
|
let mut graph = RelationshipGraph::new();
|
||||||
// Insert in arbitrary order
|
// Insert in arbitrary order
|
||||||
graph.set_relationship(StableId(3), StableId(1), make_edge(RelationshipKind::Rival, -1));
|
graph.set_relationship(
|
||||||
graph.set_relationship(StableId(1), StableId(2), make_edge(RelationshipKind::Friend, 5));
|
StableId(3),
|
||||||
graph.set_relationship(StableId(2), StableId(3), make_edge(RelationshipKind::Colleague, 0));
|
StableId(1),
|
||||||
|
make_edge(RelationshipKind::Rival, -1),
|
||||||
|
);
|
||||||
|
graph.set_relationship(
|
||||||
|
StableId(1),
|
||||||
|
StableId(2),
|
||||||
|
make_edge(RelationshipKind::Friend, 5),
|
||||||
|
);
|
||||||
|
graph.set_relationship(
|
||||||
|
StableId(2),
|
||||||
|
StableId(3),
|
||||||
|
make_edge(RelationshipKind::Colleague, 0),
|
||||||
|
);
|
||||||
|
|
||||||
// Iteration order should be deterministic (sorted by (subject, target))
|
// Iteration order should be deterministic (sorted by (subject, target))
|
||||||
let keys: Vec<_> = graph.edges.keys().collect();
|
let keys: Vec<_> = graph.edges.keys().collect();
|
||||||
|
|||||||
@@ -55,11 +55,9 @@ pub fn check_phase_transition(
|
|||||||
for (entity, current_pos, routine) in npcs.iter() {
|
for (entity, current_pos, routine) in npcs.iter() {
|
||||||
if let Some(expected_location) = routine.expected_location(current_phase) {
|
if let Some(expected_location) = routine.expected_location(current_phase) {
|
||||||
if *current_pos != expected_location {
|
if *current_pos != expected_location {
|
||||||
commands
|
commands.entity(entity).insert(PathRequest {
|
||||||
.entity(entity)
|
goal: expected_location,
|
||||||
.insert(PathRequest {
|
});
|
||||||
goal: expected_location,
|
|
||||||
});
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"Entity {:?}: routine path request to {:?} for {:?}",
|
"Entity {:?}: routine path request to {:?} for {:?}",
|
||||||
entity,
|
entity,
|
||||||
@@ -105,8 +103,7 @@ mod tests {
|
|||||||
.id();
|
.id();
|
||||||
|
|
||||||
// Advance time to Afternoon boundary
|
// Advance time to Afternoon boundary
|
||||||
world.resource_mut::<SimulationTime>().tick =
|
world.resource_mut::<SimulationTime>().tick = MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
||||||
MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(check_phase_transition);
|
schedule.add_systems(check_phase_transition);
|
||||||
@@ -163,8 +160,7 @@ mod tests {
|
|||||||
))
|
))
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
world.resource_mut::<SimulationTime>().tick =
|
world.resource_mut::<SimulationTime>().tick = MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
||||||
MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(check_phase_transition);
|
schedule.add_systems(check_phase_transition);
|
||||||
@@ -193,8 +189,7 @@ mod tests {
|
|||||||
.id();
|
.id();
|
||||||
|
|
||||||
// Transition to Afternoon, but NPC only has Morning entry
|
// Transition to Afternoon, but NPC only has Morning entry
|
||||||
world.resource_mut::<SimulationTime>().tick =
|
world.resource_mut::<SimulationTime>().tick = MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
||||||
MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(check_phase_transition);
|
schedule.add_systems(check_phase_transition);
|
||||||
|
|||||||
@@ -210,14 +210,12 @@ mod tests {
|
|||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems((
|
schedule.add_systems((
|
||||||
compute_visibility_geometry,
|
compute_visibility_geometry,
|
||||||
compute_observer_snapshot
|
compute_observer_snapshot.after(compute_visibility_geometry),
|
||||||
.after(compute_visibility_geometry),
|
|
||||||
crate::perception::observation::emit_observation_events
|
crate::perception::observation::emit_observation_events
|
||||||
.after(compute_observer_snapshot),
|
.after(compute_observer_snapshot),
|
||||||
generate_observation_events
|
generate_observation_events
|
||||||
.after(crate::perception::observation::emit_observation_events),
|
.after(crate::perception::observation::emit_observation_events),
|
||||||
crate::knowledge::events::process_knowledge_events
|
crate::knowledge::events::process_knowledge_events.after(generate_observation_events),
|
||||||
.after(generate_observation_events),
|
|
||||||
));
|
));
|
||||||
schedule.run(world);
|
schedule.run(world);
|
||||||
}
|
}
|
||||||
@@ -228,8 +226,7 @@ mod tests {
|
|||||||
let mut registry = EntityRegistry::new(0);
|
let mut registry = EntityRegistry::new(0);
|
||||||
|
|
||||||
// Set time to Afternoon
|
// Set time to Afternoon
|
||||||
world.resource_mut::<SimulationTime>().tick =
|
world.resource_mut::<SimulationTime>().tick = MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
||||||
MINUTES_PER_PHASE * TICKS_PER_GAME_MINUTE;
|
|
||||||
|
|
||||||
let player = world
|
let player = world
|
||||||
.spawn((
|
.spawn((
|
||||||
@@ -381,7 +378,11 @@ mod tests {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(absences.len(), 1, "should detect absence at visible location");
|
assert_eq!(
|
||||||
|
absences.len(),
|
||||||
|
1,
|
||||||
|
"should detect absence at visible location"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -401,9 +402,7 @@ mod tests {
|
|||||||
.id();
|
.id();
|
||||||
registry.register(player);
|
registry.register(player);
|
||||||
|
|
||||||
let npc = world
|
let npc = world.spawn((Npc, TilePosition::new(16, 14, 0))).id();
|
||||||
.spawn((Npc, TilePosition::new(16, 14, 0)))
|
|
||||||
.id();
|
|
||||||
let npc_sid = registry.register(npc);
|
let npc_sid = registry.register(npc);
|
||||||
|
|
||||||
world.insert_resource(registry);
|
world.insert_resource(registry);
|
||||||
@@ -428,9 +427,7 @@ mod tests {
|
|||||||
let mut world = setup_world();
|
let mut world = setup_world();
|
||||||
let mut registry = EntityRegistry::new(0);
|
let mut registry = EntityRegistry::new(0);
|
||||||
|
|
||||||
let npc = world
|
let npc = world.spawn((Npc, TilePosition::new(16, 14, 0))).id();
|
||||||
.spawn((Npc, TilePosition::new(16, 14, 0)))
|
|
||||||
.id();
|
|
||||||
let npc_sid = registry.register(npc);
|
let npc_sid = registry.register(npc);
|
||||||
|
|
||||||
// Player already knows about the NPC
|
// Player already knows about the NPC
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ impl PerceptionQuery for NaturalVision {
|
|||||||
z,
|
z,
|
||||||
);
|
);
|
||||||
|
|
||||||
let cone_tiles =
|
let cone_tiles = apply_vision_cone(&fov, observer_pos.x, observer_pos.y, facing, &config);
|
||||||
apply_vision_cone(&fov, observer_pos.x, observer_pos.y, facing, &config);
|
|
||||||
|
|
||||||
let visible_tiles = cone_tiles
|
let visible_tiles = cone_tiles
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -101,7 +101,12 @@ pub fn symmetric_shadowcast(
|
|||||||
visible.insert((origin_x, origin_y)); // Origin is always visible
|
visible.insert((origin_x, origin_y)); // Origin is always visible
|
||||||
|
|
||||||
// Process 4 cardinal quadrants
|
// Process 4 cardinal quadrants
|
||||||
for &cardinal in &[Cardinal::North, Cardinal::East, Cardinal::South, Cardinal::West] {
|
for &cardinal in &[
|
||||||
|
Cardinal::North,
|
||||||
|
Cardinal::East,
|
||||||
|
Cardinal::South,
|
||||||
|
Cardinal::West,
|
||||||
|
] {
|
||||||
scan_quadrant(&mut visible, is_opaque, origin_x, origin_y, range, cardinal);
|
scan_quadrant(&mut visible, is_opaque, origin_x, origin_y, range, cardinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,7 @@ pub fn apply_vision_cone(
|
|||||||
) -> Vec<(i32, i32, VisibilitySector)> {
|
) -> Vec<(i32, i32, VisibilitySector)> {
|
||||||
fov.visible_tiles()
|
fov.visible_tiles()
|
||||||
.filter_map(|(x, y)| {
|
.filter_map(|(x, y)| {
|
||||||
classify_tile(observer_x, observer_y, x, y, facing, config)
|
classify_tile(observer_x, observer_y, x, y, facing, config).map(|sector| (x, y, sector))
|
||||||
.map(|sector| (x, y, sector))
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
+162
-76
@@ -68,7 +68,12 @@ pub fn process_player_input(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
registry: Res<EntityRegistry>,
|
registry: Res<EntityRegistry>,
|
||||||
mut player_query: Query<
|
mut player_query: Query<
|
||||||
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
|
(
|
||||||
|
Entity,
|
||||||
|
&TilePosition,
|
||||||
|
Option<&mut Stance>,
|
||||||
|
Option<&mut PlayerMoveCooldown>,
|
||||||
|
),
|
||||||
With<PlayerCharacter>,
|
With<PlayerCharacter>,
|
||||||
>,
|
>,
|
||||||
inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
|
inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
|
||||||
@@ -143,34 +148,30 @@ pub fn process_player_input(
|
|||||||
time.tick_rate = rate;
|
time.tick_rate = rate;
|
||||||
tracing::debug!("Tick rate set to {:?} by player input", rate);
|
tracing::debug!("Tick rate set to {:?} by player input", rate);
|
||||||
}
|
}
|
||||||
PlayerAction::Interact { target_entity_id, ref verb } => {
|
PlayerAction::Interact {
|
||||||
match verb.as_deref() {
|
target_entity_id,
|
||||||
Some("Take") => {
|
ref verb,
|
||||||
handle_take(
|
} => match verb.as_deref() {
|
||||||
&mut commands,
|
Some("Take") => {
|
||||||
®istry,
|
handle_take(
|
||||||
&player_query,
|
&mut commands,
|
||||||
&inventory_items,
|
®istry,
|
||||||
target_entity_id,
|
&player_query,
|
||||||
);
|
&inventory_items,
|
||||||
}
|
target_entity_id,
|
||||||
Some("Place") => {
|
);
|
||||||
handle_place(
|
}
|
||||||
&mut commands,
|
Some("Place") => {
|
||||||
®istry,
|
handle_place(&mut commands, ®istry, &player_query, target_entity_id);
|
||||||
&player_query,
|
}
|
||||||
target_entity_id,
|
_ => {
|
||||||
);
|
tracing::info!(
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
tracing::info!(
|
|
||||||
"Interact: target={:?}, verb={:?} — logged only, dialogue dispatch future scope (#415)",
|
"Interact: target={:?}, verb={:?} — logged only, dialogue dispatch future scope (#415)",
|
||||||
target_entity_id,
|
target_entity_id,
|
||||||
verb,
|
verb,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
PlayerAction::UsePerceptionMode(ref mode) => {
|
PlayerAction::UsePerceptionMode(ref mode) => {
|
||||||
tracing::trace!("UsePerceptionMode({}) — no-op for Sprint 1", mode);
|
tracing::trace!("UsePerceptionMode({}) — no-op for Sprint 1", mode);
|
||||||
}
|
}
|
||||||
@@ -192,7 +193,12 @@ pub fn process_player_input(
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn apply_move(
|
fn apply_move(
|
||||||
player_query: &mut Query<
|
player_query: &mut Query<
|
||||||
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
|
(
|
||||||
|
Entity,
|
||||||
|
&TilePosition,
|
||||||
|
Option<&mut Stance>,
|
||||||
|
Option<&mut PlayerMoveCooldown>,
|
||||||
|
),
|
||||||
With<PlayerCharacter>,
|
With<PlayerCharacter>,
|
||||||
>,
|
>,
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
@@ -230,7 +236,12 @@ fn handle_take(
|
|||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
registry: &EntityRegistry,
|
registry: &EntityRegistry,
|
||||||
player_query: &Query<
|
player_query: &Query<
|
||||||
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
|
(
|
||||||
|
Entity,
|
||||||
|
&TilePosition,
|
||||||
|
Option<&mut Stance>,
|
||||||
|
Option<&mut PlayerMoveCooldown>,
|
||||||
|
),
|
||||||
With<PlayerCharacter>,
|
With<PlayerCharacter>,
|
||||||
>,
|
>,
|
||||||
inventory_items: &Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
|
inventory_items: &Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
|
||||||
@@ -260,12 +271,16 @@ fn handle_take(
|
|||||||
// Check inventory capacity
|
// Check inventory capacity
|
||||||
let occupied = occupied_slots_for(player_sid, inventory_items);
|
let occupied = occupied_slots_for(player_sid, inventory_items);
|
||||||
let Some(slot) = find_next_slot(&occupied) else {
|
let Some(slot) = find_next_slot(&occupied) else {
|
||||||
tracing::info!("Inventory full ({} slots), cannot take item", MAX_INVENTORY_SLOTS);
|
tracing::info!(
|
||||||
|
"Inventory full ({} slots), cannot take item",
|
||||||
|
MAX_INVENTORY_SLOTS
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove TilePosition (item leaves the ground), add CarriedBy + InventorySlot
|
// Remove TilePosition (item leaves the ground), add CarriedBy + InventorySlot
|
||||||
commands.entity(target_entity)
|
commands
|
||||||
|
.entity(target_entity)
|
||||||
.remove::<TilePosition>()
|
.remove::<TilePosition>()
|
||||||
.insert((CarriedBy(player_sid), InventorySlot(slot)));
|
.insert((CarriedBy(player_sid), InventorySlot(slot)));
|
||||||
|
|
||||||
@@ -284,7 +299,12 @@ fn handle_place(
|
|||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
registry: &EntityRegistry,
|
registry: &EntityRegistry,
|
||||||
player_query: &Query<
|
player_query: &Query<
|
||||||
(Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>),
|
(
|
||||||
|
Entity,
|
||||||
|
&TilePosition,
|
||||||
|
Option<&mut Stance>,
|
||||||
|
Option<&mut PlayerMoveCooldown>,
|
||||||
|
),
|
||||||
With<PlayerCharacter>,
|
With<PlayerCharacter>,
|
||||||
>,
|
>,
|
||||||
target_entity_id: Option<u64>,
|
target_entity_id: Option<u64>,
|
||||||
@@ -307,7 +327,8 @@ fn handle_place(
|
|||||||
let place_pos = *player_pos;
|
let place_pos = *player_pos;
|
||||||
|
|
||||||
// Remove inventory components, place item at player's tile
|
// Remove inventory components, place item at player's tile
|
||||||
commands.entity(target_entity)
|
commands
|
||||||
|
.entity(target_entity)
|
||||||
.remove::<CarriedBy>()
|
.remove::<CarriedBy>()
|
||||||
.remove::<InventorySlot>()
|
.remove::<InventorySlot>()
|
||||||
.insert(place_pos);
|
.insert(place_pos);
|
||||||
@@ -338,7 +359,10 @@ mod tests {
|
|||||||
});
|
});
|
||||||
queue.push(PlayerInput {
|
queue.push(PlayerInput {
|
||||||
tick: 5,
|
tick: 5,
|
||||||
action: PlayerAction::Interact { target_entity_id: None, verb: None },
|
action: PlayerAction::Interact {
|
||||||
|
target_entity_id: None,
|
||||||
|
verb: None,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let inputs = queue.drain_for_tick(3);
|
let inputs = queue.drain_for_tick(3);
|
||||||
assert_eq!(inputs.len(), 2);
|
assert_eq!(inputs.len(), 2);
|
||||||
@@ -406,7 +430,10 @@ mod tests {
|
|||||||
schedule.add_systems(process_player_input);
|
schedule.add_systems(process_player_input);
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
assert_eq!(world.resource::<SimulationTime>().tick_rate, TickRate::Paused);
|
assert_eq!(
|
||||||
|
world.resource::<SimulationTime>().tick_rate,
|
||||||
|
TickRate::Paused
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -553,7 +580,10 @@ mod tests {
|
|||||||
action: PlayerAction::MoveNorth,
|
action: PlayerAction::MoveNorth,
|
||||||
});
|
});
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
assert!(world.get::<MoveIntent>(player).is_some(), "first move should succeed");
|
assert!(
|
||||||
|
world.get::<MoveIntent>(player).is_some(),
|
||||||
|
"first move should succeed"
|
||||||
|
);
|
||||||
|
|
||||||
// Remove MoveIntent (simulating validate_movement consuming it)
|
// Remove MoveIntent (simulating validate_movement consuming it)
|
||||||
world.entity_mut(player).remove::<MoveIntent>();
|
world.entity_mut(player).remove::<MoveIntent>();
|
||||||
@@ -564,7 +594,10 @@ mod tests {
|
|||||||
action: PlayerAction::MoveNorth,
|
action: PlayerAction::MoveNorth,
|
||||||
});
|
});
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
assert!(world.get::<MoveIntent>(player).is_none(), "second move should be throttled");
|
assert!(
|
||||||
|
world.get::<MoveIntent>(player).is_none(),
|
||||||
|
"second move should be throttled"
|
||||||
|
);
|
||||||
|
|
||||||
// Tick 0 again: move north — should succeed (cooldown elapsed)
|
// Tick 0 again: move north — should succeed (cooldown elapsed)
|
||||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||||
@@ -572,7 +605,10 @@ mod tests {
|
|||||||
action: PlayerAction::MoveNorth,
|
action: PlayerAction::MoveNorth,
|
||||||
});
|
});
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
assert!(world.get::<MoveIntent>(player).is_some(), "third move should succeed after cooldown");
|
assert!(
|
||||||
|
world.get::<MoveIntent>(player).is_some(),
|
||||||
|
"third move should succeed after cooldown"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -609,7 +645,10 @@ mod tests {
|
|||||||
action: PlayerAction::MoveNorth,
|
action: PlayerAction::MoveNorth,
|
||||||
});
|
});
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
assert!(world.get::<MoveIntent>(player).is_some(), "sprint should allow every tick");
|
assert!(
|
||||||
|
world.get::<MoveIntent>(player).is_some(),
|
||||||
|
"sprint should allow every tick"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -655,16 +694,17 @@ mod tests {
|
|||||||
let player = world
|
let player = world
|
||||||
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
||||||
.id();
|
.id();
|
||||||
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
|
let player_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(player);
|
||||||
|
|
||||||
// Spawn item near player
|
// Spawn item near player
|
||||||
let item = world
|
let item = world
|
||||||
.spawn((
|
.spawn((TilePosition::new(5, 4, 0), ItemName("Manifest Copy".into())))
|
||||||
TilePosition::new(5, 4, 0),
|
|
||||||
ItemName("Manifest Copy".into()),
|
|
||||||
))
|
|
||||||
.id();
|
.id();
|
||||||
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
|
let item_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(item);
|
||||||
|
|
||||||
// Issue Take verb
|
// Issue Take verb
|
||||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||||
@@ -680,10 +720,17 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
// Item should have CarriedBy + InventorySlot, no TilePosition
|
// Item should have CarriedBy + InventorySlot, no TilePosition
|
||||||
assert!(world.get::<TilePosition>(item).is_none(), "item should leave the ground");
|
assert!(
|
||||||
let carried = world.get::<CarriedBy>(item).expect("item should have CarriedBy");
|
world.get::<TilePosition>(item).is_none(),
|
||||||
|
"item should leave the ground"
|
||||||
|
);
|
||||||
|
let carried = world
|
||||||
|
.get::<CarriedBy>(item)
|
||||||
|
.expect("item should have CarriedBy");
|
||||||
assert_eq!(carried.0, player_sid);
|
assert_eq!(carried.0, player_sid);
|
||||||
let slot = world.get::<InventorySlot>(item).expect("item should have slot");
|
let slot = world
|
||||||
|
.get::<InventorySlot>(item)
|
||||||
|
.expect("item should have slot");
|
||||||
assert_eq!(slot.0, 0, "first item goes to slot 0");
|
assert_eq!(slot.0, 0, "first item goes to slot 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +744,9 @@ mod tests {
|
|||||||
let player = world
|
let player = world
|
||||||
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
||||||
.id();
|
.id();
|
||||||
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
|
let player_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(player);
|
||||||
|
|
||||||
// Spawn item already in inventory (no TilePosition)
|
// Spawn item already in inventory (no TilePosition)
|
||||||
let item = world
|
let item = world
|
||||||
@@ -707,7 +756,9 @@ mod tests {
|
|||||||
InventorySlot(0),
|
InventorySlot(0),
|
||||||
))
|
))
|
||||||
.id();
|
.id();
|
||||||
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
|
let item_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(item);
|
||||||
|
|
||||||
// Issue Place verb
|
// Issue Place verb
|
||||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||||
@@ -723,10 +774,19 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
// Item should have TilePosition at player's location, no CarriedBy/InventorySlot
|
// Item should have TilePosition at player's location, no CarriedBy/InventorySlot
|
||||||
let pos = world.get::<TilePosition>(item).expect("item should be on ground");
|
let pos = world
|
||||||
assert_eq!(*pos, TilePosition::new(5, 5, 0), "placed at player position");
|
.get::<TilePosition>(item)
|
||||||
|
.expect("item should be on ground");
|
||||||
|
assert_eq!(
|
||||||
|
*pos,
|
||||||
|
TilePosition::new(5, 5, 0),
|
||||||
|
"placed at player position"
|
||||||
|
);
|
||||||
assert!(world.get::<CarriedBy>(item).is_none(), "CarriedBy removed");
|
assert!(world.get::<CarriedBy>(item).is_none(), "CarriedBy removed");
|
||||||
assert!(world.get::<InventorySlot>(item).is_none(), "InventorySlot removed");
|
assert!(
|
||||||
|
world.get::<InventorySlot>(item).is_none(),
|
||||||
|
"InventorySlot removed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -739,7 +799,9 @@ mod tests {
|
|||||||
let player = world
|
let player = world
|
||||||
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
||||||
.id();
|
.id();
|
||||||
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
|
let player_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(player);
|
||||||
|
|
||||||
// Item already in slot 0
|
// Item already in slot 0
|
||||||
world.spawn((
|
world.spawn((
|
||||||
@@ -750,12 +812,11 @@ mod tests {
|
|||||||
|
|
||||||
// New item on the ground
|
// New item on the ground
|
||||||
let item2 = world
|
let item2 = world
|
||||||
.spawn((
|
.spawn((TilePosition::new(5, 4, 0), ItemName("Token".into())))
|
||||||
TilePosition::new(5, 4, 0),
|
|
||||||
ItemName("Token".into()),
|
|
||||||
))
|
|
||||||
.id();
|
.id();
|
||||||
let item2_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item2);
|
let item2_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(item2);
|
||||||
|
|
||||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||||
tick: 0,
|
tick: 0,
|
||||||
@@ -769,7 +830,9 @@ mod tests {
|
|||||||
schedule.add_systems(process_player_input);
|
schedule.add_systems(process_player_input);
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let slot = world.get::<InventorySlot>(item2).expect("item should have slot");
|
let slot = world
|
||||||
|
.get::<InventorySlot>(item2)
|
||||||
|
.expect("item should have slot");
|
||||||
assert_eq!(slot.0, 1, "second item goes to slot 1");
|
assert_eq!(slot.0, 1, "second item goes to slot 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,7 +846,9 @@ mod tests {
|
|||||||
let player = world
|
let player = world
|
||||||
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
||||||
.id();
|
.id();
|
||||||
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
|
let player_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(player);
|
||||||
|
|
||||||
// Fill all 9 slots
|
// Fill all 9 slots
|
||||||
for slot in 0..MAX_INVENTORY_SLOTS {
|
for slot in 0..MAX_INVENTORY_SLOTS {
|
||||||
@@ -796,12 +861,11 @@ mod tests {
|
|||||||
|
|
||||||
// Try to take another item
|
// Try to take another item
|
||||||
let item = world
|
let item = world
|
||||||
.spawn((
|
.spawn((TilePosition::new(5, 4, 0), ItemName("Overflow".into())))
|
||||||
TilePosition::new(5, 4, 0),
|
|
||||||
ItemName("Overflow".into()),
|
|
||||||
))
|
|
||||||
.id();
|
.id();
|
||||||
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
|
let item_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(item);
|
||||||
|
|
||||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||||
tick: 0,
|
tick: 0,
|
||||||
@@ -816,8 +880,14 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
// Item should still be on the ground
|
// Item should still be on the ground
|
||||||
assert!(world.get::<TilePosition>(item).is_some(), "item stays on ground");
|
assert!(
|
||||||
assert!(world.get::<CarriedBy>(item).is_none(), "no CarriedBy when full");
|
world.get::<TilePosition>(item).is_some(),
|
||||||
|
"item stays on ground"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
world.get::<CarriedBy>(item).is_none(),
|
||||||
|
"no CarriedBy when full"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -831,15 +901,16 @@ mod tests {
|
|||||||
let player = world
|
let player = world
|
||||||
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
||||||
.id();
|
.id();
|
||||||
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
|
let player_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(player);
|
||||||
|
|
||||||
let item = world
|
let item = world
|
||||||
.spawn((
|
.spawn((TilePosition::new(5, 4, 0), ItemName("Manifest Copy".into())))
|
||||||
TilePosition::new(5, 4, 0),
|
|
||||||
ItemName("Manifest Copy".into()),
|
|
||||||
))
|
|
||||||
.id();
|
.id();
|
||||||
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
|
let item_sid = world
|
||||||
|
.resource_mut::<crate::knowledge::EntityRegistry>()
|
||||||
|
.register(item);
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(process_player_input);
|
schedule.add_systems(process_player_input);
|
||||||
@@ -854,7 +925,10 @@ mod tests {
|
|||||||
});
|
});
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
assert!(world.get::<TilePosition>(item).is_none(), "item off ground after Take");
|
assert!(
|
||||||
|
world.get::<TilePosition>(item).is_none(),
|
||||||
|
"item off ground after Take"
|
||||||
|
);
|
||||||
assert_eq!(world.get::<CarriedBy>(item).unwrap().0, player_sid);
|
assert_eq!(world.get::<CarriedBy>(item).unwrap().0, player_sid);
|
||||||
assert_eq!(world.get::<InventorySlot>(item).unwrap().0, 0);
|
assert_eq!(world.get::<InventorySlot>(item).unwrap().0, 0);
|
||||||
|
|
||||||
@@ -869,10 +943,22 @@ mod tests {
|
|||||||
world.resource_mut::<SimulationTime>().tick = 1;
|
world.resource_mut::<SimulationTime>().tick = 1;
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let pos = world.get::<TilePosition>(item).expect("item back on ground after Place");
|
let pos = world
|
||||||
assert_eq!(*pos, TilePosition::new(5, 5, 0), "placed at player position");
|
.get::<TilePosition>(item)
|
||||||
assert!(world.get::<CarriedBy>(item).is_none(), "CarriedBy removed after Place");
|
.expect("item back on ground after Place");
|
||||||
assert!(world.get::<InventorySlot>(item).is_none(), "InventorySlot removed after Place");
|
assert_eq!(
|
||||||
|
*pos,
|
||||||
|
TilePosition::new(5, 5, 0),
|
||||||
|
"placed at player position"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
world.get::<CarriedBy>(item).is_none(),
|
||||||
|
"CarriedBy removed after Place"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
world.get::<InventorySlot>(item).is_none(),
|
||||||
|
"InventorySlot removed after Place"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -52,30 +52,100 @@ impl ObjectType {
|
|||||||
pub fn verb_set(&self) -> &'static [VerbDef] {
|
pub fn verb_set(&self) -> &'static [VerbDef] {
|
||||||
match self {
|
match self {
|
||||||
Self::Readable => &[
|
Self::Readable => &[
|
||||||
VerbDef { kind: VerbKind::Read, label: "Read", priority: 1, close_only: true },
|
VerbDef {
|
||||||
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
|
kind: VerbKind::Read,
|
||||||
|
label: "Read",
|
||||||
|
priority: 1,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Observe,
|
||||||
|
label: "Observe",
|
||||||
|
priority: 2,
|
||||||
|
close_only: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Self::Container => &[
|
Self::Container => &[
|
||||||
VerbDef { kind: VerbKind::Open, label: "Open", priority: 1, close_only: true },
|
VerbDef {
|
||||||
VerbDef { kind: VerbKind::Search, label: "Search", priority: 2, close_only: true },
|
kind: VerbKind::Open,
|
||||||
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 3, close_only: false },
|
label: "Open",
|
||||||
|
priority: 1,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Search,
|
||||||
|
label: "Search",
|
||||||
|
priority: 2,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Observe,
|
||||||
|
label: "Observe",
|
||||||
|
priority: 3,
|
||||||
|
close_only: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Self::Terminal => &[
|
Self::Terminal => &[
|
||||||
VerbDef { kind: VerbKind::Use, label: "Use", priority: 1, close_only: true },
|
VerbDef {
|
||||||
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
|
kind: VerbKind::Use,
|
||||||
|
label: "Use",
|
||||||
|
priority: 1,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Observe,
|
||||||
|
label: "Observe",
|
||||||
|
priority: 2,
|
||||||
|
close_only: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Self::Door => &[
|
Self::Door => &[
|
||||||
VerbDef { kind: VerbKind::Open, label: "Open", priority: 1, close_only: true },
|
VerbDef {
|
||||||
VerbDef { kind: VerbKind::Close, label: "Close", priority: 2, close_only: true },
|
kind: VerbKind::Open,
|
||||||
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 3, close_only: false },
|
label: "Open",
|
||||||
|
priority: 1,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Close,
|
||||||
|
label: "Close",
|
||||||
|
priority: 2,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Observe,
|
||||||
|
label: "Observe",
|
||||||
|
priority: 3,
|
||||||
|
close_only: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Self::Pickup => &[
|
Self::Pickup => &[
|
||||||
VerbDef { kind: VerbKind::Take, label: "Take", priority: 1, close_only: true },
|
VerbDef {
|
||||||
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
|
kind: VerbKind::Take,
|
||||||
|
label: "Take",
|
||||||
|
priority: 1,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Observe,
|
||||||
|
label: "Observe",
|
||||||
|
priority: 2,
|
||||||
|
close_only: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Self::Furniture => &[
|
Self::Furniture => &[
|
||||||
VerbDef { kind: VerbKind::Sit, label: "Sit", priority: 1, close_only: true },
|
VerbDef {
|
||||||
VerbDef { kind: VerbKind::Observe, label: "Observe", priority: 2, close_only: false },
|
kind: VerbKind::Sit,
|
||||||
|
label: "Sit",
|
||||||
|
priority: 1,
|
||||||
|
close_only: true,
|
||||||
|
},
|
||||||
|
VerbDef {
|
||||||
|
kind: VerbKind::Observe,
|
||||||
|
label: "Observe",
|
||||||
|
priority: 2,
|
||||||
|
close_only: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +271,10 @@ pub fn compute_nearby_interactions(
|
|||||||
.to_stable(entity)
|
.to_stable(entity)
|
||||||
.map(|sid| sid.0)
|
.map(|sid| sid.0)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
tracing::error!(?entity, "entity in interaction range but not in EntityRegistry");
|
tracing::error!(
|
||||||
|
?entity,
|
||||||
|
"entity in interaction range but not in EntityRegistry"
|
||||||
|
);
|
||||||
entity.to_bits()
|
entity.to_bits()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,9 +289,7 @@ pub fn compute_nearby_interactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort interactions by distance (nearest first)
|
// Sort interactions by distance (nearest first)
|
||||||
buffer
|
buffer.interactions.sort_by_key(|a| a.distance);
|
||||||
.interactions
|
|
||||||
.sort_by_key(|a| a.distance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Buffer for nearby interaction results, consumed by snapshot generation.
|
/// Buffer for nearby interaction results, consumed by snapshot generation.
|
||||||
@@ -417,11 +488,7 @@ mod tests {
|
|||||||
fn door_close_range_gets_open_close_observe() {
|
fn door_close_range_gets_open_close_observe() {
|
||||||
let mut world = setup_world();
|
let mut world = setup_world();
|
||||||
spawn_player(&mut world, 5, 5);
|
spawn_player(&mut world, 5, 5);
|
||||||
world.spawn((
|
world.spawn((TilePosition::new(5, 6, 0), Interactable, ObjectType::Door));
|
||||||
TilePosition::new(5, 6, 0),
|
|
||||||
Interactable,
|
|
||||||
ObjectType::Door,
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(compute_nearby_interactions);
|
schedule.add_systems(compute_nearby_interactions);
|
||||||
@@ -439,11 +506,7 @@ mod tests {
|
|||||||
fn pickup_close_range_gets_take_and_observe() {
|
fn pickup_close_range_gets_take_and_observe() {
|
||||||
let mut world = setup_world();
|
let mut world = setup_world();
|
||||||
spawn_player(&mut world, 5, 5);
|
spawn_player(&mut world, 5, 5);
|
||||||
world.spawn((
|
world.spawn((TilePosition::new(5, 6, 0), Interactable, ObjectType::Pickup));
|
||||||
TilePosition::new(5, 6, 0),
|
|
||||||
Interactable,
|
|
||||||
ObjectType::Pickup,
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(compute_nearby_interactions);
|
schedule.add_systems(compute_nearby_interactions);
|
||||||
@@ -514,7 +577,10 @@ mod tests {
|
|||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(buffer.interactions.len(), 1);
|
assert_eq!(buffer.interactions.len(), 1);
|
||||||
assert_eq!(buffer.interactions[0].verbs.len(), 1);
|
assert_eq!(buffer.interactions[0].verbs.len(), 1);
|
||||||
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::ExamineObject);
|
assert_eq!(
|
||||||
|
buffer.interactions[0].verbs[0].kind,
|
||||||
|
VerbKind::ExamineObject
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -593,7 +659,10 @@ mod tests {
|
|||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(buffer.interactions.len(), 2);
|
assert_eq!(buffer.interactions.len(), 2);
|
||||||
assert_eq!(buffer.interactions[0].distance, buffer.interactions[1].distance);
|
assert_eq!(
|
||||||
|
buffer.interactions[0].distance,
|
||||||
|
buffer.interactions[1].distance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -708,11 +777,7 @@ mod tests {
|
|||||||
let mut world = setup_world();
|
let mut world = setup_world();
|
||||||
spawn_player(&mut world, 5, 5);
|
spawn_player(&mut world, 5, 5);
|
||||||
// Distance 4 = mid range (> CLOSE_RANGE=2, <= MID_RANGE=5)
|
// Distance 4 = mid range (> CLOSE_RANGE=2, <= MID_RANGE=5)
|
||||||
world.spawn((
|
world.spawn((TilePosition::new(5, 9, 0), Interactable, obj_type));
|
||||||
TilePosition::new(5, 9, 0),
|
|
||||||
Interactable,
|
|
||||||
obj_type,
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(compute_nearby_interactions);
|
schedule.add_systems(compute_nearby_interactions);
|
||||||
@@ -720,16 +785,22 @@ mod tests {
|
|||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.interactions.len(), 1,
|
buffer.interactions.len(),
|
||||||
"{:?} at mid range should produce 1 interaction", obj_type
|
1,
|
||||||
|
"{:?} at mid range should produce 1 interaction",
|
||||||
|
obj_type
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.interactions[0].verbs.len(), 1,
|
buffer.interactions[0].verbs.len(),
|
||||||
"{:?} at mid range should have exactly 1 verb (Observe)", obj_type
|
1,
|
||||||
|
"{:?} at mid range should have exactly 1 verb (Observe)",
|
||||||
|
obj_type
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.interactions[0].verbs[0].kind, VerbKind::Observe,
|
buffer.interactions[0].verbs[0].kind,
|
||||||
"{:?} at mid range verb should be Observe", obj_type
|
VerbKind::Observe,
|
||||||
|
"{:?} at mid range verb should be Observe",
|
||||||
|
obj_type
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,11 +866,16 @@ mod tests {
|
|||||||
for obj_type in types {
|
for obj_type in types {
|
||||||
for def in obj_type.verb_set() {
|
for def in obj_type.verb_set() {
|
||||||
if def.kind == VerbKind::Observe {
|
if def.kind == VerbKind::Observe {
|
||||||
assert!(!def.close_only, "{:?} Observe should be mid-range", obj_type);
|
assert!(
|
||||||
|
!def.close_only,
|
||||||
|
"{:?} Observe should be mid-range",
|
||||||
|
obj_type
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
def.close_only,
|
def.close_only,
|
||||||
"{:?} {:?} should be close-only", obj_type, def.kind
|
"{:?} {:?} should be close-only",
|
||||||
|
obj_type, def.kind
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,7 +898,9 @@ mod tests {
|
|||||||
let verbs = obj_type.verb_set();
|
let verbs = obj_type.verb_set();
|
||||||
assert!(
|
assert!(
|
||||||
verbs.len() <= 4,
|
verbs.len() <= 4,
|
||||||
"{:?} has {} verbs, D-057 max is 4", obj_type, verbs.len()
|
"{:?} has {} verbs, D-057 max is 4",
|
||||||
|
obj_type,
|
||||||
|
verbs.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -832,7 +910,12 @@ mod tests {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/// Spawn player with Stance component for sprint suppression tests.
|
/// Spawn player with Stance component for sprint suppression tests.
|
||||||
fn spawn_player_with_stance(world: &mut World, x: i32, y: i32, stance: MovementStance) -> Entity {
|
fn spawn_player_with_stance(
|
||||||
|
world: &mut World,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
stance: MovementStance,
|
||||||
|
) -> Entity {
|
||||||
world
|
world
|
||||||
.spawn((
|
.spawn((
|
||||||
PlayerCharacter,
|
PlayerCharacter,
|
||||||
@@ -854,21 +937,31 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert!(buffer.interactions.is_empty(), "sprint should suppress all interactions");
|
assert!(
|
||||||
|
buffer.interactions.is_empty(),
|
||||||
|
"sprint should suppress all interactions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sprint_suppresses_object_interactions() {
|
fn sprint_suppresses_object_interactions() {
|
||||||
let mut world = setup_world();
|
let mut world = setup_world();
|
||||||
spawn_player_with_stance(&mut world, 5, 5, MovementStance::Sprint);
|
spawn_player_with_stance(&mut world, 5, 5, MovementStance::Sprint);
|
||||||
world.spawn((TilePosition::new(5, 6, 0), Interactable, ObjectType::Terminal));
|
world.spawn((
|
||||||
|
TilePosition::new(5, 6, 0),
|
||||||
|
Interactable,
|
||||||
|
ObjectType::Terminal,
|
||||||
|
));
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(compute_nearby_interactions);
|
schedule.add_systems(compute_nearby_interactions);
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert!(buffer.interactions.is_empty(), "sprint should suppress object interactions");
|
assert!(
|
||||||
|
buffer.interactions.is_empty(),
|
||||||
|
"sprint should suppress object interactions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -882,7 +975,11 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(buffer.interactions.len(), 1, "Walk should allow interactions");
|
assert_eq!(
|
||||||
|
buffer.interactions.len(),
|
||||||
|
1,
|
||||||
|
"Walk should allow interactions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -896,7 +993,11 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(buffer.interactions.len(), 1, "Careful should allow interactions");
|
assert_eq!(
|
||||||
|
buffer.interactions.len(),
|
||||||
|
1,
|
||||||
|
"Careful should allow interactions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -910,7 +1011,11 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(buffer.interactions.len(), 1, "Crouch should allow interactions");
|
assert_eq!(
|
||||||
|
buffer.interactions.len(),
|
||||||
|
1,
|
||||||
|
"Crouch should allow interactions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -925,7 +1030,11 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert_eq!(buffer.interactions.len(), 1, "no Stance component should allow interactions");
|
assert_eq!(
|
||||||
|
buffer.interactions.len(),
|
||||||
|
1,
|
||||||
|
"no Stance component should allow interactions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -933,7 +1042,11 @@ mod tests {
|
|||||||
let mut world = setup_world();
|
let mut world = setup_world();
|
||||||
spawn_player_with_stance(&mut world, 5, 5, MovementStance::Sprint);
|
spawn_player_with_stance(&mut world, 5, 5, MovementStance::Sprint);
|
||||||
world.spawn((Npc, TilePosition::new(5, 6, 0), Interactable));
|
world.spawn((Npc, TilePosition::new(5, 6, 0), Interactable));
|
||||||
world.spawn((TilePosition::new(6, 5, 0), Interactable, ObjectType::Container));
|
world.spawn((
|
||||||
|
TilePosition::new(6, 5, 0),
|
||||||
|
Interactable,
|
||||||
|
ObjectType::Container,
|
||||||
|
));
|
||||||
world.spawn((TilePosition::new(4, 5, 0), Interactable));
|
world.spawn((TilePosition::new(4, 5, 0), Interactable));
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
@@ -941,6 +1054,9 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let buffer = read_buffer(&mut world);
|
let buffer = read_buffer(&mut world);
|
||||||
assert!(buffer.interactions.is_empty(), "sprint should suppress all 3 nearby entities");
|
assert!(
|
||||||
|
buffer.interactions.is_empty(),
|
||||||
|
"sprint should suppress all 3 nearby entities"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,7 @@ pub struct InventorySlot(pub u8);
|
|||||||
/// Find the next available inventory slot for a carrier.
|
/// Find the next available inventory slot for a carrier.
|
||||||
/// Returns None if all 9 slots are occupied.
|
/// Returns None if all 9 slots are occupied.
|
||||||
pub fn find_next_slot(occupied: &[u8]) -> Option<u8> {
|
pub fn find_next_slot(occupied: &[u8]) -> Option<u8> {
|
||||||
for slot in 0..MAX_INVENTORY_SLOTS {
|
(0..MAX_INVENTORY_SLOTS).find(|slot| !occupied.contains(slot))
|
||||||
if !occupied.contains(&slot) {
|
|
||||||
return Some(slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect inventory items for a specific carrier (by StableId).
|
/// Collect inventory items for a specific carrier (by StableId).
|
||||||
@@ -126,8 +121,7 @@ mod tests {
|
|||||||
let player = world.spawn_empty().id();
|
let player = world.spawn_empty().id();
|
||||||
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
|
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
|
||||||
|
|
||||||
let mut query_state =
|
let mut query_state = world.query::<(Entity, &CarriedBy, &ItemName, &InventorySlot)>();
|
||||||
world.query::<(Entity, &CarriedBy, &ItemName, &InventorySlot)>();
|
|
||||||
|
|
||||||
// Can't use system params directly in tests — use world query
|
// Can't use system params directly in tests — use world query
|
||||||
// Instead, verify the logic by spawning items and checking
|
// Instead, verify the logic by spawning items and checking
|
||||||
|
|||||||
@@ -36,9 +36,18 @@ pub(crate) const ANOMALY_DELAY_TICKS: u64 = 90;
|
|||||||
/// Hardcoded v0.1 sprint anomaly "double-take" lines.
|
/// Hardcoded v0.1 sprint anomaly "double-take" lines.
|
||||||
/// Future: move to content pools with trigger="sprint_anomaly".
|
/// Future: move to content pools with trigger="sprint_anomaly".
|
||||||
const ANOMALY_LINES: &[(&str, &str)] = &[
|
const ANOMALY_LINES: &[(&str, &str)] = &[
|
||||||
("sprint_anomaly_01", "Wait \u{2014} something wasn't right back there."),
|
(
|
||||||
("sprint_anomaly_02", "Hold on. That face... why were they there?"),
|
"sprint_anomaly_01",
|
||||||
("sprint_anomaly_03", "Something's off. That wasn't where they should be."),
|
"Wait \u{2014} something wasn't right back there.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sprint_anomaly_02",
|
||||||
|
"Hold on. That face... why were they there?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sprint_anomaly_03",
|
||||||
|
"Something's off. That wasn't where they should be.",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Tracks monologue state for cooldown and trigger detection.
|
/// Tracks monologue state for cooldown and trigger detection.
|
||||||
@@ -148,7 +157,11 @@ pub fn process_sprint_anomaly_monologue(
|
|||||||
time: Res<SimulationTime>,
|
time: Res<SimulationTime>,
|
||||||
mut rng: ResMut<SimRng>,
|
mut rng: ResMut<SimRng>,
|
||||||
mut query: Query<
|
mut query: Query<
|
||||||
(&mut SprintAnomalyQueue, &mut MonologueBuffer, &mut MonologueState),
|
(
|
||||||
|
&mut SprintAnomalyQueue,
|
||||||
|
&mut MonologueBuffer,
|
||||||
|
&mut MonologueState,
|
||||||
|
),
|
||||||
With<PlayerCharacter>,
|
With<PlayerCharacter>,
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
@@ -236,7 +249,7 @@ pub fn trigger_monologue(
|
|||||||
let character = state.character.as_str();
|
let character = state.character.as_str();
|
||||||
let mut candidates: Vec<(&str, &str)> = Vec::new(); // (id, text)
|
let mut candidates: Vec<(&str, &str)> = Vec::new(); // (id, text)
|
||||||
|
|
||||||
for (_district_id, district) in &content.0.districts {
|
for district in content.0.districts.values() {
|
||||||
for pool in &district.monologue_pools {
|
for pool in &district.monologue_pools {
|
||||||
if pool.character != character {
|
if pool.character != character {
|
||||||
continue;
|
continue;
|
||||||
@@ -255,7 +268,7 @@ pub fn trigger_monologue(
|
|||||||
|
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
// All lines for this trigger have been shown; allow repeats
|
// All lines for this trigger have been shown; allow repeats
|
||||||
for (_district_id, district) in &content.0.districts {
|
for district in content.0.districts.values() {
|
||||||
for pool in &district.monologue_pools {
|
for pool in &district.monologue_pools {
|
||||||
if pool.character != character {
|
if pool.character != character {
|
||||||
continue;
|
continue;
|
||||||
@@ -688,7 +701,11 @@ mod tests {
|
|||||||
// All hardcoded v0.1 lines should have id prefix and non-empty text
|
// All hardcoded v0.1 lines should have id prefix and non-empty text
|
||||||
assert!(!ANOMALY_LINES.is_empty());
|
assert!(!ANOMALY_LINES.is_empty());
|
||||||
for (id, text) in ANOMALY_LINES {
|
for (id, text) in ANOMALY_LINES {
|
||||||
assert!(id.starts_with("sprint_anomaly_"), "id={} should start with sprint_anomaly_", id);
|
assert!(
|
||||||
|
id.starts_with("sprint_anomaly_"),
|
||||||
|
"id={} should start with sprint_anomaly_",
|
||||||
|
id
|
||||||
|
);
|
||||||
assert!(!text.is_empty(), "text for {} should be non-empty", id);
|
assert!(!text.is_empty(), "text for {} should be non-empty", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,10 +733,16 @@ mod tests {
|
|||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
|
|
||||||
let mut buf_query = world.query::<&MonologueBuffer>();
|
let mut buf_query = world.query::<&MonologueBuffer>();
|
||||||
assert!(buf_query.single(&world).unwrap().event.is_none(), "should not fire before delay");
|
assert!(
|
||||||
|
buf_query.single(&world).unwrap().event.is_none(),
|
||||||
|
"should not fire before delay"
|
||||||
|
);
|
||||||
|
|
||||||
let mut q_query = world.query::<&SprintAnomalyQueue>();
|
let mut q_query = world.query::<&SprintAnomalyQueue>();
|
||||||
assert!(q_query.single(&world).unwrap().has_pending(), "still pending before delay");
|
assert!(
|
||||||
|
q_query.single(&world).unwrap().has_pending(),
|
||||||
|
"still pending before delay"
|
||||||
|
);
|
||||||
|
|
||||||
// Tick 90: delay elapsed — should fire
|
// Tick 90: delay elapsed — should fire
|
||||||
world.resource_mut::<SimulationTime>().tick = ANOMALY_DELAY_TICKS;
|
world.resource_mut::<SimulationTime>().tick = ANOMALY_DELAY_TICKS;
|
||||||
@@ -734,7 +757,10 @@ mod tests {
|
|||||||
|
|
||||||
// Queue should be cleared
|
// Queue should be cleared
|
||||||
let mut q_query = world.query::<&SprintAnomalyQueue>();
|
let mut q_query = world.query::<&SprintAnomalyQueue>();
|
||||||
assert!(!q_query.single(&world).unwrap().has_pending(), "queue cleared after fire");
|
assert!(
|
||||||
|
!q_query.single(&world).unwrap().has_pending(),
|
||||||
|
"queue cleared after fire"
|
||||||
|
);
|
||||||
|
|
||||||
// last_fired_tick should be updated
|
// last_fired_tick should be updated
|
||||||
let mut state_query = world.query::<&MonologueState>();
|
let mut state_query = world.query::<&MonologueState>();
|
||||||
|
|||||||
@@ -261,7 +261,12 @@ pub struct MoveIntent {
|
|||||||
pub fn validate_movement(
|
pub fn validate_movement(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
walkability: Option<Res<WalkabilityMap>>,
|
walkability: Option<Res<WalkabilityMap>>,
|
||||||
mut movers: Query<(Entity, &MoveIntent, &mut TilePosition, Option<&TilePresence>)>,
|
mut movers: Query<(
|
||||||
|
Entity,
|
||||||
|
&MoveIntent,
|
||||||
|
&mut TilePosition,
|
||||||
|
Option<&TilePresence>,
|
||||||
|
)>,
|
||||||
stationary: Query<(Entity, &TilePosition, Option<&TilePresence>), Without<MoveIntent>>,
|
stationary: Query<(Entity, &TilePosition, Option<&TilePresence>), Without<MoveIntent>>,
|
||||||
) {
|
) {
|
||||||
let Some(map) = walkability else {
|
let Some(map) = walkability else {
|
||||||
@@ -290,12 +295,17 @@ pub fn validate_movement(
|
|||||||
} else if occupied.contains_key(&slot) {
|
} else if occupied.contains_key(&slot) {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"Entity {:?} blocked by entity at {:?} (layer {:?})",
|
"Entity {:?} blocked by entity at {:?} (layer {:?})",
|
||||||
entity, target, layer
|
entity,
|
||||||
|
target,
|
||||||
|
layer
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"Entity {:?} moving from {:?} to {:?} (layer {:?})",
|
"Entity {:?} moving from {:?} to {:?} (layer {:?})",
|
||||||
entity, *position, target, layer
|
entity,
|
||||||
|
*position,
|
||||||
|
target,
|
||||||
|
layer
|
||||||
);
|
);
|
||||||
// Free old layer slot, claim new one
|
// Free old layer slot, claim new one
|
||||||
occupied.remove(&(*position, layer));
|
occupied.remove(&(*position, layer));
|
||||||
|
|||||||
@@ -151,10 +151,7 @@ mod tests {
|
|||||||
Npc,
|
Npc,
|
||||||
TilePosition::new(0, 0, 0),
|
TilePosition::new(0, 0, 0),
|
||||||
ComputedPath {
|
ComputedPath {
|
||||||
steps: vec![
|
steps: vec![TilePosition::new(1, 0, 0), TilePosition::new(2, 0, 0)],
|
||||||
TilePosition::new(1, 0, 0),
|
|
||||||
TilePosition::new(2, 0, 0),
|
|
||||||
],
|
|
||||||
current_index: 0,
|
current_index: 0,
|
||||||
},
|
},
|
||||||
MovementSpeed::new(3),
|
MovementSpeed::new(3),
|
||||||
|
|||||||
@@ -102,7 +102,12 @@ pub fn compute_paths(
|
|||||||
Some((path, _cost)) => {
|
Some((path, _cost)) => {
|
||||||
// path includes start position; skip it
|
// path includes start position; skip it
|
||||||
let steps: Vec<TilePosition> = path.into_iter().skip(1).collect();
|
let steps: Vec<TilePosition> = path.into_iter().skip(1).collect();
|
||||||
tracing::trace!("Entity {:?}: path to {:?}, {} steps", entity, goal, steps.len());
|
tracing::trace!(
|
||||||
|
"Entity {:?}: path to {:?}, {} steps",
|
||||||
|
entity,
|
||||||
|
goal,
|
||||||
|
steps.len()
|
||||||
|
);
|
||||||
commands.entity(entity).insert(ComputedPath {
|
commands.entity(entity).insert(ComputedPath {
|
||||||
steps,
|
steps,
|
||||||
current_index: 0,
|
current_index: 0,
|
||||||
@@ -208,10 +213,7 @@ mod tests {
|
|||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
|
|
||||||
let entity = world
|
let entity = world
|
||||||
.spawn((
|
.spawn((TilePosition::new(5, 5, 0), PathRequest { goal }))
|
||||||
TilePosition::new(5, 5, 0),
|
|
||||||
PathRequest { goal },
|
|
||||||
))
|
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ mod tests {
|
|||||||
fn cooldown_stance_switch_mid_cooldown() {
|
fn cooldown_stance_switch_mid_cooldown() {
|
||||||
let mut cd = PlayerMoveCooldown::default();
|
let mut cd = PlayerMoveCooldown::default();
|
||||||
assert!(cd.try_move(MovementStance::Crouch)); // move at crouch speed
|
assert!(cd.try_move(MovementStance::Crouch)); // move at crouch speed
|
||||||
// Switch to sprint mid-cooldown
|
// Switch to sprint mid-cooldown
|
||||||
assert!(cd.try_move(MovementStance::Sprint)); // sprint allows every tick
|
assert!(cd.try_move(MovementStance::Sprint)); // sprint allows every tick
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,13 @@ mod tests {
|
|||||||
fn movement_profile_as_ecs_component() {
|
fn movement_profile_as_ecs_component() {
|
||||||
let mut world = bevy_ecs::world::World::new();
|
let mut world = bevy_ecs::world::World::new();
|
||||||
let profile = MovementProfile::smuggler();
|
let profile = MovementProfile::smuggler();
|
||||||
let entity = world.spawn((profile, profile.initial_stance(), PlayerMoveCooldown::default())).id();
|
let entity = world
|
||||||
|
.spawn((
|
||||||
|
profile,
|
||||||
|
profile.initial_stance(),
|
||||||
|
PlayerMoveCooldown::default(),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
let stored = world.get::<MovementProfile>(entity).unwrap();
|
let stored = world.get::<MovementProfile>(entity).unwrap();
|
||||||
assert_eq!(stored.default_stance, MovementStance::Walk);
|
assert_eq!(stored.default_stance, MovementStance::Walk);
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_to_minute_conversion() {
|
fn tick_to_minute_conversion() {
|
||||||
let time = SimulationTime { tick: 10, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick: 10,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(time.game_minutes(), 1);
|
assert_eq!(time.game_minutes(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,18 +126,30 @@ mod tests {
|
|||||||
fn day_phase_boundaries() {
|
fn day_phase_boundaries() {
|
||||||
let time = SimulationTime::default();
|
let time = SimulationTime::default();
|
||||||
assert_eq!(time.day_phase(), DayPhase::Morning);
|
assert_eq!(time.day_phase(), DayPhase::Morning);
|
||||||
let time = SimulationTime { tick: 360 * TICKS_PER_GAME_MINUTE, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick: 360 * TICKS_PER_GAME_MINUTE,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(time.day_phase(), DayPhase::Afternoon);
|
assert_eq!(time.day_phase(), DayPhase::Afternoon);
|
||||||
let time = SimulationTime { tick: 720 * TICKS_PER_GAME_MINUTE, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick: 720 * TICKS_PER_GAME_MINUTE,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(time.day_phase(), DayPhase::Evening);
|
assert_eq!(time.day_phase(), DayPhase::Evening);
|
||||||
let time = SimulationTime { tick: 1080 * TICKS_PER_GAME_MINUTE, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick: 1080 * TICKS_PER_GAME_MINUTE,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(time.day_phase(), DayPhase::Night);
|
assert_eq!(time.day_phase(), DayPhase::Night);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn paused_prevents_tick_advance() {
|
fn paused_prevents_tick_advance() {
|
||||||
let mut world = bevy_ecs::world::World::new();
|
let mut world = bevy_ecs::world::World::new();
|
||||||
world.insert_resource(SimulationTime { tick_rate: TickRate::Paused, ..Default::default() });
|
world.insert_resource(SimulationTime {
|
||||||
|
tick_rate: TickRate::Paused,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(advance_tick);
|
schedule.add_systems(advance_tick);
|
||||||
schedule.run(&mut world);
|
schedule.run(&mut world);
|
||||||
@@ -154,7 +169,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn half_rate_advances_every_two_frames() {
|
fn half_rate_advances_every_two_frames() {
|
||||||
let mut world = bevy_ecs::world::World::new();
|
let mut world = bevy_ecs::world::World::new();
|
||||||
world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() });
|
world.insert_resource(SimulationTime {
|
||||||
|
tick_rate: TickRate::Half,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(advance_tick);
|
schedule.add_systems(advance_tick);
|
||||||
|
|
||||||
@@ -179,15 +197,24 @@ mod tests {
|
|||||||
fn paused_helper_method() {
|
fn paused_helper_method() {
|
||||||
let time = SimulationTime::default();
|
let time = SimulationTime::default();
|
||||||
assert!(!time.paused());
|
assert!(!time.paused());
|
||||||
let time = SimulationTime { tick_rate: TickRate::Paused, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick_rate: TickRate::Paused,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(time.paused());
|
assert!(time.paused());
|
||||||
let time = SimulationTime { tick_rate: TickRate::Half, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick_rate: TickRate::Half,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert!(!time.paused());
|
assert!(!time.paused());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn day_wraparound_at_midnight() {
|
fn day_wraparound_at_midnight() {
|
||||||
let time = SimulationTime { tick: MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick: MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(time.day_phase(), DayPhase::Morning);
|
assert_eq!(time.day_phase(), DayPhase::Morning);
|
||||||
assert_eq!(time.time_of_day_minutes(), 0);
|
assert_eq!(time.time_of_day_minutes(), 0);
|
||||||
assert_eq!(time.day(), 1);
|
assert_eq!(time.day(), 1);
|
||||||
@@ -195,7 +222,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn day_calculation() {
|
fn day_calculation() {
|
||||||
let time = SimulationTime { tick: 3 * MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE + 100, ..Default::default() };
|
let time = SimulationTime {
|
||||||
|
tick: 3 * MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE + 100,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(time.day(), 3);
|
assert_eq!(time.day(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +233,10 @@ mod tests {
|
|||||||
fn tick_rate_switch_mid_accumulation() {
|
fn tick_rate_switch_mid_accumulation() {
|
||||||
// Half->Full with 0.5 remainder: Full should tick immediately (0.5 + 1.0 >= 1.0)
|
// Half->Full with 0.5 remainder: Full should tick immediately (0.5 + 1.0 >= 1.0)
|
||||||
let mut world = bevy_ecs::world::World::new();
|
let mut world = bevy_ecs::world::World::new();
|
||||||
world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() });
|
world.insert_resource(SimulationTime {
|
||||||
|
tick_rate: TickRate::Half,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(advance_tick);
|
schedule.add_systems(advance_tick);
|
||||||
|
|
||||||
@@ -233,7 +266,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn half_rate_no_drift_over_10000_frames() {
|
fn half_rate_no_drift_over_10000_frames() {
|
||||||
let mut world = bevy_ecs::world::World::new();
|
let mut world = bevy_ecs::world::World::new();
|
||||||
world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() });
|
world.insert_resource(SimulationTime {
|
||||||
|
tick_rate: TickRate::Half,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||||
schedule.add_systems(advance_tick);
|
schedule.add_systems(advance_tick);
|
||||||
|
|
||||||
|
|||||||
@@ -66,14 +66,20 @@ fn player_moves_north_through_full_pipeline() {
|
|||||||
rmp_serde::from_slice(&response).expect("deserialize snapshot");
|
rmp_serde::from_slice(&response).expect("deserialize snapshot");
|
||||||
|
|
||||||
// Snapshot captures state at end of tick 0 (before advance_tick increments to 1)
|
// Snapshot captures state at end of tick 0 (before advance_tick increments to 1)
|
||||||
assert_eq!(snapshot.version, 6);
|
assert_eq!(snapshot.version, PROTOCOL_VERSION);
|
||||||
assert_eq!(snapshot.tick, 0);
|
assert_eq!(snapshot.tick, 0);
|
||||||
assert_eq!(snapshot.entities.len(), 1);
|
assert_eq!(snapshot.entities.len(), 1);
|
||||||
|
|
||||||
// v2 fields populated
|
// v2 fields populated
|
||||||
assert_eq!(snapshot.game_time.day, 0);
|
assert_eq!(snapshot.game_time.day, 0);
|
||||||
assert_eq!(snapshot.game_time.day_phase, settled_reach_server::simulation::time::DayPhase::Morning);
|
assert_eq!(
|
||||||
assert_eq!(snapshot.game_time.tick_rate, settled_reach_server::simulation::time::TickRate::Full);
|
snapshot.game_time.day_phase,
|
||||||
|
settled_reach_server::simulation::time::DayPhase::Morning
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.game_time.tick_rate,
|
||||||
|
settled_reach_server::simulation::time::TickRate::Full
|
||||||
|
);
|
||||||
|
|
||||||
let player_entity = &snapshot.entities[0];
|
let player_entity = &snapshot.entities[0];
|
||||||
// Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0)
|
// Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0)
|
||||||
@@ -82,7 +88,10 @@ fn player_moves_north_through_full_pipeline() {
|
|||||||
assert_eq!(player_entity.y, 15.5);
|
assert_eq!(player_entity.y, 15.5);
|
||||||
assert_eq!(player_entity.z, 0);
|
assert_eq!(player_entity.z, 0);
|
||||||
assert!(matches!(player_entity.kind, EntityKind::Player));
|
assert!(matches!(player_entity.kind, EntityKind::Player));
|
||||||
assert!(matches!(player_entity.visibility, VisibilitySector::Forward));
|
assert!(matches!(
|
||||||
|
player_entity.visibility,
|
||||||
|
VisibilitySector::Forward
|
||||||
|
));
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
drop(reader);
|
drop(reader);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
//! Run with: cargo test --test shadowcast_bench -- --ignored --nocapture
|
//! Run with: cargo test --test shadowcast_bench -- --ignored --nocapture
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rand_chacha::ChaCha8Rng;
|
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use settled_reach_server::perception::shadowcast::{symmetric_shadowcast, recursive_shadowcast};
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
use settled_reach_server::perception::shadowcast::{recursive_shadowcast, symmetric_shadowcast};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -173,12 +173,14 @@ fn benchmark_symmetric_vs_recursive() {
|
|||||||
|
|
||||||
let results = bench_config(&config);
|
let results = bench_config(&config);
|
||||||
|
|
||||||
println!(" Symmetric: {:.2}ms total, {:.2}µs/call, {:.1} tiles avg",
|
println!(
|
||||||
|
" Symmetric: {:.2}ms total, {:.2}µs/call, {:.1} tiles avg",
|
||||||
results.symmetric_ms,
|
results.symmetric_ms,
|
||||||
results.symmetric_ms * 1000.0 / config.iterations as f64,
|
results.symmetric_ms * 1000.0 / config.iterations as f64,
|
||||||
results.symmetric_avg_tiles
|
results.symmetric_avg_tiles
|
||||||
);
|
);
|
||||||
println!(" Recursive: {:.2}ms total, {:.2}µs/call, {:.1} tiles avg",
|
println!(
|
||||||
|
" Recursive: {:.2}ms total, {:.2}µs/call, {:.1} tiles avg",
|
||||||
results.recursive_ms,
|
results.recursive_ms,
|
||||||
results.recursive_ms * 1000.0 / config.iterations as f64,
|
results.recursive_ms * 1000.0 / config.iterations as f64,
|
||||||
results.recursive_avg_tiles
|
results.recursive_avg_tiles
|
||||||
|
|||||||
Reference in New Issue
Block a user