Files
settled-reach/server/tests/contamination.rs
T
jpmschweitzerandClaude Opus 4.6 b126e1830d 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>
2026-02-28 16:34:47 +01:00

154 lines
5.1 KiB
Rust

//! 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);
}