Add Iron Coast II Black Tide eight-mission campaign

This commit is contained in:
raptorreplica committed 2026-10-11 20:08:01 +11:00
1 parent a08b932beb
commit 71cb7cdba3
4492 files changed
+164577 -45

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
# Railburst Cannon 1.0.0
Enable **Railburst Cannon** in Mods, apply at the main menu, then buy it in the
shop for **685,000 credits**. It is an independent API 1 mod without dependencies
or overrides. Maximum one; resale returns 342,500 credits. It occupies a normal
inventory slot and never replaces or deletes another upgrade.
This is a **selectable, reusable special weapon**. Select it in Inventory or with
Alt/Shift during a mission, then hold Ctrl to fire. Space still drops an owned
mega bomb. Merely owning or selecting the cannon does not auto-fire it. No
ammunition is consumed; always-equipped forward guns can fire alongside it.
A native straight rail slug travels 16 logical pixels per simulation tick,
dealing 42 damage on its first real air or ground collision. That contact
penetrates at most two further living enemies ahead for 32 and 22 damage. Their
centers must stay within 12 pixels of the original firing x coordinate and within
160 pixels ahead of the first target's center. The nearest forward target wins;
stable entity order breaks ties. The primary projectile retires normally, while
the two additional contacts resolve through bounded `on_hit` damage commands.
This is finite contact-triggered penetration rather than a persistent beam.
The cooldown is 22 ticks, about 0.94 seconds at the original 70/3 Hz simulation
rate. Aim carefully: it does not home, acquire offscreen enemies, turn sideways
or clear the screen. Native point collision can miss exceptionally tiny targets
between its 16-pixel steps. Destructible terrain receives native primary damage;
the API exposes enemy snapshots, so terrain does not trigger scripted penetration.
Small air/ground impacts and impact audio occur only after an enemy contact;
empty-space shots produce fire audio and the projectile, without muzzle blasts.
The SD equipment/projectile canvases are 16×16 and 4×20, with detailed 4× HD
variants at 64×64 and 16×80. Toggle HD artwork in Options. Both modes share logical
placement and gameplay dimensions; the single-frame master has no animation
misalignment. [Artwork provenance](art-provenance.json) binds untouched built-in
generated masters, prompts, output hashes and the preparation recipe. Generated
masters, final sprites, original procedural WAVs and reproduction inputs belong
to this package; decoded original reference art is kept outside it.
`python3 generate_audio.py` reproduces the firing crack and short metal impact.
`prepare_art.rs` reproduces SD/HD fitting from the included source PNGs. The
shared read-only [native review helper](validation/review.py) builds this project,
validates both independent packages and writes indexed art/audio captures to an
explicit evidence directory. It requires the included original catalog for the
engine palette and instruments and never changes pilots or selected mods.
From the RustRaptor directory:
```sh
cargo test --locked --no-default-features --test iron_coast_stage2_weapons
cargo run --locked --no-default-features -- --data data --mods mods \
--validate-mods iron_coast_railburst
IRON_COAST_STAGE2_BALANCE_REPORT=/tmp/stage2-balance.json \
cargo test --locked --no-default-features --test iron_coast_stage2_weapons \
real_catalog_700_tick_balance_and_coexistence_report -- --ignored --nocapture
python3 mods/iron_coast_railburst/validation/review.py --output /tmp/stage2-native
```
See [validation/balance.json](validation/balance.json) for controlled 700-tick
single-target, off-axis, aligned-column, mixed-cluster and forward-gun combination
observations, and [validation/review.json](validation/review.json) for the tested
scope. These stationary fixtures establish the price/role comparison, not a claim
about every campaign encounter. Script counters and RNG checkpoints serialize
normally with pilot state; rejected callback commands roll back the whole tick.
Read the [API](../../docs/modding-api.md) for save compatibility and sandbox limits.
@@ -0,0 +1,68 @@
{
"tool": "built-in image_gen through imagegen skill",
"skill": "/home/quintine/.codex/skills/.system/imagegen/SKILL.md",
"style_reference_catalog_ids": [
"102b6",
"101cb"
],
"style_reference_delivery": "Original catalog inspected separately; decoded originals are not included in this mod. The generated artwork is newly authored.",
"source_records": [
"art/generation-equipment-v1.json",
"art/generation-projectile-v1.json"
],
"prompt_records": "prompts.json",
"conversion_recipe": "prepare_art.rs",
"conversion": "Fit alpha>=128 source bounds preserving aspect into one logical pixel margin; SD Triangle reduction and HD4x Lanczos3; binary final alpha128. Raw masters remain byte-identical to builtin outputs. Faint master glow outside the fitting bounds is not a separate in-game effect.",
"canvases": {
"equipment_sd": [
16,
16
],
"equipment_hd": [
64,
64
],
"projectile_sd": [
4,
20
],
"projectile_hd": [
16,
80
]
},
"animation_frames": 1,
"palette": "Engine maps ordinary PNG artwork to original palette0..239; no reserved palette effects requested.",
"audio_recipe": "generate_audio.py",
"audio_provenance": "audio/provenance.json",
"outputs": [
{
"path": "art/equipment-generated-source-v1.png",
"sha256": "2576062b00f8bc29078b84ff69db9cb578dfcdc08911ba4bdf90038b2c94c834"
},
{
"path": "art/equipment-hd.png",
"sha256": "afe40548427f4bc1c29a4261f4cdc1ed559d8b11e72a849cde561e9ca7429f6a"
},
{
"path": "art/equipment-sd.png",
"sha256": "c1dc5c80e9034ae742370d852c42fad5d333fc1e1e461e2a7ad23e9fe04045cf"
},
{
"path": "art/projectile-generated-source-v1.png",
"sha256": "46d958d55814c9c9fe81cd6d22847f82e1e81f8ed6ca2ff855d155c7e3281d87"
},
{
"path": "art/projectile-hd.png",
"sha256": "367f8720f69439c68552ada49f52e279ecae69a989747f16dde363c6eebb112e"
},
{
"path": "art/projectile-sd.png",
"sha256": "91958044ce398a11ed56dfe8073d09d1dd718552a481f69d349fd0490d5211cf"
}
],
"recipe_hashes": {
"prepare_art.rs": "f1ac3c61649ed9327f4169b3c0aaee3faad4cc0ee7d2c798818a2555bd9e112e",
"generate_audio.py": "1813efe19cd4c0a0914b7e41d41a5966cfcf246fabd0d318b60a65e184d3da96"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

@@ -0,0 +1,13 @@
{
"tool": "built-in image_gen",
"builtin_original_output_path": "/home/quintine/.codex/generated_images/01a129a5-0efd-7c61-8556-922e09c8ea6c/exec-cdcc2180-8730-4d67-b4f3-7772fc9d997e.png",
"local_raw_copy": "equipment-generated-source-v1.png",
"raw_sha256": "2576062b00f8bc29078b84ff69db9cb578dfcdc08911ba4bdf90038b2c94c834",
"copy_sha256": "2576062b00f8bc29078b84ff69db9cb578dfcdc08911ba4bdf90038b2c94c834",
"exact_byte_equality": true,
"prompt_file": "prompt-equipment-v1.txt",
"prompt_sha256": "b4841b8ab5ea187f3158356a03d7b533f806acd4da8256cc6664e70ddeb292ad",
"gate_lease_id": "d99da2b6-219d-44e0-babb-5ffcc0bc75d1",
"gate_outcome": "success",
"created_utc": "2026-10-11T08:17:17.156727+00:00"
}
@@ -0,0 +1,13 @@
{
"tool": "built-in image_gen",
"builtin_original_output_path": "/home/quintine/.codex/generated_images/01a129a5-0efd-7c61-8556-922e09c8ea6c/exec-a4ce4dec-d02f-43b9-9fd0-e95b61fc25de.png",
"local_raw_copy": "projectile-generated-source-v1.png",
"raw_sha256": "46d958d55814c9c9fe81cd6d22847f82e1e81f8ed6ca2ff855d155c7e3281d87",
"copy_sha256": "46d958d55814c9c9fe81cd6d22847f82e1e81f8ed6ca2ff855d155c7e3281d87",
"exact_byte_equality": true,
"prompt_file": "prompt-projectile-v1.txt",
"prompt_sha256": "55c7d44e0de631fb7ea5f98d07ec3c1581d14e9c87e5d4622ac041a72f582dbf",
"gate_lease_id": "213076b0-b1e0-4b92-a8d4-43f8ef8b1887",
"gate_outcome": "success",
"created_utc": "2026-10-11T08:18:46.812447+00:00"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

@@ -0,0 +1 @@
Use case: stylized-concept. Asset type: a single new HD equipment sprite for the shop/inventory of a Raptor-style military science-fiction vertical shooter. Generate a Railburst Cannon: compact armored gunmetal rectangular breech with a single short central accelerator muzzle pointing exactly UP, two restrained cyan induction rails along its sides, a small red service plate and bolted dark gray mounting base. Top-down orthographic overhead view, symmetrical readable silhouette with chunky industrial 1990s shooter forms, warm upper-left metal highlights and deep painted recesses. Fine machined detail for HD, clean hard silhouette readable at 16x16 logical pixels and64x64HD. Exactly one complete centered object on a genuinely transparent square canvas, with at least15percent transparent margin around the entire object. The silhouette should fill the central65percent of the canvas. No floor, black background, scene, ship, words, border, frame, cast shadow, large glow or extra objects. Muted gunmetal/steel, narrow cyan accents, tiny red service plate; no candy colors. Newly authored equipment, not a duplicate of any existing weapon.
@@ -0,0 +1 @@
Use case: stylized-concept. Asset type: one new transparent railgun projectile sprite for a Raptor-style military science-fiction vertical shooter. A single extremely narrow vertical ballistic rail slug traveling exactly UP: sharp pale steel pointed nose, short bright ivory core, a restrained slim cyan sheath, and a tapered blue-gray wake downward. It is a solid compact dart with a visible bright tip and a modest bounded trail, not a screen-length beam, not lightning, not an explosion. Top-down orthographic overhead view, perfectly straight and centered. Subject width:height about1:5; logical canvas4x20 pixels and16x80HD. Chunky crisp small-game silhouette with refined original metal/light detail forHD. One complete object on a genuinely transparent square canvas, generous blank side areas, at least15percent top/bottom transparent margin. No gun, spacecraft, text, border, background, shadow, smoke cloud, detached sparkles, branching, multiple objects or outside glow. Match restrained pale steel/ivory/cyan highlights of a classic1990s military shooter.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,34 @@
{
"tool": "original procedural Python synthesis",
"recipe": "generate_audio.py",
"source_recordings": [],
"deterministic_seed": "0x71ac092d",
"outputs": [
{
"file": "audio/fire.wav",
"sha256": "fbc829573e92ec4a824bb4ba00a3dff53c866685c0ff1270335d896a6cabdf7c",
"rate": 22050,
"channels": 1,
"bits": 16,
"frames": 3528,
"duration_seconds": 0.16,
"peak": 16253,
"rms": 2346.20609017488,
"dc_mean": 19.44331065759637,
"clipped_samples": 0
},
{
"file": "audio/impact.wav",
"sha256": "4a44c8f561c230f519ed926d849e7e0bff1db2f11cf705c67c70d2eccb1c8221",
"rate": 22050,
"channels": 1,
"bits": 16,
"frames": 5071,
"duration_seconds": 0.22997732426303855,
"peak": 15667,
"rms": 2250.0482951936838,
"dc_mean": 14.399132321041215,
"clipped_samples": 0
}
]
}
+67
View File
@@ -0,0 +1,67 @@
{
"weapons": [
{
"id": "iron_coast_railburst:projectile",
"name": "Railburst Cannon Projectile",
"damage": 42,
"speed": 16,
"cooldown": 22,
"behavior": "straight",
"target": "all",
"offsets": [
[
0,
-4
]
],
"art": {
"png": "art/projectile-sd.png",
"hd_png": "art/projectile-hd.png",
"logical_width": 4,
"logical_height": 20,
"nearest": true
},
"script": "scripts/weapon.rhai"
}
],
"items": [
{
"id": "iron_coast_railburst:equipment",
"name": "Railburst Cannon",
"price": 685000,
"max_count": 1,
"initial_count": 1,
"weapon": "iron_coast_railburst:projectile",
"special": true,
"consumable": false,
"art": {
"png": "art/equipment-sd.png",
"hd_png": "art/equipment-hd.png",
"logical_width": 16,
"logical_height": 16,
"nearest": true
},
"description": "Pierces up to 3 aligned targets. Selectable reusable special. Ctrl fires a fast rail slug at air or ground targets. A real hit penetrates up to two more enemies ahead within a narrow 24-pixel lane, with falling damage (42/32/22). Best against aligned armor; no ammunition consumed. Alt/Shift selects weapons; Space drops mega bombs."
}
],
"sounds": [
{
"id": "iron_coast_railburst:fire",
"name": "Railburst Cannon Fire",
"file": "audio/fire.wav",
"volume": 88,
"pan": 127,
"pitch": 128,
"priority": 1
},
{
"id": "iron_coast_railburst:impact",
"name": "Railburst Cannon Impact",
"file": "audio/impact.wav",
"volume": 82,
"pan": 127,
"pitch": 128,
"priority": 2
}
]
}
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Reproduce original deterministic PCM sound effects; no external recordings."""
import math, struct, wave, json, hashlib
from pathlib import Path
PACKAGE = 'iron_coast_railburst'
RATE = 22050
def render(kind):
rail = PACKAGE.endswith("railburst")
duration = (0.16 if kind == "fire" else 0.23) if rail else (0.21 if kind == "fire" else 0.46)
count = int(RATE * duration)
seed = 0x71AC092D if rail else 0xDC042681
phase = 0.0
samples = []
for n in range(count):
t = n / RATE
progress = n / count
seed ^= (seed << 13) & 0xffffffff
seed ^= seed >> 17
seed ^= (seed << 5) & 0xffffffff
seed &= 0xffffffff
noise = seed / 2147483648.0 - 1.0
if rail:
frequency = (2100 * math.exp(-22*t) + 280) if kind == "fire" else (780 * math.exp(-14*t) + 95)
tone = 0.30*math.sin(phase) + 0.18*math.sin(phase*1.993) + noise*(0.44 if kind=="fire" else 0.34)
envelope = math.exp(-(32 if kind=="fire" else 19)*t)
else:
frequency = (175 * math.exp(-10*t) + 65) if kind=="fire" else (98*math.exp(-7*t)+42)
tone = math.sin(phase)*0.53 + math.sin(phase*1.47)*0.10 + noise*(0.26 if kind=="fire" else 0.35)*math.exp(-6*t)
envelope = math.exp(-(20 if kind=="fire" else 9)*t)
phase += 2*math.pi*frequency/RATE
attack = min(1.0, n/(RATE*0.002))
tail = min(1.0, (count-n)/(RATE*0.015))
samples.append(round(max(-1.0,min(1.0,tone*envelope*attack*tail))*22000))
return samples
if __name__ == "__main__":
root=Path(__file__).resolve().parent
rows=[]
for kind in ["fire","impact"]:
samples=render(kind)
path=root/"audio"/(kind+".wav")
with wave.open(str(path),"wb") as output:
output.setparams((1,2,RATE,0,"NONE","not compressed"))
output.writeframes(struct.pack("<"+"h"*len(samples),*samples))
rows.append({"file":str(path.relative_to(root)),"sha256":hashlib.sha256(path.read_bytes()).hexdigest(),"rate":RATE,"channels":1,"bits":16,"frames":len(samples),"duration_seconds":len(samples)/RATE,"peak":max(abs(x) for x in samples),"rms":math.sqrt(sum(x*x for x in samples)/len(samples)),"dc_mean":sum(samples)/len(samples),"clipped_samples":sum(abs(x)>=32767 for x in samples)})
(root/"audio"/"provenance.json").write_text(json.dumps({"tool":"original procedural Python synthesis","recipe":"generate_audio.py","source_recordings":[],"deterministic_seed":hex(0x71AC092D if PACKAGE.endswith("railburst") else 0xDC042681),"outputs":rows},indent=2)+"\n")
+7
View File
@@ -0,0 +1,7 @@
id = "iron_coast_railburst"
name = "Railburst Cannon"
version = "1.0.0"
api_version = 1
description = "Independent Iron Coast II selectable upgrade with SD/HD artwork and procedural fire/impact audio."
content = "content.json"
scripts = ["scripts/weapon.rhai"]
+76
View File
@@ -0,0 +1,76 @@
const SHOT_W: u32 = 4;
const SHOT_H: u32 = 20;
// Fit untouched generated masters to shared logical canvases and four-times HD.
// Compile with this project's pinned image crate; see README for reproduction.
use image::{Rgba, RgbaImage, imageops};
use std::{env, fs, path::Path};
fn fit(source: &RgbaImage, w: u32, h: u32, scale: u32) -> RgbaImage {
let mut bounds = (source.width(), source.height(), 0, 0);
for (x, y, p) in source.enumerate_pixels() {
if p[3] >= 128 {
bounds.0 = bounds.0.min(x);
bounds.1 = bounds.1.min(y);
bounds.2 = bounds.2.max(x + 1);
bounds.3 = bounds.3.max(y + 1);
}
}
assert!(
bounds.2 > bounds.0 && bounds.3 > bounds.1,
"no meaningful source alpha"
);
let crop = imageops::crop_imm(
source,
bounds.0,
bounds.1,
bounds.2 - bounds.0,
bounds.3 - bounds.1,
)
.to_image();
// Exactly one logical pixel of clear margin, shared across SD and HD.
let factor = ((w - 2) as f64 / crop.width() as f64).min((h - 2) as f64 / crop.height() as f64);
let nw = (crop.width() as f64 * factor).round().max(1.) as u32;
let nh = (crop.height() as f64 * factor).round().max(1.) as u32;
let filter = if scale == 1 {
imageops::FilterType::Triangle
} else {
imageops::FilterType::Lanczos3
};
let mut scaled = imageops::resize(&crop, nw * scale, nh * scale, filter);
for p in scaled.pixels_mut() {
if p[3] < 128 {
*p = Rgba([0, 0, 0, 0]);
} else {
p[3] = 255;
}
}
let mut out = RgbaImage::new(w * scale, h * scale);
imageops::overlay(
&mut out,
&scaled,
((w * scale - nw * scale) / 2) as i64,
((h * scale - nh * scale) / 2) as i64,
);
out
}
fn main() {
let args: Vec<_> = env::args().collect();
assert_eq!(args.len(), 2, "usage: prepare_art PACKAGE_DIRECTORY");
let root = Path::new(&args[1]);
for (kind, w, h) in [("equipment", 16, 16), ("projectile", SHOT_W, SHOT_H)] {
let source = image::open(root.join(format!("art/{kind}-generated-source-v1.png")))
.unwrap()
.into_rgba8();
for (mode, scale) in [("sd", 1), ("hd", 4)] {
let image = fit(&source, w, h, scale);
let path = root.join(format!("art/{kind}-{mode}.png"));
image.save(&path).unwrap();
println!("{} {}x{}", path.display(), image.width(), image.height());
}
}
assert!(
fs::metadata(root.join("art/equipment-hd.png"))
.unwrap()
.len()
> 0
);
}
+14
View File
@@ -0,0 +1,14 @@
{
"use_case": "stylized-concept",
"tool": "built-in image_gen",
"skill": "/home/quintine/.codex/skills/.system/imagegen/SKILL.md",
"style_reference_ids": [
"102b6",
"101cb"
],
"reference_role": "Original catalog was inspected for classic palette, silhouette and scale; no decoded reference is included in this mod. New generation uses the prepared textual art direction.",
"prompts": {
"equipment": "Use case: stylized-concept. Asset type: a single new HD equipment sprite for the shop/inventory of a Raptor-style military science-fiction vertical shooter. Generate a Railburst Cannon: compact armored gunmetal rectangular breech with a single short central accelerator muzzle pointing exactly UP, two restrained cyan induction rails along its sides, a small red service plate and bolted dark gray mounting base. Top-down orthographic overhead view, symmetrical readable silhouette with chunky industrial 1990s shooter forms, warm upper-left metal highlights and deep painted recesses. Fine machined detail for HD, clean hard silhouette readable at 16x16 logical pixels and64x64HD. Exactly one complete centered object on a genuinely transparent square canvas, with at least15percent transparent margin around the entire object. The silhouette should fill the central65percent of the canvas. No floor, black background, scene, ship, words, border, frame, cast shadow, large glow or extra objects. Muted gunmetal/steel, narrow cyan accents, tiny red service plate; no candy colors. Newly authored equipment, not a duplicate of any existing weapon.",
"projectile": "Use case: stylized-concept. Asset type: one new transparent railgun projectile sprite for a Raptor-style military science-fiction vertical shooter. A single extremely narrow vertical ballistic rail slug traveling exactly UP: sharp pale steel pointed nose, short bright ivory core, a restrained slim cyan sheath, and a tapered blue-gray wake downward. It is a solid compact dart with a visible bright tip and a modest bounded trail, not a screen-length beam, not lightning, not an explosion. Top-down orthographic overhead view, perfectly straight and centered. Subject width:height about1:5; logical canvas4x20 pixels and16x80HD. Chunky crisp small-game silhouette with refined original metal/light detail forHD. One complete object on a genuinely transparent square canvas, generous blank side areas, at least15percent top/bottom transparent margin. No gun, spacecraft, text, border, background, shadow, smoke cloud, detached sparkles, branching, multiple objects or outside glow. Match restrained pale steel/ivory/cyan highlights of a classic1990s military shooter."
}
}
@@ -0,0 +1,52 @@
// Native straight projectile supplies the first collision and its 42 damage.
// Penetration is contact-triggered, finite and locked to that projectile's x lane.
fn on_fire(event, state, random) {
if event.hostile { return #{commands: [], state: state}; }
let shots = if state.contains("shots") { state.shots % 100000 + 1 } else { 1 };
let hits = if state.contains("hits") { state.hits } else { 0 };
#{commands: [#{op: "play_sound", sound: "iron_coast_railburst:fire"}],
state: #{shots: shots, hits: hits}}
}
fn on_hit(event, state, random) {
let shots = if state.contains("shots") { state.shots } else { 0 };
let hits = if state.contains("hits") { state.hits % 100000 + 1 } else { 1 };
let origin_y = event.y;
let ground = false;
for enemy in event.entities {
if enemy.entity == event.entity {
origin_y = enemy.y + enemy.height / 2;
ground = enemy.ground;
}
}
let kind = if ground { "small_ground" } else { "small_air" };
let commands = [#{op: "play_sound", sound: "iron_coast_railburst:impact"},
#{op: "effect", kind: kind, x: event.x, y: event.y}];
let struck = [event.entity];
let previous_y = origin_y;
let damages = [32, 22];
for hop in 0..2 {
let chosen = #{};
let nearest = 1000000;
for enemy in event.entities {
let cx = enemy.x + enemy.width / 2;
let cy = enemy.y + enemy.height / 2;
if enemy.health <= 0 || struck.contains(enemy.entity)
|| cx < 0 || cx >= 320 || cy < 0 || cy >= 200 { continue; }
let distance = previous_y - cy;
if abs(cx - event.x) <= 12 && distance > 0
&& origin_y - cy <= 160 && distance < nearest {
chosen = #{entity: enemy.entity, x: cx, y: cy, ground: enemy.ground};
nearest = distance;
}
}
if !chosen.contains("entity") { break; }
commands.push(#{op: "damage", entity: chosen.entity, amount: damages[hop]});
let impact = if chosen.ground { "small_ground" } else { "small_air" };
commands.push(#{op: "effect", kind: impact, x: chosen.x, y: chosen.y});
struck.push(chosen.entity);
previous_y = chosen.y;
hits = hits % 100000 + 1;
}
#{commands: commands, state: #{shots: shots, hits: hits}}
}
@@ -0,0 +1,901 @@
{
"difficulty": 2,
"fixture_health": 100000,
"rate": "70/3 Hz",
"rows": [
{
"damage": [
1344
],
"group_dps": 44.8,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1344
],
"group_dps": 44.8,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1344,
1024,
704,
0
],
"group_dps": 102.4,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1344,
0,
0,
0,
0,
0
],
"group_dps": 44.8,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
2038
],
"group_dps": 67.93333333333334,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
2038
],
"group_dps": 67.93333333333334,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
2040,
1024,
704,
0
],
"group_dps": 125.6,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
2038,
0,
0,
0,
0,
0
],
"group_dps": 67.93333333333334,
"item": "iron_coast_railburst:equipment",
"price": 685000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1080
],
"group_dps": 36.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1080,
0,
0,
0
],
"group_dps": 36.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1080,
540,
300,
300,
0,
0
],
"group_dps": 74.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
694
],
"group_dps": 23.133333333333333,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1774
],
"group_dps": 59.13333333333333,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1776,
0,
0,
0
],
"group_dps": 59.2,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1774,
540,
300,
300,
0,
0
],
"group_dps": 97.13333333333334,
"item": "iron_coast_depth_charge:equipment",
"price": 525000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
800
],
"group_dps": 26.666666666666668,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
800
],
"group_dps": 26.666666666666668,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
800,
800,
800,
800
],
"group_dps": 106.66666666666667,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
800,
800,
0,
0,
0,
800
],
"group_dps": 80.0,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1494
],
"group_dps": 49.8,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1494
],
"group_dps": 49.8,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1496,
800,
800,
800
],
"group_dps": 129.86666666666667,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1494,
800,
0,
0,
0,
800
],
"group_dps": 103.13333333333334,
"item": "ion_lance:equipment",
"price": 425000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1248
],
"group_dps": 41.6,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1248
],
"group_dps": 41.6,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1248,
780,
468,
312
],
"group_dps": 93.6,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1248,
468,
312,
0,
0,
780
],
"group_dps": 93.6,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
1942
],
"group_dps": 64.73333333333333,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1942
],
"group_dps": 64.73333333333333,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1944,
780,
468,
312
],
"group_dps": 116.8,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
1942,
468,
312,
0,
0,
780
],
"group_dps": 116.73333333333333,
"item": "lightning_cannon:equipment",
"price": 825000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": true
},
{
"damage": [
694
],
"group_dps": 23.133333333333333,
"item": "base:forward_guns",
"price": 12000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
694
],
"group_dps": 23.133333333333333,
"item": "base:forward_guns",
"price": 12000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "base:forward_guns",
"price": 12000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "base:forward_guns",
"price": 12000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
696,
0,
0,
0
],
"group_dps": 23.2,
"item": "base:forward_guns",
"price": 12000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
694,
0,
0,
0,
0,
0
],
"group_dps": 23.133333333333333,
"item": "base:forward_guns",
"price": 12000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
2400
],
"group_dps": 80.0,
"item": "base:death_ray",
"price": 950000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
2400
],
"group_dps": 80.0,
"item": "base:death_ray",
"price": 950000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "base:death_ray",
"price": 950000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "base:death_ray",
"price": 950000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
2400,
0,
0,
0
],
"group_dps": 80.0,
"item": "base:death_ray",
"price": 950000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
2400,
0,
0,
0,
0,
0
],
"group_dps": 80.0,
"item": "base:death_ray",
"price": 950000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
8000
],
"group_dps": 266.6666666666667,
"item": "base:forward_laser",
"price": 1750000,
"scene": "aligned_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
8000
],
"group_dps": 266.6666666666667,
"item": "base:forward_laser",
"price": 1750000,
"scene": "aligned_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "base:forward_laser",
"price": 1750000,
"scene": "off_axis_air",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
0
],
"group_dps": 0.0,
"item": "base:forward_laser",
"price": 1750000,
"scene": "off_axis_ground",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
8000,
0,
0,
0
],
"group_dps": 266.6666666666667,
"item": "base:forward_laser",
"price": 1750000,
"scene": "aligned_column",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
},
{
"damage": [
8000,
0,
0,
0,
0,
0
],
"group_dps": 266.6666666666667,
"item": "base:forward_laser",
"price": 1750000,
"scene": "ground_cluster",
"seconds": 30,
"ticks": 700,
"with_forward_guns": false
}
],
"target_size": [
32,
32
],
"package": "iron_coast_railburst",
"final_price": 685000,
"resale_price": 342500,
"pricing_reason": "44.8 single air/ground DPS and102.4 finite three-target aligned-column DPS, zero off-axis damage. Higher single-target throughput than425,000 Ion Lance26.67, but tighter initial acquisition and finite penetration compared with825,000 Lightning Cannon41.6 single/93.6 chain and wider acquisition. Below950,000 Death Ray80 single and1,750,000 Forward Laser266.67. Ion Lance can exceed this finite penetration against four or more perfectly stacked targets; this is a different aiming/cadence choice, not an upgrade over every role.",
"observation": "Actual World::step700ticks; deterministic stationary32x32 health100000 fixtures, ground scroll canceled after normal collision pass. Native original and new weapons unmodified. Forward-gun combination separately observed. Does not measure campaign difficulty or moving-target accuracy.",
"source_input_hashes": {
"mod.toml": "e22f6dd8e8aaeb7fe1549cdddc2be28fcda71a16f9285c975701e77a009faf84",
"content.json": "de70d59f723cce78ca4b59a3ecce704ce877517d91d83a06526daed7f42d4484",
"scripts/weapon.rhai": "209ae40d88b03bda4802c1970852d791f84b48986990d70c0e3dbcf502ba5b9b"
}
}
@@ -0,0 +1,145 @@
//! Read-only public engine asset/audio review for both independent Stage 2 mods.
//! Writes captures only to the caller's evidence directory, never to game data.
use image::{RgbaImage, imageops};
use raptor_replica::{
assets::{Catalog, Sprite, TRANSPARENT},
audio::Mixer,
content::{ContentId, Registry, art_sprite_variant},
modding::{self, ActiveMods},
render::{Canvas, nearest},
sound::{SoundBank, encode_capture},
};
use serde_json::json;
use std::{env, fs, path::Path};
fn normalized(sprite: &Sprite, w: usize, h: usize) -> Sprite {
let mut out = Sprite::blank(w, h);
for y in 0..h {
for x in 0..w {
out.pixels[y * w + x] =
sprite.pixels[(y * sprite.height / h) * sprite.width + x * sprite.width / w];
}
}
out
}
fn main() {
let args: Vec<_> = env::args().collect();
assert_eq!(args.len(), 4, "usage: native_review DATA MODS EVIDENCE");
let catalog = Catalog::open(&args[1]).unwrap();
let base = Registry::base(&catalog).unwrap();
let ids = ["iron_coast_railburst", "iron_coast_depth_charge"].map(String::from);
let discovery = modding::discover(Path::new(&args[2]));
let active = ActiveMods::build(&base, &discovery, &ids, Some(&catalog)).unwrap();
let out = Path::new(&args[3]);
fs::create_dir_all(out).unwrap();
let palette = catalog.palette().unwrap();
let mut rows = Vec::new();
let mut gallery = RgbaImage::new(1024, 1024);
let mut row = 0;
for owner in &ids {
for kind in ["equipment", "projectile"] {
let id: ContentId = format!(
"{owner}:{}",
if kind == "equipment" {
"equipment"
} else {
"projectile"
}
)
.as_str()
.into();
let art = active.registry.art(&id).unwrap();
for hd in [false, true] {
let source =
art_sprite_variant(&catalog, &active.registry, owner, art, 0, hd).unwrap();
assert!(
source
.pixels
.iter()
.filter(|c| **c != TRANSPARENT)
.all(|c| *c < 240)
);
assert!(
source.pixels[0] == TRANSPARENT
&& source.pixels[source.width - 1] == TRANSPARENT
);
let mut counts = Vec::new();
for scale in 1..=4 {
// The same canvas sizing and nearest resampling used by Host::art.
let display = if source.width == art.logical_width as usize
&& source.height == art.logical_height as usize
{
nearest(&source, scale)
} else {
normalized(
&source,
art.logical_width as usize * scale,
art.logical_height as usize * scale,
)
};
counts.push(json!({"scale":scale,"width":display.width,"height":display.height,"painted":display.pixels.iter().filter(|c|**c!=TRANSPARENT).count()}));
let mut canvas = Canvas::new(scale);
canvas.clear(12);
canvas.rect(160, 0, 160, 200, 93);
for (x, y) in [(70, 90), (230, 90)] {
canvas.blit(&display, x * scale as i32, y * scale as i32);
}
let rgba = RgbaImage::from_raw(
canvas.image.width as u32,
canvas.image.height as u32,
canvas.rgba(&palette, false),
)
.unwrap();
rgba.save(out.join(format!(
"{owner}-{kind}-{}-{scale}x.png",
if hd { "hd" } else { "sd" }
)))
.unwrap();
}
let display = normalized(
&source,
art.logical_width as usize * 8,
art.logical_height as usize * 8,
);
let mut cell = Canvas::new(1);
cell.clear(12);
cell.rect(160, 0, 160, 200, 93);
cell.blit(&display, 20, 20);
cell.blit(&display, 180, 20);
let rgba = RgbaImage::from_raw(320, 200, cell.rgba(&palette, false)).unwrap();
imageops::overlay(
&mut gallery,
&rgba,
if hd { 512 } else { 0 },
(row * 256) as i64,
);
rows.push(json!({"owner":owner,"kind":kind,"hd":hd,"physical_canvas":[source.width,source.height],"logical_canvas":[art.logical_width,art.logical_height],"frame":0,"frames":art.frames,"ordinary_palette_only":true,"clear_corner_alpha":true,"all_scales":counts}));
}
row += 1;
}
}
gallery.save(out.join("indexed-sd-hd-gallery.png")).unwrap();
let instruments = catalog.named("GENMIDI_OP2").unwrap();
let mut audio = Vec::new();
for owner in &ids {
for kind in ["fire", "impact"] {
let id: ContentId = format!("{owner}:{kind}").as_str().into();
let mut bank = SoundBank::default();
let sample = bank.mod_sample(&active.registry, &id).unwrap();
assert!(!sample.data.is_empty());
let mut mixer = Mixer::new(44100, &instruments).unwrap();
mixer.start_capture(1).unwrap();
bank.queue_mod(&active.registry, &mut mixer, &id).unwrap();
let mut samples = vec![0; 44100 * 2];
mixer.mix(&mut samples);
assert!(samples.iter().any(|x| *x != 0));
let file = format!("{owner}-{kind}-mixed.wav");
fs::write(
out.join(&file),
encode_capture(&mixer.take_capture().unwrap()).unwrap(),
)
.unwrap();
audio.push(json!({"id":id,"decoder":"SoundBank::mod_sample","mixer":"Mixer::sample through SoundBank::queue_mod","source_rate":sample.rate,"decoded_samples":sample.data.len(),"mixed_rate":44100,"mixed_channels":2,"nonzero_output_samples":samples.iter().filter(|x|**x!=0).count(),"peak":samples.iter().map(|x|(*x as i32).abs()).max().unwrap(),"capture":file}));
}
}
fs::write(out.join("native-review.json"),serde_json::to_vec_pretty(&json!({"scope":"Public PNG decoder, same Host art normalization, indexed Canvas blitter and actual SoundBank/Mixer. Synthetic backgrounds; no live desktop/gameplay claim.","assets":rows,"audio":audio})).unwrap()).unwrap();
}
@@ -0,0 +1,283 @@
{
"package": "iron_coast_railburst",
"portable_tests": {
"file": "tests/iron_coast_stage2_weapons.rs",
"passed": 9,
"balance_integration_passed": 1,
"cases": [
"independent activation and coexistence",
"shop half-price resale and duplicate rejection",
"rail aligned contact finite penetration",
"depth contact-only ground radius/falloff/cap",
"unselected/idle/misses without explosions",
"serialized state/RNG continuation and invalid-command rollback",
"explicit cooldown/reusable inventory",
"lethal primary contact continuation",
"full-slot inventory preservation",
"disable/re-enable saved equipment, exact mod pins and script state preserved without altering save"
]
},
"native_decoder_blitter_audio": {
"assets": [
{
"all_scales": [
{
"height": 16,
"painted": 94,
"scale": 1,
"width": 16
},
{
"height": 32,
"painted": 376,
"scale": 2,
"width": 32
},
{
"height": 48,
"painted": 846,
"scale": 3,
"width": 48
},
{
"height": 64,
"painted": 1504,
"scale": 4,
"width": 64
}
],
"clear_corner_alpha": true,
"frame": 0,
"frames": 1,
"hd": false,
"kind": "equipment",
"logical_canvas": [
16,
16
],
"ordinary_palette_only": true,
"owner": "iron_coast_railburst",
"physical_canvas": [
16,
16
]
},
{
"all_scales": [
{
"height": 16,
"painted": 92,
"scale": 1,
"width": 16
},
{
"height": 32,
"painted": 361,
"scale": 2,
"width": 32
},
{
"height": 48,
"painted": 809,
"scale": 3,
"width": 48
},
{
"height": 64,
"painted": 1434,
"scale": 4,
"width": 64
}
],
"clear_corner_alpha": true,
"frame": 0,
"frames": 1,
"hd": true,
"kind": "equipment",
"logical_canvas": [
16,
16
],
"ordinary_palette_only": true,
"owner": "iron_coast_railburst",
"physical_canvas": [
64,
64
]
},
{
"all_scales": [
{
"height": 20,
"painted": 26,
"scale": 1,
"width": 4
},
{
"height": 40,
"painted": 104,
"scale": 2,
"width": 8
},
{
"height": 60,
"painted": 234,
"scale": 3,
"width": 12
},
{
"height": 80,
"painted": 416,
"scale": 4,
"width": 16
}
],
"clear_corner_alpha": true,
"frame": 0,
"frames": 1,
"hd": false,
"kind": "projectile",
"logical_canvas": [
4,
20
],
"ordinary_palette_only": true,
"owner": "iron_coast_railburst",
"physical_canvas": [
4,
20
]
},
{
"all_scales": [
{
"height": 20,
"painted": 21,
"scale": 1,
"width": 4
},
{
"height": 40,
"painted": 87,
"scale": 2,
"width": 8
},
{
"height": 60,
"painted": 200,
"scale": 3,
"width": 12
},
{
"height": 80,
"painted": 352,
"scale": 4,
"width": 16
}
],
"clear_corner_alpha": true,
"frame": 0,
"frames": 1,
"hd": true,
"kind": "projectile",
"logical_canvas": [
4,
20
],
"ordinary_palette_only": true,
"owner": "iron_coast_railburst",
"physical_canvas": [
16,
80
]
}
],
"audio": [
{
"capture": "iron_coast_railburst-fire-mixed.wav",
"decoded_samples": 3528,
"decoder": "SoundBank::mod_sample",
"id": "iron_coast_railburst:fire",
"mixed_channels": 2,
"mixed_rate": 44100,
"mixer": "Mixer::sample through SoundBank::queue_mod",
"nonzero_output_samples": 6320,
"peak": 3200,
"source_rate": 22050
},
{
"capture": "iron_coast_railburst-impact-mixed.wav",
"decoded_samples": 5071,
"decoder": "SoundBank::mod_sample",
"id": "iron_coast_railburst:impact",
"mixed_channels": 2,
"mixed_rate": 44100,
"mixer": "Mixer::sample through SoundBank::queue_mod",
"nonzero_output_samples": 11168,
"peak": 2432,
"source_rate": 22050
}
],
"scope": "Public PNG decoder, same Host art normalization, indexed Canvas blitter and actual SoundBank/Mixer. Synthetic backgrounds; no live desktop/gameplay claim."
},
"visual_inspection": "Inspected original style references separately, all four raw masters and actual indexed SD/HD gallery at logical and4x canvases. Readable compact silhouettes, neutral metal with cyan rail/orange charge roles, transparent borders. SD colors intentionally simplified by reduction; no reserved palette effects. Captures use synthetic palette backgrounds, not a live mission or actual shop pane.",
"limitations": [
"Private desktop Host/shop screenshots are delegated to root integration review.",
"Native point collision may skip exceptionally tiny targets for the speed16 rail shot.",
"Terrain cells receive native primary damage but are not available as scripted penetration/splash snapshots."
],
"evidence_gallery_sha256": "c1b5d6ad1d286eb17fab1067143222a6071dff5e70a35d904757275767ec5a26",
"reproduction_byte_equality": {
"art/equipment-sd.png": true,
"art/equipment-hd.png": true,
"art/projectile-sd.png": true,
"art/projectile-hd.png": true,
"audio/fire.wav": true,
"audio/impact.wav": true
},
"native_build_bindings": {
"libraries": {
"raptor_replica": {
"path": "/home/quintine/Projects/RaptorReplica/RustRaptor/target/debug/libraptor_replica.rlib",
"sha256": "4fd737d3c6c808fd5de1ecfc1f9a41b0ef37e82dc1a186f005b8dc9c2360ccd5"
},
"image": {
"path": "/home/quintine/Projects/RaptorReplica/RustRaptor/target/debug/deps/libimage-25e356663b4aa6b0.rlib",
"sha256": "ea709066ea4c8c625efe3bf7f3741d7a68abfb7ef4b304837f1f0da20e5e2d58"
},
"serde_json": {
"path": "/home/quintine/Projects/RaptorReplica/RustRaptor/target/debug/deps/libserde_json-1d17875658175ae0.rlib",
"sha256": "e463d4b704418cc5a168d73818662f7a801a77d96e8ef3d6a121e90375e4532f"
}
},
"review_source_sha256": "32c4e836b7a01d159251427b2203dcf0d40f43bebef1def2f13a571e845d8ca3",
"validation_cases": [
{
"enabled": [
"iron_coast_railburst"
],
"stdout": "Validated 1 mods; 0 override(s)",
"stderr": "",
"exit_code": 0
},
{
"enabled": [
"iron_coast_depth_charge"
],
"stdout": "Validated 1 mods; 0 override(s)",
"stderr": "",
"exit_code": 0
},
{
"enabled": [
"iron_coast_railburst",
"iron_coast_depth_charge"
],
"stdout": "Validated 2 mods; 0 override(s)",
"stderr": "",
"exit_code": 0
}
],
"scope": "Frozen package copies; public asset, indexed Canvas and audio APIs; no live desktop or pilot mutations."
}
}
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Reproduce isolated Stage 2 package validation and public indexed/audio review."""
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--data", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parents[3]
data = (args.data or root / "data").resolve()
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
packages = ["iron_coast_railburst", "iron_coast_depth_charge"]
subprocess.run(["cargo", "build", "--locked", "--no-default-features", "--lib", "--bin", "raptor-rust"], cwd=root, check=True)
deps = root / "target/debug/deps"
suffix = ".exe" if os.name == "nt" else ""
executable = output / ("native_review" + suffix)
command = ["rustc", "--edition=2024", str(Path(__file__).with_name("native_review.rs")), "-L", f"dependency={deps}", "-o", str(executable)]
libraries = {}
for name in ["raptor_replica", "image", "serde_json"]:
library = root / "target/debug/libraptor_replica.rlib" if name == "raptor_replica" else max(deps.glob(f"lib{name}-*.rlib"), key=lambda path: path.stat().st_mtime_ns)
command += ["--extern", f"{name}={library}"]
libraries[name] = {"path": str(library), "sha256": hashlib.sha256(library.read_bytes()).hexdigest()}
subprocess.run(command, cwd=root, check=True)
cases = []
with tempfile.TemporaryDirectory(prefix="iron-coast-stage2-review-") as folder:
frozen = Path(folder)
for package in packages:
shutil.copytree(root / "mods" / package, frozen / package)
for selected in [[packages[0]], [packages[1]], packages]:
result = subprocess.run([str(root / ("target/debug/raptor-rust" + suffix)), "--data", str(data), "--mods", str(frozen), "--validate-mods", ",".join(selected)], cwd=root, check=True, capture_output=True, text=True)
print(result.stdout, end="")
cases.append({"enabled": selected, "stdout": result.stdout.strip(), "stderr": result.stderr.strip(), "exit_code": result.returncode})
subprocess.run([str(executable), str(data), str(frozen), str(output)], cwd=root, check=True)
(output / "build-bindings.json").write_text(json.dumps({"libraries": libraries, "review_source_sha256": hashlib.sha256(Path(__file__).with_name("native_review.rs").read_bytes()).hexdigest(), "validation_cases": cases, "scope": "Frozen package copies; public asset, indexed Canvas and audio APIs; no live desktop or pilot mutations."}, indent=2) + "\n")
if __name__ == "__main__":
main()