Merge remote-tracking branch 'origin/ci'
# Conflicts: # client/tests/fixtures/msgpack/snapshot_boundary_tick_0.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_127.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_2b31m1.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_2b32.msgpack # client/tests/fixtures/msgpack/snapshot_boundary_tick_32767.msgpack # client/tests/fixtures/msgpack/snapshot_empty.msgpack # client/tests/fixtures/msgpack/snapshot_multi_entity.msgpack # client/tests/fixtures/msgpack/snapshot_one_npc.msgpack # client/tests/fixtures/msgpack/snapshot_player.msgpack # client/tests/fixtures/msgpack/snapshot_v2_full.msgpack # server/Cargo.toml # server/src/bridge/text_renderer.rs # server/src/bridge/types.rs # server/src/perception/observer/mod.rs # server/src/simulation/path_follow.rs # server/tests/bridge_ipc.rs # server/tests/bridge_tcp.rs # server/tests/gen_fixtures.rs # server/tests/serialization.rs
This commit is contained in:
@@ -80,7 +80,10 @@ impl ScanEventBuffer {
|
||||
pub fn check_contraband_scan(
|
||||
time: Res<SimulationTime>,
|
||||
registry: Res<EntityRegistry>,
|
||||
mut npc_query: Query<(Entity, &TilePosition, &mut KnowledgeGraph), (With<Npc>, With<ScanAuthority>)>,
|
||||
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>)>,
|
||||
) {
|
||||
@@ -423,7 +426,10 @@ mod tests {
|
||||
|
||||
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
|
||||
let events = buffer.take();
|
||||
assert!(events.is_empty(), "NPC without ScanAuthority should not scan");
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"NPC without ScanAuthority should not scan"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -461,12 +467,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let npc = world
|
||||
.spawn((
|
||||
Npc,
|
||||
TilePosition::new(5, 6, 0),
|
||||
npc_kg,
|
||||
ScanAuthority,
|
||||
))
|
||||
.spawn((Npc, TilePosition::new(5, 6, 0), npc_kg, ScanAuthority))
|
||||
.id();
|
||||
world.resource_mut::<EntityRegistry>().register(npc);
|
||||
|
||||
@@ -477,12 +478,19 @@ mod tests {
|
||||
// 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");
|
||||
assert_eq!(
|
||||
fact.acquired_tick, 0,
|
||||
"should not overwrite existing knowledge"
|
||||
);
|
||||
|
||||
// Scan event should still fire even though NPC already knew
|
||||
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
|
||||
let events = buffer.take();
|
||||
assert_eq!(events.len(), 1, "scan event should emit even for already-known contraband");
|
||||
assert_eq!(
|
||||
events.len(),
|
||||
1,
|
||||
"scan event should emit even for already-known contraband"
|
||||
);
|
||||
assert!(events[0].detected_contraband);
|
||||
}
|
||||
|
||||
@@ -542,7 +550,11 @@ mod tests {
|
||||
// Both should emit separate scan events
|
||||
let mut buffer = world.get_mut::<ScanEventBuffer>(player).unwrap();
|
||||
let events = buffer.take();
|
||||
assert_eq!(events.len(), 2, "each ScanAuthority NPC should emit a scan event");
|
||||
assert_eq!(
|
||||
events.len(),
|
||||
2,
|
||||
"each ScanAuthority NPC should emit a scan event"
|
||||
);
|
||||
assert!(events.iter().all(|e| e.detected_contraband));
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ pub fn available_access_tiers(relationship: RelationshipState) -> Vec<AccessTier
|
||||
/// - Secret: Friendly + KnowsDetails+ (deep rapport + actionable knowledge)
|
||||
/// - Real: (Friendly or Known) + KnowsOf+ (rapport + substantive knowledge)
|
||||
/// - Surface: everything else (baseline, always available)
|
||||
///
|
||||
/// Map relationship + knowledge confidence to trust tier (D-075).
|
||||
///
|
||||
/// Trust tier gates which dialogue lines are available. The layered gate
|
||||
@@ -336,6 +337,7 @@ pub fn select_dialogue_line<'a>(
|
||||
/// line to DialogueResponseBuffer.
|
||||
///
|
||||
/// System ordering: after process_player_input, before compute_observer_snapshot.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
pub fn process_talk_interaction(
|
||||
mut commands: Commands,
|
||||
|
||||
@@ -164,6 +164,7 @@ impl ObjectType {
|
||||
/// priority adjustment (e.g. POI -> Observe first) is applied by the observer
|
||||
/// system after taking the buffer. This keeps the simulation phase free of
|
||||
/// knowledge graph dependencies (D-010 phase boundary).
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn compute_nearby_interactions(
|
||||
mut player_query: Query<
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// When sprinting past a Contradicted entity, a delayed "double-take" monologue
|
||||
// fires retroactively. Detection in observer pipeline, processing here.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use rand::Rng;
|
||||
@@ -77,7 +77,7 @@ pub struct MonologueState {
|
||||
/// Whether the enter_location monologue has fired this session.
|
||||
pub entered: bool,
|
||||
/// IDs of lines already shown (dedup within session).
|
||||
pub shown_ids: HashSet<String>,
|
||||
pub shown_ids: BTreeSet<String>,
|
||||
/// Character type for pool filtering. v0.1: always "detective".
|
||||
pub character: String,
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl Default for MonologueState {
|
||||
last_position: None,
|
||||
idle_ticks: 0,
|
||||
entered: false,
|
||||
shown_ids: HashSet::new(),
|
||||
shown_ids: BTreeSet::new(),
|
||||
// v0.1: default to detective; character selection sets this
|
||||
character: "detective".to_string(),
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::bridge::types::MovementStance;
|
||||
use crate::knowledge::types::SoundRange;
|
||||
@@ -27,7 +27,20 @@ pub struct PlayerCharacter;
|
||||
///
|
||||
/// Examples: a Standing character can walk past a Seated NPC at a console,
|
||||
/// a Fixture (terminal) shares a tile with someone Seated at it.
|
||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Component,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
pub enum TilePresence {
|
||||
/// Upright position — walking, standing, sprinting. Default for all entities.
|
||||
#[default]
|
||||
@@ -44,7 +57,9 @@ pub enum TilePresence {
|
||||
/// Tile position component for grid-based movement.
|
||||
/// Discrete integer coordinates used in simulation; converted to f32
|
||||
/// at the bridge boundary for VisibleEntity wire format.
|
||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Component, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct TilePosition {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
@@ -121,7 +136,7 @@ impl TilePosition {
|
||||
}
|
||||
|
||||
/// Chunk coordinate for chunk-based map storage (D-012).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ChunkCoord {
|
||||
pub cx: i32,
|
||||
pub cy: i32,
|
||||
@@ -166,14 +181,14 @@ impl ChunkData {
|
||||
/// Unloaded chunks are treated as unwalkable.
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
pub struct WalkabilityMap {
|
||||
chunks: HashMap<ChunkCoord, ChunkData>,
|
||||
chunks: BTreeMap<ChunkCoord, ChunkData>,
|
||||
}
|
||||
|
||||
impl WalkabilityMap {
|
||||
/// Create a walkability map covering a rectangular area with all tiles walkable.
|
||||
/// Generates chunks to cover the specified dimensions on z-level 0..z_levels.
|
||||
pub fn new(width: i32, height: i32, z_levels: i32) -> Self {
|
||||
let mut chunks = HashMap::new();
|
||||
let mut chunks = BTreeMap::new();
|
||||
let cx_max = (width + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||
let cy_max = (height + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||
for z in 0..z_levels {
|
||||
@@ -188,7 +203,7 @@ impl WalkabilityMap {
|
||||
|
||||
/// Create a walkability map covering a rectangular area with all tiles blocked.
|
||||
pub fn new_blocked(width: i32, height: i32, z_levels: i32) -> Self {
|
||||
let mut chunks = HashMap::new();
|
||||
let mut chunks = BTreeMap::new();
|
||||
let cx_max = (width + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||
let cy_max = (height + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||
for z in 0..z_levels {
|
||||
@@ -263,6 +278,7 @@ pub struct MoveIntent {
|
||||
/// entities without intents, then resolve movers in order — first valid claim
|
||||
/// to a layer slot wins.
|
||||
/// Always removes MoveIntent component after processing.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn validate_movement(
|
||||
mut commands: Commands,
|
||||
walkability: Option<Res<WalkabilityMap>>,
|
||||
@@ -285,7 +301,7 @@ pub fn validate_movement(
|
||||
|
||||
// Collect layer slots occupied by stationary entities (no MoveIntent).
|
||||
// Key: (position, layer) — two entities can share a tile if different layers.
|
||||
let mut occupied: HashMap<(TilePosition, TilePresence), Entity> = HashMap::new();
|
||||
let mut occupied: BTreeMap<(TilePosition, TilePresence), Entity> = BTreeMap::new();
|
||||
for (entity, pos, presence) in stationary.iter() {
|
||||
let layer = presence.copied().unwrap_or_default();
|
||||
occupied.insert((*pos, layer), entity);
|
||||
|
||||
@@ -53,6 +53,7 @@ impl MovementSpeed {
|
||||
/// Scoped to `ActiveSim` NPCs — only entities in the Active tier execute
|
||||
/// path movement each tick (D-026, #94). Background/StateSaved NPCs do not
|
||||
/// process path steps.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn follow_paths(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &mut ComputedPath, Option<&mut MovementSpeed>), (With<Npc>, With<ActiveSim>)>,
|
||||
|
||||
@@ -58,6 +58,7 @@ pub struct PathBlocked;
|
||||
/// Cardinal-only is a deliberate v0.1 simplification: diagonal movement
|
||||
/// would require √2 cost handling and diagonal wall-clipping checks.
|
||||
/// Removes PathRequest and inserts ComputedPath or PathBlocked.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn compute_paths(
|
||||
mut commands: Commands,
|
||||
walkability: Option<Res<WalkabilityMap>>,
|
||||
|
||||
Reference in New Issue
Block a user