345 lines
11 KiB
Rust
345 lines
11 KiB
Rust
//! Compile with validate.py; runs the actual simulation and Rhai callbacks.
|
|
use raptor_replica::{
|
|
assets::Catalog,
|
|
content::{ContentId, Registry},
|
|
modding::{self, ActiveMods, Command},
|
|
persistence::Pilot,
|
|
simulation::{Input, Outcome, World},
|
|
};
|
|
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
path::PathBuf,
|
|
};
|
|
|
|
const MISSION: &str = "iron_coast_mission_4:mission_04";
|
|
const MODS: [&str; 8] = [
|
|
"iron_coast_scout",
|
|
"iron_coast_turret",
|
|
"iron_coast_boss_4",
|
|
"iron_coast_music",
|
|
"pulse_cannon",
|
|
"seeker_swarm",
|
|
"ion_lance",
|
|
"iron_coast_mission_4",
|
|
];
|
|
|
|
fn active() -> ActiveMods {
|
|
let project = PathBuf::from(std::env::var("RAPTOR_PROJECT").unwrap());
|
|
let catalog = Catalog::open(project.join("data")).unwrap();
|
|
let base = Registry::base(&catalog).unwrap();
|
|
let discovery = modding::discover(&project.join("RustRaptor/mods"));
|
|
ActiveMods::build(
|
|
&base,
|
|
&discovery,
|
|
&MODS.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
|
Some(&catalog),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn pilot(shield: u32) -> Pilot {
|
|
let mut pilot = Pilot::default();
|
|
pilot.inventory.insert("base:energy".into(), shield);
|
|
pilot.credits = if shield == 75 { 10_000 } else { 0 };
|
|
pilot
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct NativeProof {
|
|
seen: BTreeSet<String>,
|
|
moved: BTreeSet<String>,
|
|
positions: BTreeMap<u64, (i32, i32)>,
|
|
}
|
|
|
|
fn play(pilot: &Pilot, active: &mut ActiveMods) -> (World, NativeProof) {
|
|
let mut world = World::new(&MISSION.into(), pilot, &active.registry).unwrap();
|
|
let mut proof = NativeProof::default();
|
|
for _ in 0..4000 {
|
|
for enemy in &world.enemies {
|
|
if enemy.definition.0.starts_with("base:enemy_") {
|
|
proof.seen.insert(enemy.definition.0.clone());
|
|
let prior = proof.positions.insert(enemy.entity, (enemy.x, enemy.y));
|
|
if prior.is_some_and(|p| p != (enemy.x, enemy.y)) {
|
|
proof.moved.insert(enemy.definition.0.clone());
|
|
}
|
|
}
|
|
}
|
|
let target = world
|
|
.enemies
|
|
.iter()
|
|
.filter(|e| e.health > 0 && e.y >= 0 && e.y < 130)
|
|
.min_by_key(|e| {
|
|
(
|
|
if e.definition.0 == "iron_coast_turret:unit" {
|
|
0
|
|
} else if e.definition.0 == "iron_coast_boss_4:unit" {
|
|
1
|
|
} else {
|
|
2
|
|
},
|
|
(world.player.x - e.x).abs(),
|
|
)
|
|
});
|
|
let target_x = target.map(|e| e.x + e.width / 2 - 16).unwrap_or(144);
|
|
let x = world.player.x + (target_x - world.player.x).clamp(-4, 4);
|
|
world
|
|
.step(
|
|
Input {
|
|
fire: true,
|
|
absolute: Some([x, 160]),
|
|
..Input::default()
|
|
},
|
|
&active.registry,
|
|
&mut active.scripts,
|
|
)
|
|
.unwrap();
|
|
if world.outcome != Outcome::Playing {
|
|
break;
|
|
}
|
|
}
|
|
(world, proof)
|
|
}
|
|
|
|
#[test]
|
|
fn starter_registration_and_default_fixture_complete_with_real_inputs() {
|
|
for shield in [75, 100] {
|
|
let mut active = active();
|
|
let (world, proof) = play(&pilot(shield), &mut active);
|
|
eprintln!(
|
|
"Stormwall start={shield}, tick={}, outcome={:?}, shield={}, credits={}, objectives={:?}",
|
|
world.tick, world.outcome, world.player.shield, world.player.credits, world.objectives
|
|
);
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert!(world.player.shield > 0);
|
|
assert_eq!(world.objectives["iron_coast_mission_4:complete"], 1);
|
|
assert!(world.objectives["iron_coast_turret:defeated"] >= 4);
|
|
assert!(world.objectives["iron_coast_boss_4:defeated"] >= 1);
|
|
assert_eq!(proof.seen.len(), 6, "native seen: {:?}", proof.seen);
|
|
assert_eq!(proof.moved.len(), 6, "native moved: {:?}", proof.moved);
|
|
for id in &proof.seen {
|
|
let def = &active.registry.enemies[&ContentId::from(id.as_str())];
|
|
assert!(def.legacy.is_some());
|
|
assert!(def.script.is_none());
|
|
assert!(def.path.len() >= 3);
|
|
assert_eq!(
|
|
active.registry.owners[&ContentId::from(id.as_str())],
|
|
"base"
|
|
);
|
|
}
|
|
assert!(
|
|
!world
|
|
.player
|
|
.inventory
|
|
.contains_key(&ContentId::from("pulse_cannon:equipment"))
|
|
);
|
|
assert_eq!(
|
|
active.scripts.states["iron_coast_mission_4/scripts/mission.rhai"]["loaned"],
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn successful_replay_reuses_script_state_without_stale_death_flags() {
|
|
let mut active = active();
|
|
for _ in 0..2 {
|
|
let (world, proof) = play(&pilot(75), &mut active);
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert_eq!(proof.moved.len(), 6);
|
|
assert_eq!(world.objectives["iron_coast_boss_4:defeated"], 1);
|
|
assert_eq!(world.objectives["iron_coast_mission_4:debrief"], 1);
|
|
assert!(
|
|
!world
|
|
.player
|
|
.inventory
|
|
.contains_key(&ContentId::from("pulse_cannon:equipment"))
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn owned_cannon_and_full_inventory_can_win_without_losing_equipment() {
|
|
let mut active = active();
|
|
let mut owned = pilot(75);
|
|
owned.inventory.insert("pulse_cannon:equipment".into(), 1);
|
|
let (world, _) = play(&owned, &mut active);
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert_eq!(
|
|
world.player.inventory[&ContentId::from("pulse_cannon:equipment")],
|
|
1
|
|
);
|
|
|
|
let mut full = pilot(75);
|
|
for id in active.registry.items.keys() {
|
|
if full.inventory_slots() == 20 {
|
|
break;
|
|
}
|
|
if id.0.starts_with("base:") && id.0 != "base:super_shield" {
|
|
full.inventory.entry(id.clone()).or_insert(1);
|
|
}
|
|
}
|
|
assert_eq!(full.inventory_slots(), 20);
|
|
let original = full.inventory.clone();
|
|
let (world, _) = play(&full, &mut active);
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert_eq!(world.objectives["iron_coast_mission_4:trial_issued"], 0);
|
|
assert!(
|
|
!world
|
|
.player
|
|
.inventory
|
|
.contains_key(&ContentId::from("pulse_cannon:equipment"))
|
|
);
|
|
for (id, count) in original {
|
|
if id.0 != "base:energy" {
|
|
assert_eq!(world.player.inventory[&id], count, "changed equipment {id}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn late_surviving_turrets_remain_reachable() {
|
|
let mut active = active();
|
|
let mut world = World::new(&MISSION.into(), &pilot(100), &active.registry).unwrap();
|
|
// Real movement avoids incoming aimed fire without killing the objectives.
|
|
for tick in 0..1050 {
|
|
let x = if (tick / 40) % 2 == 0 { 32 } else { 128 };
|
|
world
|
|
.step(
|
|
Input {
|
|
absolute: Some([x, 140]),
|
|
..Input::default()
|
|
},
|
|
&active.registry,
|
|
&mut active.scripts,
|
|
)
|
|
.unwrap();
|
|
if world.outcome != Outcome::Playing {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(world.outcome, Outcome::Playing);
|
|
let turrets: Vec<_> = world
|
|
.enemies
|
|
.iter()
|
|
.filter(|e| e.definition.0 == "iron_coast_turret:unit" && e.health > 0)
|
|
.collect();
|
|
assert_eq!(turrets.len(), 4);
|
|
assert!(turrets.iter().all(|e| (0..=72).contains(&e.y)));
|
|
}
|
|
|
|
#[test]
|
|
fn full_inventory_is_unchanged_and_owned_cannon_survives_failure() {
|
|
let mut active = active();
|
|
let mut full = pilot(100);
|
|
for id in active.registry.items.keys() {
|
|
if full.inventory_slots() == 20 {
|
|
break;
|
|
}
|
|
if id.0 != "pulse_cannon:equipment" && id.0 != "base:super_shield" {
|
|
full.inventory.entry(id.clone()).or_insert(1);
|
|
}
|
|
}
|
|
assert_eq!(full.inventory_slots(), 20);
|
|
let before = full.inventory.clone();
|
|
let mut world = World::new(&MISSION.into(), &full, &active.registry).unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.player.inventory, before);
|
|
assert_eq!(world.objectives["iron_coast_mission_4:trial_issued"], 0);
|
|
let mut owned = pilot(100);
|
|
owned.inventory.insert("pulse_cannon:equipment".into(), 1);
|
|
let mut world = World::new(&MISSION.into(), &owned, &active.registry).unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
world
|
|
.apply_commands(
|
|
&[Command::Damage {
|
|
entity: 0,
|
|
amount: 100_000,
|
|
}],
|
|
&active.registry,
|
|
)
|
|
.unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.outcome, Outcome::Failed);
|
|
assert_eq!(
|
|
world.player.inventory[&ContentId::from("pulse_cannon:equipment")],
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_loan_is_returned_and_retry_resets_mission_state() {
|
|
let mut active = active();
|
|
for _ in 0..2 {
|
|
let mut world = World::new(&MISSION.into(), &pilot(100), &active.registry).unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(
|
|
world.player.inventory[&ContentId::from("pulse_cannon:equipment")],
|
|
1
|
|
);
|
|
world
|
|
.apply_commands(
|
|
&[Command::Damage {
|
|
entity: 0,
|
|
amount: 100_000,
|
|
}],
|
|
&active.registry,
|
|
)
|
|
.unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.outcome, Outcome::Failed);
|
|
assert!(
|
|
!world
|
|
.player
|
|
.inventory
|
|
.contains_key(&ContentId::from("pulse_cannon:equipment"))
|
|
);
|
|
assert_eq!(world.objectives["iron_coast_mission_4:debrief"], 0);
|
|
assert_eq!(
|
|
active.scripts.states["iron_coast_mission_4/scripts/mission.rhai"]["loaned"],
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn boss_alone_cannot_win_and_reward_is_paid_once() {
|
|
let mut active = active();
|
|
let mut world = World::new(&MISSION.into(), &pilot(100), &active.registry).unwrap();
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
world
|
|
.objectives
|
|
.insert("iron_coast_boss_4:defeated".into(), 1);
|
|
world
|
|
.objectives
|
|
.insert("iron_coast_turret:defeated".into(), 3);
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.outcome, Outcome::Playing);
|
|
world
|
|
.objectives
|
|
.insert("iron_coast_turret:defeated".into(), 4);
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.outcome, Outcome::Complete);
|
|
assert_eq!(world.player.credits, 12000);
|
|
world
|
|
.step(Input::default(), &active.registry, &mut active.scripts)
|
|
.unwrap();
|
|
assert_eq!(world.player.credits, 12000);
|
|
assert_eq!(world.objectives["iron_coast_mission_4:debrief"], 1);
|
|
}
|