311 lines
12 KiB
Rust
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());
|
|
}
|