236 lines
8.1 KiB
Rust
236 lines
8.1 KiB
Rust
//! Local mission acceptance probe. Compile against the RustRaptor library.
|
|
use raptor_replica::{
|
|
assets::Catalog,
|
|
content::Registry,
|
|
modding::{self, ActiveMods},
|
|
persistence::Pilot,
|
|
simulation::{Input, Outcome, World},
|
|
};
|
|
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
path::Path,
|
|
};
|
|
|
|
const MISSION: &str = "iron_coast_mission_3:mission_03";
|
|
const PACKAGES: [&str; 8] = [
|
|
"iron_coast_scout",
|
|
"iron_coast_turret",
|
|
"iron_coast_boss_3",
|
|
"pulse_cannon",
|
|
"seeker_swarm",
|
|
"ion_lance",
|
|
"iron_coast_music",
|
|
"iron_coast_mission_3",
|
|
];
|
|
|
|
fn active(root: &Path) -> ActiveMods {
|
|
let catalog = Catalog::open(root.join("data")).unwrap();
|
|
let base = Registry::base(&catalog).unwrap();
|
|
let discovery = modding::discover(&root.join("RustRaptor/mods"));
|
|
ActiveMods::build(
|
|
&base,
|
|
&discovery,
|
|
&PACKAGES.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
|
Some(&catalog),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn pilot(shield: u32, credits: u32, difficulty: u32) -> Pilot {
|
|
let mut pilot = Pilot::default();
|
|
pilot.mission = MISSION.into();
|
|
pilot.difficulty = difficulty;
|
|
pilot.credits = credits;
|
|
pilot.inventory.insert("base:energy".into(), shield);
|
|
pilot
|
|
}
|
|
|
|
fn main() {
|
|
let root = std::env::args().nth(1).expect("pass repository root");
|
|
let mut reports = Vec::new();
|
|
for (shield, credits, difficulty) in [
|
|
(75, 10_000, 1),
|
|
(75, 10_000, 2),
|
|
(75, 10_000, 3),
|
|
(100, 0, 2),
|
|
] {
|
|
let mut active = active(Path::new(&root));
|
|
let mut world = World::new(
|
|
&MISSION.into(),
|
|
&pilot(shield, credits, difficulty),
|
|
&active.registry,
|
|
)
|
|
.unwrap();
|
|
let mut native_positions = BTreeMap::new();
|
|
let mut native_seen = BTreeSet::new();
|
|
let mut native_moving = BTreeSet::new();
|
|
for _ in 0..3000 {
|
|
world
|
|
.step(
|
|
Input {
|
|
fire: true,
|
|
absolute: Some([144, 160]),
|
|
..Input::default()
|
|
},
|
|
&active.registry,
|
|
&mut active.scripts,
|
|
)
|
|
.unwrap();
|
|
for enemy in &world.enemies {
|
|
if enemy.definition.0.starts_with("base:enemy_") {
|
|
native_seen.insert(enemy.definition.0.clone());
|
|
if let Some(prior) = native_positions.insert(enemy.entity, (enemy.x, enemy.y)) {
|
|
if prior != (enemy.x, enemy.y) {
|
|
native_moving.insert(enemy.definition.0.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if world.outcome != Outcome::Playing {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(
|
|
world.outcome,
|
|
Outcome::Complete,
|
|
"difficulty={difficulty} tick={} shield={} objectives={:?}",
|
|
world.tick,
|
|
world.player.shield,
|
|
world.objectives
|
|
);
|
|
assert_eq!(
|
|
world.objectives["iron_coast_mission_3:convoys_intercepted"],
|
|
3
|
|
);
|
|
assert_eq!(world.objectives["iron_coast_mission_3:complete"], 1);
|
|
assert!(world.objectives["iron_coast_turret:defeated"] >= 2);
|
|
assert!(world.objectives["iron_coast_boss_3:defeated"] >= 1);
|
|
assert!(world.player.shield > 0);
|
|
for id in [
|
|
"base:enemy_0_20",
|
|
"base:enemy_0_21",
|
|
"base:enemy_0_22",
|
|
"base:enemy_0_73",
|
|
] {
|
|
assert!(native_seen.contains(id), "native enemy {id} never appeared");
|
|
assert!(native_moving.contains(id), "native enemy {id} never moved");
|
|
let original = &active.registry.enemies[&id.into()];
|
|
assert!(original.legacy.is_some() && !original.path.is_empty());
|
|
}
|
|
reports.push(format!("initial_shield={shield} initial_credits={credits} difficulty={difficulty} tick={} shield={} credits={} digest={}",
|
|
world.tick,world.player.shield,world.player.credits,world.digest()));
|
|
}
|
|
// All essential targets survive unattended indefinitely in the corridor.
|
|
let mut active = active(Path::new(&root));
|
|
let mut world = World::new(&MISSION.into(), &pilot(75, 10_000, 2), &active.registry).unwrap();
|
|
for _ in 0..2200 {
|
|
world.player.shield = 100; // Retention test only; never used in playthroughs.
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
}
|
|
assert_eq!(world.outcome, Outcome::Playing);
|
|
let cargo = world
|
|
.enemies
|
|
.iter()
|
|
.filter(|e| e.definition.0 == "iron_coast_mission_3:convoy" && e.health > 0)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(cargo.len(), 3);
|
|
assert!(cargo.iter().all(|e| e.y == 44));
|
|
let escorts = world
|
|
.enemies
|
|
.iter()
|
|
.filter(|e| e.definition.0 == "iron_coast_turret:unit" && e.health > 0)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(escorts.len(), 2);
|
|
assert!(escorts.iter().all(|e| e.y == 74));
|
|
assert!(
|
|
world
|
|
.enemies
|
|
.iter()
|
|
.any(|e| e.definition.0 == "iron_coast_boss_3:unit" && e.health > 0 && e.y == 24)
|
|
);
|
|
// Starting again resets restored state, and no grants consume inventory slots.
|
|
let mut full = pilot(75, 10_000, 2);
|
|
for item in active.registry.items.keys() {
|
|
if full.inventory.len() == 20 {
|
|
break;
|
|
}
|
|
if item.0 != "base:super_shield" {
|
|
full.inventory.entry(item.clone()).or_insert(1);
|
|
}
|
|
}
|
|
assert_eq!(full.inventory.len(), 20);
|
|
let before = full.inventory.clone();
|
|
let mut restarted = World::new(&MISSION.into(), &full, &active.registry).unwrap();
|
|
restarted
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(restarted.player.inventory, before);
|
|
assert_eq!(
|
|
restarted.objectives["iron_coast_mission_3:convoys_intercepted"],
|
|
0
|
|
);
|
|
// Boss and escorts alone cannot win; all cargo is required. Reward paid once.
|
|
restarted
|
|
.objectives
|
|
.insert("iron_coast_boss_3:defeated".into(), 1);
|
|
restarted
|
|
.objectives
|
|
.insert("iron_coast_turret:defeated".into(), 2);
|
|
restarted
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(restarted.outcome, Outcome::Playing);
|
|
restarted
|
|
.objectives
|
|
.insert("iron_coast_mission_3:convoys_intercepted".into(), 3);
|
|
restarted
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(restarted.outcome, Outcome::Complete);
|
|
assert_eq!(restarted.player.credits, 17_500);
|
|
restarted
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(restarted.player.credits, 17_500);
|
|
let mut full_flight = World::new(&MISSION.into(), &full, &active.registry).unwrap();
|
|
for _ in 0..3000 {
|
|
full_flight
|
|
.step(
|
|
Input {
|
|
fire: true,
|
|
absolute: Some([144, 160]),
|
|
..Input::default()
|
|
},
|
|
&active.registry,
|
|
&mut active.scripts,
|
|
)
|
|
.unwrap();
|
|
if full_flight.outcome != Outcome::Playing {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(full_flight.outcome, Outcome::Complete);
|
|
assert_eq!(full_flight.player.inventory.len(), 20);
|
|
for (item, count) in &before {
|
|
if item.0 != "base:energy" {
|
|
assert_eq!(
|
|
full_flight.player.inventory.get(item),
|
|
Some(count),
|
|
"equipment changed: {item}"
|
|
);
|
|
}
|
|
}
|
|
reports.push(format!(
|
|
"full_inventory=20 tick={} shield={} credits={}",
|
|
full_flight.tick, full_flight.player.shield, full_flight.player.credits
|
|
));
|
|
for report in reports {
|
|
println!("{report}");
|
|
}
|
|
println!(
|
|
"retained cargo/escorts/boss; full inventory; restart reset; gated completion; single reward: passed"
|
|
);
|
|
}
|