feat(simulation): character archetype, tell escalation, and news ticker (#587, #589, #591)

- Add character_archetype to StartupMessage with serde default (Detective)
- Bump PROTOCOL_VERSION to 19
- Add escalate_tells_on_activation() and expire_routine_deviations() systems
- RoutineDeviation inserted on triangle NPCs with 300-tick TTL
- Add TickerPool resource with deterministic SimRng rotation (200 ticks)
- Emit current_ticker in ObserverSnapshot when player is in bar zone
- Load ticker YAML from district content directories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 09:13:03 +01:00
co-authored by Claude Opus 4.6
parent fc24de6128
commit 3194a6e491
16 changed files with 567 additions and 22 deletions
+10 -7
View File
@@ -95,7 +95,7 @@ pub const MAP_HEIGHT: i32 = 125;
/// is intentional for deterministic test setups but should be revisited
/// if Gauntlet is ever served by the production startup pipeline.
#[cfg(feature = "gauntlet")]
pub fn setup_gauntlet(app: &mut App) {
pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterArchetype) {
// Start with a fully blocked map, then carve rooms and corridors.
let mut walkability = WalkabilityMap::new_blocked(MAP_WIDTH, MAP_HEIGHT, 1);
@@ -192,6 +192,8 @@ pub fn setup_gauntlet(app: &mut App) {
// Spawn at Hub center: absolute (50, 58)
let profile = MovementProfile::smuggler();
let player_pos = TilePosition::new(50, 58, 0);
let mut monologue_state = MonologueState::default();
monologue_state.character = archetype.as_monologue_key().to_string();
let player = app
.world_mut()
.spawn((
@@ -200,7 +202,7 @@ pub fn setup_gauntlet(app: &mut App) {
Facing::default(),
KnowledgeGraph::new(),
NearbyInteractionBuffer::default(),
MonologueState::default(),
monologue_state,
MonologueBuffer::default(),
SprintAnomalyQueue::default(),
ScanEventBuffer::default(),
@@ -212,6 +214,7 @@ pub fn setup_gauntlet(app: &mut App) {
crate::simulation::pressure::CharacterPressure::default(),
))
.id();
app.world_mut().entity_mut(player).insert(archetype);
registry.register(player);
// --- Hub signs (StableId 1-4) ---
@@ -695,7 +698,7 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
// Run all 29 world-query invariants against the fully-initialized gauntlet world.
invariants::run_invariants(app.world_mut());
@@ -715,7 +718,7 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
let wm = app.world().resource::<WalkabilityMap>();
// Hub center at (50, 58) must be walkable
@@ -729,7 +732,7 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
let wm = app.world().resource::<WalkabilityMap>();
// North wall segment at absolute (90, 54) should be blocked
@@ -745,7 +748,7 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
let wm = app.world().resource::<WalkabilityMap>();
// corridor-E center should be walkable
@@ -759,7 +762,7 @@ mod tests {
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
setup_gauntlet(&mut app);
setup_gauntlet(&mut app, crate::bridge::types::CharacterArchetype::default());
let registry = app.world().resource::<EntityRegistry>();