diff --git a/docs/iron-coast.md b/docs/iron-coast.md new file mode 100644 index 00000000..835958f0 --- /dev/null +++ b/docs/iron-coast.md @@ -0,0 +1,107 @@ +# Iron Coast content packages + +Iron Coast is a local campaign assembled from seven independently toggleable +mods. Enable all six dependencies before enabling the campaign package. The +Mods manager validates versions and dependency order and applies the change +at the main menu. The new weapons can also be used in the original campaigns; +the enemy packages can be referenced from other missions. + +| Package | Content | +| --- | --- | +| `iron_coast_mission_1` | Breakwater mission, campaign, supply depots, field repairs, terrain, briefing art and mission audio | +| `iron_coast_scout` | Airborne scout, weaving AI, projectiles, art and audio | +| `iron_coast_turret` | Ground turret, aimed firing, projectiles, art and audio | +| `iron_coast_boss` | Harbor command ship, persistent patrol and fan volleys, art and audio | +| `pulse_cannon` | Primary paired pulses and scripted burst rounds | +| `seeker_swarm` | Selectable homing missile volleys | +| `ion_lance` | Selectable penetrating beam | + +All packages are version 1.0.0 and use mod API 1. Mission 1 requires compatible +1.x versions of the other six. Save compatibility also records each enabled +package's exact version and content hash. + +## Mission 1: Breakwater + +The player raids a coastal industrial harbor. Ten scouts cover the approach, +four ground turrets protect the docks, two destructible supply depots release +repair kits, and the command ship arrives at step 720. Destroy at least two +turrets and the command ship to finish. The boss stays in the playfield until +killed; waiting out the mission duration cannot produce victory. + +The mission grants a trial Pulse Cannon, which fires alongside forward guns, +and three field repairs. Cycle equipment to **Iron Coast Field Repair** and +press the special button to restore 25 shield per repair. Depots drop extra +repairs. Victory pays 5,000 credits plus ordinary enemy rewards. The central +turrets and boss patrol overlap the starter-gun firing corridor; the mission +is intended to remain playable without the trial weapon. + +Script objectives are namespaced and saved with the pilot: + +| Objective | Meaning | +| --- | --- | +| `iron_coast_mission_1:phase` | 1 approach, 2 docks, 3 command ship, 4 complete | +| `iron_coast_mission_1:turrets_required` | Required count of two | +| `iron_coast_turret:defeated` | Turrets destroyed | +| `iron_coast_boss:defeated` | Command ships destroyed | +| `iron_coast_mission_1:depots_opened` | Supply depots destroyed | +| `iron_coast_mission_1:complete` | All required targets destroyed | + +## Art and audio + +Original terrain asset `0x10360` and military sprite `0x102f6` were decoded and +inspected before generating the new art with the built-in imagegen skill. +Every package includes generated masters, packaged SD/HD art, WAV files and +prompt/provenance metadata in Git. Original style-reference images and original +game data remain outside Git. Mission sources are a generated shoreline texture, +harbor briefing backdrop and transparent supply depot. The source images +remain unchanged; the build step normalizes logical footprints and derives +palette SD versions and HD counterparts. + +Mission tiles have 32×32 logical canvases, 32×32 palette SD images and 128×128 +HD images. The 288×384 logical shoreline map is converted globally before +cropping into 108 tiles. A reflected copy produces 216 definitions; twelve +forward rows followed by twelve reflected rows give exact matching pixels +at each vertical repetition boundary. This keeps the map continuous but +repeats and reflects port structures. The 72-row mission map uses three such +periods. This is a deliberate first-mission content choice, not unique terrain +for the whole original game. + +Briefing/menu art uses a 320×200 logical canvas, with 320×200 SD and 1280×800 +HD files. Supply depots preserve a 32×32 collision canvas and transparent +sprite margins; repair icons use 16×16 logical canvases. The asset report +records hashes, palette membership, footprints and repeat-boundary checks. +HD remains constrained by the renderer's indexed palette, so generated color +detail is reduced to the selected palette during play. + +Mission audio is original procedural synthesis: approach signal, dock alarm, +boss warning, victory arpeggio, depot impact and repair cue. These are bounded +mono PCM16 WAVs at 22,050 Hz, 0.35–1.2 seconds, made from deterministic +oscillators and seeded noise. No original sound recording, SoundFont or online +audio service is used. The generator and output hashes are tracked. + +The generated mod assets are included in Git, so a fresh checkout includes +the complete seven packages. The original base-game asset catalog is still +required locally to play the game and remains excluded from version control. +To rebuild the mission assets from their included generated masters, run: + +```sh +python3 examples/mods/iron_coast_mission_1/build_assets.py --palette data/misc/PALETTE_DAT +cargo run --no-default-features -- --mods examples/mods --validate-mods iron_coast_scout,iron_coast_turret,iron_coast_boss,pulse_cannon,seeker_swarm,ion_lance,iron_coast_mission_1 +``` + +## Acceptance checks + +`tests/iron_coast.rs` loads all seven real manifests, definitions and scripts +with synthetic PNG/WAV fixtures. It checks dependency activation, artwork +footprints, explicit boss/turret completion gating, one-time victory rewards, +and actual headless runs through the scout waves and boss using starter guns +and the trial Pulse Cannon. These tests use no original images and replace the +generated assets with synthetic fixtures. Local package validation additionally checks +the actual generated SD/HD images and WAV files. + +Visual approval remains provisional: sprite and terrain previews were +inspected for readability, canvas alignment and transparency, with numeric +palette and repeat-seam checks. Wider player testing should assess difficulty, +audio balance and HD details during normal play before treating the new art +as final. These packages extend the Rust engine; they do not establish full +parity across the original campaigns. diff --git a/examples/mods/ion_lance/ART_PROVENANCE.md b/examples/mods/ion_lance/ART_PROVENANCE.md new file mode 100644 index 00000000..dd5ba04a --- /dev/null +++ b/examples/mods/ion_lance/ART_PROVENANCE.md @@ -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. diff --git a/examples/mods/ion_lance/README.md b/examples/mods/ion_lance/README.md new file mode 100644 index 00000000..f08f7fe3 --- /dev/null +++ b/examples/mods/ion_lance/README.md @@ -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. diff --git a/examples/mods/ion_lance/art-provenance.json b/examples/mods/ion_lance/art-provenance.json new file mode 100644 index 00000000..e53d536f --- /dev/null +++ b/examples/mods/ion_lance/art-provenance.json @@ -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" + } + } +} diff --git a/examples/mods/ion_lance/art/equipment-generated-source.png b/examples/mods/ion_lance/art/equipment-generated-source.png new file mode 100644 index 00000000..8b916899 Binary files /dev/null and b/examples/mods/ion_lance/art/equipment-generated-source.png differ diff --git a/examples/mods/ion_lance/art/equipment-hd.png b/examples/mods/ion_lance/art/equipment-hd.png new file mode 100644 index 00000000..6a7ada74 Binary files /dev/null and b/examples/mods/ion_lance/art/equipment-hd.png differ diff --git a/examples/mods/ion_lance/art/equipment-sd.png b/examples/mods/ion_lance/art/equipment-sd.png new file mode 100644 index 00000000..2aab9b09 Binary files /dev/null and b/examples/mods/ion_lance/art/equipment-sd.png differ diff --git a/examples/mods/ion_lance/art/projectile-generated-source.png b/examples/mods/ion_lance/art/projectile-generated-source.png new file mode 100644 index 00000000..13d92e6b Binary files /dev/null and b/examples/mods/ion_lance/art/projectile-generated-source.png differ diff --git a/examples/mods/ion_lance/art/projectile-hd.png b/examples/mods/ion_lance/art/projectile-hd.png new file mode 100644 index 00000000..e3935ac9 Binary files /dev/null and b/examples/mods/ion_lance/art/projectile-hd.png differ diff --git a/examples/mods/ion_lance/art/projectile-sd.png b/examples/mods/ion_lance/art/projectile-sd.png new file mode 100644 index 00000000..870da198 Binary files /dev/null and b/examples/mods/ion_lance/art/projectile-sd.png differ diff --git a/examples/mods/ion_lance/audio/fire.wav b/examples/mods/ion_lance/audio/fire.wav new file mode 100644 index 00000000..7bb6a097 Binary files /dev/null and b/examples/mods/ion_lance/audio/fire.wav differ diff --git a/examples/mods/ion_lance/audio/impact.wav b/examples/mods/ion_lance/audio/impact.wav new file mode 100644 index 00000000..d711a23f Binary files /dev/null and b/examples/mods/ion_lance/audio/impact.wav differ diff --git a/examples/mods/ion_lance/content.json b/examples/mods/ion_lance/content.json new file mode 100644 index 00000000..ed13887c --- /dev/null +++ b/examples/mods/ion_lance/content.json @@ -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 + } + ] +} diff --git a/examples/mods/ion_lance/generate_audio.py b/examples/mods/ion_lance/generate_audio.py new file mode 100644 index 00000000..1a1d0411 --- /dev/null +++ b/examples/mods/ion_lance/generate_audio.py @@ -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")) diff --git a/examples/mods/ion_lance/mod.toml b/examples/mods/ion_lance/mod.toml new file mode 100644 index 00000000..ca56f1f7 --- /dev/null +++ b/examples/mods/ion_lance/mod.toml @@ -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"] diff --git a/examples/mods/ion_lance/prepare_art.rs b/examples/mods/ion_lance/prepare_art.rs new file mode 100644 index 00000000..853fc71b --- /dev/null +++ b/examples/mods/ion_lance/prepare_art.rs @@ -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() + ); + } + } +} diff --git a/examples/mods/ion_lance/scripts/equipment.rhai b/examples/mods/ion_lance/scripts/equipment.rhai new file mode 100644 index 00000000..a149c4d4 --- /dev/null +++ b/examples/mods/ion_lance/scripts/equipment.rhai @@ -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 } } +} diff --git a/examples/mods/ion_lance/scripts/weapon.rhai b/examples/mods/ion_lance/scripts/weapon.rhai new file mode 100644 index 00000000..4a1edd61 --- /dev/null +++ b/examples/mods/ion_lance/scripts/weapon.rhai @@ -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 } } +} diff --git a/examples/mods/iron_coast_boss/README.md b/examples/mods/iron_coast_boss/README.md new file mode 100644 index 00000000..abb46ed1 --- /dev/null +++ b/examples/mods/iron_coast_boss/README.md @@ -0,0 +1,17 @@ +# Iron Coast Command Cruiser + +Independent version 1.0.0 enemy package for mod API 1. Enable/disable it from the in-game Mods screen and use it in any mission through `iron_coast_boss:unit`. Iron Coast mission 1 supplies placements and completion objectives; this package does not add a mission or override base content. + +Airborne command cruiser enters at 2 pixels/tick, settles at y24 and patrols x80..144 indefinitely. It fires a fan of three 3 damage shells every 54 ticks. A standard pilot can destroy it with starter guns; it stays in their firing corridor. + +Logical collision canvas `96×64`, top-left anchor `(0,0)`, health `160`, reward `1800` credits, firing interval `54` simulation ticks. The engine halves boss health on difficulties 0/1; difficulty 2/3 uses the declared health. SD mode uses `art/unit-sd.png`; enable HD art in Settings for `art/unit-hd.png`. The HD canvas is 8× logical resolution and bypasses automatic ScaleNx. Projectile artwork has an 8×8 logical canvas and an independent 64×64 HD version. + +The boss uses two registered frames (idle/amber charged); the scout and turret use stable single frames. All supplied variants stay on the same canvas and anchor. `art-qa.json` records transparency bounds and, for the boss, animation silhouette overlap. + +Enemy deaths increment the current mission objective `iron_coast_boss:defeated`. Persistent script `destroyed`, `hits` and `shots` counters are statistics; current-mission objectives reset when the mission restarts. Callbacks guard retired entities and use deterministic simulation data. No wall clock or unseeded random input enters the scripts. + +Generated PNG masters, SD/HD strips, previews and WAV sounds are included in Git. Original reference images under `art/source/` are excluded. Exact built-in imagegen prompts are in `prompts/`, with source IDs and generated-image provenance in `provenance.json`. + +To rebuild derived artwork from supplied generated masters and regenerate audio, install Pillow and run `python3 build_assets.py`. To regenerate only audio with the Python standard library, run `python3 build_assets.py --audio-only`. WAVs are original procedural mono 16-bit PCM at 22050 Hz; `audio-recipe.json` records deterministic seeds, sizes and hashes. The art pipeline resizes and assembles supplied images; it does not redraw them. + +Validate from the repository root: `cargo run -- --mods examples/mods --validate-mods iron_coast_boss`. Synthetic behavior and save/replay tests: `cargo test --test enemies_mods`. Verify shipped artwork locally: `cargo test --test enemies_mods shipped_enemy_assets_are_valid_and_aligned`. diff --git a/examples/mods/iron_coast_boss/art-qa.json b/examples/mods/iron_coast_boss/art-qa.json new file mode 100644 index 00000000..596541c4 --- /dev/null +++ b/examples/mods/iron_coast_boss/art-qa.json @@ -0,0 +1,41 @@ +{ + "logical_size": [ + 96, + 64 + ], + "hd_scale": 8, + "origin": [ + 0, + 0 + ], + "frames": 2, + "frame_order": [ + "idle", + "charged" + ], + "alpha": "Preserved RGBA; runtime uses alpha>=128 indexed transparency", + "registration": "Common full-canvas resize and equal padding for every frame", + "sources": [ + { + "source": "art/unit-generated-v1.png", + "source_sha256": "87d644a5d584ad82e285241e3a5cdc58e5c61fa851415a4d6660e0ae83ec5e85", + "sd_opaque_bounds": [ + 25, + 3, + 71, + 61 + ] + }, + { + "source": "art/unit-charge-generated-v1.png", + "source_sha256": "cc8401140982260b4675a0a28fd209b8ed8d636e4cd4fa98a9cdf2fed37b1bff", + "sd_opaque_bounds": [ + 25, + 3, + 71, + 61 + ] + } + ], + "animation_silhouette_overlap": 0.9985000050675504 +} diff --git a/examples/mods/iron_coast_boss/art/preview.png b/examples/mods/iron_coast_boss/art/preview.png new file mode 100644 index 00000000..1555720b Binary files /dev/null and b/examples/mods/iron_coast_boss/art/preview.png differ diff --git a/examples/mods/iron_coast_boss/art/projectile-generated-v1.png b/examples/mods/iron_coast_boss/art/projectile-generated-v1.png new file mode 100644 index 00000000..b1d1da06 Binary files /dev/null and b/examples/mods/iron_coast_boss/art/projectile-generated-v1.png differ diff --git a/examples/mods/iron_coast_boss/art/projectile-hd.png b/examples/mods/iron_coast_boss/art/projectile-hd.png new file mode 100644 index 00000000..07fc070a Binary files /dev/null and b/examples/mods/iron_coast_boss/art/projectile-hd.png differ diff --git a/examples/mods/iron_coast_boss/art/projectile-sd.png b/examples/mods/iron_coast_boss/art/projectile-sd.png new file mode 100644 index 00000000..f3231717 Binary files /dev/null and b/examples/mods/iron_coast_boss/art/projectile-sd.png differ diff --git a/examples/mods/iron_coast_boss/art/unit-charge-generated-v1.png b/examples/mods/iron_coast_boss/art/unit-charge-generated-v1.png new file mode 100644 index 00000000..061d18dd Binary files /dev/null and b/examples/mods/iron_coast_boss/art/unit-charge-generated-v1.png differ diff --git a/examples/mods/iron_coast_boss/art/unit-generated-v1.png b/examples/mods/iron_coast_boss/art/unit-generated-v1.png new file mode 100644 index 00000000..84b1ffb6 Binary files /dev/null and b/examples/mods/iron_coast_boss/art/unit-generated-v1.png differ diff --git a/examples/mods/iron_coast_boss/art/unit-hd.png b/examples/mods/iron_coast_boss/art/unit-hd.png new file mode 100644 index 00000000..8234b63b Binary files /dev/null and b/examples/mods/iron_coast_boss/art/unit-hd.png differ diff --git a/examples/mods/iron_coast_boss/art/unit-sd.png b/examples/mods/iron_coast_boss/art/unit-sd.png new file mode 100644 index 00000000..c09a3d0b Binary files /dev/null and b/examples/mods/iron_coast_boss/art/unit-sd.png differ diff --git a/examples/mods/iron_coast_boss/audio-recipe.json b/examples/mods/iron_coast_boss/audio-recipe.json new file mode 100644 index 00000000..b2a57d16 --- /dev/null +++ b/examples/mods/iron_coast_boss/audio-recipe.json @@ -0,0 +1,23 @@ +{ + "generator": "build_assets.py deterministic chirp/noise synthesis; no recordings", + "kind": "boss", + "seed": 83, + "files": { + "audio/shot.wav": { + "sample_rate": 22050, + "channels": 1, + "bits": 16, + "samples": 5292, + "peak": 25558, + "sha256": "32d2e5ea26a9333f8163f7653c9b5a7dc00cf16ac9956f4abfbf32802ca459a7" + }, + "audio/death.wav": { + "sample_rate": 22050, + "channels": 1, + "bits": 16, + "samples": 19845, + "peak": 25558, + "sha256": "3bf364c2fa21dbaca81af03e9d2e19552ede430912e1bd4fd967da0611a0c438" + } + } +} diff --git a/examples/mods/iron_coast_boss/audio/death.wav b/examples/mods/iron_coast_boss/audio/death.wav new file mode 100644 index 00000000..e60b8402 Binary files /dev/null and b/examples/mods/iron_coast_boss/audio/death.wav differ diff --git a/examples/mods/iron_coast_boss/audio/shot.wav b/examples/mods/iron_coast_boss/audio/shot.wav new file mode 100644 index 00000000..1903eb98 Binary files /dev/null and b/examples/mods/iron_coast_boss/audio/shot.wav differ diff --git a/examples/mods/iron_coast_boss/build_assets.py b/examples/mods/iron_coast_boss/build_assets.py new file mode 100644 index 00000000..496fcc88 --- /dev/null +++ b/examples/mods/iron_coast_boss/build_assets.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Convert saved imagegen artwork and synthesize reproducible package-local WAVs. + +This script resizes/atlases supplied art; it never redraws source or generated art. +Generated source PNGs stay in ignored art/. See prompts/ and provenance.json. +Use --audio-only to regenerate sound without Pillow or private artwork. +""" +import argparse +import hashlib +import json +import math +from pathlib import Path +import random +import struct +import wave + + +def sounds(root, kind): + rate = 22050 + values = { + "scout": (0.11, 1400, 380, 0.34, 230, 19), + "turret": (0.18, 900, 160, 0.52, 150, 37), + "boss": (0.24, 500, 80, 0.90, 75, 83), + } + shot_length, start, end, death_length, bass, seed = values[kind] + (root / "audio").mkdir(exist_ok=True) + stats = {} + for name, duration in [("shot", shot_length), ("death", death_length)]: + rng = random.Random(seed + (100 if name == "death" else 0)) + count = int(duration * rate) + phase = low_noise = 0.0 + samples = [] + for index in range(count): + t = index / rate + position = index / max(1, count - 1) + noise = rng.uniform(-1, 1) + low_noise = low_noise * 0.86 + noise * 0.14 + if name == "shot": + frequency = end + (start - end) * (1 - position) ** 2 + phase += 2 * math.pi * frequency / rate + value = (0.60 * math.sin(phase) + 0.15 * math.sin(2.31 * phase) + + 0.16 * noise + 0.16 * math.sin(2 * math.pi * bass * t)) + envelope = min(1, t / 0.002) * math.exp(-position * 6.0) + else: + frequency = bass * (1.0 - position * 0.65) + phase += 2 * math.pi * frequency / rate + value = 0.62 * low_noise + 0.19 * noise + 0.28 * math.sin(phase) + envelope = min(1, t / 0.003) * math.exp(-position * 4.7) + # Five milliseconds at the tail avoid discontinuities at mixer retirement. + envelope *= min(1, (count - 1 - index) / (rate * 0.005)) + samples.append(value * max(0, envelope)) + peak = max(abs(value) for value in samples) + pcm = [round(value / peak * 0.78 * 32767) for value in samples] + path = root / "audio" / (name + ".wav") + with wave.open(str(path), "wb") as output: + output.setnchannels(1) + output.setsampwidth(2) + output.setframerate(rate) + output.writeframes(struct.pack("<" + "h" * len(pcm), *pcm)) + stats[str(path.relative_to(root))] = { + "sample_rate": rate, "channels": 1, "bits": 16, + "samples": count, "peak": max(abs(value) for value in pcm), + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + (root / "audio-recipe.json").write_text(json.dumps({ + "generator": "build_assets.py deterministic chirp/noise synthesis; no recordings", + "kind": kind, "seed": seed, "files": stats, + }, indent=2) + "\n") + + +def artwork(root, kind): + from PIL import Image + dimensions = {"scout": (24, 24), "turret": (32, 32), "boss": (96, 64)} + width, height = dimensions[kind] + paths = [root / "art/unit-generated-v1.png"] + if kind == "boss": + paths.append(root / "art/unit-charge-generated-v1.png") + hd_frames, sd_frames = [], [] + stats = [] + for path in paths: + source = Image.open(path).convert("RGBA") + if source.width * height != source.height * width: + raise ValueError(f"{path}: source canvas aspect differs from logical canvas") + # Identical canvas resize and padding for all frames retain their registration. + hd = Image.new("RGBA", (width * 8, height * 8), (0, 0, 0, 0)) + pad_x, pad_y = width // 3, height // 3 + resized = source.resize((hd.width - 2 * pad_x, hd.height - 2 * pad_y), Image.Resampling.LANCZOS) + hd.alpha_composite(resized, (pad_x, pad_y)) + sd = hd.resize((width, height), Image.Resampling.BOX) + for image in [hd, sd]: + bbox = image.getchannel("A").point(lambda value: 255 if value >= 128 else 0).getbbox() + if bbox is None or bbox[0] == 0 or bbox[1] == 0 or bbox[2] == image.width or bbox[3] == image.height: + raise ValueError(f"{path}: opaque silhouette touches logical canvas edge") + hd_frames.append(hd) + sd_frames.append(sd) + stats.append({"source": str(path.relative_to(root)), + "source_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "sd_opaque_bounds": list(sd.getchannel("A").point(lambda value: 255 if value >= 128 else 0).getbbox())}) + for name, frames in [("unit-hd.png", hd_frames), ("unit-sd.png", sd_frames)]: + atlas = Image.new("RGBA", (frames[0].width * len(frames), frames[0].height), (0, 0, 0, 0)) + for index, frame in enumerate(frames): + atlas.alpha_composite(frame, (index * frame.width, 0)) + atlas.save(root / "art" / name) + # Shared visual ammunition language; each package includes its own local copy. + source = Image.open(root / "art/projectile-generated-v1.png").convert("RGBA") + hd = source.resize((64, 64), Image.Resampling.LANCZOS) + hd.save(root / "art/projectile-hd.png") + hd.resize((8, 8), Image.Resampling.BOX).save(root / "art/projectile-sd.png") + metadata = {"logical_size": [width, height], "hd_scale": 8, "origin": [0, 0], + "frames": len(paths), "frame_order": ["idle", "charged"][:len(paths)], + "alpha": "Preserved RGBA; runtime uses alpha>=128 indexed transparency", + "registration": "Common full-canvas resize and equal padding for every frame", + "sources": stats} + if len(hd_frames) == 2: + masks = [f.getchannel("A").point(lambda value: 255 if value >= 128 else 0) for f in hd_frames] + pairs = list(zip(masks[0].get_flattened_data(), masks[1].get_flattened_data())) + metadata["animation_silhouette_overlap"] = sum(bool(a and b) for a, b in pairs) / sum(bool(a or b) for a, b in pairs) + if metadata["animation_silhouette_overlap"] < 0.99: + raise ValueError("generated animation changes the silhouette beyond accepted registration tolerance") + (root / "art-qa.json").write_text(json.dumps(metadata, indent=2) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audio-only", action="store_true") + args = parser.parse_args() + root = Path(__file__).resolve().parent + kind = root.name.removeprefix("iron_coast_") + sounds(root, kind) + if not args.audio_only: + artwork(root, kind) + print(f"Generated {root.name} package assets") + + +if __name__ == "__main__": + main() diff --git a/examples/mods/iron_coast_boss/content.json b/examples/mods/iron_coast_boss/content.json new file mode 100644 index 00000000..7aa7f2d6 --- /dev/null +++ b/examples/mods/iron_coast_boss/content.json @@ -0,0 +1,59 @@ +{ + "weapons": [ + { + "id": "iron_coast_boss:projectile", + "name": "Iron Coast Command Cruiser cannon shell", + "damage": 3, + "speed": 4, + "cooldown": 54, + "behavior": "straight", + "target": "all", + "art": { + "png": "art/projectile-sd.png", + "hd_png": "art/projectile-hd.png", + "logical_width": 8, + "logical_height": 8 + } + } + ], + "enemies": [ + { + "id": "iron_coast_boss:unit", + "name": "Iron Coast Command Cruiser", + "health": 160, + "speed": 0, + "reward": 1800, + "ground": false, + "boss": true, + "cooldown": 54, + "art": { + "png": "art/unit-sd.png", + "hd_png": "art/unit-hd.png", + "logical_width": 96, + "logical_height": 64, + "frames": 2 + }, + "script": "scripts/ai.rhai" + } + ], + "sounds": [ + { + "id": "iron_coast_boss:shot", + "name": "Iron Coast Command Cruiser cannon", + "file": "audio/shot.wav", + "volume": 95, + "pan": 127, + "pitch": 128, + "priority": 2 + }, + { + "id": "iron_coast_boss:death", + "name": "Iron Coast Command Cruiser destruction", + "file": "audio/death.wav", + "volume": 120, + "pan": 127, + "pitch": 128, + "priority": 4 + } + ] +} diff --git a/examples/mods/iron_coast_boss/mod.toml b/examples/mods/iron_coast_boss/mod.toml new file mode 100644 index 00000000..602b7b9b --- /dev/null +++ b/examples/mods/iron_coast_boss/mod.toml @@ -0,0 +1,7 @@ +id = "iron_coast_boss" +name = "Iron Coast Command Cruiser" +version = "1.0.0" +api_version = 1 +description = "A persistent patrol boss with a three-shell cannon fan. Includes SD and HD artwork, deterministic Rhai AI and generated sound effects." +content = "content.json" +scripts = ["scripts/ai.rhai"] diff --git a/examples/mods/iron_coast_boss/prompts/projectile-hd-v1.txt b/examples/mods/iron_coast_boss/prompts/projectile-hd-v1.txt new file mode 100644 index 00000000..92306a65 --- /dev/null +++ b/examples/mods/iron_coast_boss/prompts/projectile-hd-v1.txt @@ -0,0 +1,6 @@ +Use case: style-transfer +Asset type: transparent HD projectile sprite for hostile cannon fire in a top-down military science-fiction shoot-em-up. +Input: original Raptor ESHOT_BLK 8×8 amber projectile, enlarged nearest-neighbor, is a reference for color and readability. +Primary request: a single small compact circular amber/orange energy shell viewed directly overhead, bright pale-yellow center fading to orange metal/plasma rim, dark crisp outer edge. Match the naval steel/amber Raptor remaster style. It will be reduced to an 8×8 logical square, so keep a simple readable round footprint and avoid microdetails. This is one ammunition projectile, no weapon or spacecraft. +Composition: perfectly centered isolated circular sprite on square canvas, visible orb occupies about62% width/height with equal large transparent margins. Keep all glow contained inside the circular orb, no external haze, shadow, trail or rays. Do not stretch it. +Constraints: true transparent background, no extra objects, no checkerboard or black background baked into pixels, no text, logos, watermarks, perspective or decorative framing. diff --git a/examples/mods/iron_coast_boss/prompts/unit-charge-v1.txt b/examples/mods/iron_coast_boss/prompts/unit-charge-v1.txt new file mode 100644 index 00000000..952025a8 --- /dev/null +++ b/examples/mods/iron_coast_boss/prompts/unit-charge-v1.txt @@ -0,0 +1,3 @@ +Use case: precise-object-edit +Asset type: second animation frame of the Iron Coast Command Cruiser boss HD sprite. +Input image is the completed sprite edit target. Create a charged-weapons variant changing ONLY the existing small amber lamps and the two central circular side cannon wells: slightly brighten those lamps and illuminate inside each cannon well with a compact amber/orange core. Keep every hull panel, prong, silhouette edge, x/y anchor, exact 3:2 image dimensions, and camera view pixel-aligned with the input. No new external glow, muzzle flash, particles, smoke or shadow. Preserve the exact transparent background and empty side margins. Add a thin transparent margin around top/bottom if necessary to avoid edge clipping, but do not shift or reshape the vessel. Do not render a background, text, logos or watermarks. Single isolated overhead sprite facing DOWN. diff --git a/examples/mods/iron_coast_boss/prompts/unit-hd-v1.txt b/examples/mods/iron_coast_boss/prompts/unit-hd-v1.txt new file mode 100644 index 00000000..9f2c3816 --- /dev/null +++ b/examples/mods/iron_coast_boss/prompts/unit-hd-v1.txt @@ -0,0 +1,6 @@ +Use case: style-transfer +Asset type: transparent HD enemy boss sprite for a top-down vertical shoot-em-up, Iron Coast Command Cruiser. +Input image: original Raptor SHIP32G1_PIC (96×64 logical canvas, enlarged nearest-neighbor) is the source of truth for silhouette, proportions, center anchoring and DOWNWARD orientation. +Primary request: regenerate the pictured large military science-fiction coastal command cruiser as clean HD game art. Preserve the original very specific shape: tall central armored hull oriented vertically nose DOWN, twin small stern engine towers at the TOP, wide lateral blocky wing/weapon platforms across the upper-middle, a central bridge/reactor block, and two prongs flanking the narrow lower bow. Keep all sections recognizable and in the same positions. Heavy naval steel/slate plates, subtle olive and brown industrial accents, bolted hatches, narrow amber status lamps, small red service panels. Original utilitarian Raptor overhead sprite style with sharper surface detail. It is one aircraft/hover cruiser rather than a ship floating in water. +Composition: a WIDE 3:2 transparent canvas corresponding exactly to96×64 logical pixels. Full perfectly top-down view without perspective. Cruiser centered on x48,y32. Keep original transparent side margins: actual vessel about55% of canvas width and94% of canvas height, with no clipping or cropped tips. Bow points downward. Do not rotate or enlarge the vessel to fill the side margins. Gentle light from upper-left, crisp silhouette, no external shadow. +Constraints: one isolated single sprite, true transparent background. No sea, terrain, floor, wake, exhaust trails, motion blur, text, logos, watermark, smoke, UI, borders or painted checkerboard. Preserve the attached silhouette and exact canvas proportions. diff --git a/examples/mods/iron_coast_boss/provenance.json b/examples/mods/iron_coast_boss/provenance.json new file mode 100644 index 00000000..d840a8a4 --- /dev/null +++ b/examples/mods/iron_coast_boss/provenance.json @@ -0,0 +1,36 @@ +{ + "api_version": 1, + "mod_version": "1.0.0", + "tool": "built-in image_gen.imagegen via imagegen skill", + "skill": "/home/quintine/.codex/skills/.system/imagegen/SKILL.md", + "generated_at": "2026-10-10", + "source_reference": { + "asset_id_hex": "10335", + "catalog_path": "sprites/SHIP32G1_PIC", + "sha256": "640ed8f1d45ead01337ce2ed95a1be1b44f88fe0cfecdc61e98537093d1e9324", + "role": "Original silhouette/orientation/style reference inspected before imagegen. Original reference pixels excluded from Git under art/source/." + }, + "assets": [ + { + "file": "art/unit-generated-v1.png", + "prompt": "prompts/unit-hd-v1.txt", + "tool_output": "exec-3e7092b2-f469-4d4d-9eee-c7d760172125.png", + "sha256": "87d644a5d584ad82e285241e3a5cdc58e5c61fa851415a4d6660e0ae83ec5e85" + }, + { + "file": "art/unit-charge-generated-v1.png", + "prompt": "prompts/unit-charge-v1.txt", + "tool_output": "exec-7a5f11a2-bb90-4007-81c1-865e2058f912.png", + "sha256": "cc8401140982260b4675a0a28fd209b8ed8d636e4cd4fa98a9cdf2fed37b1bff" + }, + { + "file": "art/projectile-generated-v1.png", + "prompt": "prompts/projectile-hd-v1.txt", + "tool_output": "exec-96076703-18b2-4aad-9bce-4eaceef4d8bd.png", + "note": "Common generated amber projectile copied into each independent package.", + "sha256": "386709d83ebf8a9e6c5119c785afc77152ae934780b3b0afcf8ede510622bc20" + } + ], + "pipeline": "build_assets.py: common full-canvas resize/padding, alpha-preserving RGBA SD/8xHD strips; no bitmap redraw", + "version_control": "Generated masters, shipped SD/HD artwork, previews and synthesized WAVs included in Git per user request. Original catalog reference pixels excluded." +} diff --git a/examples/mods/iron_coast_boss/scripts/ai.rhai b/examples/mods/iron_coast_boss/scripts/ai.rhai new file mode 100644 index 00000000..e017efae --- /dev/null +++ b/examples/mods/iron_coast_boss/scripts/ai.rhai @@ -0,0 +1,56 @@ +fn is_alive(event) { + for unit in event.entities { + if unit.entity == event.entity && unit.health > 0 { return true; } + } + false +} + +// Per-entity directions permit multiple cruisers without sharing a patrol cursor. +fn on_spawn(event, state, random) { + if !is_alive(event) { return #{ commands: [], state: state }; } + if !state.contains("directions") { state.directions = #{}; } + state.directions[event.entity.to_string()] = 1; + #{ commands: [#{ op: "velocity", entity: event.entity, vx: 0, vy: 2 }], state: state } +} + +fn on_tick(event, state, random) { + if event.health <= 0 || !is_alive(event) { return #{ commands: [], state: state }; } + if event.y < 24 { + return #{ commands: [#{ op: "velocity", entity: event.entity, vx: 0, vy: 2 }], state: state }; + } + let key = event.entity.to_string(); + if !state.contains("directions") { state.directions = #{}; } + let direction = if state.directions.contains(key) { state.directions[key] } else { 1 }; + if event.x >= 144 { direction = -1; } + if event.x <= 80 { direction = 1; } + state.directions[key] = direction; + let commands = [ + #{ op: "move", entity: event.entity, x: event.x, y: 24 }, + #{ op: "velocity", entity: event.entity, vx: direction, vy: 0 } + ]; + if event.tick % 54 == 0 { + commands.push(#{ op: "projectile", weapon: "iron_coast_boss:projectile", x: event.x + 35, y: 64, vx: -2, vy: 4, hostile: true }); + commands.push(#{ op: "projectile", weapon: "iron_coast_boss:projectile", x: event.x + 48, y: 64, vx: 0, vy: 4, hostile: true }); + commands.push(#{ op: "projectile", weapon: "iron_coast_boss:projectile", x: event.x + 61, y: 64, vx: 2, vy: 4, hostile: true }); + commands.push(#{ op: "play_sound", sound: "iron_coast_boss:shot" }); + state.shots = if state.contains("shots") { state.shots + 1 } else { 1 }; + } + #{ commands: commands, state: state } +} + +fn on_hit(event, state, random) { + state.hits = if state.contains("hits") { state.hits + 1 } else { 1 }; + #{ commands: [], state: state } +} + +fn on_death(event, state, random) { + if state.contains("directions") { state.directions.remove(event.entity.to_string()); } + state.destroyed = if state.contains("destroyed") { state.destroyed + 1 } else { 1 }; + let defeated = if event.objectives.contains("iron_coast_boss:defeated") { event.objectives["iron_coast_boss:defeated"] + 1 } else { 1 }; + #{ commands: [ + #{ op: "objective", key: "iron_coast_boss:defeated", value: defeated }, + #{ op: "effect", x: event.x + 35, y: event.y + 36 }, + #{ op: "effect", x: event.x + 61, y: event.y + 36 }, + #{ op: "play_sound", sound: "iron_coast_boss:death" } + ], state: state } +} diff --git a/examples/mods/iron_coast_mission_1/README.md b/examples/mods/iron_coast_mission_1/README.md new file mode 100644 index 00000000..04fd3633 --- /dev/null +++ b/examples/mods/iron_coast_mission_1/README.md @@ -0,0 +1,29 @@ +# Iron Coast — Mission 1: Breakwater + +Enable this package and its six dependencies in the in-game Mods screen: +`iron_coast_scout`, `iron_coast_turret`, `iron_coast_boss`, `pulse_cannon`, +`seeker_swarm`, and `ion_lance`. Each package is independent and version 1.0.0. +Choose **Iron Coast 1 — Breakwater** in the mission list. + +Disable at least two harbor turrets and destroy the command ship to complete +the raid. Scout waves arrive during the approach; two supply depots release +repair kits. The mission grants a trial Pulse Cannon and three field repairs. +Repairs restore 25 shield when selected and used with the special button. +Victory awards 5,000 credits in addition to ordinary destruction rewards. + +This mission uses explicit script completion. Its duration is a catalog hint; +waiting out the timer cannot win. The boss stays in the firing corridor, and +the two central turrets can be hit with starter forward guns. Equipment and +mission progress remain subject to the normal mod-save compatibility checks. + +The generated masters, packaged SD/HD PNGs and synthesized WAVs are included +in Git with their definitions, scripts, generator, prompts and validation +report. Original game data and the original style-reference PNGs remain +outside Git. The three +`art/*-source-hd.png` inputs were generated with the built-in imagegen tool +using the original terrain and military sprite as style/scale references. +Run `python3 examples/mods/iron_coast_mission_1/build_assets.py --palette +data/misc/PALETTE_DAT` to regenerate the exact +packaged crops, SD palette conversion, HD footprints and synthesized WAVs. +See `prompts.md`, `asset-report.json`, and `docs/iron-coast.md` for provenance +and validation limits. diff --git a/examples/mods/iron_coast_mission_1/art/briefing-source-hd.png b/examples/mods/iron_coast_mission_1/art/briefing-source-hd.png new file mode 100644 index 00000000..f2c6b903 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/briefing-source-hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/briefing_hd.png b/examples/mods/iron_coast_mission_1/art/briefing_hd.png new file mode 100644 index 00000000..05968748 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/briefing_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/briefing_sd.png b/examples/mods/iron_coast_mission_1/art/briefing_sd.png new file mode 100644 index 00000000..1d79075f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/briefing_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/depot-source-hd.png b/examples/mods/iron_coast_mission_1/art/depot-source-hd.png new file mode 100644 index 00000000..55a7d31e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/depot-source-hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/depot_hd.png b/examples/mods/iron_coast_mission_1/art/depot_hd.png new file mode 100644 index 00000000..5afa8242 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/depot_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/depot_sd.png b/examples/mods/iron_coast_mission_1/art/depot_sd.png new file mode 100644 index 00000000..c04ee647 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/depot_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/repair_hd.png b/examples/mods/iron_coast_mission_1/art/repair_hd.png new file mode 100644 index 00000000..e1137c28 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/repair_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/repair_sd.png b/examples/mods/iron_coast_mission_1/art/repair_sd.png new file mode 100644 index 00000000..acb8b876 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/repair_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/terrain-preview-hd.png b/examples/mods/iron_coast_mission_1/art/terrain-preview-hd.png new file mode 100644 index 00000000..d82d21e4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/terrain-preview-hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/terrain-preview-sd.png b/examples/mods/iron_coast_mission_1/art/terrain-preview-sd.png new file mode 100644 index 00000000..53d93472 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/terrain-preview-sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/terrain-source-hd.png b/examples/mods/iron_coast_mission_1/art/terrain-source-hd.png new file mode 100644 index 00000000..bb5abc1f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/terrain-source-hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_000_hd.png b/examples/mods/iron_coast_mission_1/art/tile_000_hd.png new file mode 100644 index 00000000..becba564 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_000_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_000_sd.png b/examples/mods/iron_coast_mission_1/art/tile_000_sd.png new file mode 100644 index 00000000..23fe19ba Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_000_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_001_hd.png b/examples/mods/iron_coast_mission_1/art/tile_001_hd.png new file mode 100644 index 00000000..f5345736 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_001_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_001_sd.png b/examples/mods/iron_coast_mission_1/art/tile_001_sd.png new file mode 100644 index 00000000..0c4d23ee Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_001_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_002_hd.png b/examples/mods/iron_coast_mission_1/art/tile_002_hd.png new file mode 100644 index 00000000..6e81b21e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_002_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_002_sd.png b/examples/mods/iron_coast_mission_1/art/tile_002_sd.png new file mode 100644 index 00000000..cca28bec Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_002_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_003_hd.png b/examples/mods/iron_coast_mission_1/art/tile_003_hd.png new file mode 100644 index 00000000..e1a76515 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_003_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_003_sd.png b/examples/mods/iron_coast_mission_1/art/tile_003_sd.png new file mode 100644 index 00000000..9630c580 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_003_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_004_hd.png b/examples/mods/iron_coast_mission_1/art/tile_004_hd.png new file mode 100644 index 00000000..25880e54 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_004_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_004_sd.png b/examples/mods/iron_coast_mission_1/art/tile_004_sd.png new file mode 100644 index 00000000..e571d3b4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_004_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_005_hd.png b/examples/mods/iron_coast_mission_1/art/tile_005_hd.png new file mode 100644 index 00000000..4b114330 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_005_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_005_sd.png b/examples/mods/iron_coast_mission_1/art/tile_005_sd.png new file mode 100644 index 00000000..666cb867 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_005_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_006_hd.png b/examples/mods/iron_coast_mission_1/art/tile_006_hd.png new file mode 100644 index 00000000..e1ef0366 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_006_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_006_sd.png b/examples/mods/iron_coast_mission_1/art/tile_006_sd.png new file mode 100644 index 00000000..26178ede Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_006_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_007_hd.png b/examples/mods/iron_coast_mission_1/art/tile_007_hd.png new file mode 100644 index 00000000..62f64195 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_007_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_007_sd.png b/examples/mods/iron_coast_mission_1/art/tile_007_sd.png new file mode 100644 index 00000000..7ba3b543 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_007_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_008_hd.png b/examples/mods/iron_coast_mission_1/art/tile_008_hd.png new file mode 100644 index 00000000..6f133abc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_008_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_008_sd.png b/examples/mods/iron_coast_mission_1/art/tile_008_sd.png new file mode 100644 index 00000000..c980b909 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_008_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_009_hd.png b/examples/mods/iron_coast_mission_1/art/tile_009_hd.png new file mode 100644 index 00000000..1cd54cb0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_009_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_009_sd.png b/examples/mods/iron_coast_mission_1/art/tile_009_sd.png new file mode 100644 index 00000000..2cd40194 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_009_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_010_hd.png b/examples/mods/iron_coast_mission_1/art/tile_010_hd.png new file mode 100644 index 00000000..0879a819 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_010_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_010_sd.png b/examples/mods/iron_coast_mission_1/art/tile_010_sd.png new file mode 100644 index 00000000..9c111055 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_010_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_011_hd.png b/examples/mods/iron_coast_mission_1/art/tile_011_hd.png new file mode 100644 index 00000000..816c9538 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_011_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_011_sd.png b/examples/mods/iron_coast_mission_1/art/tile_011_sd.png new file mode 100644 index 00000000..ce021f9a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_011_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_012_hd.png b/examples/mods/iron_coast_mission_1/art/tile_012_hd.png new file mode 100644 index 00000000..60f7e30a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_012_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_012_sd.png b/examples/mods/iron_coast_mission_1/art/tile_012_sd.png new file mode 100644 index 00000000..0cf6fc35 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_012_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_013_hd.png b/examples/mods/iron_coast_mission_1/art/tile_013_hd.png new file mode 100644 index 00000000..e6eeb717 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_013_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_013_sd.png b/examples/mods/iron_coast_mission_1/art/tile_013_sd.png new file mode 100644 index 00000000..e1e84bfd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_013_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_014_hd.png b/examples/mods/iron_coast_mission_1/art/tile_014_hd.png new file mode 100644 index 00000000..8f11444c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_014_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_014_sd.png b/examples/mods/iron_coast_mission_1/art/tile_014_sd.png new file mode 100644 index 00000000..0a929d60 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_014_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_015_hd.png b/examples/mods/iron_coast_mission_1/art/tile_015_hd.png new file mode 100644 index 00000000..bd561a7e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_015_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_015_sd.png b/examples/mods/iron_coast_mission_1/art/tile_015_sd.png new file mode 100644 index 00000000..ace6794e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_015_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_016_hd.png b/examples/mods/iron_coast_mission_1/art/tile_016_hd.png new file mode 100644 index 00000000..02a8cddc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_016_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_016_sd.png b/examples/mods/iron_coast_mission_1/art/tile_016_sd.png new file mode 100644 index 00000000..9ad5e25f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_016_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_017_hd.png b/examples/mods/iron_coast_mission_1/art/tile_017_hd.png new file mode 100644 index 00000000..4eb4b548 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_017_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_017_sd.png b/examples/mods/iron_coast_mission_1/art/tile_017_sd.png new file mode 100644 index 00000000..50485e18 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_017_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_018_hd.png b/examples/mods/iron_coast_mission_1/art/tile_018_hd.png new file mode 100644 index 00000000..1f5cfb25 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_018_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_018_sd.png b/examples/mods/iron_coast_mission_1/art/tile_018_sd.png new file mode 100644 index 00000000..4df338fc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_018_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_019_hd.png b/examples/mods/iron_coast_mission_1/art/tile_019_hd.png new file mode 100644 index 00000000..fd50b9e6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_019_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_019_sd.png b/examples/mods/iron_coast_mission_1/art/tile_019_sd.png new file mode 100644 index 00000000..3c1ba136 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_019_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_020_hd.png b/examples/mods/iron_coast_mission_1/art/tile_020_hd.png new file mode 100644 index 00000000..702dcb24 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_020_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_020_sd.png b/examples/mods/iron_coast_mission_1/art/tile_020_sd.png new file mode 100644 index 00000000..fe8556d6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_020_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_021_hd.png b/examples/mods/iron_coast_mission_1/art/tile_021_hd.png new file mode 100644 index 00000000..d47517bb Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_021_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_021_sd.png b/examples/mods/iron_coast_mission_1/art/tile_021_sd.png new file mode 100644 index 00000000..2ac22fb6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_021_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_022_hd.png b/examples/mods/iron_coast_mission_1/art/tile_022_hd.png new file mode 100644 index 00000000..14d28429 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_022_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_022_sd.png b/examples/mods/iron_coast_mission_1/art/tile_022_sd.png new file mode 100644 index 00000000..ea71f359 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_022_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_023_hd.png b/examples/mods/iron_coast_mission_1/art/tile_023_hd.png new file mode 100644 index 00000000..806b104d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_023_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_023_sd.png b/examples/mods/iron_coast_mission_1/art/tile_023_sd.png new file mode 100644 index 00000000..f8462cbe Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_023_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_024_hd.png b/examples/mods/iron_coast_mission_1/art/tile_024_hd.png new file mode 100644 index 00000000..3bbe8473 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_024_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_024_sd.png b/examples/mods/iron_coast_mission_1/art/tile_024_sd.png new file mode 100644 index 00000000..bacf4c83 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_024_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_025_hd.png b/examples/mods/iron_coast_mission_1/art/tile_025_hd.png new file mode 100644 index 00000000..1f7ce7f4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_025_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_025_sd.png b/examples/mods/iron_coast_mission_1/art/tile_025_sd.png new file mode 100644 index 00000000..894b07f8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_025_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_026_hd.png b/examples/mods/iron_coast_mission_1/art/tile_026_hd.png new file mode 100644 index 00000000..05e78c2f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_026_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_026_sd.png b/examples/mods/iron_coast_mission_1/art/tile_026_sd.png new file mode 100644 index 00000000..1d471267 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_026_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_027_hd.png b/examples/mods/iron_coast_mission_1/art/tile_027_hd.png new file mode 100644 index 00000000..ebbf33f8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_027_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_027_sd.png b/examples/mods/iron_coast_mission_1/art/tile_027_sd.png new file mode 100644 index 00000000..69471354 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_027_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_028_hd.png b/examples/mods/iron_coast_mission_1/art/tile_028_hd.png new file mode 100644 index 00000000..c0b12aee Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_028_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_028_sd.png b/examples/mods/iron_coast_mission_1/art/tile_028_sd.png new file mode 100644 index 00000000..dd8e68bc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_028_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_029_hd.png b/examples/mods/iron_coast_mission_1/art/tile_029_hd.png new file mode 100644 index 00000000..1e456eb8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_029_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_029_sd.png b/examples/mods/iron_coast_mission_1/art/tile_029_sd.png new file mode 100644 index 00000000..529bad72 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_029_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_030_hd.png b/examples/mods/iron_coast_mission_1/art/tile_030_hd.png new file mode 100644 index 00000000..3916dd66 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_030_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_030_sd.png b/examples/mods/iron_coast_mission_1/art/tile_030_sd.png new file mode 100644 index 00000000..aeed75be Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_030_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_031_hd.png b/examples/mods/iron_coast_mission_1/art/tile_031_hd.png new file mode 100644 index 00000000..4ec56247 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_031_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_031_sd.png b/examples/mods/iron_coast_mission_1/art/tile_031_sd.png new file mode 100644 index 00000000..2cb959d6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_031_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_032_hd.png b/examples/mods/iron_coast_mission_1/art/tile_032_hd.png new file mode 100644 index 00000000..cbe49798 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_032_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_032_sd.png b/examples/mods/iron_coast_mission_1/art/tile_032_sd.png new file mode 100644 index 00000000..eb49018c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_032_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_033_hd.png b/examples/mods/iron_coast_mission_1/art/tile_033_hd.png new file mode 100644 index 00000000..c918087d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_033_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_033_sd.png b/examples/mods/iron_coast_mission_1/art/tile_033_sd.png new file mode 100644 index 00000000..90b76ade Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_033_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_034_hd.png b/examples/mods/iron_coast_mission_1/art/tile_034_hd.png new file mode 100644 index 00000000..90fe99dd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_034_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_034_sd.png b/examples/mods/iron_coast_mission_1/art/tile_034_sd.png new file mode 100644 index 00000000..dc9114ca Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_034_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_035_hd.png b/examples/mods/iron_coast_mission_1/art/tile_035_hd.png new file mode 100644 index 00000000..6cc79851 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_035_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_035_sd.png b/examples/mods/iron_coast_mission_1/art/tile_035_sd.png new file mode 100644 index 00000000..1c44e996 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_035_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_036_hd.png b/examples/mods/iron_coast_mission_1/art/tile_036_hd.png new file mode 100644 index 00000000..125dd34d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_036_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_036_sd.png b/examples/mods/iron_coast_mission_1/art/tile_036_sd.png new file mode 100644 index 00000000..9f525490 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_036_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_037_hd.png b/examples/mods/iron_coast_mission_1/art/tile_037_hd.png new file mode 100644 index 00000000..3240be6c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_037_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_037_sd.png b/examples/mods/iron_coast_mission_1/art/tile_037_sd.png new file mode 100644 index 00000000..2234b512 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_037_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_038_hd.png b/examples/mods/iron_coast_mission_1/art/tile_038_hd.png new file mode 100644 index 00000000..8a42567b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_038_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_038_sd.png b/examples/mods/iron_coast_mission_1/art/tile_038_sd.png new file mode 100644 index 00000000..12813f13 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_038_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_039_hd.png b/examples/mods/iron_coast_mission_1/art/tile_039_hd.png new file mode 100644 index 00000000..98c27ced Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_039_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_039_sd.png b/examples/mods/iron_coast_mission_1/art/tile_039_sd.png new file mode 100644 index 00000000..dd5da8f2 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_039_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_040_hd.png b/examples/mods/iron_coast_mission_1/art/tile_040_hd.png new file mode 100644 index 00000000..5df1e043 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_040_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_040_sd.png b/examples/mods/iron_coast_mission_1/art/tile_040_sd.png new file mode 100644 index 00000000..ac6f691a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_040_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_041_hd.png b/examples/mods/iron_coast_mission_1/art/tile_041_hd.png new file mode 100644 index 00000000..0d098e96 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_041_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_041_sd.png b/examples/mods/iron_coast_mission_1/art/tile_041_sd.png new file mode 100644 index 00000000..13d7b9f5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_041_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_042_hd.png b/examples/mods/iron_coast_mission_1/art/tile_042_hd.png new file mode 100644 index 00000000..fe9bc1f2 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_042_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_042_sd.png b/examples/mods/iron_coast_mission_1/art/tile_042_sd.png new file mode 100644 index 00000000..199e0404 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_042_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_043_hd.png b/examples/mods/iron_coast_mission_1/art/tile_043_hd.png new file mode 100644 index 00000000..1ed7ff76 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_043_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_043_sd.png b/examples/mods/iron_coast_mission_1/art/tile_043_sd.png new file mode 100644 index 00000000..e7229f3f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_043_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_044_hd.png b/examples/mods/iron_coast_mission_1/art/tile_044_hd.png new file mode 100644 index 00000000..cbe5fb19 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_044_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_044_sd.png b/examples/mods/iron_coast_mission_1/art/tile_044_sd.png new file mode 100644 index 00000000..dfc5349b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_044_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_045_hd.png b/examples/mods/iron_coast_mission_1/art/tile_045_hd.png new file mode 100644 index 00000000..56d9a351 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_045_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_045_sd.png b/examples/mods/iron_coast_mission_1/art/tile_045_sd.png new file mode 100644 index 00000000..6b7e3d50 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_045_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_046_hd.png b/examples/mods/iron_coast_mission_1/art/tile_046_hd.png new file mode 100644 index 00000000..43cf7fb1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_046_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_046_sd.png b/examples/mods/iron_coast_mission_1/art/tile_046_sd.png new file mode 100644 index 00000000..0547e8f7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_046_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_047_hd.png b/examples/mods/iron_coast_mission_1/art/tile_047_hd.png new file mode 100644 index 00000000..ef92a039 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_047_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_047_sd.png b/examples/mods/iron_coast_mission_1/art/tile_047_sd.png new file mode 100644 index 00000000..f8660c33 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_047_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_048_hd.png b/examples/mods/iron_coast_mission_1/art/tile_048_hd.png new file mode 100644 index 00000000..1bb63fb7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_048_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_048_sd.png b/examples/mods/iron_coast_mission_1/art/tile_048_sd.png new file mode 100644 index 00000000..edf2cbcd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_048_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_049_hd.png b/examples/mods/iron_coast_mission_1/art/tile_049_hd.png new file mode 100644 index 00000000..27f97557 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_049_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_049_sd.png b/examples/mods/iron_coast_mission_1/art/tile_049_sd.png new file mode 100644 index 00000000..0e702e77 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_049_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_050_hd.png b/examples/mods/iron_coast_mission_1/art/tile_050_hd.png new file mode 100644 index 00000000..11cca00c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_050_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_050_sd.png b/examples/mods/iron_coast_mission_1/art/tile_050_sd.png new file mode 100644 index 00000000..c060ecbd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_050_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_051_hd.png b/examples/mods/iron_coast_mission_1/art/tile_051_hd.png new file mode 100644 index 00000000..43ef5bfa Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_051_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_051_sd.png b/examples/mods/iron_coast_mission_1/art/tile_051_sd.png new file mode 100644 index 00000000..4528bdb7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_051_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_052_hd.png b/examples/mods/iron_coast_mission_1/art/tile_052_hd.png new file mode 100644 index 00000000..a4bcf4d5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_052_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_052_sd.png b/examples/mods/iron_coast_mission_1/art/tile_052_sd.png new file mode 100644 index 00000000..666814b9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_052_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_053_hd.png b/examples/mods/iron_coast_mission_1/art/tile_053_hd.png new file mode 100644 index 00000000..5b9e3057 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_053_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_053_sd.png b/examples/mods/iron_coast_mission_1/art/tile_053_sd.png new file mode 100644 index 00000000..b877abf5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_053_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_054_hd.png b/examples/mods/iron_coast_mission_1/art/tile_054_hd.png new file mode 100644 index 00000000..1ceca270 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_054_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_054_sd.png b/examples/mods/iron_coast_mission_1/art/tile_054_sd.png new file mode 100644 index 00000000..959d79aa Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_054_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_055_hd.png b/examples/mods/iron_coast_mission_1/art/tile_055_hd.png new file mode 100644 index 00000000..8f7ffadc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_055_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_055_sd.png b/examples/mods/iron_coast_mission_1/art/tile_055_sd.png new file mode 100644 index 00000000..18689867 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_055_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_056_hd.png b/examples/mods/iron_coast_mission_1/art/tile_056_hd.png new file mode 100644 index 00000000..1c612d4d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_056_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_056_sd.png b/examples/mods/iron_coast_mission_1/art/tile_056_sd.png new file mode 100644 index 00000000..6d750a13 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_056_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_057_hd.png b/examples/mods/iron_coast_mission_1/art/tile_057_hd.png new file mode 100644 index 00000000..e77d4491 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_057_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_057_sd.png b/examples/mods/iron_coast_mission_1/art/tile_057_sd.png new file mode 100644 index 00000000..924bde71 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_057_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_058_hd.png b/examples/mods/iron_coast_mission_1/art/tile_058_hd.png new file mode 100644 index 00000000..9b9cf3f1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_058_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_058_sd.png b/examples/mods/iron_coast_mission_1/art/tile_058_sd.png new file mode 100644 index 00000000..a49a84ee Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_058_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_059_hd.png b/examples/mods/iron_coast_mission_1/art/tile_059_hd.png new file mode 100644 index 00000000..86d57f96 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_059_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_059_sd.png b/examples/mods/iron_coast_mission_1/art/tile_059_sd.png new file mode 100644 index 00000000..9bc08f0c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_059_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_060_hd.png b/examples/mods/iron_coast_mission_1/art/tile_060_hd.png new file mode 100644 index 00000000..ad1045e8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_060_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_060_sd.png b/examples/mods/iron_coast_mission_1/art/tile_060_sd.png new file mode 100644 index 00000000..ee1153e1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_060_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_061_hd.png b/examples/mods/iron_coast_mission_1/art/tile_061_hd.png new file mode 100644 index 00000000..5b786859 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_061_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_061_sd.png b/examples/mods/iron_coast_mission_1/art/tile_061_sd.png new file mode 100644 index 00000000..666ce696 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_061_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_062_hd.png b/examples/mods/iron_coast_mission_1/art/tile_062_hd.png new file mode 100644 index 00000000..79d93f4d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_062_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_062_sd.png b/examples/mods/iron_coast_mission_1/art/tile_062_sd.png new file mode 100644 index 00000000..4bcdfb10 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_062_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_063_hd.png b/examples/mods/iron_coast_mission_1/art/tile_063_hd.png new file mode 100644 index 00000000..76540cbc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_063_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_063_sd.png b/examples/mods/iron_coast_mission_1/art/tile_063_sd.png new file mode 100644 index 00000000..4d14af03 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_063_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_064_hd.png b/examples/mods/iron_coast_mission_1/art/tile_064_hd.png new file mode 100644 index 00000000..03ed9c65 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_064_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_064_sd.png b/examples/mods/iron_coast_mission_1/art/tile_064_sd.png new file mode 100644 index 00000000..44c4adc9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_064_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_065_hd.png b/examples/mods/iron_coast_mission_1/art/tile_065_hd.png new file mode 100644 index 00000000..2752a26b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_065_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_065_sd.png b/examples/mods/iron_coast_mission_1/art/tile_065_sd.png new file mode 100644 index 00000000..f91ddde9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_065_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_066_hd.png b/examples/mods/iron_coast_mission_1/art/tile_066_hd.png new file mode 100644 index 00000000..664b034e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_066_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_066_sd.png b/examples/mods/iron_coast_mission_1/art/tile_066_sd.png new file mode 100644 index 00000000..38e95050 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_066_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_067_hd.png b/examples/mods/iron_coast_mission_1/art/tile_067_hd.png new file mode 100644 index 00000000..8687415d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_067_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_067_sd.png b/examples/mods/iron_coast_mission_1/art/tile_067_sd.png new file mode 100644 index 00000000..71d07732 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_067_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_068_hd.png b/examples/mods/iron_coast_mission_1/art/tile_068_hd.png new file mode 100644 index 00000000..323f558f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_068_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_068_sd.png b/examples/mods/iron_coast_mission_1/art/tile_068_sd.png new file mode 100644 index 00000000..f1299746 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_068_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_069_hd.png b/examples/mods/iron_coast_mission_1/art/tile_069_hd.png new file mode 100644 index 00000000..09d6182e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_069_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_069_sd.png b/examples/mods/iron_coast_mission_1/art/tile_069_sd.png new file mode 100644 index 00000000..59780a97 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_069_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_070_hd.png b/examples/mods/iron_coast_mission_1/art/tile_070_hd.png new file mode 100644 index 00000000..330fd3f8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_070_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_070_sd.png b/examples/mods/iron_coast_mission_1/art/tile_070_sd.png new file mode 100644 index 00000000..f033e976 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_070_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_071_hd.png b/examples/mods/iron_coast_mission_1/art/tile_071_hd.png new file mode 100644 index 00000000..0009fe2e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_071_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_071_sd.png b/examples/mods/iron_coast_mission_1/art/tile_071_sd.png new file mode 100644 index 00000000..608ab76d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_071_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_072_hd.png b/examples/mods/iron_coast_mission_1/art/tile_072_hd.png new file mode 100644 index 00000000..e004818c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_072_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_072_sd.png b/examples/mods/iron_coast_mission_1/art/tile_072_sd.png new file mode 100644 index 00000000..87c4ea71 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_072_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_073_hd.png b/examples/mods/iron_coast_mission_1/art/tile_073_hd.png new file mode 100644 index 00000000..e1f51bb3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_073_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_073_sd.png b/examples/mods/iron_coast_mission_1/art/tile_073_sd.png new file mode 100644 index 00000000..42c8930a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_073_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_074_hd.png b/examples/mods/iron_coast_mission_1/art/tile_074_hd.png new file mode 100644 index 00000000..e4240cec Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_074_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_074_sd.png b/examples/mods/iron_coast_mission_1/art/tile_074_sd.png new file mode 100644 index 00000000..35f8c705 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_074_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_075_hd.png b/examples/mods/iron_coast_mission_1/art/tile_075_hd.png new file mode 100644 index 00000000..21b33b5b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_075_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_075_sd.png b/examples/mods/iron_coast_mission_1/art/tile_075_sd.png new file mode 100644 index 00000000..4d1083ba Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_075_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_076_hd.png b/examples/mods/iron_coast_mission_1/art/tile_076_hd.png new file mode 100644 index 00000000..370eef01 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_076_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_076_sd.png b/examples/mods/iron_coast_mission_1/art/tile_076_sd.png new file mode 100644 index 00000000..268ce5c4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_076_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_077_hd.png b/examples/mods/iron_coast_mission_1/art/tile_077_hd.png new file mode 100644 index 00000000..c8f77b87 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_077_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_077_sd.png b/examples/mods/iron_coast_mission_1/art/tile_077_sd.png new file mode 100644 index 00000000..b2257d68 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_077_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_078_hd.png b/examples/mods/iron_coast_mission_1/art/tile_078_hd.png new file mode 100644 index 00000000..bcbbd932 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_078_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_078_sd.png b/examples/mods/iron_coast_mission_1/art/tile_078_sd.png new file mode 100644 index 00000000..509dbbdc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_078_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_079_hd.png b/examples/mods/iron_coast_mission_1/art/tile_079_hd.png new file mode 100644 index 00000000..553ced51 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_079_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_079_sd.png b/examples/mods/iron_coast_mission_1/art/tile_079_sd.png new file mode 100644 index 00000000..fe5b0265 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_079_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_080_hd.png b/examples/mods/iron_coast_mission_1/art/tile_080_hd.png new file mode 100644 index 00000000..fb83a728 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_080_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_080_sd.png b/examples/mods/iron_coast_mission_1/art/tile_080_sd.png new file mode 100644 index 00000000..005c5e0f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_080_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_081_hd.png b/examples/mods/iron_coast_mission_1/art/tile_081_hd.png new file mode 100644 index 00000000..ea7f54f9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_081_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_081_sd.png b/examples/mods/iron_coast_mission_1/art/tile_081_sd.png new file mode 100644 index 00000000..b1557069 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_081_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_082_hd.png b/examples/mods/iron_coast_mission_1/art/tile_082_hd.png new file mode 100644 index 00000000..7a9f4f92 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_082_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_082_sd.png b/examples/mods/iron_coast_mission_1/art/tile_082_sd.png new file mode 100644 index 00000000..50a0985b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_082_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_083_hd.png b/examples/mods/iron_coast_mission_1/art/tile_083_hd.png new file mode 100644 index 00000000..bd4679a3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_083_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_083_sd.png b/examples/mods/iron_coast_mission_1/art/tile_083_sd.png new file mode 100644 index 00000000..b79ba21a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_083_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_084_hd.png b/examples/mods/iron_coast_mission_1/art/tile_084_hd.png new file mode 100644 index 00000000..210dc518 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_084_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_084_sd.png b/examples/mods/iron_coast_mission_1/art/tile_084_sd.png new file mode 100644 index 00000000..08c7f124 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_084_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_085_hd.png b/examples/mods/iron_coast_mission_1/art/tile_085_hd.png new file mode 100644 index 00000000..90f6929e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_085_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_085_sd.png b/examples/mods/iron_coast_mission_1/art/tile_085_sd.png new file mode 100644 index 00000000..6aa17f69 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_085_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_086_hd.png b/examples/mods/iron_coast_mission_1/art/tile_086_hd.png new file mode 100644 index 00000000..42e326f3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_086_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_086_sd.png b/examples/mods/iron_coast_mission_1/art/tile_086_sd.png new file mode 100644 index 00000000..7cb50ce9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_086_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_087_hd.png b/examples/mods/iron_coast_mission_1/art/tile_087_hd.png new file mode 100644 index 00000000..bf80db91 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_087_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_087_sd.png b/examples/mods/iron_coast_mission_1/art/tile_087_sd.png new file mode 100644 index 00000000..026a73b8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_087_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_088_hd.png b/examples/mods/iron_coast_mission_1/art/tile_088_hd.png new file mode 100644 index 00000000..91c3530b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_088_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_088_sd.png b/examples/mods/iron_coast_mission_1/art/tile_088_sd.png new file mode 100644 index 00000000..243ed500 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_088_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_089_hd.png b/examples/mods/iron_coast_mission_1/art/tile_089_hd.png new file mode 100644 index 00000000..bce5282e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_089_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_089_sd.png b/examples/mods/iron_coast_mission_1/art/tile_089_sd.png new file mode 100644 index 00000000..acc8b4c7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_089_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_090_hd.png b/examples/mods/iron_coast_mission_1/art/tile_090_hd.png new file mode 100644 index 00000000..1d3f45ef Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_090_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_090_sd.png b/examples/mods/iron_coast_mission_1/art/tile_090_sd.png new file mode 100644 index 00000000..fce6192d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_090_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_091_hd.png b/examples/mods/iron_coast_mission_1/art/tile_091_hd.png new file mode 100644 index 00000000..47fa1de2 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_091_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_091_sd.png b/examples/mods/iron_coast_mission_1/art/tile_091_sd.png new file mode 100644 index 00000000..77cd7f56 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_091_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_092_hd.png b/examples/mods/iron_coast_mission_1/art/tile_092_hd.png new file mode 100644 index 00000000..97149c0e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_092_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_092_sd.png b/examples/mods/iron_coast_mission_1/art/tile_092_sd.png new file mode 100644 index 00000000..9eccf80d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_092_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_093_hd.png b/examples/mods/iron_coast_mission_1/art/tile_093_hd.png new file mode 100644 index 00000000..79c855b7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_093_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_093_sd.png b/examples/mods/iron_coast_mission_1/art/tile_093_sd.png new file mode 100644 index 00000000..ff699643 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_093_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_094_hd.png b/examples/mods/iron_coast_mission_1/art/tile_094_hd.png new file mode 100644 index 00000000..2b7d00d8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_094_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_094_sd.png b/examples/mods/iron_coast_mission_1/art/tile_094_sd.png new file mode 100644 index 00000000..65fec5bf Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_094_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_095_hd.png b/examples/mods/iron_coast_mission_1/art/tile_095_hd.png new file mode 100644 index 00000000..a36d839d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_095_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_095_sd.png b/examples/mods/iron_coast_mission_1/art/tile_095_sd.png new file mode 100644 index 00000000..289b059e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_095_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_096_hd.png b/examples/mods/iron_coast_mission_1/art/tile_096_hd.png new file mode 100644 index 00000000..6d5ac2ad Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_096_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_096_sd.png b/examples/mods/iron_coast_mission_1/art/tile_096_sd.png new file mode 100644 index 00000000..996f4060 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_096_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_097_hd.png b/examples/mods/iron_coast_mission_1/art/tile_097_hd.png new file mode 100644 index 00000000..9bfe430c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_097_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_097_sd.png b/examples/mods/iron_coast_mission_1/art/tile_097_sd.png new file mode 100644 index 00000000..1924eb0b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_097_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_098_hd.png b/examples/mods/iron_coast_mission_1/art/tile_098_hd.png new file mode 100644 index 00000000..f2af0fb9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_098_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_098_sd.png b/examples/mods/iron_coast_mission_1/art/tile_098_sd.png new file mode 100644 index 00000000..ed91fb6f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_098_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_099_hd.png b/examples/mods/iron_coast_mission_1/art/tile_099_hd.png new file mode 100644 index 00000000..65f9a931 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_099_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_099_sd.png b/examples/mods/iron_coast_mission_1/art/tile_099_sd.png new file mode 100644 index 00000000..834db018 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_099_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_100_hd.png b/examples/mods/iron_coast_mission_1/art/tile_100_hd.png new file mode 100644 index 00000000..7335ad59 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_100_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_100_sd.png b/examples/mods/iron_coast_mission_1/art/tile_100_sd.png new file mode 100644 index 00000000..d41711da Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_100_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_101_hd.png b/examples/mods/iron_coast_mission_1/art/tile_101_hd.png new file mode 100644 index 00000000..3c6f47b7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_101_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_101_sd.png b/examples/mods/iron_coast_mission_1/art/tile_101_sd.png new file mode 100644 index 00000000..5e913214 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_101_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_102_hd.png b/examples/mods/iron_coast_mission_1/art/tile_102_hd.png new file mode 100644 index 00000000..b3391987 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_102_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_102_sd.png b/examples/mods/iron_coast_mission_1/art/tile_102_sd.png new file mode 100644 index 00000000..3c3d994d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_102_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_103_hd.png b/examples/mods/iron_coast_mission_1/art/tile_103_hd.png new file mode 100644 index 00000000..7ec33f73 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_103_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_103_sd.png b/examples/mods/iron_coast_mission_1/art/tile_103_sd.png new file mode 100644 index 00000000..67ac2ae5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_103_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_104_hd.png b/examples/mods/iron_coast_mission_1/art/tile_104_hd.png new file mode 100644 index 00000000..227c1c5c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_104_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_104_sd.png b/examples/mods/iron_coast_mission_1/art/tile_104_sd.png new file mode 100644 index 00000000..81e5b447 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_104_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_105_hd.png b/examples/mods/iron_coast_mission_1/art/tile_105_hd.png new file mode 100644 index 00000000..5a7ded19 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_105_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_105_sd.png b/examples/mods/iron_coast_mission_1/art/tile_105_sd.png new file mode 100644 index 00000000..bd80e7ee Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_105_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_106_hd.png b/examples/mods/iron_coast_mission_1/art/tile_106_hd.png new file mode 100644 index 00000000..fc0241e5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_106_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_106_sd.png b/examples/mods/iron_coast_mission_1/art/tile_106_sd.png new file mode 100644 index 00000000..c1604dff Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_106_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_107_hd.png b/examples/mods/iron_coast_mission_1/art/tile_107_hd.png new file mode 100644 index 00000000..63c47178 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_107_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_107_sd.png b/examples/mods/iron_coast_mission_1/art/tile_107_sd.png new file mode 100644 index 00000000..a433005f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_107_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_108_hd.png b/examples/mods/iron_coast_mission_1/art/tile_108_hd.png new file mode 100644 index 00000000..7826e147 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_108_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_108_sd.png b/examples/mods/iron_coast_mission_1/art/tile_108_sd.png new file mode 100644 index 00000000..25e6ca45 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_108_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_109_hd.png b/examples/mods/iron_coast_mission_1/art/tile_109_hd.png new file mode 100644 index 00000000..f7bfb5bc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_109_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_109_sd.png b/examples/mods/iron_coast_mission_1/art/tile_109_sd.png new file mode 100644 index 00000000..eb818cd4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_109_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_110_hd.png b/examples/mods/iron_coast_mission_1/art/tile_110_hd.png new file mode 100644 index 00000000..c1918c28 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_110_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_110_sd.png b/examples/mods/iron_coast_mission_1/art/tile_110_sd.png new file mode 100644 index 00000000..a84d2284 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_110_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_111_hd.png b/examples/mods/iron_coast_mission_1/art/tile_111_hd.png new file mode 100644 index 00000000..c4f695d0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_111_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_111_sd.png b/examples/mods/iron_coast_mission_1/art/tile_111_sd.png new file mode 100644 index 00000000..acdcc229 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_111_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_112_hd.png b/examples/mods/iron_coast_mission_1/art/tile_112_hd.png new file mode 100644 index 00000000..27afb1cd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_112_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_112_sd.png b/examples/mods/iron_coast_mission_1/art/tile_112_sd.png new file mode 100644 index 00000000..66581d7a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_112_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_113_hd.png b/examples/mods/iron_coast_mission_1/art/tile_113_hd.png new file mode 100644 index 00000000..68ce4f6e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_113_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_113_sd.png b/examples/mods/iron_coast_mission_1/art/tile_113_sd.png new file mode 100644 index 00000000..5a4bd584 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_113_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_114_hd.png b/examples/mods/iron_coast_mission_1/art/tile_114_hd.png new file mode 100644 index 00000000..6dbf1d05 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_114_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_114_sd.png b/examples/mods/iron_coast_mission_1/art/tile_114_sd.png new file mode 100644 index 00000000..e0ac9cb7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_114_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_115_hd.png b/examples/mods/iron_coast_mission_1/art/tile_115_hd.png new file mode 100644 index 00000000..65807e6c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_115_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_115_sd.png b/examples/mods/iron_coast_mission_1/art/tile_115_sd.png new file mode 100644 index 00000000..7b5a39fc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_115_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_116_hd.png b/examples/mods/iron_coast_mission_1/art/tile_116_hd.png new file mode 100644 index 00000000..eb920c1b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_116_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_116_sd.png b/examples/mods/iron_coast_mission_1/art/tile_116_sd.png new file mode 100644 index 00000000..d2547b70 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_116_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_117_hd.png b/examples/mods/iron_coast_mission_1/art/tile_117_hd.png new file mode 100644 index 00000000..4e7bd2da Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_117_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_117_sd.png b/examples/mods/iron_coast_mission_1/art/tile_117_sd.png new file mode 100644 index 00000000..22e8d3d9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_117_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_118_hd.png b/examples/mods/iron_coast_mission_1/art/tile_118_hd.png new file mode 100644 index 00000000..5838702c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_118_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_118_sd.png b/examples/mods/iron_coast_mission_1/art/tile_118_sd.png new file mode 100644 index 00000000..d8093bde Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_118_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_119_hd.png b/examples/mods/iron_coast_mission_1/art/tile_119_hd.png new file mode 100644 index 00000000..387f779e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_119_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_119_sd.png b/examples/mods/iron_coast_mission_1/art/tile_119_sd.png new file mode 100644 index 00000000..9dce0aa0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_119_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_120_hd.png b/examples/mods/iron_coast_mission_1/art/tile_120_hd.png new file mode 100644 index 00000000..8fc15679 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_120_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_120_sd.png b/examples/mods/iron_coast_mission_1/art/tile_120_sd.png new file mode 100644 index 00000000..addd2f0f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_120_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_121_hd.png b/examples/mods/iron_coast_mission_1/art/tile_121_hd.png new file mode 100644 index 00000000..e1e6f855 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_121_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_121_sd.png b/examples/mods/iron_coast_mission_1/art/tile_121_sd.png new file mode 100644 index 00000000..ecd0b5bc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_121_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_122_hd.png b/examples/mods/iron_coast_mission_1/art/tile_122_hd.png new file mode 100644 index 00000000..d0998239 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_122_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_122_sd.png b/examples/mods/iron_coast_mission_1/art/tile_122_sd.png new file mode 100644 index 00000000..48befb13 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_122_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_123_hd.png b/examples/mods/iron_coast_mission_1/art/tile_123_hd.png new file mode 100644 index 00000000..673df551 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_123_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_123_sd.png b/examples/mods/iron_coast_mission_1/art/tile_123_sd.png new file mode 100644 index 00000000..f8126926 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_123_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_124_hd.png b/examples/mods/iron_coast_mission_1/art/tile_124_hd.png new file mode 100644 index 00000000..005ac0be Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_124_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_124_sd.png b/examples/mods/iron_coast_mission_1/art/tile_124_sd.png new file mode 100644 index 00000000..c78eef3a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_124_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_125_hd.png b/examples/mods/iron_coast_mission_1/art/tile_125_hd.png new file mode 100644 index 00000000..9d31efa6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_125_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_125_sd.png b/examples/mods/iron_coast_mission_1/art/tile_125_sd.png new file mode 100644 index 00000000..61e63e02 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_125_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_126_hd.png b/examples/mods/iron_coast_mission_1/art/tile_126_hd.png new file mode 100644 index 00000000..fa49bdd8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_126_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_126_sd.png b/examples/mods/iron_coast_mission_1/art/tile_126_sd.png new file mode 100644 index 00000000..6bab2aa5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_126_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_127_hd.png b/examples/mods/iron_coast_mission_1/art/tile_127_hd.png new file mode 100644 index 00000000..96c3ed1d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_127_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_127_sd.png b/examples/mods/iron_coast_mission_1/art/tile_127_sd.png new file mode 100644 index 00000000..aa33c012 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_127_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_128_hd.png b/examples/mods/iron_coast_mission_1/art/tile_128_hd.png new file mode 100644 index 00000000..8a687ceb Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_128_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_128_sd.png b/examples/mods/iron_coast_mission_1/art/tile_128_sd.png new file mode 100644 index 00000000..d3375b03 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_128_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_129_hd.png b/examples/mods/iron_coast_mission_1/art/tile_129_hd.png new file mode 100644 index 00000000..96b02a0a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_129_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_129_sd.png b/examples/mods/iron_coast_mission_1/art/tile_129_sd.png new file mode 100644 index 00000000..59e3d7b5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_129_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_130_hd.png b/examples/mods/iron_coast_mission_1/art/tile_130_hd.png new file mode 100644 index 00000000..fc7d5e38 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_130_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_130_sd.png b/examples/mods/iron_coast_mission_1/art/tile_130_sd.png new file mode 100644 index 00000000..842b9497 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_130_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_131_hd.png b/examples/mods/iron_coast_mission_1/art/tile_131_hd.png new file mode 100644 index 00000000..dc2a255f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_131_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_131_sd.png b/examples/mods/iron_coast_mission_1/art/tile_131_sd.png new file mode 100644 index 00000000..6a9a5bf9 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_131_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_132_hd.png b/examples/mods/iron_coast_mission_1/art/tile_132_hd.png new file mode 100644 index 00000000..6725c87d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_132_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_132_sd.png b/examples/mods/iron_coast_mission_1/art/tile_132_sd.png new file mode 100644 index 00000000..38197e44 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_132_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_133_hd.png b/examples/mods/iron_coast_mission_1/art/tile_133_hd.png new file mode 100644 index 00000000..a7a351cb Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_133_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_133_sd.png b/examples/mods/iron_coast_mission_1/art/tile_133_sd.png new file mode 100644 index 00000000..cd6b3cab Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_133_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_134_hd.png b/examples/mods/iron_coast_mission_1/art/tile_134_hd.png new file mode 100644 index 00000000..dc189f61 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_134_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_134_sd.png b/examples/mods/iron_coast_mission_1/art/tile_134_sd.png new file mode 100644 index 00000000..11dc4751 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_134_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_135_hd.png b/examples/mods/iron_coast_mission_1/art/tile_135_hd.png new file mode 100644 index 00000000..7b0a66ff Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_135_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_135_sd.png b/examples/mods/iron_coast_mission_1/art/tile_135_sd.png new file mode 100644 index 00000000..76ad59e5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_135_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_136_hd.png b/examples/mods/iron_coast_mission_1/art/tile_136_hd.png new file mode 100644 index 00000000..617e466b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_136_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_136_sd.png b/examples/mods/iron_coast_mission_1/art/tile_136_sd.png new file mode 100644 index 00000000..d052f666 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_136_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_137_hd.png b/examples/mods/iron_coast_mission_1/art/tile_137_hd.png new file mode 100644 index 00000000..6edc6da1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_137_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_137_sd.png b/examples/mods/iron_coast_mission_1/art/tile_137_sd.png new file mode 100644 index 00000000..e947cb78 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_137_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_138_hd.png b/examples/mods/iron_coast_mission_1/art/tile_138_hd.png new file mode 100644 index 00000000..7bfc8f46 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_138_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_138_sd.png b/examples/mods/iron_coast_mission_1/art/tile_138_sd.png new file mode 100644 index 00000000..0c0527de Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_138_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_139_hd.png b/examples/mods/iron_coast_mission_1/art/tile_139_hd.png new file mode 100644 index 00000000..93a0e0c1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_139_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_139_sd.png b/examples/mods/iron_coast_mission_1/art/tile_139_sd.png new file mode 100644 index 00000000..cd4e8361 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_139_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_140_hd.png b/examples/mods/iron_coast_mission_1/art/tile_140_hd.png new file mode 100644 index 00000000..6f083970 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_140_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_140_sd.png b/examples/mods/iron_coast_mission_1/art/tile_140_sd.png new file mode 100644 index 00000000..90b702b8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_140_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_141_hd.png b/examples/mods/iron_coast_mission_1/art/tile_141_hd.png new file mode 100644 index 00000000..373f46ab Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_141_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_141_sd.png b/examples/mods/iron_coast_mission_1/art/tile_141_sd.png new file mode 100644 index 00000000..d1575a61 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_141_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_142_hd.png b/examples/mods/iron_coast_mission_1/art/tile_142_hd.png new file mode 100644 index 00000000..55918413 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_142_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_142_sd.png b/examples/mods/iron_coast_mission_1/art/tile_142_sd.png new file mode 100644 index 00000000..e3c2d79c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_142_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_143_hd.png b/examples/mods/iron_coast_mission_1/art/tile_143_hd.png new file mode 100644 index 00000000..6cf1ed84 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_143_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_143_sd.png b/examples/mods/iron_coast_mission_1/art/tile_143_sd.png new file mode 100644 index 00000000..5c132e32 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_143_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_144_hd.png b/examples/mods/iron_coast_mission_1/art/tile_144_hd.png new file mode 100644 index 00000000..982a6794 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_144_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_144_sd.png b/examples/mods/iron_coast_mission_1/art/tile_144_sd.png new file mode 100644 index 00000000..fc9f8862 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_144_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_145_hd.png b/examples/mods/iron_coast_mission_1/art/tile_145_hd.png new file mode 100644 index 00000000..9ef6f6e5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_145_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_145_sd.png b/examples/mods/iron_coast_mission_1/art/tile_145_sd.png new file mode 100644 index 00000000..75091341 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_145_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_146_hd.png b/examples/mods/iron_coast_mission_1/art/tile_146_hd.png new file mode 100644 index 00000000..d9210724 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_146_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_146_sd.png b/examples/mods/iron_coast_mission_1/art/tile_146_sd.png new file mode 100644 index 00000000..4e2e8127 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_146_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_147_hd.png b/examples/mods/iron_coast_mission_1/art/tile_147_hd.png new file mode 100644 index 00000000..1f970a07 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_147_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_147_sd.png b/examples/mods/iron_coast_mission_1/art/tile_147_sd.png new file mode 100644 index 00000000..329b6d08 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_147_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_148_hd.png b/examples/mods/iron_coast_mission_1/art/tile_148_hd.png new file mode 100644 index 00000000..26988c36 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_148_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_148_sd.png b/examples/mods/iron_coast_mission_1/art/tile_148_sd.png new file mode 100644 index 00000000..d91d192a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_148_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_149_hd.png b/examples/mods/iron_coast_mission_1/art/tile_149_hd.png new file mode 100644 index 00000000..4626e3cd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_149_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_149_sd.png b/examples/mods/iron_coast_mission_1/art/tile_149_sd.png new file mode 100644 index 00000000..00cca7ce Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_149_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_150_hd.png b/examples/mods/iron_coast_mission_1/art/tile_150_hd.png new file mode 100644 index 00000000..ba50a8b6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_150_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_150_sd.png b/examples/mods/iron_coast_mission_1/art/tile_150_sd.png new file mode 100644 index 00000000..21c04d66 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_150_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_151_hd.png b/examples/mods/iron_coast_mission_1/art/tile_151_hd.png new file mode 100644 index 00000000..5aedecce Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_151_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_151_sd.png b/examples/mods/iron_coast_mission_1/art/tile_151_sd.png new file mode 100644 index 00000000..9a98bc56 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_151_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_152_hd.png b/examples/mods/iron_coast_mission_1/art/tile_152_hd.png new file mode 100644 index 00000000..41fc7241 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_152_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_152_sd.png b/examples/mods/iron_coast_mission_1/art/tile_152_sd.png new file mode 100644 index 00000000..b79b5086 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_152_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_153_hd.png b/examples/mods/iron_coast_mission_1/art/tile_153_hd.png new file mode 100644 index 00000000..f07af42c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_153_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_153_sd.png b/examples/mods/iron_coast_mission_1/art/tile_153_sd.png new file mode 100644 index 00000000..99d92f7d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_153_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_154_hd.png b/examples/mods/iron_coast_mission_1/art/tile_154_hd.png new file mode 100644 index 00000000..c4e40e2e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_154_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_154_sd.png b/examples/mods/iron_coast_mission_1/art/tile_154_sd.png new file mode 100644 index 00000000..6f9495fd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_154_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_155_hd.png b/examples/mods/iron_coast_mission_1/art/tile_155_hd.png new file mode 100644 index 00000000..d9628d21 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_155_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_155_sd.png b/examples/mods/iron_coast_mission_1/art/tile_155_sd.png new file mode 100644 index 00000000..fde825b6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_155_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_156_hd.png b/examples/mods/iron_coast_mission_1/art/tile_156_hd.png new file mode 100644 index 00000000..fee491b0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_156_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_156_sd.png b/examples/mods/iron_coast_mission_1/art/tile_156_sd.png new file mode 100644 index 00000000..cce135e3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_156_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_157_hd.png b/examples/mods/iron_coast_mission_1/art/tile_157_hd.png new file mode 100644 index 00000000..db7707de Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_157_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_157_sd.png b/examples/mods/iron_coast_mission_1/art/tile_157_sd.png new file mode 100644 index 00000000..2cbefcad Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_157_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_158_hd.png b/examples/mods/iron_coast_mission_1/art/tile_158_hd.png new file mode 100644 index 00000000..567fee3e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_158_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_158_sd.png b/examples/mods/iron_coast_mission_1/art/tile_158_sd.png new file mode 100644 index 00000000..cb08d853 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_158_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_159_hd.png b/examples/mods/iron_coast_mission_1/art/tile_159_hd.png new file mode 100644 index 00000000..4f7ea49f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_159_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_159_sd.png b/examples/mods/iron_coast_mission_1/art/tile_159_sd.png new file mode 100644 index 00000000..6caeb457 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_159_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_160_hd.png b/examples/mods/iron_coast_mission_1/art/tile_160_hd.png new file mode 100644 index 00000000..68d4e826 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_160_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_160_sd.png b/examples/mods/iron_coast_mission_1/art/tile_160_sd.png new file mode 100644 index 00000000..f9ab5b24 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_160_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_161_hd.png b/examples/mods/iron_coast_mission_1/art/tile_161_hd.png new file mode 100644 index 00000000..d315796b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_161_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_161_sd.png b/examples/mods/iron_coast_mission_1/art/tile_161_sd.png new file mode 100644 index 00000000..e5f0317a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_161_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_162_hd.png b/examples/mods/iron_coast_mission_1/art/tile_162_hd.png new file mode 100644 index 00000000..3d6ea400 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_162_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_162_sd.png b/examples/mods/iron_coast_mission_1/art/tile_162_sd.png new file mode 100644 index 00000000..b72fcb70 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_162_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_163_hd.png b/examples/mods/iron_coast_mission_1/art/tile_163_hd.png new file mode 100644 index 00000000..1e339945 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_163_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_163_sd.png b/examples/mods/iron_coast_mission_1/art/tile_163_sd.png new file mode 100644 index 00000000..7dccaa56 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_163_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_164_hd.png b/examples/mods/iron_coast_mission_1/art/tile_164_hd.png new file mode 100644 index 00000000..8ae977a7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_164_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_164_sd.png b/examples/mods/iron_coast_mission_1/art/tile_164_sd.png new file mode 100644 index 00000000..a913c254 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_164_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_165_hd.png b/examples/mods/iron_coast_mission_1/art/tile_165_hd.png new file mode 100644 index 00000000..47976b92 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_165_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_165_sd.png b/examples/mods/iron_coast_mission_1/art/tile_165_sd.png new file mode 100644 index 00000000..1585380a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_165_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_166_hd.png b/examples/mods/iron_coast_mission_1/art/tile_166_hd.png new file mode 100644 index 00000000..f24fb694 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_166_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_166_sd.png b/examples/mods/iron_coast_mission_1/art/tile_166_sd.png new file mode 100644 index 00000000..27d65a87 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_166_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_167_hd.png b/examples/mods/iron_coast_mission_1/art/tile_167_hd.png new file mode 100644 index 00000000..90ea5b5b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_167_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_167_sd.png b/examples/mods/iron_coast_mission_1/art/tile_167_sd.png new file mode 100644 index 00000000..059aab51 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_167_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_168_hd.png b/examples/mods/iron_coast_mission_1/art/tile_168_hd.png new file mode 100644 index 00000000..8575b16f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_168_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_168_sd.png b/examples/mods/iron_coast_mission_1/art/tile_168_sd.png new file mode 100644 index 00000000..71a31c4d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_168_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_169_hd.png b/examples/mods/iron_coast_mission_1/art/tile_169_hd.png new file mode 100644 index 00000000..45ea34f3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_169_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_169_sd.png b/examples/mods/iron_coast_mission_1/art/tile_169_sd.png new file mode 100644 index 00000000..fad5cd19 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_169_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_170_hd.png b/examples/mods/iron_coast_mission_1/art/tile_170_hd.png new file mode 100644 index 00000000..1c2c9c46 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_170_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_170_sd.png b/examples/mods/iron_coast_mission_1/art/tile_170_sd.png new file mode 100644 index 00000000..fc5aace1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_170_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_171_hd.png b/examples/mods/iron_coast_mission_1/art/tile_171_hd.png new file mode 100644 index 00000000..79f6de48 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_171_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_171_sd.png b/examples/mods/iron_coast_mission_1/art/tile_171_sd.png new file mode 100644 index 00000000..8a715210 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_171_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_172_hd.png b/examples/mods/iron_coast_mission_1/art/tile_172_hd.png new file mode 100644 index 00000000..17b2945b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_172_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_172_sd.png b/examples/mods/iron_coast_mission_1/art/tile_172_sd.png new file mode 100644 index 00000000..0ad27223 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_172_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_173_hd.png b/examples/mods/iron_coast_mission_1/art/tile_173_hd.png new file mode 100644 index 00000000..e82fffc1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_173_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_173_sd.png b/examples/mods/iron_coast_mission_1/art/tile_173_sd.png new file mode 100644 index 00000000..c6017dc0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_173_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_174_hd.png b/examples/mods/iron_coast_mission_1/art/tile_174_hd.png new file mode 100644 index 00000000..d749f8a3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_174_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_174_sd.png b/examples/mods/iron_coast_mission_1/art/tile_174_sd.png new file mode 100644 index 00000000..3c8569fc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_174_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_175_hd.png b/examples/mods/iron_coast_mission_1/art/tile_175_hd.png new file mode 100644 index 00000000..bc76c784 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_175_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_175_sd.png b/examples/mods/iron_coast_mission_1/art/tile_175_sd.png new file mode 100644 index 00000000..fafe37a0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_175_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_176_hd.png b/examples/mods/iron_coast_mission_1/art/tile_176_hd.png new file mode 100644 index 00000000..88bbe125 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_176_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_176_sd.png b/examples/mods/iron_coast_mission_1/art/tile_176_sd.png new file mode 100644 index 00000000..a85f090d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_176_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_177_hd.png b/examples/mods/iron_coast_mission_1/art/tile_177_hd.png new file mode 100644 index 00000000..5c07383c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_177_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_177_sd.png b/examples/mods/iron_coast_mission_1/art/tile_177_sd.png new file mode 100644 index 00000000..aae45856 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_177_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_178_hd.png b/examples/mods/iron_coast_mission_1/art/tile_178_hd.png new file mode 100644 index 00000000..b7c90b59 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_178_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_178_sd.png b/examples/mods/iron_coast_mission_1/art/tile_178_sd.png new file mode 100644 index 00000000..b1f52354 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_178_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_179_hd.png b/examples/mods/iron_coast_mission_1/art/tile_179_hd.png new file mode 100644 index 00000000..d1880a38 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_179_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_179_sd.png b/examples/mods/iron_coast_mission_1/art/tile_179_sd.png new file mode 100644 index 00000000..06173a15 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_179_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_180_hd.png b/examples/mods/iron_coast_mission_1/art/tile_180_hd.png new file mode 100644 index 00000000..07882228 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_180_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_180_sd.png b/examples/mods/iron_coast_mission_1/art/tile_180_sd.png new file mode 100644 index 00000000..ad4497f4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_180_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_181_hd.png b/examples/mods/iron_coast_mission_1/art/tile_181_hd.png new file mode 100644 index 00000000..817c610c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_181_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_181_sd.png b/examples/mods/iron_coast_mission_1/art/tile_181_sd.png new file mode 100644 index 00000000..6e2cd3ad Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_181_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_182_hd.png b/examples/mods/iron_coast_mission_1/art/tile_182_hd.png new file mode 100644 index 00000000..3e49ef38 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_182_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_182_sd.png b/examples/mods/iron_coast_mission_1/art/tile_182_sd.png new file mode 100644 index 00000000..7c014b74 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_182_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_183_hd.png b/examples/mods/iron_coast_mission_1/art/tile_183_hd.png new file mode 100644 index 00000000..1a7dca4e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_183_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_183_sd.png b/examples/mods/iron_coast_mission_1/art/tile_183_sd.png new file mode 100644 index 00000000..26db2dd5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_183_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_184_hd.png b/examples/mods/iron_coast_mission_1/art/tile_184_hd.png new file mode 100644 index 00000000..b5b4420e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_184_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_184_sd.png b/examples/mods/iron_coast_mission_1/art/tile_184_sd.png new file mode 100644 index 00000000..23974a46 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_184_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_185_hd.png b/examples/mods/iron_coast_mission_1/art/tile_185_hd.png new file mode 100644 index 00000000..1ba97f81 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_185_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_185_sd.png b/examples/mods/iron_coast_mission_1/art/tile_185_sd.png new file mode 100644 index 00000000..afc5c4aa Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_185_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_186_hd.png b/examples/mods/iron_coast_mission_1/art/tile_186_hd.png new file mode 100644 index 00000000..d7d8054f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_186_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_186_sd.png b/examples/mods/iron_coast_mission_1/art/tile_186_sd.png new file mode 100644 index 00000000..42c53475 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_186_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_187_hd.png b/examples/mods/iron_coast_mission_1/art/tile_187_hd.png new file mode 100644 index 00000000..341261a5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_187_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_187_sd.png b/examples/mods/iron_coast_mission_1/art/tile_187_sd.png new file mode 100644 index 00000000..0a7f6da0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_187_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_188_hd.png b/examples/mods/iron_coast_mission_1/art/tile_188_hd.png new file mode 100644 index 00000000..6f0e322f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_188_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_188_sd.png b/examples/mods/iron_coast_mission_1/art/tile_188_sd.png new file mode 100644 index 00000000..ce0ef800 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_188_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_189_hd.png b/examples/mods/iron_coast_mission_1/art/tile_189_hd.png new file mode 100644 index 00000000..5492becc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_189_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_189_sd.png b/examples/mods/iron_coast_mission_1/art/tile_189_sd.png new file mode 100644 index 00000000..d70c8df5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_189_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_190_hd.png b/examples/mods/iron_coast_mission_1/art/tile_190_hd.png new file mode 100644 index 00000000..b05c7f9b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_190_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_190_sd.png b/examples/mods/iron_coast_mission_1/art/tile_190_sd.png new file mode 100644 index 00000000..68c2359d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_190_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_191_hd.png b/examples/mods/iron_coast_mission_1/art/tile_191_hd.png new file mode 100644 index 00000000..c2e7bf64 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_191_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_191_sd.png b/examples/mods/iron_coast_mission_1/art/tile_191_sd.png new file mode 100644 index 00000000..3a714f77 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_191_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_192_hd.png b/examples/mods/iron_coast_mission_1/art/tile_192_hd.png new file mode 100644 index 00000000..cff1270a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_192_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_192_sd.png b/examples/mods/iron_coast_mission_1/art/tile_192_sd.png new file mode 100644 index 00000000..b373fc22 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_192_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_193_hd.png b/examples/mods/iron_coast_mission_1/art/tile_193_hd.png new file mode 100644 index 00000000..b66d7ef5 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_193_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_193_sd.png b/examples/mods/iron_coast_mission_1/art/tile_193_sd.png new file mode 100644 index 00000000..fe144a0f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_193_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_194_hd.png b/examples/mods/iron_coast_mission_1/art/tile_194_hd.png new file mode 100644 index 00000000..278d2ed4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_194_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_194_sd.png b/examples/mods/iron_coast_mission_1/art/tile_194_sd.png new file mode 100644 index 00000000..e07aeaad Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_194_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_195_hd.png b/examples/mods/iron_coast_mission_1/art/tile_195_hd.png new file mode 100644 index 00000000..57d61a0b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_195_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_195_sd.png b/examples/mods/iron_coast_mission_1/art/tile_195_sd.png new file mode 100644 index 00000000..1a283ff0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_195_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_196_hd.png b/examples/mods/iron_coast_mission_1/art/tile_196_hd.png new file mode 100644 index 00000000..83befd5c Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_196_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_196_sd.png b/examples/mods/iron_coast_mission_1/art/tile_196_sd.png new file mode 100644 index 00000000..16c25483 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_196_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_197_hd.png b/examples/mods/iron_coast_mission_1/art/tile_197_hd.png new file mode 100644 index 00000000..3a4f0a60 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_197_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_197_sd.png b/examples/mods/iron_coast_mission_1/art/tile_197_sd.png new file mode 100644 index 00000000..7cc142bf Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_197_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_198_hd.png b/examples/mods/iron_coast_mission_1/art/tile_198_hd.png new file mode 100644 index 00000000..abd9bb21 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_198_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_198_sd.png b/examples/mods/iron_coast_mission_1/art/tile_198_sd.png new file mode 100644 index 00000000..b6448899 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_198_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_199_hd.png b/examples/mods/iron_coast_mission_1/art/tile_199_hd.png new file mode 100644 index 00000000..ec6e52e1 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_199_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_199_sd.png b/examples/mods/iron_coast_mission_1/art/tile_199_sd.png new file mode 100644 index 00000000..c4582c8f Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_199_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_200_hd.png b/examples/mods/iron_coast_mission_1/art/tile_200_hd.png new file mode 100644 index 00000000..1f1cc993 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_200_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_200_sd.png b/examples/mods/iron_coast_mission_1/art/tile_200_sd.png new file mode 100644 index 00000000..440b4d9e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_200_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_201_hd.png b/examples/mods/iron_coast_mission_1/art/tile_201_hd.png new file mode 100644 index 00000000..b54ee86e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_201_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_201_sd.png b/examples/mods/iron_coast_mission_1/art/tile_201_sd.png new file mode 100644 index 00000000..a0868dcf Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_201_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_202_hd.png b/examples/mods/iron_coast_mission_1/art/tile_202_hd.png new file mode 100644 index 00000000..1d205780 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_202_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_202_sd.png b/examples/mods/iron_coast_mission_1/art/tile_202_sd.png new file mode 100644 index 00000000..6fe4937e Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_202_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_203_hd.png b/examples/mods/iron_coast_mission_1/art/tile_203_hd.png new file mode 100644 index 00000000..771067f7 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_203_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_203_sd.png b/examples/mods/iron_coast_mission_1/art/tile_203_sd.png new file mode 100644 index 00000000..01c75d35 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_203_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_204_hd.png b/examples/mods/iron_coast_mission_1/art/tile_204_hd.png new file mode 100644 index 00000000..19ac97d8 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_204_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_204_sd.png b/examples/mods/iron_coast_mission_1/art/tile_204_sd.png new file mode 100644 index 00000000..576614fb Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_204_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_205_hd.png b/examples/mods/iron_coast_mission_1/art/tile_205_hd.png new file mode 100644 index 00000000..82dbbd2a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_205_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_205_sd.png b/examples/mods/iron_coast_mission_1/art/tile_205_sd.png new file mode 100644 index 00000000..f20b0ee3 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_205_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_206_hd.png b/examples/mods/iron_coast_mission_1/art/tile_206_hd.png new file mode 100644 index 00000000..df3136a4 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_206_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_206_sd.png b/examples/mods/iron_coast_mission_1/art/tile_206_sd.png new file mode 100644 index 00000000..00c2e288 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_206_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_207_hd.png b/examples/mods/iron_coast_mission_1/art/tile_207_hd.png new file mode 100644 index 00000000..f7655a18 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_207_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_207_sd.png b/examples/mods/iron_coast_mission_1/art/tile_207_sd.png new file mode 100644 index 00000000..b6ab3dc6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_207_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_208_hd.png b/examples/mods/iron_coast_mission_1/art/tile_208_hd.png new file mode 100644 index 00000000..23c30ae6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_208_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_208_sd.png b/examples/mods/iron_coast_mission_1/art/tile_208_sd.png new file mode 100644 index 00000000..38ecb7cc Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_208_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_209_hd.png b/examples/mods/iron_coast_mission_1/art/tile_209_hd.png new file mode 100644 index 00000000..b5714a3d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_209_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_209_sd.png b/examples/mods/iron_coast_mission_1/art/tile_209_sd.png new file mode 100644 index 00000000..76b78828 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_209_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_210_hd.png b/examples/mods/iron_coast_mission_1/art/tile_210_hd.png new file mode 100644 index 00000000..3386bb91 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_210_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_210_sd.png b/examples/mods/iron_coast_mission_1/art/tile_210_sd.png new file mode 100644 index 00000000..cd084390 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_210_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_211_hd.png b/examples/mods/iron_coast_mission_1/art/tile_211_hd.png new file mode 100644 index 00000000..61d6c4ac Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_211_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_211_sd.png b/examples/mods/iron_coast_mission_1/art/tile_211_sd.png new file mode 100644 index 00000000..8c12d27d Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_211_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_212_hd.png b/examples/mods/iron_coast_mission_1/art/tile_212_hd.png new file mode 100644 index 00000000..67c28601 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_212_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_212_sd.png b/examples/mods/iron_coast_mission_1/art/tile_212_sd.png new file mode 100644 index 00000000..89f060c6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_212_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_213_hd.png b/examples/mods/iron_coast_mission_1/art/tile_213_hd.png new file mode 100644 index 00000000..db32d9c6 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_213_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_213_sd.png b/examples/mods/iron_coast_mission_1/art/tile_213_sd.png new file mode 100644 index 00000000..cb83f0cd Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_213_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_214_hd.png b/examples/mods/iron_coast_mission_1/art/tile_214_hd.png new file mode 100644 index 00000000..fc96acec Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_214_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_214_sd.png b/examples/mods/iron_coast_mission_1/art/tile_214_sd.png new file mode 100644 index 00000000..baa25232 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_214_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_215_hd.png b/examples/mods/iron_coast_mission_1/art/tile_215_hd.png new file mode 100644 index 00000000..57fbee1b Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_215_hd.png differ diff --git a/examples/mods/iron_coast_mission_1/art/tile_215_sd.png b/examples/mods/iron_coast_mission_1/art/tile_215_sd.png new file mode 100644 index 00000000..2ec19443 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/art/tile_215_sd.png differ diff --git a/examples/mods/iron_coast_mission_1/asset-report.json b/examples/mods/iron_coast_mission_1/asset-report.json new file mode 100644 index 00000000..30e4b074 --- /dev/null +++ b/examples/mods/iron_coast_mission_1/asset-report.json @@ -0,0 +1,79 @@ +{ + "tile_count": 216, + "tile_logical_size": [ + 32, + 32 + ], + "hd_tile_size": [ + 128, + 128 + ], + "sd_palette_colors": 256, + "palette_sha256": "1cf6adb926d1dee1888b3e3d33b51a7049b3e5eb5f90a3067ef71c8970547204", + "vertical_repeat": "12 rows forward + 12 rows reflected; matching boundary pixels", + "seam_checks_passed": true, + "depot_transparent_pixels": 623, + "audio": { + "approach": { + "file": "audio/approach.wav", + "samples": 19845, + "seconds": 0.9, + "sha256": "148cf8ef6b521ef27dcca06f29af5d360f146c057a9b8dc1f793503993acb3cd" + }, + "dock_alarm": { + "file": "audio/dock_alarm.wav", + "samples": 22050, + "seconds": 1.0, + "sha256": "441275e69c4070859962c75bae2736db17596dfd4d05a298c1d8128be6214180" + }, + "boss_alarm": { + "file": "audio/boss_alarm.wav", + "samples": 24255, + "seconds": 1.1, + "sha256": "865d2e14156645071e4d918d406ff744e6bfd15f68360ce8d7888fce422d798f" + }, + "victory": { + "file": "audio/victory.wav", + "samples": 26460, + "seconds": 1.2, + "sha256": "df9c2e3aaa2e2811b8f4920d7e0bfffa4edba0e57c65d0d9fb5520115e4ecc93" + }, + "depot_open": { + "file": "audio/depot_open.wav", + "samples": 7717, + "seconds": 0.35, + "sha256": "373abe2cbf75f2d511a26705ed39837ded766f7a52ab7949a408399ad1ac3736" + }, + "repair_use": { + "file": "audio/repair_use.wav", + "samples": 13230, + "seconds": 0.6, + "sha256": "0a56bbaa3bcfd486a06ce24ebfb31753c6e69d530b45b5ffb54ee7015a06741a" + } + }, + "actual_sd_unique_colors": 122, + "sd_pixels_match_original_palette": true, + "sources": { + "terrain-source-hd.png": { + "size": [ + 1086, + 1448 + ], + "sha256": "1b31977372192fd1f597f7bd07fae9dd9a4d6004fcacca2278569b175bea5989" + }, + "briefing-source-hd.png": { + "size": [ + 1586, + 992 + ], + "sha256": "f41f3197a0290e560271fcd5306f555a42522e46b33a4ed32e1d042659dd869f" + }, + "depot-source-hd.png": { + "size": [ + 1254, + 1254 + ], + "sha256": "3aef4edd440c1eec7570ce5edce9825b1a65b55bf5472b8e594488874d45818c" + } + } +} diff --git a/examples/mods/iron_coast_mission_1/audio/approach.wav b/examples/mods/iron_coast_mission_1/audio/approach.wav new file mode 100644 index 00000000..6a068006 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/audio/approach.wav differ diff --git a/examples/mods/iron_coast_mission_1/audio/boss_alarm.wav b/examples/mods/iron_coast_mission_1/audio/boss_alarm.wav new file mode 100644 index 00000000..b1e8b0b0 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/audio/boss_alarm.wav differ diff --git a/examples/mods/iron_coast_mission_1/audio/depot_open.wav b/examples/mods/iron_coast_mission_1/audio/depot_open.wav new file mode 100644 index 00000000..76733986 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/audio/depot_open.wav differ diff --git a/examples/mods/iron_coast_mission_1/audio/dock_alarm.wav b/examples/mods/iron_coast_mission_1/audio/dock_alarm.wav new file mode 100644 index 00000000..4b52bd5a Binary files /dev/null and b/examples/mods/iron_coast_mission_1/audio/dock_alarm.wav differ diff --git a/examples/mods/iron_coast_mission_1/audio/repair_use.wav b/examples/mods/iron_coast_mission_1/audio/repair_use.wav new file mode 100644 index 00000000..6bdc8f09 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/audio/repair_use.wav differ diff --git a/examples/mods/iron_coast_mission_1/audio/victory.wav b/examples/mods/iron_coast_mission_1/audio/victory.wav new file mode 100644 index 00000000..82a976b2 Binary files /dev/null and b/examples/mods/iron_coast_mission_1/audio/victory.wav differ diff --git a/examples/mods/iron_coast_mission_1/build_assets.py b/examples/mods/iron_coast_mission_1/build_assets.py new file mode 100644 index 00000000..c9b71e21 --- /dev/null +++ b/examples/mods/iron_coast_mission_1/build_assets.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Package imagegen sources into palette SD/HD game footprints and synthesize WAVs. + +This performs deterministic cropping/resampling/palette conversion, not image +generation. The three imagegen originals remain beside the packaged local files. +""" +import argparse +import hashlib +import json +import math +import random +import struct +import wave +from pathlib import Path +from PIL import Image, ImageOps + +ROOT = Path(__file__).resolve().parent + + +def palette_image(path): + raw = path.read_bytes() + if len(raw) != 768: + raise ValueError("expected the original 768-byte VGA palette") + colors = [(min(value, 63) << 2) | (min(value, 63) >> 4) for value in raw] + palette = Image.new("P", (1, 1)) + palette.putpalette(colors) + return palette, colors + + +def sd(image, size, palette): + image = image.convert("RGBA").resize(size, Image.Resampling.LANCZOS) + alpha = image.getchannel("A").point(lambda a: 255 if a >= 128 else 0) + # Use one full-canvas palette pass before splitting terrain, so tile borders + # receive identical context and no independent quantizer creates seams. + rgb = image.convert("RGB").quantize(palette=palette, dither=Image.Dither.NONE).convert("RGBA") + rgb.putalpha(alpha) + return rgb + + +def wav(name, duration, notes, mode="tone"): + sample_rate = 22050 + rng = random.Random(7053 + sum(map(ord, name))) + values = bytearray() + count = int(duration * sample_rate) + for frame in range(count): + t = frame / sample_rate + section = min(len(notes) - 1, int(t / duration * len(notes))) + frequency = notes[section] + local = (t / duration * len(notes)) % 1 + envelope = min(1, local * 18) * (1 - local) ** 0.8 + signal = math.sin(2 * math.pi * frequency * t) + signal += 0.24 * math.sin(2 * math.pi * frequency * 2 * t) + if mode == "metal": + signal += 0.45 * rng.uniform(-1, 1) * math.exp(-t * 15) + signal += 0.3 * math.sin(2 * math.pi * 817 * t) * math.exp(-t * 10) + if mode == "alarm": + signal += 0.25 * math.sin(2 * math.pi * frequency * 0.5 * t) + value = max(-32767, min(32767, int(signal * envelope * 7200))) + values += struct.pack("=280 && !state.dock_alarm { + state.dock_alarm=true; + commands.push(#{op:"objective",key:"iron_coast_mission_1:phase",value:2}); + commands.push(#{op:"play_sound",sound:"iron_coast_mission_1:dock_alarm"}); + } + if event.tick>=720 && !state.boss_alarm { + state.boss_alarm=true; + commands.push(#{op:"objective",key:"iron_coast_mission_1:phase",value:3}); + commands.push(#{op:"play_sound",sound:"iron_coast_mission_1:boss_alarm"}); + } + let turrets=0; + if event.objectives.contains("iron_coast_turret:defeated") { + turrets=event.objectives["iron_coast_turret:defeated"]; + } + let boss=0; + if event.objectives.contains("iron_coast_boss:defeated") { + boss=event.objectives["iron_coast_boss:defeated"]; + } + // Surviving turrets must be disabled even if the command ship died first. + if turrets>=2 && boss>=1 && !state.complete { + state.complete=true; + commands.push(#{op:"objective",key:"iron_coast_mission_1:phase",value:4}); + commands.push(#{op:"objective",key:"iron_coast_mission_1:complete",value:1}); + commands.push(#{op:"credits",amount:5000}); + commands.push(#{op:"play_sound",sound:"iron_coast_mission_1:victory"}); + commands.push(#{op:"complete"}); + } + #{commands:commands,state:state} +} + +fn on_mission_end(event,state,random) { + let value=0; + if event.success {value=1;} + #{commands:[#{op:"objective",key:"iron_coast_mission_1:debrief",value:value}],state:state} +} diff --git a/examples/mods/iron_coast_mission_1/scripts/repair.rhai b/examples/mods/iron_coast_mission_1/scripts/repair.rhai new file mode 100644 index 00000000..e48cf1aa --- /dev/null +++ b/examples/mods/iron_coast_mission_1/scripts/repair.rhai @@ -0,0 +1,6 @@ +fn on_use(event,state,random) { + #{commands:[ + #{op:"shield",amount:25}, + #{op:"play_sound",sound:"iron_coast_mission_1:repair_use"} + ],state:state} +} diff --git a/examples/mods/iron_coast_scout/README.md b/examples/mods/iron_coast_scout/README.md new file mode 100644 index 00000000..723c4831 --- /dev/null +++ b/examples/mods/iron_coast_scout/README.md @@ -0,0 +1,17 @@ +# Iron Coast Scout + +Independent version 1.0.0 enemy package for mod API 1. Enable/disable it from the in-game Mods screen and use it in any mission through `iron_coast_scout:unit`. Iron Coast mission 1 supplies placements and completion objectives; this package does not add a mission or override base content. + +Airborne interceptor weaves left/right every 24 ticks, advances 1 pixel/tick, aims at the pilot and fires a 2 damage shell. + +Logical collision canvas `24×24`, top-left anchor `(0,0)`, health `14`, reward `75` credits, firing interval `54` simulation ticks. The engine halves boss health on difficulties 0/1; difficulty 2/3 uses the declared health. SD mode uses `art/unit-sd.png`; enable HD art in Settings for `art/unit-hd.png`. The HD canvas is 8× logical resolution and bypasses automatic ScaleNx. Projectile artwork has an 8×8 logical canvas and an independent 64×64 HD version. + +The boss uses two registered frames (idle/amber charged); the scout and turret use stable single frames. All supplied variants stay on the same canvas and anchor. `art-qa.json` records transparency bounds and, for the boss, animation silhouette overlap. + +Enemy deaths increment the current mission objective `iron_coast_scout:defeated`. Persistent script `destroyed`, `hits` and `shots` counters are statistics; current-mission objectives reset when the mission restarts. Callbacks guard retired entities and use deterministic simulation data. No wall clock or unseeded random input enters the scripts. + +Generated PNG masters, SD/HD strips, previews and WAV sounds are included in Git. Original reference images under `art/source/` are excluded. Exact built-in imagegen prompts are in `prompts/`, with source IDs and generated-image provenance in `provenance.json`. + +To rebuild derived artwork from supplied generated masters and regenerate audio, install Pillow and run `python3 build_assets.py`. To regenerate only audio with the Python standard library, run `python3 build_assets.py --audio-only`. WAVs are original procedural mono 16-bit PCM at 22050 Hz; `audio-recipe.json` records deterministic seeds, sizes and hashes. The art pipeline resizes and assembles supplied images; it does not redraw them. + +Validate from the repository root: `cargo run -- --mods examples/mods --validate-mods iron_coast_scout`. Synthetic behavior and save/replay tests: `cargo test --test enemies_mods`. Verify shipped artwork locally: `cargo test --test enemies_mods shipped_enemy_assets_are_valid_and_aligned`. diff --git a/examples/mods/iron_coast_scout/art-qa.json b/examples/mods/iron_coast_scout/art-qa.json new file mode 100644 index 00000000..14531f45 --- /dev/null +++ b/examples/mods/iron_coast_scout/art-qa.json @@ -0,0 +1,29 @@ +{ + "logical_size": [ + 24, + 24 + ], + "hd_scale": 8, + "origin": [ + 0, + 0 + ], + "frames": 1, + "frame_order": [ + "idle" + ], + "alpha": "Preserved RGBA; runtime uses alpha>=128 indexed transparency", + "registration": "Common full-canvas resize and equal padding for every frame", + "sources": [ + { + "source": "art/unit-generated-v1.png", + "source_sha256": "e2b6a0df185bda2e368e05a02af883cb99f5960cd379ad9454003449e1321503", + "sd_opaque_bounds": [ + 3, + 2, + 22, + 22 + ] + } + ] +} diff --git a/examples/mods/iron_coast_scout/art/preview.png b/examples/mods/iron_coast_scout/art/preview.png new file mode 100644 index 00000000..63a59a62 Binary files /dev/null and b/examples/mods/iron_coast_scout/art/preview.png differ diff --git a/examples/mods/iron_coast_scout/art/projectile-generated-v1.png b/examples/mods/iron_coast_scout/art/projectile-generated-v1.png new file mode 100644 index 00000000..b1d1da06 Binary files /dev/null and b/examples/mods/iron_coast_scout/art/projectile-generated-v1.png differ diff --git a/examples/mods/iron_coast_scout/art/projectile-hd.png b/examples/mods/iron_coast_scout/art/projectile-hd.png new file mode 100644 index 00000000..07fc070a Binary files /dev/null and b/examples/mods/iron_coast_scout/art/projectile-hd.png differ diff --git a/examples/mods/iron_coast_scout/art/projectile-sd.png b/examples/mods/iron_coast_scout/art/projectile-sd.png new file mode 100644 index 00000000..f3231717 Binary files /dev/null and b/examples/mods/iron_coast_scout/art/projectile-sd.png differ diff --git a/examples/mods/iron_coast_scout/art/unit-generated-v1.png b/examples/mods/iron_coast_scout/art/unit-generated-v1.png new file mode 100644 index 00000000..684371e4 Binary files /dev/null and b/examples/mods/iron_coast_scout/art/unit-generated-v1.png differ diff --git a/examples/mods/iron_coast_scout/art/unit-hd.png b/examples/mods/iron_coast_scout/art/unit-hd.png new file mode 100644 index 00000000..d322a55f Binary files /dev/null and b/examples/mods/iron_coast_scout/art/unit-hd.png differ diff --git a/examples/mods/iron_coast_scout/art/unit-sd.png b/examples/mods/iron_coast_scout/art/unit-sd.png new file mode 100644 index 00000000..f39e2bca Binary files /dev/null and b/examples/mods/iron_coast_scout/art/unit-sd.png differ diff --git a/examples/mods/iron_coast_scout/audio-recipe.json b/examples/mods/iron_coast_scout/audio-recipe.json new file mode 100644 index 00000000..af208d6b --- /dev/null +++ b/examples/mods/iron_coast_scout/audio-recipe.json @@ -0,0 +1,23 @@ +{ + "generator": "build_assets.py deterministic chirp/noise synthesis; no recordings", + "kind": "scout", + "seed": 19, + "files": { + "audio/shot.wav": { + "sample_rate": 22050, + "channels": 1, + "bits": 16, + "samples": 2425, + "peak": 25558, + "sha256": "094e3398d598762ac1d1eed70c7e3177e56f525c726385945e5071f4e215a00f" + }, + "audio/death.wav": { + "sample_rate": 22050, + "channels": 1, + "bits": 16, + "samples": 7497, + "peak": 25558, + "sha256": "066c916daecaa06358ea2fe99b482b9ce115e0f9a63296664063a738dca5212e" + } + } +} diff --git a/examples/mods/iron_coast_scout/audio/death.wav b/examples/mods/iron_coast_scout/audio/death.wav new file mode 100644 index 00000000..7a038416 Binary files /dev/null and b/examples/mods/iron_coast_scout/audio/death.wav differ diff --git a/examples/mods/iron_coast_scout/audio/shot.wav b/examples/mods/iron_coast_scout/audio/shot.wav new file mode 100644 index 00000000..de239e11 Binary files /dev/null and b/examples/mods/iron_coast_scout/audio/shot.wav differ diff --git a/examples/mods/iron_coast_scout/build_assets.py b/examples/mods/iron_coast_scout/build_assets.py new file mode 100644 index 00000000..496fcc88 --- /dev/null +++ b/examples/mods/iron_coast_scout/build_assets.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Convert saved imagegen artwork and synthesize reproducible package-local WAVs. + +This script resizes/atlases supplied art; it never redraws source or generated art. +Generated source PNGs stay in ignored art/. See prompts/ and provenance.json. +Use --audio-only to regenerate sound without Pillow or private artwork. +""" +import argparse +import hashlib +import json +import math +from pathlib import Path +import random +import struct +import wave + + +def sounds(root, kind): + rate = 22050 + values = { + "scout": (0.11, 1400, 380, 0.34, 230, 19), + "turret": (0.18, 900, 160, 0.52, 150, 37), + "boss": (0.24, 500, 80, 0.90, 75, 83), + } + shot_length, start, end, death_length, bass, seed = values[kind] + (root / "audio").mkdir(exist_ok=True) + stats = {} + for name, duration in [("shot", shot_length), ("death", death_length)]: + rng = random.Random(seed + (100 if name == "death" else 0)) + count = int(duration * rate) + phase = low_noise = 0.0 + samples = [] + for index in range(count): + t = index / rate + position = index / max(1, count - 1) + noise = rng.uniform(-1, 1) + low_noise = low_noise * 0.86 + noise * 0.14 + if name == "shot": + frequency = end + (start - end) * (1 - position) ** 2 + phase += 2 * math.pi * frequency / rate + value = (0.60 * math.sin(phase) + 0.15 * math.sin(2.31 * phase) + + 0.16 * noise + 0.16 * math.sin(2 * math.pi * bass * t)) + envelope = min(1, t / 0.002) * math.exp(-position * 6.0) + else: + frequency = bass * (1.0 - position * 0.65) + phase += 2 * math.pi * frequency / rate + value = 0.62 * low_noise + 0.19 * noise + 0.28 * math.sin(phase) + envelope = min(1, t / 0.003) * math.exp(-position * 4.7) + # Five milliseconds at the tail avoid discontinuities at mixer retirement. + envelope *= min(1, (count - 1 - index) / (rate * 0.005)) + samples.append(value * max(0, envelope)) + peak = max(abs(value) for value in samples) + pcm = [round(value / peak * 0.78 * 32767) for value in samples] + path = root / "audio" / (name + ".wav") + with wave.open(str(path), "wb") as output: + output.setnchannels(1) + output.setsampwidth(2) + output.setframerate(rate) + output.writeframes(struct.pack("<" + "h" * len(pcm), *pcm)) + stats[str(path.relative_to(root))] = { + "sample_rate": rate, "channels": 1, "bits": 16, + "samples": count, "peak": max(abs(value) for value in pcm), + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + (root / "audio-recipe.json").write_text(json.dumps({ + "generator": "build_assets.py deterministic chirp/noise synthesis; no recordings", + "kind": kind, "seed": seed, "files": stats, + }, indent=2) + "\n") + + +def artwork(root, kind): + from PIL import Image + dimensions = {"scout": (24, 24), "turret": (32, 32), "boss": (96, 64)} + width, height = dimensions[kind] + paths = [root / "art/unit-generated-v1.png"] + if kind == "boss": + paths.append(root / "art/unit-charge-generated-v1.png") + hd_frames, sd_frames = [], [] + stats = [] + for path in paths: + source = Image.open(path).convert("RGBA") + if source.width * height != source.height * width: + raise ValueError(f"{path}: source canvas aspect differs from logical canvas") + # Identical canvas resize and padding for all frames retain their registration. + hd = Image.new("RGBA", (width * 8, height * 8), (0, 0, 0, 0)) + pad_x, pad_y = width // 3, height // 3 + resized = source.resize((hd.width - 2 * pad_x, hd.height - 2 * pad_y), Image.Resampling.LANCZOS) + hd.alpha_composite(resized, (pad_x, pad_y)) + sd = hd.resize((width, height), Image.Resampling.BOX) + for image in [hd, sd]: + bbox = image.getchannel("A").point(lambda value: 255 if value >= 128 else 0).getbbox() + if bbox is None or bbox[0] == 0 or bbox[1] == 0 or bbox[2] == image.width or bbox[3] == image.height: + raise ValueError(f"{path}: opaque silhouette touches logical canvas edge") + hd_frames.append(hd) + sd_frames.append(sd) + stats.append({"source": str(path.relative_to(root)), + "source_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "sd_opaque_bounds": list(sd.getchannel("A").point(lambda value: 255 if value >= 128 else 0).getbbox())}) + for name, frames in [("unit-hd.png", hd_frames), ("unit-sd.png", sd_frames)]: + atlas = Image.new("RGBA", (frames[0].width * len(frames), frames[0].height), (0, 0, 0, 0)) + for index, frame in enumerate(frames): + atlas.alpha_composite(frame, (index * frame.width, 0)) + atlas.save(root / "art" / name) + # Shared visual ammunition language; each package includes its own local copy. + source = Image.open(root / "art/projectile-generated-v1.png").convert("RGBA") + hd = source.resize((64, 64), Image.Resampling.LANCZOS) + hd.save(root / "art/projectile-hd.png") + hd.resize((8, 8), Image.Resampling.BOX).save(root / "art/projectile-sd.png") + metadata = {"logical_size": [width, height], "hd_scale": 8, "origin": [0, 0], + "frames": len(paths), "frame_order": ["idle", "charged"][:len(paths)], + "alpha": "Preserved RGBA; runtime uses alpha>=128 indexed transparency", + "registration": "Common full-canvas resize and equal padding for every frame", + "sources": stats} + if len(hd_frames) == 2: + masks = [f.getchannel("A").point(lambda value: 255 if value >= 128 else 0) for f in hd_frames] + pairs = list(zip(masks[0].get_flattened_data(), masks[1].get_flattened_data())) + metadata["animation_silhouette_overlap"] = sum(bool(a and b) for a, b in pairs) / sum(bool(a or b) for a, b in pairs) + if metadata["animation_silhouette_overlap"] < 0.99: + raise ValueError("generated animation changes the silhouette beyond accepted registration tolerance") + (root / "art-qa.json").write_text(json.dumps(metadata, indent=2) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audio-only", action="store_true") + args = parser.parse_args() + root = Path(__file__).resolve().parent + kind = root.name.removeprefix("iron_coast_") + sounds(root, kind) + if not args.audio_only: + artwork(root, kind) + print(f"Generated {root.name} package assets") + + +if __name__ == "__main__": + main() diff --git a/examples/mods/iron_coast_scout/content.json b/examples/mods/iron_coast_scout/content.json new file mode 100644 index 00000000..873530fa --- /dev/null +++ b/examples/mods/iron_coast_scout/content.json @@ -0,0 +1,60 @@ +{ + "weapons": [ + { + "id": "iron_coast_scout:projectile", + "name": "Iron Coast Scout cannon shell", + "damage": 2, + "speed": 3, + "cooldown": 54, + "behavior": "straight", + "target": "all", + "art": { + "png": "art/projectile-sd.png", + "hd_png": "art/projectile-hd.png", + "logical_width": 8, + "logical_height": 8 + } + } + ], + "enemies": [ + { + "id": "iron_coast_scout:unit", + "name": "Iron Coast Scout", + "health": 14, + "speed": 0, + "reward": 75, + "ground": false, + "boss": false, + "cooldown": 54, + "art": { + "png": "art/unit-sd.png", + "hd_png": "art/unit-hd.png", + "logical_width": 24, + "logical_height": 24, + "frames": 1 + }, + "script": "scripts/ai.rhai", + "weapon": "iron_coast_scout:projectile" + } + ], + "sounds": [ + { + "id": "iron_coast_scout:shot", + "name": "Iron Coast Scout cannon", + "file": "audio/shot.wav", + "volume": 75, + "pan": 127, + "pitch": 128, + "priority": 2 + }, + { + "id": "iron_coast_scout:death", + "name": "Iron Coast Scout destruction", + "file": "audio/death.wav", + "volume": 100, + "pan": 127, + "pitch": 128, + "priority": 3 + } + ] +} diff --git a/examples/mods/iron_coast_scout/mod.toml b/examples/mods/iron_coast_scout/mod.toml new file mode 100644 index 00000000..302ef853 --- /dev/null +++ b/examples/mods/iron_coast_scout/mod.toml @@ -0,0 +1,7 @@ +id = "iron_coast_scout" +name = "Iron Coast Scout" +version = "1.0.0" +api_version = 1 +description = "An agile weaving interceptor with amber cannon fire. Includes SD and HD artwork, deterministic Rhai AI and generated sound effects." +content = "content.json" +scripts = ["scripts/ai.rhai"] diff --git a/examples/mods/iron_coast_scout/prompts/projectile-hd-v1.txt b/examples/mods/iron_coast_scout/prompts/projectile-hd-v1.txt new file mode 100644 index 00000000..92306a65 --- /dev/null +++ b/examples/mods/iron_coast_scout/prompts/projectile-hd-v1.txt @@ -0,0 +1,6 @@ +Use case: style-transfer +Asset type: transparent HD projectile sprite for hostile cannon fire in a top-down military science-fiction shoot-em-up. +Input: original Raptor ESHOT_BLK 8×8 amber projectile, enlarged nearest-neighbor, is a reference for color and readability. +Primary request: a single small compact circular amber/orange energy shell viewed directly overhead, bright pale-yellow center fading to orange metal/plasma rim, dark crisp outer edge. Match the naval steel/amber Raptor remaster style. It will be reduced to an 8×8 logical square, so keep a simple readable round footprint and avoid microdetails. This is one ammunition projectile, no weapon or spacecraft. +Composition: perfectly centered isolated circular sprite on square canvas, visible orb occupies about62% width/height with equal large transparent margins. Keep all glow contained inside the circular orb, no external haze, shadow, trail or rays. Do not stretch it. +Constraints: true transparent background, no extra objects, no checkerboard or black background baked into pixels, no text, logos, watermarks, perspective or decorative framing. diff --git a/examples/mods/iron_coast_scout/prompts/unit-hd-v1.txt b/examples/mods/iron_coast_scout/prompts/unit-hd-v1.txt new file mode 100644 index 00000000..e4b3e5d7 --- /dev/null +++ b/examples/mods/iron_coast_scout/prompts/unit-hd-v1.txt @@ -0,0 +1,6 @@ +Use case: style-transfer +Asset type: transparent HD sprite for the Iron Coast Scout enemy, a top-down vertical shoot-em-up. +Input image: the supplied original 24×24 Raptor SHIP39G1_PIC sprite is the source of truth for the silhouette and orientation, enlarged with nearest-neighbor only. +Primary request: create one polished HD remaster of this small agile military science-fiction fighter, preserving the exact overhead view, V-shaped swept wings at the top and narrow central nose pointing straight DOWN. Naval steel/slate armor, subtle olive panels, restrained tiny amber engine vents and red service markings, crisp machined panel seams and readable shape. This is an enemy fighter facing down toward the player. Match the utilitarian 1990s Raptor design language while adding clean HD surface detail. +Composition: centered on a square transparent canvas. Keep original proportions and symmetric straight-flight pose. Ship fills approximately 92% of canvas width and 92% of height, with a small equal transparent margin; no part clipped. No perspective, no rotation, no banking. No extra external parts or trails. Directional lighting gently from upper left, dark edges, no cast shadow outside ship. +Constraints: one single isolated sprite, genuine transparent background, no lettering, logos, watermarks, environment, ground plane, drop shadow, border or checkerboard painted into pixels. Preserve alpha transparency and the original silhouette; do not put the ship in a photograph or poster. diff --git a/examples/mods/iron_coast_scout/provenance.json b/examples/mods/iron_coast_scout/provenance.json new file mode 100644 index 00000000..c76c08e9 --- /dev/null +++ b/examples/mods/iron_coast_scout/provenance.json @@ -0,0 +1,30 @@ +{ + "api_version": 1, + "mod_version": "1.0.0", + "tool": "built-in image_gen.imagegen via imagegen skill", + "skill": "/home/quintine/.codex/skills/.system/imagegen/SKILL.md", + "generated_at": "2026-10-10", + "source_reference": { + "asset_id_hex": "10344", + "catalog_path": "sprites/SHIP39G1_PIC", + "sha256": "7cb09e925ea7786b331d576c360aad09b5e18f35d0258ec78ab5ef2e50c2590e", + "role": "Original silhouette/orientation/style reference inspected before imagegen. Original reference pixels excluded from Git under art/source/." + }, + "assets": [ + { + "file": "art/unit-generated-v1.png", + "prompt": "prompts/unit-hd-v1.txt", + "tool_output": "exec-3dc36f34-2cec-45f6-8262-ea3a5e577396.png", + "sha256": "e2b6a0df185bda2e368e05a02af883cb99f5960cd379ad9454003449e1321503" + }, + { + "file": "art/projectile-generated-v1.png", + "prompt": "prompts/projectile-hd-v1.txt", + "tool_output": "exec-96076703-18b2-4aad-9bce-4eaceef4d8bd.png", + "note": "Common generated amber projectile copied into each independent package.", + "sha256": "386709d83ebf8a9e6c5119c785afc77152ae934780b3b0afcf8ede510622bc20" + } + ], + "pipeline": "build_assets.py: common full-canvas resize/padding, alpha-preserving RGBA SD/8xHD strips; no bitmap redraw", + "version_control": "Generated masters, shipped SD/HD artwork, previews and synthesized WAVs included in Git per user request. Original catalog reference pixels excluded." +} diff --git a/examples/mods/iron_coast_scout/scripts/ai.rhai b/examples/mods/iron_coast_scout/scripts/ai.rhai new file mode 100644 index 00000000..669cfb18 --- /dev/null +++ b/examples/mods/iron_coast_scout/scripts/ai.rhai @@ -0,0 +1,40 @@ +fn is_alive(event) { + for unit in event.entities { + if unit.entity == event.entity && unit.health > 0 { return true; } + } + false +} + +// Velocity changes are committed after the callback completes. +fn on_spawn(event, state, random) { + if !is_alive(event) { return #{ commands: [], state: state }; } + #{ commands: [#{ op: "velocity", entity: event.entity, vx: 1, vy: 1 }], state: state } +} + +fn on_tick(event, state, random) { + if event.health <= 0 || !is_alive(event) { return #{ commands: [], state: state }; } + let sideways = if ((event.tick + event.entity * 7) / 24) % 2 == 0 { 1 } else { -1 }; + if event.x <= 24 { sideways = 1; } + if event.x >= 272 { sideways = -1; } + #{ commands: [#{ op: "velocity", entity: event.entity, vx: sideways, vy: 1 }], state: state } +} + +fn on_fire(event, state, random) { + let shots = if state.contains("shots") { state.shots + 1 } else { 1 }; + state.shots = shots; + #{ commands: [#{ op: "play_sound", sound: "iron_coast_scout:shot" }], state: state } +} + +fn on_hit(event, state, random) { + state.hits = if state.contains("hits") { state.hits + 1 } else { 1 }; + #{ commands: [], state: state } +} + +fn on_death(event, state, random) { + state.destroyed = if state.contains("destroyed") { state.destroyed + 1 } else { 1 }; + let defeated = if event.objectives.contains("iron_coast_scout:defeated") { event.objectives["iron_coast_scout:defeated"] + 1 } else { 1 }; + #{ commands: [ + #{ op: "objective", key: "iron_coast_scout:defeated", value: defeated }, + #{ op: "play_sound", sound: "iron_coast_scout:death" } + ], state: state } +} diff --git a/examples/mods/iron_coast_turret/README.md b/examples/mods/iron_coast_turret/README.md new file mode 100644 index 00000000..53bd7992 --- /dev/null +++ b/examples/mods/iron_coast_turret/README.md @@ -0,0 +1,17 @@ +# Iron Coast Turret + +Independent version 1.0.0 enemy package for mod API 1. Enable/disable it from the in-game Mods screen and use it in any mission through `iron_coast_turret:unit`. Iron Coast mission 1 supplies placements and completion objectives; this package does not add a mission or override base content. + +Ground emplacement scrolls with terrain at 1 pixel/tick and fires 3 damage aimed shells. Ground targeting rules still apply. + +Logical collision canvas `32×32`, top-left anchor `(0,0)`, health `30`, reward `120` credits, firing interval `70` simulation ticks. The engine halves boss health on difficulties 0/1; difficulty 2/3 uses the declared health. SD mode uses `art/unit-sd.png`; enable HD art in Settings for `art/unit-hd.png`. The HD canvas is 8× logical resolution and bypasses automatic ScaleNx. Projectile artwork has an 8×8 logical canvas and an independent 64×64 HD version. + +The boss uses two registered frames (idle/amber charged); the scout and turret use stable single frames. All supplied variants stay on the same canvas and anchor. `art-qa.json` records transparency bounds and, for the boss, animation silhouette overlap. + +Enemy deaths increment the current mission objective `iron_coast_turret:defeated`. Persistent script `destroyed`, `hits` and `shots` counters are statistics; current-mission objectives reset when the mission restarts. Callbacks guard retired entities and use deterministic simulation data. No wall clock or unseeded random input enters the scripts. + +Generated PNG masters, SD/HD strips, previews and WAV sounds are included in Git. Original reference images under `art/source/` are excluded. Exact built-in imagegen prompts are in `prompts/`, with source IDs and generated-image provenance in `provenance.json`. + +To rebuild derived artwork from supplied generated masters and regenerate audio, install Pillow and run `python3 build_assets.py`. To regenerate only audio with the Python standard library, run `python3 build_assets.py --audio-only`. WAVs are original procedural mono 16-bit PCM at 22050 Hz; `audio-recipe.json` records deterministic seeds, sizes and hashes. The art pipeline resizes and assembles supplied images; it does not redraw them. + +Validate from the repository root: `cargo run -- --mods examples/mods --validate-mods iron_coast_turret`. Synthetic behavior and save/replay tests: `cargo test --test enemies_mods`. Verify shipped artwork locally: `cargo test --test enemies_mods shipped_enemy_assets_are_valid_and_aligned`. diff --git a/examples/mods/iron_coast_turret/art-qa.json b/examples/mods/iron_coast_turret/art-qa.json new file mode 100644 index 00000000..24d6d6de --- /dev/null +++ b/examples/mods/iron_coast_turret/art-qa.json @@ -0,0 +1,29 @@ +{ + "logical_size": [ + 32, + 32 + ], + "hd_scale": 8, + "origin": [ + 0, + 0 + ], + "frames": 1, + "frame_order": [ + "idle" + ], + "alpha": "Preserved RGBA; runtime uses alpha>=128 indexed transparency", + "registration": "Common full-canvas resize and equal padding for every frame", + "sources": [ + { + "source": "art/unit-generated-v1.png", + "source_sha256": "37a7be58863d84028b01f67b133ae694d070cfe8155d939c0cbe81ca4ab621a7", + "sd_opaque_bounds": [ + 3, + 2, + 29, + 30 + ] + } + ] +} diff --git a/examples/mods/iron_coast_turret/art/preview.png b/examples/mods/iron_coast_turret/art/preview.png new file mode 100644 index 00000000..27e45256 Binary files /dev/null and b/examples/mods/iron_coast_turret/art/preview.png differ diff --git a/examples/mods/iron_coast_turret/art/projectile-generated-v1.png b/examples/mods/iron_coast_turret/art/projectile-generated-v1.png new file mode 100644 index 00000000..b1d1da06 Binary files /dev/null and b/examples/mods/iron_coast_turret/art/projectile-generated-v1.png differ diff --git a/examples/mods/iron_coast_turret/art/projectile-hd.png b/examples/mods/iron_coast_turret/art/projectile-hd.png new file mode 100644 index 00000000..07fc070a Binary files /dev/null and b/examples/mods/iron_coast_turret/art/projectile-hd.png differ diff --git a/examples/mods/iron_coast_turret/art/projectile-sd.png b/examples/mods/iron_coast_turret/art/projectile-sd.png new file mode 100644 index 00000000..f3231717 Binary files /dev/null and b/examples/mods/iron_coast_turret/art/projectile-sd.png differ diff --git a/examples/mods/iron_coast_turret/art/unit-generated-v1.png b/examples/mods/iron_coast_turret/art/unit-generated-v1.png new file mode 100644 index 00000000..8c9ac16c Binary files /dev/null and b/examples/mods/iron_coast_turret/art/unit-generated-v1.png differ diff --git a/examples/mods/iron_coast_turret/art/unit-hd.png b/examples/mods/iron_coast_turret/art/unit-hd.png new file mode 100644 index 00000000..9714e8b7 Binary files /dev/null and b/examples/mods/iron_coast_turret/art/unit-hd.png differ diff --git a/examples/mods/iron_coast_turret/art/unit-sd.png b/examples/mods/iron_coast_turret/art/unit-sd.png new file mode 100644 index 00000000..17c9dc05 Binary files /dev/null and b/examples/mods/iron_coast_turret/art/unit-sd.png differ diff --git a/examples/mods/iron_coast_turret/audio-recipe.json b/examples/mods/iron_coast_turret/audio-recipe.json new file mode 100644 index 00000000..86f76485 --- /dev/null +++ b/examples/mods/iron_coast_turret/audio-recipe.json @@ -0,0 +1,23 @@ +{ + "generator": "build_assets.py deterministic chirp/noise synthesis; no recordings", + "kind": "turret", + "seed": 37, + "files": { + "audio/shot.wav": { + "sample_rate": 22050, + "channels": 1, + "bits": 16, + "samples": 3969, + "peak": 25558, + "sha256": "0149d363391a8acfb61dbcbd76ba21a994dec58c4531b65fe0570e968b014308" + }, + "audio/death.wav": { + "sample_rate": 22050, + "channels": 1, + "bits": 16, + "samples": 11466, + "peak": 25558, + "sha256": "3d891b723229608f96886f9fa15a33d39ab2f5e729177b98e9d0c0afebd24854" + } + } +} diff --git a/examples/mods/iron_coast_turret/audio/death.wav b/examples/mods/iron_coast_turret/audio/death.wav new file mode 100644 index 00000000..92bb36a0 Binary files /dev/null and b/examples/mods/iron_coast_turret/audio/death.wav differ diff --git a/examples/mods/iron_coast_turret/audio/shot.wav b/examples/mods/iron_coast_turret/audio/shot.wav new file mode 100644 index 00000000..5a3b1f49 Binary files /dev/null and b/examples/mods/iron_coast_turret/audio/shot.wav differ diff --git a/examples/mods/iron_coast_turret/build_assets.py b/examples/mods/iron_coast_turret/build_assets.py new file mode 100644 index 00000000..496fcc88 --- /dev/null +++ b/examples/mods/iron_coast_turret/build_assets.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Convert saved imagegen artwork and synthesize reproducible package-local WAVs. + +This script resizes/atlases supplied art; it never redraws source or generated art. +Generated source PNGs stay in ignored art/. See prompts/ and provenance.json. +Use --audio-only to regenerate sound without Pillow or private artwork. +""" +import argparse +import hashlib +import json +import math +from pathlib import Path +import random +import struct +import wave + + +def sounds(root, kind): + rate = 22050 + values = { + "scout": (0.11, 1400, 380, 0.34, 230, 19), + "turret": (0.18, 900, 160, 0.52, 150, 37), + "boss": (0.24, 500, 80, 0.90, 75, 83), + } + shot_length, start, end, death_length, bass, seed = values[kind] + (root / "audio").mkdir(exist_ok=True) + stats = {} + for name, duration in [("shot", shot_length), ("death", death_length)]: + rng = random.Random(seed + (100 if name == "death" else 0)) + count = int(duration * rate) + phase = low_noise = 0.0 + samples = [] + for index in range(count): + t = index / rate + position = index / max(1, count - 1) + noise = rng.uniform(-1, 1) + low_noise = low_noise * 0.86 + noise * 0.14 + if name == "shot": + frequency = end + (start - end) * (1 - position) ** 2 + phase += 2 * math.pi * frequency / rate + value = (0.60 * math.sin(phase) + 0.15 * math.sin(2.31 * phase) + + 0.16 * noise + 0.16 * math.sin(2 * math.pi * bass * t)) + envelope = min(1, t / 0.002) * math.exp(-position * 6.0) + else: + frequency = bass * (1.0 - position * 0.65) + phase += 2 * math.pi * frequency / rate + value = 0.62 * low_noise + 0.19 * noise + 0.28 * math.sin(phase) + envelope = min(1, t / 0.003) * math.exp(-position * 4.7) + # Five milliseconds at the tail avoid discontinuities at mixer retirement. + envelope *= min(1, (count - 1 - index) / (rate * 0.005)) + samples.append(value * max(0, envelope)) + peak = max(abs(value) for value in samples) + pcm = [round(value / peak * 0.78 * 32767) for value in samples] + path = root / "audio" / (name + ".wav") + with wave.open(str(path), "wb") as output: + output.setnchannels(1) + output.setsampwidth(2) + output.setframerate(rate) + output.writeframes(struct.pack("<" + "h" * len(pcm), *pcm)) + stats[str(path.relative_to(root))] = { + "sample_rate": rate, "channels": 1, "bits": 16, + "samples": count, "peak": max(abs(value) for value in pcm), + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + (root / "audio-recipe.json").write_text(json.dumps({ + "generator": "build_assets.py deterministic chirp/noise synthesis; no recordings", + "kind": kind, "seed": seed, "files": stats, + }, indent=2) + "\n") + + +def artwork(root, kind): + from PIL import Image + dimensions = {"scout": (24, 24), "turret": (32, 32), "boss": (96, 64)} + width, height = dimensions[kind] + paths = [root / "art/unit-generated-v1.png"] + if kind == "boss": + paths.append(root / "art/unit-charge-generated-v1.png") + hd_frames, sd_frames = [], [] + stats = [] + for path in paths: + source = Image.open(path).convert("RGBA") + if source.width * height != source.height * width: + raise ValueError(f"{path}: source canvas aspect differs from logical canvas") + # Identical canvas resize and padding for all frames retain their registration. + hd = Image.new("RGBA", (width * 8, height * 8), (0, 0, 0, 0)) + pad_x, pad_y = width // 3, height // 3 + resized = source.resize((hd.width - 2 * pad_x, hd.height - 2 * pad_y), Image.Resampling.LANCZOS) + hd.alpha_composite(resized, (pad_x, pad_y)) + sd = hd.resize((width, height), Image.Resampling.BOX) + for image in [hd, sd]: + bbox = image.getchannel("A").point(lambda value: 255 if value >= 128 else 0).getbbox() + if bbox is None or bbox[0] == 0 or bbox[1] == 0 or bbox[2] == image.width or bbox[3] == image.height: + raise ValueError(f"{path}: opaque silhouette touches logical canvas edge") + hd_frames.append(hd) + sd_frames.append(sd) + stats.append({"source": str(path.relative_to(root)), + "source_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "sd_opaque_bounds": list(sd.getchannel("A").point(lambda value: 255 if value >= 128 else 0).getbbox())}) + for name, frames in [("unit-hd.png", hd_frames), ("unit-sd.png", sd_frames)]: + atlas = Image.new("RGBA", (frames[0].width * len(frames), frames[0].height), (0, 0, 0, 0)) + for index, frame in enumerate(frames): + atlas.alpha_composite(frame, (index * frame.width, 0)) + atlas.save(root / "art" / name) + # Shared visual ammunition language; each package includes its own local copy. + source = Image.open(root / "art/projectile-generated-v1.png").convert("RGBA") + hd = source.resize((64, 64), Image.Resampling.LANCZOS) + hd.save(root / "art/projectile-hd.png") + hd.resize((8, 8), Image.Resampling.BOX).save(root / "art/projectile-sd.png") + metadata = {"logical_size": [width, height], "hd_scale": 8, "origin": [0, 0], + "frames": len(paths), "frame_order": ["idle", "charged"][:len(paths)], + "alpha": "Preserved RGBA; runtime uses alpha>=128 indexed transparency", + "registration": "Common full-canvas resize and equal padding for every frame", + "sources": stats} + if len(hd_frames) == 2: + masks = [f.getchannel("A").point(lambda value: 255 if value >= 128 else 0) for f in hd_frames] + pairs = list(zip(masks[0].get_flattened_data(), masks[1].get_flattened_data())) + metadata["animation_silhouette_overlap"] = sum(bool(a and b) for a, b in pairs) / sum(bool(a or b) for a, b in pairs) + if metadata["animation_silhouette_overlap"] < 0.99: + raise ValueError("generated animation changes the silhouette beyond accepted registration tolerance") + (root / "art-qa.json").write_text(json.dumps(metadata, indent=2) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audio-only", action="store_true") + args = parser.parse_args() + root = Path(__file__).resolve().parent + kind = root.name.removeprefix("iron_coast_") + sounds(root, kind) + if not args.audio_only: + artwork(root, kind) + print(f"Generated {root.name} package assets") + + +if __name__ == "__main__": + main() diff --git a/examples/mods/iron_coast_turret/content.json b/examples/mods/iron_coast_turret/content.json new file mode 100644 index 00000000..30c4ec1e --- /dev/null +++ b/examples/mods/iron_coast_turret/content.json @@ -0,0 +1,60 @@ +{ + "weapons": [ + { + "id": "iron_coast_turret:projectile", + "name": "Iron Coast Turret cannon shell", + "damage": 3, + "speed": 3, + "cooldown": 70, + "behavior": "straight", + "target": "all", + "art": { + "png": "art/projectile-sd.png", + "hd_png": "art/projectile-hd.png", + "logical_width": 8, + "logical_height": 8 + } + } + ], + "enemies": [ + { + "id": "iron_coast_turret:unit", + "name": "Iron Coast Turret", + "health": 30, + "speed": 0, + "reward": 120, + "ground": true, + "boss": false, + "cooldown": 70, + "art": { + "png": "art/unit-sd.png", + "hd_png": "art/unit-hd.png", + "logical_width": 32, + "logical_height": 32, + "frames": 1 + }, + "script": "scripts/ai.rhai", + "weapon": "iron_coast_turret:projectile" + } + ], + "sounds": [ + { + "id": "iron_coast_turret:shot", + "name": "Iron Coast Turret cannon", + "file": "audio/shot.wav", + "volume": 95, + "pan": 127, + "pitch": 128, + "priority": 2 + }, + { + "id": "iron_coast_turret:death", + "name": "Iron Coast Turret destruction", + "file": "audio/death.wav", + "volume": 120, + "pan": 127, + "pitch": 128, + "priority": 3 + } + ] +} diff --git a/examples/mods/iron_coast_turret/mod.toml b/examples/mods/iron_coast_turret/mod.toml new file mode 100644 index 00000000..5ff34846 --- /dev/null +++ b/examples/mods/iron_coast_turret/mod.toml @@ -0,0 +1,7 @@ +id = "iron_coast_turret" +name = "Iron Coast Turret" +version = "1.0.0" +api_version = 1 +description = "Armored ground defense with aimed heavy shells. Includes SD and HD artwork, deterministic Rhai AI and generated sound effects." +content = "content.json" +scripts = ["scripts/ai.rhai"] diff --git a/examples/mods/iron_coast_turret/prompts/projectile-hd-v1.txt b/examples/mods/iron_coast_turret/prompts/projectile-hd-v1.txt new file mode 100644 index 00000000..92306a65 --- /dev/null +++ b/examples/mods/iron_coast_turret/prompts/projectile-hd-v1.txt @@ -0,0 +1,6 @@ +Use case: style-transfer +Asset type: transparent HD projectile sprite for hostile cannon fire in a top-down military science-fiction shoot-em-up. +Input: original Raptor ESHOT_BLK 8×8 amber projectile, enlarged nearest-neighbor, is a reference for color and readability. +Primary request: a single small compact circular amber/orange energy shell viewed directly overhead, bright pale-yellow center fading to orange metal/plasma rim, dark crisp outer edge. Match the naval steel/amber Raptor remaster style. It will be reduced to an 8×8 logical square, so keep a simple readable round footprint and avoid microdetails. This is one ammunition projectile, no weapon or spacecraft. +Composition: perfectly centered isolated circular sprite on square canvas, visible orb occupies about62% width/height with equal large transparent margins. Keep all glow contained inside the circular orb, no external haze, shadow, trail or rays. Do not stretch it. +Constraints: true transparent background, no extra objects, no checkerboard or black background baked into pixels, no text, logos, watermarks, perspective or decorative framing. diff --git a/examples/mods/iron_coast_turret/prompts/unit-hd-v1.txt b/examples/mods/iron_coast_turret/prompts/unit-hd-v1.txt new file mode 100644 index 00000000..ea59481f --- /dev/null +++ b/examples/mods/iron_coast_turret/prompts/unit-hd-v1.txt @@ -0,0 +1,6 @@ +Use case: style-transfer +Asset type: transparent HD sprite for Iron Coast armored ground turret in a top-down vertical shoot-em-up. +Input image: original 24×24 Raptor SHIP37G1_PIC enlarged nearest-neighbor, reference for the compact circular armored emplacement, overhead camera and utilitarian style. Adapt it to a 32×32 logical square canvas. +Primary request: one HD remaster of a heavy oval/circular ground defense turret. Two parallel recessed cannon ports facing DOWN along the central vertical axis, surrounded by concentric naval steel and dark olive armored plates, small dark red inner gun wells, bolted side plating. Maintain the original compact oval footprint, fully overhead with no visible side walls or horizon; weapons belong to a stationary ground installation, not an aircraft. Restrained amber indicator lights. Readable dark outline and precise industrial panel detail. +Composition: square canvas, centered, approximately 90% width and 90% height, symmetric and aligned on exact center; keep small equal transparent margins. Gentle top-left illumination, no external shadow. Cannon barrels remain contained within the turret footprint. +Constraints: single isolated sprite, genuine transparent background, no ground, floor, text, logos, watermark, smoke, muzzle flash, trails, border or checkerboard painted into pixels. No perspective or rotation. diff --git a/examples/mods/iron_coast_turret/provenance.json b/examples/mods/iron_coast_turret/provenance.json new file mode 100644 index 00000000..401d9fc8 --- /dev/null +++ b/examples/mods/iron_coast_turret/provenance.json @@ -0,0 +1,30 @@ +{ + "api_version": 1, + "mod_version": "1.0.0", + "tool": "built-in image_gen.imagegen via imagegen skill", + "skill": "/home/quintine/.codex/skills/.system/imagegen/SKILL.md", + "generated_at": "2026-10-10", + "source_reference": { + "asset_id_hex": "1033e", + "catalog_path": "sprites/SHIP37G1_PIC", + "sha256": "00a6f1bebfb75af8dafbef2b974417f95580827ebcf5181f5ba6712845eaf865", + "role": "Original silhouette/orientation/style reference inspected before imagegen. Original reference pixels excluded from Git under art/source/." + }, + "assets": [ + { + "file": "art/unit-generated-v1.png", + "prompt": "prompts/unit-hd-v1.txt", + "tool_output": "exec-6df0f241-f8a0-4552-80f6-e3d0aef35aad.png", + "sha256": "37a7be58863d84028b01f67b133ae694d070cfe8155d939c0cbe81ca4ab621a7" + }, + { + "file": "art/projectile-generated-v1.png", + "prompt": "prompts/projectile-hd-v1.txt", + "tool_output": "exec-96076703-18b2-4aad-9bce-4eaceef4d8bd.png", + "note": "Common generated amber projectile copied into each independent package.", + "sha256": "386709d83ebf8a9e6c5119c785afc77152ae934780b3b0afcf8ede510622bc20" + } + ], + "pipeline": "build_assets.py: common full-canvas resize/padding, alpha-preserving RGBA SD/8xHD strips; no bitmap redraw", + "version_control": "Generated masters, shipped SD/HD artwork, previews and synthesized WAVs included in Git per user request. Original catalog reference pixels excluded." +} diff --git a/examples/mods/iron_coast_turret/scripts/ai.rhai b/examples/mods/iron_coast_turret/scripts/ai.rhai new file mode 100644 index 00000000..bb34b0c8 --- /dev/null +++ b/examples/mods/iron_coast_turret/scripts/ai.rhai @@ -0,0 +1,27 @@ +// Ground emplacements inherit deterministic terrain scrolling (one pixel/tick). +fn on_spawn(event, state, random) { + #{ commands: [], state: state } +} + +fn on_tick(event, state, random) { + #{ commands: [], state: state } +} + +fn on_fire(event, state, random) { + state.shots = if state.contains("shots") { state.shots + 1 } else { 1 }; + #{ commands: [#{ op: "play_sound", sound: "iron_coast_turret:shot" }], state: state } +} + +fn on_hit(event, state, random) { + state.hits = if state.contains("hits") { state.hits + 1 } else { 1 }; + #{ commands: [], state: state } +} + +fn on_death(event, state, random) { + state.destroyed = if state.contains("destroyed") { state.destroyed + 1 } else { 1 }; + let defeated = if event.objectives.contains("iron_coast_turret:defeated") { event.objectives["iron_coast_turret:defeated"] + 1 } else { 1 }; + #{ commands: [ + #{ op: "objective", key: "iron_coast_turret:defeated", value: defeated }, + #{ op: "play_sound", sound: "iron_coast_turret:death" } + ], state: state } +} diff --git a/examples/mods/pulse_cannon/ART_PROVENANCE.md b/examples/mods/pulse_cannon/ART_PROVENANCE.md new file mode 100644 index 00000000..6c6ba8ea --- /dev/null +++ b/examples/mods/pulse_cannon/ART_PROVENANCE.md @@ -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. diff --git a/examples/mods/pulse_cannon/README.md b/examples/mods/pulse_cannon/README.md new file mode 100644 index 00000000..28678dc2 --- /dev/null +++ b/examples/mods/pulse_cannon/README.md @@ -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. diff --git a/examples/mods/pulse_cannon/art-provenance.json b/examples/mods/pulse_cannon/art-provenance.json new file mode 100644 index 00000000..6837eda8 --- /dev/null +++ b/examples/mods/pulse_cannon/art-provenance.json @@ -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" + } + } +} diff --git a/examples/mods/pulse_cannon/art/equipment-generated-source.png b/examples/mods/pulse_cannon/art/equipment-generated-source.png new file mode 100644 index 00000000..7d166801 Binary files /dev/null and b/examples/mods/pulse_cannon/art/equipment-generated-source.png differ diff --git a/examples/mods/pulse_cannon/art/equipment-hd.png b/examples/mods/pulse_cannon/art/equipment-hd.png new file mode 100644 index 00000000..44af94d5 Binary files /dev/null and b/examples/mods/pulse_cannon/art/equipment-hd.png differ diff --git a/examples/mods/pulse_cannon/art/equipment-sd.png b/examples/mods/pulse_cannon/art/equipment-sd.png new file mode 100644 index 00000000..c2d11f0a Binary files /dev/null and b/examples/mods/pulse_cannon/art/equipment-sd.png differ diff --git a/examples/mods/pulse_cannon/art/projectile-generated-source.png b/examples/mods/pulse_cannon/art/projectile-generated-source.png new file mode 100644 index 00000000..5874cafa Binary files /dev/null and b/examples/mods/pulse_cannon/art/projectile-generated-source.png differ diff --git a/examples/mods/pulse_cannon/art/projectile-hd.png b/examples/mods/pulse_cannon/art/projectile-hd.png new file mode 100644 index 00000000..f847883c Binary files /dev/null and b/examples/mods/pulse_cannon/art/projectile-hd.png differ diff --git a/examples/mods/pulse_cannon/art/projectile-sd.png b/examples/mods/pulse_cannon/art/projectile-sd.png new file mode 100644 index 00000000..41aabb88 Binary files /dev/null and b/examples/mods/pulse_cannon/art/projectile-sd.png differ diff --git a/examples/mods/pulse_cannon/audio/fire.wav b/examples/mods/pulse_cannon/audio/fire.wav new file mode 100644 index 00000000..18c5371b Binary files /dev/null and b/examples/mods/pulse_cannon/audio/fire.wav differ diff --git a/examples/mods/pulse_cannon/audio/impact.wav b/examples/mods/pulse_cannon/audio/impact.wav new file mode 100644 index 00000000..16ae071c Binary files /dev/null and b/examples/mods/pulse_cannon/audio/impact.wav differ diff --git a/examples/mods/pulse_cannon/content.json b/examples/mods/pulse_cannon/content.json new file mode 100644 index 00000000..aa320c79 --- /dev/null +++ b/examples/mods/pulse_cannon/content.json @@ -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 + } + ] +} diff --git a/examples/mods/pulse_cannon/generate_audio.py b/examples/mods/pulse_cannon/generate_audio.py new file mode 100644 index 00000000..6d40aecf --- /dev/null +++ b/examples/mods/pulse_cannon/generate_audio.py @@ -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")) diff --git a/examples/mods/pulse_cannon/mod.toml b/examples/mods/pulse_cannon/mod.toml new file mode 100644 index 00000000..e7befa59 --- /dev/null +++ b/examples/mods/pulse_cannon/mod.toml @@ -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"] diff --git a/examples/mods/pulse_cannon/prepare_art.rs b/examples/mods/pulse_cannon/prepare_art.rs new file mode 100644 index 00000000..47b2060e --- /dev/null +++ b/examples/mods/pulse_cannon/prepare_art.rs @@ -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() + ); + } + } +} diff --git a/examples/mods/pulse_cannon/scripts/equipment.rhai b/examples/mods/pulse_cannon/scripts/equipment.rhai new file mode 100644 index 00000000..1125e095 --- /dev/null +++ b/examples/mods/pulse_cannon/scripts/equipment.rhai @@ -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 } } +} diff --git a/examples/mods/pulse_cannon/scripts/weapon.rhai b/examples/mods/pulse_cannon/scripts/weapon.rhai new file mode 100644 index 00000000..f6927794 --- /dev/null +++ b/examples/mods/pulse_cannon/scripts/weapon.rhai @@ -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 } } +} diff --git a/examples/mods/seeker_swarm/ART_PROVENANCE.md b/examples/mods/seeker_swarm/ART_PROVENANCE.md new file mode 100644 index 00000000..eeda7c87 --- /dev/null +++ b/examples/mods/seeker_swarm/ART_PROVENANCE.md @@ -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. diff --git a/examples/mods/seeker_swarm/README.md b/examples/mods/seeker_swarm/README.md new file mode 100644 index 00000000..0060c8bb --- /dev/null +++ b/examples/mods/seeker_swarm/README.md @@ -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. diff --git a/examples/mods/seeker_swarm/art-provenance.json b/examples/mods/seeker_swarm/art-provenance.json new file mode 100644 index 00000000..4c3a89f4 --- /dev/null +++ b/examples/mods/seeker_swarm/art-provenance.json @@ -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" + } + } +} diff --git a/examples/mods/seeker_swarm/art/equipment-generated-source.png b/examples/mods/seeker_swarm/art/equipment-generated-source.png new file mode 100644 index 00000000..6d31ee67 Binary files /dev/null and b/examples/mods/seeker_swarm/art/equipment-generated-source.png differ diff --git a/examples/mods/seeker_swarm/art/equipment-hd.png b/examples/mods/seeker_swarm/art/equipment-hd.png new file mode 100644 index 00000000..1210bec7 Binary files /dev/null and b/examples/mods/seeker_swarm/art/equipment-hd.png differ diff --git a/examples/mods/seeker_swarm/art/equipment-sd.png b/examples/mods/seeker_swarm/art/equipment-sd.png new file mode 100644 index 00000000..c0b75b9d Binary files /dev/null and b/examples/mods/seeker_swarm/art/equipment-sd.png differ diff --git a/examples/mods/seeker_swarm/art/projectile-generated-source.png b/examples/mods/seeker_swarm/art/projectile-generated-source.png new file mode 100644 index 00000000..43447230 Binary files /dev/null and b/examples/mods/seeker_swarm/art/projectile-generated-source.png differ diff --git a/examples/mods/seeker_swarm/art/projectile-hd.png b/examples/mods/seeker_swarm/art/projectile-hd.png new file mode 100644 index 00000000..28da9b2a Binary files /dev/null and b/examples/mods/seeker_swarm/art/projectile-hd.png differ diff --git a/examples/mods/seeker_swarm/art/projectile-sd.png b/examples/mods/seeker_swarm/art/projectile-sd.png new file mode 100644 index 00000000..10c11a10 Binary files /dev/null and b/examples/mods/seeker_swarm/art/projectile-sd.png differ diff --git a/examples/mods/seeker_swarm/audio/fire.wav b/examples/mods/seeker_swarm/audio/fire.wav new file mode 100644 index 00000000..c7a6e676 Binary files /dev/null and b/examples/mods/seeker_swarm/audio/fire.wav differ diff --git a/examples/mods/seeker_swarm/audio/impact.wav b/examples/mods/seeker_swarm/audio/impact.wav new file mode 100644 index 00000000..d75a20ac Binary files /dev/null and b/examples/mods/seeker_swarm/audio/impact.wav differ diff --git a/examples/mods/seeker_swarm/content.json b/examples/mods/seeker_swarm/content.json new file mode 100644 index 00000000..0f1fce81 --- /dev/null +++ b/examples/mods/seeker_swarm/content.json @@ -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 + } + ] +} diff --git a/examples/mods/seeker_swarm/generate_audio.py b/examples/mods/seeker_swarm/generate_audio.py new file mode 100644 index 00000000..7ab6dca7 --- /dev/null +++ b/examples/mods/seeker_swarm/generate_audio.py @@ -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")) diff --git a/examples/mods/seeker_swarm/mod.toml b/examples/mods/seeker_swarm/mod.toml new file mode 100644 index 00000000..8327cee5 --- /dev/null +++ b/examples/mods/seeker_swarm/mod.toml @@ -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"] diff --git a/examples/mods/seeker_swarm/prepare_art.rs b/examples/mods/seeker_swarm/prepare_art.rs new file mode 100644 index 00000000..26a0a1db --- /dev/null +++ b/examples/mods/seeker_swarm/prepare_art.rs @@ -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() + ); + } + } +} diff --git a/examples/mods/seeker_swarm/scripts/equipment.rhai b/examples/mods/seeker_swarm/scripts/equipment.rhai new file mode 100644 index 00000000..c157cfdb --- /dev/null +++ b/examples/mods/seeker_swarm/scripts/equipment.rhai @@ -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 } } +} diff --git a/examples/mods/seeker_swarm/scripts/weapon.rhai b/examples/mods/seeker_swarm/scripts/weapon.rhai new file mode 100644 index 00000000..781239a9 --- /dev/null +++ b/examples/mods/seeker_swarm/scripts/weapon.rhai @@ -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 } } +} diff --git a/tests/enemies_mods.rs b/tests/enemies_mods.rs new file mode 100644 index 00000000..03f913e2 --- /dev/null +++ b/tests/enemies_mods.rs @@ -0,0 +1,452 @@ +//! Enemy package tests use synthetic PNG/WAV fixtures, never original/generated art. +use raptor_replica::{ + content::{ContentId, ContentPack, Registry}, + modding::{self, ActiveMods, Command}, + persistence::Pilot, + simulation::{Input, Outcome, World}, +}; +use serde_json::json; +use std::{fs, path::Path}; + +const ENEMIES: [&str; 3] = ["iron_coast_scout", "iron_coast_turret", "iron_coast_boss"]; + +fn base() -> Registry { + let mut registry = Registry::default(); + let pack: ContentPack = serde_json::from_value(json!({ + "weapons": [{"id":"base:projectile_forward_guns", "name":"Forward Guns", + "damage":1,"speed":8,"cooldown":2,"behavior":"straight","target":"all", + "offsets":[[-8,0],[8,0]]}], + "items":[{"id":"base:forward_guns","name":"Forward Guns","price":0, + "weapon":"base:projectile_forward_guns"}, + {"id":"base:energy","name":"Shield","price":0,"max_count":100}], + "missions":[{"id":"base:sector_1_wave_1","name":"Synthetic Range","duration":5000}] + })) + .unwrap(); + registry.apply(pack, "base").unwrap(); + registry.validate().unwrap(); + registry +} + +fn wav() -> Vec { + let mut data = b"RIFF".to_vec(); + data.extend(40_u32.to_le_bytes()); + data.extend(b"WAVEfmt "); + data.extend(16_u32.to_le_bytes()); + data.extend(1_u16.to_le_bytes()); + data.extend(1_u16.to_le_bytes()); + data.extend(22050_u32.to_le_bytes()); + data.extend(44100_u32.to_le_bytes()); + data.extend(2_u16.to_le_bytes()); + data.extend(16_u16.to_le_bytes()); + data.extend(b"data"); + data.extend(4_u32.to_le_bytes()); + data.extend([0, 0, 0, 0]); + data +} + +fn install_fixture(root: &Path, id: &str) { + let source = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("examples/mods") + .join(id); + let output = root.join(id); + fs::create_dir_all(output.join("scripts")).unwrap(); + fs::create_dir_all(output.join("art")).unwrap(); + fs::create_dir_all(output.join("audio")).unwrap(); + for name in ["mod.toml", "content.json", "scripts/ai.rhai"] { + fs::copy(source.join(name), output.join(name)).unwrap(); + } + let content: ContentPack = + serde_json::from_slice(&fs::read(source.join("content.json")).unwrap()).unwrap(); + for art in content + .enemies + .iter() + .filter_map(|e| e.art.as_ref()) + .chain(content.weapons.iter().filter_map(|w| w.art.as_ref())) + { + for (path, scale) in [ + (art.png.as_ref().unwrap(), 1), + (art.hd_png.as_ref().unwrap(), 2), + ] { + let mut image = image::RgbaImage::new( + art.logical_width * art.frames * scale, + art.logical_height * scale, + ); + for frame in 0..art.frames { + for y in scale..(art.logical_height - 1) * scale { + for x in scale..(art.logical_width - 1) * scale { + image.put_pixel( + frame * art.logical_width * scale + x, + y, + image::Rgba([80, 90, 100, 255]), + ); + } + } + } + image.save(output.join(path)).unwrap(); + } + } + for sound in content.sounds { + fs::write(output.join(sound.file), wav()).unwrap(); + } +} + +fn active(ids: &[&str]) -> (tempfile::TempDir, ActiveMods) { + let temp = tempfile::tempdir().unwrap(); + for id in ids { + install_fixture(temp.path(), id); + } + let discovered = modding::discover(temp.path()); + assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); + let selected: Vec = ids.iter().map(|id| id.to_string()).collect(); + let active = ActiveMods::build(&base(), &discovered, &selected, None).unwrap(); + (temp, active) +} + +fn world(active: &ActiveMods) -> World { + World::new( + &ContentId::from("base:sector_1_wave_1"), + &Pilot::default(), + &active.registry, + ) + .unwrap() +} + +#[test] +fn all_enemy_packages_validate_independently_and_together() { + for id in ENEMIES { + let (_temp, a) = active(&[id]); + assert_eq!(a.pins.len(), 1); + assert_eq!(a.registry.enemies.len(), 1); + assert!( + a.registry + .weapons + .contains_key(&ContentId::new(format!("{id}:projectile")).unwrap()) + ); + assert_eq!(a.registry.sounds.len(), 2); + assert_eq!( + a.registry.enemies[&ContentId::new(format!("{id}:unit")).unwrap()] + .script + .as_deref(), + Some("scripts/ai.rhai") + ); + } + let (_temp, a) = active(&ENEMIES); + assert_eq!(a.pins.len(), 3); + assert_eq!(a.registry.enemies.len(), 3); + assert!(a.registry.overrides.is_empty()); +} + +#[test] +fn scout_weaves_in_both_directions_and_aims_hostile_fire() { + let (_temp, mut a) = active(&["iron_coast_scout"]); + let mut w = world(&a); + w.player.x = 0; + w.spawn( + &ContentId::from("iron_coast_scout:unit"), + 120, + 0, + &a.registry, + ) + .unwrap(); + let mut right = false; + let mut left = false; + let mut sounds = 0; + for _ in 0..95 { + w.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + let e = &w.enemies[0]; + right |= e.vx > 0; + left |= e.vx < 0; + assert!(e.x >= 24 && e.x <= 272); + assert_eq!(e.vy, 1); + sounds += w + .mod_sounds + .iter() + .filter(|id| id.0 == "iron_coast_scout:shot") + .count(); + } + assert!(right && left); + assert_eq!(w.enemies[0].y, 94); + assert!(sounds >= 2); + assert!( + w.projectiles + .iter() + .any(|p| p.hostile && p.weapon.0 == "iron_coast_scout:projectile" && p.vx < 0) + ); +} + +#[test] +fn turret_scrolls_with_terrain_and_aims_at_player() { + let (_temp, mut a) = active(&["iron_coast_turret"]); + let mut w = world(&a); + w.player.x = 0; + w.spawn( + &ContentId::from("iron_coast_turret:unit"), + 220, + 0, + &a.registry, + ) + .unwrap(); + let mut sounds = 0; + for _ in 0..80 { + w.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + sounds += w + .mod_sounds + .iter() + .filter(|id| id.0 == "iron_coast_turret:shot") + .count(); + } + assert_eq!((w.enemies[0].x, w.enemies[0].y), (220, 80)); + assert!(a.registry.enemies[&w.enemies[0].definition].ground); + assert_eq!(sounds, 2); + assert!( + w.projectiles + .iter() + .any(|p| p.hostile && p.vx < 0 && p.vy >= 0) + ); +} + +#[test] +fn boss_enters_patrols_fires_fans_and_remains_on_screen() { + let (_temp, mut a) = active(&["iron_coast_boss"]); + let mut w = world(&a); + w.player.x = 0; + w.spawn( + &ContentId::from("iron_coast_boss:unit"), + 112, + -64, + &a.registry, + ) + .unwrap(); + let mut left = false; + let mut right = false; + let mut fans = 0; + for tick in 0..1500 { + w.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + assert_eq!(w.outcome, Outcome::Playing); + assert_eq!(w.enemies.len(), 1, "boss retired at{tick}"); + let e = &w.enemies[0]; + assert_eq!((e.width, e.height), (96, 64)); + if tick > 46 { + assert_eq!(e.y, 24); + assert!((80..=144).contains(&e.x)); + left |= e.vx < 0; + right |= e.vx > 0; + } + if w.mod_sounds + .contains(&ContentId::from("iron_coast_boss:shot")) + { + let bullets: Vec<_> = w + .projectiles + .iter() + .filter(|p| p.age == 0 && p.hostile) + .collect(); + assert_eq!(bullets.len(), 3); + assert_eq!(bullets.iter().map(|p| p.vx).collect::>(), [-2, 0, 2]); + fans += 1; + } + } + assert!(left && right); + assert!(fans >= 25); +} + +#[test] +fn boss_can_be_defeated_with_starter_guns_without_healing() { + let (_temp, mut a) = active(&["iron_coast_boss"]); + let mut w = world(&a); + w.player.x = 128; + w.spawn( + &ContentId::from("iron_coast_boss:unit"), + 112, + -64, + &a.registry, + ) + .unwrap(); + let mut defeated_at = None; + for tick in 0..600 { + w.step( + Input { + fire: true, + ..Input::default() + }, + &a.registry, + &mut a.scripts, + ) + .unwrap(); + if w.objectives.get("iron_coast_boss:defeated") == Some(&1) { + defeated_at = Some(tick); + break; + } + assert_eq!(w.outcome, Outcome::Playing, "starter pilot died at{tick}"); + } + assert!( + defeated_at.is_some(), + "boss cannot be killed with starter guns" + ); + assert!(w.enemies.is_empty()); + assert!(w.player.shield > 0); + assert_eq!(w.player.credits, 1800); + assert!( + w.mod_sounds + .contains(&ContentId::from("iron_coast_boss:death")) + ); +} + +#[test] +fn death_rewards_objectives_and_sounds_are_emitted_exactly_once() { + let (_temp, mut a) = active(&ENEMIES); + let mut w = world(&a); + for (index, id) in ENEMIES.iter().enumerate() { + let unit = ContentId::new(format!("{id}:unit")).unwrap(); + let entity = w + .spawn(&unit, 30 + index as i32 * 100, 0, &a.registry) + .unwrap(); + w.apply_commands( + &[Command::Damage { + entity, + amount: 100000, + }], + &a.registry, + ) + .unwrap(); + } + w.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + assert!(w.enemies.is_empty()); + assert_eq!(w.player.credits, 75 + 120 + 1800); + for id in ENEMIES { + assert_eq!(w.objectives.get(&format!("{id}:defeated")), Some(&1)); + assert!( + w.mod_sounds + .contains(&ContentId::new(format!("{id}:death")).unwrap()) + ); + } + let previous = w.player.credits; + w.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + assert_eq!(w.player.credits, previous); + assert!(w.mod_sounds.is_empty()); +} + +#[test] +fn enemy_scripts_replay_identically_and_restore_serialized_state() { + let (_temp, mut first) = active(&ENEMIES); + let (_other, mut second) = active(&ENEMIES); + let mut a = world(&first); + for (index, id) in ENEMIES.iter().enumerate() { + a.spawn( + &ContentId::new(format!("{id}:unit")).unwrap(), + 60 + index as i32 * 30, + -24, + &first.registry, + ) + .unwrap(); + } + a.player.x = 0; + let mut b = a.clone(); + for _ in 0..75 { + a.step(Input::default(), &first.registry, &mut first.scripts) + .unwrap(); + b.step(Input::default(), &second.registry, &mut second.scripts) + .unwrap(); + assert_eq!(a.digest(), b.digest()); + assert_eq!(first.scripts.checkpoints(), second.scripts.checkpoints()); + } + let snapshot = serde_json::to_vec(&a).unwrap(); + b = serde_json::from_slice(&snapshot).unwrap(); + let checkpoint = first.scripts.checkpoints(); + second.scripts.restore(checkpoint); + for _ in 0..25 { + a.step(Input::default(), &first.registry, &mut first.scripts) + .unwrap(); + b.step(Input::default(), &second.registry, &mut second.scripts) + .unwrap(); + assert_eq!(a.digest(), b.digest()); + } +} + +#[test] +fn mission_defeat_counts_reset_while_persistent_script_statistics_continue() { + let (_temp, mut a) = active(&ENEMIES); + for id in ENEMIES { + let unit = ContentId::new(format!("{id}:unit")).unwrap(); + let mut first = world(&a); + for _ in 0..2 { + let entity = first.spawn(&unit, 20, 0, &a.registry).unwrap(); + first + .step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + first + .apply_commands( + &[Command::Damage { + entity, + amount: 100000, + }], + &a.registry, + ) + .unwrap(); + first + .step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + } + assert_eq!(first.objectives.get(&format!("{id}:defeated")), Some(&2)); + let mut next = world(&a); + let entity = next.spawn(&unit, 20, 0, &a.registry).unwrap(); + next.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + next.apply_commands( + &[Command::Damage { + entity, + amount: 100000, + }], + &a.registry, + ) + .unwrap(); + next.step(Input::default(), &a.registry, &mut a.scripts) + .unwrap(); + assert_eq!(next.objectives.get(&format!("{id}:defeated")), Some(&1)); + let state = a.scripts.checkpoints().0; + assert_eq!(state[&format!("{id}/scripts/ai.rhai")]["destroyed"], 3); + if id == "iron_coast_boss" { + assert!( + state[&format!("{id}/scripts/ai.rhai")]["directions"] + .as_object() + .unwrap() + .is_empty() + ); + } + } +} + +#[test] +fn shipped_enemy_assets_are_valid_and_aligned() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/mods"); + let selected: Vec<_> = ENEMIES.iter().map(|id| id.to_string()).collect(); + let discovery = modding::discover(&root); + ActiveMods::build(&base(), &discovery, &selected, None).unwrap(); + for id in ENEMIES { + let content: ContentPack = + serde_json::from_slice(&fs::read(root.join(id).join("content.json")).unwrap()).unwrap(); + let art = content.enemies[0].art.as_ref().unwrap(); + for path in [art.png.as_ref().unwrap(), art.hd_png.as_ref().unwrap()] { + let image = image::open(root.join(id).join(path)).unwrap().into_rgba8(); + assert_eq!( + image.width() / art.frames * art.logical_height, + image.height() * art.logical_width + ); + for frame in 0..art.frames { + let width = image.width() / art.frames; + for x in frame * width..(frame + 1) * width { + assert!(image.get_pixel(x, 0)[3] < 128); + assert!(image.get_pixel(x, image.height() - 1)[3] < 128); + } + for y in 0..image.height() { + assert!(image.get_pixel(frame * width, y)[3] < 128); + assert!(image.get_pixel((frame + 1) * width - 1, y)[3] < 128); + } + } + } + } +} diff --git a/tests/iron_coast.rs b/tests/iron_coast.rs new file mode 100644 index 00000000..252d9ec2 --- /dev/null +++ b/tests/iron_coast.rs @@ -0,0 +1,291 @@ +//! Campaign acceptance uses synthetic PNG/WAV assets; original data is unnecessary. +use raptor_replica::{ + content::{ContentPack, Registry}, + modding::{self, ActiveMods, Manifest}, + persistence::Pilot, + simulation::{Input, Outcome, World}, +}; +use serde_json::{Value, json}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +const PACKAGES: [&str; 7] = [ + "iron_coast_scout", + "iron_coast_turret", + "iron_coast_boss", + "pulse_cannon", + "seeker_swarm", + "ion_lance", + "iron_coast_mission_1", +]; +const MISSION: &str = "iron_coast_mission_1:mission_01"; + +fn base() -> Registry { + let mut registry = Registry::default(); + let pack:ContentPack=serde_json::from_value(json!({ + "weapons":[{"id":"base:projectile_forward_guns","name":"Forward Guns","damage":1,"speed":8,"cooldown":2,"behavior":"straight","target":"all","legacy":0,"art":{"asset":42,"logical_width":4,"logical_height":8,"frames":4}}], + "items":[{"id":"base:forward_guns","name":"Forward Guns","price":0,"max_count":1,"weapon":"base:projectile_forward_guns","legacy":0},{"id":"base:energy","name":"Shield","price":0,"max_count":100,"legacy":16}], + "missions":[{"id":"base:sector_1_wave_1","name":"Base fixture","duration":1000}] + })).unwrap(); + registry.apply(pack, "base").unwrap(); + registry.validate().unwrap(); + registry +} + +fn fixture_resources(value: &Value, root: &Path) { + match value { + Value::Object(object) => { + if let (Some(width), Some(height)) = ( + object.get("logical_width").and_then(Value::as_u64), + object.get("logical_height").and_then(Value::as_u64), + ) { + let frames = object.get("frames").and_then(Value::as_u64).unwrap_or(1); + for (key, scale) in [("png", 1), ("hd_png", 4)] { + if let Some(file) = object.get(key).and_then(Value::as_str) { + let path = root.join(file); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let image = image::RgbaImage::from_pixel( + (width * frames * scale) as u32, + (height * scale) as u32, + image::Rgba([80, 96, 104, 255]), + ); + image.save(path).unwrap(); + } + } + } + if let Some(file) = object.get("file").and_then(Value::as_str) { + let path = root.join(file); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + // Small valid mono PCM16 WAV; integration checks use the actual cues. + let mut wav = Vec::new(); + wav.extend_from_slice(b"RIFF"); + wav.extend_from_slice(&100_u32.to_le_bytes()); + wav.extend_from_slice(b"WAVEfmt "); + wav.extend_from_slice(&16_u32.to_le_bytes()); + wav.extend_from_slice(&1_u16.to_le_bytes()); + wav.extend_from_slice(&1_u16.to_le_bytes()); + wav.extend_from_slice(&22050_u32.to_le_bytes()); + wav.extend_from_slice(&44100_u32.to_le_bytes()); + wav.extend_from_slice(&2_u16.to_le_bytes()); + wav.extend_from_slice(&16_u16.to_le_bytes()); + wav.extend_from_slice(b"data"); + wav.extend_from_slice(&64_u32.to_le_bytes()); + wav.extend_from_slice(&[0; 64]); + fs::write(path, wav).unwrap(); + } + for child in object.values() { + fixture_resources(child, root); + } + } + Value::Array(values) => { + for child in values { + fixture_resources(child, root); + } + } + _ => {} + } +} + +fn active() -> (tempfile::TempDir, ActiveMods) { + let temporary = tempfile::tempdir().unwrap(); + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/mods"); + for id in PACKAGES { + let original = source.join(id); + let root = temporary.path().join(id); + fs::create_dir_all(&root).unwrap(); + let manifest_data = fs::read(original.join("mod.toml")) + .expect("all seven Iron Coast packages must be installed"); + let manifest: Manifest = + toml::from_str(std::str::from_utf8(&manifest_data).unwrap()).unwrap(); + fs::write(root.join("mod.toml"), manifest_data).unwrap(); + let content = fs::read(original.join(&manifest.content)).unwrap(); + let value: Value = serde_json::from_slice(&content).unwrap(); + fs::write(root.join(&manifest.content), content).unwrap(); + for script in &manifest.scripts { + let path = root.join(script); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::copy(original.join(script), path).unwrap(); + } + fixture_resources(&value, &root); + } + let discovery = modding::discover(temporary.path()); + assert!(discovery.errors.is_empty(), "{:?}", discovery.errors); + let enabled = PACKAGES.iter().map(|s| s.to_string()).collect::>(); + let active = ActiveMods::build(&base(), &discovery, &enabled, None).unwrap(); + (temporary, active) +} + +#[test] +fn campaign_dependencies_and_generated_footprints_validate_together() { + let (_temporary, active) = active(); + assert_eq!(active.pins.len(), 7); + let mission = &active.registry.missions[&MISSION.into()]; + assert!(!mission.auto_complete); + assert_eq!(mission.columns, 9); + assert_eq!(mission.layout.len(), 72 * 9); + assert!( + mission + .spawns + .iter() + .any(|s| s.enemy.0 == "iron_coast_boss:unit" && s.tick == 720) + ); + assert_eq!(mission.art.as_ref().unwrap().logical_width, 320); + assert!( + active.registry.campaigns[&"iron_coast_mission_1:campaign".into()] + .missions + .contains(&MISSION.into()) + ); +} + +#[test] +fn boss_alone_cannot_complete_and_reward_is_paid_once() { + let (_temporary, mut active) = active(); + let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap(); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + world + .objectives + .insert("iron_coast_boss:defeated".into(), 1); + world + .objectives + .insert("iron_coast_turret:defeated".into(), 1); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + assert_eq!(world.outcome, Outcome::Playing); + world + .objectives + .insert("iron_coast_turret:defeated".into(), 2); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + assert_eq!(world.outcome, Outcome::Complete); + assert_eq!(world.objectives["iron_coast_mission_1:complete"], 1); + assert_eq!(world.player.credits, 5000); + assert_eq!(world.objectives["iron_coast_mission_1:debrief"], 1); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + assert_eq!(world.player.credits, 5000); +} + +fn play(use_trial: bool) -> (World, Vec<(u64, String)>) { + let (_temporary, mut active) = active(); + let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap(); + let mut seen = Vec::new(); + for tick in 0..2000 { + let input = Input { + fire: true, + special: tick % 90 == 60, + cycle: tick == 1, + absolute: Some([144, 160]), + ..Input::default() + }; + world + .step(input, &active.registry, &mut active.scripts) + .unwrap(); + if tick == 0 && !use_trial { + world + .player + .inventory + .remove(&"pulse_cannon:equipment".into()); + } + for enemy in &world.enemies { + if !seen.iter().any(|(_, id)| id == &enemy.definition.0) { + seen.push((world.tick, enemy.definition.0.clone())); + } + } + if world.outcome != Outcome::Playing { + break; + } + } + if use_trial { + let weapon_state = &active.scripts.states["pulse_cannon/scripts/weapon.rhai"]; + assert!(weapon_state["bursts"].as_i64().unwrap() > 0); + assert!(weapon_state["hits"].as_i64().unwrap() > 0); + let serialized = serde_json::to_string(&active.scripts.states).unwrap(); + let recovered: serde_json::Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(recovered["pulse_cannon/scripts/weapon.rhai"], *weapon_state); + } + (world, seen) +} + +#[test] +fn restarted_mission_counts_new_turret_kills_independently_of_saved_statistics() { + let (_temporary, mut active) = active(); + for _ in 0..2 { + let mut world = World::new(&MISSION.into(), &Pilot::default(), &active.registry).unwrap(); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + let entity = world + .spawn(&"iron_coast_turret:unit".into(), 144, 0, &active.registry) + .unwrap(); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + world + .apply_commands( + &[modding::Command::Damage { + entity, + amount: 1000, + }], + &active.registry, + ) + .unwrap(); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + assert_eq!(world.objectives["iron_coast_turret:defeated"], 1); + assert_eq!(world.outcome, Outcome::Playing); + } + assert_eq!( + active.scripts.states["iron_coast_turret/scripts/ai.rhai"]["destroyed"], + 2 + ); +} + +#[test] +fn starter_guns_can_complete_real_waves_and_boss() { + let (world, seen) = play(false); + assert_eq!( + world.outcome, + Outcome::Complete, + "tick={} shield={} objectives={:?}", + world.tick, + world.player.shield, + world.objectives + ); + assert!( + seen.iter() + .any(|(tick, id)| *tick >= 720 && id == "iron_coast_boss:unit") + ); + assert!(world.objectives["iron_coast_turret:defeated"] >= 2); + assert!(world.objectives["iron_coast_boss:defeated"] >= 1); + assert_eq!(world.objectives["iron_coast_mission_1:phase"], 4); + assert!(world.player.shield > 0); + assert!(world.tick > 720 && world.tick < 2000); +} + +#[test] +fn trial_pulse_cannon_completes_and_preserves_scripted_weapon_state() { + let (world, _seen) = play(true); + assert_eq!( + world.outcome, + Outcome::Complete, + "tick={} shield={} objectives={:?}", + world.tick, + world.player.shield, + world.objectives + ); + assert!( + world + .player + .inventory + .contains_key(&"pulse_cannon:equipment".into()) + ); + assert!(world.player.credits >= 5000); +} diff --git a/tests/weapons_mods.rs b/tests/weapons_mods.rs new file mode 100644 index 00000000..8b5599cf --- /dev/null +++ b/tests/weapons_mods.rs @@ -0,0 +1,373 @@ +//! Validate the shipped weapon definitions/scripts using synthetic PNG and WAV inputs. +//! No original or locally generated game artwork is needed in CI. +use raptor_replica::{ + content::{ContentId, ContentPack, Registry}, + modding::{self, ActiveMods}, + persistence::Pilot, + simulation::{Input, World}, +}; +use serde_json::{Value, json}; +use std::{fs, path::Path}; + +const PACKAGES: [&str; 3] = ["pulse_cannon", "seeker_swarm", "ion_lance"]; +fn content(value: Value) -> ContentPack { + serde_json::from_value(value).unwrap() +} +fn base() -> Registry { + let mut registry = Registry::default(); + registry.apply(content(json!({ + "items": [{"id":"base:energy","name":"Shield","price":100,"max_count":100}], + "missions": [{"id":"base:practice","name":"Practice","duration":2000}], + "enemies": [ + {"id":"base:target","name":"Stationary target","health":100,"speed":0}, + {"id":"base:ground_target","name":"Ground target","health":100,"speed":0,"ground":true} + ] + })), "base").unwrap(); + registry +} +fn wav() -> Vec { + let mut bytes = b"RIFF".to_vec(); + bytes.extend(164u32.to_le_bytes()); + bytes.extend(b"WAVEfmt "); + bytes.extend(16u32.to_le_bytes()); + bytes.extend(1u16.to_le_bytes()); + bytes.extend(1u16.to_le_bytes()); + bytes.extend(11025u32.to_le_bytes()); + bytes.extend(22050u32.to_le_bytes()); + bytes.extend(2u16.to_le_bytes()); + bytes.extend(16u16.to_le_bytes()); + bytes.extend(b"data"); + bytes.extend(128u32.to_le_bytes()); + bytes.extend([0; 128]); + bytes +} +fn install_synthetic(root: &Path, id: &str) { + let source = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("examples/mods") + .join(id); + let target = root.join(id); + fs::create_dir_all(target.join("scripts")).unwrap(); + fs::create_dir_all(target.join("art")).unwrap(); + fs::create_dir_all(target.join("audio")).unwrap(); + for file in [ + "mod.toml", + "content.json", + "scripts/weapon.rhai", + "scripts/equipment.rhai", + ] { + fs::copy(source.join(file), target.join(file)).unwrap(); + } + let definitions: Value = + serde_json::from_slice(&fs::read(target.join("content.json")).unwrap()).unwrap(); + for entry in definitions["weapons"] + .as_array() + .unwrap() + .iter() + .chain(definitions["items"].as_array().unwrap()) + { + let art = &entry["art"]; + for (property, scale) in [("png", 1), ("hd_png", 4)] { + let image = image::RgbaImage::from_pixel( + art["logical_width"].as_u64().unwrap() as u32 * scale, + art["logical_height"].as_u64().unwrap() as u32 * scale, + image::Rgba([60, 90, 120, 255]), + ); + image + .save(target.join(art[property].as_str().unwrap())) + .unwrap(); + } + } + for sound in definitions["sounds"].as_array().unwrap() { + fs::write(target.join(sound["file"].as_str().unwrap()), wav()).unwrap(); + } +} +fn activate(ids: &[&str]) -> (tempfile::TempDir, ActiveMods) { + let root = tempfile::tempdir().unwrap(); + for id in ids { + install_synthetic(root.path(), id); + } + let discovery = modding::discover(root.path()); + assert!(discovery.errors.is_empty(), "{:?}", discovery.errors); + let active = ActiveMods::build( + &base(), + &discovery, + &ids.iter().map(|id| id.to_string()).collect::>(), + None, + ) + .unwrap(); + (root, active) +} +fn pilot(id: &str, registry: &Registry) -> Pilot { + let equipment = ContentId::new(format!("{id}:equipment")).unwrap(); + Pilot { + inventory: [(equipment.clone(), 1), ("base:energy".into(), 100)] + .into_iter() + .collect(), + selected: registry.items[&equipment].special.then_some(equipment), + mission: "base:practice".into(), + ..Pilot::default() + } +} +fn world(id: &str, registry: &Registry) -> World { + World::new(&"base:practice".into(), &pilot(id, registry), registry).unwrap() +} +fn state<'a>(active: &'a ActiveMods, id: &str) -> &'a Value { + &active.scripts.states[&format!("{id}/scripts/weapon.rhai")] +} + +#[test] +fn each_weapon_package_is_independent_and_the_three_can_coexist() { + for id in PACKAGES { + let (_root, active) = activate(&[id]); + assert_eq!(active.pins.len(), 1); + assert_eq!(active.pins[0].version, "1.0.0"); + assert_eq!(active.registry.weapons.len(), 1); + assert_eq!(active.registry.sounds.len(), 2); + assert!( + active + .registry + .items + .contains_key(&format!("{id}:equipment").as_str().into()) + ); + assert!( + active + .registry + .weapons + .contains_key(&format!("{id}:projectile").as_str().into()) + ); + } + let (_root, all) = activate(&PACKAGES); + assert_eq!(all.pins.len(), 3); + assert_eq!(all.registry.weapons.len(), 3); + assert_eq!(all.registry.sounds.len(), 6); + assert!(all.registry.overrides.is_empty()); +} + +#[test] +fn shop_charges_each_price_and_rejects_duplicate_equipment() { + let (_root, active) = activate(&PACKAGES); + for id in PACKAGES { + let item: ContentId = format!("{id}:equipment").as_str().into(); + let price = active.registry.items[&item].price; + let mut pilot = Pilot { + credits: price, + inventory: Default::default(), + ..Pilot::default() + }; + pilot.buy(&item, &active.registry).unwrap(); + assert_eq!(pilot.credits, 0); + assert_eq!(pilot.inventory[&item], 1); + assert!(pilot.buy(&item, &active.registry).is_err()); + pilot.sell(&item, &active.registry).unwrap(); + assert!(pilot.credits > 0); + assert_eq!(pilot.inventory.get(&item).copied().unwrap_or(0), 0); + } +} + +#[test] +fn pulse_cannon_emits_paired_rounds_and_every_third_burst_is_scripted() { + let (_root, mut active) = activate(&["pulse_cannon"]); + let mut world = world("pulse_cannon", &active.registry); + for _ in 0..19 { + world + .step( + Input { + fire: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + } + assert_eq!(state(&active, "pulse_cannon")["bursts"], 3); + assert_eq!(world.projectiles.len(), 8); + assert_eq!(world.projectiles.iter().filter(|p| p.vx != 0).count(), 2); + assert_eq!(world.mod_sounds, vec![ContentId::from("pulse_cannon:fire")]); + assert!(!world.effects.is_empty()); +} + +#[test] +fn pulse_hits_deal_damage_and_run_effect_audio_and_hit_state_callbacks() { + let (_root, mut active) = activate(&["pulse_cannon"]); + let mut world = world("pulse_cannon", &active.registry); + world + .spawn(&"base:target".into(), 140, 100, &active.registry) + .unwrap(); + world + .step( + Input { + fire: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + let mut hit_sound = false; + for _ in 0..8 { + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + hit_sound |= world.mod_sounds.contains(&"pulse_cannon:impact".into()); + } + assert_eq!(world.enemies[0].health, 92); + assert_eq!(state(&active, "pulse_cannon")["hits"], 2); + assert!(hit_sound); + assert!(!world.effects.is_empty()); +} + +#[test] +fn seeker_swarm_homes_and_fragmentation_damages_nearby_enemies() { + let (_root, mut active) = activate(&["seeker_swarm"]); + let mut world = world("seeker_swarm", &active.registry); + let primary = world + .spawn(&"base:target".into(), 180, 110, &active.registry) + .unwrap(); + let neighbor = world + .spawn(&"base:ground_target".into(), 180, 110, &active.registry) + .unwrap(); + world + .step( + Input { + fire: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + assert_eq!(world.projectiles.len(), 3); + assert!(world.projectiles.iter().all(|p| p.vx > 0)); + for _ in 0..12 { + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + } + let target = world.enemies.iter().find(|e| e.entity == primary).unwrap(); + let nearby = world.enemies.iter().find(|e| e.entity == neighbor).unwrap(); + assert_eq!(target.health, 82); + assert_eq!(nearby.health, 94); + assert_eq!(state(&active, "seeker_swarm")["hits"], 3); +} + +#[test] +fn ion_lance_pierces_air_and_ground_targets_in_its_column_only() { + let (_root, mut active) = activate(&["ion_lance"]); + let mut world = world("ion_lance", &active.registry); + world + .spawn(&"base:target".into(), 144, 100, &active.registry) + .unwrap(); + world + .spawn(&"base:ground_target".into(), 144, 60, &active.registry) + .unwrap(); + world + .spawn(&"base:target".into(), 220, 60, &active.registry) + .unwrap(); + world + .step( + Input { + fire: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + world + .step(Input::default(), &active.registry, &mut active.scripts) + .unwrap(); + assert_eq!( + world.enemies.iter().map(|e| e.health).collect::>(), + [84, 84, 100] + ); + assert_eq!(state(&active, "ion_lance")["hits"], 4); + assert!(world.mod_sounds.contains(&"ion_lance:impact".into())); + assert!(!world.effects.is_empty()); +} + +#[test] +fn equipped_specials_and_primary_upgrade_fire_without_consuming_equipment() { + let (_root, mut active) = activate(&PACKAGES); + let mut pilot = pilot("pulse_cannon", &active.registry); + pilot.inventory.insert("seeker_swarm:equipment".into(), 1); + pilot.inventory.insert("ion_lance:equipment".into(), 1); + pilot.selected = Some("ion_lance:equipment".into()); + let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap(); + world + .step( + Input { + fire: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + assert_eq!(state(&active, "pulse_cannon")["bursts"], 1); + assert_eq!(state(&active, "ion_lance")["bursts"], 1); + assert!( + !active + .scripts + .states + .contains_key("seeker_swarm/scripts/weapon.rhai") + ); + for id in PACKAGES { + assert_eq!( + world.player.inventory[&format!("{id}:equipment").as_str().into()], + 1 + ); + } + world.player.selected = Some("seeker_swarm:equipment".into()); + world + .step( + Input { + special: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + assert_eq!(state(&active, "seeker_swarm")["bursts"], 1); +} + +#[test] +fn scripted_bursts_and_state_resume_deterministically() { + let (_root, mut active) = activate(&["pulse_cannon"]); + let mut world = world("pulse_cannon", &active.registry); + for _ in 0..10 { + world + .step( + Input { + fire: true, + ..Input::default() + }, + &active.registry, + &mut active.scripts, + ) + .unwrap(); + } + let mut resumed = world.clone(); + let (_other_root, mut other) = activate(&["pulse_cannon"]); + other.scripts.states = active.scripts.states.clone(); + other.scripts.rng = active.scripts.rng.clone(); + for _ in 0..20 { + let input = Input { + fire: true, + ..Input::default() + }; + world + .step(input, &active.registry, &mut active.scripts) + .unwrap(); + resumed + .step(input, &other.registry, &mut other.scripts) + .unwrap(); + } + assert_eq!(world.digest(), resumed.digest()); + assert_eq!(active.scripts.states, other.scripts.states); + assert_eq!( + serde_json::to_value(&active.scripts.rng).unwrap(), + serde_json::to_value(&other.scripts.rng).unwrap() + ); +}