Make RustRaptor the repository root and track its playable bundle

This commit is contained in:
raptorreplica committed 2026-10-11 16:33:37 +11:00
1 parent 4b1edefeec
commit 8f459796b1
9860 files changed
+21084 -61019

No files matched your search

+7
View File
@@ -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 (285,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 mods --validate-mods seeker_swarm`. Read the shared [API documentation](../../docs/modding-api.md) for save pins, dependencies and sandbox limits.
+65
View File
@@ -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.
+76
View File
@@ -0,0 +1,76 @@
{
"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": 285000,
"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",
"description": "Selectable reusable special. Fires three homing missiles at air or ground enemies. Hits scatter fragments into up to two nearby enemies. Select it in Inventory or cycle specials, then use Fire or Special."
}
],
"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
}
]
}
+46
View File
@@ -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()
);
}
}
}
+6
View File
@@ -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 } }
}
+31
View File
@@ -0,0 +1,31 @@
// Built-in homing projectiles handle the normal volley and collision rules.
// Firing callbacks add audio and counters; impact callbacks add hit visuals.
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" }
];
#{ 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 } }
}