feat(simulation): spawn 3 NPCs with full component bundles
Implements ticket #84 for Sprint 3: - Replace single bare NPC with 3 distinct NPCs: dock worker (16,13) with full 4-phase routine and MovementSpeed(2), field tech (14,18) with partial routine, stationary guard (18,14) with no routine - Register all entities (player + 3 NPCs) in EntityRegistry - Populate RelationshipGraph with colleague and rival edges - Add NpcPlugin to app for routine system registration - Add multi-entity visibility tests: 3 NPCs in LOS all visible, NPC behind wall excluded from snapshot Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+154
-11
@@ -6,10 +6,17 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use settled_reach_server::bridge::tcp::TcpBridge;
|
||||
use settled_reach_server::bridge::{BridgePlugin, BridgeResource, ServerRunning};
|
||||
use settled_reach_server::knowledge::registry::EntityRegistry;
|
||||
use settled_reach_server::knowledge::{KnowledgeGraph, KnowledgePlugin};
|
||||
use settled_reach_server::npc::Npc;
|
||||
use settled_reach_server::npc::relationships::{RelationshipEdge, RelationshipGraph};
|
||||
use settled_reach_server::npc::{
|
||||
Contentment, DailyRoutine, Npc, NpcPlugin, RelationshipKind, RoutineEntry, ToleranceThreshold,
|
||||
Want, WantKind,
|
||||
};
|
||||
use settled_reach_server::perception::vision_cone::Facing;
|
||||
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
||||
use settled_reach_server::simulation::path_follow::MovementSpeed;
|
||||
use settled_reach_server::simulation::time::DayPhase;
|
||||
use settled_reach_server::simulation::SimulationPlugin;
|
||||
|
||||
fn main() {
|
||||
@@ -41,24 +48,160 @@ fn main() {
|
||||
app.add_plugins(SimulationPlugin);
|
||||
app.add_plugins(BridgePlugin);
|
||||
app.add_plugins(KnowledgePlugin);
|
||||
app.add_plugins(NpcPlugin);
|
||||
app.insert_resource(BridgeResource::new(bridge));
|
||||
app.insert_resource(WalkabilityMap::new(32, 32, 1));
|
||||
|
||||
// Proof room: wall at (16,14) between player and NPC
|
||||
// NPC at (16,13) hidden behind wall until player moves around it
|
||||
// Proof room: wall at (16,14) between player and NPC 1
|
||||
{
|
||||
let mut wm = app.world_mut().resource_mut::<WalkabilityMap>();
|
||||
wm.set_walkable(&TilePosition::new(16, 14, 0), false);
|
||||
}
|
||||
|
||||
app.world_mut().spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing::default(),
|
||||
KnowledgeGraph::new(),
|
||||
));
|
||||
app.world_mut()
|
||||
.spawn((Npc, TilePosition::new(16, 13, 0)));
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
// Player at (16,16)
|
||||
let player = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing::default(),
|
||||
KnowledgeGraph::new(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
|
||||
// NPC 1: Dock worker at (16,13) — behind wall, full routine
|
||||
let npc1 = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
TilePosition::new(16, 13, 0),
|
||||
Want {
|
||||
primary: WantKind::Wealth,
|
||||
intensity: 6,
|
||||
description: "Wants a bigger share of docking fees".into(),
|
||||
},
|
||||
DailyRoutine {
|
||||
entries: vec![
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Morning,
|
||||
location: TilePosition::new(16, 13, 0),
|
||||
activity: "Prep cargo bay".into(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Afternoon,
|
||||
location: TilePosition::new(20, 10, 0),
|
||||
activity: "Unload freight".into(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Evening,
|
||||
location: TilePosition::new(10, 20, 0),
|
||||
activity: "Drink at canteen".into(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Night,
|
||||
location: TilePosition::new(16, 13, 0),
|
||||
activity: "Sleep in bunk".into(),
|
||||
},
|
||||
],
|
||||
description: "Dock worker shift pattern".into(),
|
||||
},
|
||||
Contentment { level: 20 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 30,
|
||||
threshold: 70,
|
||||
},
|
||||
MovementSpeed::new(2),
|
||||
))
|
||||
.id();
|
||||
let npc1_sid = registry.register(npc1);
|
||||
|
||||
// NPC 2: Field tech at (14,18) — visible to player, has routine
|
||||
let npc2 = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
TilePosition::new(14, 18, 0),
|
||||
Want {
|
||||
primary: WantKind::Knowledge,
|
||||
intensity: 8,
|
||||
description: "Obsessed with pre-Collapse sensor arrays".into(),
|
||||
},
|
||||
DailyRoutine {
|
||||
entries: vec![
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Morning,
|
||||
location: TilePosition::new(14, 18, 0),
|
||||
activity: "Calibrate instruments".into(),
|
||||
},
|
||||
RoutineEntry {
|
||||
phase: DayPhase::Afternoon,
|
||||
location: TilePosition::new(22, 22, 0),
|
||||
activity: "Field survey".into(),
|
||||
},
|
||||
],
|
||||
description: "Field tech survey pattern".into(),
|
||||
},
|
||||
Contentment { level: 45 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 10,
|
||||
threshold: 60,
|
||||
},
|
||||
MovementSpeed::default(),
|
||||
))
|
||||
.id();
|
||||
let npc2_sid = registry.register(npc2);
|
||||
|
||||
// NPC 3: Guard at (18,14) — stationary, no routine
|
||||
let npc3 = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Npc,
|
||||
TilePosition::new(18, 14, 0),
|
||||
Want {
|
||||
primary: WantKind::Safety,
|
||||
intensity: 4,
|
||||
description: "Wants a quiet shift".into(),
|
||||
},
|
||||
Contentment { level: -5 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 45,
|
||||
threshold: 55,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
let npc3_sid = registry.register(npc3);
|
||||
|
||||
// Populate RelationshipGraph with a few edges
|
||||
{
|
||||
let mut rel_graph = app.world_mut().resource_mut::<RelationshipGraph>();
|
||||
// Dock worker and guard are colleagues with moderate trust
|
||||
rel_graph.set_relationship(
|
||||
npc1_sid,
|
||||
npc3_sid,
|
||||
RelationshipEdge {
|
||||
kind: RelationshipKind::Colleague,
|
||||
trust: 3,
|
||||
history: vec![],
|
||||
last_interaction_tick: 0,
|
||||
},
|
||||
);
|
||||
// Guard distrusts the field tech (rival for resources)
|
||||
rel_graph.set_relationship(
|
||||
npc3_sid,
|
||||
npc2_sid,
|
||||
RelationshipEdge {
|
||||
kind: RelationshipKind::Rival,
|
||||
trust: -4,
|
||||
history: vec![],
|
||||
last_interaction_tick: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
app.insert_resource(registry);
|
||||
|
||||
tracing::info!("Simulation initialized, entering game loop");
|
||||
|
||||
|
||||
@@ -701,4 +701,71 @@ mod tests {
|
||||
"entity without last_known_position should not appear as ghost"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_npcs_in_los_all_visible() {
|
||||
let mut world = setup_world(32, 32);
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
));
|
||||
// Three NPCs in front of player, no walls
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 14, 0)));
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(14, 14, 0)));
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(18, 14, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
// Player + 3 NPCs = 4 entities
|
||||
assert_eq!(snapshot.entities.len(), 4);
|
||||
let npcs: Vec<_> = snapshot
|
||||
.entities
|
||||
.iter()
|
||||
.filter(|e| matches!(e.kind, EntityKind::Npc))
|
||||
.collect();
|
||||
assert_eq!(npcs.len(), 3);
|
||||
assert!(npcs.iter().all(|n| n.observation == EntityVisibility::Visible));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npc_behind_wall_excluded_from_multi_entity_snapshot() {
|
||||
let mut world = setup_world(32, 32);
|
||||
// Wall at (16,14)
|
||||
world
|
||||
.resource_mut::<WalkabilityMap>()
|
||||
.set_walkable(&TilePosition::new(16, 14, 0), false);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
));
|
||||
// NPC 1: behind wall (should be hidden)
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(16, 13, 0)));
|
||||
// NPC 2: to the side, no wall (should be visible)
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(14, 14, 0)));
|
||||
// NPC 3: also visible
|
||||
world.spawn((crate::npc::Npc, TilePosition::new(18, 15, 0)));
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(compute_observer_snapshot);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
// Player + 2 visible NPCs = 3 (NPC behind wall excluded)
|
||||
let npcs: Vec<_> = snapshot
|
||||
.entities
|
||||
.iter()
|
||||
.filter(|e| matches!(e.kind, EntityKind::Npc))
|
||||
.collect();
|
||||
assert_eq!(npcs.len(), 2, "NPC behind wall should be excluded");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user