feat(simulation): sprint anomaly double-take monologue (#428, D-055)

When sprinting past a Contradicted entity, queue a delayed retroactive
monologue that fires after ~1.5s: "Wait — something wasn't right back
there." SprintAnomalyQueue component with first-in-wins semantics.
Detection in observer pipeline, processing in monologue system.
Wire MovementProfile, Stance, and SprintAnomalyQueue into player spawn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 16:00:20 +01:00
co-authored by Claude Opus 4.6
parent 1d76100f5d
commit df81b24cd1
4 changed files with 459 additions and 6 deletions
+22 -3
View File
@@ -16,7 +16,7 @@ use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
use crate::perception::vision_cone::Facing;
use crate::simulation::interaction::NearbyInteractionBuffer;
use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName};
use crate::simulation::monologue::MonologueBuffer;
use crate::simulation::monologue::{MonologueBuffer, SprintAnomalyQueue};
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
use crate::simulation::stance::Stance;
use crate::simulation::time::SimulationTime;
@@ -53,7 +53,7 @@ pub fn compute_observer_snapshot(
geometry: Res<VisibilityGeometry>,
registry: Res<EntityRegistry>,
mut observer_query: Query<
(Entity, &TilePosition, Option<&Facing>, &KnowledgeGraph, &mut NearbyInteractionBuffer, &mut MonologueBuffer, Option<&Stance>, Option<&CharacterArchetype>),
(Entity, &TilePosition, Option<&Facing>, &KnowledgeGraph, &mut NearbyInteractionBuffer, &mut MonologueBuffer, Option<&Stance>, Option<&CharacterArchetype>, Option<&mut SprintAnomalyQueue>),
With<PlayerCharacter>,
>,
all_entities: Query<(
@@ -65,7 +65,7 @@ pub fn compute_observer_snapshot(
inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>,
mut buffer: ResMut<SnapshotBuffer>,
) {
let Ok((observer_entity, _observer_pos, facing_opt, observer_kg, mut interaction_buffer, mut monologue_buffer, stance_opt, archetype_opt)) =
let Ok((observer_entity, _observer_pos, facing_opt, observer_kg, mut interaction_buffer, mut monologue_buffer, stance_opt, archetype_opt, mut anomaly_queue_opt)) =
observer_query.single_mut()
else {
return;
@@ -101,6 +101,25 @@ pub fn compute_observer_snapshot(
&mut entities,
);
// Sprint anomaly detection (#428, D-055)
// When sprinting, scan visible entities for Contradicted KG state.
// Queue the first match for delayed "double-take" monologue.
if stance_opt.map(|s| s.0) == Some(MovementStance::Sprint) {
if let Some(anomaly_queue) = anomaly_queue_opt.as_mut() {
if !anomaly_queue.has_pending() {
for &wire_id in &visible_ids {
let stable_id = StableId(wire_id);
if let Some(knowledge) = observer_kg.entity_knowledge(&stable_id) {
if knowledge.state == KnowledgeState::Contradicted {
anomaly_queue.push_anomaly(wire_id, time.tick);
break; // First-in wins
}
}
}
}
}
}
let game_time = GameTime {
day: time.day(),
time_of_day: time.time_of_day_minutes(),