200 lines
7.1 KiB
Rust
200 lines
7.1 KiB
Rust
//! Actual engine/script acceptance. Original assets stay outside this package.
|
|
use anyhow::{Result, ensure};
|
|
use raptor_replica::{
|
|
assets::Catalog,
|
|
content::{ContentId, Registry},
|
|
modding::{self, ActiveMods},
|
|
persistence::Pilot,
|
|
simulation::{Input, Outcome, World},
|
|
};
|
|
use serde_json::json;
|
|
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
path::PathBuf,
|
|
};
|
|
const MISSION: &str = "iron_coast_mission_2:mission_02";
|
|
const MODS: [&str; 8] = [
|
|
"iron_coast_scout",
|
|
"iron_coast_turret",
|
|
"iron_coast_boss_2",
|
|
"iron_coast_music",
|
|
"pulse_cannon",
|
|
"seeker_swarm",
|
|
"ion_lance",
|
|
"iron_coast_mission_2",
|
|
];
|
|
fn run(
|
|
active: &mut ActiveMods,
|
|
shield: i32,
|
|
credits: u32,
|
|
owned: bool,
|
|
full: bool,
|
|
) -> Result<serde_json::Value> {
|
|
eprintln!(
|
|
"Run: initial shield {shield}, credits {credits}, owned pulse {owned}, full inventory {full}"
|
|
);
|
|
let mut pilot = Pilot::default();
|
|
pilot.inventory.insert("base:energy".into(), shield as u32);
|
|
pilot.credits = credits;
|
|
pilot.mission = ContentId::new(MISSION)?;
|
|
pilot.difficulty = 2;
|
|
if owned {
|
|
pilot.inventory.insert("pulse_cannon:equipment".into(), 1);
|
|
}
|
|
if full {
|
|
for (id, item) in &active.registry.items {
|
|
if pilot.inventory_slots() >= 20 {
|
|
break;
|
|
}
|
|
if item.legacy == Some(16)
|
|
|| item.legacy == Some(17)
|
|
|| id.0 == "pulse_cannon:equipment"
|
|
{
|
|
continue;
|
|
}
|
|
pilot.inventory.insert(id.clone(), item.initial_count);
|
|
}
|
|
ensure!(
|
|
pilot.inventory_slots() == 20,
|
|
"full inventory fixture must use20 slots"
|
|
);
|
|
}
|
|
let original = pilot.inventory.clone();
|
|
let mut world = World::new(&pilot.mission, &pilot, &active.registry)?;
|
|
let mut cues = BTreeSet::new();
|
|
let mut seen = BTreeSet::new();
|
|
let mut native_positions = BTreeMap::new();
|
|
let mut native_moved = BTreeSet::new();
|
|
for _ in 0..2300 {
|
|
let x = world
|
|
.enemies
|
|
.iter()
|
|
.find(|e| e.definition.0 == "iron_coast_mission_2:relay" && e.health > 0)
|
|
.map(|e| e.x)
|
|
.unwrap_or(144);
|
|
world.step(
|
|
Input {
|
|
fire: true,
|
|
absolute: Some([world.player.x + (x - world.player.x).clamp(-4, 4), 160]),
|
|
..Input::default()
|
|
},
|
|
&active.registry,
|
|
&mut active.scripts,
|
|
)?;
|
|
for sound in &world.mod_sounds {
|
|
cues.insert(sound.0.clone());
|
|
}
|
|
for enemy in &world.enemies {
|
|
seen.insert(enemy.definition.0.clone());
|
|
if enemy.definition.0.starts_with("base:enemy_") {
|
|
ensure!(
|
|
active.registry.enemies[&enemy.definition].legacy.is_some(),
|
|
"native data remains native"
|
|
);
|
|
let entry = native_positions
|
|
.entry(enemy.entity)
|
|
.or_insert((enemy.x, enemy.y));
|
|
if *entry != (enemy.x, enemy.y) {
|
|
native_moved.insert(enemy.definition.0.clone());
|
|
}
|
|
}
|
|
}
|
|
if world.outcome != Outcome::Playing {
|
|
break;
|
|
}
|
|
}
|
|
ensure!(
|
|
world.outcome == Outcome::Complete,
|
|
"run ended {:?} tick{} shield{} objectives{:?}",
|
|
world.outcome,
|
|
world.tick,
|
|
world.player.shield,
|
|
world.objectives
|
|
);
|
|
ensure!(
|
|
world.objectives["iron_coast_mission_2:relays_disabled"] == 3,
|
|
"all3 relays are required"
|
|
);
|
|
ensure!(world.objectives["iron_coast_mission_2:debrief"] == 1);
|
|
ensure!(world.player.shield > 0);
|
|
ensure!(
|
|
world
|
|
.player
|
|
.inventory
|
|
.contains_key(&"pulse_cannon:equipment".into())
|
|
== owned,
|
|
"loan returned, owned gun preserved"
|
|
);
|
|
if full {
|
|
for (id, count) in &original {
|
|
if id.0 != "base:energy" && id.0 != "base:super_shield" {
|
|
ensure!(
|
|
world.player.inventory.get(id) == Some(count),
|
|
"full inventory equipment preserved: {id}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ensure!(
|
|
native_moved.len() == 3,
|
|
"all3 actual original enemies move before completion"
|
|
);
|
|
let before = world.player.credits;
|
|
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
|
ensure!(before == world.player.credits, "no repeated reward");
|
|
Ok(
|
|
json!({"shield_initial":shield,"credits_initial":credits,"owned_pulse":owned,"full_inventory":full,"tick":world.tick,"outcome":format!("{:?}",world.outcome),"shield_remaining":world.player.shield,"credits_final":world.player.credits,"credit_gain":world.player.credits-credits,"objectives":world.objectives,"seen_enemies":seen,"native_definitions_observed_moving":native_moved,"played_cues":cues,"digest":world.digest()}),
|
|
)
|
|
}
|
|
fn main() -> Result<()> {
|
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../../..")
|
|
.canonicalize()?;
|
|
let data = std::env::args()
|
|
.nth(1)
|
|
.map(PathBuf::from)
|
|
.unwrap_or(root.join("data"));
|
|
let mods = root.join("RustRaptor/mods");
|
|
let catalog = Catalog::open(data)?;
|
|
let base = Registry::base(&catalog)?;
|
|
let discovery = modding::discover(&mods);
|
|
let enabled = MODS.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
|
let mut active = ActiveMods::build(&base, &discovery, &enabled, Some(&catalog))?;
|
|
let results = vec![
|
|
run(&mut active, 75, 10000, false, false)?,
|
|
run(&mut active, 100, 0, false, false)?,
|
|
run(&mut active, 75, 10000, true, false)?,
|
|
run(&mut active, 75, 10000, false, true)?,
|
|
];
|
|
let mut gate = World::new(&MISSION.into(), &Pilot::default(), &active.registry)?;
|
|
gate.step(Input::default(), &active.registry, &mut active.scripts)?;
|
|
gate.objectives
|
|
.insert("iron_coast_boss_2:defeated".into(), 1);
|
|
gate.objectives
|
|
.insert("iron_coast_mission_2:relays_disabled".into(), 2);
|
|
gate.step(Input::default(), &active.registry, &mut active.scripts)?;
|
|
ensure!(
|
|
gate.outcome == Outcome::Playing,
|
|
"boss alone or only2 relays cannot complete"
|
|
);
|
|
gate.objectives
|
|
.insert("iron_coast_mission_2:relays_disabled".into(), 3);
|
|
gate.step(Input::default(), &active.registry, &mut active.scripts)?;
|
|
ensure!(
|
|
gate.outcome == Outcome::Complete && gate.player.credits == 7500,
|
|
"final objective awards7500 once"
|
|
);
|
|
gate.step(Input::default(), &active.registry, &mut active.scripts)?;
|
|
ensure!(
|
|
gate.player.credits == 7500,
|
|
"repeated step cannot pay again"
|
|
);
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(
|
|
&json!({"mission":MISSION,"strategy":"Held fire; y160; move at most 4 pixels/tick toward first living relay x, otherwise x144. No direct damage, shield or credit commands.","runs":results,"gating_checks":{"requires_three_relays_and_boss":true,"completion_reward_exactly_7500_once":true,"restart_state_reset":true}})
|
|
)?
|
|
);
|
|
Ok(())
|
|
}
|