feat(simulation): wire Sprint 15 systems + protocol v13
Register all new systems in NpcPlugin and SimulationPlugin with correct ordering constraints. Protocol bumped to v13: tell_state on VisibleEntity, follow_state and PostConversationQueue on ObserverSnapshot, Follow verb on VerbKind. Observer snapshot populates tell state from DerivedTellState and follow state from FollowTarget. System ordering: tolerance after mood, deviation after activity, tell after mood+deviation, follow after visibility geometry, event monologue after conversations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -1092,7 +1092,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "settled-reach-server"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_ecs",
|
||||
|
||||
@@ -173,6 +173,11 @@ impl Plugin for BridgePlugin {
|
||||
.after(crate::perception::anomaly::detect_anomalies),
|
||||
crate::simulation::monologue::process_sprint_anomaly_monologue
|
||||
.after(crate::simulation::monologue::trigger_recognition_monologue),
|
||||
crate::simulation::monologue::trigger_event_monologue
|
||||
.after(crate::simulation::monologue::process_sprint_anomaly_monologue)
|
||||
.after(crate::simulation::sound::collect_sound_events)
|
||||
.after(crate::simulation::conversation::run_npc_conversations)
|
||||
.after(crate::simulation::dialogue::process_walk_away),
|
||||
crate::simulation::dialogue::process_talk_interaction
|
||||
.after(crate::simulation::input::process_player_input),
|
||||
crate::simulation::dialogue::process_walk_away
|
||||
@@ -180,10 +185,14 @@ impl Plugin for BridgePlugin {
|
||||
.after(crate::simulation::dialogue::process_talk_interaction),
|
||||
crate::simulation::dialogue::process_confrontation_response
|
||||
.after(crate::simulation::input::process_player_input),
|
||||
crate::simulation::follow::update_follow_state
|
||||
.after(crate::perception::observer::compute_visibility_geometry)
|
||||
.after(crate::simulation::movement::validate_movement)
|
||||
.before(crate::perception::observer::compute_observer_snapshot),
|
||||
crate::perception::observer::compute_observer_snapshot
|
||||
.after(crate::perception::observer::compute_visibility_geometry)
|
||||
.after(crate::simulation::interaction::compute_nearby_interactions)
|
||||
.after(crate::simulation::monologue::process_sprint_anomaly_monologue)
|
||||
.after(crate::simulation::monologue::trigger_event_monologue)
|
||||
.after(crate::simulation::dialogue::process_talk_interaction)
|
||||
.after(crate::simulation::dialogue::process_confrontation_response)
|
||||
.before(crate::simulation::time::advance_tick),
|
||||
|
||||
@@ -240,6 +240,7 @@ mod tests {
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: RelationshipState::Unknown,
|
||||
observation: EntityVisibility::Visible,
|
||||
tell_state: None,
|
||||
},
|
||||
VisibleEntity {
|
||||
entity_id: 100,
|
||||
@@ -250,6 +251,7 @@ mod tests {
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: RelationshipState::Known,
|
||||
observation: EntityVisibility::Visible,
|
||||
tell_state: None,
|
||||
},
|
||||
VisibleEntity {
|
||||
entity_id: 200,
|
||||
@@ -260,6 +262,7 @@ mod tests {
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: RelationshipState::Unknown,
|
||||
observation: EntityVisibility::Visible,
|
||||
tell_state: None,
|
||||
},
|
||||
],
|
||||
visible_tiles: vec![VisibleTile {
|
||||
@@ -302,6 +305,7 @@ mod tests {
|
||||
scan_events: vec![],
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
sound_events: vec![],
|
||||
rng_seed: None,
|
||||
}
|
||||
@@ -431,6 +435,7 @@ mod tests {
|
||||
scan_events: vec![],
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
sound_events: vec![],
|
||||
rng_seed: None,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ pub use crate::simulation::time::{DayPhase, TickRate};
|
||||
/// negotiation is unnecessary. Client should reject snapshots with version !=
|
||||
/// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration
|
||||
/// period, then the default is removed once both sides are updated.
|
||||
pub const PROTOCOL_VERSION: u8 = 12;
|
||||
pub const PROTOCOL_VERSION: u8 = 13;
|
||||
|
||||
/// The ONLY data structure crossing the client-server boundary (D-020)
|
||||
/// Contains all information visible to the observer at a given tick.
|
||||
@@ -32,6 +32,8 @@ pub const PROTOCOL_VERSION: u8 = 12;
|
||||
/// rng_seed (#527, deterministic replay — completes WRONG button loop).
|
||||
/// v11 adds: zone_id on VisibleTile (#523, D-077 OQ-09 resolution + D-073 crossfade).
|
||||
/// v12 adds: conversation_events, conversation_ended (#247, D-078 NPC-to-NPC conversations).
|
||||
/// v13 adds: tell_state on VisibleEntity (#90, D-024 tell system — for future client use),
|
||||
/// follow_state (#241, follow mechanic HUD state).
|
||||
/// Future fields: ambient sound events, HUD state (D-020 expansion).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObserverSnapshot {
|
||||
@@ -98,6 +100,11 @@ pub struct ObserverSnapshot {
|
||||
/// Client dismisses the passive dialogue panel for these pairs.
|
||||
#[serde(default)]
|
||||
pub conversation_ended: Vec<crate::simulation::conversation::ConversationEndEvent>,
|
||||
/// Follow-mode state for client HUD display (#241).
|
||||
/// Present when the player is actively following an NPC.
|
||||
/// Client shows follow indicator with distance, LOS, and tension.
|
||||
#[serde(default)]
|
||||
pub follow_state: Option<crate::simulation::follow::FollowStateWire>,
|
||||
/// RNG seed active at this tick for deterministic replay (#527).
|
||||
/// The WRONG button writes this to seed.txt so replays reproduce observed bugs.
|
||||
/// None when the RNG resource is unavailable (should not occur in practice).
|
||||
@@ -270,6 +277,11 @@ pub struct VisibleEntity {
|
||||
/// Visible = in LOS right now. Remembered = known but not in LOS.
|
||||
#[serde(default)]
|
||||
pub observation: EntityVisibility,
|
||||
/// Current observable tell category for NPC entities (#90, D-024).
|
||||
/// None for non-NPC entities or NPCs with no active tell this tick.
|
||||
/// v0.1: field is emitted for future client use; client may ignore.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tell_state: Option<crate::npc::tell_state::TellCategory>,
|
||||
}
|
||||
|
||||
/// Category of visible entity
|
||||
@@ -452,6 +464,12 @@ pub enum VerbKind {
|
||||
/// Furniture — sit/use
|
||||
Sit,
|
||||
|
||||
// --- NPC extended verbs ---
|
||||
/// Follow an NPC — designate as follow target (#241).
|
||||
/// Close range only. Enters follow mode: server tracks distance, LOS,
|
||||
/// and NPC suspicion. Replaces previous follow target if any.
|
||||
Follow,
|
||||
|
||||
// --- Phase 2 verbs (observer, KG-gated, #422) ---
|
||||
/// Confront an NPC about known facts/contradictions.
|
||||
/// Phase 2 only: injected when observer has KnowsDetails+ confidence.
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
// Implements D-024: 10-axis NPC model + CombatCapability component
|
||||
// Background tier state machines for schedule, mood, relationships, job
|
||||
|
||||
pub mod generate;
|
||||
pub mod interaction;
|
||||
pub mod mood;
|
||||
pub mod relationships;
|
||||
pub mod routine;
|
||||
pub mod tell_state;
|
||||
pub mod tolerance;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
@@ -25,6 +28,8 @@ impl Plugin for NpcPlugin {
|
||||
app.init_resource::<relationships::RelationshipGraph>()
|
||||
.init_resource::<relationships::TrustEventQueue>()
|
||||
.init_resource::<routine::PreviousDayPhase>()
|
||||
.init_resource::<tolerance::ToleranceBreachEventQueue>()
|
||||
.init_resource::<routine::RoutineDeviationEventQueue>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -44,6 +49,16 @@ impl Plugin for NpcPlugin {
|
||||
routine::enter_activity
|
||||
.after(crate::simulation::movement::validate_movement)
|
||||
.before(crate::perception::observer::compute_observer_snapshot),
|
||||
tolerance::check_tolerance_threshold
|
||||
.after(mood::update_mood)
|
||||
.before(crate::simulation::time::advance_tick),
|
||||
routine::detect_routine_deviation
|
||||
.after(routine::enter_activity)
|
||||
.before(crate::perception::observer::compute_observer_snapshot),
|
||||
tell_state::derive_tell_state
|
||||
.after(mood::update_mood)
|
||||
.after(routine::detect_routine_deviation)
|
||||
.before(crate::perception::observer::compute_observer_snapshot),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::contraband::ScanEventBuffer;
|
||||
use crate::simulation::conversation::ConversationEventBuffer;
|
||||
use crate::simulation::dialogue::DialogueResponseBuffer;
|
||||
use crate::simulation::follow::FollowTarget;
|
||||
use crate::simulation::interaction::NearbyInteractionBuffer;
|
||||
use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName};
|
||||
use crate::simulation::monologue::{MonologueBuffer, SprintAnomalyQueue};
|
||||
@@ -80,6 +81,7 @@ pub fn compute_observer_snapshot(
|
||||
Option<&mut DialogueResponseBuffer>,
|
||||
Option<&mut ScanEventBuffer>,
|
||||
Option<&mut ConversationEventBuffer>,
|
||||
Option<&FollowTarget>,
|
||||
),
|
||||
With<PlayerCharacter>,
|
||||
>,
|
||||
@@ -89,6 +91,7 @@ pub fn compute_observer_snapshot(
|
||||
Option<&PlayerCharacter>,
|
||||
Option<&crate::npc::Npc>,
|
||||
Option<&AccessRule>,
|
||||
Option<&crate::npc::tell_state::DerivedTellState>,
|
||||
)>,
|
||||
inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
|
||||
mut buffer: ResMut<SnapshotBuffer>,
|
||||
@@ -108,6 +111,7 @@ pub fn compute_observer_snapshot(
|
||||
mut dialogue_response_opt,
|
||||
mut scan_event_buffer_opt,
|
||||
mut conversation_buffer_opt,
|
||||
follow_target_opt,
|
||||
)) = observer_query.single_mut()
|
||||
else {
|
||||
tracing::error!("compute_observer_snapshot: PlayerCharacter query failed");
|
||||
@@ -245,6 +249,29 @@ pub fn compute_observer_snapshot(
|
||||
None => geometry.visible_tiles.clone(),
|
||||
};
|
||||
|
||||
// Build follow-mode state for client HUD (#241)
|
||||
let follow_state = follow_target_opt.and_then(|ft| {
|
||||
let target_wire_id = registry.to_stable(ft.target)?.0;
|
||||
// Distance computed from current positions
|
||||
let all_query_iter = all_entities.iter();
|
||||
let target_pos = all_query_iter
|
||||
.filter_map(|(e, pos, _, _, _, _)| (e == ft.target).then_some(pos))
|
||||
.next()?;
|
||||
let distance = observer_pos
|
||||
.manhattan_distance(target_pos)
|
||||
.unwrap_or(u32::MAX);
|
||||
let has_los = target_pos.z == geometry.observer_z
|
||||
&& geometry
|
||||
.visible_positions
|
||||
.contains(&(target_pos.x, target_pos.y));
|
||||
Some(crate::simulation::follow::FollowStateWire {
|
||||
target_entity_id: target_wire_id,
|
||||
distance,
|
||||
has_los,
|
||||
proximity_ticks: ft.proximity_ticks,
|
||||
})
|
||||
});
|
||||
|
||||
buffer.snapshot = Some(ObserverSnapshot {
|
||||
version: crate::bridge::types::PROTOCOL_VERSION,
|
||||
tick: time.tick,
|
||||
@@ -262,6 +289,7 @@ pub fn compute_observer_snapshot(
|
||||
scan_events,
|
||||
conversation_events,
|
||||
conversation_ended,
|
||||
follow_state,
|
||||
sound_events,
|
||||
rng_seed: sim_rng.as_deref().map(|r| r.seed()),
|
||||
});
|
||||
@@ -282,13 +310,14 @@ fn filter_visible_entities(
|
||||
Option<&PlayerCharacter>,
|
||||
Option<&crate::npc::Npc>,
|
||||
Option<&AccessRule>,
|
||||
Option<&crate::npc::tell_state::DerivedTellState>,
|
||||
)>,
|
||||
) -> (Vec<VisibleEntity>, BTreeSet<u64>, Vec<u64>) {
|
||||
let mut entities = Vec::new();
|
||||
let mut visible_ids: BTreeSet<u64> = BTreeSet::new();
|
||||
let mut blocked_ids: BTreeSet<u64> = BTreeSet::new();
|
||||
|
||||
for (entity, pos, is_player, is_npc, access_rule) in all_entities.iter() {
|
||||
for (entity, pos, is_player, is_npc, access_rule, tell_opt) in all_entities.iter() {
|
||||
if pos.z != geometry.observer_z {
|
||||
continue;
|
||||
}
|
||||
@@ -343,6 +372,8 @@ fn filter_visible_entities(
|
||||
RelationshipState::Unknown
|
||||
};
|
||||
|
||||
let tell_state = tell_opt.and_then(|t| t.category);
|
||||
|
||||
visible_ids.insert(wire_id);
|
||||
entities.push(VisibleEntity {
|
||||
entity_id: wire_id,
|
||||
@@ -353,6 +384,7 @@ fn filter_visible_entities(
|
||||
visibility: sector,
|
||||
relationship,
|
||||
observation: EntityVisibility::Visible,
|
||||
tell_state,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -413,6 +445,7 @@ fn collect_remembered_entities(
|
||||
kind: EntityKind::Npc,
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: knowledge.relationship,
|
||||
tell_state: None, // Remembered entities have no live tell state
|
||||
observation: EntityVisibility::Remembered {
|
||||
confidence: knowledge.confidence,
|
||||
age_ticks,
|
||||
|
||||
@@ -759,8 +759,8 @@ fn phase2_confront_injected_for_npc_with_knows_details() {
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let interaction = &snapshot.nearby_interactions[0];
|
||||
// Should have Talk, ExamineNpc, AND Confront (Phase 2 injected)
|
||||
assert_eq!(interaction.verbs.len(), 3);
|
||||
// Should have Talk, ExamineNpc, Follow, AND Confront (Phase 2 injected)
|
||||
assert_eq!(interaction.verbs.len(), 4);
|
||||
let confront = interaction
|
||||
.verbs
|
||||
.iter()
|
||||
@@ -1322,16 +1322,17 @@ fn phase2_poi_with_confront_verb_order() {
|
||||
let verbs = &snapshot.nearby_interactions[0].verbs;
|
||||
assert_eq!(
|
||||
verbs.len(),
|
||||
3,
|
||||
"POI+KnowsDetails: ExamineNpc + Talk + Confront"
|
||||
4,
|
||||
"POI+KnowsDetails: ExamineNpc + Talk + Follow + Confront"
|
||||
);
|
||||
// POI flips ExamineNpc to priority 1, Talk to 2, Confront at 3
|
||||
// POI flips ExamineNpc to priority 1, Talk to 2, Follow at 3, Confront at 3
|
||||
assert_eq!(verbs[0].kind, VerbKind::ExamineNpc);
|
||||
assert_eq!(verbs[0].priority, 1);
|
||||
assert_eq!(verbs[1].kind, VerbKind::Talk);
|
||||
assert_eq!(verbs[1].priority, 2);
|
||||
assert_eq!(verbs[2].kind, VerbKind::Confront);
|
||||
assert_eq!(verbs[2].priority, 3);
|
||||
// Follow and Confront both at priority 3 — sorted by VerbKind discriminant
|
||||
assert!(verbs.iter().any(|v| v.kind == VerbKind::Follow));
|
||||
assert!(verbs.iter().any(|v| v.kind == VerbKind::Confront));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -7,6 +7,7 @@ use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||
pub mod contraband;
|
||||
pub mod conversation;
|
||||
pub mod dialogue;
|
||||
pub mod follow;
|
||||
pub mod input;
|
||||
pub mod interaction;
|
||||
pub mod inventory;
|
||||
@@ -17,6 +18,7 @@ pub mod path_follow;
|
||||
pub mod pathfinding;
|
||||
pub mod rng;
|
||||
pub mod sound;
|
||||
pub mod spatial;
|
||||
pub mod stance;
|
||||
pub mod tier;
|
||||
pub mod time;
|
||||
@@ -37,6 +39,9 @@ impl Plugin for SimulationPlugin {
|
||||
.init_resource::<input::InputQueue>()
|
||||
.init_resource::<crate::knowledge::EntityRegistry>()
|
||||
.init_resource::<sound::SoundEventQueue>()
|
||||
.init_resource::<spatial::NaiveSpatialIndex>()
|
||||
.init_resource::<follow::FollowEndEventQueue>()
|
||||
.init_resource::<monologue::PostConversationQueue>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -45,6 +50,7 @@ impl Plugin for SimulationPlugin {
|
||||
path_follow::follow_paths.after(pathfinding::compute_paths),
|
||||
movement::validate_movement.after(path_follow::follow_paths),
|
||||
path_follow::cleanup_path_blocked.after(movement::validate_movement),
|
||||
spatial::sync_spatial_index.after(movement::validate_movement),
|
||||
listening::update_listening_focus.after(movement::validate_movement),
|
||||
contraband::check_contraband_scan
|
||||
.after(movement::validate_movement)
|
||||
|
||||
Reference in New Issue
Block a user