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 @@
# pulse_cannon 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×12 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 @@
# Pulse Cannon 1.0.0
Enable **Pulse Cannon** in Mods and apply at the main menu. This independent API 1 example has no dependencies. Buy its equipment in the shop (6,500 credits, maximum one) or collect its pickup. It is a primary upgrade and fires alongside the starter guns.
Projectile: 4 damage, 8 logical pixels per step, 9 step cooldown, `straight`, air and ground targets. Every third paired burst adds two angled shots using deterministic random values. 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×12 SD and 32×48 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 pulse_cannon`. Read the shared [API documentation](../../../docs/modding-api.md) for save pins, dependencies and sandbox limits.
@@ -0,0 +1,65 @@
{
"package": "pulse_cannon",
"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-663abae5-23a6-445b-888b-60b823c1c96a.png",
"prompt": "Use case: stylized-concept. Asset type: HD sprite for the equipment shop/inventory in a 1990s military science-fiction vertical shooter. Input image is an ORIGINAL STYLE REFERENCE ONLY, not an edit target. Generate a distinct Pulse Cannon: compact twin-barrel gun pod, squat armored gray metal body, restrained cyan capacitor accents and a red service panel. Top-down orthographic overhead view, barrel points exactly UP, no perspective tilt. Isolated single object centered on genuinely transparent 1024x1024 square canvas, silhouette occupying central 70% with at least 12% transparent margin. Style consistent with the reference: readable chunky military silhouette, muted gunmetal highlights, strongest light from upper-left, limited palette. Add restrained machined panel detail for HD view. No text, no labels, no glow outside silhouette, no ground shadow, no frames, no extra objects. Will be displayed at logical 16x16 pixels SD and 64x64 HD. Preserve clear shape when downsampled.",
"sha256": "01632fef820816f36ce56936e274c986e95d3be328e907b0a3ca812598265219",
"local_path": "art/equipment-generated-source.png"
},
{
"kind": "projectile",
"path": "/home/quintine/.codex/generated_images/01a12340-dd1f-71e1-869a-0c9f61a82318/exec-1c5c328f-a363-4b2a-9cf0-300f72806812.png",
"prompt": "Use case: stylized-concept. Asset type: isolated projectile sprite for a military science-fiction vertical shooter. Reference image is an ORIGINAL STYLE REFERENCE ONLY for chunky readable projectile pixels. Generate ONE short cyan-blue pulse bolt traveling exactly UP: rectangular bright pale core, stepped blue/cyan outer shell, compact tapered energy tail below. Orthographic straight-on top-down view, clean bounded silhouette, no weapon or spacecraft. Genuine transparent background. Tall narrow silhouette with width:height ratio 2:3, occupying central 65% height of a square canvas with large transparent margins. Restrained crisp pixel-inspired HD detailing matching 1990s Raptor, minimal colors, no expansive glow, no labels, no text, no shadow, no border, no multiple projectiles. Gamelogical canvas8x12 and32x48 HD.",
"sha256": "eac18f0856d0c4c75a9aa484a2c368bf70ccb2b7fb4358a603771253eea9ad7d",
"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": "01632fef820816f36ce56936e274c986e95d3be328e907b0a3ca812598265219",
"bytes": 595039
},
"art/equipment-sd.png": {
"sha256": "210a81f92cc9463577461df87e0d80f87402e0470837f70f52cca881d11eeaac",
"bytes": 615
},
"art/equipment-hd.png": {
"sha256": "ffc12be3d453065fe02c9796b8bae9dc3e23296bd9156b811a8e4b4c9ab1f791",
"bytes": 5286
},
"art/projectile-generated-source.png": {
"sha256": "eac18f0856d0c4c75a9aa484a2c368bf70ccb2b7fb4358a603771253eea9ad7d",
"bytes": 801668
},
"art/projectile-sd.png": {
"sha256": "ba31c5397043be8e8138e7574c08fdaf351a7e110bf83645b4394242819afc60",
"bytes": 240
},
"art/projectile-hd.png": {
"sha256": "a359c389c218189b7a9d07bcd9f53b8a5a1a002fb1175ad80a9ce33c06ad78dc",
"bytes": 992
}
},
"sounds": {
"audio/fire.wav": {
"sha256": "8cc128db1209e04734267e571acb7aef9dd1ba230f71d909eacf6a0ed272b903",
"bytes": 5776,
"synthesis": "generate_audio.py PCM16 mono22050Hz"
},
"audio/impact.wav": {
"sha256": "d5cbbeddb0fdaadfc89000e86bd01e22cff31766f188eb64fcf96056e23aca2e",
"bytes": 7540,
"synthesis": "generate_audio.py PCM16 mono22050Hz"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.
Binary file not shown.
+71
View File
@@ -0,0 +1,71 @@
{
"weapons": [
{
"id": "pulse_cannon:projectile",
"name": "Pulse Cannon Projectile",
"damage": 4,
"speed": 8,
"cooldown": 9,
"behavior": "straight",
"target": "all",
"offsets": [
[
-4,
-3
],
[
4,
-3
]
],
"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": "pulse_cannon:equipment",
"name": "Pulse Cannon",
"price": 6500,
"max_count": 1,
"initial_count": 1,
"weapon": "pulse_cannon:projectile",
"special": false,
"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": "pulse_cannon:fire",
"name": "Pulse Cannon Fire",
"file": "audio/fire.wav",
"volume": 92,
"pan": 127,
"pitch": 128,
"priority": 1
},
{
"id": "pulse_cannon:impact",
"name": "Pulse Cannon 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 = "pulse_cannon"
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 = "pulse_cannon"
name = "Pulse Cannon"
version = "1.0.0"
api_version = 1
description = "Naval science-fiction pulse cannon: 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 = "pulse_cannon";
const SHOT: (u32, u32) = (8, 12);
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: "pulse_cannon:recovered", value: recovered }],
state: #{ recovered: recovered } }
}
@@ -0,0 +1,28 @@
// Built-in straight 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: "pulse_cannon:fire" },
#{ op: "effect", x: event.x, y: event.y - 3 }
];
// Every third burst adds a deterministic pair of shallow angled pulses.
if bursts % 3 == 0 {
let spread = 1 + random[0] % 2;
commands.push(#{ op: "projectile", weapon: "pulse_cannon:projectile", x: event.x - 8, y: event.y - 3, vx: -spread, vy: -8 });
commands.push(#{ op: "projectile", weapon: "pulse_cannon:projectile", x: event.x + 8, y: event.y - 3, vx: spread, vy: -8 });
}
#{ 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: "pulse_cannon:impact" },
#{ op: "effect", x: event.x, y: event.y }
];
#{ commands: commands, state: #{ bursts: bursts, hits: hits } }
}