Make RustRaptor the repository root and track its playable bundle
This commit is contained in:
1 parent
4b1edefeec
commit
8f459796b1
9860 files changed
+21084
-61019
No files matched your search
@@ -0,0 +1,244 @@
|
||||
//! Standalone mod acceptance without original copyrighted game data.
|
||||
use anyhow::{Result, ensure};
|
||||
use raptor_replica::{
|
||||
content::{ContentId, ContentPack, Registry},
|
||||
modding::{self, ActiveMods, Command, ScriptRuntime},
|
||||
persistence::Pilot,
|
||||
simulation::{Input, Outcome, Projectile, World},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::PathBuf;
|
||||
const OWNER: &str = "iron_coast_boss_2";
|
||||
const SCRIPT: &str = "scripts/ai.rhai";
|
||||
const STATE: &str = "iron_coast_boss_2/scripts/ai.rhai";
|
||||
const OBJECTIVE: &str = "iron_coast_boss_2:defeated";
|
||||
|
||||
fn event(entity: u64, health: i32, x: i32, y: i32) -> Value {
|
||||
json!({"entity":entity,"health":health,"x":x,"y":y,"tick":900,
|
||||
"objectives":{},"entities":[{"entity":entity,"health":health,"x":x,"y":y}]})
|
||||
}
|
||||
fn invoke(runtime: &mut ScriptRuntime, hook: &str, event: Value) -> Result<Vec<Command>> {
|
||||
runtime.begin_tick();
|
||||
runtime.invoke(OWNER, SCRIPT, hook, &event, 31)
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let mods = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.canonicalize()?;
|
||||
let mut base = Registry::default();
|
||||
let content: ContentPack = serde_json::from_value(json!({
|
||||
"weapons":[{"id":"base:projectile_forward_guns","name":"Forward guns","damage":1000,"speed":8,"cooldown":2,"behavior":"straight","target":"all"}],
|
||||
"items":[{"id":"base:forward_guns","name":"Forward guns","price":0,"weapon":"base:projectile_forward_guns"},
|
||||
{"id":"base:energy","name":"Shield","price":0,"max_count":100}],
|
||||
"missions":[{"id":"base:sector_1_wave_1","name":"Boss test","duration":10000,"auto_complete":false,
|
||||
"spawns":[{"tick":1,"enemy":"iron_coast_boss_2:unit","x":112,"y":-64}]}]
|
||||
}))?;
|
||||
base.apply(content, "base")?;
|
||||
let discovery = modding::discover(&mods);
|
||||
let mut active = ActiveMods::build(&base, &discovery, &[OWNER.into()], None)?;
|
||||
let def = &active.registry.enemies[&ContentId::from("iron_coast_boss_2:unit")];
|
||||
ensure!(def.health == 160 && def.reward == 1800 && def.boss && def.speed == 0);
|
||||
let rt = &mut active.scripts;
|
||||
let mut cues = Vec::new();
|
||||
invoke(rt, "on_spawn", event(1, 160, 112, 24))?;
|
||||
let mut first = Vec::new();
|
||||
let mut second = Vec::new();
|
||||
for age in 1..=108 {
|
||||
let commands = invoke(rt, "on_tick", event(1, 160, 112, 24))?;
|
||||
for command in &commands {
|
||||
if let Command::PlaySound { sound } = command {
|
||||
cues.push((age, sound.clone()));
|
||||
}
|
||||
}
|
||||
let shots: Vec<_> = commands
|
||||
.iter()
|
||||
.filter_map(|command| {
|
||||
if let Command::Projectile {
|
||||
x,
|
||||
y,
|
||||
vx,
|
||||
vy,
|
||||
hostile,
|
||||
weapon,
|
||||
} = command
|
||||
{
|
||||
Some((*x, *y, *vx, *vy, *hostile, weapon.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if age == 54 {
|
||||
first = shots;
|
||||
} else if age == 108 {
|
||||
second = shots;
|
||||
} else {
|
||||
ensure!(shots.is_empty(), "unexpected volley at age {age}");
|
||||
}
|
||||
}
|
||||
ensure!(first.len() == 2 && second.len() == 2 && first != second);
|
||||
ensure!(first.iter().all(|p| p.2 == 0 && p.3 == 4 && p.4));
|
||||
ensure!(second[0].2 == -1 && second[1].2 == 1);
|
||||
ensure!(
|
||||
cues == vec![
|
||||
(42, format!("{OWNER}:warning")),
|
||||
(54, format!("{OWNER}:shot")),
|
||||
(96, format!("{OWNER}:warning")),
|
||||
(108, format!("{OWNER}:shot"))
|
||||
]
|
||||
);
|
||||
invoke(rt, "on_spawn", event(2, 160, 112, 24))?;
|
||||
ensure!(rt.states[STATE]["ages"]["2"] == 0);
|
||||
ensure!(
|
||||
invoke(rt, "on_death", event(1, 160, 112, 24))?.is_empty(),
|
||||
"living unit cannot register a death"
|
||||
);
|
||||
ensure!(
|
||||
invoke(rt, "on_tick", event(1, 0, 112, 24))?.is_empty(),
|
||||
"dead unit cannot fire or warn"
|
||||
);
|
||||
let mut death = event(1, 0, 112, 24);
|
||||
death["entities"] = json!([{"entity":2,"health":160}]);
|
||||
let commands = invoke(rt, "on_death", death.clone())?;
|
||||
ensure!(
|
||||
commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::Objective{key,value} if key==OBJECTIVE && *value==1))
|
||||
);
|
||||
ensure!(commands.iter().all(|c| !matches!(c, Command::Complete)));
|
||||
ensure!(
|
||||
invoke(rt, "on_death", death)?.is_empty(),
|
||||
"duplicate death cannot increment objective"
|
||||
);
|
||||
ensure!(rt.states[STATE]["ages"].get("1").is_none());
|
||||
ensure!(
|
||||
rt.states[STATE]["ages"]["2"] == 0,
|
||||
"another entity retains independent cursor"
|
||||
);
|
||||
let mut death2 = event(2, 0, 112, 24);
|
||||
death2["entities"] = json!([]);
|
||||
death2["objectives"] = json!({OBJECTIVE:1});
|
||||
let commands = invoke(rt, "on_death", death2)?;
|
||||
ensure!(
|
||||
commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::Objective{key,value} if key==OBJECTIVE && *value==2))
|
||||
);
|
||||
ensure!(rt.states[STATE]["ages"].as_object().unwrap().is_empty());
|
||||
ensure!(
|
||||
rt.states[STATE]["directions"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// Run actual engine hooks and command application; no scripted mission completion.
|
||||
active = ActiveMods::build(&base, &discovery, &[OWNER.into()], None)?;
|
||||
let pilot = Pilot::default();
|
||||
let mut world = World::new(&pilot.mission, &pilot, &active.registry)?;
|
||||
let mut actual_cues = std::collections::BTreeSet::new();
|
||||
for _ in 0..150 {
|
||||
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
for cue in &world.mod_sounds {
|
||||
actual_cues.insert(cue.0.clone());
|
||||
}
|
||||
}
|
||||
ensure!(world.enemies.len() == 1 && world.enemies[0].y == 24);
|
||||
ensure!(world.enemies[0].x >= 79 && world.enemies[0].x <= 145);
|
||||
ensure!(
|
||||
actual_cues.contains("iron_coast_boss_2:warning")
|
||||
&& actual_cues.contains("iron_coast_boss_2:shot")
|
||||
);
|
||||
let initial_credits = world.player.credits;
|
||||
world.enemies[0].health = 0;
|
||||
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(world.objectives[OBJECTIVE] == 1 && world.enemies.is_empty());
|
||||
ensure!(world.player.credits == initial_credits + 1800);
|
||||
ensure!(
|
||||
world.outcome == Outcome::Playing,
|
||||
"boss never completes the mission itself"
|
||||
);
|
||||
let shots = active.scripts.states[STATE]["shots"].clone();
|
||||
for _ in 0..120 {
|
||||
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
}
|
||||
ensure!(world.objectives[OBJECTIVE] == 1 && active.scripts.states[STATE]["shots"] == shots);
|
||||
ensure!(
|
||||
active.scripts.states[STATE]["ages"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
// A projectile can kill a scheduled spawn during advance, before on_spawn dispatch.
|
||||
// Use an actual stationary projectile inside the arriving boss silhouette.
|
||||
let mut early_active = ActiveMods::build(&base, &discovery, &[OWNER.into()], None)?;
|
||||
let mut early_world = World::new(&pilot.mission, &pilot, &early_active.registry)?;
|
||||
early_world.step(
|
||||
Input::default(),
|
||||
&early_active.registry,
|
||||
&mut early_active.scripts,
|
||||
)?;
|
||||
early_world.projectiles.push(Projectile {
|
||||
entity: 9000,
|
||||
weapon: ContentId::from("base:projectile_forward_guns"),
|
||||
x: 160,
|
||||
y: -40,
|
||||
px: 160,
|
||||
py: -40,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
hostile: false,
|
||||
age: 0,
|
||||
legacy: None,
|
||||
legacy_enemy: None,
|
||||
});
|
||||
early_world.step(
|
||||
Input::default(),
|
||||
&early_active.registry,
|
||||
&mut early_active.scripts,
|
||||
)?;
|
||||
ensure!(
|
||||
early_world.enemies.len() == 1 && early_world.enemies[0].health <= 0,
|
||||
"lethal projectile must kill the boss in its spawn step"
|
||||
);
|
||||
early_world.step(
|
||||
Input::default(),
|
||||
&early_active.registry,
|
||||
&mut early_active.scripts,
|
||||
)?;
|
||||
ensure!(
|
||||
early_world.objectives.get(OBJECTIVE) == Some(&1),
|
||||
"queued spawn-step kill must increment its actual-death objective"
|
||||
);
|
||||
ensure!(
|
||||
early_active.scripts.states[STATE]["ages"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
// Reusing an entity ID in a new mission must count its new life, while another
|
||||
// death notification for that life must stay harmless.
|
||||
let rt = &mut early_active.scripts;
|
||||
invoke(rt, "on_spawn", event(1, 160, 112, 24))?;
|
||||
let mut reused = event(1, 0, 112, 24);
|
||||
reused["entities"] = json!([]);
|
||||
reused["objectives"] = json!({OBJECTIVE: 1});
|
||||
let reused_commands = invoke(rt, "on_death", reused.clone())?;
|
||||
ensure!(reused_commands.iter().any(|c| matches!(c,
|
||||
Command::Objective { key, value } if key == OBJECTIVE && *value == 2)));
|
||||
ensure!(invoke(rt, "on_death", reused)?.is_empty());
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"mod":OWNER,"package_validation":"passed; PNGs, sound decoding, manifest and script",
|
||||
"warning_ages":[42,96],"volley_ages":[54,108],"first_volley":first,"second_volley":second,
|
||||
"actual_engine_tick":world.tick,"death_objective":world.objectives[OBJECTIVE],
|
||||
"reward":world.player.credits-initial_credits,"outcome":format!("{:?}",world.outcome),
|
||||
"per_entity_state_cleanup":"passed","dead_or_repeated_callbacks":"no commands",
|
||||
"actual_cues":actual_cues,
|
||||
"queued_spawn_step_lethal_projectile":"passed",
|
||||
"reused_entity_id_new_life":"counts once"
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user