Files
RustRaptor/mods/iron_coast_mission_6/validation/src/main.rs
T

311 lines
12 KiB
Rust

use raptor_replica::{
assets::Catalog,
content::{ContentPack, Registry},
modding::{self, ActiveMods},
persistence::Pilot,
simulation::{Input, Outcome, World},
};
use serde_json::{Value, json};
use std::{fs, path::PathBuf};
const MISSION: &str = "iron_coast_mission_6:mission_06";
const MODS: [&str; 8] = [
"iron_coast_scout",
"iron_coast_turret",
"iron_coast_boss_6",
"iron_coast_music",
"pulse_cannon",
"seeker_swarm",
"ion_lance",
"iron_coast_mission_6",
];
fn base(catalog: &Catalog) -> Registry {
let mut registry = Registry::base(catalog).unwrap();
let mut value = json!({"items":[]});
for n in 0..20 {
value["items"].as_array_mut().unwrap().push(json!({"id":format!("fixture:filler_{n}"),"name":"Neutral saved equipment","price":0,"max_count":1}));
}
let pack: ContentPack = serde_json::from_value(value).unwrap();
registry.apply(pack, "fixture").unwrap();
registry.validate().unwrap();
registry
}
fn active() -> ActiveMods {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let discovery = modding::discover(&root);
let enabled = MODS.iter().map(|id| id.to_string()).collect::<Vec<_>>();
let data = std::env::var_os("RAPTOR_TEST_DATA")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../../data"));
let catalog = Catalog::open(data).unwrap();
ActiveMods::build(&base(&catalog), &discovery, &enabled, Some(&catalog)).unwrap()
}
fn play(active: &mut ActiveMods, shield: u32, credits: u32, difficulty: u32, full: bool) -> Value {
active.scripts.restore(Default::default());
let mut pilot = Pilot {
credits,
difficulty,
..Pilot::default()
};
pilot.inventory.insert("base:energy".into(), shield);
if full {
let mut filler = 0;
while pilot.inventory_slots() < 20 {
pilot
.inventory
.insert(format!("fixture:filler_{filler}").as_str().into(), 1);
filler += 1;
}
}
let original = pilot.inventory.clone();
let mut world = World::new(&MISSION.into(), &pilot, &active.registry).unwrap();
let mut maximum_enemies = 0;
for _ in 0..5000 {
// A controlled mouse-like run: bounded lateral movement, always firing,
// aim first at the enemy closest to the player, keep the bottom lane.
let desired_x = world
.enemies
.iter()
.filter(|enemy| enemy.health > 0 && enemy.y >= 0)
.max_by_key(|enemy| enemy.y)
.map(|enemy| enemy.x + enemy.width / 2 - 16)
.unwrap_or(144);
let x = world.player.x + (desired_x - world.player.x).clamp(-4, 4);
let input = Input {
fire: true,
cycle: world.tick == 1,
absolute: Some([x, 160]),
..Input::default()
};
world
.step(input, &active.registry, &mut active.scripts)
.unwrap();
maximum_enemies = maximum_enemies.max(world.enemies.len());
if world.outcome != Outcome::Playing {
break;
}
}
assert_eq!(
world.outcome,
Outcome::Complete,
"tick={} shield={} turrets={:?} bosses={:?}",
world.tick,
world.player.shield,
world.objectives.get("iron_coast_turret:defeated"),
world.objectives.get("iron_coast_boss_6:defeated")
);
assert_eq!(world.objectives["iron_coast_turret:defeated"], 6);
assert_eq!(world.objectives["iron_coast_boss_6:defeated"], 2);
assert_eq!(world.objectives["iron_coast_mission_6:complete"], 1);
assert!(
!world
.player
.inventory
.contains_key(&"ion_lance:equipment".into())
);
if full {
for (id, count) in &original {
if id.0 != "base:energy" {
assert_eq!(world.player.inventory.get(id), Some(count));
}
}
assert_eq!(world.player.inventory.len(), 20);
}
let final_credits = world.player.credits;
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.player.credits, final_credits);
println!(
"Controlled run complete: difficulty={difficulty} starting_shield={shield} tick={} ending_shield={}",
world.tick, world.player.shield
);
json!({"full_inventory_no_loan":full,"starting_shield":shield,"starting_credits":credits,"difficulty":difficulty,
"outcome":"complete","tick":world.tick,"seconds":world.tick as f64 * 3.0 / 70.0,
"ending_shield":world.player.shield,"ending_credits":world.player.credits,
"maximum_enemies":maximum_enemies,"turrets_defeated":6,"cruisers_defeated":2,
"loan_returned":true,"reward_paid_once":true})
}
fn full_inventory(active: &mut ActiveMods) {
for reserves in [0, 4] {
active.scripts.restore(Default::default());
let mut pilot = Pilot::default();
if reserves > 0 {
pilot.inventory.insert("base:super_shield".into(), 100);
pilot.super_shield_reserves = vec![100; reserves];
}
let mut filler = 0;
while pilot.inventory_slots() < 20 {
pilot
.inventory
.insert(format!("fixture:filler_{filler}").as_str().into(), 1);
filler += 1;
}
let mut world = World::new(&MISSION.into(), &pilot, &active.registry).unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.player.inventory, pilot.inventory);
assert_eq!(
world.player.super_shield_reserves,
pilot.super_shield_reserves
);
assert!(
!active.scripts.states["iron_coast_mission_6/scripts/mission.rhai"]["loaned"]
.as_bool()
.unwrap()
);
}
}
fn mandatory_batteries_stay_reachable(active: &mut ActiveMods) {
active.scripts.restore(Default::default());
let pilot = Pilot {
inventory: [
("base:energy".into(), 100),
("base:super_shield".into(), 100),
]
.into(),
super_shield_reserves: vec![100; 4],
..Pilot::default()
};
let mut world = World::new(&MISSION.into(), &pilot, &active.registry).unwrap();
let mut positions = std::collections::BTreeMap::new();
let mut moving = std::collections::BTreeSet::new();
for _ in 0..1400 {
world
.step(
Input {
absolute: Some([280, 160]),
..Input::default()
},
&active.registry,
&mut active.scripts,
)
.unwrap();
for enemy in &world.enemies {
if enemy.definition.0.starts_with("base:enemy_") {
assert!(active.registry.enemies[&enemy.definition].legacy.is_some());
let key = (enemy.definition.0.clone(), enemy.entity);
if let Some(previous) = positions.insert(key.clone(), (enemy.x, enemy.y)) {
if previous != (enemy.x, enemy.y) {
moving.insert(enemy.definition.0.clone());
}
}
}
}
if world.outcome != Outcome::Playing {
break;
}
}
assert!(
moving.len() >= 4,
"Native loop/sweep movement must remain visible: {moving:?}"
);
assert_eq!(world.outcome, Outcome::Playing);
let turrets = world
.enemies
.iter()
.filter(|enemy| enemy.definition.0 == "iron_coast_turret:unit")
.collect::<Vec<_>>();
assert_eq!(turrets.len(), 6);
assert!(
turrets
.iter()
.all(|enemy| enemy.y <= 80 && enemy.health > 0)
);
}
fn state_resets_and_owned_lance_survives(active: &mut ActiveMods) {
active.scripts.restore(Default::default());
for _ in 0..2 {
let mut pilot = Pilot::default();
pilot.inventory.insert("ion_lance:equipment".into(), 1);
let mut world = World::new(&MISSION.into(), &pilot, &active.registry).unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.objectives["iron_coast_turret:defeated"], 0);
assert_eq!(world.objectives["iron_coast_boss_6:defeated"], 0);
world.tick = 2350;
world
.objectives
.insert("iron_coast_turret:defeated".into(), 6);
world
.objectives
.insert("iron_coast_boss_6:defeated".into(), 2);
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.outcome, Outcome::Complete);
assert_eq!(world.player.inventory[&"ion_lance:equipment".into()], 1);
assert!(
!active.scripts.states["iron_coast_mission_6/scripts/mission.rhai"]["loaned"]
.as_bool()
.unwrap()
);
}
}
fn failure_returns_loan(active: &mut ActiveMods) {
for owned in [false, true] {
active.scripts.restore(Default::default());
let mut pilot = Pilot::default();
if owned {
pilot.inventory.insert("ion_lance:equipment".into(), 1);
}
let mut world = World::new(&MISSION.into(), &pilot, &active.registry).unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
world
.apply_commands(
&[modding::Command::Damage {
entity: 0,
amount: 100000,
}],
&active.registry,
)
.unwrap();
world
.step(Input::default(), &active.registry, &mut active.scripts)
.unwrap();
assert_eq!(world.outcome, Outcome::Failed);
assert_eq!(world.objectives["iron_coast_mission_6:debrief"], 0);
assert_eq!(
world
.player
.inventory
.contains_key(&"ion_lance:equipment".into()),
owned
);
}
}
fn main() {
let mut active = active();
failure_returns_loan(&mut active);
println!("Failed sorties return only the loan and preserve owned Ion Lance");
full_inventory(&mut active);
println!("Full 20-slot inventory and physical shield module preservation passed");
mandatory_batteries_stay_reachable(&mut active);
println!("All six mandatory batteries remain reachable after scrolling passed");
state_resets_and_owned_lance_survives(&mut active);
println!("Restart reset and owned Ion Lance preservation passed");
let runs = [(75, 10_000), (100, 0)]
.into_iter()
.flat_map(|(shield, credits)| (0..=3).map(move |difficulty| (shield, credits, difficulty)))
.map(|(shield, credits, difficulty)| play(&mut active, shield, credits, difficulty, false))
.collect::<Vec<_>>();
let full_run = play(&mut active, 75, 10_000, 2, true);
let report = json!({"fixture":"Actual original catalog/native enemies, shipped mod PNG/WAV files and production deterministic simulation; neutral fixture items fill the no-loan inventory",
"run_count":runs.len()+1,"runs":runs,"full_inventory_run":full_run,"full_inventory_and_shield_reserves":true,"mandatory_batteries_remain_reachable":true,"classic_native_movement_observed":true,"restart_state_reset":true,"owned_weapon_preserved":true,"failure_loan_cleanup":true});
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../validation-results.json");
fs::write(path, serde_json::to_string_pretty(&report).unwrap() + "\n").unwrap();
println!("{}", serde_json::to_string_pretty(&report).unwrap());
}