refactor(server): decompose observer pipeline and fix interaction boundary
Extract visibility geometry into a separate system behind a PerceptionQuery trait, enabling D-017 perception mode swapping. Two-stage pipeline: compute_visibility_geometry writes to VisibilityGeometry resource, compute_observer_snapshot reads it. Remove KnowledgeGraph from compute_nearby_interactions (simulation phase boundary violation). Verb availability stays in simulation; POI-based priority adjustment moves to observer via apply_poi_verb_priority helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -145,18 +145,25 @@ impl Plugin for BridgePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<SnapshotBuffer>()
|
||||
.init_resource::<ServerRunning>()
|
||||
.init_resource::<crate::perception::query::VisibilityGeometry>()
|
||||
.init_resource::<crate::perception::query::ActivePerceptionMode>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
receive_bridge_inputs.before(crate::simulation::input::process_player_input),
|
||||
receive_bridge_inputs
|
||||
.before(crate::simulation::input::process_player_input),
|
||||
crate::perception::observer::compute_visibility_geometry
|
||||
.after(crate::simulation::movement::validate_movement),
|
||||
crate::simulation::interaction::compute_nearby_interactions
|
||||
.after(crate::simulation::movement::validate_movement),
|
||||
crate::perception::observer::compute_observer_snapshot
|
||||
.after(crate::simulation::movement::validate_movement)
|
||||
.after(crate::perception::observer::compute_visibility_geometry)
|
||||
.after(crate::simulation::interaction::compute_nearby_interactions)
|
||||
.before(crate::simulation::time::advance_tick),
|
||||
crate::perception::observation::emit_observation_events
|
||||
.after(crate::perception::observer::compute_observer_snapshot),
|
||||
send_bridge_snapshot
|
||||
.after(crate::perception::observer::compute_observer_snapshot)
|
||||
.after(crate::simulation::interaction::compute_nearby_interactions),
|
||||
.after(crate::perception::observer::compute_observer_snapshot),
|
||||
),
|
||||
);
|
||||
tracing::debug!("BridgePlugin initialized");
|
||||
|
||||
@@ -186,7 +186,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge::registry::EntityRegistry;
|
||||
use crate::npc::RoutineEntry;
|
||||
use crate::perception::observer::compute_observer_snapshot;
|
||||
use crate::perception::observer::{compute_observer_snapshot, compute_visibility_geometry};
|
||||
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
|
||||
use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::movement::WalkabilityMap;
|
||||
use crate::simulation::time::{DayPhase, MINUTES_PER_PHASE, TICKS_PER_GAME_MINUTE};
|
||||
@@ -199,16 +200,18 @@ mod tests {
|
||||
world.init_resource::<crate::knowledge::KnowledgeEventQueue>();
|
||||
world.init_resource::<EntityRegistry>();
|
||||
world.init_resource::<ObservationEventQueue>();
|
||||
world.init_resource::<VisibilityGeometry>();
|
||||
world.init_resource::<ActivePerceptionMode>();
|
||||
world
|
||||
}
|
||||
|
||||
/// Run the observation pipeline: snapshot -> emit -> interpret -> knowledge update.
|
||||
/// Interpretation runs BEFORE knowledge updates so it can detect new entities
|
||||
/// and compare against the PREVIOUS tick's knowledge state.
|
||||
/// Run the observation pipeline: geometry -> snapshot -> emit -> interpret -> knowledge.
|
||||
fn run_pipeline(world: &mut World) {
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((
|
||||
compute_observer_snapshot,
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot
|
||||
.after(compute_visibility_geometry),
|
||||
crate::perception::observation::emit_observation_events
|
||||
.after(compute_observer_snapshot),
|
||||
generate_observation_events
|
||||
|
||||
@@ -8,6 +8,7 @@ use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||
pub mod interpretation;
|
||||
pub mod observation;
|
||||
pub mod observer;
|
||||
pub mod query;
|
||||
pub mod shadowcast;
|
||||
pub mod vision_cone;
|
||||
|
||||
@@ -18,6 +19,8 @@ pub struct PerceptionPlugin;
|
||||
impl Plugin for PerceptionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<interpretation::ObservationEventQueue>()
|
||||
.init_resource::<query::VisibilityGeometry>()
|
||||
.init_resource::<query::ActivePerceptionMode>()
|
||||
.add_systems(
|
||||
Update,
|
||||
interpretation::generate_observation_events
|
||||
|
||||
@@ -90,7 +90,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
|
||||
use crate::npc::Npc;
|
||||
use crate::perception::observer::compute_observer_snapshot;
|
||||
use crate::perception::observer::{compute_observer_snapshot, compute_visibility_geometry};
|
||||
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
|
||||
use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::movement::WalkabilityMap;
|
||||
|
||||
@@ -101,6 +102,8 @@ mod tests {
|
||||
world.init_resource::<SnapshotBuffer>();
|
||||
world.init_resource::<KnowledgeEventQueue>();
|
||||
world.init_resource::<EntityRegistry>();
|
||||
world.init_resource::<VisibilityGeometry>();
|
||||
world.init_resource::<ActivePerceptionMode>();
|
||||
world
|
||||
}
|
||||
|
||||
@@ -129,7 +132,11 @@ mod tests {
|
||||
|
||||
// First: compute snapshot so NPC is visible
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((compute_observer_snapshot, emit_observation_events).chain());
|
||||
schedule.add_systems((
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot.after(compute_visibility_geometry),
|
||||
emit_observation_events.after(compute_observer_snapshot),
|
||||
));
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<KnowledgeEventQueue>();
|
||||
@@ -171,7 +178,11 @@ mod tests {
|
||||
walkability.set_walkable(&TilePosition::new(16, 15, 0), false);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((compute_observer_snapshot, emit_observation_events).chain());
|
||||
schedule.add_systems((
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot.after(compute_visibility_geometry),
|
||||
emit_observation_events.after(compute_observer_snapshot),
|
||||
));
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<KnowledgeEventQueue>();
|
||||
@@ -203,7 +214,11 @@ mod tests {
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((compute_observer_snapshot, emit_observation_events).chain());
|
||||
schedule.add_systems((
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot.after(compute_visibility_geometry),
|
||||
emit_observation_events.after(compute_observer_snapshot),
|
||||
));
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<KnowledgeEventQueue>();
|
||||
|
||||
@@ -1,27 +1,52 @@
|
||||
//! Observer visibility query system (#112)
|
||||
//!
|
||||
//! Replaces the unfiltered `generate_snapshot` with a visibility-aware version.
|
||||
//! Combines shadowcasting + vision cone to determine what the observer can see,
|
||||
//! then populates ObserverSnapshot v2 with only visible entities and tiles.
|
||||
//! Two-stage pipeline:
|
||||
//! 1. compute_visibility_geometry — FOV + vision cone → VisibilityGeometry resource
|
||||
//! 2. compute_observer_snapshot — entity filtering + knowledge overlay → ObserverSnapshot
|
||||
//!
|
||||
//! D-017 perception modes swap the geometry producer via PerceptionQuery trait.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::bridge::types::*;
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
|
||||
use crate::perception::shadowcast::compute_fov;
|
||||
use crate::perception::vision_cone::{apply_vision_cone, Facing, VisionConeConfig};
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph, StableId};
|
||||
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
|
||||
use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::interaction::NearbyInteractionBuffer;
|
||||
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
||||
use crate::simulation::time::SimulationTime;
|
||||
|
||||
/// Compute observer snapshot with LOS filtering and vision cone.
|
||||
/// Compute visibility geometry using the active perception mode.
|
||||
/// Stage 1 of the observer pipeline: FOV + vision cone → VisibilityGeometry.
|
||||
///
|
||||
/// System ordering: after validate_movement, before advance_tick.
|
||||
/// Replaces bridge::generate_snapshot.
|
||||
/// System ordering: after validate_movement, before compute_observer_snapshot.
|
||||
pub fn compute_visibility_geometry(
|
||||
walkability: Res<WalkabilityMap>,
|
||||
mode: Res<ActivePerceptionMode>,
|
||||
observer_query: Query<(&TilePosition, Option<&Facing>), With<PlayerCharacter>>,
|
||||
mut geometry: ResMut<VisibilityGeometry>,
|
||||
) {
|
||||
let Ok((observer_pos, facing_opt)) = observer_query.single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let facing = facing_opt
|
||||
.map(|f| f.0)
|
||||
.unwrap_or(FacingDirection::default());
|
||||
|
||||
*geometry = mode.0.compute_geometry(observer_pos, facing, &walkability);
|
||||
}
|
||||
|
||||
/// Assemble observer snapshot from precomputed geometry and entity state.
|
||||
/// Stage 2 of the observer pipeline: entity filtering + knowledge overlay → snapshot.
|
||||
///
|
||||
/// System ordering: after compute_visibility_geometry + compute_nearby_interactions,
|
||||
/// before advance_tick.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn compute_observer_snapshot(
|
||||
time: Res<SimulationTime>,
|
||||
walkability: Res<WalkabilityMap>,
|
||||
geometry: Res<VisibilityGeometry>,
|
||||
registry: Res<EntityRegistry>,
|
||||
mut observer_query: Query<
|
||||
(&TilePosition, Option<&Facing>, &KnowledgeGraph, &mut NearbyInteractionBuffer),
|
||||
@@ -35,7 +60,7 @@ pub fn compute_observer_snapshot(
|
||||
)>,
|
||||
mut buffer: ResMut<SnapshotBuffer>,
|
||||
) {
|
||||
let Ok((observer_pos, facing_opt, observer_kg, mut interaction_buffer)) =
|
||||
let Ok((_observer_pos, facing_opt, observer_kg, mut interaction_buffer)) =
|
||||
observer_query.single_mut()
|
||||
else {
|
||||
return;
|
||||
@@ -45,54 +70,70 @@ pub fn compute_observer_snapshot(
|
||||
.map(|f| f.0)
|
||||
.unwrap_or(FacingDirection::default());
|
||||
|
||||
let config = VisionConeConfig::default();
|
||||
let z = observer_pos.z;
|
||||
let (mut entities, visible_ids) =
|
||||
filter_visible_entities(&geometry, ®istry, observer_kg, &all_entities);
|
||||
|
||||
// Step 1: Compute raw FOV using symmetric shadowcasting
|
||||
let fov = compute_fov(
|
||||
|x, y| !walkability.can_move_to(&TilePosition::new(x, y, z)),
|
||||
observer_pos.x,
|
||||
observer_pos.y,
|
||||
config.forward_range,
|
||||
z,
|
||||
collect_remembered_entities(
|
||||
observer_kg,
|
||||
&visible_ids,
|
||||
&geometry.visible_positions,
|
||||
geometry.observer_z,
|
||||
time.tick,
|
||||
&mut entities,
|
||||
);
|
||||
|
||||
// Step 2: Apply vision cone to get sector-tagged tiles
|
||||
let cone_tiles =
|
||||
apply_vision_cone(&fov, observer_pos.x, observer_pos.y, facing, &config);
|
||||
let game_time = GameTime {
|
||||
day: time.day(),
|
||||
time_of_day: time.time_of_day_minutes(),
|
||||
day_phase: time.day_phase(),
|
||||
tick_rate: time.tick_rate,
|
||||
};
|
||||
|
||||
// Step 3: Build visible_tiles for the snapshot
|
||||
let visible_tiles: Vec<VisibleTile> = cone_tiles
|
||||
.iter()
|
||||
.map(|&(x, y, sector)| VisibleTile {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
visibility: sector,
|
||||
})
|
||||
.collect();
|
||||
// Take interactions and adjust POI verb priority (D-060)
|
||||
let mut nearby_interactions = interaction_buffer.take();
|
||||
apply_poi_verb_priority(&mut nearby_interactions, observer_kg);
|
||||
|
||||
// Step 4: Build lookup set for fast entity visibility check
|
||||
let visible_positions: HashSet<(i32, i32)> =
|
||||
cone_tiles.iter().map(|&(x, y, _)| (x, y)).collect();
|
||||
tracing::trace!(
|
||||
"compute_observer_snapshot: tick={}, visible={}, remembered={}, tiles={}",
|
||||
time.tick,
|
||||
visible_ids.len(),
|
||||
entities.len() - visible_ids.len(),
|
||||
geometry.visible_tiles.len(),
|
||||
);
|
||||
|
||||
// Build sector lookup (position -> sector)
|
||||
let sector_lookup: std::collections::HashMap<(i32, i32), VisibilitySector> = cone_tiles
|
||||
.iter()
|
||||
.map(|&(x, y, sector)| ((x, y), sector))
|
||||
.collect();
|
||||
buffer.snapshot = Some(ObserverSnapshot {
|
||||
version: crate::bridge::types::PROTOCOL_VERSION,
|
||||
tick: time.tick,
|
||||
game_time,
|
||||
player_facing: facing,
|
||||
entities,
|
||||
visible_tiles: geometry.visible_tiles.clone(),
|
||||
nearby_interactions,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5: Filter entities by visibility, overlay knowledge
|
||||
/// Filter entities by visibility using precomputed geometry.
|
||||
/// Returns (visible entities, set of visible wire IDs).
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn filter_visible_entities(
|
||||
geometry: &VisibilityGeometry,
|
||||
registry: &EntityRegistry,
|
||||
observer_kg: &KnowledgeGraph,
|
||||
all_entities: &Query<(
|
||||
Entity,
|
||||
&TilePosition,
|
||||
Option<&PlayerCharacter>,
|
||||
Option<&crate::npc::Npc>,
|
||||
)>,
|
||||
) -> (Vec<VisibleEntity>, HashSet<u64>) {
|
||||
let mut entities = Vec::new();
|
||||
let mut visible_entity_bits: HashSet<u64> = HashSet::new();
|
||||
let mut visible_ids: HashSet<u64> = HashSet::new();
|
||||
|
||||
for (entity, pos, is_player, is_npc) in all_entities.iter() {
|
||||
// Different z-level: not visible
|
||||
if pos.z != z {
|
||||
if pos.z != geometry.observer_z {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not in visible tile set: not visible
|
||||
if !visible_positions.contains(&(pos.x, pos.y)) {
|
||||
if !geometry.visible_positions.contains(&(pos.x, pos.y)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -105,12 +146,12 @@ pub fn compute_observer_snapshot(
|
||||
EntityKind::Object
|
||||
};
|
||||
|
||||
let sector = sector_lookup
|
||||
let sector = geometry
|
||||
.sector_lookup
|
||||
.get(&(pos.x, pos.y))
|
||||
.copied()
|
||||
.unwrap_or(VisibilitySector::Peripheral);
|
||||
|
||||
// Look up relationship from knowledge graph (D-033 entity color)
|
||||
let relationship = if is_player.is_some() {
|
||||
RelationshipState::Known // Self
|
||||
} else if let Some(stable_id) = registry.to_stable(entity) {
|
||||
@@ -129,7 +170,7 @@ pub fn compute_observer_snapshot(
|
||||
tracing::error!(?entity, "entity visible but not in EntityRegistry");
|
||||
entity.to_bits()
|
||||
});
|
||||
visible_entity_bits.insert(wire_id);
|
||||
visible_ids.insert(wire_id);
|
||||
entities.push(VisibleEntity {
|
||||
entity_id: wire_id,
|
||||
x: rx,
|
||||
@@ -142,42 +183,7 @@ pub fn compute_observer_snapshot(
|
||||
});
|
||||
}
|
||||
|
||||
// Step 6: Add remembered entities from knowledge graph (#366)
|
||||
collect_remembered_entities(
|
||||
observer_kg,
|
||||
&visible_entity_bits,
|
||||
&visible_positions,
|
||||
z,
|
||||
time.tick,
|
||||
&mut entities,
|
||||
);
|
||||
|
||||
// Step 7: Build GameTime from SimulationTime
|
||||
let game_time = GameTime {
|
||||
day: time.day(),
|
||||
time_of_day: time.time_of_day_minutes(),
|
||||
day_phase: time.day_phase(),
|
||||
tick_rate: time.tick_rate,
|
||||
};
|
||||
|
||||
tracing::trace!(
|
||||
"compute_observer_snapshot: tick={}, visible={}, remembered={}, tiles={}",
|
||||
time.tick,
|
||||
visible_entity_bits.len(),
|
||||
entities.len() - visible_entity_bits.len(),
|
||||
visible_tiles.len(),
|
||||
);
|
||||
|
||||
// Step 8: Assemble snapshot (v4: added nearby_interactions)
|
||||
buffer.snapshot = Some(ObserverSnapshot {
|
||||
version: crate::bridge::types::PROTOCOL_VERSION,
|
||||
tick: time.tick,
|
||||
game_time,
|
||||
player_facing: facing,
|
||||
entities,
|
||||
visible_tiles,
|
||||
nearby_interactions: interaction_buffer.take(),
|
||||
});
|
||||
(entities, visible_ids)
|
||||
}
|
||||
|
||||
/// Collect remembered entities from the knowledge graph — entities the observer
|
||||
@@ -240,5 +246,28 @@ fn collect_remembered_entities(
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust verb priority for PersonOfInterest NPCs (D-060).
|
||||
/// Moves ExamineNpc to priority 1 and Talk to priority 2 when the observer
|
||||
/// knows the entity as POI. Called after interaction buffer is taken.
|
||||
fn apply_poi_verb_priority(
|
||||
interactions: &mut [NearbyInteraction],
|
||||
observer_kg: &KnowledgeGraph,
|
||||
) {
|
||||
for interaction in interactions.iter_mut() {
|
||||
let stable_id = StableId(interaction.entity_id);
|
||||
let relationship = observer_kg.relationship_with(&stable_id);
|
||||
if relationship == RelationshipState::PersonOfInterest {
|
||||
for verb in &mut interaction.verbs {
|
||||
match verb.kind {
|
||||
VerbKind::ExamineNpc => verb.priority = 1,
|
||||
VerbKind::Talk => verb.priority = 2,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
interaction.verbs.sort_by_key(|v| (v.priority, v.kind as u8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
use super::*;
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
|
||||
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
|
||||
use crate::perception::vision_cone::Facing;
|
||||
use bevy_ecs::world::World;
|
||||
|
||||
/// Helper: set up a test world with player, walkability map, and knowledge resources
|
||||
/// Helper: set up a test world with resources for the two-stage observer pipeline.
|
||||
fn setup_world(width: i32, height: i32) -> World {
|
||||
let mut world = World::new();
|
||||
world.insert_resource(SimulationTime::default());
|
||||
world.insert_resource(WalkabilityMap::new(width, height, 1));
|
||||
world.init_resource::<SnapshotBuffer>();
|
||||
world.init_resource::<EntityRegistry>();
|
||||
world.init_resource::<VisibilityGeometry>();
|
||||
world.init_resource::<ActivePerceptionMode>();
|
||||
world
|
||||
}
|
||||
|
||||
/// Run the two-stage observer pipeline: geometry + snapshot.
|
||||
fn run_observer_pipeline(world: &mut World) {
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot.after(compute_visibility_geometry),
|
||||
));
|
||||
schedule.run(world);
|
||||
}
|
||||
|
||||
/// Run the full pipeline including interaction system.
|
||||
fn run_full_pipeline(world: &mut World) {
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((
|
||||
crate::simulation::interaction::compute_nearby_interactions,
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot
|
||||
.after(compute_visibility_geometry)
|
||||
.after(crate::simulation::interaction::compute_nearby_interactions),
|
||||
));
|
||||
schedule.run(world);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_always_visible_in_snapshot() {
|
||||
let mut world = setup_world(32, 32);
|
||||
@@ -24,9 +50,7 @@ fn player_always_visible_in_snapshot() {
|
||||
NearbyInteractionBuffer::default(),
|
||||
));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().expect("snapshot should exist");
|
||||
@@ -49,9 +73,7 @@ fn npc_in_los_visible() {
|
||||
// NPC directly north of player (in forward cone)
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 14, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -81,9 +103,7 @@ fn npc_behind_wall_not_visible() {
|
||||
// NPC behind the wall
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 12, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -109,9 +129,7 @@ fn npc_behind_player_not_visible() {
|
||||
// NPC far behind player (south, in blind spot)
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 26, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -136,9 +154,7 @@ fn different_z_level_not_visible() {
|
||||
// NPC on different z-level
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 14, 1)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -165,9 +181,7 @@ fn game_time_populated() {
|
||||
NearbyInteractionBuffer::default(),
|
||||
));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -190,9 +204,7 @@ fn visible_tiles_populated() {
|
||||
NearbyInteractionBuffer::default(),
|
||||
));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -233,9 +245,7 @@ fn visible_npc_has_relationship_from_knowledge() {
|
||||
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -279,9 +289,7 @@ fn remembered_entity_appears_as_ghost() {
|
||||
world.insert_resource(registry);
|
||||
world.insert_resource({ let mut t = SimulationTime::default(); t.tick = 100; t });
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -338,9 +346,7 @@ fn direct_confidence_not_shown_as_remembered() {
|
||||
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -385,9 +391,7 @@ fn remembered_entity_on_visible_tile_not_shown() {
|
||||
registry.register(player);
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -431,9 +435,7 @@ fn remembered_entity_different_z_not_shown() {
|
||||
registry.register(player);
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -486,9 +488,7 @@ fn knowledge_without_position_not_shown() {
|
||||
registry.register(player);
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -519,9 +519,7 @@ fn multiple_npcs_in_los_all_visible() {
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(14, 14, 0)));
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(18, 14, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -558,9 +556,7 @@ fn npc_behind_wall_excluded_from_multi_entity_snapshot() {
|
||||
// NPC 3: also visible
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(18, 15, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
run_observer_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
@@ -572,3 +568,50 @@ fn npc_behind_wall_excluded_from_multi_entity_snapshot() {
|
||||
.collect();
|
||||
assert_eq!(npcs.len(), 2, "NPC behind wall should be excluded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poi_interaction_gets_observe_first_priority() {
|
||||
let mut world = setup_world(32, 32);
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
// NPC in close range, directly north of player and in LOS
|
||||
let npc = world
|
||||
.spawn((
|
||||
crate::npc::Npc,
|
||||
TilePosition::new(16, 15, 0),
|
||||
crate::simulation::interaction::Interactable,
|
||||
))
|
||||
.id();
|
||||
let npc_sid = registry.register(npc);
|
||||
|
||||
// Player knows NPC as PersonOfInterest
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(npc_sid, TilePosition::new(16, 15, 0), 50);
|
||||
kg.set_relationship(&npc_sid, RelationshipState::PersonOfInterest);
|
||||
|
||||
let player = world
|
||||
.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
kg,
|
||||
NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
world.insert_resource(registry);
|
||||
|
||||
// Run full pipeline: interaction computes default priority,
|
||||
// then observer applies POI adjustment
|
||||
run_full_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
// POI: Observe takes priority over Talk
|
||||
assert_eq!(interaction.verbs[0].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(interaction.verbs[0].priority, 1);
|
||||
assert_eq!(interaction.verbs[1].kind, VerbKind::Talk);
|
||||
assert_eq!(interaction.verbs[1].priority, 2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Perception query trait (D-017).
|
||||
//!
|
||||
//! Abstraction for perception mode geometry computation. Each mode
|
||||
//! (natural vision, thermal, EM, etc.) implements PerceptionQuery to
|
||||
//! provide mode-specific FOV and visibility sector computation.
|
||||
//! v0.1 implements only NaturalVision.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::bridge::types::{FacingDirection, VisibilitySector, VisibleTile};
|
||||
use crate::perception::shadowcast::compute_fov;
|
||||
use crate::perception::vision_cone::{apply_vision_cone, VisionConeConfig};
|
||||
use crate::simulation::movement::{TilePosition, WalkabilityMap};
|
||||
|
||||
/// Cached FOV geometry for the current frame. Produced by
|
||||
/// compute_visibility_geometry, consumed by compute_observer_snapshot.
|
||||
/// D-017 perception modes swap the geometry producer while the consumer
|
||||
/// remains unchanged.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct VisibilityGeometry {
|
||||
pub visible_tiles: Vec<VisibleTile>,
|
||||
pub visible_positions: HashSet<(i32, i32)>,
|
||||
pub sector_lookup: HashMap<(i32, i32), VisibilitySector>,
|
||||
pub observer_z: i32,
|
||||
}
|
||||
|
||||
/// Trait for perception mode geometry computation (D-017).
|
||||
///
|
||||
/// Each perception mode implements this to produce a VisibilityGeometry
|
||||
/// from the observer's position and facing. v0.1 only implements
|
||||
/// NaturalVision; D-017 adds Thermal, EM, etc.
|
||||
pub trait PerceptionQuery: Send + Sync {
|
||||
fn compute_geometry(
|
||||
&self,
|
||||
observer_pos: &TilePosition,
|
||||
facing: FacingDirection,
|
||||
walkability: &WalkabilityMap,
|
||||
) -> VisibilityGeometry;
|
||||
}
|
||||
|
||||
/// Natural vision — default perception mode.
|
||||
/// Uses symmetric shadowcasting (D-011) + directional vision cone (D-015).
|
||||
pub struct NaturalVision;
|
||||
|
||||
impl PerceptionQuery for NaturalVision {
|
||||
fn compute_geometry(
|
||||
&self,
|
||||
observer_pos: &TilePosition,
|
||||
facing: FacingDirection,
|
||||
walkability: &WalkabilityMap,
|
||||
) -> VisibilityGeometry {
|
||||
let config = VisionConeConfig::default();
|
||||
let z = observer_pos.z;
|
||||
|
||||
let fov = compute_fov(
|
||||
|x, y| !walkability.can_move_to(&TilePosition::new(x, y, z)),
|
||||
observer_pos.x,
|
||||
observer_pos.y,
|
||||
config.forward_range,
|
||||
z,
|
||||
);
|
||||
|
||||
let cone_tiles =
|
||||
apply_vision_cone(&fov, observer_pos.x, observer_pos.y, facing, &config);
|
||||
|
||||
let visible_tiles = cone_tiles
|
||||
.iter()
|
||||
.map(|&(x, y, sector)| VisibleTile {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
visibility: sector,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let visible_positions = cone_tiles.iter().map(|&(x, y, _)| (x, y)).collect();
|
||||
|
||||
let sector_lookup = cone_tiles
|
||||
.iter()
|
||||
.map(|&(x, y, sector)| ((x, y), sector))
|
||||
.collect();
|
||||
|
||||
VisibilityGeometry {
|
||||
visible_tiles,
|
||||
visible_positions,
|
||||
sector_lookup,
|
||||
observer_z: z,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource wrapping the active perception mode (D-017).
|
||||
/// Defaults to NaturalVision. Swap this resource to change perception modes.
|
||||
#[derive(Resource)]
|
||||
pub struct ActivePerceptionMode(pub Box<dyn PerceptionQuery>);
|
||||
|
||||
impl Default for ActivePerceptionMode {
|
||||
fn default() -> Self {
|
||||
Self(Box::new(NaturalVision))
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,14 @@
|
||||
// Implements #404: server-side verb computation for context-sensitive [E] key
|
||||
// Spec: docs/design/interaction-verbs-v0.1.md
|
||||
// D-060: actions[] renamed to verbs[] across all surfaces
|
||||
//
|
||||
// Phase boundary: this system determines verb AVAILABILITY based on proximity
|
||||
// and entity type only. Verb PRIORITY adjustment (e.g. POI flipping Observe
|
||||
// above Talk) is a perception concern handled by the observer system.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use crate::bridge::types::{EntityKind, NearbyInteraction, VerbKind, VerbOption};
|
||||
use crate::knowledge::types::RelationshipState;
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
|
||||
use crate::knowledge::EntityRegistry;
|
||||
use crate::npc::Npc;
|
||||
use crate::simulation::movement::{PlayerCharacter, TilePosition};
|
||||
|
||||
@@ -23,13 +26,14 @@ pub struct Interactable;
|
||||
/// For each entity in range, determines available verbs sorted by priority.
|
||||
/// Results are written to the NearbyInteractionBuffer for inclusion in ObserverSnapshot.
|
||||
///
|
||||
/// NOTE: Checks proximity only, not line-of-sight. The client filters
|
||||
/// interaction prompts against visible entities. Server-side LOS filtering
|
||||
/// is deferred until the interaction system can read the observer's visible set.
|
||||
/// NOTE: Determines verb availability and default priority only. Relationship-based
|
||||
/// priority adjustment (e.g. POI → Observe first) is applied by the observer
|
||||
/// system after taking the buffer. This keeps the simulation phase free of
|
||||
/// knowledge graph dependencies (D-010 phase boundary).
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn compute_nearby_interactions(
|
||||
mut player_query: Query<
|
||||
(&TilePosition, &KnowledgeGraph, &mut NearbyInteractionBuffer),
|
||||
(&TilePosition, &mut NearbyInteractionBuffer),
|
||||
With<PlayerCharacter>,
|
||||
>,
|
||||
registry: Res<EntityRegistry>,
|
||||
@@ -38,7 +42,7 @@ pub fn compute_nearby_interactions(
|
||||
(With<Interactable>, Without<PlayerCharacter>),
|
||||
>,
|
||||
) {
|
||||
let Ok((player_pos, knowledge, mut buffer)) = player_query.single_mut() else {
|
||||
let Ok((player_pos, mut buffer)) = player_query.single_mut() else {
|
||||
return;
|
||||
};
|
||||
buffer.interactions.clear();
|
||||
@@ -58,50 +62,26 @@ pub fn compute_nearby_interactions(
|
||||
EntityKind::Object
|
||||
};
|
||||
|
||||
// Look up relationship state from knowledge graph
|
||||
let relationship = if let Some(stable_id) = registry.to_stable(entity) {
|
||||
knowledge.relationship_with(&stable_id)
|
||||
} else {
|
||||
RelationshipState::Unknown
|
||||
};
|
||||
|
||||
let is_poi = relationship == RelationshipState::PersonOfInterest;
|
||||
let is_close = distance <= CLOSE_RANGE;
|
||||
|
||||
let mut verbs = Vec::new();
|
||||
|
||||
match entity_type {
|
||||
EntityKind::Npc => {
|
||||
if is_close {
|
||||
if is_poi {
|
||||
// Post-contradiction: Observe takes priority over Talk
|
||||
verbs.push(VerbOption {
|
||||
kind: VerbKind::ExamineNpc,
|
||||
label: "Observe".into(),
|
||||
priority: 1,
|
||||
available: true,
|
||||
});
|
||||
verbs.push(VerbOption {
|
||||
kind: VerbKind::Talk,
|
||||
label: "Talk".into(),
|
||||
priority: 2,
|
||||
available: true,
|
||||
});
|
||||
} else {
|
||||
// Default: Talk takes priority
|
||||
verbs.push(VerbOption {
|
||||
kind: VerbKind::Talk,
|
||||
label: "Talk".into(),
|
||||
priority: 1,
|
||||
available: true,
|
||||
});
|
||||
verbs.push(VerbOption {
|
||||
kind: VerbKind::ExamineNpc,
|
||||
label: "Observe".into(),
|
||||
priority: 2,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
// Default priority: Talk first, Observe second.
|
||||
// Observer adjusts priority for POI entities.
|
||||
verbs.push(VerbOption {
|
||||
kind: VerbKind::Talk,
|
||||
label: "Talk".into(),
|
||||
priority: 1,
|
||||
available: true,
|
||||
});
|
||||
verbs.push(VerbOption {
|
||||
kind: VerbKind::ExamineNpc,
|
||||
label: "Observe".into(),
|
||||
priority: 2,
|
||||
available: true,
|
||||
});
|
||||
} else {
|
||||
// Mid range: only Examine NPC (Talk requires close range)
|
||||
verbs.push(VerbOption {
|
||||
@@ -187,13 +167,13 @@ mod tests {
|
||||
world
|
||||
}
|
||||
|
||||
/// Spawn player with standard components + NearbyInteractionBuffer
|
||||
/// Spawn player with standard components (no KnowledgeGraph — interaction
|
||||
/// system doesn't access it; POI priority is handled by observer).
|
||||
fn spawn_player(world: &mut World, x: i32, y: i32) -> Entity {
|
||||
world
|
||||
.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(x, y, 0),
|
||||
KnowledgeGraph::new(),
|
||||
NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id()
|
||||
@@ -219,7 +199,9 @@ mod tests {
|
||||
assert_eq!(buffer.interactions.len(), 1);
|
||||
assert_eq!(buffer.interactions[0].verbs.len(), 2);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::Talk);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].priority, 1);
|
||||
assert_eq!(buffer.interactions[0].verbs[1].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(buffer.interactions[0].verbs[1].priority, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -236,6 +218,7 @@ mod tests {
|
||||
assert_eq!(buffer.interactions.len(), 1);
|
||||
assert_eq!(buffer.interactions[0].verbs.len(), 1);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].priority, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -252,38 +235,6 @@ mod tests {
|
||||
assert!(buffer.interactions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poi_npc_observe_takes_priority() {
|
||||
let mut world = setup_world();
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
let npc = world
|
||||
.spawn((Npc, TilePosition::new(5, 6, 0), Interactable))
|
||||
.id();
|
||||
let npc_sid = registry.register(npc);
|
||||
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(npc_sid, TilePosition::new(5, 6, 0), 50);
|
||||
kg.set_relationship(&npc_sid, RelationshipState::PersonOfInterest);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(5, 5, 0),
|
||||
kg,
|
||||
NearbyInteractionBuffer::default(),
|
||||
));
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_nearby_interactions);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let buffer = read_buffer(&mut world);
|
||||
assert_eq!(buffer.interactions.len(), 1);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(buffer.interactions[0].verbs[1].kind, VerbKind::Talk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_in_close_range_gets_examine() {
|
||||
let mut world = setup_world();
|
||||
@@ -344,39 +295,6 @@ mod tests {
|
||||
assert!(buffer.interactions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poi_npc_at_mid_range_gets_observe_only() {
|
||||
let mut world = setup_world();
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
let npc = world
|
||||
.spawn((Npc, TilePosition::new(5, 9, 0), Interactable))
|
||||
.id();
|
||||
let npc_sid = registry.register(npc);
|
||||
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(npc_sid, TilePosition::new(5, 9, 0), 50);
|
||||
kg.set_relationship(&npc_sid, RelationshipState::PersonOfInterest);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(5, 5, 0),
|
||||
kg,
|
||||
NearbyInteractionBuffer::default(),
|
||||
));
|
||||
world.insert_resource(registry);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_nearby_interactions);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let buffer = read_buffer(&mut world);
|
||||
assert_eq!(buffer.interactions.len(), 1);
|
||||
assert_eq!(buffer.interactions[0].verbs.len(), 1);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(buffer.interactions[0].verbs[0].priority, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equidistant_npcs_sorted_deterministically() {
|
||||
let mut world = setup_world();
|
||||
|
||||
@@ -31,12 +31,9 @@ impl Plugin for SimulationPlugin {
|
||||
pathfinding::compute_paths.after(input::process_player_input),
|
||||
path_follow::follow_paths.after(pathfinding::compute_paths),
|
||||
movement::validate_movement.after(path_follow::follow_paths),
|
||||
interaction::compute_nearby_interactions
|
||||
.after(movement::validate_movement),
|
||||
path_follow::cleanup_path_blocked.after(movement::validate_movement),
|
||||
time::advance_tick
|
||||
.after(path_follow::cleanup_path_blocked)
|
||||
.after(interaction::compute_nearby_interactions),
|
||||
.after(path_follow::cleanup_path_blocked),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user