Match full reference demo and validate original missions, transitions and sprite rendering

This commit is contained in:
raptorreplica committed 2026-10-10 12:42:33 +11:00
1 parent 967ba69ee8
commit 0844f6dffa
21 files changed
+3984 -219

No files matched your search

+581 -1
View File
@@ -2,7 +2,7 @@ use raptor_replica::{
content::{ContentId, ContentPack, Registry},
modding::{Command, ScriptRuntime},
persistence::Pilot,
simulation::{Input, LineMove, World},
simulation::{Input, LegacyProjectile, LineMove, Projectile, World},
};
use serde_json::json;
@@ -24,6 +24,36 @@ fn fixture() -> Registry {
reg
}
#[test]
fn demo_coordinates_bypass_normal_input_bounds() {
let reg = fixture();
let mut world = World::new(&Pilot::default().mission, &Pilot::default(), &reg).unwrap();
let mut scripts = ScriptRuntime::new();
world
.step(
Input {
absolute: Some([284, 162]),
demo_bank: Some(3),
..Input::default()
},
&reg,
&mut scripts,
)
.unwrap();
assert_eq!((world.player.x, world.player.y), (284, 162));
world
.step(
Input {
absolute: Some([284, 162]),
..Input::default()
},
&reg,
&mut scripts,
)
.unwrap();
assert_eq!((world.player.x, world.player.y), (282, 160));
}
#[test]
fn original_guns_use_banked_muzzles_and_accelerate_after_position_snapshot() {
let reg = fixture();
@@ -184,3 +214,553 @@ fn consumable_use_has_snapshot_and_only_fires_on_press() {
assert_eq!(world.player.credits, 14);
assert_eq!(world.player.selected, None);
}
fn loss_fixture() -> Registry {
let mut registry = fixture();
registry.apply(serde_json::from_value(json!({"items":[
{"id":"base:dumb_missile","name":"Dumb Missile","price":0,"special":true,"legacy":3},
{"id":"base:mini_gun","name":"Mini Gun","price":0,"special":true,"legacy":4},
{"id":"base:death_ray","name":"Death Ray","price":0,"special":true,"legacy":14},
{"id":"base:detect","name":"Detector","price":0,"legacy":17},
{"id":"base:itembuy6","name":"Cash Cargo","price":0,"max_count":50,"legacy":23}
]})).unwrap(), "base").unwrap();
registry
}
fn low_shield_world(registry: &Registry) -> (World, ScriptRuntime) {
let mut pilot = Pilot {
difficulty: 3,
..Pilot::default()
};
for id in [
"base:dumb_missile",
"base:mini_gun",
"base:death_ray",
"base:detect",
] {
pilot.inventory.insert(id.into(), 1);
}
pilot.inventory.insert("base:itembuy6".into(), 50);
let mut world = World::new(&pilot.mission, &pilot, registry).unwrap();
let mut scripts = ScriptRuntime::new();
world
.step(Input::default(), registry, &mut scripts)
.unwrap();
(world, scripts)
}
#[test]
fn low_shield_damage_loses_selected_weapon_then_cycles_in_legacy_order() {
let registry = loss_fixture();
let (mut world, mut scripts) = low_shield_world(&registry);
world.player.selected = Some("base:death_ray".into());
world
.apply_commands(
&[Command::Damage {
entity: 0,
amount: 92,
}],
&registry,
)
.unwrap();
let seed = world.rng.seed;
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert_eq!(world.player.shield, 8);
assert!(
!world
.player
.inventory
.contains_key(&"base:death_ray".into())
);
assert_eq!(world.player.selected, Some("base:dumb_missile".into()));
assert_eq!(
world.player.inventory[&ContentId::from("base:itembuy6")],
50
);
assert!(world.sounds.contains(&119)); // FX_CRASH has no random pitch.
assert_eq!(world.rng.seed, seed);
world
.apply_commands(
&[Command::Damage {
entity: 0,
amount: 1,
}],
&registry,
)
.unwrap();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert!(
!world
.player
.inventory
.contains_key(&"base:dumb_missile".into())
);
assert_eq!(world.player.selected, Some("base:mini_gun".into()));
}
#[test]
fn low_shield_without_selection_loses_highest_legacy_item_and_protects_essentials() {
let registry = loss_fixture();
let (mut world, mut scripts) = low_shield_world(&registry);
world
.apply_commands(
&[Command::Damage {
entity: 0,
amount: 92,
}],
&registry,
)
.unwrap();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert!(!world.player.inventory.contains_key(&"base:itembuy6".into()));
for id in [
"base:forward_guns",
"base:energy",
"base:detect",
"base:death_ray",
] {
assert!(
world.player.inventory.contains_key(&id.into()),
"protected or lower-numbered item {id} lost"
);
}
assert_eq!(world.player.selected, None);
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert!(
world
.player
.inventory
.contains_key(&"base:death_ray".into())
);
assert!(!world.sounds.contains(&119)); // Remaining at low shield does not lose another item.
}
#[test]
fn low_energy_with_super_shield_and_damage_above_threshold_preserve_equipment() {
let registry = loss_fixture();
let (mut world, mut scripts) = low_shield_world(&registry);
world.player.selected = Some("base:death_ray".into());
world
.apply_commands(
&[Command::Damage {
entity: 0,
amount: 89,
}],
&registry,
)
.unwrap();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert_eq!(world.player.shield, 11);
assert!(
world
.player
.inventory
.contains_key(&"base:death_ray".into())
);
world.player.super_shield = 5;
world
.apply_commands(&[Command::Shield { amount: -3 }], &registry)
.unwrap();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert_eq!(world.player.shield, 8);
assert!(
world
.player
.inventory
.contains_key(&"base:death_ray".into())
);
assert!(!world.sounds.contains(&119));
}
#[test]
fn legacy_kamikaze_arms_chase_when_final_leg_starts_and_crosses_target() {
let mut registry = fixture();
let enemy = registry
.enemies
.get_mut(&ContentId::from("base:drone"))
.unwrap();
enemy.health = 10000;
enemy.speed = 4;
enemy.path = vec![[0, -114], [0, -96]];
let mut fields = vec![0; 26];
fields[7] = 1;
fields[9] = 1;
fields[18] = 4;
fields[19] = 2;
fields[21] = 2; // Original F_KAMI.
enemy.legacy = Some(raptor_replica::assets::LegacyEnemy {
name: "Synthetic Kamikaze".into(),
fields,
shoot_type: vec![0; 24],
engine_x: vec![0; 24],
engine_y: vec![0; 24],
engine_width: vec![0; 24],
gun_x: vec![0; 24],
gun_y: vec![0; 24],
flight_x: vec![0; 32],
flight_y: vec![0; 32],
});
let mut world = World::new(&Pilot::default().mission, &Pilot::default(), &registry).unwrap();
let entity = world
.spawn(&"base:drone".into(), 48, -131, &registry)
.unwrap();
let mut scripts = ScriptRuntime::new();
let mut armed = false;
let mut crossed = false;
for _ in 0..120 {
world
.step(
Input {
absolute: Some([96, 136]),
..Input::default()
},
&registry,
&mut scripts,
)
.unwrap();
let Some(enemy) = world.enemies.iter().find(|e| e.entity == entity) else {
break;
};
if enemy.kami == 1 {
armed = true;
assert_eq!(enemy.movement.ty, -12);
assert!(!enemy.movement.done);
assert_eq!(enemy.path_index, 1);
}
if enemy.kami == 2 {
assert_eq!((enemy.movement.tx, enemy.movement.ty), (112, 152));
crossed |= enemy.movement.y > 152;
}
}
assert!(armed, "last authored leg never armed chase");
assert!(crossed, "terminal chase stopped at its target");
assert!(
!world.enemies.iter().any(|e| e.entity == entity),
"terminal kamikaze failed to retire offscreen"
);
}
#[test]
fn legacy_beam_hits_launch_column_before_following_player_for_display() {
let mut registry = fixture();
registry.apply(serde_json::from_value(json!({
"weapons":[{"id":"base:projectile_death_ray","name":"Death Ray","damage":6,"speed":0,"cooldown":7,"behavior":"beam","target":"all","legacy":14,"art":{"asset":50,"logical_width":2,"logical_height":8,"frames":4}}],
"items":[{"id":"base:death_ray","name":"Death Ray","price":0,"special":true,"legacy":14,"weapon":"base:projectile_death_ray"}]
})).unwrap(), "base").unwrap();
let pilot = Pilot {
selected: Some("base:death_ray".into()),
inventory: [("base:death_ray".into(), 1), ("base:energy".into(), 100)]
.into_iter()
.collect(),
..Pilot::default()
};
let mut world = World::new(&pilot.mission, &pilot, &registry).unwrap();
world
.spawn(&"base:drone".into(), 157, 40, &registry)
.unwrap();
let mut scripts = ScriptRuntime::new();
world
.step(
Input {
fire: true,
..Input::default()
},
&registry,
&mut scripts,
)
.unwrap();
assert_eq!(world.enemies[0].health, 14);
world
.step(
Input {
absolute: Some([141, 160]),
..Input::default()
},
&registry,
&mut scripts,
)
.unwrap();
assert_eq!(world.projectiles[0].x, 156); // Followed artwork is outside the enemy.
assert_eq!(world.enemies[0].health, 8); // Damage still tests launch column159.
}
fn collision_fixture() -> Registry {
let mut registry = fixture();
registry.apply(serde_json::from_value(json!({"weapons":[
{"id":"base:projectile_energy_grab","name":"Energy Grab","damage":3,"speed":4,"cooldown":2,"behavior":"straight","target":"air","legacy":10,"art":{"asset":51,"logical_width":8,"logical_height":8,"frames":6}},
{"id":"base:projectile_ground_bomb","name":"Ground Bomb","damage":50,"speed":1,"cooldown":30,"behavior":"straight","target":"ground","legacy":9,"art":{"asset":52,"logical_width":8,"logical_height":8}},
{"id":"base:projectile_mega_bomb","name":"Mega Bomb","damage":50,"speed":2,"cooldown":60,"behavior":"straight","target":"all","legacy":11,"art":{"asset":53,"logical_width":8,"logical_height":8,"frames":4}},
{"id":"base:hostile","name":"Hostile","damage":2,"speed":2,"cooldown":1,"behavior":"straight","target":"all"}
],"enemies":[
{"id":"base:ground","name":"Ground Target","health":30,"ground":true},
{"id":"base:air","name":"Air Target","health":64,"speed":0,"path":[[0,200]],"weapon":"base:hostile"}
]})).unwrap(), "base").unwrap();
let air = registry
.enemies
.get_mut(&ContentId::from("base:air"))
.unwrap();
let mut fields = vec![0; 26];
fields[5] = 1; // Native suck susceptibility.
fields[7] = 1;
fields[9] = 1;
fields[13] = 64;
fields[16] = 1;
fields[21] = 3;
fields[22] = 1;
air.legacy = Some(raptor_replica::assets::LegacyEnemy {
name: "Synthetic Susceptible Air Target".into(),
fields,
shoot_type: vec![1; 24],
engine_x: vec![0; 24],
engine_y: vec![0; 24],
engine_width: vec![0; 24],
gun_x: vec![16; 24],
gun_y: vec![16; 24],
flight_x: vec![0; 32],
flight_y: vec![0; 32],
});
registry
}
fn inject_legacy_shot(
world: &mut World,
id: &str,
x: i32,
y: i32,
done: bool,
registry: &Registry,
) -> u64 {
let weapon = &registry.weapons[&ContentId::from(id)];
let art = weapon.art.as_ref().unwrap();
let mut movement = LineMove::new(
x + art.logical_width as i32 / 2,
y + art.logical_height as i32 / 2,
x,
0,
);
movement.done = done;
let entity = world.next_entity;
world.next_entity += 1;
world.projectiles.push(Projectile {
entity,
weapon: id.into(),
x,
y,
px: x,
py: y,
vx: 0,
vy: 0,
hostile: false,
age: 0,
legacy_enemy: None,
legacy: Some(LegacyProjectile {
movement,
speed: weapon.speed,
frame: 0,
art_frame: 0,
delay: false,
hit: false,
start: [world.player.x + 16, world.player.y + 16],
}),
});
entity
}
#[test]
fn energy_grab_hits_air_by_one_and_suppresses_fire_after_resistance_is_exhausted() {
let registry = collision_fixture();
let mut world = World::new(&Pilot::default().mission, &Pilot::default(), &registry).unwrap();
world
.spawn(&"base:ground".into(), 64, 64, &registry)
.unwrap();
let entity = world.spawn(&"base:air".into(), 64, 64, &registry).unwrap();
let mut scripts = ScriptRuntime::new();
for (expected_health, expected_resistance, disabled) in
[(63, 1, false), (62, -2, false), (61, -2, true)]
{
inject_legacy_shot(
&mut world,
"base:projectile_energy_grab",
80,
80,
false,
&registry,
);
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
let air = world.enemies.iter().find(|e| e.entity == entity).unwrap();
assert_eq!(air.health, expected_health);
assert_eq!(air.energy_resistance, expected_resistance);
assert_eq!(air.shoot_disabled, disabled);
assert_eq!(world.enemies[0].health, 30); // Ground target overlaps the shot.
}
assert!(world.sounds.contains(&134)); // FX_EGRAB.
let hostile = world.projectiles.iter().filter(|p| p.hostile).count();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert_eq!(
world.projectiles.iter().filter(|p| p.hostile).count(),
hostile
);
assert!(
world
.enemies
.iter()
.find(|e| e.entity == entity)
.unwrap()
.shoot_disabled
);
}
#[test]
fn energy_grab_does_not_disable_an_immune_air_enemy() {
let mut registry = collision_fixture();
registry
.enemies
.get_mut(&"base:air".into())
.unwrap()
.legacy
.as_mut()
.unwrap()
.fields[5] = 0;
let mut world = World::new(&Pilot::default().mission, &Pilot::default(), &registry).unwrap();
world.spawn(&"base:air".into(), 64, 64, &registry).unwrap();
let mut scripts = ScriptRuntime::new();
for _ in 0..5 {
inject_legacy_shot(
&mut world,
"base:projectile_energy_grab",
80,
80,
false,
&registry,
);
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
}
assert_eq!(world.enemies[0].health, 59);
assert_eq!(world.enemies[0].energy_resistance, 4);
assert!(!world.enemies[0].shoot_disabled);
}
fn terrain_fixture() -> Registry {
let mut registry = collision_fixture();
let pack = serde_json::from_value(json!({"missions":[{
"id":"base:sector_1_wave_1","name":"Terrain Fixture","duration":1000,"auto_complete":false,"legacy_map":123,"columns":3,"layout":vec![0;30],
"tiles":[
{"art":{"asset":60,"logical_width":32,"logical_height":32},"health":200,"destroyed":{"asset":61,"logical_width":32,"logical_height":32}},
{"art":{"asset":62,"logical_width":32,"logical_height":32},"health":200}
]
}],"overrides":["base:sector_1_wave_1"]})).unwrap();
registry.apply(pack, "base").unwrap();
registry
}
#[test]
fn ground_bomb_damages_tile_neighbors_and_overlapping_ground_enemy_together() {
let registry = terrain_fixture();
let mut world = World::new(&Pilot::default().mission, &Pilot::default(), &registry).unwrap();
world
.spawn(&"base:ground".into(), 48, 73, &registry)
.unwrap();
let shot = inject_legacy_shot(
&mut world,
"base:projectile_ground_bomb",
64,
89,
false,
&registry,
);
let mut scripts = ScriptRuntime::new();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert_eq!(world.enemies[0].health, 25);
assert!(!world.projectiles.iter().any(|p| p.entity == shot));
for position in [19, 18, 16, 20] {
assert_eq!(
world.terrain_health[position], 150,
"primary neighbor {position}"
);
}
for position in [15, 13, 17] {
assert_eq!(
world.terrain_health[position], 175,
"upper neighbor {position}"
);
}
assert_eq!(world.terrain_health[22], 200);
assert_eq!(world.terrain_health[21], 200);
}
#[test]
fn mega_bomb_uses_visible_destructible_tiles_at_fixed_twenty_damage_and_clears_shots() {
let mut registry = terrain_fixture();
registry
.missions
.get_mut(&Pilot::default().mission)
.unwrap()
.layout[8] = 1;
let mut world = World::new(&Pilot::default().mission, &Pilot::default(), &registry).unwrap();
world.terrain_dead[7] = true;
world
.spawn(&"base:ground".into(), 64, 64, &registry)
.unwrap();
let hostile = world.next_entity;
world.next_entity += 1;
world.projectiles.push(Projectile {
entity: hostile,
weapon: "base:hostile".into(),
x: 100,
y: 100,
px: 100,
py: 100,
vx: 0,
vy: 1,
hostile: true,
age: 0,
legacy: None,
legacy_enemy: None,
});
inject_legacy_shot(
&mut world,
"base:projectile_mega_bomb",
100,
100,
true,
&registry,
);
let mut scripts = ScriptRuntime::new();
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
assert_eq!(world.enemies[0].health, -20); // Original enemy damage50, separate from tiles.
assert!(world.projectiles.is_empty());
for position in 0..30 {
let expected = if position < 6 || position == 7 || position == 8 {
200
} else {
180
};
assert_eq!(
world.terrain_health[position], expected,
"terrain position{position}"
);
}
}