Add separate Iron Coast mission, weapon and enemy mods with SD HD artwork and audio

This commit is contained in:
raptorreplica committed 2026-10-10 12:20:24 +11:00
1 parent dbb1d4596b
commit 967ba69ee8
573 files changed
+6515

No files matched your search

@@ -0,0 +1,7 @@
# seeker_swarm artwork provenance
Generated locally with the built-in image generation tool and the imagegen skill. Original catalog IDs `0x102b1` (gray gun inventory icon) and `0x101cf` (8×8 projectile) were decoded and inspected as style/scale references. Original pixels are not included in the mod or Git. The prompt uses these references to create a distinct naval industrial weapon rather than copy the original.
Generated master images, SD/HD sprites and sound WAVs are tracked with the package, as requested. Original game pixels and decoded reference images remain outside Git. The generated source is converted into 16×16/64×64 equipment canvases and a 8×16 logical projectile canvas plus its 4× HD counterpart. Transparent margins remain inside the logical canvas. Sound synthesis is deterministic, PCM16 mono 22,050 Hz; `generate_audio.py` is the complete reproduction source.
Prompts and generated-source SHA256 hashes are recorded in `art-provenance.json` after output verification. Palette conversion occurs in the runtime loader; gameplay dimensions do not depend on HD artwork.
+9
View File
@@ -0,0 +1,9 @@
# Seeker Swarm 1.0.0
Enable **Seeker Swarm** in Mods and apply at the main menu. This independent API 1 example has no dependencies. Buy its equipment in the shop (14,000 credits, maximum one) or collect its pickup. Choose it in Inventory or cycle special weapons during a mission. The equipped weapon fires with Fire or Special; it consumes no ammunition.
Projectile: 6 damage, 6 logical pixels per step, 18 step cooldown, `homing`, air and ground targets. Three homing missiles fire together; an impact deals two fragmentation damage to up to two nearby enemies. Rhai stores bounded burst/hit counters and emits local firing/impact sounds and effects.
`art/equipment-sd.png` is a 16×16 logical icon, `art/equipment-hd.png` its detailed 4× variant. Projectile canvases are 8×16 SD and 32×64 HD. Switch HD artwork in Options. The generated master images, SD/HD sprites, WAV files, provenance and reproduction inputs are tracked in Git. Original game assets and decoded reference images remain outside version control. `python3 generate_audio.py` reproduces both sound files.
Validate locally with `cargo run --no-default-features -- --mods examples/mods --validate-mods seeker_swarm`. Read the shared [API documentation](../../../docs/modding-api.md) for save pins, dependencies and sandbox limits.
@@ -0,0 +1,65 @@
{
"package": "seeker_swarm",
"tool": "built-in image_gen",
"skill": "/home/quintine/.codex/skills/.system/imagegen/SKILL.md",
"reference_role": "Original catalog sprites inspected as style and scale references, not copied as mod artwork",
"reference_assets": [
"0x102b1",
"0x101cf"
],
"sources": [
{
"kind": "equipment",
"path": "/home/quintine/.codex/generated_images/01a12340-dd1f-71e1-869a-0c9f61a82318/exec-a0a127b8-67e8-43f2-a080-cbfefceac7b2.png",
"prompt": "Use case: stylized-concept. Asset type: HD sprite for equipment shop/inventory in 1990s military science-fiction vertical shooter. Input image is an ORIGINAL STYLE REFERENCE ONLY, not an edit target. Generate a distinct Seeker Swarm launcher: compact gray armored missile pod with three narrow rocket tips pointing exactly UP, dark launch channels, restrained amber indicator dots and red service tab. Top-down orthographic overhead view, symmetrical compact military silhouette. Isolated single object centered on genuinely transparent 1024x1024 square canvas, silhouette central 70%, at least 12% transparent margin. Reference style: chunky readable silhouette, muted gunmetal, upper-left highlights, limited palette. Restrained machined detail appropriate HD. No text, labels, glow, shadows outside body, border, frame, extra objects. Display logical 16x16 SD and64x64 HD; make silhouette readable after reduction.",
"sha256": "29336ba6ae08bdb8c42feb257c6a7f9d05c174fa10643674602d1bd6c86797ff",
"local_path": "art/equipment-generated-source.png"
},
{
"kind": "projectile",
"path": "/home/quintine/.codex/generated_images/01a12340-dd1f-71e1-869a-0c9f61a82318/exec-defa402a-c1f2-4a56-923e-98d8b64f081e.png",
"prompt": "Use case: stylized-concept. Asset type: ONE isolated homing missile projectile sprite for a military science-fiction vertical shooter. Reference image is original STYLE reference for chunky readability ONLY. Generate a small slender naval gray missile pointing EXACTLY UP, white metal pointed nose, gray body with a narrow red band, two small fins, short amber engine exhaust directly below. Top-down orthographic overhead view, centered symmetric military silhouette. Genuine transparent square canvas with large margins; missile silhouette width:height1:2 and occupies central70%height. Palette gray slate white amber red, strongest light upper-left, crisp readable pixel-inspired HD detail. No spacecraft, no launcher, no multiplemissiles, no expansiveglow, no shadow, no text/labels/border. Logicalcanvas8x16 and32x64HD.",
"sha256": "59ed9ff1c0302955ff6d047c9527ba3f2c9c2ff15c5a9c07ebb86fc08887cf14",
"local_path": "art/projectile-generated-source.png"
}
],
"conversion": "prepare_art.rs crops to alpha>=128 silhouette, fits with transparent margins, preserves aspect and reduces to logical SD/4xHD. SD nearest sampling alpha128; HD Lanczos3 alpha64. Pixel values quantized at runtime.",
"outputs": {
"art/equipment-generated-source.png": {
"sha256": "29336ba6ae08bdb8c42feb257c6a7f9d05c174fa10643674602d1bd6c86797ff",
"bytes": 528586
},
"art/equipment-sd.png": {
"sha256": "8b7d5dc945536f8b35b38bc37c4f607fbba7bb6da4cc00691c18576a72224764",
"bytes": 643
},
"art/equipment-hd.png": {
"sha256": "caeca8f6e4993dcbb6047974caf8dae442d2abe2043b516685602cece6f970df",
"bytes": 6002
},
"art/projectile-generated-source.png": {
"sha256": "59ed9ff1c0302955ff6d047c9527ba3f2c9c2ff15c5a9c07ebb86fc08887cf14",
"bytes": 256643
},
"art/projectile-sd.png": {
"sha256": "c382c66203efd29ef467951ae9213d95e24d760837e51221d2ec9addb519125c",
"bytes": 246
},
"art/projectile-hd.png": {
"sha256": "dc9ee59e6c461fba0373c842af4c12c8f25a1ed4b21eecb214888de7914f9d09",
"bytes": 1426
}
},
"sounds": {
"audio/fire.wav": {
"sha256": "08fc2bb9b9054954f9bc8fe378e6d3aaa26081f3eae172f4f3fa14c580b4c20a",
"bytes": 5776,
"synthesis": "generate_audio.py PCM16 mono22050Hz"
},
"audio/impact.wav": {
"sha256": "c32ab425f9e4be45a0921657cf76c845d1f9c432cd2bf64e6aa16be725c2c882",
"bytes": 7540,
"synthesis": "generate_audio.py PCM16 mono22050Hz"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.
Binary file not shown.
+75
View File
@@ -0,0 +1,75 @@
{
"weapons": [
{
"id": "seeker_swarm:projectile",
"name": "Seeker Swarm Projectile",
"damage": 6,
"speed": 6,
"cooldown": 18,
"behavior": "homing",
"target": "all",
"offsets": [
[
-7,
-2
],
[
0,
-5
],
[
7,
-2
]
],
"art": {
"png": "art/projectile-sd.png",
"hd_png": "art/projectile-hd.png",
"logical_width": 8,
"logical_height": 16,
"nearest": true
},
"script": "scripts/weapon.rhai"
}
],
"items": [
{
"id": "seeker_swarm:equipment",
"name": "Seeker Swarm",
"price": 14000,
"max_count": 1,
"initial_count": 1,
"weapon": "seeker_swarm: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
},
"script": "scripts/equipment.rhai"
}
],
"sounds": [
{
"id": "seeker_swarm:fire",
"name": "Seeker Swarm Fire",
"file": "audio/fire.wav",
"volume": 92,
"pan": 127,
"pitch": 128,
"priority": 1
},
{
"id": "seeker_swarm:impact",
"name": "Seeker Swarm Impact",
"file": "audio/impact.wav",
"volume": 78,
"pan": 127,
"pitch": 128,
"priority": 2
}
]
}
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Reproduce local procedural weapon audio; WAV outputs are tracked with the package."""
import math
import struct
import wave
from pathlib import Path
PACKAGE = "seeker_swarm"
RATE = 22050
def render(kind):
duration = 0.13 if kind == "fire" else 0.17
count = int(RATE * duration)
phase = 0.0
seed = 0xA7C51D3
samples = []
for n in range(count):
t = n / count
seed ^= (seed << 13) & 0xffffffff
seed ^= seed >> 17
seed ^= (seed << 5) & 0xffffffff
seed &= 0xffffffff
noise = (seed / 2147483648.0) - 1.0
if PACKAGE == "pulse_cannon":
frequency = (1350 * (1 - t) + 190) if kind == "fire" else 130
tone = math.sin(phase) * 0.64 + noise * 0.22
elif PACKAGE == "seeker_swarm":
frequency = 330 + 850 * t if kind == "fire" else 95
tone = noise * 0.48 + math.sin(phase) * 0.35
else:
frequency = 1850 + 680 * t if kind == "fire" else 680 * (1 - t) + 110
tone = math.sin(phase) * 0.52 + math.sin(phase * 1.997) * 0.2 + noise * 0.06
phase += 2 * math.pi * frequency / RATE
attack = min(1.0, n / (RATE * 0.003))
envelope = attack * (1 - t) ** (3 if kind == "impact" else 2)
samples.append(round(max(-1, min(1, tone * envelope)) * 21000))
return struct.pack("<" + "h" * len(samples), *samples)
if __name__ == "__main__":
output = Path(__file__).resolve().parent / "audio"
output.mkdir(exist_ok=True)
for kind in ["fire", "impact"]:
with wave.open(str(output / (kind + ".wav")), "wb") as wav:
wav.setparams((1, 2, RATE, 0, "NONE", "not compressed"))
wav.writeframes(render(kind))
print(output / (kind + ".wav"))
+7
View File
@@ -0,0 +1,7 @@
id = "seeker_swarm"
name = "Seeker Swarm"
version = "1.0.0"
api_version = 1
description = "Naval science-fiction seeker swarm: independent weapon, equipment, SD/HD artwork and local firing/impact audio."
content = "content.json"
scripts = ["scripts/weapon.rhai", "scripts/equipment.rhai"]
+90
View File
@@ -0,0 +1,90 @@
//! Build game-ready logical and HD PNG canvases from the inspected generated sources.
//! Compile with the project's image dependency, then pass equipment source, shot source.
use image::{Rgba, RgbaImage, imageops};
use std::{env, fs, path::Path};
const PACKAGE: &str = "seeker_swarm";
const SHOT: (u32, u32) = (8, 16);
fn canvas(source: &RgbaImage, width: u32, height: u32, hd: bool) -> RgbaImage {
// Only the solid silhouette defines its fitting box; faint outer glows stay bounded.
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,
"source has no opaque silhouette"
);
let cropped = imageops::crop_imm(
source,
bounds.0,
bounds.1,
bounds.2 - bounds.0,
bounds.3 - bounds.1,
)
.to_image();
let margin = if hd { 4 } else { 1 };
let available = (
width.saturating_sub(margin * 2).max(1),
height.saturating_sub(margin * 2).max(1),
);
let factor = (available.0 as f64 / cropped.width() as f64)
.min(available.1 as f64 / cropped.height() as f64);
let size = (
(cropped.width() as f64 * factor).round().max(1.) as u32,
(cropped.height() as f64 * factor).round().max(1.) as u32,
);
let filter = if hd {
imageops::FilterType::Lanczos3
} else {
imageops::FilterType::Nearest
};
let mut scaled = imageops::resize(&cropped, size.0, size.1, filter);
for p in scaled.pixels_mut() {
if p[3] < (if hd { 64 } else { 128 }) {
*p = Rgba([0, 0, 0, 0]);
} else {
p[3] = 255;
}
}
let mut out = RgbaImage::new(width, height);
imageops::overlay(
&mut out,
&scaled,
((width - size.0) / 2) as i64,
((height - size.1) / 2) as i64,
);
out
}
fn main() {
let args: Vec<_> = env::args_os().collect();
assert_eq!(
args.len(),
4,
"usage: prepare_art EQUIPMENT_SOURCE SHOT_SOURCE PACKAGE_DIRECTORY"
);
let output = Path::new(&args[3]).join("art");
fs::create_dir_all(&output).unwrap();
for (kind, path, logical) in [
("equipment", &args[1], (16, 16)),
("projectile", &args[2], SHOT),
] {
let source = image::open(path).unwrap().into_rgba8();
fs::copy(path, output.join(format!("{kind}-generated-source.png"))).unwrap();
for (mode, scale) in [("sd", 1), ("hd", 4)] {
let image = canvas(&source, logical.0 * scale, logical.1 * scale, scale == 4);
let path = output.join(format!("{kind}-{mode}.png"));
image.save(&path).unwrap();
println!(
"{PACKAGE}: {}×{} {}",
image.width(),
image.height(),
path.display()
);
}
}
}
@@ -0,0 +1,6 @@
// Recovery is recorded separately from the weapon firing state.
fn on_pickup(event, state, random) {
let recovered = if state.contains("recovered") { state.recovered % 100000 + 1 } else { 1 };
#{ commands: [#{ op: "objective", key: "seeker_swarm:recovered", value: recovered }],
state: #{ recovered: recovered } }
}
@@ -0,0 +1,32 @@
// Built-in homing projectiles handle the normal volley and collision rules.
// Callbacks add deterministic visuals, audio and serializable weapon counters.
fn on_fire(event, state, random) {
let bursts = if state.contains("bursts") { state.bursts % 100000 + 1 } else { 1 };
let hits = if state.contains("hits") { state.hits } else { 0 };
let commands = [
#{ op: "play_sound", sound: "seeker_swarm:fire" },
#{ op: "effect", x: event.x, y: event.y - 3 }
];
#{ commands: commands, state: #{ bursts: bursts, hits: hits } }
}
fn on_hit(event, state, random) {
let bursts = if state.contains("bursts") { state.bursts } else { 0 };
let hits = if state.contains("hits") { state.hits % 100000 + 1 } else { 1 };
let commands = [
#{ op: "play_sound", sound: "seeker_swarm:impact" },
#{ op: "effect", x: event.x, y: event.y }
];
// A small fragmentation charge affects at most two nearby surviving enemies.
let fragments = 0;
for enemy in event.entities {
if fragments < 2 && enemy.entity != event.entity && enemy.health > 0
&& abs(enemy.x + enemy.width / 2 - event.x) <= 18
&& abs(enemy.y + enemy.height / 2 - event.y) <= 18 {
commands.push(#{ op: "damage", entity: enemy.entity, amount: 2 });
fragments += 1;
}
}
#{ commands: commands, state: #{ bursts: bursts, hits: hits } }
}