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 @@
|
||||
# ion_lance 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 4×32 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 @@
|
||||
# Ion Lance 1.0.0
|
||||
|
||||
Enable **Ion Lance** in Mods and apply at the main menu. This independent API 1 example has no dependencies. Buy its equipment in the shop (22,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: 8 damage, 12 logical pixels per step, 14 step cooldown, `beam`, air and ground targets. The narrow beam pierces every enemy in its column for two steps (16 total damage per enemy per trigger). 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 4×32 SD and 16×128 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 ion_lance`. Read the shared [API documentation](../../../docs/modding-api.md) for save pins, dependencies and sandbox limits.
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"package": "ion_lance",
|
||||
"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-2c5afac0-cc57-42f9-a0ca-f01125ba186a.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 Ion Lance: compact single long railgun barrel pointing exactly UP from an armored gray rectangular gun pod, two subtle electric-blue induction rails, red service panel. Top-down orthographic overhead view, precise thin symmetrical military silhouette. Isolated single object centered on genuinely transparent 1024x1024 square canvas, silhouette central 70% with12% transparentmargin. Match reference muted gunmetal, chunky readability, upper-left highlight. Restrained panel/machined detail forHD. No text, labels, outside glow/shadows, frame, border, extraobjects. Displaylogical16x16SDand64x64HD, so silhouette must survive reduction.",
|
||||
"sha256": "145215af5159eb196fc18c8c4f9f0e1c318ff552b9a83e4d8fc0d354309da4f5",
|
||||
"local_path": "art/equipment-generated-source.png"
|
||||
},
|
||||
{
|
||||
"kind": "projectile",
|
||||
"path": "/home/quintine/.codex/generated_images/01a12340-dd1f-71e1-869a-0c9f61a82318/exec-da141e14-efd2-4a36-a56f-bbc33225674f.png",
|
||||
"prompt": "Use case: stylized-concept. Asset type: ONE isolated ion beam pulse projectile sprite for a military science-fiction vertical shooter. Original input is STYLE reference only for readable chunky projectile palette. Generate a straight very narrow vertical electric blue/white ion lance beam traveling exactly UP: continuous thin pale core, bounded cobalt/cyan stepped edges, brightest pointed upper tip, slightly tapered bottom tail. No gun, no spacecraft, no extraneous light halo. Orthographic view, perfectly straight, centered, width:height1:8. Genuine transparent square canvas with generous sides and15%transparent top/bottom margin. Pixel-inspired military science-fiction HD detail, minimal colors, reference's crispbounded silhouette. No text, labels, shadows, frame, multiple objects. Intendedlogicalcanvas4x32 and16x128HD.",
|
||||
"sha256": "1a1c19b40354554658a4b72073ed7a7aec9a2bfb185c20d9654a29e2eef75614",
|
||||
"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": "145215af5159eb196fc18c8c4f9f0e1c318ff552b9a83e4d8fc0d354309da4f5",
|
||||
"bytes": 324909
|
||||
},
|
||||
"art/equipment-sd.png": {
|
||||
"sha256": "bb366d1604bd54079cb5e4c23f7e14aa9da1c9f494e66be1f9f3b870e0adec40",
|
||||
"bytes": 338
|
||||
},
|
||||
"art/equipment-hd.png": {
|
||||
"sha256": "66aa9a088453059518d4f76d961518edc577729018262d270bc6c3d806f665e4",
|
||||
"bytes": 2451
|
||||
},
|
||||
"art/projectile-generated-source.png": {
|
||||
"sha256": "1a1c19b40354554658a4b72073ed7a7aec9a2bfb185c20d9654a29e2eef75614",
|
||||
"bytes": 162173
|
||||
},
|
||||
"art/projectile-sd.png": {
|
||||
"sha256": "d8d2376739d39c343e9afc242b48956fcc8e48bc09fbc626360f8b3c4a52b730",
|
||||
"bytes": 308
|
||||
},
|
||||
"art/projectile-hd.png": {
|
||||
"sha256": "671cae899e2c5c9fd180663255915faaefe14267ddc46bd33ccd04dc51ba0dd4",
|
||||
"bytes": 1688
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"audio/fire.wav": {
|
||||
"sha256": "c155d02643f92f4e06373987d97728052617fca36d8ee9e712199078e89480a6",
|
||||
"bytes": 5776,
|
||||
"synthesis": "generate_audio.py PCM16 mono22050Hz"
|
||||
},
|
||||
"audio/impact.wav": {
|
||||
"sha256": "19b9be1a9fc960faa616a497535f5489c8d3021fc60f324aca9489c1be4a5ab6",
|
||||
"bytes": 7540,
|
||||
"synthesis": "generate_audio.py PCM16 mono22050Hz"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 338 B |
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 308 B |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "ion_lance:projectile",
|
||||
"name": "Ion Lance Projectile",
|
||||
"damage": 8,
|
||||
"speed": 12,
|
||||
"cooldown": 14,
|
||||
"behavior": "beam",
|
||||
"target": "all",
|
||||
"offsets": [
|
||||
[
|
||||
0,
|
||||
-4
|
||||
]
|
||||
],
|
||||
"art": {
|
||||
"png": "art/projectile-sd.png",
|
||||
"hd_png": "art/projectile-hd.png",
|
||||
"logical_width": 4,
|
||||
"logical_height": 32,
|
||||
"nearest": true
|
||||
},
|
||||
"script": "scripts/weapon.rhai"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "ion_lance:equipment",
|
||||
"name": "Ion Lance",
|
||||
"price": 22000,
|
||||
"max_count": 1,
|
||||
"initial_count": 1,
|
||||
"weapon": "ion_lance: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": "ion_lance:fire",
|
||||
"name": "Ion Lance Fire",
|
||||
"file": "audio/fire.wav",
|
||||
"volume": 92,
|
||||
"pan": 127,
|
||||
"pitch": 128,
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"id": "ion_lance:impact",
|
||||
"name": "Ion Lance 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 = "ion_lance"
|
||||
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 = "ion_lance"
|
||||
name = "Ion Lance"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
description = "Naval science-fiction ion lance: 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 = "ion_lance";
|
||||
const SHOT: (u32, u32) = (4, 32);
|
||||
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: "ion_lance:recovered", value: recovered }],
|
||||
state: #{ recovered: recovered } }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Built-in beam 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: "ion_lance: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: "ion_lance:impact" },
|
||||
#{ op: "effect", x: event.x, y: event.y }
|
||||
];
|
||||
#{ commands: commands, state: #{ bursts: bursts, hits: hits } }
|
||||
}
|
||||
Reference in new issue
Block a user