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,221 @@
|
||||
//! Package-local callback acceptance probe; compile against raptor_replica and serde_json.
|
||||
use raptor_replica::{
|
||||
content::{ContentPack, Registry},
|
||||
modding::{self, ActiveMods, Command, ScriptRuntime},
|
||||
persistence::Pilot,
|
||||
simulation::{Input, World},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::Path;
|
||||
|
||||
const OWNER: &str = "iron_coast_boss_3";
|
||||
const SCRIPT: &str = "scripts/ai.rhai";
|
||||
|
||||
fn runtime(path: &Path) -> ScriptRuntime {
|
||||
let mut result = ScriptRuntime::new();
|
||||
result
|
||||
.add(
|
||||
OWNER,
|
||||
SCRIPT,
|
||||
&std::fs::read_to_string(path.join(SCRIPT)).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
result
|
||||
}
|
||||
|
||||
fn event(entity: u64, x: i32, y: i32, health: i32) -> Value {
|
||||
json!({"entity":entity, "x":x, "y":y, "health":health, "tick":999,
|
||||
"entities":[{"entity":entity,"health":health}],"objectives":{}})
|
||||
}
|
||||
|
||||
fn call(runtime: &mut ScriptRuntime, hook: &str, event: Value) -> Vec<Command> {
|
||||
runtime.invoke(OWNER, SCRIPT, hook, &event, 303).unwrap()
|
||||
}
|
||||
|
||||
fn queued_spawn_replay(package: &Path) {
|
||||
let mut base = Registry::default();
|
||||
let content: ContentPack = serde_json::from_value(json!({
|
||||
"weapons":[{"id":"base:projectile_forward_guns","name":"Forward Guns","damage":1,"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":"Spawn lifecycle collision regression","duration":500,"auto_complete":false,
|
||||
"spawns":[{"tick":0,"enemy":"iron_coast_boss_3:unit","x":112,"y":120}]}]
|
||||
})).unwrap();
|
||||
base.apply(content, "base").unwrap();
|
||||
let discovery = modding::discover(package.parent().unwrap());
|
||||
let mut active = ActiveMods::build(&base, &discovery, &[OWNER.to_string()], None).unwrap();
|
||||
// A real collision during advance kills this fragile boss before dispatch.
|
||||
// Its on_spawn callback is queued before on_death, and both see no live boss.
|
||||
active
|
||||
.registry
|
||||
.enemies
|
||||
.get_mut(&"iron_coast_boss_3:unit".into())
|
||||
.unwrap()
|
||||
.health = 1;
|
||||
let mut pilot = Pilot::default();
|
||||
pilot.difficulty = 2;
|
||||
pilot.credits = 0;
|
||||
let mut reused_id = None;
|
||||
for replay in 0..3 {
|
||||
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
|
||||
world
|
||||
.step(
|
||||
Input {
|
||||
absolute: Some([144, 160]),
|
||||
..Input::default()
|
||||
},
|
||||
&active.registry,
|
||||
&mut active.scripts,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
world.enemies.is_empty(),
|
||||
"collision must remove the boss before dispatch"
|
||||
);
|
||||
assert_eq!(
|
||||
world.objectives.get("iron_coast_boss_3:defeated"),
|
||||
Some(&1),
|
||||
"replay {replay} must count its new lifecycle"
|
||||
);
|
||||
assert_eq!(
|
||||
world.player.credits, 1800,
|
||||
"exactly one engine reward per lifecycle"
|
||||
);
|
||||
let checkpoints = active.scripts.checkpoints();
|
||||
let state = &checkpoints.0["iron_coast_boss_3/scripts/ai.rhai"];
|
||||
let ids = state["dead"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(ids.len(), 1);
|
||||
if let Some(ref previous) = reused_id {
|
||||
assert_eq!(previous, ids[0]);
|
||||
} else {
|
||||
reused_id = Some(ids[0].clone());
|
||||
}
|
||||
assert!(state["units"].as_object().unwrap().is_empty());
|
||||
assert!(world.projectiles.is_empty(), "dead spawn must not shoot");
|
||||
assert_eq!(world.mod_sounds, vec!["iron_coast_boss_3:death".into()]);
|
||||
world
|
||||
.step(Input::default(), &active.registry, &mut active.scripts)
|
||||
.unwrap();
|
||||
assert_eq!(world.player.credits, 1800);
|
||||
assert_eq!(world.objectives.get("iron_coast_boss_3:defeated"), Some(&1));
|
||||
// Reconstruct the runtime from saved state exactly as a pilot load does.
|
||||
let mut restored = runtime(package);
|
||||
restored.restore(checkpoints);
|
||||
active.scripts = restored;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("pass package directory");
|
||||
let path = Path::new(&path);
|
||||
let mut ai = runtime(path);
|
||||
call(&mut ai, "on_spawn", event(1, 112, -64, 160));
|
||||
for _ in 0..20 {
|
||||
let commands = call(&mut ai, "on_tick", event(1, 112, -64, 160));
|
||||
assert!(
|
||||
!commands
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Projectile { .. } | Command::PlaySound { .. }))
|
||||
);
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
let mut volleys = Vec::new();
|
||||
for tick in 1..=108 {
|
||||
let commands = call(&mut ai, "on_tick", event(1, 112, 24, 160));
|
||||
if commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::PlaySound{sound} if sound=="iron_coast_boss_3:warning"))
|
||||
{
|
||||
warnings.push(tick);
|
||||
}
|
||||
let shells = commands
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
Command::Projectile {
|
||||
weapon,
|
||||
x,
|
||||
y,
|
||||
vx,
|
||||
vy,
|
||||
hostile,
|
||||
} => {
|
||||
assert_eq!(weapon, "iron_coast_boss_3:projectile");
|
||||
assert_eq!((*y, *vy, *hostile), (78, 4, true));
|
||||
Some((*x, *vx))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !shells.is_empty() {
|
||||
volleys.push((tick, shells));
|
||||
}
|
||||
}
|
||||
assert_eq!(warnings, vec![45, 99]);
|
||||
assert_eq!(
|
||||
volleys,
|
||||
vec![
|
||||
(54, vec![(147, 0), (147, 1)]),
|
||||
(108, vec![(173, 0), (173, -1)])
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
call(&mut ai, "on_tick", event(1, 144, 24, 160))
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Velocity { vx: -1, vy: 0, .. }))
|
||||
);
|
||||
assert!(
|
||||
call(&mut ai, "on_tick", event(1, 80, 24, 160))
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Velocity { vx: 1, vy: 0, .. }))
|
||||
);
|
||||
// A second boss starts a separate clock and cannot inherit the first gun phase.
|
||||
call(&mut ai, "on_spawn", event(2, 112, 24, 160));
|
||||
assert!(
|
||||
!call(&mut ai, "on_tick", event(2, 112, 24, 160))
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Projectile { .. }))
|
||||
);
|
||||
let mut restored = runtime(path);
|
||||
restored.restore(ai.checkpoints());
|
||||
for _ in 0..111 {
|
||||
assert_eq!(
|
||||
serde_json::to_value(call(&mut ai, "on_tick", event(1, 112, 24, 160))).unwrap(),
|
||||
serde_json::to_value(call(&mut restored, "on_tick", event(1, 112, 24, 160))).unwrap()
|
||||
);
|
||||
}
|
||||
// A dead or absent entity cannot move or fire. Death reports once and never wins a mission itself.
|
||||
assert!(call(&mut ai, "on_tick", event(1, 112, 24, 0)).is_empty());
|
||||
let mut missing = event(1, 112, 24, 160);
|
||||
missing["entities"] = json!([]);
|
||||
assert!(call(&mut ai, "on_tick", missing).is_empty());
|
||||
let death = call(&mut ai, "on_death", event(1, 112, 24, 0));
|
||||
assert!(
|
||||
death.iter().any(
|
||||
|c| matches!(c,Command::Objective{key,value:1} if key=="iron_coast_boss_3:defeated")
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
!death
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Complete | Command::Credits { .. }))
|
||||
);
|
||||
assert!(call(&mut ai, "on_death", event(1, 112, 24, 0)).is_empty());
|
||||
let mut second = event(2, 112, 24, 0);
|
||||
second["objectives"] = json!({"iron_coast_boss_3:defeated":1});
|
||||
assert!(
|
||||
call(&mut ai, "on_death", second).iter().any(
|
||||
|c| matches!(c,Command::Objective{key,value:2} if key=="iron_coast_boss_3:defeated")
|
||||
)
|
||||
);
|
||||
queued_spawn_replay(path);
|
||||
println!(
|
||||
"Saltbreaker: arrival/patrol, two-shell alternating volleys, nine-step warnings, independent entities, checkpoint replay, dead-unit safety and exact death objectives passed"
|
||||
);
|
||||
println!(
|
||||
"Real World lethal collision before queued spawn: three restored-state replays reuse the same entity ID and each count and reward exactly once"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user