feat(simulation): sprint 10 — hub teleport, confrontation response, walk-away phase 2, gauntlet rooms 4-7, blocked_entities debug field

#491: PlayerAction::TeleportToHub — moves player to hub spawn, clears
dialogue/monologue/interaction buffer, Gauntlet-only with log warning.
#520: ConfrontationDelivered event — Tier 2 animation shift, relationship
state decrement (D-033), monologue spike emission.
#519: Walk-away Phase 2 — NPC animation shift + routine deviation on
dialogue exit (D-064).
#498: Four new Gauntlet rooms — Interaction Gallery, Fog Theater, Crowd
Plaza, Dialogue Room with constants and wiring.
#514: blocked_entities Vec<u64> on ObserverSnapshot, protocol v9.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 12:58:14 +01:00
co-authored by Claude Opus 4.6
parent b4a3784218
commit 273d29f26f
18 changed files with 1219 additions and 50 deletions
+2 -4
View File
@@ -142,10 +142,8 @@ pub fn emit_observation_events(
let pending_ids: Vec<crate::knowledge::types::StableId> =
delay.pending().iter().map(|p| p.stable_id).collect();
for sid in pending_ids {
if !visible_stable_ids.contains(&sid.0) {
if delay.cancel(&sid).is_some() {
tracing::debug!("Cognitive delay cancelled: stable_id={} left LOS", sid.0);
}
if !visible_stable_ids.contains(&sid.0) && delay.cancel(&sid).is_some() {
tracing::debug!("Cognitive delay cancelled: stable_id={} left LOS", sid.0);
}
}
}
+24 -14
View File
@@ -116,7 +116,7 @@ pub fn compute_observer_snapshot(
})
.unwrap_or_default();
let (mut entities, visible_ids) =
let (mut entities, visible_ids, blocked_entities) =
filter_visible_entities(&geometry, &registry, observer_kg, &all_entities);
collect_remembered_entities(
@@ -206,11 +206,13 @@ pub fn compute_observer_snapshot(
current_monologue,
pending_recognitions,
dialogue_response,
blocked_entities,
});
}
/// Filter entities by visibility using precomputed geometry.
/// Returns (visible entities, set of visible wire IDs).
/// Returns (visible entities, set of visible wire IDs, blocked entity IDs).
/// Blocked entities are on the same z-level but not in visible_positions (#514).
#[allow(clippy::type_complexity)]
fn filter_visible_entities(
geometry: &VisibilityGeometry,
@@ -222,15 +224,31 @@ fn filter_visible_entities(
Option<&PlayerCharacter>,
Option<&crate::npc::Npc>,
)>,
) -> (Vec<VisibleEntity>, BTreeSet<u64>) {
) -> (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) in all_entities.iter() {
if pos.z != geometry.observer_z {
continue;
}
// Resolve wire ID early — needed for both visible and blocked paths
let wire_id = registry
.to_stable(entity)
.map(|sid| sid.0)
.unwrap_or_else(|| {
tracing::error!(?entity, "entity not in EntityRegistry");
entity.to_bits()
});
if !geometry.visible_positions.contains(&(pos.x, pos.y)) {
// Same z-level but not visible — blocked by LOS or outside vision cone.
// Exclude the player entity (always at origin, always visible).
if is_player.is_none() {
blocked_ids.insert(wire_id);
}
continue;
}
@@ -257,16 +275,6 @@ fn filter_visible_entities(
RelationshipState::Unknown
};
// Fallback to Entity::to_bits() is intentional for per-frame systems:
// panicking would crash the server every tick. The error log makes this
// loud enough to catch in testing while keeping the server alive.
let wire_id = registry
.to_stable(entity)
.map(|sid| sid.0)
.unwrap_or_else(|| {
tracing::error!(?entity, "entity visible but not in EntityRegistry");
entity.to_bits()
});
visible_ids.insert(wire_id);
entities.push(VisibleEntity {
entity_id: wire_id,
@@ -280,7 +288,9 @@ fn filter_visible_entities(
});
}
(entities, visible_ids)
// BTreeSet iteration is sorted — deterministic output guaranteed
let blocked_vec: Vec<u64> = blocked_ids.into_iter().collect();
(entities, visible_ids, blocked_vec)
}
/// Collect remembered entities from the knowledge graph — entities the observer