feat(simulation): add contamination activation mechanic (#254)
Timer-based storyteller system fires after 1800 ticks (30 game-min). Sets ContaminationActive resource, applies tension delta to all ActiveFork triangles, and emits ContaminationEvent for downstream monologue/behavioral hooks. Q-017 fallback constants in place. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
//! Integration test: contamination activation mechanic (#254).
|
||||
//!
|
||||
//! Verifies that the storyteller activates contamination after the configured
|
||||
//! delay and that all ActiveFork triangles receive pressure.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::schedule::Schedule;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use settled_reach_server::content::template::{
|
||||
RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState,
|
||||
};
|
||||
use settled_reach_server::knowledge::types::StableId;
|
||||
use settled_reach_server::simulation::tier::ActiveSim;
|
||||
use settled_reach_server::simulation::time::SimulationTime;
|
||||
use settled_reach_server::storyteller::{
|
||||
tick_contamination_activation, ContaminationActive, ContaminationEventQueue,
|
||||
CONTAMINATION_DELAY_TICKS, CONTAMINATION_PRESSURE_DELTA,
|
||||
};
|
||||
|
||||
fn make_triangle(
|
||||
slug: &str,
|
||||
classification: TriangleClassification,
|
||||
base_ids: [u64; 3],
|
||||
) -> TriangleState {
|
||||
let mut role_assignments = BTreeMap::new();
|
||||
role_assignments.insert(RoleId::new("role-a"), StableId(base_ids[0]));
|
||||
role_assignments.insert(RoleId::new("role-b"), StableId(base_ids[1]));
|
||||
role_assignments.insert(RoleId::new("role-c"), StableId(base_ids[2]));
|
||||
|
||||
TriangleState {
|
||||
triangle_id: TriangleId::from_seed_and_slug(0, slug),
|
||||
role_assignments,
|
||||
tension: 0,
|
||||
phase: match classification {
|
||||
TriangleClassification::ActiveFork => TrianglePhase::Simmering,
|
||||
TriangleClassification::PassiveTension => TrianglePhase::Dormant,
|
||||
},
|
||||
tension_rate: 1,
|
||||
template_id: TemplateId::from_seed_and_slug(0, "authored"),
|
||||
classification,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acceptance test: run 1801 ticks, assert ContaminationActive is set and
|
||||
/// all ActiveFork TriangleState entities have tension > 0.
|
||||
#[test]
|
||||
fn contamination_activates_after_1801_ticks() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<SimulationTime>();
|
||||
world.init_resource::<ContaminationActive>();
|
||||
world.init_resource::<ContaminationEventQueue>();
|
||||
|
||||
// Spawn 3 ActiveFork + 2 PassiveTension triangles (matching real content)
|
||||
let fork1 = world
|
||||
.spawn((
|
||||
make_triangle("hub-power", TriangleClassification::ActiveFork, [1, 2, 3]),
|
||||
ActiveSim,
|
||||
))
|
||||
.id();
|
||||
let fork2 = world
|
||||
.spawn((
|
||||
make_triangle(
|
||||
"bar-tensions",
|
||||
TriangleClassification::ActiveFork,
|
||||
[4, 5, 6],
|
||||
),
|
||||
ActiveSim,
|
||||
))
|
||||
.id();
|
||||
let fork3 = world
|
||||
.spawn((
|
||||
make_triangle(
|
||||
"informant-question",
|
||||
TriangleClassification::ActiveFork,
|
||||
[7, 8, 9],
|
||||
),
|
||||
ActiveSim,
|
||||
))
|
||||
.id();
|
||||
let passive1 = world
|
||||
.spawn((
|
||||
make_triangle(
|
||||
"worried-knowledge",
|
||||
TriangleClassification::PassiveTension,
|
||||
[10, 11, 12],
|
||||
),
|
||||
ActiveSim,
|
||||
))
|
||||
.id();
|
||||
let passive2 = world
|
||||
.spawn((
|
||||
make_triangle(
|
||||
"worried-partner",
|
||||
TriangleClassification::PassiveTension,
|
||||
[13, 14, 15],
|
||||
),
|
||||
ActiveSim,
|
||||
))
|
||||
.id();
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(tick_contamination_activation);
|
||||
|
||||
// Simulate 1801 ticks
|
||||
for tick in 0..=1800 {
|
||||
world.resource_mut::<SimulationTime>().tick = tick;
|
||||
schedule.run(&mut world);
|
||||
}
|
||||
|
||||
// Assert ContaminationActive is set
|
||||
assert!(
|
||||
world.resource::<ContaminationActive>().0,
|
||||
"ContaminationActive must be true after 1801 ticks"
|
||||
);
|
||||
|
||||
// Assert all ActiveFork triangles have tension > 0
|
||||
for (label, entity) in [("hub-power", fork1), ("bar-tensions", fork2), ("informant-question", fork3)] {
|
||||
let state = world
|
||||
.get::<TriangleState>(entity)
|
||||
.unwrap_or_else(|| panic!("{} triangle entity should exist", label));
|
||||
assert!(
|
||||
state.tension > 0,
|
||||
"ActiveFork triangle '{}' should have tension > 0 after contamination, got {}",
|
||||
label,
|
||||
state.tension
|
||||
);
|
||||
assert_eq!(
|
||||
state.tension, CONTAMINATION_PRESSURE_DELTA,
|
||||
"ActiveFork triangle '{}' tension should be exactly {} (contamination delta)",
|
||||
label,
|
||||
CONTAMINATION_PRESSURE_DELTA
|
||||
);
|
||||
}
|
||||
|
||||
// Assert PassiveTension triangles were NOT pressured
|
||||
for (label, entity) in [("worried-knowledge", passive1), ("worried-partner", passive2)] {
|
||||
let state = world
|
||||
.get::<TriangleState>(entity)
|
||||
.unwrap_or_else(|| panic!("{} triangle entity should exist", label));
|
||||
assert_eq!(
|
||||
state.tension, 0,
|
||||
"PassiveTension triangle '{}' should have tension 0 (not affected by contamination)",
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
// Assert exactly one ContaminationEvent was emitted
|
||||
let events = world.resource_mut::<ContaminationEventQueue>().drain();
|
||||
assert_eq!(events.len(), 1, "exactly one ContaminationEvent expected");
|
||||
assert_eq!(events[0].tick, CONTAMINATION_DELAY_TICKS);
|
||||
assert_eq!(events[0].triangles_affected, 3);
|
||||
}
|
||||
Reference in New Issue
Block a user