feat(simulation): contraband detection + dialogue confidence gate (D-075) (#425, #524)

Contraband detection (Task #425, D-065):
- New module: simulation/contraband.rs — NPC scan checks carried items
  against the KnowledgeGraph confidence gate. NPCs with Authority access
  can initiate a scan; scan outcome depends on item CarriedBy + KG entry.
- Adds ContrabandScanResult event type and ContrabanEntry component.
- Wired into simulation/mod.rs module list.

Dialogue confidence gate (Task #524, D-075 — OQ-18 resolution):
- relationship_to_trust() gains confidence parameter (KnowledgeConfidence).
- Trust tier mapping: (Friendly, KnowsDetails+)→Secret, (Friendly|Known,
  KnowsOf+)→Real, otherwise Surface. Access tier (Layer 1) unchanged.
- Caller process_talk_interaction passes observer KG confidence_of target.
- Resolves OQ-18: confidence co-gates TrustTier, not AccessTier.

Supporting changes:
- decisions/content.md: add D-075 (16 decisions, dated 2026-02-19)
- knowledge/types.rs: expose KnowledgeConfidence comparison helpers
- knowledge/registry.rs: minor API polish
- bridge/types.rs: ContrabandScanResult wire type
- bridge/text_renderer.rs: render contraband scan status
- perception/observer: include carried item count in snapshot
- npc/mod.rs: NPC scan range constant, authority flag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 11:12:24 +01:00
co-authored by Claude Sonnet 4.6
parent 54754e4080
commit d103e445e7
12 changed files with 670 additions and 46 deletions
+14 -2
View File
@@ -161,8 +161,18 @@ pub fn format_snapshot_text(snapshot: &ObserverSnapshot) -> String {
// Blocked entities (debug, #514)
if !snapshot.blocked_entities.is_empty() {
let ids: Vec<String> = snapshot.blocked_entities.iter().map(|id| id.to_string()).collect();
writeln!(out, "Blocked (LOS): {} [{}]", snapshot.blocked_entities.len(), ids.join(", ")).ok();
let ids: Vec<String> = snapshot
.blocked_entities
.iter()
.map(|id| id.to_string())
.collect();
writeln!(
out,
"Blocked (LOS): {} [{}]",
snapshot.blocked_entities.len(),
ids.join(", ")
)
.ok();
}
writeln!(out, "===").ok();
@@ -288,6 +298,7 @@ mod tests {
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
}
}
@@ -410,6 +421,7 @@ mod tests {
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
};
let text = format_snapshot_text(&snap);
assert!(text.contains("Tick 0"));
+5
View File
@@ -69,6 +69,11 @@ pub struct ObserverSnapshot {
/// Client shows speaker name + dialogue text in a dialogue box.
#[serde(default)]
pub dialogue_response: Option<DialogueResponseEvent>,
/// Scan events from NPCs with ScanAuthority this tick (#425, D-065).
/// Present when an NPC scanned the player's inventory. Client renders
/// scan indicator on the scanning NPC. Empty when no scans occurred.
#[serde(default)]
pub scan_events: Vec<crate::simulation::contraband::ScanEvent>,
/// Debug field: entity IDs on the same z-level that are not visible due to
/// LOS obstruction or being outside the vision cone (#514).
/// Sorted ascending for deterministic output. Client can safely ignore.
+2 -1
View File
@@ -77,7 +77,8 @@ impl EntityRegistry {
assert!(
target >= self.next_id,
"cannot reserve backwards: next_id={}, target={}",
self.next_id, target
self.next_id,
target
);
self.next_id = target;
}
+2 -2
View File
@@ -140,8 +140,8 @@ impl RelationshipState {
Self::Friendly => Self::Known,
Self::Known => Self::PersonOfInterest,
Self::PersonOfInterest => Self::Hostile,
Self::Unknown => Self::Unknown, // no-op: can't confront a stranger
Self::Hostile => Self::Hostile, // floor: already worst state
Self::Unknown => Self::Unknown, // no-op: can't confront a stranger
Self::Hostile => Self::Hostile, // floor: already worst state
}
}
}
+9 -2
View File
@@ -24,8 +24,15 @@ impl Plugin for NpcPlugin {
.init_resource::<routine::PreviousDayPhase>()
.add_systems(
Update,
routine::check_phase_transition
.before(crate::simulation::pathfinding::compute_paths),
(
routine::check_phase_transition
.before(crate::simulation::pathfinding::compute_paths),
crate::simulation::contraband::check_contraband_scan
.after(crate::simulation::movement::validate_movement)
.before(
crate::perception::observer::compute_observer_snapshot,
),
),
);
tracing::debug!("NpcPlugin initialized");
+8
View File
@@ -15,6 +15,7 @@ use crate::knowledge::{EntityRegistry, KnowledgeGraph, StableId};
use crate::perception::cognitive_delay::CognitiveDelay;
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
use crate::perception::vision_cone::Facing;
use crate::simulation::contraband::ScanEventBuffer;
use crate::simulation::dialogue::DialogueResponseBuffer;
use crate::simulation::interaction::NearbyInteractionBuffer;
use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName};
@@ -68,6 +69,7 @@ pub fn compute_observer_snapshot(
Option<&mut SprintAnomalyQueue>,
Option<&CognitiveDelay>,
Option<&mut DialogueResponseBuffer>,
Option<&mut ScanEventBuffer>,
),
With<PlayerCharacter>,
>,
@@ -92,6 +94,7 @@ pub fn compute_observer_snapshot(
mut anomaly_queue_opt,
cognitive_delay_opt,
mut dialogue_response_opt,
mut scan_event_buffer_opt,
)) = observer_query.single_mut()
else {
tracing::error!("compute_observer_snapshot: PlayerCharacter query failed");
@@ -168,6 +171,10 @@ pub fn compute_observer_snapshot(
let current_monologue = monologue_buffer.take();
let dialogue_response = dialogue_response_opt.as_mut().and_then(|buf| buf.take());
let scan_events = scan_event_buffer_opt
.as_mut()
.map(|buf| buf.take())
.unwrap_or_default();
// Build pending recognitions from CognitiveDelay (#423, D-060)
let pending_recognitions = cognitive_delay_opt
@@ -207,6 +214,7 @@ pub fn compute_observer_snapshot(
pending_recognitions,
dialogue_response,
blocked_entities,
scan_events,
});
}
+1 -4
View File
@@ -2106,10 +2106,7 @@ fn npc_behind_wall_appears_in_blocked_entities() {
"NPC behind wall should appear in blocked_entities"
);
// Not in visible entities
let npc_visible = snapshot
.entities
.iter()
.any(|e| e.entity_id == npc_sid.0);
let npc_visible = snapshot.entities.iter().any(|e| e.entity_id == npc_sid.0);
assert!(!npc_visible, "NPC should not be in visible entities");
}
+498
View File
@@ -0,0 +1,498 @@
// Contraband detection system — NPC scan checks carried items + KG (#425, D-065)
//
// NPCs with ScanAuthority check the player's inventory for Contraband items
// when within interaction range. On detection, the NPC's KnowledgeGraph is
// updated with HasContraband fact at KnowsDetails confidence (DirectObservation
// source). A ScanEvent is emitted to the player's ScanEventBuffer for
// client-side rendering via ObserverSnapshot.
//
// System ordering: after perception phase, before snapshot phase.
// Uses StableId references throughout — no raw bevy Entity handles in KG entries.
use bevy_ecs::prelude::*;
use crate::knowledge::types::{
FactId, FactKnowledge, KnowledgeConfidence, KnowledgeSource, KnowledgeState,
};
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
use crate::npc::Npc;
use crate::simulation::inventory::CarriedBy;
use crate::simulation::movement::{PlayerCharacter, TilePosition};
use crate::simulation::time::SimulationTime;
/// Scan range for contraband detection (Manhattan distance, same z-level).
/// Matches CLOSE_RANGE from interaction system — NPC must be adjacent.
pub const SCAN_RANGE: u32 = 2;
/// Marker component on item entities that are contraband (D-065).
/// Unlicensed lattice components, medical-grade replacements, Severance tech.
#[derive(Component, Debug, Clone, Copy)]
pub struct Contraband;
/// Component on NPC entities with scan permissions.
/// Only NPCs with this component perform contraband checks.
#[derive(Component, Debug, Clone)]
pub struct ScanAuthority;
/// Wire-format scan event for ObserverSnapshot inclusion.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ScanEvent {
/// StableId of the NPC that performed the scan.
pub scanner_entity_id: u64,
/// Whether contraband was detected.
pub detected_contraband: bool,
}
/// Per-player buffer holding scan events for snapshot inclusion.
/// Cleared each tick by the snapshot builder via `take()`.
#[derive(Component, Debug, Default)]
pub struct ScanEventBuffer {
events: Vec<ScanEvent>,
}
impl ScanEventBuffer {
/// Push a scan event.
pub fn push(&mut self, event: ScanEvent) {
self.events.push(event);
}
/// Drain and return events, leaving the buffer empty.
pub fn take(&mut self) -> Vec<ScanEvent> {
std::mem::take(&mut self.events)
}
}
/// Check for contraband in the player's inventory when scanned by NPC.
///
/// For each NPC with ScanAuthority within SCAN_RANGE of the player:
/// 1. Query player's carried items for Contraband marker
/// 2. If found and NPC doesn't already know: update NPC's KnowledgeGraph
/// with HasContraband fact (KnowsDetails, DirectObservation source)
/// 3. Emit ScanEvent to the player's ScanEventBuffer
///
/// System ordering: after validate_movement, before compute_observer_snapshot.
#[allow(clippy::type_complexity)]
pub fn check_contraband_scan(
time: Res<SimulationTime>,
registry: Res<EntityRegistry>,
mut npc_query: Query<(Entity, &TilePosition, &mut KnowledgeGraph), (With<Npc>, With<ScanAuthority>)>,
mut player_query: Query<(Entity, &TilePosition, &mut ScanEventBuffer), With<PlayerCharacter>>,
items_query: Query<(&CarriedBy, Option<&Contraband>)>,
) {
let Ok((player_entity, player_pos, mut scan_buffer)) = player_query.single_mut() else {
return;
};
let player_pos = *player_pos;
let Some(player_sid) = registry.to_stable(player_entity) else {
return;
};
// Check if player carries any contraband
let has_contraband = items_query
.iter()
.any(|(carried_by, contraband_opt)| carried_by.0 == player_sid && contraband_opt.is_some());
for (npc_entity, npc_pos, mut npc_kg) in npc_query.iter_mut() {
// Range check: same z-level + within scan range
let Some(distance) = npc_pos.manhattan_distance(&player_pos) else {
continue; // Different z-level
};
if distance > SCAN_RANGE {
continue;
}
let Some(npc_sid) = registry.to_stable(npc_entity) else {
continue;
};
// Build the fact ID for this specific player
let fact_id = FactId(format!("contraband.detected_{}", player_sid.0));
if has_contraband {
// Skip if NPC already knows about this player's contraband
if npc_kg.fact_at_least(&fact_id, KnowledgeConfidence::KnowsDetails) {
continue;
}
// Update NPC's KG: record HasContraband fact
npc_kg.facts.insert(
fact_id,
FactKnowledge {
confidence: KnowledgeConfidence::KnowsDetails,
source: KnowledgeSource::DirectObservation { tick: time.tick },
state: KnowledgeState::Active,
acquired_tick: time.tick,
},
);
// Also ensure the NPC has entity knowledge of the player
npc_kg.observe_entity(player_sid, player_pos, time.tick);
tracing::info!(
npc_id = npc_sid.0,
player_id = player_sid.0,
tick = time.tick,
"Contraband detected: NPC scanned player and found contraband"
);
}
// Emit scan event regardless (client renders the scan itself)
scan_buffer.push(ScanEvent {
scanner_entity_id: npc_sid.0,
detected_contraband: has_contraband,
});
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::knowledge::graph::KnowledgeGraph;
use crate::knowledge::registry::EntityRegistry;
use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName};
use crate::simulation::rng::SimRng;
use crate::simulation::time::SimulationTime;
use bevy_ecs::world::World;
fn setup_world() -> World {
let mut world = World::new();
world.init_resource::<SimulationTime>();
world.insert_resource(SimRng::new(42));
world.init_resource::<EntityRegistry>();
world
}
#[test]
fn scan_detects_contraband_item() {
let mut world = setup_world();
// Spawn player
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
// Spawn contraband item carried by player
world.spawn((
CarriedBy(player_sid),
ItemName("Unlicensed Lattice Module".into()),
InventorySlot(0),
Contraband,
));
// Spawn NPC with ScanAuthority adjacent to player
let npc = world
.spawn((
Npc,
TilePosition::new(5, 6, 0),
KnowledgeGraph::new(),
ScanAuthority,
))
.id();
let npc_sid = world.resource_mut::<EntityRegistry>().register(npc);
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
// NPC's KG should now contain HasContraband fact
let npc_kg = world.get::<KnowledgeGraph>(npc).unwrap();
let fact_id = FactId(format!("contraband.detected_{}", player_sid.0));
assert!(
npc_kg.fact_at_least(&fact_id, KnowledgeConfidence::KnowsDetails),
"NPC should know about player's contraband"
);
// NPC should also have entity knowledge of the player
assert!(
npc_kg.knows_entity(&player_sid),
"NPC should have entity knowledge of the player after scan"
);
let _ = npc_sid; // used indirectly
}
#[test]
fn scan_emits_event_to_buffer() {
let mut world = setup_world();
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
// Contraband item
world.spawn((
CarriedBy(player_sid),
ItemName("Lattice Component".into()),
InventorySlot(0),
Contraband,
));
let npc = world
.spawn((
Npc,
TilePosition::new(5, 6, 0),
KnowledgeGraph::new(),
ScanAuthority,
))
.id();
world.resource_mut::<EntityRegistry>().register(npc);
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
let events = buffer.take();
assert_eq!(events.len(), 1);
assert!(events[0].detected_contraband);
}
#[test]
fn no_contraband_no_kg_update() {
let mut world = setup_world();
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
// Non-contraband item
world.spawn((
CarriedBy(player_sid),
ItemName("Comm Log".into()),
InventorySlot(0),
));
let npc = world
.spawn((
Npc,
TilePosition::new(5, 6, 0),
KnowledgeGraph::new(),
ScanAuthority,
))
.id();
world.resource_mut::<EntityRegistry>().register(npc);
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
// NPC's KG should NOT have HasContraband fact
let npc_kg = world.get::<KnowledgeGraph>(npc).unwrap();
let fact_id = FactId(format!("contraband.detected_{}", player_sid.0));
assert!(
!npc_kg.knows_fact(&fact_id),
"NPC should not know about contraband when player has none"
);
// But scan event should still fire (NPC still scanned)
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
let events = buffer.take();
assert_eq!(events.len(), 1);
assert!(!events[0].detected_contraband);
}
#[test]
fn out_of_range_no_scan() {
let mut world = setup_world();
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
world.spawn((
CarriedBy(player_sid),
ItemName("Lattice Component".into()),
InventorySlot(0),
Contraband,
));
// NPC far away (distance 5, beyond SCAN_RANGE=2)
world.spawn((
Npc,
TilePosition::new(5, 10, 0),
KnowledgeGraph::new(),
ScanAuthority,
));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
let events = buffer.take();
assert!(events.is_empty(), "out-of-range NPC should not scan");
}
#[test]
fn different_z_level_no_scan() {
let mut world = setup_world();
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
world.spawn((
CarriedBy(player_sid),
ItemName("Lattice Component".into()),
InventorySlot(0),
Contraband,
));
// NPC on different z-level
world.spawn((
Npc,
TilePosition::new(5, 6, 1),
KnowledgeGraph::new(),
ScanAuthority,
));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
let events = buffer.take();
assert!(events.is_empty(), "different z-level should prevent scan");
}
#[test]
fn npc_without_scan_authority_does_not_scan() {
let mut world = setup_world();
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
world.spawn((
CarriedBy(player_sid),
ItemName("Lattice Component".into()),
InventorySlot(0),
Contraband,
));
// NPC without ScanAuthority
world.spawn((Npc, TilePosition::new(5, 6, 0), KnowledgeGraph::new()));
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
let events = buffer.take();
assert!(events.is_empty(), "NPC without ScanAuthority should not scan");
}
#[test]
fn duplicate_scan_skipped_when_already_known() {
let mut world = setup_world();
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(5, 5, 0),
KnowledgeGraph::new(),
ScanEventBuffer::default(),
))
.id();
let player_sid = world.resource_mut::<EntityRegistry>().register(player);
world.spawn((
CarriedBy(player_sid),
ItemName("Lattice Component".into()),
InventorySlot(0),
Contraband,
));
// Pre-populate NPC's KG with contraband knowledge
let mut npc_kg = KnowledgeGraph::new();
let fact_id = FactId(format!("contraband.detected_{}", player_sid.0));
npc_kg.facts.insert(
fact_id.clone(),
FactKnowledge {
confidence: KnowledgeConfidence::KnowsDetails,
source: KnowledgeSource::DirectObservation { tick: 0 },
state: KnowledgeState::Active,
acquired_tick: 0,
},
);
let npc = world
.spawn((
Npc,
TilePosition::new(5, 6, 0),
npc_kg,
ScanAuthority,
))
.id();
world.resource_mut::<EntityRegistry>().register(npc);
let mut schedule = bevy_ecs::schedule::Schedule::default();
schedule.add_systems(check_contraband_scan);
schedule.run(&mut world);
// NPC already knew — KG should not be re-written (fact tick stays 0)
let npc_kg = world.get::<KnowledgeGraph>(npc).unwrap();
let fact = npc_kg.facts.get(&fact_id).unwrap();
assert_eq!(fact.acquired_tick, 0, "should not overwrite existing knowledge");
}
#[test]
fn scan_event_buffer_take_drains() {
let mut buffer = ScanEventBuffer::default();
buffer.push(ScanEvent {
scanner_entity_id: 1,
detected_contraband: true,
});
buffer.push(ScanEvent {
scanner_entity_id: 2,
detected_contraband: false,
});
let events = buffer.take();
assert_eq!(events.len(), 2);
let events2 = buffer.take();
assert!(events2.is_empty(), "take should drain the buffer");
}
}
+98 -31
View File
@@ -171,18 +171,27 @@ pub fn available_access_tiers(relationship: RelationshipState) -> Vec<AccessTier
}
}
/// Map RelationshipState to the player's effective TrustTier.
/// Map RelationshipState + KnowledgeConfidence to the player's effective TrustTier.
///
/// v0.1 mapping:
/// - Friendly → Real (relationship depth unlocks deeper trust)
/// - All others → Surface
///
/// TODO: TrustTier::Secret is currently unreachable. It should gate on
/// KG confidence (e.g., KnowsDetails+ for a specific secret topic) rather
/// than RelationshipState alone. Tracked for Phase 2 narrative expansion.
pub fn relationship_to_trust(relationship: RelationshipState) -> TrustTier {
/// D-075 layered gate: trust requires BOTH relationship depth AND knowledge depth.
/// - Secret: Friendly + KnowsDetails+ (deep rapport + actionable knowledge)
/// - Real: (Friendly or Known) + KnowsOf+ (rapport + substantive knowledge)
/// - Surface: everything else (baseline, always available)
pub fn relationship_to_trust(
relationship: RelationshipState,
confidence: crate::knowledge::types::KnowledgeConfidence,
) -> TrustTier {
use crate::knowledge::types::KnowledgeConfidence;
match relationship {
RelationshipState::Friendly => TrustTier::Real,
RelationshipState::Friendly if confidence >= KnowledgeConfidence::KnowsDetails => {
TrustTier::Secret
}
RelationshipState::Friendly | RelationshipState::Known
if confidence >= KnowledgeConfidence::KnowsOf =>
{
TrustTier::Real
}
_ => TrustTier::Surface,
}
}
@@ -373,8 +382,11 @@ pub fn process_talk_interaction(
// Layer 2: Derive active situations from game state
let situations = derive_situations(time.day_phase(), relationship);
// Layer 3: Trust tier from relationship
let trust = relationship_to_trust(relationship);
// Layer 3: Trust tier from relationship + confidence (D-075)
let confidence = target_stable
.and_then(|sid| observer_kg.confidence_of(&sid))
.unwrap_or(crate::knowledge::types::KnowledgeConfidence::Suspects);
let trust = relationship_to_trust(relationship, confidence);
// Query Layers 1-3: collect candidates across all available access tiers
let mut candidates: Vec<&IndexedDialogueLine> = Vec::new();
@@ -565,10 +577,7 @@ const CONFRONTATION_LINES: &[(&str, &str)] = &[
"confront_01",
"That changed everything between us. No going back.",
),
(
"confront_02",
"The look on their face... they know I know.",
),
("confront_02", "The look on their face... they know I know."),
(
"confront_03",
"Cards on the table. Let's see what happens next.",
@@ -600,13 +609,8 @@ pub fn process_confrontation_response(
With<PlayerCharacter>,
>,
) {
let Ok((
player_entity,
confrontation,
mut observer_kg,
mut monologue_buf,
mut monologue_state,
)) = query.single_mut()
let Ok((player_entity, confrontation, mut observer_kg, mut monologue_buf, mut monologue_state)) =
query.single_mut()
else {
return;
};
@@ -714,26 +718,83 @@ mod tests {
assert_eq!(tiers, vec![AccessTier::Hostile]);
}
// -- Trust tier tests (D-075: layered confidence gate) --------------------
#[test]
fn trust_friendly_is_real() {
fn trust_friendly_knows_details_is_secret() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Friendly),
relationship_to_trust(
RelationshipState::Friendly,
KnowledgeConfidence::KnowsDetails
),
TrustTier::Secret
);
}
#[test]
fn trust_friendly_direct_is_secret() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Friendly, KnowledgeConfidence::Direct),
TrustTier::Secret
);
}
#[test]
fn trust_friendly_knows_of_is_real() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Friendly, KnowledgeConfidence::KnowsOf),
TrustTier::Real
);
}
#[test]
fn trust_others_are_surface() {
fn trust_friendly_suspects_is_surface() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Unknown),
relationship_to_trust(RelationshipState::Friendly, KnowledgeConfidence::Suspects),
TrustTier::Surface
);
}
#[test]
fn trust_known_knows_of_is_real() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Known),
relationship_to_trust(RelationshipState::Known, KnowledgeConfidence::KnowsOf),
TrustTier::Real
);
}
#[test]
fn trust_known_suspects_is_surface() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::Known, KnowledgeConfidence::Suspects),
TrustTier::Surface
);
}
#[test]
fn trust_unknown_is_always_surface() {
use crate::knowledge::types::KnowledgeConfidence;
assert_eq!(
relationship_to_trust(RelationshipState::PersonOfInterest),
relationship_to_trust(RelationshipState::Unknown, KnowledgeConfidence::Direct),
TrustTier::Surface
);
}
#[test]
fn trust_poi_is_always_surface() {
use crate::knowledge::types::KnowledgeConfidence;
// PersonOfInterest uses Authority access, not trust depth
assert_eq!(
relationship_to_trust(
RelationshipState::PersonOfInterest,
KnowledgeConfidence::KnowsDetails
),
TrustTier::Surface
);
}
@@ -1576,7 +1637,10 @@ mod tests {
DeviationTrigger::WalkAway,
"Deviation trigger should be WalkAway"
);
assert_eq!(deviation.tick, 42, "Deviation should record the walk-away tick");
assert_eq!(
deviation.tick, 42,
"Deviation should record the walk-away tick"
);
}
#[test]
@@ -1659,7 +1723,10 @@ mod tests {
// RoutineDeviation should be recorded (symmetric with walk-away)
let deviation = world.get::<crate::npc::RoutineDeviation>(npc);
assert!(deviation.is_some(), "NPC should get RoutineDeviation after confrontation");
assert!(
deviation.is_some(),
"NPC should get RoutineDeviation after confrontation"
);
assert_eq!(
deviation.unwrap().trigger,
crate::npc::DeviationTrigger::Confrontation,
+10 -3
View File
@@ -474,7 +474,10 @@ fn handle_confront(
target: target_entity,
});
tracing::debug!(target_id, "Confront: ConfrontationDelivered marker set on player");
tracing::debug!(
target_id,
"Confront: ConfrontationDelivered marker set on player"
);
}
/// Handle Place verb: remove an item from inventory and place it on the ground
@@ -1850,7 +1853,9 @@ mod tests {
schedule.add_systems(process_player_input);
schedule.run(&mut world);
let pos = world.get::<TilePosition>(player).expect("player has position");
let pos = world
.get::<TilePosition>(player)
.expect("player has position");
let hub_spawn = crate::test_world::constants::HUB.spawn;
assert_eq!(pos.x, hub_spawn.x, "player x at hub spawn");
assert_eq!(pos.y, hub_spawn.y, "player y at hub spawn");
@@ -1979,7 +1984,9 @@ mod tests {
schedule.add_systems(process_player_input);
schedule.run(&mut world);
let pos = world.get::<TilePosition>(player).expect("player has position");
let pos = world
.get::<TilePosition>(player)
.expect("player has position");
let hub_spawn = crate::test_world::constants::HUB.spawn;
assert_eq!(pos.x, hub_spawn.x, "teleport works while paused");
}
+1
View File
@@ -4,6 +4,7 @@
use bevy_app::prelude::*;
use bevy_ecs::schedule::IntoScheduleConfigs;
pub mod contraband;
pub mod dialogue;
pub mod input;
pub mod interaction;