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>
219 lines
7.3 KiB
Rust
219 lines
7.3 KiB
Rust
// The Settled Reach - Simulation Server
|
|
// Entry point for standalone simulation binary
|
|
|
|
use bevy_app::prelude::*;
|
|
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::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() {
|
|
// Initialize tracing subscriber for logging
|
|
tracing_subscriber::registry()
|
|
.with(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| "settled_reach_server=debug".into()),
|
|
)
|
|
.with(tracing_subscriber::fmt::layer())
|
|
.init();
|
|
|
|
let addr = std::env::args()
|
|
.nth(1)
|
|
.or_else(|| std::env::var("SR_ADDR").ok())
|
|
.unwrap_or_else(|| "127.0.0.1:9876".to_string());
|
|
|
|
tracing::info!("The Settled Reach - Simulation Server starting");
|
|
tracing::info!("Waiting for client connection on {}", addr);
|
|
|
|
let bridge = TcpBridge::accept(&addr).unwrap_or_else(|e| {
|
|
tracing::error!("Failed to accept client connection on {}: {}", addr, e);
|
|
std::process::exit(1);
|
|
});
|
|
tracing::info!("Client connected, initializing simulation");
|
|
|
|
// Create the bevy App and add plugins
|
|
let mut app = App::new();
|
|
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 1
|
|
{
|
|
let mut wm = app.world_mut().resource_mut::<WalkabilityMap>();
|
|
wm.set_walkable(&TilePosition::new(16, 14, 0), false);
|
|
}
|
|
|
|
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");
|
|
|
|
// Game loop: run until client disconnects
|
|
loop {
|
|
app.update();
|
|
// Check ServerRunning resource
|
|
if !app.world().resource::<ServerRunning>().0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
tracing::info!("Simulation server shutting down");
|
|
}
|