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:
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user