Record repository relocation and original ground-target investigation
This commit is contained in:
1 parent
8f459796b1
commit
98465133aa
4 files changed
+505
-50
No files matched your search
@@ -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
|
Package PNG/WAV decoding remains covered by the separate mod validator; this
|
||||||
collision test loads the actual definitions, logical canvases and Rhai sources.
|
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:
|
Run the portable checks with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test --locked --test machine_gun_ground --test gameplay
|
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
|
```sh
|
||||||
RAPTOR_TEST_DATA=../data cargo test --locked --test machine_gun_ground \
|
RAPTOR_TEST_DATA=../data cargo test --locked --no-default-features \
|
||||||
real_forward_guns_damage_classic_and_iron_coast_ground_units -- --ignored
|
--test machine_gun_ground -- --ignored
|
||||||
```
|
```
|
||||||
|
|
||||||
Commands above run from `RustRaptor/`. Set the usual local SDL library path
|
Commands above run from `RustRaptor/`. Set the usual local SDL library path
|
||||||
|
|||||||
@@ -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
|
confirmation restores the selected mod and its list position. Opening details
|
||||||
again while already in diagnostics preserves its parent, so Escape always
|
again while already in diagnostics preserves its parent, so Escape always
|
||||||
returns one level. Portable regressions exercise these navigation sequences.
|
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.
|
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
|
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/`.
|
||||||
@@ -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
|
gate was paused with zero active leases, and has now been resumed. Recover each
|
||||||
worker from its existing outputs before submitting another generation request.
|
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
|
## 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
|
menus and always-4:3 original cinematics, including when gameplay stretching
|
||||||
is selected. Fresh 1080p intro capture confirms black 240-pixel side borders.
|
is selected. Fresh 1080p intro capture confirms black 240-pixel side borders.
|
||||||
- `RustRaptor/bin/raptor-rust`, `lib/`, `data/` and
|
- `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,
|
- Before shutdown, 320 default tests, the complete 42-image menu/intro audit,
|
||||||
separate cinematic aspect regression, mod/catalog/HD validation and copied
|
separate cinematic aspect regression, mod/catalog/HD validation and copied
|
||||||
1280×800/1080p SDL/audio smoke checks passed. Later fixes require fresh checks.
|
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.
|
are preserved. Review and commit them separately; do not discard their work.
|
||||||
The executable/package must be refreshed after source or accepted-art changes.
|
The executable/package must be refreshed after source or accepted-art changes.
|
||||||
|
|
||||||
## Next user fixes
|
## Next user fixes
|
||||||
|
|
||||||
1. Mod/save diagnostics: parent and selection are now preserved when details
|
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,
|
2. Installed mod rows now stay in ID order through toggles and priority changes,
|
||||||
with dependency and confirmation behavior preserved. Navigation tests pass.
|
with dependency and confirmation behavior preserved. Navigation tests pass.
|
||||||
3. Default Forward Guns still appear to miss miscellaneous buildings/cars.
|
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;
|
Root owns menu fixes. `iron_coast_5` temporarily owns the ground-target diagnosis;
|
||||||
its completed boss candidates and pending equipment review remain preserved.
|
its completed boss candidates and pending equipment review remain preserved.
|
||||||
|
|
||||||
## HD state and owners
|
## HD state and owners
|
||||||
|
|
||||||
The original picture inventory is 5,753 IDs; 221 are registered and 5,532 remain
|
The original picture inventory is 5,753 IDs. The development pack now has 340
|
||||||
unregistered. Existing raw candidates are not counted as accepted remasters.
|
registered frames across 122 complete sequences; 5,413 IDs remain unregistered.
|
||||||
All original/generated base pixels and proof captures remain ignored; authored
|
The packaged runtime still has its earlier 221-frame snapshot. Existing raw
|
||||||
mod artwork and sound belong in Git.
|
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 |
|
| Agent | Recover and continue |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
+406
-38
@@ -267,16 +267,34 @@ fn real_forward_guns_damage_classic_and_iron_coast_ground_units() {
|
|||||||
// movement and scripts intact while isolating their actual collisions.
|
// 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();
|
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();
|
active.registry.apply(mission, "test").unwrap();
|
||||||
let first = &base.missions[&ContentId::from("base:sector_1_wave_1")];
|
// Retain the original ten-case kill assertions. Additional native enemies
|
||||||
let mut ground: Vec<_> = first
|
// 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
|
.spawns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|spawn| spawn.enemy.clone())
|
.map(|spawn| spawn.enemy.clone())
|
||||||
.filter(|id| base.enemies[id].ground && !base.enemies[id].boss)
|
.filter(|id| base.enemies[id].ground && !base.enemies[id].boss)
|
||||||
.collect();
|
.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.sort();
|
||||||
ground.dedup();
|
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(
|
ground.extend(
|
||||||
[
|
[
|
||||||
"iron_coast_turret:unit",
|
"iron_coast_turret:unit",
|
||||||
@@ -288,60 +306,410 @@ fn real_forward_guns_damage_classic_and_iron_coast_ground_units() {
|
|||||||
.map(ContentId::from),
|
.map(ContentId::from),
|
||||||
);
|
);
|
||||||
let mut observations = Vec::new();
|
let mut observations = Vec::new();
|
||||||
|
let mut undamaged = Vec::new();
|
||||||
for id in ground {
|
for id in ground {
|
||||||
let pilot = Pilot {
|
let pilot = Pilot {
|
||||||
mission: "test:ground_range".into(),
|
mission: "test:ground_range".into(),
|
||||||
..starter()
|
..starter()
|
||||||
};
|
};
|
||||||
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
|
let mut attempts = Vec::new();
|
||||||
let entity = world.spawn(&id, 144, 24, &active.registry).unwrap();
|
let mut final_world = None;
|
||||||
let initial = world.enemies[0].health;
|
let mut final_entity = 0;
|
||||||
let mut lowest = initial;
|
let mut lowest = active.registry.enemies[&id].health;
|
||||||
for _ in 0..120 {
|
let initial = lowest;
|
||||||
// Normal bounded absolute mouse movement aligns the ship with the
|
for attempt in 0..2 {
|
||||||
// observed ground target; health and projectiles are never injected.
|
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
|
||||||
let target = world.enemies.iter().find(|enemy| enemy.entity == entity);
|
let entity = world.spawn(&id, 144, 24, &active.registry).unwrap();
|
||||||
let Some(enemy) = target else {
|
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;
|
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}");
|
let world = final_world.unwrap();
|
||||||
assert!(
|
let entity = final_entity;
|
||||||
!world.enemies.iter().any(|enemy| enemy.entity == entity),
|
if lowest >= initial {
|
||||||
"starter machine gun did not destroy {id}; lowest health {lowest}"
|
undamaged.push(format!(
|
||||||
);
|
"{id}: health {initial}, tick {}, outcome {:?}",
|
||||||
assert_eq!(
|
world.tick, world.outcome
|
||||||
world.player.credits,
|
));
|
||||||
pilot.credits + active.registry.enemies[&id].reward,
|
}
|
||||||
"incorrect kill reward for {id}"
|
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!({
|
observations.push(json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": active.registry.enemies[&id].name,
|
"name": active.registry.enemies[&id].name,
|
||||||
"initial_health": initial,
|
"initial_health": initial,
|
||||||
"lowest_observed_health": lowest,
|
"lowest_observed_health": lowest,
|
||||||
"destroyed": true,
|
"destroyed": destroyed,
|
||||||
|
"escaped": !target_present && !destroyed,
|
||||||
|
"outcome": format!("{:?}", world.outcome),
|
||||||
"ticks": world.tick,
|
"ticks": world.tick,
|
||||||
"reward": world.player.credits - pilot.credits,
|
"reward": world.player.credits - pilot.credits,
|
||||||
"remaining_shield": world.player.shield
|
"remaining_shield": world.player.shield
|
||||||
|
,"attempts": attempts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if let Some(path) = std::env::var_os("RAPTOR_GROUND_REPORT") {
|
if let Some(path) = std::env::var_os("RAPTOR_GROUND_REPORT") {
|
||||||
std::fs::write(path, serde_json::to_vec_pretty(&observations).unwrap()).unwrap();
|
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, ®istry).unwrap();
|
||||||
|
let mut scripts = ScriptRuntime::new();
|
||||||
|
let mut sounds = 0;
|
||||||
|
for _ in 0..100 {
|
||||||
|
shoot(&mut world, ®istry, &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, ®istry).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(), ®istry, &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()
|
||||||
|
},
|
||||||
|
®istry,
|
||||||
|
&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, ®istry).unwrap();
|
||||||
|
let mut scripts = ScriptRuntime::new();
|
||||||
|
let def = ®istry.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()
|
||||||
|
},
|
||||||
|
®istry,
|
||||||
|
&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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user