Add Iron Coast II Black Tide eight-mission campaign
No files matched your search
@@ -0,0 +1,62 @@
|
||||
# Depth Charge 1.0.0
|
||||
|
||||
Enable **Depth Charge** in Mods, apply at the main menu, then buy it in the shop
|
||||
for **525,000 credits**. It is an independent API 1 mod without dependencies or
|
||||
overrides. Maximum one; resale returns 262,500 credits. It occupies a normal
|
||||
inventory slot and preserves other equipment.
|
||||
|
||||
This is a **selectable, reusable ground/naval special weapon**. Choose it in
|
||||
Inventory or with Alt/Shift, then hold Ctrl to launch. Space remains the owned
|
||||
mega-bomb control. It does not consume charges or auto-attack merely because it
|
||||
is equipped. Always-equipped forward guns remain available alongside it.
|
||||
|
||||
The native straight projectile travels six logical pixels per simulation tick.
|
||||
A real ground-enemy collision deals 72 primary damage and starts one local
|
||||
ground-blast effect. At most three other living ground enemies can receive
|
||||
collateral damage: 36 damage within 16 pixels of the primary enemy's center,
|
||||
or 20 damage between 16 and 36 pixels inclusive. The closest neighbors are
|
||||
selected first, with stable entity order breaking ties. Aircraft, offscreen
|
||||
enemies, repeated targets and centers outside that radius are excluded. If the
|
||||
primary target dies, its contact still triggers the local blast correctly.
|
||||
|
||||
The deliberate 48-tick cooldown is about 2.06 seconds at 70/3 Hz. The charge does
|
||||
not home or detonate automatically in empty space. Slow travel makes moving
|
||||
ground targets an aiming challenge; there is no screen-wide damage command.
|
||||
Destructible terrain receives native primary damage, but cannot trigger scripted
|
||||
collateral because the public snapshot contains enemies rather than terrain
|
||||
cells. Enemy contacts emit impact audio; a miss produces only the launch sound
|
||||
and projectile. The normal native terrain destruction effects remain available.
|
||||
|
||||
SD equipment/projectile canvases are 16×16 and 8×12; detailed HD variants are
|
||||
64×64 and 32×48. Options selects SD or HD artwork without changing logical
|
||||
placement or damage. Both are single aligned frames with transparent margins.
|
||||
[Artwork provenance](art-provenance.json) records unchanged generated masters,
|
||||
prompts, final PNG hashes and the Rust preparation recipe. All new images, WAVs,
|
||||
source masters and recipes are included; decoded original style references are
|
||||
kept outside the mod.
|
||||
|
||||
`python3 generate_audio.py` reproduces the mechanical launch and low naval impact
|
||||
from deterministic synthesis without external recordings. `prepare_art.rs`
|
||||
rebuilds SD/HD fitting from the included source masters. The companion Railburst
|
||||
package's [review helper](../iron_coast_railburst/validation/review.py) is a developer
|
||||
tool for both packages and is not a gameplay dependency.
|
||||
|
||||
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_depth_charge
|
||||
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
|
||||
```
|
||||
|
||||
[validation/balance.json](validation/balance.json) records the actual controlled
|
||||
700-tick damage observations supporting this price and role. Portable checks
|
||||
cover ground filtering, inner/outer falloff, the finite neighbor cap, misses,
|
||||
kills, cooldown, independent activation, coexistence, half-price resale, full
|
||||
inventory preservation, deterministic state/RNG continuation and whole-tick
|
||||
rollback. [validation/review.json](validation/review.json) distinguishes public
|
||||
decoder/blitter/mixer captures from live desktop review. Read the [API](../../docs/modding-api.md)
|
||||
for save pins and script execution 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": [
|
||||
8,
|
||||
12
|
||||
],
|
||||
"projectile_hd": [
|
||||
32,
|
||||
48
|
||||
]
|
||||
},
|
||||
"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": "669202bb78951419c9a1f61070be5dba7a55d8bfe096a071ae9cde86a8e26d3a"
|
||||
},
|
||||
{
|
||||
"path": "art/equipment-hd.png",
|
||||
"sha256": "b906ee08176c1fe429d61fcb22fe0ea06a513d11efb063def6639809466274ea"
|
||||
},
|
||||
{
|
||||
"path": "art/equipment-sd.png",
|
||||
"sha256": "61a9a9edc3fdb257e7f82e70c61d1fd3688728cd9c65cadf2b2e50e82fb789fd"
|
||||
},
|
||||
{
|
||||
"path": "art/projectile-generated-source-v1.png",
|
||||
"sha256": "0bc76b678c0d2db073ccf76ba2162465b3beb7fd78d505d875c95c594daeb29d"
|
||||
},
|
||||
{
|
||||
"path": "art/projectile-hd.png",
|
||||
"sha256": "1fb0ef6030151d68a477546711f8af120eb2563541d117147c6c2ee4f42f730d"
|
||||
},
|
||||
{
|
||||
"path": "art/projectile-sd.png",
|
||||
"sha256": "80a98973fa5e887bec6d90e603b50cef693f7ea69d00b3b8608569a1e59e0776"
|
||||
}
|
||||
],
|
||||
"recipe_hashes": {
|
||||
"prepare_art.rs": "bd1d0514317e5063989f2371b9ca9fd71756cb502b0066d9932f6f39031de3f6",
|
||||
"generate_audio.py": "a554a3a0e2b8f5432705fe32006c87998984b6e27430ac5a77b163c313760fce"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 912 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 610 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-584bb594-5382-4b01-95ac-a78ce6f08c45.png",
|
||||
"local_raw_copy": "equipment-generated-source-v1.png",
|
||||
"raw_sha256": "669202bb78951419c9a1f61070be5dba7a55d8bfe096a071ae9cde86a8e26d3a",
|
||||
"copy_sha256": "669202bb78951419c9a1f61070be5dba7a55d8bfe096a071ae9cde86a8e26d3a",
|
||||
"exact_byte_equality": true,
|
||||
"prompt_file": "prompt-equipment-v1.txt",
|
||||
"prompt_sha256": "3ec33270866c302c452984e3dae48110f2b5438703b90a81dcf27bb3e06f31cb",
|
||||
"gate_lease_id": "c2377ab5-8490-4d0c-8470-8bb44a22ff8f",
|
||||
"gate_outcome": "success",
|
||||
"created_utc": "2026-10-11T08:19:49.376523+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-4c2fdbca-d17c-4334-893e-963638433a60.png",
|
||||
"local_raw_copy": "projectile-generated-source-v1.png",
|
||||
"raw_sha256": "0bc76b678c0d2db073ccf76ba2162465b3beb7fd78d505d875c95c594daeb29d",
|
||||
"copy_sha256": "0bc76b678c0d2db073ccf76ba2162465b3beb7fd78d505d875c95c594daeb29d",
|
||||
"exact_byte_equality": true,
|
||||
"prompt_file": "prompt-projectile-v1.txt",
|
||||
"prompt_sha256": "1d314f5a738e58139d69fb67aa372022f32fa40ca32399953a67daf36b58cc46",
|
||||
"gate_lease_id": "aeddcd06-2ad8-4327-b176-13d542e243ec",
|
||||
"gate_outcome": "success",
|
||||
"created_utc": "2026-10-11T08:20:28.206524+00:00"
|
||||
}
|
||||
|
After Width: | Height: | Size: 894 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 274 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 Depth Charge launcher module: compact dark gunmetal armored double cradle holding two stout gray-blue naval charges side by side, their rounded copper-gray noses pointing exactly UP, two small burnt-orange safety bands and dark aft fins. Squat robust naval ordnance silhouette, readable top-down orthographic overhead view. Muted steel, deep navy-blue enamel, tiny orange safety accents, warm upper-left metal highlights, precise dark recesses. Match chunky industrial1990s military shooter readability with refined HD metal panels and bolts. Logical16x16 and64x64HD. Exactly one complete centered equipment assembly on genuinely transparent square canvas, minimum15percent transparent margin all sides, central65percent occupancy. No ship, water, ground, scenery, text, border, frame, floor, cast shadow, outside glow, separate loose charges or extra objects. Newly authored naval equipment.
|
||||
@@ -0,0 +1 @@
|
||||
Use case: stylized-concept. Asset type: one new transparent naval depth-charge projectile sprite for a Raptor-style military science-fiction vertical shooter. Exactly one stout gray-blue metallic cylindrical charge traveling exactly UP, rounded copper-steel nose at top, a small burnt-orange safety band around the middle, dark navy barrel and three short robust aft stabilizer fins at bottom. Top-down orthographic overhead view; compact straight symmetrical body, width:height about2:3, no perspective or rotation. Logical8x12pixels and32x48HD. Crisp chunky silhouette readable in SD with refined small metal seams forHD, subdued1990s military shooter palette and upper-left lighting. One full object centered on a genuinely transparent square canvas with at least15percent clear margin all sides. No weapon launcher, aircraft, water, floor, smoke, flame, glow, shadow, text, border, frame, debris, additional objects or an explosion. Keep fins short and legible.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"tool": "original procedural Python synthesis",
|
||||
"recipe": "generate_audio.py",
|
||||
"source_recordings": [],
|
||||
"deterministic_seed": "0xdc042681",
|
||||
"outputs": [
|
||||
{
|
||||
"file": "audio/fire.wav",
|
||||
"sha256": "810ac71ce38d2012b6145baa8cb54f39d8eb4807e6c8b05528ad83d8123ff510",
|
||||
"rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"frames": 4630,
|
||||
"duration_seconds": 0.20997732426303856,
|
||||
"peak": 15254,
|
||||
"rms": 2870.510735780875,
|
||||
"dc_mean": -13.693304535637148,
|
||||
"clipped_samples": 0
|
||||
},
|
||||
{
|
||||
"file": "audio/impact.wav",
|
||||
"sha256": "aef495acd4c70c4d7dfe0868471a769624e23636659c2580fa867355cffc894a",
|
||||
"rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"frames": 10143,
|
||||
"duration_seconds": 0.46,
|
||||
"peak": 17180,
|
||||
"rms": 3110.238630183446,
|
||||
"dc_mean": 2.850142955733018,
|
||||
"clipped_samples": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "iron_coast_depth_charge:projectile",
|
||||
"name": "Depth Charge Projectile",
|
||||
"damage": 72,
|
||||
"speed": 6,
|
||||
"cooldown": 48,
|
||||
"behavior": "straight",
|
||||
"target": "ground",
|
||||
"offsets": [
|
||||
[
|
||||
0,
|
||||
-4
|
||||
]
|
||||
],
|
||||
"art": {
|
||||
"png": "art/projectile-sd.png",
|
||||
"hd_png": "art/projectile-hd.png",
|
||||
"logical_width": 8,
|
||||
"logical_height": 12,
|
||||
"nearest": true
|
||||
},
|
||||
"script": "scripts/weapon.rhai"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "iron_coast_depth_charge:equipment",
|
||||
"name": "Depth Charge",
|
||||
"price": 525000,
|
||||
"max_count": 1,
|
||||
"initial_count": 1,
|
||||
"weapon": "iron_coast_depth_charge: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": "Ground/naval blast, 36px radius. Selectable reusable ground/naval special. Ctrl launches a slow charge; a real ground hit deals 72 damage and splashes at most three other ground enemies within 36 pixels (36 inner / 20 outer). Deliberate 2.06-second cadence; no air damage or ammunition consumed. Alt/Shift selects weapons; Space drops mega bombs."
|
||||
}
|
||||
],
|
||||
"sounds": [
|
||||
{
|
||||
"id": "iron_coast_depth_charge:fire",
|
||||
"name": "Depth Charge Fire",
|
||||
"file": "audio/fire.wav",
|
||||
"volume": 88,
|
||||
"pan": 127,
|
||||
"pitch": 128,
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"id": "iron_coast_depth_charge:impact",
|
||||
"name": "Depth Charge 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_depth_charge'
|
||||
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")
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "iron_coast_depth_charge"
|
||||
name = "Depth Charge"
|
||||
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"]
|
||||
@@ -0,0 +1,76 @@
|
||||
const SHOT_W: u32 = 8;
|
||||
const SHOT_H: u32 = 12;
|
||||
// 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
|
||||
);
|
||||
}
|
||||
@@ -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 Depth Charge launcher module: compact dark gunmetal armored double cradle holding two stout gray-blue naval charges side by side, their rounded copper-gray noses pointing exactly UP, two small burnt-orange safety bands and dark aft fins. Squat robust naval ordnance silhouette, readable top-down orthographic overhead view. Muted steel, deep navy-blue enamel, tiny orange safety accents, warm upper-left metal highlights, precise dark recesses. Match chunky industrial1990s military shooter readability with refined HD metal panels and bolts. Logical16x16 and64x64HD. Exactly one complete centered equipment assembly on genuinely transparent square canvas, minimum15percent transparent margin all sides, central65percent occupancy. No ship, water, ground, scenery, text, border, frame, floor, cast shadow, outside glow, separate loose charges or extra objects. Newly authored naval equipment.",
|
||||
"projectile": "Use case: stylized-concept. Asset type: one new transparent naval depth-charge projectile sprite for a Raptor-style military science-fiction vertical shooter. Exactly one stout gray-blue metallic cylindrical charge traveling exactly UP, rounded copper-steel nose at top, a small burnt-orange safety band around the middle, dark navy barrel and three short robust aft stabilizer fins at bottom. Top-down orthographic overhead view; compact straight symmetrical body, width:height about2:3, no perspective or rotation. Logical8x12pixels and32x48HD. Crisp chunky silhouette readable in SD with refined small metal seams forHD, subdued1990s military shooter palette and upper-left lighting. One full object centered on a genuinely transparent square canvas with at least15percent clear margin all sides. No weapon launcher, aircraft, water, floor, smoke, flame, glow, shadow, text, border, frame, debris, additional objects or an explosion. Keep fins short and legible."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Damage is collision-triggered. Empty space, aircraft and inactive equipment
|
||||
// never start a blast. Native targeting handles the primary ground collision.
|
||||
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_depth_charge: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 center_x = event.x;
|
||||
let center_y = event.y;
|
||||
for enemy in event.entities {
|
||||
if enemy.entity == event.entity {
|
||||
center_x = enemy.x + enemy.width / 2;
|
||||
center_y = enemy.y + enemy.height / 2;
|
||||
}
|
||||
}
|
||||
let commands = [#{op: "play_sound", sound: "iron_coast_depth_charge:impact"},
|
||||
#{op: "effect", kind: "ground_blast", x: center_x, y: center_y}];
|
||||
let struck = [event.entity];
|
||||
for neighbor in 0..3 {
|
||||
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.ground || enemy.health <= 0 || struck.contains(enemy.entity)
|
||||
|| cx < 0 || cx >= 320 || cy < 0 || cy >= 200 { continue; }
|
||||
let dx = cx - center_x;
|
||||
let dy = cy - center_y;
|
||||
let distance = dx * dx + dy * dy;
|
||||
if distance <= 36 * 36 && distance < nearest {
|
||||
chosen = #{entity: enemy.entity};
|
||||
nearest = distance;
|
||||
}
|
||||
}
|
||||
if !chosen.contains("entity") { break; }
|
||||
let damage = if nearest <= 16 * 16 { 36 } else { 20 };
|
||||
commands.push(#{op: "damage", entity: chosen.entity, amount: damage});
|
||||
struck.push(chosen.entity);
|
||||
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_depth_charge",
|
||||
"final_price": 525000,
|
||||
"resale_price": 262500,
|
||||
"pricing_reason": "36 ground single-target DPS and74 finite mixed-cluster ground DPS, zero air/off-axis damage, 48-tick slow cadence. Above425,000 Ion Lance26.67 single and285,000 Seeker Swarm23.4 single for local ground utility, but slower and ground-only. Below825,000 Lightning Cannon41.6 air/ground single and93.6 chain,950,000 Death Ray80 single,1,750,000 Forward Laser266.67. Forward guns add normal primary damage, not unbounded splash.",
|
||||
"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": "b0acc9149813522720ca6fc1e246a5a383e8641c89f7623558a6a5baa0e2d892",
|
||||
"content.json": "1806d8451b6734096609c2188e42e002de9c82d7ac19361f51a329fe066ca07e",
|
||||
"scripts/weapon.rhai": "23eb0d61ce768e6d8392b394c9274deee7ac5624915c4d3bae40e2050fef4d66"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"package": "iron_coast_depth_charge",
|
||||
"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": 147,
|
||||
"scale": 1,
|
||||
"width": 16
|
||||
},
|
||||
{
|
||||
"height": 32,
|
||||
"painted": 588,
|
||||
"scale": 2,
|
||||
"width": 32
|
||||
},
|
||||
{
|
||||
"height": 48,
|
||||
"painted": 1323,
|
||||
"scale": 3,
|
||||
"width": 48
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"painted": 2352,
|
||||
"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_depth_charge",
|
||||
"physical_canvas": [
|
||||
16,
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"all_scales": [
|
||||
{
|
||||
"height": 16,
|
||||
"painted": 141,
|
||||
"scale": 1,
|
||||
"width": 16
|
||||
},
|
||||
{
|
||||
"height": 32,
|
||||
"painted": 561,
|
||||
"scale": 2,
|
||||
"width": 32
|
||||
},
|
||||
{
|
||||
"height": 48,
|
||||
"painted": 1252,
|
||||
"scale": 3,
|
||||
"width": 48
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"painted": 2236,
|
||||
"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_depth_charge",
|
||||
"physical_canvas": [
|
||||
64,
|
||||
64
|
||||
]
|
||||
},
|
||||
{
|
||||
"all_scales": [
|
||||
{
|
||||
"height": 12,
|
||||
"painted": 34,
|
||||
"scale": 1,
|
||||
"width": 8
|
||||
},
|
||||
{
|
||||
"height": 24,
|
||||
"painted": 136,
|
||||
"scale": 2,
|
||||
"width": 16
|
||||
},
|
||||
{
|
||||
"height": 36,
|
||||
"painted": 306,
|
||||
"scale": 3,
|
||||
"width": 24
|
||||
},
|
||||
{
|
||||
"height": 48,
|
||||
"painted": 544,
|
||||
"scale": 4,
|
||||
"width": 32
|
||||
}
|
||||
],
|
||||
"clear_corner_alpha": true,
|
||||
"frame": 0,
|
||||
"frames": 1,
|
||||
"hd": false,
|
||||
"kind": "projectile",
|
||||
"logical_canvas": [
|
||||
8,
|
||||
12
|
||||
],
|
||||
"ordinary_palette_only": true,
|
||||
"owner": "iron_coast_depth_charge",
|
||||
"physical_canvas": [
|
||||
8,
|
||||
12
|
||||
]
|
||||
},
|
||||
{
|
||||
"all_scales": [
|
||||
{
|
||||
"height": 12,
|
||||
"painted": 28,
|
||||
"scale": 1,
|
||||
"width": 8
|
||||
},
|
||||
{
|
||||
"height": 24,
|
||||
"painted": 110,
|
||||
"scale": 2,
|
||||
"width": 16
|
||||
},
|
||||
{
|
||||
"height": 36,
|
||||
"painted": 248,
|
||||
"scale": 3,
|
||||
"width": 24
|
||||
},
|
||||
{
|
||||
"height": 48,
|
||||
"painted": 444,
|
||||
"scale": 4,
|
||||
"width": 32
|
||||
}
|
||||
],
|
||||
"clear_corner_alpha": true,
|
||||
"frame": 0,
|
||||
"frames": 1,
|
||||
"hd": true,
|
||||
"kind": "projectile",
|
||||
"logical_canvas": [
|
||||
8,
|
||||
12
|
||||
],
|
||||
"ordinary_palette_only": true,
|
||||
"owner": "iron_coast_depth_charge",
|
||||
"physical_canvas": [
|
||||
32,
|
||||
48
|
||||
]
|
||||
}
|
||||
],
|
||||
"audio": [
|
||||
{
|
||||
"capture": "iron_coast_depth_charge-fire-mixed.wav",
|
||||
"decoded_samples": 4630,
|
||||
"decoder": "SoundBank::mod_sample",
|
||||
"id": "iron_coast_depth_charge:fire",
|
||||
"mixed_channels": 2,
|
||||
"mixed_rate": 44100,
|
||||
"mixer": "Mixer::sample through SoundBank::queue_mod",
|
||||
"nonzero_output_samples": 11448,
|
||||
"peak": 2688,
|
||||
"source_rate": 22050
|
||||
},
|
||||
{
|
||||
"capture": "iron_coast_depth_charge-impact-mixed.wav",
|
||||
"decoded_samples": 10143,
|
||||
"decoder": "SoundBank::mod_sample",
|
||||
"id": "iron_coast_depth_charge:impact",
|
||||
"mixed_channels": 2,
|
||||
"mixed_rate": 44100,
|
||||
"mixer": "Mixer::sample through SoundBank::queue_mod",
|
||||
"nonzero_output_samples": 27200,
|
||||
"peak": 3200,
|
||||
"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,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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 979 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
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"
|
||||
}
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
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.
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,40 @@
|
||||
# Iron Coast II: Gatebreaker
|
||||
|
||||
A standalone boss content package for Black Tide. Enable this package directly
|
||||
in **Mods**, or enable its mission to select it as a dependency. It adds
|
||||
`iron_coast_stage2_boss_1:unit` and no campaign, overrides or equipment.
|
||||
|
||||
The surfaced/ground target has 320 health, awards 12,000 credits and retains
|
||||
a 128×64 logical collision canvas in SD and HD. It remains reachable until killed.
|
||||
Its two phases change at half of that life’s actual spawn health on every
|
||||
difficulty, with a new attack timer and twelve ticks of audible warning before
|
||||
every volley. Difficulty 0/1 uses the engine’s half-health boss rule; 2/3 uses
|
||||
the nominal health above. Cadence is 84 / 66 simulation ticks
|
||||
(first/second phase), at the original 70/3 Hz. Every projectile is hostile and
|
||||
leaves deliberate escape lanes; no phase is invulnerable.
|
||||
|
||||
Actual death increments `iron_coast_stage2_boss_1:defeated` once. It does not finish a mission.
|
||||
Per-life cursors are removed on death and pruned on spawn/replay; early kills
|
||||
before the queued spawn callback still register the real death.
|
||||
|
||||
Artwork is a genuine new builtin imagegen master, with full alpha and identical
|
||||
SD/HD canvas alignment. `build_assets.py` reproduces palette-aware SD art, 4× HD
|
||||
and deterministic PCM WAV effects from the tracked raw masters. Projectile art
|
||||
is the same newly authored micro-missile independently included with each boss.
|
||||
See [generation provenance](generation-provenance.json) and
|
||||
[asset recipe/report](asset-report.json). Original Raptor style references and
|
||||
local review captures are external.
|
||||
|
||||
From `RustRaptor/`:
|
||||
|
||||
```sh
|
||||
cargo run --locked --no-default-features -- --mods mods --validate-mods iron_coast_stage2_boss_1
|
||||
CARGO_TARGET_DIR="$PWD/target" cargo run --locked --release --manifest-path mods/iron_coast_stage2_boss_1/validation/Cargo.toml -- --boss-only
|
||||
python3 mods/iron_coast_stage2_boss_1/build_assets.py
|
||||
```
|
||||
|
||||
The real-catalog standalone kill, every-difficulty phase boundaries, early and
|
||||
duplicate hook checks and native Mixer/mute observations passed. Exact results
|
||||
and scopes are recorded in `acceptance-report.json`.
|
||||
|
||||
Asset recipes use Python 3 with Pillow. The game uses the shipped PNG and WAV files.
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"utc": "2026-10-11T08:51:02.199238+00:00",
|
||||
"status": "author_checked_review_ready",
|
||||
"real_catalog": true,
|
||||
"engine_source_hashes_at_report": {
|
||||
"src/assets.rs": "4cdf11a229f6ca4c6183fb545dba43e9a2323f839f7a095292045aa13a773ddc",
|
||||
"src/content.rs": "7f35df7100c450ba4bb30a0fdc90101c115b5ae42a7aac57829441c5b7761647",
|
||||
"src/modding.rs": "f37d67004392f5726121e58a995900cd7062016e3f060ea131b0df38995e2912",
|
||||
"src/simulation.rs": "0ec9f6da11ab82c1461868a7b147e4cdb14689fa7373bd0c24067d9014e9583a",
|
||||
"src/render.rs": "d4abdccbc3c1ee840ef8548bcff43080521937ef9f9f5ed69e810f417ef1e842",
|
||||
"src/sound.rs": "aff6779b85f9f4f4bc30a15ad75ec6fdd61d42895639700f4842870636e73c4b",
|
||||
"src/persistence.rs": "9a4adf1e33d3504db3d4d3e6091316cc7116fb35c89b2de8fbd4640d368aa0e2",
|
||||
"Cargo.lock": "aac0a22725250ab35828223f8d129e4d2a4e6113cf81463e59600da3a59ffdea"
|
||||
},
|
||||
"catalog_file_hashes": {
|
||||
"data/INDEX": "4ce78e5e638b6603114dd233ceaa41f386c0963ba4aa02ecf88ce0215b096681",
|
||||
"data/misc/PALETTE_DAT": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"data/misc/GENMIDI_OP2": "fdc93c076bf44207299e61ec1bbcfafca421454bfa99dd14fa4e5dcada1547b9"
|
||||
},
|
||||
"compiled_validation_binary_sha256": "98f43eef5943061a18a465e95e8089189be793b9f8cbac275b8ebe982953567b",
|
||||
"full_external_result_sha256": "63f97c1f8cab57452ee9235124551178fe2593c5e40f4a5ecd6e2aa9bf0cd19e",
|
||||
"cli": {
|
||||
"selected_packages": 10,
|
||||
"overrides": 0,
|
||||
"log_sha256": "c9401f44dc4fa438f75ccc00223e95f79cfa8bffcd1c8db5c106963f7b474822"
|
||||
},
|
||||
"visual_limits": "Author inspected native indexed SD1/HD4 Canvas/PNG-loader diagnostics, exact new source masters and SD/HD final art. Terrain in owned captures uses a simplified row sampler. Live SDL/Host captures and independent eight-stage progression are root-owned; no live device or whole-map rendering claim.",
|
||||
"package": "iron_coast_stage2_boss_1",
|
||||
"content_sha256": "06d945df4b52b2147bc6e084f4d2c0c5a684f9b8ccfdd8f2b14cbf6c111c65bb",
|
||||
"scripts": {
|
||||
"scripts/ai.rhai": "8d4319cf6e71ad0aad1888f197ed44ce392570d1d9e2f033df33167c9300d878"
|
||||
},
|
||||
"asset_report_sha256": "47cd7665858657dc316f5a29ddf210f45eb8ed723e31237710ada55381feebdd",
|
||||
"raw_identity_checks": [
|
||||
{
|
||||
"package": "iron_coast_stage2_boss_1",
|
||||
"role": "unit_or_terrain",
|
||||
"raw": "mods/iron_coast_stage2_boss_1/art/unit-generated-v1.png",
|
||||
"raw_sha256": "62df3558a79431a86b4ada4fe393b003506aaf912ae74415a33411724b688f65",
|
||||
"builtin_byte_identical": true,
|
||||
"raw_dimensions": [
|
||||
1774,
|
||||
887
|
||||
],
|
||||
"raw_alpha": "RGBA",
|
||||
"reported_success": true
|
||||
},
|
||||
{
|
||||
"package": "iron_coast_stage2_boss_1",
|
||||
"role": "projectile",
|
||||
"raw": "mods/iron_coast_stage2_boss_1/art/projectile-generated-v1.png",
|
||||
"raw_sha256": "9cc6e7e6cac60f045050bdefe1c84f5fc7185bcb585b87b7a7df5b2206a38ebb",
|
||||
"builtin_byte_identical": true,
|
||||
"raw_dimensions": [
|
||||
887,
|
||||
1774
|
||||
],
|
||||
"raw_alpha": "RGBA",
|
||||
"reported_success": true
|
||||
}
|
||||
],
|
||||
"standalone_world_kill": {
|
||||
"boss": "iron_coast_stage2_boss_1:unit",
|
||||
"cues": [
|
||||
"iron_coast_stage2_boss_1:death",
|
||||
"iron_coast_stage2_boss_1:shot",
|
||||
"iron_coast_stage2_boss_1:warning"
|
||||
],
|
||||
"digest_before_extra_step": "4ef3f340c570094ccc262640df26df187b3954236284b618da2d3245392c95ee",
|
||||
"minimum_observed_health": 0,
|
||||
"serialized_script_state_bytes": 98,
|
||||
"shield": 75,
|
||||
"tick": 331
|
||||
},
|
||||
"all_difficulty_phase_checks": [
|
||||
{
|
||||
"actual_spawn_max_health": 160,
|
||||
"boss": "iron_coast_stage2_boss_1:unit",
|
||||
"difficulty": 0,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
},
|
||||
{
|
||||
"actual_spawn_max_health": 160,
|
||||
"boss": "iron_coast_stage2_boss_1:unit",
|
||||
"difficulty": 1,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
},
|
||||
{
|
||||
"actual_spawn_max_health": 320,
|
||||
"boss": "iron_coast_stage2_boss_1:unit",
|
||||
"difficulty": 2,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
},
|
||||
{
|
||||
"actual_spawn_max_health": 320,
|
||||
"boss": "iron_coast_stage2_boss_1:unit",
|
||||
"difficulty": 3,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
}
|
||||
],
|
||||
"early_duplicate_callback_scope": "Direct Rhai hook protocol checks register an already-dead spawn silently and count its death once; independent actual lethal-spawn World review remains separate.",
|
||||
"sound_mixer_checks": [
|
||||
{
|
||||
"fx_mute_zero": true,
|
||||
"mixed_peak": 3072,
|
||||
"sound": "iron_coast_stage2_boss_1:death"
|
||||
},
|
||||
{
|
||||
"fx_mute_zero": true,
|
||||
"mixed_peak": 2688,
|
||||
"sound": "iron_coast_stage2_boss_1:shot"
|
||||
},
|
||||
{
|
||||
"fx_mute_zero": true,
|
||||
"mixed_peak": 1920,
|
||||
"sound": "iron_coast_stage2_boss_1:warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"reference_role": "Original Raptor military style only, not target geometry or approved HD reference",
|
||||
"reference_review_path": "/home/quintine/Projects/RaptorReplica/RustRaptor/.reference/iron-coast-stage2-authoring/original-style-references.png",
|
||||
"reference_review_sha256": "d92f7d68008a73d626dcc9125920b7b329d029ca87a01d28bc4005f80c4f9174",
|
||||
"original_ids": [
|
||||
"10139",
|
||||
"10344",
|
||||
"400d5",
|
||||
"4011f"
|
||||
],
|
||||
"original_reference_files_not_packaged": true
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
{
|
||||
"utc": "2026-10-11T08:16:03.984933+00:00",
|
||||
"recipe": "build_assets.py",
|
||||
"recipe_sha256": "f86680d12fc6e15b585f4bba40170242518c397acdd0c1cb0992d22e0a46f462",
|
||||
"palette_sha256": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"ordinary_palette_entries_only": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
37,
|
||||
38,
|
||||
39,
|
||||
40,
|
||||
41,
|
||||
42,
|
||||
43,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
56,
|
||||
57,
|
||||
58,
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
68,
|
||||
69,
|
||||
70,
|
||||
71,
|
||||
72,
|
||||
73,
|
||||
74,
|
||||
75,
|
||||
76,
|
||||
77,
|
||||
78,
|
||||
79,
|
||||
80,
|
||||
81,
|
||||
82,
|
||||
83,
|
||||
84,
|
||||
85,
|
||||
86,
|
||||
87,
|
||||
88,
|
||||
89,
|
||||
90,
|
||||
91,
|
||||
92,
|
||||
93,
|
||||
94,
|
||||
95,
|
||||
96,
|
||||
97,
|
||||
98,
|
||||
99,
|
||||
100,
|
||||
101,
|
||||
102,
|
||||
103,
|
||||
104,
|
||||
105,
|
||||
106,
|
||||
107,
|
||||
108,
|
||||
109,
|
||||
110,
|
||||
111,
|
||||
112,
|
||||
113,
|
||||
114,
|
||||
115,
|
||||
116,
|
||||
117,
|
||||
118,
|
||||
119,
|
||||
120,
|
||||
121,
|
||||
122,
|
||||
123,
|
||||
124,
|
||||
125,
|
||||
126,
|
||||
127,
|
||||
128,
|
||||
129,
|
||||
130,
|
||||
131,
|
||||
132,
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
136,
|
||||
137,
|
||||
138,
|
||||
139,
|
||||
140,
|
||||
141,
|
||||
142,
|
||||
143,
|
||||
144,
|
||||
145,
|
||||
146,
|
||||
147,
|
||||
148,
|
||||
149,
|
||||
150,
|
||||
151,
|
||||
152,
|
||||
153,
|
||||
154,
|
||||
155,
|
||||
156,
|
||||
157,
|
||||
158,
|
||||
159,
|
||||
160,
|
||||
161,
|
||||
162,
|
||||
163,
|
||||
164,
|
||||
165,
|
||||
166,
|
||||
167,
|
||||
168,
|
||||
169,
|
||||
170,
|
||||
171,
|
||||
172,
|
||||
173,
|
||||
174,
|
||||
175,
|
||||
176,
|
||||
177,
|
||||
178,
|
||||
179,
|
||||
180,
|
||||
181,
|
||||
182,
|
||||
183,
|
||||
184,
|
||||
185,
|
||||
186,
|
||||
187,
|
||||
188,
|
||||
189,
|
||||
190,
|
||||
191,
|
||||
192,
|
||||
193,
|
||||
194,
|
||||
195,
|
||||
196,
|
||||
197,
|
||||
198,
|
||||
199,
|
||||
200,
|
||||
201,
|
||||
202,
|
||||
203,
|
||||
204,
|
||||
205,
|
||||
206,
|
||||
207,
|
||||
208,
|
||||
209,
|
||||
210,
|
||||
211,
|
||||
212,
|
||||
213,
|
||||
214,
|
||||
215,
|
||||
216,
|
||||
217,
|
||||
218,
|
||||
219,
|
||||
220,
|
||||
221,
|
||||
222,
|
||||
223,
|
||||
224,
|
||||
225,
|
||||
226,
|
||||
227,
|
||||
228,
|
||||
229,
|
||||
230,
|
||||
231,
|
||||
232,
|
||||
233,
|
||||
234,
|
||||
235,
|
||||
236,
|
||||
237,
|
||||
238,
|
||||
239
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"source": "art/unit-generated-v1.png",
|
||||
"sha256": "62df3558a79431a86b4ada4fe393b003506aaf912ae74415a33411724b688f65",
|
||||
"logical_size": [
|
||||
128,
|
||||
64
|
||||
],
|
||||
"hd_size": [
|
||||
512,
|
||||
256
|
||||
],
|
||||
"alpha_below128_transparent": true,
|
||||
"original_master_not_modified": true,
|
||||
"whole_alpha_bounding_crop_for_projectile": null
|
||||
},
|
||||
{
|
||||
"source": "art/projectile-generated-v1.png",
|
||||
"sha256": "9cc6e7e6cac60f045050bdefe1c84f5fc7185bcb585b87b7a7df5b2206a38ebb",
|
||||
"logical_size": [
|
||||
8,
|
||||
16
|
||||
],
|
||||
"hd_size": [
|
||||
32,
|
||||
64
|
||||
],
|
||||
"alpha_below128_transparent": true,
|
||||
"original_master_not_modified": true,
|
||||
"whole_alpha_bounding_crop_for_projectile": [
|
||||
363,
|
||||
637,
|
||||
525,
|
||||
1110
|
||||
]
|
||||
}
|
||||
],
|
||||
"audio": [
|
||||
{
|
||||
"file": "audio/warning.wav",
|
||||
"sha256": "84497a75c3eafd803c3e87353c3f2e7e568ba6a57b021d910fa67a3e478f023d",
|
||||
"samples": 11025,
|
||||
"rate": 22050,
|
||||
"duration": 0.5,
|
||||
"peak": 7344,
|
||||
"rms": 2423.21,
|
||||
"seed": 8081
|
||||
},
|
||||
{
|
||||
"file": "audio/shot.wav",
|
||||
"sha256": "823b9d2d3d51ccefea88e1511afaeb7a40d5e22ce47001bc3376e6419f1d0b09",
|
||||
"samples": 3528,
|
||||
"rate": 22050,
|
||||
"duration": 0.16,
|
||||
"peak": 9463,
|
||||
"rms": 1891.06,
|
||||
"seed": 7589
|
||||
},
|
||||
{
|
||||
"file": "audio/death.wav",
|
||||
"sha256": "599d493fa8b7e9944a9932135a62d0007e04533bb78f80e83aff862ca2ab4092",
|
||||
"samples": 23152,
|
||||
"rate": 22050,
|
||||
"duration": 1.05,
|
||||
"peak": 8143,
|
||||
"rms": 2001.53,
|
||||
"seed": 7641
|
||||
}
|
||||
],
|
||||
"final_art": [
|
||||
{
|
||||
"file": "art/projectile-hd.png",
|
||||
"sha256": "449bc288ba96deca89aa9318fba47662d8bf8e07d82494f978ccbc4f5dd4a869",
|
||||
"size": [
|
||||
32,
|
||||
64
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-sd.png",
|
||||
"sha256": "5f408b4713a948f75d7ba1e6ee582fe8eb91a235df41e2e6d1bb4dac050fbac3",
|
||||
"size": [
|
||||
8,
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "art/unit-hd.png",
|
||||
"sha256": "b20cef0904d3fe854af05d44667d35a491e4a58948358d034ea37f4a555830a3",
|
||||
"size": [
|
||||
512,
|
||||
256
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "art/unit-sd.png",
|
||||
"sha256": "a2ab4da8797b6f820b9ed5f43a86637015eea239248283fd59094d96c71873da",
|
||||
"size": [
|
||||
128,
|
||||
64
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce SD/HD canvases and PCM WAVs from untouched builtin generated PNGs.
|
||||
No decoded original images are packaged. No painted/masked master edits occur.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import argparse,datetime,hashlib,json,math,random,struct,wave
|
||||
from PIL import Image,ImageOps
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
h=lambda p:hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
||||
def palette(path):
|
||||
raw=path.read_bytes();assert len(raw)==768
|
||||
values=[(min(v,63)<<2)|(min(v,63)>>4) for v in raw]
|
||||
values[720:]=[0]*48
|
||||
im=Image.new('P',(1,1));im.putpalette(values);return im
|
||||
|
||||
def small(im,size,pal):
|
||||
im=im.convert('RGBA').resize(size,Image.Resampling.LANCZOS)
|
||||
alpha=im.getchannel('A').point(lambda a:255 if a>=128 else 0)
|
||||
out=im.convert('RGB').quantize(palette=pal,dither=Image.Dither.NONE).convert('RGBA');out.putalpha(alpha);return out
|
||||
|
||||
def canvases(source,name,size,pal):
|
||||
src=Image.open(source).convert('RGBA');target=(size[0]*4,size[1]*4)
|
||||
bounds=None
|
||||
if name=='projectile':
|
||||
bounds=src.getchannel('A').point(lambda a:255 if a>=128 else 0).getbbox();assert bounds;src=src.crop(bounds)
|
||||
fit_size=(round(target[0]*.8),round(target[1]*.8)) if name=='projectile' else target
|
||||
hd=Image.new('RGBA',target);fitted=ImageOps.contain(src,fit_size,Image.Resampling.LANCZOS);hd.alpha_composite(fitted,((target[0]-fitted.width)//2,(target[1]-fitted.height)//2))
|
||||
hd.save(ROOT/'art'/f'{name}-hd.png');small(hd,size,pal).save(ROOT/'art'/f'{name}-sd.png')
|
||||
return {'source':str(source.relative_to(ROOT)),'sha256':h(source),'logical_size':size,'hd_size':target,'alpha_below128_transparent':True,'original_master_not_modified':True,'whole_alpha_bounding_crop_for_projectile':bounds}
|
||||
|
||||
def synth(name,duration,base):
|
||||
rate=22050;n=int(rate*duration);rng=random.Random(7043+sum(map(ord,name))+base);data=bytearray();peak=0;sumsq=0
|
||||
for i in range(n):
|
||||
t=i/rate;u=i/n;env=min(1,u*30)*(1-u)**1.3
|
||||
if name=='shot':s=(math.sin(2*math.pi*(95*t-40*t*t))+rng.uniform(-1,1)*0.45)*math.exp(-u*3)
|
||||
elif name=='death':s=rng.uniform(-1,1)*0.65+math.sin(2*math.pi*(70*t-18*t*t))*0.4
|
||||
elif name=='warning' or name=='boss_alarm':s=math.sin(2*math.pi*(base+70*math.sin(2*math.pi*t*5))*t)*0.8+0.12*math.sin(2*math.pi*base*.5*t)
|
||||
else:
|
||||
notes=[1,1.25,1.5,2] if name=='victory' else [1,1.5,1.25];freq=base*notes[min(len(notes)-1,int(u*len(notes)))];s=math.sin(2*math.pi*freq*t)+0.15*math.sin(2*math.pi*freq*2*t)
|
||||
v=max(-32767,min(32767,int(s*env*8500)));peak=max(peak,abs(v));sumsq+=v*v;data+=struct.pack('<h',v)
|
||||
p=ROOT/'audio'/f'{name}.wav'
|
||||
with wave.open(str(p),'wb') as w:w.setnchannels(1);w.setsampwidth(2);w.setframerate(rate);w.writeframes(data)
|
||||
return {'file':str(p.relative_to(ROOT)),'sha256':h(p),'samples':n,'rate':rate,'duration':duration,'peak':peak,'rms':round(math.sqrt(sumsq/n),2),'seed':7043+sum(map(ord,name))+base}
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument('--palette',type=Path,default=ROOT.parents[1]/'data/misc/PALETTE_DAT');args=parser.parse_args();pal=palette(args.palette)
|
||||
report={'utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'recipe':Path(__file__).name,'recipe_sha256':h(__file__),'palette_sha256':h(args.palette),'ordinary_palette_entries_only':list(range(240)),'assets':[]}
|
||||
n=int(ROOT.name.rsplit('_',1)[1]);is_boss='_boss_' in ROOT.name
|
||||
if is_boss:
|
||||
report['assets'].append(canvases(ROOT/'art/unit-generated-v1.png','unit',(128,64) if n==1 else (48,80),pal))
|
||||
report['assets'].append(canvases(ROOT/'art/projectile-generated-v1.png','projectile',(8,16),pal))
|
||||
cues=[('warning',.5,250+n*30),('shot',.16,100),('death',1.05,80)]
|
||||
else:
|
||||
raw=ROOT/'art/terrain-generated-v1.png';src=Image.open(raw).convert('RGBA');hd=src.resize((1280,2048),Image.Resampling.LANCZOS);sd=small(hd,(320,512),pal)
|
||||
for reverse in [False,True]:
|
||||
hi=ImageOps.flip(hd) if reverse else hd;lo=ImageOps.flip(sd) if reverse else sd
|
||||
for y in range(16):
|
||||
for x in range(10):
|
||||
index=(160 if reverse else 0)+y*10+x
|
||||
hi.crop((x*128,y*128,(x+1)*128,(y+1)*128)).save(ROOT/'art'/f'tile_{index:03}-hd.png')
|
||||
lo.crop((x*32,y*32,(x+1)*32,(y+1)*32)).save(ROOT/'art'/f'tile_{index:03}-sd.png')
|
||||
# Reflected cycle joins equal boundary rows at full and logical resolution.
|
||||
for im in [hd,sd]:assert im.crop((0,im.height-1,im.width,im.height)).tobytes()==ImageOps.flip(im).crop((0,0,im.width,1)).tobytes()
|
||||
hd.save(ROOT/'art/terrain-preview-hd.png');sd.save(ROOT/'art/terrain-preview-sd.png')
|
||||
bg=ImageOps.fit(src,(1280,800),Image.Resampling.LANCZOS,centering=(.5,.35));bg.save(ROOT/'art/briefing-hd.png');small(bg,(320,200),pal).save(ROOT/'art/briefing-sd.png')
|
||||
report['assets'].append({'source':'art/terrain-generated-v1.png','sha256':h(raw),'logical_map':[320,512],'hd_map':[1280,2048],'reflected_cycle':True,'tiles':320,'briefing':'same generated map cropped with 8:5 viewport, no separate generation','source_not_modified':True})
|
||||
cues=[('approach',.6,164+n*30),('objective',.36,392),('boss_alarm',.75,275),('victory',.95,262)]
|
||||
(ROOT/'audio').mkdir(exist_ok=True);report['audio']=[synth(*c) for c in cues]
|
||||
report['final_art']=[{'file':str(p.relative_to(ROOT)),'sha256':h(p),'size':Image.open(p).size} for p in sorted((ROOT/'art').glob('*.png')) if 'generated' not in p.name]
|
||||
(ROOT/'asset-report.json').write_text(json.dumps(report,indent=2)+'\n');print(ROOT.name,'assets',len(report['final_art']),'sounds',len(cues))
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_1:projectile",
|
||||
"name": "Gatebreaker naval missile",
|
||||
"damage": 3,
|
||||
"speed": 4,
|
||||
"cooldown": 72,
|
||||
"behavior": "straight",
|
||||
"target": "all",
|
||||
"art": {
|
||||
"png": "art/projectile-sd.png",
|
||||
"hd_png": "art/projectile-hd.png",
|
||||
"logical_width": 8,
|
||||
"logical_height": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"enemies": [
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_1:unit",
|
||||
"name": "Gatebreaker",
|
||||
"health": 320,
|
||||
"speed": 0,
|
||||
"reward": 12000,
|
||||
"ground": true,
|
||||
"boss": true,
|
||||
"cooldown": 72,
|
||||
"art": {
|
||||
"png": "art/unit-sd.png",
|
||||
"hd_png": "art/unit-hd.png",
|
||||
"logical_width": 128,
|
||||
"logical_height": 64
|
||||
},
|
||||
"script": "scripts/ai.rhai"
|
||||
}
|
||||
],
|
||||
"sounds": [
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_1:warning",
|
||||
"name": "Gatebreaker warning",
|
||||
"file": "audio/warning.wav",
|
||||
"volume": 76,
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_1:shot",
|
||||
"name": "Gatebreaker shot",
|
||||
"file": "audio/shot.wav",
|
||||
"volume": 88,
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_1:death",
|
||||
"name": "Gatebreaker death",
|
||||
"file": "audio/death.wav",
|
||||
"volume": 110,
|
||||
"priority": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"tool": "builtin image_gen",
|
||||
"utc": "2026-10-11T08:11:23.928716+00:00",
|
||||
"original_builtin": "/home/quintine/.codex/generated_images/01a1245d-6057-70c2-a44c-c7a159ca21cb/exec-982128c3-31c6-4ec2-9a82-205319ceea4c.png",
|
||||
"raw": "art/unit-generated-v1.png",
|
||||
"raw_sha256": "62df3558a79431a86b4ada4fe393b003506aaf912ae74415a33411724b688f65",
|
||||
"prompt": "prompts/unit-v1.txt",
|
||||
"prompt_sha256": "cdb6cac4be7536c0cabc52009f5e13b24aee4082244ec11cef32c221a9bfb466",
|
||||
"lease": "f19cc23c-c36a-47bc-bc5f-42d9f151cb86",
|
||||
"job": "stage2-boss1-unit-v1",
|
||||
"reference_role": "authentic original style only",
|
||||
"raw_preserved": true,
|
||||
"projectile": {
|
||||
"tool": "builtin image_gen",
|
||||
"original_builtin": "/home/quintine/.codex/generated_images/01a1245d-6057-70c2-a44c-c7a159ca21cb/exec-e4223f76-7281-4bbd-954a-66f3026d93e0.png",
|
||||
"raw": "art/projectile-generated-v1.png",
|
||||
"raw_sha256": "9cc6e7e6cac60f045050bdefe1c84f5fc7185bcb585b87b7a7df5b2206a38ebb",
|
||||
"prompt": "prompts/projectile-v1.txt",
|
||||
"lease": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"job": "stage2-boss-projectile-v1",
|
||||
"reuse": "Same newly authored micro-missile art independently packaged with each boss; no package dependency.",
|
||||
"raw_preserved": true,
|
||||
"prompt_sha256": "9b3fc62e11770453f8799159e739e5a9d682e5d5fcc5dab5753012555b2f6f93"
|
||||
},
|
||||
"exact_gate_history": [
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:10:11+00:00",
|
||||
"event": "acquired",
|
||||
"lease_id": "f19cc23c-c36a-47bc-bc5f-42d9f151cb86",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss1-unit-v1",
|
||||
"expires_at": "2026-10-11T08:40:11+00:00"
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:10:11+00:00",
|
||||
"event": "started",
|
||||
"lease_id": "f19cc23c-c36a-47bc-bc5f-42d9f151cb86",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss1-unit-v1",
|
||||
"note": "Owner is submitting the built-in imagegen request; this slot requires an explicit report or release."
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:10:41+00:00",
|
||||
"event": "report",
|
||||
"lease_id": "f19cc23c-c36a-47bc-bc5f-42d9f151cb86",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss1-unit-v1",
|
||||
"outcome": "success"
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:14:24+00:00",
|
||||
"event": "acquired",
|
||||
"lease_id": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss-projectile-v1",
|
||||
"expires_at": "2026-10-11T08:44:24+00:00"
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:14:24+00:00",
|
||||
"event": "started",
|
||||
"lease_id": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss-projectile-v1",
|
||||
"note": "Owner is submitting the built-in imagegen request; this slot requires an explicit report or release."
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:14:46+00:00",
|
||||
"event": "report",
|
||||
"lease_id": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss-projectile-v1",
|
||||
"outcome": "success"
|
||||
}
|
||||
],
|
||||
"authentic_style_reference_pins": {
|
||||
"role": "authentic original palette/material style; new designs do not copy target geometry",
|
||||
"sources": [
|
||||
{
|
||||
"asset_id": "10139",
|
||||
"catalog_path": "sprites/TARGT2G1_PIC",
|
||||
"original_payload_sha256": "16647e06c0121136adaede8ba6636f4eab4af33bb15be83c673b6201114412e3"
|
||||
},
|
||||
{
|
||||
"asset_id": "10344",
|
||||
"catalog_path": "sprites/SHIP39G1_PIC",
|
||||
"original_payload_sha256": "7cb09e925ea7786b331d576c360aad09b5e18f35d0258ec78ab5ef2e50c2590e"
|
||||
},
|
||||
{
|
||||
"asset_id": "400d5",
|
||||
"catalog_path": "misc/unnamed.3295",
|
||||
"original_payload_sha256": "52b3724f81fe97fc2884f45440e711776e253de93a26da2bcf1c9fe06241fd2f"
|
||||
},
|
||||
{
|
||||
"asset_id": "4011f",
|
||||
"catalog_path": "misc/unnamed.3369",
|
||||
"original_payload_sha256": "ec08b9e0761eb2b408df6b5c4c71019b1f277478237cf336412d01672a7158ee"
|
||||
}
|
||||
],
|
||||
"palette_sha256": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"external_contact_sheet_sha256": "d92f7d68008a73d626dcc9125920b7b329d029ca87a01d28bc4005f80c4f9174",
|
||||
"decoded_reference_images_excluded_from_package": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "iron_coast_stage2_boss_1"
|
||||
name = "Iron Coast II: Gatebreaker"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
content = "content.json"
|
||||
scripts = ["scripts/ai.rhai"]
|
||||
description = "A standalone Gatebreaker boss with telegraphed attacks, vulnerable phases, genuine SD/HD artwork and synthesized naval effects."
|
||||
@@ -0,0 +1,3 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: tiny top-down missile sprite, authored mod projectile.
|
||||
Primary request: one short naval micro-missile for the Iron Coast II bosses. Centered narrow dark gunmetal capsule, pale gray nose pointing DOWN, two little triangular tail fins at TOP and a very small orange exhaust at the top tip. Orthographic overhead complete silhouette, upper-left lighting, crisp old Raptor military sprite medium with restrained HD panels. Strong simple silhouette legible at8x16 logical pixels. Canvas640x1280 exactly1:2 aspect, at least15% transparent margin on all sides, genuine alpha outside missile/exhaust. No background, water, shadows beyond missile, explosion, long flame streak, extra bullets, lettering or watermark.
|
||||
@@ -0,0 +1,4 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: complete top-down transparent boss sprite for a vertical military shoot-em-up. New authored mod artwork, not a remaster of the reference.
|
||||
Primary request: Gatebreaker, a broad low armored shore-command platform of Iron Coast II: Black Tide. The authentic Raptor reference is style only: muted gunmetal, old industrial bronze, deliberate small-scale silhouettes and upper-left light. Create a distinct horizontal armored structure, two heavy square cannon towers at the outer sides, low central octagonal command bunker, paired recessed amber charge lamps. Its ground footprint is a wide 2:1 rectangle, no aircraft wings, no perspective horizon. Flat orthographic overhead view; front edge faces DOWN toward player. Complete body and cast contact shadows contained well within transparent margins, centered and symmetrical enough to read at 128x64 logical pixels. HD painted sprite with crisp contour and restrained mechanical detail, no noisy gravel, no red explosion, no text, no extra props, no checkerboard. Black Tide maritime midnight steel-blue palette with warm warning accents, natural gray armor.
|
||||
Canvas: 1280x640, exact2:1 aspect. Genuine transparent alpha everywhere outside the whole platform. All barrels, side armor and tiny lamp pixels stay inside full canvas with at least5% transparent margin. Do not mimic or copy the exact source structures; match the restrained original military sprite medium only.
|
||||
@@ -0,0 +1,53 @@
|
||||
// Per-life cursors are serializable and deterministic. No engine-specific boss behavior.
|
||||
fn alive(event,id) {
|
||||
for unit in event.entities { if unit.entity==id && unit.health>0 { return true; } }
|
||||
false
|
||||
}
|
||||
fn on_spawn(event,state,random) {
|
||||
if !state.contains("ages") { state.ages=#{}; state.phases=#{}; state.directions=#{}; }
|
||||
if !state.contains("maximums") {state.maximums=#{};}
|
||||
// Remove cursors from a prior replay or an explicitly despawned life.
|
||||
for key in state.ages.keys() {
|
||||
let present=false;for unit in event.entities {if unit.entity.to_string()==key && unit.health>0 {present=true;}}
|
||||
if !present { state.ages.remove(key);state.phases.remove(key);state.directions.remove(key);state.maximums.remove(key); }
|
||||
}
|
||||
let key=event.entity.to_string();state.ages[key]=0;state.phases[key]=1;state.directions[key]=1;state.maximums[key]=1;
|
||||
for unit in event.entities {if unit.entity==event.entity && unit.health>0 {state.maximums[key]=unit.health;}}
|
||||
if !alive(event,event.entity) { return #{commands:[],state:state}; }
|
||||
#{commands:[#{op:"velocity",entity:event.entity,vx:0,vy:2}],state:state}
|
||||
}
|
||||
fn on_tick(event,state,random) {
|
||||
if event.health<=0 || !alive(event,event.entity) { return #{commands:[],state:state}; }
|
||||
let key=event.entity.to_string();
|
||||
if !state.contains("ages") || !state.ages.contains(key) { return #{commands:[],state:state}; }
|
||||
if event.y<20 { return #{commands:[#{op:"velocity",entity:event.entity,vx:0,vy:2}],state:state}; }
|
||||
// Older saved cursors lack maximums; initialize at their current living health.
|
||||
if !state.contains("maximums") {state.maximums=#{};}
|
||||
if !state.maximums.contains(key) {state.maximums[key]=event.health;}
|
||||
let phase=if event.health*2<=state.maximums[key] {2} else {1};
|
||||
if state.phases[key]!=phase {state.phases[key]=phase;state.ages[key]=0;}
|
||||
let age=state.ages[key]+1;state.ages[key]=age;
|
||||
let commands=[];
|
||||
commands.push(#{op:"move",entity:event.entity,x:96,y:20});commands.push(#{op:"velocity",entity:event.entity,vx:0,vy:0});
|
||||
let cycle=if phase==1 {84} else {66};
|
||||
if age%cycle==cycle-12 {commands.push(#{op:"play_sound",sound:"iron_coast_stage2_boss_1:warning"});}
|
||||
if age%cycle==0 {
|
||||
if phase==1 {
|
||||
commands.push(#{op:"projectile",weapon:"iron_coast_stage2_boss_1:projectile",x:120,y:72,vx:0,vy:4,hostile:true});
|
||||
commands.push(#{op:"projectile",weapon:"iron_coast_stage2_boss_1:projectile",x:200,y:72,vx:0,vy:4,hostile:true});
|
||||
} else {
|
||||
for lane in [-2,-1,1,2] {
|
||||
commands.push(#{op:"projectile",weapon:"iron_coast_stage2_boss_1:projectile",x:160+lane*18,y:72,vx:lane,vy:4,hostile:true});
|
||||
}
|
||||
}
|
||||
commands.push(#{op:"play_sound",sound:"iron_coast_stage2_boss_1:shot"});
|
||||
}
|
||||
#{commands:commands,state:state}
|
||||
}
|
||||
fn on_death(event,state,random) {
|
||||
let key=event.entity.to_string();
|
||||
if alive(event,event.entity) || !state.contains("ages") || !state.ages.contains(key) {return #{commands:[],state:state};}
|
||||
state.ages.remove(key);state.phases.remove(key);state.directions.remove(key);state.maximums.remove(key);
|
||||
let value=if event.objectives.contains("iron_coast_stage2_boss_1:defeated") {event.objectives["iron_coast_stage2_boss_1:defeated"]+1} else {1};
|
||||
#{commands:[#{op:"objective",key:"iron_coast_stage2_boss_1:defeated",value:value},#{op:"effect",x:event.x+64,y:event.y+32},#{op:"play_sound",sound:"iron_coast_stage2_boss_1:death"}],state:state}
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.9.1",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iron-coast-stage2-early-validation"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"raptor-replica",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "raptor-replica"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"image",
|
||||
"rhai",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0334639972c0ea5a3fd366aa36116754a11431b619fec3ed559b3f73bcbcebf5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"no-std-compat",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd3a7535e50bf36857e7be7bec276d334e8c2dfa469c2201226fd01638ea5ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6a4b9ba8738cb4a4f399d37e266becfd475e75eb73425b87a05a2f2039ba63e"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.7",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "iron-coast-stage2-early-validation"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
[dependencies]
|
||||
raptor-replica = { path = "../../..", default-features = false }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
[[bin]]
|
||||
name = "validate-stage2-early"
|
||||
path = "validate.rs"
|
||||
@@ -0,0 +1,646 @@
|
||||
//! Actual API/World integration; all captured original art stays outside packages.
|
||||
use anyhow::{Result, ensure};
|
||||
use raptor_replica::{
|
||||
assets::{Catalog, Sprite},
|
||||
content::{ContentId, ContentPack, Registry, art_sprite_variant},
|
||||
modding::{self, ActiveMods, Command},
|
||||
persistence::Pilot,
|
||||
render::{self, Canvas},
|
||||
simulation::{Input, Outcome, World},
|
||||
sound,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
fn id(s: &str) -> ContentId {
|
||||
s.into()
|
||||
}
|
||||
fn build(
|
||||
base: &Registry,
|
||||
mods: &Path,
|
||||
enabled: &[String],
|
||||
catalog: Option<&Catalog>,
|
||||
) -> Result<ActiveMods> {
|
||||
ActiveMods::build(base, &modding::discover(mods), enabled, catalog)
|
||||
}
|
||||
fn input(world: &World, required: &str, boss: &str) -> Input {
|
||||
let target = world
|
||||
.enemies
|
||||
.iter()
|
||||
.filter(|e| e.health > 0 && e.y >= 0 && e.y < 140)
|
||||
.find(|e| e.definition.0 == required)
|
||||
.or_else(|| {
|
||||
world
|
||||
.enemies
|
||||
.iter()
|
||||
.find(|e| e.definition.0 == boss && e.health > 0)
|
||||
})
|
||||
.or_else(|| {
|
||||
world
|
||||
.enemies
|
||||
.iter()
|
||||
.filter(|e| e.health > 0 && e.y >= 0 && e.y < 130)
|
||||
.min_by_key(|e| (e.x - world.player.x).abs())
|
||||
});
|
||||
let desired = target.map(|e| e.x + e.width / 2 - 16).unwrap_or(144);
|
||||
Input {
|
||||
fire: true,
|
||||
absolute: Some([
|
||||
world.player.x + (desired - world.player.x).clamp(-4, 4),
|
||||
160,
|
||||
]),
|
||||
..Input::default()
|
||||
}
|
||||
}
|
||||
fn ppm(path: &Path, im: &Sprite, palette: &[[u8; 3]; 256]) -> Result<()> {
|
||||
let mut bytes = format!("P6\n{} {}\n255\n", im.width, im.height).into_bytes();
|
||||
for c in &im.pixels {
|
||||
bytes.extend_from_slice(if *c < 256 {
|
||||
&palette[*c as usize]
|
||||
} else {
|
||||
&[18, 22, 28]
|
||||
});
|
||||
}
|
||||
fs::write(path, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
fn capture(
|
||||
catalog: &Catalog,
|
||||
active: &ActiveMods,
|
||||
world: &World,
|
||||
out: &Path,
|
||||
label: &str,
|
||||
) -> Result<()> {
|
||||
fs::create_dir_all(out)?;
|
||||
let palette = catalog.palette()?;
|
||||
let mission = &active.registry.missions[&world.mission];
|
||||
let owner = active
|
||||
.registry
|
||||
.owners
|
||||
.get(&world.mission)
|
||||
.map(String::as_str)
|
||||
.unwrap_or("base");
|
||||
for hd in [false, true] {
|
||||
let scale = if hd { 4 } else { 1 };
|
||||
let mut c = Canvas::new(scale);
|
||||
c.clear(0);
|
||||
if !mission.tiles.is_empty() {
|
||||
let row = (world.tick as usize / 16)
|
||||
% mission.layout.len().div_ceil(mission.columns as usize);
|
||||
for gy in 0..7 {
|
||||
for gx in 0..mission.columns as usize {
|
||||
let k = ((row + gy) * mission.columns as usize + gx) % mission.layout.len();
|
||||
let tile = &mission.tiles[mission.layout[k] as usize];
|
||||
let s = art_sprite_variant(catalog, &active.registry, owner, &tile.art, 0, hd)?;
|
||||
let s = raptor_replica::terrain_hd::logical_sprite(&s, 32 * scale, 32 * scale)?;
|
||||
c.blit(&s, (gx * 32 * scale) as i32, (gy * 32 * scale) as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in &world.enemies {
|
||||
let def = &active.registry.enemies[&e.definition];
|
||||
if let Some(art) = &def.art {
|
||||
let owner = active
|
||||
.registry
|
||||
.owners
|
||||
.get(&e.definition)
|
||||
.map(String::as_str)
|
||||
.unwrap_or("base");
|
||||
let s = art_sprite_variant(catalog, &active.registry, owner, art, 0, hd)?;
|
||||
let s = raptor_replica::terrain_hd::logical_sprite(
|
||||
&s,
|
||||
art.logical_width as usize * scale,
|
||||
art.logical_height as usize * scale,
|
||||
)?;
|
||||
c.blit(&s, e.x * scale as i32, e.y * scale as i32);
|
||||
}
|
||||
}
|
||||
for p in &world.projectiles {
|
||||
if let Some(def) = active.registry.weapons.get(&p.weapon) {
|
||||
if let Some(art) = &def.art {
|
||||
let owner = active
|
||||
.registry
|
||||
.owners
|
||||
.get(&p.weapon)
|
||||
.map(String::as_str)
|
||||
.unwrap_or("base");
|
||||
let s = art_sprite_variant(catalog, &active.registry, owner, art, 0, hd)?;
|
||||
let s = raptor_replica::terrain_hd::logical_sprite(
|
||||
&s,
|
||||
art.logical_width as usize * scale,
|
||||
art.logical_height as usize * scale,
|
||||
)?;
|
||||
c.blit(&s, p.x * scale as i32, p.y * scale as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
let player =
|
||||
catalog.sprite(catalog.id("LPLAYER_PIC")? + world.player.bank.clamp(0, 6) as u32)?;
|
||||
let player = render::nearest(&player, scale);
|
||||
c.blit(
|
||||
&player,
|
||||
world.player.x * scale as i32,
|
||||
world.player.y * scale as i32,
|
||||
);
|
||||
ppm(
|
||||
&out.join(format!("{label}-{}.ppm", if hd { "hd4" } else { "sd1" })),
|
||||
&c.image,
|
||||
&palette,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn bosses(base: &Registry, mods: &Path, catalog: &Catalog) -> Result<Value> {
|
||||
let mut rows = vec![];
|
||||
for n in [1, 2] {
|
||||
let owner = format!("iron_coast_stage2_boss_{n}");
|
||||
let unit = format!("{owner}:unit");
|
||||
let objective = format!("{owner}:defeated");
|
||||
let mut active = build(base, mods, &[owner.clone()], Some(catalog))?;
|
||||
let def = &active.registry.enemies[&id(&unit)];
|
||||
ensure!(def.ground && def.boss && def.legacy.is_none());
|
||||
active.registry.apply(serde_json::from_value::<ContentPack>(json!({"missions":[{"id":"base:stage2_boss_fixture","name":"Boss integration fixture","duration":6000,"auto_complete":false,"spawns":[{"tick":0,"enemy":unit,"x":96,"y":-80}]}]}))?,"base")?;
|
||||
let mut p = Pilot::default();
|
||||
p.mission = id("base:stage2_boss_fixture");
|
||||
p.inventory.insert(id("base:energy"), 75);
|
||||
let mut w = World::new(&p.mission, &p, &active.registry)?;
|
||||
let mut cues = BTreeSet::new();
|
||||
let mut minimum_health = i32::MAX;
|
||||
for _ in 0..2200 {
|
||||
w.step(input(&w, "", &unit), &active.registry, &mut active.scripts)?;
|
||||
for s in &w.mod_sounds {
|
||||
cues.insert(s.0.clone());
|
||||
}
|
||||
for e in &w.enemies {
|
||||
minimum_health = minimum_health.min(e.health);
|
||||
}
|
||||
if w.objectives.get(&objective) == Some(&1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ensure!(
|
||||
w.objectives.get(&objective) == Some(&1),
|
||||
"boss{n} not killed {:?} tick{} health{}",
|
||||
w.outcome,
|
||||
w.tick,
|
||||
w.player.shield
|
||||
);
|
||||
ensure!(w.player.shield > 0);
|
||||
ensure!(cues.contains(&format!("{owner}:death")));
|
||||
let snapshot = active.scripts.checkpoints();
|
||||
let at = w.digest();
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(w.objectives[&objective] == 1);
|
||||
rows.push(json!({"boss":unit,"tick":w.tick,"shield":w.player.shield,"minimum_observed_health":minimum_health,"cues":cues,"digest_before_extra_step":at,"serialized_script_state_bytes":serde_json::to_vec(&snapshot.0)?.len()}));
|
||||
// Duplicate dead callback cannot pay objective again; prior life cursor removed.
|
||||
active.scripts.begin_tick();
|
||||
let commands = active.scripts.invoke(
|
||||
&owner,
|
||||
"scripts/ai.rhai",
|
||||
"on_death",
|
||||
&json!({"entity":1,"x":96,"y":20,"entities":[],"objectives":{objective.clone():1}}),
|
||||
0,
|
||||
)?;
|
||||
ensure!(commands.is_empty());
|
||||
// A queued early-killed spawn must register exactly its death, never emit movement/sound.
|
||||
active.scripts.begin_tick();
|
||||
let event = json!({"entity":900,"x":96,"y":20,"entities":[],"objectives":{}});
|
||||
ensure!(
|
||||
active
|
||||
.scripts
|
||||
.invoke(&owner, "scripts/ai.rhai", "on_spawn", &event, 0)?
|
||||
.is_empty()
|
||||
);
|
||||
active.scripts.begin_tick();
|
||||
let commands = active
|
||||
.scripts
|
||||
.invoke(&owner, "scripts/ai.rhai", "on_death", &event, 0)?;
|
||||
ensure!(
|
||||
commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::Objective{key,value}if key==&objective&&*value==1))
|
||||
);
|
||||
}
|
||||
Ok(json!({"standalone_real_world_bosses":rows,"early_spawn_death_and_duplicate_death":true}))
|
||||
}
|
||||
fn missions(base: &Registry, mods: &Path, catalog: &Catalog, output: &Path) -> Result<Value> {
|
||||
let mut report = vec![];
|
||||
for n in [1, 2] {
|
||||
let owner = format!("iron_coast_stage2_mission_{n}");
|
||||
let boss = format!("iron_coast_stage2_boss_{n}");
|
||||
let required = format!(
|
||||
"iron_coast_stage2_{}",
|
||||
if n == 1 { "siege" } else { "skimmer" }
|
||||
);
|
||||
let mission = format!("{owner}:mission_0{n}");
|
||||
let enabled = vec![
|
||||
"iron_coast_stage2_music".into(),
|
||||
"iron_coast_stage2_interceptor".into(),
|
||||
required.clone(),
|
||||
boss.clone(),
|
||||
owner.clone(),
|
||||
];
|
||||
let count = if n == 1 { 3 } else { 4 };
|
||||
let mut runs = vec![];
|
||||
for case in 0..7 {
|
||||
let difficulty = match case {
|
||||
4 => 0,
|
||||
5 => 1,
|
||||
6 => 3,
|
||||
_ => 2,
|
||||
};
|
||||
let full = case == 2;
|
||||
let purchased = case == 3;
|
||||
let mut case_enabled = enabled.clone();
|
||||
if purchased {
|
||||
case_enabled.extend([
|
||||
"iron_coast_railburst".into(),
|
||||
"iron_coast_depth_charge".into(),
|
||||
]);
|
||||
}
|
||||
let mut active = build(base, mods, &case_enabled, Some(catalog))?;
|
||||
let mut p = Pilot::default();
|
||||
p.mission = id(&mission);
|
||||
p.difficulty = difficulty;
|
||||
let initial_shield = if case == 1 { 100 } else { 75 };
|
||||
p.inventory.insert(id("base:energy"), initial_shield);
|
||||
p.credits = if case == 1 { 0 } else { 10000 };
|
||||
if purchased {
|
||||
for weapon in [
|
||||
"iron_coast_railburst:equipment",
|
||||
"iron_coast_depth_charge:equipment",
|
||||
] {
|
||||
p.credits += active.registry.items[&id(weapon)].price;
|
||||
p.buy(&id(weapon), &active.registry)?;
|
||||
}
|
||||
p.selected = Some(id(if n == 1 {
|
||||
"iron_coast_railburst:equipment"
|
||||
} else {
|
||||
"iron_coast_depth_charge:equipment"
|
||||
}));
|
||||
}
|
||||
let initial_credits = p.credits;
|
||||
if full {
|
||||
for (id, item) in &active.registry.items {
|
||||
if p.inventory_slots() >= 20 {
|
||||
break;
|
||||
}
|
||||
if item.legacy == Some(16) || item.legacy == Some(17) || item.consumable {
|
||||
continue;
|
||||
}
|
||||
p.inventory.insert(id.clone(), item.initial_count);
|
||||
}
|
||||
ensure!(p.inventory_slots() == 20);
|
||||
}
|
||||
let inventory = p.inventory.clone();
|
||||
eprintln!(
|
||||
"mission{n}: case{case} difficulty{difficulty} full{full} purchased{purchased} starting"
|
||||
);
|
||||
let mut w = World::new(&p.mission, &p, &active.registry)?;
|
||||
let mut cues = BTreeSet::new();
|
||||
let mut native = BTreeMap::new();
|
||||
let mut moving = BTreeSet::new();
|
||||
let mut checkpoint = None;
|
||||
let mut captures = BTreeSet::new();
|
||||
for _ in 0..5000 {
|
||||
w.step(
|
||||
input(&w, &(required.clone() + ":unit"), &(boss.clone() + ":unit")),
|
||||
&active.registry,
|
||||
&mut active.scripts,
|
||||
)?;
|
||||
for s in &w.mod_sounds {
|
||||
cues.insert(s.0.clone());
|
||||
}
|
||||
for e in &w.enemies {
|
||||
if e.definition.0.starts_with("base:enemy_") {
|
||||
ensure!(active.registry.enemies[&e.definition].legacy.is_some());
|
||||
let old = native.entry(e.entity).or_insert((e.x, e.y));
|
||||
if *old != (e.x, e.y) {
|
||||
moving.insert(e.definition.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if case == 0 && [500, 1600, 2000, 2350, 2500].contains(&w.tick) {
|
||||
capture(
|
||||
catalog,
|
||||
&active,
|
||||
&w,
|
||||
output,
|
||||
&format!("mission{n}-tick{}", w.tick),
|
||||
)?;
|
||||
captures.insert(w.tick);
|
||||
}
|
||||
if w.tick == 1000 {
|
||||
checkpoint = Some((w.clone(), active.scripts.checkpoints()));
|
||||
}
|
||||
if w.outcome != Outcome::Playing {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ensure!(
|
||||
w.outcome == Outcome::Complete,
|
||||
"mission{n} full{full} {:?} tick{} shield{} objectives{:?}",
|
||||
w.outcome,
|
||||
w.tick,
|
||||
w.player.shield,
|
||||
w.objectives
|
||||
);
|
||||
ensure!(
|
||||
w.objectives[&(required.clone() + ":defeated")] == count
|
||||
&& w.objectives[&(boss.clone() + ":defeated")] == 1
|
||||
);
|
||||
ensure!(w.objectives[&(owner.clone() + ":debrief")] == 1);
|
||||
ensure!(w.player.shield > 0);
|
||||
ensure!(moving.len() == 3, "all native classes observed moving");
|
||||
if full {
|
||||
for (item, count) in &inventory {
|
||||
if item.0 != "base:energy" && item.0 != "base:super_shield" {
|
||||
ensure!(
|
||||
w.player.inventory.get(item) == Some(count),
|
||||
"inventory preservation:{item}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if purchased {
|
||||
ensure!(
|
||||
w.player
|
||||
.inventory
|
||||
.get(&id("iron_coast_railburst:equipment"))
|
||||
== Some(&1)
|
||||
&& w.player
|
||||
.inventory
|
||||
.get(&id("iron_coast_depth_charge:equipment"))
|
||||
== Some(&1),
|
||||
"owned gear preserved"
|
||||
);
|
||||
}
|
||||
let final_tick = w.tick;
|
||||
let final_digest = w.digest();
|
||||
let final_scripts = serde_json::to_value(active.scripts.checkpoints())?;
|
||||
let earned = w.player.credits;
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(w.player.credits == earned, "reward repeated");
|
||||
let (mut resumed, states) = checkpoint.unwrap();
|
||||
let mut resumed_active = build(base, mods, &case_enabled, Some(catalog))?;
|
||||
resumed_active
|
||||
.scripts
|
||||
.restore(serde_json::from_slice(&serde_json::to_vec(&states)?)?);
|
||||
while resumed.tick < final_tick {
|
||||
resumed.step(
|
||||
input(
|
||||
&resumed,
|
||||
&(required.clone() + ":unit"),
|
||||
&(boss.clone() + ":unit"),
|
||||
),
|
||||
&resumed_active.registry,
|
||||
&mut resumed_active.scripts,
|
||||
)?;
|
||||
}
|
||||
ensure!(
|
||||
resumed.digest() == final_digest
|
||||
&& serde_json::to_value(resumed_active.scripts.checkpoints())? == final_scripts,
|
||||
"state/RNG continuation deterministic"
|
||||
);
|
||||
// Restart reuses same script state but all mission goals/cursors must reset.
|
||||
let mut restart = World::new(&p.mission, &p, &active.registry)?;
|
||||
restart.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(
|
||||
restart.objectives[&(required.clone() + ":defeated")] == 0
|
||||
&& restart.objectives[&(boss.clone() + ":defeated")] == 0
|
||||
&& restart.objectives[&(owner.clone() + ":complete")] == 0
|
||||
);
|
||||
runs.push(json!({"difficulty":difficulty,"full20_inventory":full,"purchased_new_gear":purchased,"initial_shield":initial_shield,"initial_credits":initial_credits,"tick":final_tick,"shield":w.player.shield,"credits":earned,"credit_gain":earned-initial_credits,"objectives":w.objectives,"native_classes_moving":moving,"cues":cues,"captures":captures,"digest":final_digest,"serialized_state_rng_round_trip_identical":true,"replay_reset":true,"reward_once":true}));
|
||||
fs::write(
|
||||
output.join(format!("mission{n}-progress.json")),
|
||||
serde_json::to_vec_pretty(&runs)?,
|
||||
)?;
|
||||
eprintln!(
|
||||
"mission{n}: case{case} difficulty{difficulty} passed tick{final_tick} shield{} credits{earned}",
|
||||
w.player.shield
|
||||
);
|
||||
}
|
||||
// Authentic objective callback gating; no elapsed timeout can complete.
|
||||
let mut active = build(base, mods, &enabled, Some(catalog))?;
|
||||
let mut p = Pilot::default();
|
||||
p.mission = id(&mission);
|
||||
let mut w = World::new(&p.mission, &p, &active.registry)?;
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
w.objectives.insert(boss.clone() + ":defeated", 1);
|
||||
w.objectives
|
||||
.insert(required.clone() + ":defeated", count - 1);
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(w.outcome == Outcome::Playing);
|
||||
w.objectives.insert(required.clone() + ":defeated", count);
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(
|
||||
w.outcome == Outcome::Complete
|
||||
&& w.player.credits == if n == 1 { 24000 } else { 32000 }
|
||||
);
|
||||
// This persistence-only fixture suppresses pilot death. It is separate
|
||||
// from every ordinary-loadout balance result above, which uses actual
|
||||
// purchased/registered health and unmodified gameplay damage.
|
||||
let mut late = World::new(&p.mission, &p, &active.registry)?;
|
||||
// Use the hardest difficulty for the persistence stress fixture.
|
||||
late.difficulty = 3;
|
||||
for _ in 0..5000 {
|
||||
// Normal original pickups can replace a shield value. Restore
|
||||
// this explicit fixture buffer each step, rather than treating
|
||||
// it as a persistent piece of gameplay equipment.
|
||||
late.player.shield = 10000;
|
||||
late.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
}
|
||||
ensure!(
|
||||
late.outcome == Outcome::Playing,
|
||||
"late persistence fixture: {:?} tick{} shield{} objectives{:?}",
|
||||
late.outcome,
|
||||
late.tick,
|
||||
late.player.shield,
|
||||
late.objectives
|
||||
);
|
||||
let required_alive = late
|
||||
.enemies
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
e.definition.0 == required.clone() + ":unit"
|
||||
&& e.health > 0
|
||||
&& e.y >= 0
|
||||
&& e.y < 140
|
||||
})
|
||||
.count();
|
||||
ensure!(
|
||||
required_alive == count as usize,
|
||||
"required convoys remain reachable"
|
||||
);
|
||||
ensure!(
|
||||
late.enemies
|
||||
.iter()
|
||||
.any(|e| { e.definition.0 == boss.clone() + ":unit" && e.health > 0 && e.y == 20 }),
|
||||
"boss must remain until actual death"
|
||||
);
|
||||
let definition = &active.registry.missions[&p.mission];
|
||||
let map_height = definition.layout.len() / definition.columns as usize * 32;
|
||||
ensure!(
|
||||
late.scroll as usize + 200 < map_height,
|
||||
"late fight has terrain coverage"
|
||||
);
|
||||
report.push(json!({"mission":mission,"enabled_packages":enabled,"required_deaths":count,"runs":runs,"missing_one_required_death_blocks_victory":true,"late_fight_fixture":{"tick":late.tick,"scroll":late.scroll,"reachable_required_units":required_alive,"boss_remains":true,"playing_beyond_declared_duration":true,"terrain_rows":map_height/32,"terrain_covered":true,"scope":"Persistence-only difficulty3 fixture restoring shield10000 before each step to suppress pilot death; ordinary balance runs above have no health/damage cheats."}}));
|
||||
}
|
||||
Ok(
|
||||
json!({"missions":report,"strategy":"Held Fire; mouseposition moves≤4 logicalpx/tick, y160, aim at required living convoy centers then boss then ordinary visible enemies. No damage/shield/credit cheats or loans.","capture_scope":"Actual PNG loader, native palette, World entity positions, Canvas.blit; simplified terrain row sampler/headless diagnostics, not liveSDL/Host capture."}),
|
||||
)
|
||||
}
|
||||
|
||||
fn difficulty_phases(base: &Registry, mods: &Path, catalog: &Catalog) -> Result<Value> {
|
||||
let mut rows = vec![];
|
||||
for n in [1, 2] {
|
||||
for difficulty in 0..4 {
|
||||
let owner = format!("iron_coast_stage2_boss_{n}");
|
||||
let unit = format!("{owner}:unit");
|
||||
let mut active = build(base, mods, &[owner.clone()], Some(catalog))?;
|
||||
active.registry.apply(serde_json::from_value::<ContentPack>(json!({"missions":[{"id":"base:stage2_phase_fixture","name":"Phase fixture","duration":6000,"auto_complete":false,"spawns":[{"tick":0,"enemy":unit,"x":96,"y":-80}]}]}))?,"base")?;
|
||||
let mut p = Pilot::default();
|
||||
p.mission = id("base:stage2_phase_fixture");
|
||||
p.difficulty = difficulty;
|
||||
let mut w = World::new(&p.mission, &p, &active.registry)?;
|
||||
for _ in 0..70 {
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
}
|
||||
let e = w.enemies.first().unwrap();
|
||||
let key = e.entity.to_string();
|
||||
let maximum = e.health;
|
||||
let statekey = format!("{owner}/scripts/ai.rhai");
|
||||
ensure!(
|
||||
active.scripts.states[&statekey]["maximums"][&key] == maximum
|
||||
&& active.scripts.states[&statekey]["phases"][&key] == 1,
|
||||
"difficulty{difficulty} must start phase1"
|
||||
);
|
||||
w.enemies[0].health = maximum / 2 + 1;
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(active.scripts.states[&statekey]["phases"][&key] == 1);
|
||||
w.enemies[0].health = maximum / 2;
|
||||
w.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(
|
||||
active.scripts.states[&statekey]["phases"][&key] == 2
|
||||
&& active.scripts.states[&statekey]["ages"][&key] == 1
|
||||
);
|
||||
rows.push(json!({"boss":unit,"difficulty":difficulty,"actual_spawn_max_health":maximum,"initial_phase":1,"half_health_phase":2,"phase_timer_reset":true}));
|
||||
}
|
||||
}
|
||||
Ok(
|
||||
json!({"all4_difficulties":rows,"fixture_scope":"Health boundary mutation tests only; ordinary boss/mission outcomes use unmodified damage gameplay."}),
|
||||
)
|
||||
}
|
||||
fn audio(
|
||||
base: &Registry,
|
||||
mods: &Path,
|
||||
catalog: &Catalog,
|
||||
enabled: &[String],
|
||||
output: &Path,
|
||||
) -> Result<Value> {
|
||||
let active = build(base, mods, enabled, Some(catalog))?;
|
||||
let mut bank = sound::SoundBank::default();
|
||||
let mut rows = vec![];
|
||||
fs::create_dir_all(output)?;
|
||||
for (id, _) in &active.registry.sounds {
|
||||
if !id.0.starts_with("iron_coast_stage2_") {
|
||||
continue;
|
||||
}
|
||||
let mut mixer = raptor_replica::audio::Mixer::new(22050, &catalog.named("GENMIDI_OP2")?)?;
|
||||
mixer.fx_volume = 127;
|
||||
mixer.music_volume = 0;
|
||||
mixer.start_capture(2)?;
|
||||
bank.queue_mod(&active.registry, &mut mixer, id)?;
|
||||
let mut samples = vec![0i16; 22050 * 4];
|
||||
mixer.mix(&mut samples);
|
||||
let peak = samples.iter().map(|v| i32::from(*v).abs()).max().unwrap();
|
||||
ensure!(peak > 0, "audible cue:{id}");
|
||||
let capture = mixer.take_capture().unwrap();
|
||||
fs::write(
|
||||
output.join(format!("{}.wav", id.0.replace(':', "-"))),
|
||||
sound::encode_capture(&capture)?,
|
||||
)?;
|
||||
let mut muted = raptor_replica::audio::Mixer::new(22050, &catalog.named("GENMIDI_OP2")?)?;
|
||||
muted.fx_volume = 0;
|
||||
muted.music_volume = 0;
|
||||
bank.queue_mod(&active.registry, &mut muted, id)?;
|
||||
let mut quiet = vec![0i16; 22050 * 4];
|
||||
muted.mix(&mut quiet);
|
||||
ensure!(quiet.iter().all(|v| *v == 0), "mute:{id}");
|
||||
rows.push(json!({"sound":id,"mixed_peak":peak,"fx_mute_zero":true}));
|
||||
}
|
||||
Ok(
|
||||
json!({"mixer":"Native SoundBank/Mixer with original GENMIDI,2s PCM capture each cue; no live audio device claim","sounds":rows}),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../..")
|
||||
.canonicalize()?;
|
||||
let catalog = Catalog::open(root.join("data"))?;
|
||||
let base = Registry::base(&catalog)?;
|
||||
let mods = root.join("mods");
|
||||
let output = root.join(".reference/iron-coast-stage2-authoring/runtime");
|
||||
fs::create_dir_all(&output)?;
|
||||
let only_boss = std::env::args().any(|s| s == "--boss-only");
|
||||
let mut report = json!({"bosses":bosses(&base,&mods,&catalog)?,"difficulty_phase_checks":difficulty_phases(&base,&mods,&catalog)?});
|
||||
if !only_boss {
|
||||
report["mission_acceptance"] = missions(&base, &mods, &catalog, &output)?;
|
||||
report["combined_audio"] = audio(
|
||||
&base,
|
||||
&mods,
|
||||
&catalog,
|
||||
&[
|
||||
"iron_coast_stage2_boss_1".into(),
|
||||
"iron_coast_stage2_boss_2".into(),
|
||||
"iron_coast_stage2_mission_1".into(),
|
||||
"iron_coast_stage2_mission_2".into(),
|
||||
"iron_coast_stage2_interceptor".into(),
|
||||
"iron_coast_stage2_siege".into(),
|
||||
"iron_coast_stage2_skimmer".into(),
|
||||
"iron_coast_stage2_music".into(),
|
||||
],
|
||||
&output.join("audio"),
|
||||
)?;
|
||||
}
|
||||
report["boss_audio"] = audio(
|
||||
&base,
|
||||
&mods,
|
||||
&catalog,
|
||||
&[
|
||||
"iron_coast_stage2_boss_1".into(),
|
||||
"iron_coast_stage2_boss_2".into(),
|
||||
],
|
||||
&output.join("audio"),
|
||||
)?;
|
||||
for n in [1, 2] {
|
||||
let owner = format!("iron_coast_stage2_boss_{n}");
|
||||
let active = build(&base, &mods, &[owner], Some(&catalog))?;
|
||||
for (id, s) in &active.registry.sounds {
|
||||
if id.0.starts_with("iron_coast_stage2_boss_") {
|
||||
let p = active
|
||||
.registry
|
||||
.asset_roots
|
||||
.get(id.0.split_once(':').unwrap().0)
|
||||
.unwrap()
|
||||
.join(&s.file);
|
||||
let decoded = sound::decode(&fs::read(p)?)?;
|
||||
ensure!(!decoded.data.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
fs::write(
|
||||
output.join(if only_boss {
|
||||
"boss-acceptance.json"
|
||||
} else {
|
||||
"acceptance.json"
|
||||
}),
|
||||
serde_json::to_vec_pretty(&report)?,
|
||||
)?;
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Iron Coast II: Undertow
|
||||
|
||||
A standalone boss content package for Black Tide. Enable this package directly
|
||||
in **Mods**, or enable its mission to select it as a dependency. It adds
|
||||
`iron_coast_stage2_boss_2:unit` and no campaign, overrides or equipment.
|
||||
|
||||
The surfaced/ground target has 400 health, awards 16,000 credits and retains
|
||||
a 48×80 logical collision canvas in SD and HD. It remains reachable until killed.
|
||||
Its two phases change at half of that life’s actual spawn health on every
|
||||
difficulty, with a new attack timer and twelve ticks of audible warning before
|
||||
every volley. Difficulty 0/1 uses the engine’s half-health boss rule; 2/3 uses
|
||||
the nominal health above. Cadence is 76 / 62 simulation ticks
|
||||
(first/second phase), at the original 70/3 Hz. Every projectile is hostile and
|
||||
leaves deliberate escape lanes; no phase is invulnerable.
|
||||
|
||||
Actual death increments `iron_coast_stage2_boss_2:defeated` once. It does not finish a mission.
|
||||
Per-life cursors are removed on death and pruned on spawn/replay; early kills
|
||||
before the queued spawn callback still register the real death.
|
||||
|
||||
Artwork is a genuine new builtin imagegen master, with full alpha and identical
|
||||
SD/HD canvas alignment. `build_assets.py` reproduces palette-aware SD art, 4× HD
|
||||
and deterministic PCM WAV effects from the tracked raw masters. Projectile art
|
||||
is the same newly authored micro-missile independently included with each boss.
|
||||
See [generation provenance](generation-provenance.json) and
|
||||
[asset recipe/report](asset-report.json). Original Raptor style references and
|
||||
local review captures are external.
|
||||
|
||||
From `RustRaptor/`:
|
||||
|
||||
```sh
|
||||
cargo run --locked --no-default-features -- --mods mods --validate-mods iron_coast_stage2_boss_2
|
||||
CARGO_TARGET_DIR="$PWD/target" cargo run --locked --release --manifest-path mods/iron_coast_stage2_boss_1/validation/Cargo.toml -- --boss-only
|
||||
python3 mods/iron_coast_stage2_boss_2/build_assets.py
|
||||
```
|
||||
|
||||
The real-catalog standalone kill, every-difficulty phase boundaries, early and
|
||||
duplicate hook checks and native Mixer/mute observations passed. Exact results
|
||||
and scopes are recorded in `acceptance-report.json`.
|
||||
|
||||
Asset recipes use Python 3 with Pillow. The game uses the shipped PNG and WAV files.
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"utc": "2026-10-11T08:51:02.199238+00:00",
|
||||
"status": "author_checked_review_ready",
|
||||
"real_catalog": true,
|
||||
"engine_source_hashes_at_report": {
|
||||
"src/assets.rs": "4cdf11a229f6ca4c6183fb545dba43e9a2323f839f7a095292045aa13a773ddc",
|
||||
"src/content.rs": "7f35df7100c450ba4bb30a0fdc90101c115b5ae42a7aac57829441c5b7761647",
|
||||
"src/modding.rs": "f37d67004392f5726121e58a995900cd7062016e3f060ea131b0df38995e2912",
|
||||
"src/simulation.rs": "0ec9f6da11ab82c1461868a7b147e4cdb14689fa7373bd0c24067d9014e9583a",
|
||||
"src/render.rs": "d4abdccbc3c1ee840ef8548bcff43080521937ef9f9f5ed69e810f417ef1e842",
|
||||
"src/sound.rs": "aff6779b85f9f4f4bc30a15ad75ec6fdd61d42895639700f4842870636e73c4b",
|
||||
"src/persistence.rs": "9a4adf1e33d3504db3d4d3e6091316cc7116fb35c89b2de8fbd4640d368aa0e2",
|
||||
"Cargo.lock": "aac0a22725250ab35828223f8d129e4d2a4e6113cf81463e59600da3a59ffdea"
|
||||
},
|
||||
"catalog_file_hashes": {
|
||||
"data/INDEX": "4ce78e5e638b6603114dd233ceaa41f386c0963ba4aa02ecf88ce0215b096681",
|
||||
"data/misc/PALETTE_DAT": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"data/misc/GENMIDI_OP2": "fdc93c076bf44207299e61ec1bbcfafca421454bfa99dd14fa4e5dcada1547b9"
|
||||
},
|
||||
"compiled_validation_binary_sha256": "98f43eef5943061a18a465e95e8089189be793b9f8cbac275b8ebe982953567b",
|
||||
"full_external_result_sha256": "63f97c1f8cab57452ee9235124551178fe2593c5e40f4a5ecd6e2aa9bf0cd19e",
|
||||
"cli": {
|
||||
"selected_packages": 10,
|
||||
"overrides": 0,
|
||||
"log_sha256": "c9401f44dc4fa438f75ccc00223e95f79cfa8bffcd1c8db5c106963f7b474822"
|
||||
},
|
||||
"visual_limits": "Author inspected native indexed SD1/HD4 Canvas/PNG-loader diagnostics, exact new source masters and SD/HD final art. Terrain in owned captures uses a simplified row sampler. Live SDL/Host captures and independent eight-stage progression are root-owned; no live device or whole-map rendering claim.",
|
||||
"package": "iron_coast_stage2_boss_2",
|
||||
"content_sha256": "e664d46f2cd4a20b7dff8030e6f8932fc5abeb321590e02b53d8a782687d3bda",
|
||||
"scripts": {
|
||||
"scripts/ai.rhai": "2e794a01968fc418b86f07c8704276fdfaf35443f07a0f1a1975e52c0f7adc7c"
|
||||
},
|
||||
"asset_report_sha256": "aae01dfc03415a0995e6512fce62af1a740093691f3272a8b3f101eb3f983084",
|
||||
"raw_identity_checks": [
|
||||
{
|
||||
"package": "iron_coast_stage2_boss_2",
|
||||
"role": "unit_or_terrain",
|
||||
"raw": "mods/iron_coast_stage2_boss_2/art/unit-generated-v1.png",
|
||||
"raw_sha256": "ed272f27c8aa31f3292ff584a3cf0ff1ec2a7fdf8213105e503c64fc133389d0",
|
||||
"builtin_byte_identical": true,
|
||||
"raw_dimensions": [
|
||||
972,
|
||||
1619
|
||||
],
|
||||
"raw_alpha": "RGBA",
|
||||
"reported_success": true
|
||||
},
|
||||
{
|
||||
"package": "iron_coast_stage2_boss_2",
|
||||
"role": "projectile",
|
||||
"raw": "mods/iron_coast_stage2_boss_2/art/projectile-generated-v1.png",
|
||||
"raw_sha256": "9cc6e7e6cac60f045050bdefe1c84f5fc7185bcb585b87b7a7df5b2206a38ebb",
|
||||
"builtin_byte_identical": true,
|
||||
"raw_dimensions": [
|
||||
887,
|
||||
1774
|
||||
],
|
||||
"raw_alpha": "RGBA",
|
||||
"reported_success": true
|
||||
}
|
||||
],
|
||||
"standalone_world_kill": {
|
||||
"boss": "iron_coast_stage2_boss_2:unit",
|
||||
"cues": [
|
||||
"iron_coast_stage2_boss_2:death",
|
||||
"iron_coast_stage2_boss_2:shot",
|
||||
"iron_coast_stage2_boss_2:warning"
|
||||
],
|
||||
"digest_before_extra_step": "ffe7a5201733c36b665fbec556de6e17f9e2b7093a854913904a8690a37a129e",
|
||||
"minimum_observed_health": 0,
|
||||
"serialized_script_state_bytes": 98,
|
||||
"shield": 75,
|
||||
"tick": 412
|
||||
},
|
||||
"all_difficulty_phase_checks": [
|
||||
{
|
||||
"actual_spawn_max_health": 200,
|
||||
"boss": "iron_coast_stage2_boss_2:unit",
|
||||
"difficulty": 0,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
},
|
||||
{
|
||||
"actual_spawn_max_health": 200,
|
||||
"boss": "iron_coast_stage2_boss_2:unit",
|
||||
"difficulty": 1,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
},
|
||||
{
|
||||
"actual_spawn_max_health": 400,
|
||||
"boss": "iron_coast_stage2_boss_2:unit",
|
||||
"difficulty": 2,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
},
|
||||
{
|
||||
"actual_spawn_max_health": 400,
|
||||
"boss": "iron_coast_stage2_boss_2:unit",
|
||||
"difficulty": 3,
|
||||
"half_health_phase": 2,
|
||||
"initial_phase": 1,
|
||||
"phase_timer_reset": true
|
||||
}
|
||||
],
|
||||
"early_duplicate_callback_scope": "Direct Rhai hook protocol checks register an already-dead spawn silently and count its death once; independent actual lethal-spawn World review remains separate.",
|
||||
"sound_mixer_checks": [
|
||||
{
|
||||
"fx_mute_zero": true,
|
||||
"mixed_peak": 3072,
|
||||
"sound": "iron_coast_stage2_boss_2:death"
|
||||
},
|
||||
{
|
||||
"fx_mute_zero": true,
|
||||
"mixed_peak": 2688,
|
||||
"sound": "iron_coast_stage2_boss_2:shot"
|
||||
},
|
||||
{
|
||||
"fx_mute_zero": true,
|
||||
"mixed_peak": 1920,
|
||||
"sound": "iron_coast_stage2_boss_2:warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"reference_role": "Original Raptor military style only, not target geometry or approved HD reference",
|
||||
"reference_review_path": "/home/quintine/Projects/RaptorReplica/RustRaptor/.reference/iron-coast-stage2-authoring/original-style-references.png",
|
||||
"reference_review_sha256": "d92f7d68008a73d626dcc9125920b7b329d029ca87a01d28bc4005f80c4f9174",
|
||||
"original_ids": [
|
||||
"10139",
|
||||
"10344",
|
||||
"400d5",
|
||||
"4011f"
|
||||
],
|
||||
"original_reference_files_not_packaged": true
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
{
|
||||
"utc": "2026-10-11T08:16:04.830947+00:00",
|
||||
"recipe": "build_assets.py",
|
||||
"recipe_sha256": "f86680d12fc6e15b585f4bba40170242518c397acdd0c1cb0992d22e0a46f462",
|
||||
"palette_sha256": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"ordinary_palette_entries_only": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
37,
|
||||
38,
|
||||
39,
|
||||
40,
|
||||
41,
|
||||
42,
|
||||
43,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
56,
|
||||
57,
|
||||
58,
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
68,
|
||||
69,
|
||||
70,
|
||||
71,
|
||||
72,
|
||||
73,
|
||||
74,
|
||||
75,
|
||||
76,
|
||||
77,
|
||||
78,
|
||||
79,
|
||||
80,
|
||||
81,
|
||||
82,
|
||||
83,
|
||||
84,
|
||||
85,
|
||||
86,
|
||||
87,
|
||||
88,
|
||||
89,
|
||||
90,
|
||||
91,
|
||||
92,
|
||||
93,
|
||||
94,
|
||||
95,
|
||||
96,
|
||||
97,
|
||||
98,
|
||||
99,
|
||||
100,
|
||||
101,
|
||||
102,
|
||||
103,
|
||||
104,
|
||||
105,
|
||||
106,
|
||||
107,
|
||||
108,
|
||||
109,
|
||||
110,
|
||||
111,
|
||||
112,
|
||||
113,
|
||||
114,
|
||||
115,
|
||||
116,
|
||||
117,
|
||||
118,
|
||||
119,
|
||||
120,
|
||||
121,
|
||||
122,
|
||||
123,
|
||||
124,
|
||||
125,
|
||||
126,
|
||||
127,
|
||||
128,
|
||||
129,
|
||||
130,
|
||||
131,
|
||||
132,
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
136,
|
||||
137,
|
||||
138,
|
||||
139,
|
||||
140,
|
||||
141,
|
||||
142,
|
||||
143,
|
||||
144,
|
||||
145,
|
||||
146,
|
||||
147,
|
||||
148,
|
||||
149,
|
||||
150,
|
||||
151,
|
||||
152,
|
||||
153,
|
||||
154,
|
||||
155,
|
||||
156,
|
||||
157,
|
||||
158,
|
||||
159,
|
||||
160,
|
||||
161,
|
||||
162,
|
||||
163,
|
||||
164,
|
||||
165,
|
||||
166,
|
||||
167,
|
||||
168,
|
||||
169,
|
||||
170,
|
||||
171,
|
||||
172,
|
||||
173,
|
||||
174,
|
||||
175,
|
||||
176,
|
||||
177,
|
||||
178,
|
||||
179,
|
||||
180,
|
||||
181,
|
||||
182,
|
||||
183,
|
||||
184,
|
||||
185,
|
||||
186,
|
||||
187,
|
||||
188,
|
||||
189,
|
||||
190,
|
||||
191,
|
||||
192,
|
||||
193,
|
||||
194,
|
||||
195,
|
||||
196,
|
||||
197,
|
||||
198,
|
||||
199,
|
||||
200,
|
||||
201,
|
||||
202,
|
||||
203,
|
||||
204,
|
||||
205,
|
||||
206,
|
||||
207,
|
||||
208,
|
||||
209,
|
||||
210,
|
||||
211,
|
||||
212,
|
||||
213,
|
||||
214,
|
||||
215,
|
||||
216,
|
||||
217,
|
||||
218,
|
||||
219,
|
||||
220,
|
||||
221,
|
||||
222,
|
||||
223,
|
||||
224,
|
||||
225,
|
||||
226,
|
||||
227,
|
||||
228,
|
||||
229,
|
||||
230,
|
||||
231,
|
||||
232,
|
||||
233,
|
||||
234,
|
||||
235,
|
||||
236,
|
||||
237,
|
||||
238,
|
||||
239
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"source": "art/unit-generated-v1.png",
|
||||
"sha256": "ed272f27c8aa31f3292ff584a3cf0ff1ec2a7fdf8213105e503c64fc133389d0",
|
||||
"logical_size": [
|
||||
48,
|
||||
80
|
||||
],
|
||||
"hd_size": [
|
||||
192,
|
||||
320
|
||||
],
|
||||
"alpha_below128_transparent": true,
|
||||
"original_master_not_modified": true,
|
||||
"whole_alpha_bounding_crop_for_projectile": null
|
||||
},
|
||||
{
|
||||
"source": "art/projectile-generated-v1.png",
|
||||
"sha256": "9cc6e7e6cac60f045050bdefe1c84f5fc7185bcb585b87b7a7df5b2206a38ebb",
|
||||
"logical_size": [
|
||||
8,
|
||||
16
|
||||
],
|
||||
"hd_size": [
|
||||
32,
|
||||
64
|
||||
],
|
||||
"alpha_below128_transparent": true,
|
||||
"original_master_not_modified": true,
|
||||
"whole_alpha_bounding_crop_for_projectile": [
|
||||
363,
|
||||
637,
|
||||
525,
|
||||
1110
|
||||
]
|
||||
}
|
||||
],
|
||||
"audio": [
|
||||
{
|
||||
"file": "audio/warning.wav",
|
||||
"sha256": "3076a52993dd30e4eb781256c323a865615d85720fe45c47baa4b67541d6ceb9",
|
||||
"samples": 11025,
|
||||
"rate": 22050,
|
||||
"duration": 0.5,
|
||||
"peak": 7338,
|
||||
"rms": 2447.2,
|
||||
"seed": 8111
|
||||
},
|
||||
{
|
||||
"file": "audio/shot.wav",
|
||||
"sha256": "823b9d2d3d51ccefea88e1511afaeb7a40d5e22ce47001bc3376e6419f1d0b09",
|
||||
"samples": 3528,
|
||||
"rate": 22050,
|
||||
"duration": 0.16,
|
||||
"peak": 9463,
|
||||
"rms": 1891.06,
|
||||
"seed": 7589
|
||||
},
|
||||
{
|
||||
"file": "audio/death.wav",
|
||||
"sha256": "599d493fa8b7e9944a9932135a62d0007e04533bb78f80e83aff862ca2ab4092",
|
||||
"samples": 23152,
|
||||
"rate": 22050,
|
||||
"duration": 1.05,
|
||||
"peak": 8143,
|
||||
"rms": 2001.53,
|
||||
"seed": 7641
|
||||
}
|
||||
],
|
||||
"final_art": [
|
||||
{
|
||||
"file": "art/projectile-hd.png",
|
||||
"sha256": "449bc288ba96deca89aa9318fba47662d8bf8e07d82494f978ccbc4f5dd4a869",
|
||||
"size": [
|
||||
32,
|
||||
64
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-sd.png",
|
||||
"sha256": "5f408b4713a948f75d7ba1e6ee582fe8eb91a235df41e2e6d1bb4dac050fbac3",
|
||||
"size": [
|
||||
8,
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "art/unit-hd.png",
|
||||
"sha256": "e2a136b0b054a94ef7ccc33212602ccdb6b748d7b6eee33ba05c28dbcf7d7f8c",
|
||||
"size": [
|
||||
192,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "art/unit-sd.png",
|
||||
"sha256": "7106674b5ab821fcb121a36f5400680c40558ece3e9a14799f5012ebf4d0a11a",
|
||||
"size": [
|
||||
48,
|
||||
80
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce SD/HD canvases and PCM WAVs from untouched builtin generated PNGs.
|
||||
No decoded original images are packaged. No painted/masked master edits occur.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import argparse,datetime,hashlib,json,math,random,struct,wave
|
||||
from PIL import Image,ImageOps
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
h=lambda p:hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
||||
def palette(path):
|
||||
raw=path.read_bytes();assert len(raw)==768
|
||||
values=[(min(v,63)<<2)|(min(v,63)>>4) for v in raw]
|
||||
values[720:]=[0]*48
|
||||
im=Image.new('P',(1,1));im.putpalette(values);return im
|
||||
|
||||
def small(im,size,pal):
|
||||
im=im.convert('RGBA').resize(size,Image.Resampling.LANCZOS)
|
||||
alpha=im.getchannel('A').point(lambda a:255 if a>=128 else 0)
|
||||
out=im.convert('RGB').quantize(palette=pal,dither=Image.Dither.NONE).convert('RGBA');out.putalpha(alpha);return out
|
||||
|
||||
def canvases(source,name,size,pal):
|
||||
src=Image.open(source).convert('RGBA');target=(size[0]*4,size[1]*4)
|
||||
bounds=None
|
||||
if name=='projectile':
|
||||
bounds=src.getchannel('A').point(lambda a:255 if a>=128 else 0).getbbox();assert bounds;src=src.crop(bounds)
|
||||
fit_size=(round(target[0]*.8),round(target[1]*.8)) if name=='projectile' else target
|
||||
hd=Image.new('RGBA',target);fitted=ImageOps.contain(src,fit_size,Image.Resampling.LANCZOS);hd.alpha_composite(fitted,((target[0]-fitted.width)//2,(target[1]-fitted.height)//2))
|
||||
hd.save(ROOT/'art'/f'{name}-hd.png');small(hd,size,pal).save(ROOT/'art'/f'{name}-sd.png')
|
||||
return {'source':str(source.relative_to(ROOT)),'sha256':h(source),'logical_size':size,'hd_size':target,'alpha_below128_transparent':True,'original_master_not_modified':True,'whole_alpha_bounding_crop_for_projectile':bounds}
|
||||
|
||||
def synth(name,duration,base):
|
||||
rate=22050;n=int(rate*duration);rng=random.Random(7043+sum(map(ord,name))+base);data=bytearray();peak=0;sumsq=0
|
||||
for i in range(n):
|
||||
t=i/rate;u=i/n;env=min(1,u*30)*(1-u)**1.3
|
||||
if name=='shot':s=(math.sin(2*math.pi*(95*t-40*t*t))+rng.uniform(-1,1)*0.45)*math.exp(-u*3)
|
||||
elif name=='death':s=rng.uniform(-1,1)*0.65+math.sin(2*math.pi*(70*t-18*t*t))*0.4
|
||||
elif name=='warning' or name=='boss_alarm':s=math.sin(2*math.pi*(base+70*math.sin(2*math.pi*t*5))*t)*0.8+0.12*math.sin(2*math.pi*base*.5*t)
|
||||
else:
|
||||
notes=[1,1.25,1.5,2] if name=='victory' else [1,1.5,1.25];freq=base*notes[min(len(notes)-1,int(u*len(notes)))];s=math.sin(2*math.pi*freq*t)+0.15*math.sin(2*math.pi*freq*2*t)
|
||||
v=max(-32767,min(32767,int(s*env*8500)));peak=max(peak,abs(v));sumsq+=v*v;data+=struct.pack('<h',v)
|
||||
p=ROOT/'audio'/f'{name}.wav'
|
||||
with wave.open(str(p),'wb') as w:w.setnchannels(1);w.setsampwidth(2);w.setframerate(rate);w.writeframes(data)
|
||||
return {'file':str(p.relative_to(ROOT)),'sha256':h(p),'samples':n,'rate':rate,'duration':duration,'peak':peak,'rms':round(math.sqrt(sumsq/n),2),'seed':7043+sum(map(ord,name))+base}
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument('--palette',type=Path,default=ROOT.parents[1]/'data/misc/PALETTE_DAT');args=parser.parse_args();pal=palette(args.palette)
|
||||
report={'utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'recipe':Path(__file__).name,'recipe_sha256':h(__file__),'palette_sha256':h(args.palette),'ordinary_palette_entries_only':list(range(240)),'assets':[]}
|
||||
n=int(ROOT.name.rsplit('_',1)[1]);is_boss='_boss_' in ROOT.name
|
||||
if is_boss:
|
||||
report['assets'].append(canvases(ROOT/'art/unit-generated-v1.png','unit',(128,64) if n==1 else (48,80),pal))
|
||||
report['assets'].append(canvases(ROOT/'art/projectile-generated-v1.png','projectile',(8,16),pal))
|
||||
cues=[('warning',.5,250+n*30),('shot',.16,100),('death',1.05,80)]
|
||||
else:
|
||||
raw=ROOT/'art/terrain-generated-v1.png';src=Image.open(raw).convert('RGBA');hd=src.resize((1280,2048),Image.Resampling.LANCZOS);sd=small(hd,(320,512),pal)
|
||||
for reverse in [False,True]:
|
||||
hi=ImageOps.flip(hd) if reverse else hd;lo=ImageOps.flip(sd) if reverse else sd
|
||||
for y in range(16):
|
||||
for x in range(10):
|
||||
index=(160 if reverse else 0)+y*10+x
|
||||
hi.crop((x*128,y*128,(x+1)*128,(y+1)*128)).save(ROOT/'art'/f'tile_{index:03}-hd.png')
|
||||
lo.crop((x*32,y*32,(x+1)*32,(y+1)*32)).save(ROOT/'art'/f'tile_{index:03}-sd.png')
|
||||
# Reflected cycle joins equal boundary rows at full and logical resolution.
|
||||
for im in [hd,sd]:assert im.crop((0,im.height-1,im.width,im.height)).tobytes()==ImageOps.flip(im).crop((0,0,im.width,1)).tobytes()
|
||||
hd.save(ROOT/'art/terrain-preview-hd.png');sd.save(ROOT/'art/terrain-preview-sd.png')
|
||||
bg=ImageOps.fit(src,(1280,800),Image.Resampling.LANCZOS,centering=(.5,.35));bg.save(ROOT/'art/briefing-hd.png');small(bg,(320,200),pal).save(ROOT/'art/briefing-sd.png')
|
||||
report['assets'].append({'source':'art/terrain-generated-v1.png','sha256':h(raw),'logical_map':[320,512],'hd_map':[1280,2048],'reflected_cycle':True,'tiles':320,'briefing':'same generated map cropped with 8:5 viewport, no separate generation','source_not_modified':True})
|
||||
cues=[('approach',.6,164+n*30),('objective',.36,392),('boss_alarm',.75,275),('victory',.95,262)]
|
||||
(ROOT/'audio').mkdir(exist_ok=True);report['audio']=[synth(*c) for c in cues]
|
||||
report['final_art']=[{'file':str(p.relative_to(ROOT)),'sha256':h(p),'size':Image.open(p).size} for p in sorted((ROOT/'art').glob('*.png')) if 'generated' not in p.name]
|
||||
(ROOT/'asset-report.json').write_text(json.dumps(report,indent=2)+'\n');print(ROOT.name,'assets',len(report['final_art']),'sounds',len(cues))
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_2:projectile",
|
||||
"name": "Undertow naval missile",
|
||||
"damage": 4,
|
||||
"speed": 4,
|
||||
"cooldown": 72,
|
||||
"behavior": "straight",
|
||||
"target": "all",
|
||||
"art": {
|
||||
"png": "art/projectile-sd.png",
|
||||
"hd_png": "art/projectile-hd.png",
|
||||
"logical_width": 8,
|
||||
"logical_height": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"enemies": [
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_2:unit",
|
||||
"name": "Undertow",
|
||||
"health": 400,
|
||||
"speed": 0,
|
||||
"reward": 16000,
|
||||
"ground": true,
|
||||
"boss": true,
|
||||
"cooldown": 72,
|
||||
"art": {
|
||||
"png": "art/unit-sd.png",
|
||||
"hd_png": "art/unit-hd.png",
|
||||
"logical_width": 48,
|
||||
"logical_height": 80
|
||||
},
|
||||
"script": "scripts/ai.rhai"
|
||||
}
|
||||
],
|
||||
"sounds": [
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_2:warning",
|
||||
"name": "Undertow warning",
|
||||
"file": "audio/warning.wav",
|
||||
"volume": 76,
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_2:shot",
|
||||
"name": "Undertow shot",
|
||||
"file": "audio/shot.wav",
|
||||
"volume": 88,
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"id": "iron_coast_stage2_boss_2:death",
|
||||
"name": "Undertow death",
|
||||
"file": "audio/death.wav",
|
||||
"volume": 110,
|
||||
"priority": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"tool": "builtin image_gen",
|
||||
"utc": "2026-10-11T08:14:12.481837+00:00",
|
||||
"original_builtin": "/home/quintine/.codex/generated_images/01a1245d-6057-70c2-a44c-c7a159ca21cb/exec-14913e18-0f98-487c-9769-24cceb3f3252.png",
|
||||
"raw": "art/unit-generated-v1.png",
|
||||
"raw_sha256": "ed272f27c8aa31f3292ff584a3cf0ff1ec2a7fdf8213105e503c64fc133389d0",
|
||||
"prompt": "prompts/unit-v1.txt",
|
||||
"prompt_sha256": "98e18f0aadc50d8426c9405f019fab31db851c343b48dbc243c785192fef05c9",
|
||||
"lease": "397f831b-6922-424e-b79f-1ef3a1ff5d66",
|
||||
"job": "stage2-boss2-unit-v1",
|
||||
"raw_preserved": true,
|
||||
"projectile": {
|
||||
"tool": "builtin image_gen",
|
||||
"original_builtin": "/home/quintine/.codex/generated_images/01a1245d-6057-70c2-a44c-c7a159ca21cb/exec-e4223f76-7281-4bbd-954a-66f3026d93e0.png",
|
||||
"raw": "art/projectile-generated-v1.png",
|
||||
"raw_sha256": "9cc6e7e6cac60f045050bdefe1c84f5fc7185bcb585b87b7a7df5b2206a38ebb",
|
||||
"prompt": "prompts/projectile-v1.txt",
|
||||
"lease": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"job": "stage2-boss-projectile-v1",
|
||||
"reuse": "Same newly authored micro-missile art independently packaged with each boss; no package dependency.",
|
||||
"raw_preserved": true,
|
||||
"prompt_sha256": "9b3fc62e11770453f8799159e739e5a9d682e5d5fcc5dab5753012555b2f6f93"
|
||||
},
|
||||
"exact_gate_history": [
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:13:00+00:00",
|
||||
"event": "acquired",
|
||||
"lease_id": "397f831b-6922-424e-b79f-1ef3a1ff5d66",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss2-unit-v1",
|
||||
"expires_at": "2026-10-11T08:43:00+00:00"
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:13:00+00:00",
|
||||
"event": "started",
|
||||
"lease_id": "397f831b-6922-424e-b79f-1ef3a1ff5d66",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss2-unit-v1",
|
||||
"note": "Owner is submitting the built-in imagegen request; this slot requires an explicit report or release."
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:13:29+00:00",
|
||||
"event": "report",
|
||||
"lease_id": "397f831b-6922-424e-b79f-1ef3a1ff5d66",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss2-unit-v1",
|
||||
"outcome": "success"
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:14:24+00:00",
|
||||
"event": "acquired",
|
||||
"lease_id": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss-projectile-v1",
|
||||
"expires_at": "2026-10-11T08:44:24+00:00"
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:14:24+00:00",
|
||||
"event": "started",
|
||||
"lease_id": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss-projectile-v1",
|
||||
"note": "Owner is submitting the built-in imagegen request; this slot requires an explicit report or release."
|
||||
},
|
||||
{
|
||||
"timestamp_utc": "2026-10-11T08:14:46+00:00",
|
||||
"event": "report",
|
||||
"lease_id": "5a385d2a-1997-4f67-8932-beb12dccba61",
|
||||
"owner": "/root/iron_coast_2",
|
||||
"job_id": "stage2-boss-projectile-v1",
|
||||
"outcome": "success"
|
||||
}
|
||||
],
|
||||
"authentic_style_reference_pins": {
|
||||
"role": "authentic original palette/material style; new designs do not copy target geometry",
|
||||
"sources": [
|
||||
{
|
||||
"asset_id": "10139",
|
||||
"catalog_path": "sprites/TARGT2G1_PIC",
|
||||
"original_payload_sha256": "16647e06c0121136adaede8ba6636f4eab4af33bb15be83c673b6201114412e3"
|
||||
},
|
||||
{
|
||||
"asset_id": "10344",
|
||||
"catalog_path": "sprites/SHIP39G1_PIC",
|
||||
"original_payload_sha256": "7cb09e925ea7786b331d576c360aad09b5e18f35d0258ec78ab5ef2e50c2590e"
|
||||
},
|
||||
{
|
||||
"asset_id": "400d5",
|
||||
"catalog_path": "misc/unnamed.3295",
|
||||
"original_payload_sha256": "52b3724f81fe97fc2884f45440e711776e253de93a26da2bcf1c9fe06241fd2f"
|
||||
},
|
||||
{
|
||||
"asset_id": "4011f",
|
||||
"catalog_path": "misc/unnamed.3369",
|
||||
"original_payload_sha256": "ec08b9e0761eb2b408df6b5c4c71019b1f277478237cf336412d01672a7158ee"
|
||||
}
|
||||
],
|
||||
"palette_sha256": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"external_contact_sheet_sha256": "d92f7d68008a73d626dcc9125920b7b329d029ca87a01d28bc4005f80c4f9174",
|
||||
"decoded_reference_images_excluded_from_package": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "iron_coast_stage2_boss_2"
|
||||
name = "Iron Coast II: Undertow"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
content = "content.json"
|
||||
scripts = ["scripts/ai.rhai"]
|
||||
description = "A standalone Undertow boss with telegraphed attacks, vulnerable phases, genuine SD/HD artwork and synthesized naval effects."
|
||||
@@ -0,0 +1,3 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: tiny top-down missile sprite, authored mod projectile.
|
||||
Primary request: one short naval micro-missile for the Iron Coast II bosses. Centered narrow dark gunmetal capsule, pale gray nose pointing DOWN, two little triangular tail fins at TOP and a very small orange exhaust at the top tip. Orthographic overhead complete silhouette, upper-left lighting, crisp old Raptor military sprite medium with restrained HD panels. Strong simple silhouette legible at8x16 logical pixels. Canvas640x1280 exactly1:2 aspect, at least15% transparent margin on all sides, genuine alpha outside missile/exhaust. No background, water, shadows beyond missile, explosion, long flame streak, extra bullets, lettering or watermark.
|
||||
@@ -0,0 +1,4 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: complete top-down transparent boss sprite for a vertical military shoot-em-up. New authored mod artwork, not an original remaster.
|
||||
Primary request: Undertow, a surfaced long missile submarine from Iron Coast II: Black Tide. Reference is authentic old Raptor military sprite style only. New silhouette: narrow fore-aft rounded submarine hull, pointed bow facing DOWN, twin deck missile tubes parallel to hull, small central sail with periscope, stern rudder and side stabilizers near the upper stern. Long vertical3:5 ship footprint unlike a broad flying cruiser. Orthographic overhead, no perspective horizon. Dark matte navy and muted gunmetal with worn pale deck panels, restrained amber warning lamps; upper-left lighting and low painted shadows. No water background, no wake outside alpha, no smoke, no explosions, no text, no logos, no other objects. Entire submarine centered and intact, including its small fins, within generous 8% transparent margins. Crisp readable SD silhouette at48x80 logical pixels, rich but restrained 4x HD detail, smooth armored materials rather than pebble noise.
|
||||
Canvas:768x1280, exact3:5 aspect, genuine transparent alpha outside intact submarine.
|
||||
@@ -0,0 +1,56 @@
|
||||
// Per-life cursors are serializable and deterministic. No engine-specific boss behavior.
|
||||
fn alive(event,id) {
|
||||
for unit in event.entities { if unit.entity==id && unit.health>0 { return true; } }
|
||||
false
|
||||
}
|
||||
fn on_spawn(event,state,random) {
|
||||
if !state.contains("ages") { state.ages=#{}; state.phases=#{}; state.directions=#{}; }
|
||||
if !state.contains("maximums") {state.maximums=#{};}
|
||||
// Remove cursors from a prior replay or an explicitly despawned life.
|
||||
for key in state.ages.keys() {
|
||||
let present=false;for unit in event.entities {if unit.entity.to_string()==key && unit.health>0 {present=true;}}
|
||||
if !present { state.ages.remove(key);state.phases.remove(key);state.directions.remove(key);state.maximums.remove(key); }
|
||||
}
|
||||
let key=event.entity.to_string();state.ages[key]=0;state.phases[key]=1;state.directions[key]=1;state.maximums[key]=1;
|
||||
for unit in event.entities {if unit.entity==event.entity && unit.health>0 {state.maximums[key]=unit.health;}}
|
||||
if !alive(event,event.entity) { return #{commands:[],state:state}; }
|
||||
#{commands:[#{op:"velocity",entity:event.entity,vx:0,vy:2}],state:state}
|
||||
}
|
||||
fn on_tick(event,state,random) {
|
||||
if event.health<=0 || !alive(event,event.entity) { return #{commands:[],state:state}; }
|
||||
let key=event.entity.to_string();
|
||||
if !state.contains("ages") || !state.ages.contains(key) { return #{commands:[],state:state}; }
|
||||
if event.y<20 { return #{commands:[#{op:"velocity",entity:event.entity,vx:0,vy:2}],state:state}; }
|
||||
// Older saved cursors lack maximums; initialize at their current living health.
|
||||
if !state.contains("maximums") {state.maximums=#{};}
|
||||
if !state.maximums.contains(key) {state.maximums[key]=event.health;}
|
||||
let phase=if event.health*2<=state.maximums[key] {2} else {1};
|
||||
if state.phases[key]!=phase {state.phases[key]=phase;state.ages[key]=0;}
|
||||
let age=state.ages[key]+1;state.ages[key]=age;
|
||||
let commands=[];
|
||||
let direction=state.directions[key];
|
||||
if event.x>=224 {direction=-1;} if event.x<=48 {direction=1;}
|
||||
state.directions[key]=direction;
|
||||
commands.push(#{op:"move",entity:event.entity,x:event.x,y:20});commands.push(#{op:"velocity",entity:event.entity,vx:direction,vy:0});
|
||||
let cycle=if phase==1 {76} else {62};
|
||||
if age%cycle==cycle-12 {commands.push(#{op:"play_sound",sound:"iron_coast_stage2_boss_2:warning"});}
|
||||
if age%cycle==0 {
|
||||
if phase==1 {
|
||||
commands.push(#{op:"projectile",weapon:"iron_coast_stage2_boss_2:projectile",x:event.x+10,y:90,vx:-1,vy:4,hostile:true});
|
||||
commands.push(#{op:"projectile",weapon:"iron_coast_stage2_boss_2:projectile",x:event.x+38,y:90,vx:1,vy:4,hostile:true});
|
||||
} else {
|
||||
for lane in [-2,0,2] {
|
||||
commands.push(#{op:"projectile",weapon:"iron_coast_stage2_boss_2:projectile",x:event.x+24+lane*6,y:90,vx:lane,vy:4,hostile:true});
|
||||
}
|
||||
}
|
||||
commands.push(#{op:"play_sound",sound:"iron_coast_stage2_boss_2:shot"});
|
||||
}
|
||||
#{commands:commands,state:state}
|
||||
}
|
||||
fn on_death(event,state,random) {
|
||||
let key=event.entity.to_string();
|
||||
if alive(event,event.entity) || !state.contains("ages") || !state.ages.contains(key) {return #{commands:[],state:state};}
|
||||
state.ages.remove(key);state.phases.remove(key);state.directions.remove(key);state.maximums.remove(key);
|
||||
let value=if event.objectives.contains("iron_coast_stage2_boss_2:defeated") {event.objectives["iron_coast_stage2_boss_2:defeated"]+1} else {1};
|
||||
#{commands:[#{op:"objective",key:"iron_coast_stage2_boss_2:defeated",value:value},#{op:"effect",x:event.x+24,y:event.y+40},#{op:"play_sound",sound:"iron_coast_stage2_boss_2:death"}],state:state}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Iron Coast II — Stormglass
|
||||
|
||||
Independent API1 enemy package: enable this mod alone to make `iron_coast_stage2_boss_3:unit` available to missions. It does not add a playable campaign, shop item, dependency, override or equipment grant. Mission3 uses this boss; other mod missions can reference the unit and its `:defeated` objective.
|
||||
|
||||
Stormglass is a104×72 ground boss with240 normal health and2400 credit reward. It enters at2pixels per tick then holds y24, patrolling x60–156 at1pixel per tick. Each salvo gives a14-tick visual/audio warning and movement pause. Alternating side turrets fire two3-damage shells every72ticks above halfhealth; below halfhealth a central counter-angled shell joins a60-tick cycle. All projectiles have finite standard collision behavior. The actual spawn-health snapshot defines the half-health transition, so easier difficulty begins in the first phase despite the engine halving starting health.
|
||||
|
||||
State is deterministic and per entity. Arrival and fire clocks are independent. Dead or missing snapshots cannot shoot; actual deaths increment `iron_coast_stage2_boss_3:defeated` once, while despawn/escape never count. Spawn clears reused entity-ID flags before checking the live snapshot, allowing saved-state mission replays even when the engine collects a ground projectile death on the following step. The boss never completes a mission or pays a script credit reward; the engine grants its one defined reward.
|
||||
|
||||
The unit and shell have genuine transparent SD/8×HD PNGs and untouched builtin masters, with tracked `prompts/`, per-art provenance, `asset-report.json`, `audio-recipe.json` and `build_assets.py`. Runtime-invisible alpha padding is cropped at16 while runtime alpha128samples remain inside the fitted logical canvas. Generated warning/shot/death WAVs are original seeded mono16-bit22050Hz synthesis. The recipe defaults to the bundled `RustRaptor/data/misc/PALETTE_DAT`; decoded original style references remain outside the package.
|
||||
|
||||
From `RustRaptor/`, run `target/release/raptor-rust --data data --mods mods --validate-mods iron_coast_stage2_boss_3` and `cargo run --manifest-path mods/iron_coast_stage2_boss_3/validation/Cargo.toml --target-dir target/mod-validation --bin validate --release -- mods/iron_coast_stage2_boss_3`. The local probe checks exact warning/volley timing and phase sizes, independent entities, arrival/bounds, deterministic saved state, dead/missing guards, actual difficulty0–3 health transitions and three reused-ID lethal-spawn/reward replays. Mission-level combat balance and SD/HD scene evidence are supplied with Mission3.
|
||||
|
||||
Retired death guards are bounded to the most recent128 entity IDs, including deterministic migration of older checkpoints. Spawn resets a reused ID before any alive check; duplicate deaths for the current lifecycle emit no commands. A new World also discards clocks of absent entities while retaining live peers. The `retained-state` validation binary exercises1,205 actual-catalog/native lethal-collision lives, including1,200 distinct IDs, five reused-ID lives, JSON state/RNG restoration, a960-guard old checkpoint, duplicate callbacks, exact rewards and difficulty0–3. It additionally checks1,205 abandoned-live World resets and two independent live clocks. Run it from `RustRaptor/` with `cargo run --manifest-path mods/iron_coast_stage2_boss_3/validation/Cargo.toml --target-dir target/mod-validation --bin retained-state --release -- mods/iron_coast_stage2_boss_3`. The corresponding `validation/retained-state.json` records actual observations; package pins there identify the activation before generated proof files were finalized. The final external freeze ledger binds the complete resulting package.
|
||||
|
||||
A health-phase change restarts its per-entity warning clock and immediately telegraphs the new phase. It cannot shorten the14-tick charge by changing the modulo period. The actual World regression lowers health immediately after the second high-health warning at all four difficulties and verifies a fresh warning followed by exactly14ticks before release; it also upgrades an old checkpoint lacking the phase latch.
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 165 B |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 552 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"palette_sha256": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204",
|
||||
"ordinary_palette_entries_only": true,
|
||||
"audio": {
|
||||
"warning": {
|
||||
"path": "audio/warning.wav",
|
||||
"sha256": "70865f6e434074019ffb06dcf2e7c9304347286bbdca61ccdbb4c23739aaaa66",
|
||||
"sample_rate": 22050,
|
||||
"bits": 16,
|
||||
"channels": 1,
|
||||
"samples": 7938,
|
||||
"seconds": 0.36,
|
||||
"seed": 2199,
|
||||
"peak": 15728,
|
||||
"recipe": "deterministic seeded chirp/noise, original synthesis, no recordings"
|
||||
},
|
||||
"shot": {
|
||||
"path": "audio/shot.wav",
|
||||
"sha256": "df4dca41b95e92e838ee94e18de4414b4ca990668a635c0005178a516392660a",
|
||||
"sample_rate": 22050,
|
||||
"bits": 16,
|
||||
"channels": 1,
|
||||
"samples": 4410,
|
||||
"seconds": 0.2,
|
||||
"seed": 2200,
|
||||
"peak": 20316,
|
||||
"recipe": "deterministic seeded chirp/noise, original synthesis, no recordings"
|
||||
},
|
||||
"death": {
|
||||
"path": "audio/death.wav",
|
||||
"sha256": "6449a792d04b1208b230f1e75b76a3876692666e52c7d0deed42b528dab212f3",
|
||||
"sample_rate": 22050,
|
||||
"bits": 16,
|
||||
"channels": 1,
|
||||
"samples": 20948,
|
||||
"seconds": 0.9500226757369614,
|
||||
"seed": 2201,
|
||||
"peak": 20316,
|
||||
"recipe": "deterministic seeded chirp/noise, original synthesis, no recordings"
|
||||
}
|
||||
},
|
||||
"unit": {
|
||||
"master": "art/unit-source-v1.png",
|
||||
"master_sha256": "ab3d53a367890a4a842bd57d86eb14a8f65abf2748ae3d5fc45ed875c9826fad",
|
||||
"master_size": [
|
||||
1509,
|
||||
1042
|
||||
],
|
||||
"crop_runtime_invisible_padding": [
|
||||
207,
|
||||
76,
|
||||
1303,
|
||||
960
|
||||
],
|
||||
"crop_threshold": 16,
|
||||
"runtime_alpha_threshold": 128,
|
||||
"logical_canvas": [
|
||||
104,
|
||||
72
|
||||
],
|
||||
"hd_canvas": [
|
||||
832,
|
||||
576
|
||||
],
|
||||
"sd_bounds": [
|
||||
11,
|
||||
3,
|
||||
93,
|
||||
69
|
||||
],
|
||||
"hd_bounds": [
|
||||
88,
|
||||
24,
|
||||
743,
|
||||
552
|
||||
],
|
||||
"frame_count": 1,
|
||||
"origin": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"generated_art_repainted": false
|
||||
},
|
||||
"shell": {
|
||||
"master": "art/shell-v2-source-v1.png",
|
||||
"master_sha256": "2f512c5ed0c993da8988dca809a522946d13bbbbe8b288b62361dc5ec4add8e7",
|
||||
"master_size": [
|
||||
1620,
|
||||
971
|
||||
],
|
||||
"crop_runtime_invisible_padding": [
|
||||
725,
|
||||
263,
|
||||
896,
|
||||
709
|
||||
],
|
||||
"crop_threshold": 16,
|
||||
"runtime_alpha_threshold": 128,
|
||||
"logical_canvas": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"hd_canvas": [
|
||||
64,
|
||||
64
|
||||
],
|
||||
"sd_bounds": [
|
||||
3,
|
||||
1,
|
||||
5,
|
||||
7
|
||||
],
|
||||
"hd_bounds": [
|
||||
23,
|
||||
8,
|
||||
41,
|
||||
56
|
||||
],
|
||||
"frame_count": 1,
|
||||
"origin": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"generated_art_repainted": false
|
||||
}
|
||||
}
|
||||