Add separate Iron Coast mission, weapon and enemy mods with SD HD artwork and audio
This commit is contained in:
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.
|
||||
@@ -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.
@@ -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"))
|
||||
@@ -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"]
|
||||
@@ -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 } }
|
||||
}
|
||||
Reference in new issue
Block a user