Files
RustRaptor/tests/iron_coast.rs
T

292 lines
11 KiB
Rust

//! Campaign acceptance uses synthetic PNG/WAV assets; original data is unnecessary.
use raptor_replica::{
content::{ContentPack, Registry},
modding::{self, ActiveMods, Manifest},
persistence::Pilot,
simulation::{Input, Outcome, World},
};
use serde_json::{Value, json};
use std::{
fs,
path::{Path, PathBuf},
};
const PACKAGES: [&str; 7] = [
"iron_coast_scout",
"iron_coast_turret",
"iron_coast_boss",
"pulse_cannon",
"seeker_swarm",
"ion_lance",
"iron_coast_mission_1",
];
const MISSION: &str = "iron_coast_mission_1:mission_01";
fn base() -> Registry {
let mut registry = Registry::default();
let pack:ContentPack=serde_json::from_value(json!({
"weapons":[{"id":"base:projectile_forward_guns","name":"Forward Guns","damage":1,"speed":8,"cooldown":2,"behavior":"straight","target":"all","legacy":0,"art":{"asset":42,"logical_width":4,"logical_height":8,"frames":4}}],
"items":[{"id":"base:forward_guns","name":"Forward Guns","price":0,"max_count":1,"weapon":"base:projectile_forward_guns","legacy":0},{"id":"base:energy","name":"Shield","price":0,"max_count":100,"legacy":16}],
"missions":[{"id":"base:sector_1_wave_1","name":"Base fixture","duration":1000}]
})).unwrap();
registry.apply(pack, "base").unwrap();
registry.validate().unwrap();
registry
}
fn fixture_resources(value: &Value, root: &Path) {
match value {
Value::Object(object) => {
if let (Some(width), Some(height)) = (
object.get("logical_width").and_then(Value::as_u64),
object.get("logical_height").and_then(Value::as_u64),
) {
let frames = object.get("frames").and_then(Value::as_u64).unwrap_or(1);
for (key, scale) in [("png", 1), ("hd_png", 4)] {
if let Some(file) = object.get(key).and_then(Value::as_str) {
let path = root.join(file);
fs::create_dir_all(path.parent().unwrap()).unwrap();
let image = image::RgbaImage::from_pixel(
(width * frames * scale) as u32,
(height * scale) as u32,
image::Rgba([80, 96, 104, 255]),
);
image.save(path).unwrap();
}
}
}
if let Some(file) = object.get("file").and_then(Value::as_str) {
let path = root.join(file);
fs::create_dir_all(path.parent().unwrap()).unwrap();
// Small valid mono PCM16 WAV; integration checks use the actual cues.
let mut wav = Vec::new();
wav.extend_from_slice(b"RIFF");
wav.extend_from_slice(&100_u32.to_le_bytes());
wav.extend_from_slice(b"WAVEfmt ");
wav.extend_from_slice(&16_u32.to_le_bytes());
wav.extend_from_slice(&1_u16.to_le_bytes());
wav.extend_from_slice(&1_u16.to_le_bytes());
wav.extend_from_slice(&22050_u32.to_le_bytes());
wav.extend_from_slice(&44100_u32.to_le_bytes());
wav.extend_from_slice(&2_u16.to_le_bytes());
wav.extend_from_slice(&16_u16.to_le_bytes());
wav.extend_from_slice(b"data");
wav.extend_from_slice(&64_u32.to_le_bytes());
wav.extend_from_slice(&[0; 64]);
fs::write(path, wav).unwrap();
}
for child in object.values() {
fixture_resources(child, root);
}
}
Value::Array(values) => {
for child in values {
fixture_resources(child, root);
}
}
_ => {}
}
}
fn active() -> (tempfile::TempDir, ActiveMods) {
let temporary = tempfile::tempdir().unwrap();
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/mods");
for id in PACKAGES {
let original = source.join(id);
let root = temporary.path().join(id);
fs::create_dir_all(&root).unwrap();
let manifest_data = fs::read(original.join("mod.toml"))
.expect("all seven Iron Coast packages must be installed");
let manifest: Manifest =
toml::from_str(std::str::from_utf8(&manifest_data).unwrap()).unwrap();
fs::write(root.join("mod.toml"), manifest_data).unwrap();
let content = fs::read(original.join(&manifest.content)).unwrap();
let value: Value = serde_json::from_slice(&content).unwrap();
fs::write(root.join(&manifest.content), content).unwrap();
for script in &manifest.scripts {
let path = root.join(script);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::copy(original.join(script), path).unwrap();
}
fixture_resources(&value, &root);
}
let discovery = modding::discover(temporary.path());
assert!(discovery.errors.is_empty(), "{:?}", discovery.errors);
let enabled = PACKAGES.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let active = ActiveMods::build(&base(), &discovery, &enabled, None).unwrap();
(temporary, active)
}
#[test]
fn campaign_dependencies_and_generated_footprints_validate_together() {
let (_temporary, active) = active();
assert_eq!(active.pins.len(), 7);
let mission = &active.registry.missions[&MISSION.into()];
assert!(!mission.auto_complete);
assert_eq!(mission.columns, 9);
assert_eq!(mission.layout.len(), 72 * 9);
assert!(
mission
.spawns
.iter()
.any(|s| s.enemy.0 == "iron_coast_boss:unit" && s.tick == 720)
);
assert_eq!(mission.art.as_ref().unwrap().logical_width, 320);
assert!(
active.registry.campaigns[&"iron_coast_mission_1:campaign".into()]
.missions
.contains(&MISSION.into())
);
}
#[test]
fn boss_alone_cannot_complete_and_reward_is_paid_once() {
let (_temporary, mut active) = active();
let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
world
.objectives
.insert("iron_coast_boss:defeated".into(), 1);
world
.objectives
.insert("iron_coast_turret:defeated".into(), 1);
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.outcome, Outcome::Playing);
world
.objectives
.insert("iron_coast_turret:defeated".into(), 2);
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.outcome, Outcome::Complete);
assert_eq!(world.objectives["iron_coast_mission_1:complete"], 1);
assert_eq!(world.player.credits, 5000);
assert_eq!(world.objectives["iron_coast_mission_1:debrief"], 1);
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.player.credits, 5000);
}
fn play(use_trial: bool) -> (World, Vec<(u64, String)>) {
let (_temporary, mut active) = active();
let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap();
let mut seen = Vec::new();
for tick in 0..2000 {
let input = Input {
fire: true,
special: tick % 90 == 60,
cycle: tick == 1,
absolute: Some([144, 160]),
..Input::default()
};
world
.step(input, &active.registry, &mut active.scripts)
.unwrap();
if tick == 0 && !use_trial {
world
.player
.inventory
.remove(&"pulse_cannon:equipment".into());
}
for enemy in &world.enemies {
if !seen.iter().any(|(_, id)| id == &enemy.definition.0) {
seen.push((world.tick, enemy.definition.0.clone()));
}
}
if world.outcome != Outcome::Playing {
break;
}
}
if use_trial {
let weapon_state = &active.scripts.states["pulse_cannon/scripts/weapon.rhai"];
assert!(weapon_state["bursts"].as_i64().unwrap() > 0);
assert!(weapon_state["hits"].as_i64().unwrap() > 0);
let serialized = serde_json::to_string(&active.scripts.states).unwrap();
let recovered: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert_eq!(recovered["pulse_cannon/scripts/weapon.rhai"], *weapon_state);
}
(world, seen)
}
#[test]
fn restarted_mission_counts_new_turret_kills_independently_of_saved_statistics() {
let (_temporary, mut active) = active();
for _ in 0..2 {
let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
let entity = world
.spawn(&"iron_coast_turret:unit".into(), 144, 0, &active.registry)
.unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
world
.apply_commands(
&[modding::Command::Damage {
entity,
amount: 1000,
}],
&active.registry,
)
.unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.objectives["iron_coast_turret:defeated"], 1);
assert_eq!(world.outcome, Outcome::Playing);
}
assert_eq!(
active.scripts.states["iron_coast_turret/scripts/ai.rhai"]["destroyed"],
2
);
}
#[test]
fn starter_guns_can_complete_real_waves_and_boss() {
let (world, seen) = play(false);
assert_eq!(
world.outcome,
Outcome::Complete,
"tick={} shield={} objectives={:?}",
world.tick,
world.player.shield,
world.objectives
);
assert!(
seen.iter()
.any(|(tick, id)| *tick >= 720 && id == "iron_coast_boss:unit")
);
assert!(world.objectives["iron_coast_turret:defeated"] >= 2);
assert!(world.objectives["iron_coast_boss:defeated"] >= 1);
assert_eq!(world.objectives["iron_coast_mission_1:phase"], 4);
assert!(world.player.shield > 0);
assert!(world.tick > 720 && world.tick < 2000);
}
#[test]
fn trial_pulse_cannon_completes_and_preserves_scripted_weapon_state() {
let (world, _seen) = play(true);
assert_eq!(
world.outcome,
Outcome::Complete,
"tick={} shield={} objectives={:?}",
world.tick,
world.player.shield,
world.objectives
);
assert!(
world
.player
.inventory
.contains_key(&"pulse_cannon:equipment".into())
);
assert!(world.player.credits >= 5000);
}