Record repository relocation and original ground-target investigation

This commit is contained in:
raptorreplica committed 2026-10-11 16:37:50 +11:00
1 parent 8f459796b1
commit 98465133aa
4 files changed
+505 -50

No files matched your search

+54 -3
View File
@@ -39,17 +39,68 @@ reward; this run wrote `.reference/machine-gun-ground.json` outside Git.
Package PNG/WAV decoding remains covered by the separate mod validator; this
collision test loads the actual definitions, logical canvases and Rhai sources.
The October 11 follow-up expands that check to all 110 distinct non-boss ground
enemy definitions referenced by the 27 original maps, plus those five Iron
Coast units. All 115 can take damage from the default guns. The original ten
cases still require destruction and the exact reward. Additional cases require
damage, rather than treating disappearance as a kill: moving units can escape,
and the deliberately aggressive secret enemies can kill a starter pilot. Of
the 115 range cases, 91 were actually destroyed. Four needed a second ordinary
input strategy: a closer firing lane with movement lead. The report retains
both attempts. This proves collision eligibility, not that an unequipped pilot
can defeat every original enemy from a stationary firing lane.
Buildings, parked cars and other map props are usually terrain definitions,
rather than enemies. A separate local test covers every one of the 889 unique
destructible terrain definitions used by the original maps, with actual health,
replacement art and rewards. All explode, settle and pay once in the native
collision window. One definition starts with negative health and therefore
explodes without a hit, as it does in the classic source. Original terrain
requires health below zero, rather than exactly zero, before its deferred
explosion; the follow-up does not change that rule.
Two focused tests cover the reported Bravo Sector waves 6 and 7. They keep each
complete original layout and advance ordinary simulation steps to the actual
displayed positions. All 17 destructible occurrences in rows 130 through 149
are destroyed by default-gun input. Examples in wave 7 include building `1045a`
at row 143, column 6 (25 health, 100 credits), and small vehicle/debris tiles
`1040c`, `1040a`, `1040e` and `1040d` (3–5 health). Nearby industrial/fence tiles
`1045c` through `1045f` remain intact: their original intact and replacement IDs
are identical, and the classic `TILE_IsHit` deliberately rejects them.
The spawn-path test retains the native tick, placement, dimensions, movement
and full terrain for the four ordinary early ground emplacements. With capped
four-pixel mouse movement and the default guns, the wave 6 bonus emplacement
and turret die at ticks 565 and 648; wave 7's two turrets die at ticks 392 and
744. All runs retain 75 shield. The report records each original spawn position,
the first gun-hit coordinates, current logical bounds and prior interpolation
position. Every first hit is inside the displayed enemy's current bounds;
interpolation differs by the expected single vertical pixel.
No further native collision defect was reproduced, so this follow-up changes
tests and documentation only. The original point collision rules, update order,
terrain delay and deliberately static scenery remain intact. These simulation
checks do not independently prove every HD image's visible footprint or the
desktop presentation; the exact reported screenshot would still be useful if
the player sees a destructible object that differs from these examples.
The detailed JSON reports and original map contact images are local under
`.reference/ground-investigation/` in the parent project directory, outside the
Git root. The runtime catalog is tracked in `data/`. Their report variables are
`RAPTOR_GROUND_REPORT`, `RAPTOR_TERRAIN_REPORT`, `RAPTOR_EARLY_TERRAIN_REPORT` and
`RAPTOR_EARLY_SPAWN_REPORT`.
Run the portable checks with:
```sh
cargo test --locked --test machine_gun_ground --test gameplay
```
After campaign package files are stable, run the local original-data check with:
After campaign package files are stable, run all four local original-data checks with:
```sh
RAPTOR_TEST_DATA=../data cargo test --locked --test machine_gun_ground \
real_forward_guns_damage_classic_and_iron_coast_ground_units -- --ignored
RAPTOR_TEST_DATA=../data cargo test --locked --no-default-features \
--test machine_gun_ground -- --ignored
```
Commands above run from `RustRaptor/`. Set the usual local SDL library path
+8 -1
View File
@@ -15,6 +15,9 @@ priority without moving the cursor. Returning from details or a disable
confirmation restores the selected mod and its list position. Opening details
again while already in diagnostics preserves its parent, so Escape always
returns one level. Portable regressions exercise these navigation sequences.
The 42-capture audit was rerun after the navigation fixes on 11 October and
all four current contact sheets were visually reviewed; panels, focus,
confirmation text and shop information remain readable and correctly placed.
The audit also verifies the complete startup reaches its return screen at exactly 1,891 70 Hz service ticks and restores menu music. Cancelling a mod-mismatch prompt preserves the original save; accepting it displays unavailable equipment without silently discarding it.
@@ -25,4 +28,8 @@ RAPTOR_TEST_DATA="$PWD/../data" LD_LIBRARY_PATH="$HOME/.local/lib" \
cargo test --locked --lib every_menu_screen_and_original_intro_visual_audit -- --ignored
```
Captures and the action-row manifest stay ignored in `../.reference/ui-all/`. The background was generated with the imagegen skill's built-in image generation tool; the exact prompt and provenance are tracked in `art/menu-background-v1.prompt.txt` and `art/provenance.json`. Original cinematic pixels remain outside Git.
Captures and the action-row manifest remain local in `../.reference/ui-all/`,
outside the current Git root. The background was generated with the imagegen
skill's built-in image generation tool; its prompt and provenance are tracked
in `art/menu-background-v1.prompt.txt` and `art/provenance.json`. The packaged
original cinematics are now included in tracked `data/`.
+37 -8
View File
@@ -6,9 +6,25 @@ helper or packaging shell process remained at recovery. The image-generation
gate was paused with zero active leases, and has now been resumed. Recover each
worker from its existing outputs before submitting another generation request.
## Repository relocation and GitHub
`RustRaptor/` is now the Git root, including its `.git/` metadata. Migration
commit `8f45979` preserves history and is pushed to the public
[Quintine/RustRaptor](https://github.com/Quintine/RustRaptor) repository on
`codex/rust-rewrite`. Run all Git commands from `RustRaptor/`, with paths such
as `src/desktop.rs` and `docs/hd-backlog.md`.
The user explicitly requested that the bundled game and data be included in
Git. `bin/`, `lib/`, `data/` and `runtime-manifest.json` are tracked; caches,
review files and duplicate zip exports are ignored. All 6,566 runtime data
hashes match in a fresh public GitHub checkout, which validates all 6,449
original catalog entries. Its SDL game also launched from `/tmp` using only
the bundled library and an isolated profile. Pending source, mod and artwork-review work survived
the relocation. See [repository layout](repository.md).
## Engine and package
- Last committed engine checkpoint: `fdf2ff0`, standalone RustRaptor, widescreen
- Packaged executable checkpoint: `fdf2ff0`, standalone RustRaptor, widescreen
menus and always-4:3 original cinematics, including when gameplay stretching
is selected. Fresh 1080p intro capture confirms black 240-pixel side borders.
- `RustRaptor/bin/raptor-rust`, `lib/`, `data/` and
@@ -18,28 +34,41 @@ worker from its existing outputs before submitting another generation request.
- Before shutdown, 320 default tests, the complete 42-image menu/intro audit,
separate cinematic aspect regression, mod/catalog/HD validation and copied
1280×800/1080p SDL/audio smoke checks passed. Later fixes require fresh checks.
- Uncommitted lightning weapon/API packages and terrain compositor improvements
- Source navigation fixes are committed in `4b1edef`; the terrain compositor
fix is committed in `ab23255`. Uncommitted Lightning weapon/API packages
are preserved. Review and commit them separately; do not discard their work.
The executable/package must be refreshed after source or accepted-art changes.
## Next user fixes
1. Mod/save diagnostics: parent and selection are now preserved when details
are reopened. The Escape regression passes; full screenshot audit is running.
are reopened. The Escape regression and fresh 42-capture screenshot audit
pass, including repeated details, confirmation and back navigation.
2. Installed mod rows now stay in ID order through toggles and priority changes,
with dependency and confirmation behavior preserved. Navigation tests pass.
3. Default Forward Guns still appear to miss miscellaneous buildings/cars.
Expand actual original-content coverage beyond the earlier ten-unit test.
Actual original-data checks now cover all 889 destructible terrain
definitions, 17 early wave 6/7 occurrences, all 110 classic ground units
and four early native spawn paths. These pass. Some industrial pieces are
deliberately indestructible in the original tables; the exact reported
object remains unidentified, so no speculative native collision change was made.
The resumed source suite passes 333 tests with 31 optional checks ignored.
The original menu/intro audit passes and completes at 1,891 service ticks.
These checks include pending Lightning API/mod changes, which still require
independent review and a refreshed packaged executable before release.
Root owns menu fixes. `iron_coast_5` temporarily owns the ground-target diagnosis;
its completed boss candidates and pending equipment review remain preserved.
## HD state and owners
The original picture inventory is 5,753 IDs; 221 are registered and 5,532 remain
unregistered. Existing raw candidates are not counted as accepted remasters.
All original/generated base pixels and proof captures remain ignored; authored
mod artwork and sound belong in Git.
The original picture inventory is 5,753 IDs. The development pack now has 340
registered frames across 122 complete sequences; 5,413 IDs remain unregistered.
The packaged runtime still has its earlier 221-frame snapshot. Existing raw
candidates are not counted as accepted remasters. Working originals, generated
drafts and proof captures remain outside the Git root; the approved runtime
snapshot and all authored mod artwork and sound are tracked in Git.
| Agent | Recover and continue |
| --- | --- |
+406 -38
View File
@@ -267,16 +267,34 @@ fn real_forward_guns_damage_classic_and_iron_coast_ground_units() {
// movement and scripts intact while isolating their actual collisions.
let mission: ContentPack = serde_json::from_value(json!({"missions":[{"id":"test:ground_range","name":"Ground range","duration":10000,"auto_complete":false}]})).unwrap();
active.registry.apply(mission, "test").unwrap();
let first = &base.missions[&ContentId::from("base:sector_1_wave_1")];
let mut ground: Vec<_> = first
// Retain the original ten-case kill assertions. Additional native enemies
// include the 100-health secret cow; a starter pilot may die in this fixed
// firing lane, so those cases check damage without claiming combat wins.
let must_destroy: std::collections::BTreeSet<_> = base.missions
[&ContentId::from("base:sector_1_wave_1")]
.spawns
.iter()
.map(|spawn| spawn.enemy.clone())
.filter(|id| base.enemies[id].ground && !base.enemies[id].boss)
.collect();
let mut ground: Vec<_> = base
.missions
.values()
.filter(|mission| mission.legacy_map.is_some())
.flat_map(|mission| mission.spawns.iter())
.map(|spawn| spawn.enemy.clone())
.filter(|id| base.enemies[id].ground && !base.enemies[id].boss)
.collect();
ground.sort();
ground.dedup();
assert!(!ground.is_empty());
assert!(
ground.len() > 5,
"exercise every native ground unit referenced by the 27 maps"
);
eprintln!(
"Auditing {} original non-boss ground definitions",
ground.len()
);
ground.extend(
[
"iron_coast_turret:unit",
@@ -288,60 +306,410 @@ fn real_forward_guns_damage_classic_and_iron_coast_ground_units() {
.map(ContentId::from),
);
let mut observations = Vec::new();
let mut undamaged = Vec::new();
for id in ground {
let pilot = Pilot {
mission: "test:ground_range".into(),
..starter()
};
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
let entity = world.spawn(&id, 144, 24, &active.registry).unwrap();
let initial = world.enemies[0].health;
let mut lowest = initial;
for _ in 0..120 {
// Normal bounded absolute mouse movement aligns the ship with the
// observed ground target; health and projectiles are never injected.
let target = world.enemies.iter().find(|enemy| enemy.entity == entity);
let Some(enemy) = target else {
let mut attempts = Vec::new();
let mut final_world = None;
let mut final_entity = 0;
let mut lowest = active.registry.enemies[&id].health;
let initial = lowest;
for attempt in 0..2 {
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
let entity = world.spawn(&id, 144, 24, &active.registry).unwrap();
lowest = initial;
for _ in 0..120 {
// Normal bounded absolute mouse movement aligns the ship with the
// observed ground target; health and projectiles are never injected.
let target = world.enemies.iter().find(|enemy| enemy.entity == entity);
let Some(enemy) = target else {
break;
};
// The first attempt preserves the original ten-case firing lane.
// If it cannot reach an additional native target, a second ordinary
// input strategy uses a closer lane and leads horizontal movement.
// Narrow moving units can outrun shots aimed at their old position;
// hostile secret units can kill a stationary pilot before a distant
// shot reaches them. Neither case establishes a collision defect.
let y = if attempt == 0 { 160 } else { 80 };
let lead = if attempt == 0 {
0
} else {
let velocity = active.registry.enemies[&id].legacy.as_ref().map_or(
0,
|legacy| match legacy.fields[21] {
4 => -active.registry.enemies[&id].speed,
5 => active.registry.enemies[&id].speed,
_ => 0,
},
);
let flight_steps = ((y + 16 - enemy.y - enemy.height / 2).max(0) + 11) / 12;
velocity * flight_steps
};
let x = (enemy.x + enemy.width / 2 - 16 + lead).clamp(5, 282);
world
.step(
Input {
fire: true,
absolute: Some([x, y]),
..Input::default()
},
&active.registry,
&mut active.scripts,
)
.unwrap();
if let Some(enemy) = world.enemies.iter().find(|enemy| enemy.entity == entity) {
lowest = lowest.min(enemy.health);
}
}
attempts.push(json!({
"control": if attempt == 0 { "current-position aim at y160" } else { "movement lead at y80" },
"lowest_observed_health": lowest,
"ticks": world.tick,
"outcome": format!("{:?}", world.outcome),
"remaining_shield": world.player.shield
}));
final_entity = entity;
final_world = Some(world);
if lowest < initial {
break;
};
let x = (enemy.x + enemy.width / 2 - 16).clamp(5, 282);
world
.step(
Input {
fire: true,
absolute: Some([x, 160]),
..Input::default()
},
&active.registry,
&mut active.scripts,
)
.unwrap();
if let Some(enemy) = world.enemies.iter().find(|enemy| enemy.entity == entity) {
lowest = lowest.min(enemy.health);
}
}
assert!(lowest < initial, "starter machine gun did not damage {id}");
assert!(
!world.enemies.iter().any(|enemy| enemy.entity == entity),
"starter machine gun did not destroy {id}; lowest health {lowest}"
);
assert_eq!(
world.player.credits,
pilot.credits + active.registry.enemies[&id].reward,
"incorrect kill reward for {id}"
);
let world = final_world.unwrap();
let entity = final_entity;
if lowest >= initial {
undamaged.push(format!(
"{id}: health {initial}, tick {}, outcome {:?}",
world.tick, world.outcome
));
}
let target_present = world.enemies.iter().any(|enemy| enemy.entity == entity);
let destroyed = !target_present && lowest <= 0;
if must_destroy.contains(&id) || active.registry.enemies[&id].legacy.is_none() {
assert!(
destroyed,
"starter machine gun did not destroy {id}; lowest health {lowest}, outcome {:?}",
world.outcome
);
}
if destroyed {
assert_eq!(
world.player.credits,
pilot.credits + active.registry.enemies[&id].reward,
"incorrect kill reward for {id}"
);
}
observations.push(json!({
"id": id,
"name": active.registry.enemies[&id].name,
"initial_health": initial,
"lowest_observed_health": lowest,
"destroyed": true,
"destroyed": destroyed,
"escaped": !target_present && !destroyed,
"outcome": format!("{:?}", world.outcome),
"ticks": world.tick,
"reward": world.player.credits - pilot.credits,
"remaining_shield": world.player.shield
,"attempts": attempts
}));
}
if let Some(path) = std::env::var_os("RAPTOR_GROUND_REPORT") {
std::fs::write(path, serde_json::to_vec_pretty(&observations).unwrap()).unwrap();
}
assert!(
undamaged.is_empty(),
"Ground definitions never damaged: {undamaged:?}"
);
}
#[test]
#[ignore = "requires locally installed original catalog"]
fn real_forward_guns_damage_every_classic_destructible_terrain_definition() {
use std::collections::BTreeMap;
let catalog = catalog();
let mut registry = Registry::base(&catalog).unwrap();
// Buildings, parked cars and other scenery are native terrain, not EnemyDef.
// Use every intact/destruction pair actually referenced by the 27 maps.
let mut definitions = BTreeMap::new();
for mission in registry
.missions
.values()
.filter(|m| m.legacy_map.is_some())
{
for tile in &mission.tiles {
if tile.destroyed.is_some() {
definitions
.entry(tile.art.asset.unwrap())
.or_insert_with(|| tile.clone());
}
}
}
assert!(definitions.len() > 100);
eprintln!(
"Auditing {} original destructible terrain definitions",
definitions.len()
);
let mission_id = ContentId::from("base:sector_1_wave_1");
let background = registry.missions[&mission_id]
.tiles
.iter()
.find(|tile| tile.destroyed.is_none())
.unwrap()
.clone();
let mut observations = Vec::new();
for (asset, tile) in definitions {
let mission = registry.missions.get_mut(&mission_id).unwrap();
mission.spawns.clear();
mission.auto_complete = false;
mission.duration = 10000;
mission.tiles = vec![background.clone(), tile.clone()];
mission.layout = vec![0; 90];
let position = 4 * 9 + 4;
mission.layout[position] = 1;
let pilot = starter();
let mut world = World::new(&mission_id, &pilot, &registry).unwrap();
let mut scripts = ScriptRuntime::new();
let mut sounds = 0;
for _ in 0..100 {
shoot(&mut world, &registry, &mut scripts, 1);
sounds += world.sound_events.iter().filter(|s| s.asset == 139).count();
}
assert!(
world.terrain_dead[position],
"classic tile {asset:05x} was not destroyed; health {} from {}",
world.terrain_health[position], tile.health
);
assert!(
world.terrain_settled[position],
"classic tile {asset:05x} never settled"
);
assert_eq!(
world.player.credits,
pilot.credits + tile.reward,
"classic tile {asset:05x} reward"
);
assert_eq!(sounds, 1, "classic tile {asset:05x} ground-explosion sound");
observations.push(json!({"asset":format!("{asset:05x}"),"original_health":tile.health,"reward":tile.reward,"destroyed":true,"settled":true,"ticks":world.tick}));
}
if let Some(path) = std::env::var_os("RAPTOR_TERRAIN_REPORT") {
std::fs::write(path, serde_json::to_vec_pretty(&observations).unwrap()).unwrap();
}
}
#[test]
#[ignore = "requires locally installed original catalog"]
fn real_wave_6_and_7_early_terrain_hits_match_visible_original_map_positions() {
let catalog = catalog();
let mut registry = Registry::base(&catalog).unwrap();
let mut observations = Vec::new();
let mut destroyed_count = 0;
let mut static_count = 0;
for wave in [6, 7] {
let id = ContentId::new(format!("base:sector_1_wave_{wave}")).unwrap();
let original = registry.missions[&id].clone();
// Keep the complete original layout, art, tile rules and native map
// identity. Only enemy spawns are suppressed to isolate terrain hits.
let mut quiet = original.clone();
quiet.spawns.clear();
quiet.auto_complete = false;
registry.missions.insert(id.clone(), quiet);
for row in 130..150 {
for col in 0..9 {
let position = row * 9 + col;
let tile = &original.tiles[original.layout[position]];
let asset = tile.art.asset.unwrap();
// Include every destructible object in the early map section,
// plus the explicitly static industrial/fence artwork nearby.
let destructible = tile.destroyed.is_some();
let static_landmark = matches!(asset, 0x1045c..=0x1045f);
if !destructible && !static_landmark {
continue;
}
let pilot = Pilot {
mission: id.clone(),
..starter()
};
let mut world = World::new(&id, &pilot, &registry).unwrap();
let mut scripts = ScriptRuntime::new();
let rows = original.layout.len() / 9;
// This is exactly scene()'s original terrain row position:
// 200 - rows*32 + scroll + row*32. Advance ordinary idle
// steps until the tested row is at y=40, then fire normally.
let idle = (40 - (200 - rows as i32 * 32 + row as i32 * 32)).max(0);
for _ in 0..idle {
world
.step(Input::default(), &registry, &mut scripts)
.unwrap();
}
assert_eq!(200 - rows as i32 * 32 + world.scroll + row as i32 * 32, 40);
let initial_health = world.terrain_health[position];
for _ in 0..70 {
world
.step(
Input {
fire: true,
absolute: Some([16 + col as i32 * 32, 160]),
..Input::default()
},
&registry,
&mut scripts,
)
.unwrap();
}
if destructible {
assert!(
world.terrain_dead[position],
"wave{wave} row{row} col{col} asset{asset:05x} remains alive at its displayed position, health {}",
world.terrain_health[position]
);
assert!(world.terrain_settled[position]);
destroyed_count += 1;
} else {
assert_eq!(world.terrain_health[position], initial_health);
assert!(!world.terrain_dead[position]);
static_count += 1;
}
observations.push(json!({"wave":wave,"row":row,"col":col,"asset":format!("{asset:05x}"),"destructible":destructible,"initial_health":initial_health,"final_health":world.terrain_health[position],"dead":world.terrain_dead[position],"settled":world.terrain_settled[position],"idle_steps":idle,"normal_fire_steps":70,"credits_gained":world.player.credits-pilot.credits}));
}
}
}
assert_eq!(destroyed_count, 17);
assert!(static_count >= 4);
if let Some(path) = std::env::var_os("RAPTOR_EARLY_TERRAIN_REPORT") {
std::fs::write(path, serde_json::to_vec_pretty(&observations).unwrap()).unwrap();
}
}
#[test]
#[ignore = "requires locally installed original catalog"]
fn real_wave_6_and_7_early_ground_spawns_use_original_visible_positions() {
let catalog = catalog();
let mut registry = Registry::base(&catalog).unwrap();
let mut observations = Vec::new();
for wave in [6, 7] {
let id = ContentId::new(format!("base:sector_1_wave_{wave}")).unwrap();
let original = registry.missions[&id].clone();
let early: Vec<_> = original
.spawns
.iter()
.filter(|spawn| {
// Native difficulty codes 0..2 are disabled secret placements.
// These four early ordinary emplacements are enabled at all levels.
spawn.tick <= 600
&& spawn.difficulty == 3
&& registry.enemies[&spawn.enemy].ground
&& !registry.enemies[&spawn.enemy].boss
})
.cloned()
.collect();
assert_eq!(early.len(), 2);
for spawn in early {
let mut isolated = original.clone();
isolated.spawns = vec![spawn.clone()];
isolated.auto_complete = false;
registry.missions.insert(id.clone(), isolated);
let pilot = Pilot {
mission: id.clone(),
..starter()
};
let mut world = World::new(&id, &pilot, &registry).unwrap();
let mut scripts = ScriptRuntime::new();
let def = &registry.enemies[&spawn.enemy];
let art = def.art.as_ref().unwrap();
assert_eq!(def.legacy.as_ref().unwrap().fields[21], 3);
let expected_x = spawn.x + 16 - art.logical_width as i32 / 2;
let expected_y = spawn.y - art.logical_height as i32 / 2 + 1;
let mut observed_spawn = None;
let mut first_damage = None;
let mut entity = None;
let mut lowest = def.health;
let mut last_health = None;
for _ in 0..spawn.tick + 400 {
// Capped mouse movement is achievable with ordinary controls.
// Full original terrain remains present, including objects
// that can intercept bullets before the emplacement is hit.
let desired = (expected_x + art.logical_width as i32 / 2 - 16).clamp(5, 282);
let x = world.player.x + (desired - world.player.x).clamp(-4, 4);
world
.step(
Input {
fire: true,
absolute: Some([x, 160]),
..Input::default()
},
&registry,
&mut scripts,
)
.unwrap();
if let Some(enemy) = world
.enemies
.iter()
.find(|enemy| enemy.definition == spawn.enemy)
{
if entity.is_none() {
entity = Some(enemy.entity);
assert_eq!([enemy.x, enemy.y], [expected_x, expected_y]);
assert_eq!(enemy.age, 1);
assert_eq!(world.tick, spawn.tick + 1);
observed_spawn = Some(
json!({"world_tick":world.tick,"position":[enemy.x,enemy.y],"previous_position":[enemy.px,enemy.py],"width":enemy.width,"height":enemy.height}),
);
}
lowest = lowest.min(enemy.health);
last_health = Some(enemy.health);
if first_damage.is_none() && enemy.health < def.health {
let hit_points: Vec<_> = world
.projectiles
.iter()
.filter(|projectile| {
projectile.weapon == ContentId::from("base:projectile_forward_guns")
&& projectile.legacy.as_ref().is_some_and(|shot| shot.hit)
&& projectile.x > enemy.x
&& projectile.x < enemy.x + enemy.width - 1
&& projectile.y > enemy.y
&& projectile.y < enemy.y + enemy.height - 1
})
.map(|projectile| [projectile.x, projectile.y])
.collect();
assert!(
!hit_points.is_empty(),
"first damage lacks a default-gun collision point"
);
first_damage = Some(
json!({"world_tick":world.tick,"health":enemy.health,"logical_bounds":[enemy.x,enemy.y,enemy.width,enemy.height],"interpolation_start":[enemy.px,enemy.py],"hit_points":hit_points}),
);
}
} else if entity.is_some() {
break;
}
}
assert!(entity.is_some(), "wave {wave} never spawns {}", spawn.enemy);
assert!(
lowest < def.health,
"wave {wave} default gun never damages {}",
spawn.enemy
);
assert!(
last_health.is_some_and(|health| health <= 0),
"wave {wave} target escaped rather than being destroyed: {} / health {last_health:?}",
spawn.enemy
);
assert!(
!world
.enemies
.iter()
.any(|enemy| Some(enemy.entity) == entity)
);
assert_eq!(format!("{:?}", world.outcome), "Playing");
assert!(world.player.credits >= pilot.credits + def.reward);
observations.push(json!({"wave":wave,"id":spawn.enemy,"name":def.name,"native_spawn_tick":spawn.tick,"native_spawn_position":[spawn.x,spawn.y],"observed_spawn":observed_spawn,"first_damage":first_damage,"initial_health":def.health,"lowest_observed_health":lowest,"destroyed":true,"finished_tick":world.tick,"remaining_shield":world.player.shield,"credits_gained":world.player.credits-pilot.credits}));
}
}
assert_eq!(observations.len(), 4);
if let Some(path) = std::env::var_os("RAPTOR_EARLY_SPAWN_REPORT") {
std::fs::write(path, serde_json::to_vec_pretty(&observations).unwrap()).unwrap();
}
}