Checkpoint Rust gameplay, Iron Coast campaign and full HD pipeline
Refine the classic menus, shop, save compatibility prompts, original intro, indexed effects and audio. Restore forward-gun damage and rewards for JSON ground targets while preserving native replay behavior. Add six separate Iron Coast missions, distinct bosses and the authored FM soundtrack, with source art, SD/HD sprites, sounds, dependencies and validation in the mod packages. Validate the complete 19-package campaign route and save/load progression. Inventory every original picture and implement source-bound HD review, bounded lazy artwork caches, indexed terrain composition and palette-safe conversion. Track generation jobs and independent acceptance separately; the complete original-art remaster is still in progress. Validation: default and headless portable suites; final library 106 passed; original 1889-step reference replay; complete six-stage campaign and replay; 42 menu/intro captures; full registered HD pack at four presentation scales; terrain context/scrolling checks and fullscreen 1080p rendering.
No files matched your search
@@ -12,8 +12,8 @@ The registered game has three sectors with nine waves each. Archive slots are as
|
||||
- Rust MUS, GENMIDI FM synthesis and digital DSP mixing, with native PCM comparisons.
|
||||
- Menus, shop, equipment, mission selection, pilot slots, atomic saves, read-only legacy import and original AGX cinematics.
|
||||
- Local mods, dependencies/conflicts, explicit overrides/priority/hashes, bounded deterministic Rhai hooks and transactional commands.
|
||||
- In-game enable/disable and pending changes, save diagnostics, schemas, validation CLI and scripted Aurora example.
|
||||
- Optional source-hashed HD loader/setting; one accepted player-frame proof with original fallback.
|
||||
- In-game enable/disable and pending changes, save mismatch confirmation and backups, schemas, validation CLI and scripted Aurora example.
|
||||
- Optional source-hashed HD loader/setting; a complete reviewed light-player banking sequence and selected enemy/boss/equipment/projectile/effect artwork with original fallback.
|
||||
- C++/Rust replay recording and comparison tools, synthetic tests and local-original-data checks.
|
||||
|
||||
## Validation and remaining work
|
||||
@@ -27,4 +27,4 @@ See [validation instructions](docs/rust-rewrite.md) and [HD coverage](docs/hd-ar
|
||||
|
||||
## New content
|
||||
|
||||
Separate agents built Iron Coast mission 1, three individual weapon mods and three individual enemy mods using the public API. Each package ships local SD/HD artwork and procedurally generated sound effects, with definitions, scripts, images and sounds tracked together in Git. The mission dependencies and two controlled victories are documented in [Iron Coast](docs/iron-coast.md).
|
||||
Iron Coast has six individual mission mods, a campaign assembly, three individual weapon mods, three shared enemy mods, five distinct stage-boss mods and an original FM soundtrack package. New stages mix authored units with original enemies and their native movement paths. Each authored unit and mission supplies SD/HD artwork and synthesized sound effects; definitions, scripts, images, sounds and music are tracked together in Git. Dependency behavior, objectives and controlled playthroughs are documented in [Iron Coast](docs/iron-coast.md).
|
||||
@@ -22,8 +22,10 @@ select another catalog with `--data DIR`. No C++ checkout or build is needed to
|
||||
build or play the Rust game. Some optional comparison tests use retained source.
|
||||
|
||||
The **Mods** screen discovers the bundled `mods/` packages when no user mods
|
||||
folder exists. Enable the Iron Coast mission package to select its six
|
||||
dependencies automatically, then apply at the main menu. The three weapons can also be enabled individually.
|
||||
folder exists. Enable **Iron Coast Campaign** to select all six mission mods,
|
||||
their enemies, distinct bosses, weapons and original soundtrack automatically,
|
||||
then apply at the main menu. Missions, bosses and the three weapons can also
|
||||
be enabled individually; each mission selects its own dependencies.
|
||||
For a user mods folder, pass `--mods DIR` or create
|
||||
`$XDG_DATA_HOME/RaptorReplica/mods/`. Saves and configuration use separate XDG
|
||||
`RaptorReplica` directories, with read-only legacy import.
|
||||
@@ -32,6 +34,10 @@ Enter the **Hangar** to fly, choose missions, visit the supply room, manage
|
||||
equipment or **Save pilot**. New pilots have editable names, callsigns and ID
|
||||
pictures and difficulty; existing pilots can edit their ID from the hangar.
|
||||
Registration starts with 10,000 credits and 75 energy, as in the original.
|
||||
The original Apogee/Cygnus logos and intro play on normal startup and through
|
||||
**Watch intro**. Saves with changed or missing mods show the differences and
|
||||
offer **Load anyway**, retaining unavailable equipment and progression. The
|
||||
first subsequent save keeps a backup of the previous file.
|
||||
The supply room
|
||||
shows an equipment image, description, price, resale value, quantity and
|
||||
whether it fires automatically, is selectable, is consumed or works passively.
|
||||
@@ -44,7 +50,7 @@ Existing untouched Rust default bindings migrate to these controls.
|
||||
| Folder | Contents |
|
||||
| --- | --- |
|
||||
| `src/` | Engine, SDL host and validation CLI |
|
||||
| `mods/` | Aurora example, Iron Coast mission, three weapons and three enemies |
|
||||
| `mods/` | Aurora example; six Iron Coast missions, campaign assembly, music, three weapons, three shared enemies and five stage bosses |
|
||||
| `schemas/` | Version 1 mod API schemas |
|
||||
| `tests/` | Portable fixtures and opt-in original-data checks |
|
||||
| `tools/` | Replay comparator, content-table generator and asset reference tools |
|
||||
@@ -54,8 +60,10 @@ Existing untouched Rust default bindings migrate to these controls.
|
||||
All bundled mod definitions, scripts, SD/HD PNGs, WAVs, generation prompts and
|
||||
recipes are tracked in Git. Original data and local reference pixels are excluded.
|
||||
The engine supports original and 2×/3×/4× crisp scaling, plus an optional HD
|
||||
artwork setting. Bundled mods have SD/HD artwork; the base-game HD proof covers
|
||||
one player frame, with crisp originals supplying the other frames.
|
||||
artwork setting. Bundled mods have SD/HD artwork; the reviewed base-game pack
|
||||
covers a complete light-player banking sequence and selected enemies, boss,
|
||||
equipment, projectiles and effects. [The HD backlog](docs/hd-backlog.md) records
|
||||
the remaining artwork and drafts awaiting review.
|
||||
|
||||
First-mission wins at difficulties 1 and 2, a complete 1,889-step native demo
|
||||
comparison, assisted coverage of all 27 missions at four difficulties, and
|
||||
@@ -68,6 +76,9 @@ cases; wider playtesting and complete base-game HD coverage remain work in progr
|
||||
- [Weapon prices and balance](docs/weapon-balance.md)
|
||||
- [Sprite and shadow validation](docs/visual-validation.md)
|
||||
- [Sound playback validation](docs/sound-validation.md)
|
||||
- [Classic intro validation](docs/intro-validation.md)
|
||||
- [Menu screenshot review](docs/menu-validation.md)
|
||||
- [Campaign music](docs/music-validation.md)
|
||||
- [Display measurements](docs/performance.md)
|
||||
- [First mission](docs/first-mission-validation.md)
|
||||
- [Campaign coverage](docs/campaign-validation.md)
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,7 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: background artwork for the RustRaptor game main menu, no text or UI baked into the image.
|
||||
Primary request: modern HD reinterpretation of a classic 1990s top-down military sci-fi shoot-em-up menu atmosphere, cohesive with silver gunmetal spacecraft and dark red accents.
|
||||
Scene/backdrop: a quiet futuristic coastal aircraft hangar at dusk, looking toward a blue-grey ocean and distant storm-lit industrial shoreline. One compact silver gunmetal fighter with dark red trim is parked near the lower left foreground, viewed from above and slightly behind. Restrained amber hangar lights, subtle steel paneling, atmospheric depth.
|
||||
Composition/framing: wide landscape 16:10 composition. Keep the upper quarter and the entire right half low contrast, dark navy and visually quiet for overlaid menu controls. The spacecraft and most scenery detail occupy lower left and edges. Give the art a cinematic hand-painted game illustration finish with crisp mechanical detailing, not photographic clutter.
|
||||
Color palette: deep navy, cool slate blue, silver gunmetal, restrained dark red and warm amber accents, readable dark negative space.
|
||||
Constraints: no people, no lettering, no logos, no buttons, no watermarks, no interface panels, no combat explosions. This is a game background asset; the actual menu text is rendered separately. High resolution.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"assets": [{
|
||||
"file": "menu-background-v1.png",
|
||||
"purpose": "Full-color main menu and hangar background",
|
||||
"tool": "built-in image_gen",
|
||||
"prompt_file": "menu-background-v1.prompt.txt",
|
||||
"source": "newly authored illustration; no original game pixels supplied",
|
||||
"review": "Inspected generated output; dark negative space supports menu overlays. Actual composed menu reviewed in screenshot audit."
|
||||
}]
|
||||
}
|
||||
@@ -2,20 +2,56 @@
|
||||
|
||||
The Rust Options screen includes **HD art**. It starts disabled. Enable it to use reviewed artwork from `../data/hd/hd.json`; each missing or rejected frame keeps the selected original / 2x / 3x / 4x crisp treatment. The scale setting still determines the indexed rendering resolution, and the HD toggle can be changed while paused. HD art is presentation only: simulation, collision sizes, banking positions and save content IDs stay unchanged.
|
||||
|
||||
The current local base-game proof covers **the light player ship's straight banking frame only** (`0x1011d`, frame 3 of 7). The other six frames, the dark-sector ship, enemies, projectiles and terrain retain original art. Switching into a bank therefore changes visual style. This proof is suitable for evaluating the look; it is not a finished remaster. Original pixels and this base-game proof remain outside Git. The separate authored mod packages include their generated SD/HD PNGs, sounds and provenance in Git.
|
||||
The local base-game pack currently supplies **63 reviewed replacement frames**. Complete additions cover all seven light and dark player banking poses, two first-mission fighters, its industrial boss, three ground-defense targets and a five-frame opening bunker, five equipment/pickup definitions, the two-frame Plasma projectile, the two-frame enemy cannon pulse, the twelve-frame energy ring and sixteen canonical terrain tiles. The firing-flash poses still use original artwork. The complete catalog backlog and generation status are listed in [hd-backlog.md](hd-backlog.md).
|
||||
|
||||
The source was decoded with `tools/hd_art_source.py`, inspected, and passed to the **built-in image_gen tool** using the imagegen skill. The optional proof and its exact generation prompt are saved locally at:
|
||||
| Reviewed artwork | Original asset IDs | Supplied / complete frames |
|
||||
| --- | --- | ---: |
|
||||
| Light player banking | `0x1011a`–`0x10120` | 7 / 7 |
|
||||
| Dark player banking | `0x20045`–`0x2004b` | 7 / 7 |
|
||||
| First-mission fighter `SHIP01G1_PIC` | `0x102f8` | 1 / 1 |
|
||||
| First-mission fighter `SHIP27G1_PIC` | `0x10330` | 1 / 1 |
|
||||
| First-mission industrial boss `SHIP10G1_PIC` | `0x10306` | 1 / 1 |
|
||||
| Ground-defense target `TARGT1G1_PIC` | `0x10138` | 1 / 1 |
|
||||
| Ground-defense targets `TARGT2G1_PIC`, `TARGT3G1_PIC` | `0x10139`, `0x1013a` | 2 / 2 |
|
||||
| Opening ground-defense bunker `TARGT6G1_PIC` | `0x1013d`–`0x10141` | 5 / 5 |
|
||||
| Forward Guns equipment | `0x102b0` | 1 / 1 |
|
||||
| Plasma Guns equipment | `0x102b1`–`0x102b2` | 2 / 2 |
|
||||
| Dumb Missile equipment | `0x102b5` | 1 / 1 |
|
||||
| Air Missile equipment | `0x102bf` | 1 / 1 |
|
||||
| Super Shield pickup | `0x102d0` | 1 / 1 |
|
||||
| Plasma projectile | `0x101cf`–`0x101d0` | 2 / 2 |
|
||||
| Enemy cannon pulse | `0x101d6`–`0x101d7` | 2 / 2 |
|
||||
| Energy ring | `0x101ae`–`0x101b9` | 12 / 12 |
|
||||
| Canonical terrain set 1, first shard | `0x10360`–`0x1036f` | 16 / 16 |
|
||||
|
||||
Every registered frame passed the exact original source hash, logical canvas, aspect, visible alpha and path checks. Complete contact sheets were inspected at source size and the 4× presentation size. Fighter/boss silhouettes have approximately 84–93% overlap with their source masks at logical resolution; their logical collision sizes are unchanged. Small contours and interpreted metal detail differ. Missing artwork keeps the crisp original fallback. HD remains an opt-in preview while the dark and firing player families and game screenshots are reviewed.
|
||||
|
||||
An actual Rust `HdAssets::sprite` pass also checked all registered frames at scales 1–4 with the game palette: every output has the expected dimensions and visible pixels, and the pack reports no diagnostics. Complete 4× source/HD sheets from that runtime path were reviewed for readable projectiles, equipment alignment, banking progression and energy-ring timing stages. These sprite checks complement live gameplay captures and do not establish full-game visual completion.
|
||||
|
||||
The complete light banking sequence uses matching silver/gunmetal materials, dark red accents and the seven distinct original roll poses. Its safe square crop rectangles remove transparent canvas margin without excluding any pixel with alpha at least 32. Independent Rust `image::Lanczos3` checks use the runtime's two resampling stages, first to the resident 128×128 canvas and then to the original 32×32 logical size. All seven frames have source silhouette overlap of 87.7–92.3%, bounds within one logical pixel and centroid displacement below 0.74 logical pixels. Full-sequence visual review also checked the nose, canopies, wing ends and engine exits; numeric overlap alone does not approve a frame.
|
||||
|
||||
Equipment visible material masks overlap their originals by approximately 93–100%, and both Plasma icon frames share the same aligned alpha footprint. Full alpha-mask overlap is lower, approximately 54–78%, because nearly black original opaque outlines become transparent around the detailed replacement. This difference is recorded separately from the visible metal silhouette. The ground-defense original and its replacement are both fully opaque, which the source-aware loader now accepts.
|
||||
|
||||
Small-scale differences remain visible. At 1×, each Plasma projectile loses one tail row: the original 8/6-pixel heights become 7/5 pixels, while 4× retains the original logical extents. The first energy effect's fine cardinal rays also contract under binary alpha at small scales; its 1× bounds become `(7,5)..(34,34)` against original `(3,2)..(38,37)`. At 4× the rays remain visible but end approximately 1–2 logical pixels inside the source extremes. Later rings use smoother, thinner contours. These differences affect presentation and are follow-up candidates for per-asset fallback; source collision dimensions remain unchanged.
|
||||
|
||||
Generated drafts are not active coverage. Earlier player atlas attempts and shortened extreme-bank variants were rejected. The complete light family replaces the initial straight-frame proof; the dark family uses coherent lighting edits of those reviewed poses and the original dark palette. Its exact runtime logical-mask overlap is 87.3–91.4%, with centroid displacement below 0.68 pixels, bounds within one pixel and no clipped meaningful alpha. Other equipment families remain uncovered. A 3×3 shoreline prototype preserves the broad coastline and demonstrates the terrain renderer, but remains unregistered: repeated canonical tiles, neighbors and destroyed states need individual artwork and seam review. Original AGX cinematics retain their original artwork.
|
||||
|
||||
The first sixteen canonical terrain replacements passed independent review of every source/logical/4× frame, all 48 supplied mission contexts and all 755 declared cardinal neighbor pairs: 170 HD/HD and 585 HD/original. Actual native scene checks exercised all 32 cached scroll phases and content-hash/load-order invalidation. None of these sixteen IDs has a declared destroyed partner; the separate native destruction-cache proof therefore adds no HD debris coverage. Mixed detail contrast remains until neighboring tiles are remastered. The next terrain family remains held because tile `0x20111` invented a canopy on an originally treeless grass/dirt tile.
|
||||
|
||||
Original pixels and original-derived base-game artwork remain outside Git under `../data/hd/`. The separate authored mod packages include their generated SD/HD PNGs, sounds and provenance in Git. The new authored menu backdrop has its own packaged rendering path and is not counted as an original catalog sprite replacement.
|
||||
|
||||
The sources were decoded with `tools/hd_art_source.py`, inspected, and passed to the **built-in image_gen tool** using the imagegen skill. Sources, raw generations, prompts, contact sheets, frame metadata and review notes remain in local family folders (`enemies/`, `bosses/`, `effects/`, `equipment/`, `player-light/`, `player-dark/`, and `terrain/`). The superseded initial straight-player proof and its exact generation prompt remain saved locally at:
|
||||
|
||||
- `../data/hd/light-player-straight-hd-v1.png`
|
||||
- `../data/hd/prompts/light-player-straight-v1.txt`
|
||||
- `../data/hd/provenance.json`
|
||||
- `../data/hd/source/light-player-straight.png`
|
||||
|
||||
The PNG has real alpha transparency, is 1254x1254, and was copied unchanged from the tool's output. At the original 32x32 logical resolution, its visible alpha footprint overlaps the source by 92.1% intersection-over-union; its bounds are `(3,0)..(29,31)` against `(2,0)..(29,31)` for the original. The nose, wing layout, silver/gunmetal body and dark red accents remain recognizable, but panel detail and some contours differ. Further artistic review and a complete, consistent banking sequence are required before enabling HD art by default. An additional extreme-bank attempt is saved as `../data/hd/light-player-bank-0-hd-v1.png` with `prompts/light-player-bank-0-v1.txt`, but is excluded from the active pack because its footprint overlap is only 62.2% and its engine ends are shortened. The original extreme-bank sprite continues to render.
|
||||
That proof PNG has real alpha transparency, is 1254×1254, and was copied unchanged from the tool's output. Its logical silhouette overlap was 92.1%, but it is no longer mapped into the active pack because the complete family now has consistent materials across all seven poses. An earlier extreme-bank attempt remains at `../data/hd/light-player-bank-0-hd-v1.png` with `prompts/light-player-bank-0-v1.txt`; it is rejected because its footprint overlap is only 62.2% and its engine ends are shortened. Reviewed production frames and their crop metadata are in `../data/hd/player-light/`. The generated PNG bytes remain unchanged.
|
||||
|
||||
## Pack format
|
||||
|
||||
The following is metadata only; artwork belongs in a separately distributed local pack. Decimal IDs are shown alongside their hexadecimal meaning.
|
||||
The following is metadata only; artwork belongs in a separately distributed local pack. This example shows one entry from the complete seven-frame light family. Decimal IDs are shown alongside their hexadecimal meaning.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -29,12 +65,13 @@ The following is metadata only; artwork belongs in a separately distributed loca
|
||||
"frame_count": 7,
|
||||
"frames": [
|
||||
{
|
||||
"frame": 3,
|
||||
"file": "light-player-straight-hd-v1.png",
|
||||
"frame": 0,
|
||||
"file": "player-light/frame-0-hd-v1.png",
|
||||
"logical_width": 32,
|
||||
"logical_height": 32,
|
||||
"origin": [0, 0],
|
||||
"source_sha256": "SHA-256 of the exact original 0x1011d catalog payload"
|
||||
"crop": [30, 10, 1220, 1220],
|
||||
"source_sha256": "SHA-256 of the exact original 0x1011a catalog payload"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -42,11 +79,23 @@ The following is metadata only; artwork belongs in a separately distributed loca
|
||||
}
|
||||
```
|
||||
|
||||
`base_asset` is `0x1011a`; the supplied frame targets `base_asset + frame`, so frames never depend on filenames or glob ordering. `frame_count` declares the whole original animation sequence and `frames` explicitly declares replacement coverage. An optional `crop: [x,y,width,height]` selects a rectangle from a PNG atlas.
|
||||
`base_asset` is `0x1011a`; the supplied frame targets `base_asset + frame`, so frames never depend on filenames or glob ordering. `frame_count` declares the whole original animation sequence and `frames` explicitly declares replacement coverage. An optional `crop: [x,y,width,height]` selects a rectangle from a PNG atlas or aligns a generated canvas by removing only transparent margin. The current pack supplies all seven light-player entries.
|
||||
|
||||
Version 1 requires exact original logical canvas dimensions and the original top-left anchor `[0,0]`. Canvas alignment is retained even when transparent margins surround the visible image. The PNG or crop must have the same aspect ratio as the original canvas and at least twice its logical resolution. The source hash prevents artwork silently matching a changed original asset. Duplicate IDs, invalid sequence metadata, bad paths, crop bounds, missing transparency and unreasonable image allocations are rejected. Paths must remain inside the pack directory, including after symlink resolution. Open **D: details** from the main menu to inspect rejected frames and coverage.
|
||||
Version 1 requires exact original logical canvas dimensions and the original top-left anchor `[0,0]`. Canvas alignment is retained even when transparent margins surround the visible image. The PNG or crop must have the same aspect ratio as the original canvas and at least twice its logical resolution. The source hash prevents artwork silently matching a changed original asset. Original sprites with transparent negative space require transparent HD artwork; fully opaque originals may retain fully opaque replacements. Duplicate IDs, invalid sequence metadata, bad paths, crop bounds and unreasonable image allocations are rejected. Paths must remain inside the pack directory, including after symlink resolution. Open **D: details** from the main menu to inspect rejected frames and coverage.
|
||||
|
||||
HD frames are resampled directly to the selected presentation canvas and quantized to the game palette. They bypass ScaleNx filtering, preserve transparent pixels and use the normal layer order. Indexed output allows palette effects to apply consistently; subtle RGB shades and alpha edges are reduced by the original palette and binary sprite transparency. Terrain is currently composed at original resolution before its joined ScaleNx pass; use crisp terrain until a dedicated HD terrain composition path is validated.
|
||||
The original picture header's two fields before width/height are `opt1` and `opt2`; they are not drawing origins. The audit records these as `header_options`. Replacement `origin: [0,0]` refers to the original canvas anchor and does not reinterpret those header options as sprite offsets.
|
||||
|
||||
The streaming runtime source-checks the complete manifest and validates each original generator-sized PNG/crop when used. It stores no more than the original logical canvas at 4× resolution, the highest supported presentation scale. Raw generated PNGs and provenance remain intact on disk. Three 32 MiB LRU caches hold decoded atlases, derived RGBA frames and palette-bound indexed sprites; eviction never removes a registered frame. A separate derived disk cache is capped at 512 MiB. The full catalog is no longer subject to a 128 MiB artwork admission limit. See [hd-streaming.md](hd-streaming.md) for validation ceilings, cache identities and corruption handling.
|
||||
|
||||
Original source locations also constrain palette classes: ordinary art uses indices 0–239, water stays in 240–244, fire in 245–249, and original blink/flash labels 250–255 remain exact. New HD coverage over an originally transparent location stays ordinary. Compact source-label metadata has a separate 32 MiB ceiling and is omitted for wholly ordinary sources. This prevents static metal or equipment highlights from acquiring fire cycles or blinking merely because a generated RGB value matches an animated palette entry. An actual all-scale recheck of the 63-frame pack confirmed every wholly ordinary source and output remains outside indices 240–255.
|
||||
|
||||
HD frames are resampled directly to the selected presentation canvas and quantized to the game palette. They bypass ScaleNx filtering, preserve transparent pixels and use the normal layer order. Indexed output allows palette effects to apply consistently; subtle RGB shades and alpha edges are reduced by the original palette and binary sprite transparency. The validated [terrain renderer](hd-terrain.md) overlays canonical, full-opaque HD tiles on the joined original layer. It shares HD edges and corners where source material classes agree, transitions HD/original boundaries over two logical pixels, and preserves water/fire/flash palette classes. Intact and destroyed IDs are independent targets. The renderer adds no remaster coverage by itself; each terrain family needs the same contextual review before registration.
|
||||
|
||||
The metadata-only [completion ledger](hd-completion.json) tracks every one of the 5,753 original picture IDs, including unused enemies, damaged variants, HUD graphics and unnamed terrain. It separates missing, in-progress, candidate, reviewed, registered and render-reachable stages. `tools/hd_job_queue.py` creates 674 disjoint picture jobs, plus 13 classic cinematic and four bitmap-font verification jobs. Original AGX artwork and sharp native glyphs have explicit preservation policies; they are never reported as AI remasters. The ignored `data/hd/jobs/queues/` files assign continuous enemy, boss, equipment/UI, effect/projectile and terrain work with exact source hashes, complete animation families and neighbor/state references. Exact-payload aliases may share reviewed artwork while retaining each ID's source binding.
|
||||
|
||||
Workers can validate an isolated proposal with `python3 tools/hd_candidate_review.py --candidate ../data/hd/jobs/JOB_ID/candidate.json`. The helper hard-links raw artwork into a temporary pack, links against the newest built Rust game library, and exports all scales 1–4 plus source/logical/4× contact sheets. It records raw artwork hashes and the library identity, warns when asset-loader source is newer than the build, and pages large reviews in groups of sixteen frames to avoid an unbounded giant image. Every page listed in `runtime-contact-sheets.json` requires inspection. It never edits the active pack or generated pixels. A successful gallery still needs coordinator visual approval and separate scene/context evidence.
|
||||
|
||||
Generation workers share a four-submission gate through `tools/hd_generation_gate.py`. They acquire before a new built-in request, report its actual outcome afterward, and prepare or review other jobs while all slots are reserved. Thirty-minute leases can be renewed; stale reservations expire after a two-minute grace period without interrupting a running tool. Connection failures retain their exact sanitized text and UTC timestamp, trigger a 60–600-second exponential cooldown, and permit at most three connection failures per job in a thirty-minute retry batch. Existing in-flight calls are allowed to finish. Logs and gate state stay under ignored `data/hd/jobs/.imagegen-gate/`.
|
||||
|
||||
## Source and sequence review
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Original artwork HD backlog
|
||||
|
||||
The checked catalog contains **5,753 standalone picture frames**. The active local HD pack supplies **63**; **5,690** have no registered HD replacement. These counts include unnamed graphics outside `sprites/`. Scale2x/3x/4x filtering is crisp scaling, not a newly authored HD replacement. Authored mod SD/HD art is separate from this original-catalog audit.
|
||||
|
||||
| Category | Picture frames | Registered HD | Missing HD | Declared sequences / terrain sets |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Player banking | 21 | 14 | 7 | 3 |
|
||||
| Bosses | 249 | 1 | 248 | 79 |
|
||||
| Enemies and ground objects | 419 | 10 | 409 | 183 |
|
||||
| Player and enemy projectiles | 51 | 4 | 47 | 18 |
|
||||
| Explosions and effects | 281 | 12 | 269 | 25 |
|
||||
| Equipment and pickups | 47 | 6 | 41 | 23 |
|
||||
| Terrain | 4,535 | 16 | 4,519 | 4 |
|
||||
| Menus, portraits, HUD and other pictures | 150 | 0 | 150 | mixed static / animated |
|
||||
|
||||
## Registered pack and generation jobs
|
||||
|
||||
Only registered frames passing canvas, source-hash, PNG and alpha checks count as registered HD. This is loader coverage, not proof that every original animation event is reachable in the current renderer. A generated image or successful still-image review does not complete an animated family. The optional pack remains disabled by default.
|
||||
|
||||
| Registered sequence | Asset base | Valid / complete frames |
|
||||
| --- | --- | ---: |
|
||||
| Light player banking | `0x1011a` | 7 / 7 |
|
||||
| First mission industrial boss | `0x10306` | 1 / 1 |
|
||||
| Plasma Guns projectile HD | `0x101cf` | 2 / 2 |
|
||||
| Enemy cannon projectile HD | `0x101d6` | 2 / 2 |
|
||||
| SHIP01G1 faithful HD remaster | `0x102f8` | 1 / 1 |
|
||||
| Energy effect HD | `0x101ae` | 12 / 12 |
|
||||
| SHIP27G1 faithful HD remaster | `0x10330` | 1 / 1 |
|
||||
| Forward Guns equipment | `0x102b0` | 1 / 1 |
|
||||
| Plasma Guns equipment | `0x102b1` | 2 / 2 |
|
||||
| Dumb Missile equipment | `0x102b5` | 1 / 1 |
|
||||
| Air Missile equipment | `0x102bf` | 1 / 1 |
|
||||
| TARGT1G1 faithful HD remaster | `0x10138` | 1 / 1 |
|
||||
| TARGT6G1_PIC | `0x1013d` | 5 / 5 |
|
||||
| Dark player banking | `0x20045` | 7 / 7 |
|
||||
| TARGT2G1_PIC | `0x10139` | 1 / 1 |
|
||||
| TARGT3G1_PIC | `0x1013a` | 1 / 1 |
|
||||
| Terrain 10360 | `0x10360` | 1 / 1 |
|
||||
| Terrain 10361 | `0x10361` | 1 / 1 |
|
||||
| Terrain 10362 | `0x10362` | 1 / 1 |
|
||||
| Terrain 10363 | `0x10363` | 1 / 1 |
|
||||
| Terrain 10364 | `0x10364` | 1 / 1 |
|
||||
| Terrain 10365 | `0x10365` | 1 / 1 |
|
||||
| Terrain 10366 | `0x10366` | 1 / 1 |
|
||||
| Terrain 10367 | `0x10367` | 1 / 1 |
|
||||
| Terrain 10368 | `0x10368` | 1 / 1 |
|
||||
| Terrain 10369 | `0x10369` | 1 / 1 |
|
||||
| Terrain 1036a | `0x1036a` | 1 / 1 |
|
||||
| Terrain 1036b | `0x1036b` | 1 / 1 |
|
||||
| Terrain 1036c | `0x1036c` | 1 / 1 |
|
||||
| Terrain 1036d | `0x1036d` | 1 / 1 |
|
||||
| Terrain 1036e | `0x1036e` | 1 / 1 |
|
||||
| Terrain 1036f | `0x1036f` | 1 / 1 |
|
||||
| Super Shield pickup | `0x102d0` | 1 / 1 |
|
||||
|
||||
| Generation job | Status |
|
||||
| --- | --- |
|
||||
| Light player banking, 7 poses | Complete 7-pose family registered after source and full-sequence review and exact Rust resident 4× → logical QA; earlier failed drafts stay unregistered. |
|
||||
| Dark player banking, 7 poses | Complete coherent seven-frame family registered after actual Rust source/logical/indexed4× review. Earlier independent drafts remain rejected. |
|
||||
| Firing-flash player, 7 poses | Awaiting a complete coherent family; the straight firing-flash pose has a distinct source alpha mask and needs separate review. |
|
||||
| First mission enemies and boss | Boss, two common fighters and opaque ground-defense target complete and registered. |
|
||||
| Projectiles and effects | Plasma 2, enemy cannon 2 and energy ring 12 frames complete and registered; other originals retain crisp fallback. |
|
||||
| Equipment and pickups | Forward Guns 1, Plasma 2, Dumb Missile 1 and Air Missile 1 frames complete and registered; other originals retain crisp fallback. |
|
||||
| Terrain shoreline | Contextual 3×3 prototype remains unregistered. Canonical terrain renderer and ten portable checks pass; all reusable tile IDs, neighbors and intact/destroyed states still require generated artwork review. |
|
||||
| Menu background | Separate authored background job and menu renderer integration; not part of original sprite HD manifest. |
|
||||
| Original cinematic artwork | Original AGX sequences retained; no HD cinematic replacement registered. |
|
||||
| First canonical terrain shard,16 IDs | Registered after all48 actualcontexts,755cardinalpairs and32native scrollphases; independent review passes. No IDhas a destroyedpartner; this adds no HD debris coverage. |
|
||||
|
||||
The catalog also contains **303 AGX cinematic delta frames in 13 sequences** and **4 bitmap fonts**. These have explicit native-art preservation policies: keep the classic cinematics and sharp original glyphs, validate complete playback/alignment, and never count them as generated HD replacements. The AGX frames are separate from standalone pictures.
|
||||
|
||||
## Sequence priorities
|
||||
|
||||
1. Keep all light/dark-player banking poses and firing-flash poses coherent as complete families. Preserve the exact firing-flash source mask, which differs from the light/dark mask for the straight pose.
|
||||
2. Ground defenses, airborne enemies and complete boss animation sequences; share palettes and the original logical canvases, rather than treating each enemy frame as an unrelated illustration.
|
||||
3. Original projectiles and pickup/equipment animations. Tiny projectiles need careful readability checks; per-asset nearest fallback remains temporary until a clean remaster passes review.
|
||||
4. Explosions, flame/spark sequences and other effects. Check full animation timing, transparency and palette cycling, not only a still image.
|
||||
5. All four terrain sets, including unnamed catalog tiles. Generate with neighboring context and verify joined seams before changing runtime coverage.
|
||||
6. Every original menu background, portrait, HUD and retained unused picture. Gameplay UI text and button labels must remain readable and match their actions. Verify the preserved native cinematic/font paths separately.
|
||||
|
||||
## Reproduce the audit
|
||||
|
||||
```sh
|
||||
python3 tools/hd_art_audit.py --data ../data \
|
||||
--json ../data/hd/coverage-audit.json --markdown docs/hd-backlog.md
|
||||
python3 tools/hd_job_queue.py --data ../data
|
||||
```
|
||||
|
||||
The JSON gives every picture's asset ID, path, logical dimensions, original header options, source hash and HD status, plus each declared animation/set. The [completion ledger](hd-completion.json) records every required picture and native-art verification target, including complete families, ownership and candidate/review/registration stages. Both contain metadata only. Original and original-derived artwork remains outside Git.
|
||||
|
||||
Folder totals: `sprites/`: 985, `tiles/`: 232, `misc/`: 4,536. These folders are not semantic categories; terrain and other pictures also live in `misc/`.
|
||||
|
||||
## Secondary effect coverage
|
||||
|
||||
Native animation registrations, pickup masks and beam impact/power companion arrays contribute 281 distinct effect frames. The 25 declared ranges include one five-frame descending-smoke subset of the nine-frame smoke family, leaving 24 complete families. This includes muzzle flashes/casings, laser impacts, smoke trails, lightning, sparks and the super-shield glow as well as explosions. Pickup glow uses indexed light-table mask semantics and needs a different HD treatment from an ordinary color sprite. These assets are not UI artwork merely because they are absent from the content registries. Renderer event reachability needs its own check before claiming complete gameplay effect coverage.
|
||||
|
||||
## Runtime reachability and duplicate artwork
|
||||
|
||||
The terrain backlog contains **4,524 distinct payload hashes** and **4,524 distinct decoded images** across 4,535 catalog IDs. Duplicate IDs can share the same reviewed treatment; they still require explicit source-bound metadata. The canonical HD terrain renderer is validated: intact/destroyed source-bound tiles, shared HD edge/corner treatment and palette-class preservation are implemented. This adds no artwork coverage; each generated tile still requires its own contextual seam review.
|
||||
|
||||
The Python inventory matches the Rust decoder's complete picture ID and logical-dimension inventory. The shipped **27 missions** reference **512 of 528 enemy definitions**, which reduce to **260 of 262 distinct art sequences**, and **4,217 terrain frames**, including destroyed variants. Enemy names may alias the same artwork; the category table counts each asset ID once and the sequence count deduplicates identical base/frame ranges.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Complete-original HD review
|
||||
|
||||
Independent review started **2026-10-10**. This is an ongoing acceptance record, separate from the generation jobs and registration coordinator. Full completion requires all **5,753 standalone source picture IDs** to have reviewed, registered remasters and their retained rendering paths to work. Candidate images, queue entries, original fallback and a successful PNG decode do not establish completion.
|
||||
|
||||
## Inventory checked independently
|
||||
|
||||
A strict picture decoder read every one of the 6,449 original catalog entries and checked the recorded byte lengths. Its 5,753 picture IDs, source SHA-256 hashes and logical canvases match the maintained completion ledger and the previous independent Rust pixel audit. **4,768 pictures are outside `sprites/`.** Every picture has `remaster_required: true`; no unused enemy, directional, terrain or miscellaneous picture has been excluded.
|
||||
|
||||
The disjoint queues contain 659 jobs and 5,714 source targets in this snapshot. Their union with the 46 registered pictures covers all 5,753 IDs exactly; seven registered dark-player pictures remain in their frozen queue. Every job target's source hash and canvas match the independent inventory. The original unused WORM direction and DSHP picture families remain explicitly required.
|
||||
|
||||
All 4,535 terrain pictures remain required, including the 734 destroyed-only IDs absent from intact-map screenshots. Original missions reference 3,483 intact and 736 destroyed IDs, a union of 4,217. The complete target inventory includes the terrain pictures not referenced by those missions.
|
||||
|
||||
The **303 AGX delta frames** and **four bitmap fonts** have explicit preservation policies: classic original cinematics and sharply scaled native glyphs. These exceptions are separate from picture coverage and are never counted as HD replacements.
|
||||
|
||||
Snapshot ledger SHA-256: `3b024a1a1dd342afabfb0f3829f1933769869f6a8798059826e796b3f7e34fb4`. The ignored evidence is `.reference/hd-full-review/inventory.json`; later ledger updates need a fresh snapshot rather than treating this dated count as current.
|
||||
|
||||
## Registered pack reviewed
|
||||
|
||||
Manifest SHA-256 `aa2b823acce157f0b49ee2f44bad133cefeca0304905038f004b59b4b6c22835` supplies **46 frames in 16 complete families**. The actual Rust loader decoded each at all four presentation scales, producing 184 correctly sized indexed sprites with no diagnostics. Every source hash, logical canvas and zero origin passed. All seven source/1×/4× contact pages and six separate native-resolution 2×/3× pages were inspected, including every bank pose and every animation frame.
|
||||
|
||||
| Reviewed family | Original IDs | Frames | Assessment |
|
||||
| --- | --- | ---: | --- |
|
||||
| Light player banking | `1011a–10120` | 7 | Bank ordering, materials, canopy, wing tips and engines remain readable. |
|
||||
| Dark player banking | `20045–2004b` | 7 | Distinct dark lighting preserves the approved bank geometry and canvas. |
|
||||
| Ground targets TARGT1/2/3 | `10138–1013a` | 3 | Source orientation and centered mechanism preserved; full opaque canvases intentional. |
|
||||
| Ground target TARGT6 | `1013d–10141` | 5 | Complete opening/firing sequence aligned on the same square structure. |
|
||||
| Fighters SHIP01G1 / SHIP27G1 | `102f8`, `10330` | 2 | Source directions, canopy and gunmetal/blue or neutral material cues retained. |
|
||||
| Industrial boss | `10306` | 1 | Full source footprint and left/right machinery retained. |
|
||||
| Plasma and enemy-cannon projectiles | `101cf–101d0`, `101d6–101d7` | 4 | Readable indexed projectiles; Plasma's known 1× tail shortening remains documented. |
|
||||
| Energy flash/rings | `101ae–101b9` | 12 | Complete expansion sequence aligned; known narrower first flash and thin rings remain documented. |
|
||||
| Forward, Plasma, Dumb Missile, Air Missile equipment | `102b0–102b2`, `102b5`, `102bf` | 5 | Logical inventory canvases retained; loss of original dark opaque outlines remains documented rather than called identical transparency. |
|
||||
|
||||
This pack is **partial: 5,707 required pictures still have no registered remaster in this snapshot**. Static fighter approval does not cover other old enemies, bosses, directional variants or terrain. A generated menu background does not count as remastering the original portraits, HUD and retained menu pictures.
|
||||
|
||||
Evidence: `.reference/hd-full-review/snapshot-aa2b823a/` contains the exact manifest snapshot, fresh actual Rust 1×–4× indexed outputs, `runtime-qa.json` and every contact-sheet page. The loader library is current for asset decoding and HD loading. Desktop rendering changed after that library was built, so this gallery does not establish those newer runtime routes.
|
||||
|
||||
A subsequent registered snapshot, **63 frames**, has manifest SHA-256 `33ec0a782a852ffef36880432d3e8d472f18a39e9a31d545fe47eb0256bb0dfd`. Fresh actual Rust validation passes all 252 scale outputs without diagnostics. Every prior 46 frame's indexed output is byte-identical to the previous reviewed snapshot; the 16 canonical terrain frames and Super Shield pickup match their independently reviewed isolated outputs. The newly added terrain's native 2× and 3× outputs were also inspected. The picture inventory still has **5,690 unregistered targets** at this snapshot. Evidence: `.reference/hd-full-review/snapshot-33ec0a78/review-comparison.json`.
|
||||
|
||||
**Palette animation acceptance is temporarily held** while a discovered generic HD quantization issue is corrected: static equipment highlights can select native cycling/fire/flash indices 240–255 despite their original source having none. The PNG source and shape can be valid while the runtime asset flashes unintentionally. The coordinator reported this in Micro Missile/Missile Pods/Mega Bomb candidates and requested an engine correction. Recheck the full registered pack after that correction before treating the previous geometry approvals as complete runtime palette approval.
|
||||
|
||||
## Isolated candidate reviews
|
||||
|
||||
| Candidate | Candidate JSON SHA-256 | Actual Rust outputs | Status |
|
||||
| --- | --- | ---: | --- |
|
||||
| Terrain `10360–1036f` | `ad6062f325e730bdb71d22190ff1589f5d86f6c69c62ed94e8caacfdd9ce955c` | 16 × four scales | All source/1×/4× rows inspected; 48 contexts, 755 cardinal pairs and four native scene captures independently pass. Approved for partial-pack registration. |
|
||||
| Terrain `2010d–2011c` | `74d3a5e8cc32569c849c8baeee3a47d7c265e238c5d00b5f84a98a52198e8527` | 16 × four scales | Held: `20111` invents a tree canopy absent from source. |
|
||||
| TARGT5 v2 `1013c` | `e7af1db9fb30f995eaf2c2c360f86c952401bac874087b4f4c462487a9e9ca8a` | One × four scales | Structure preserved and prior orange speckles removed; darker soil needs neighbor review. |
|
||||
| TARG10 v1 `10161` | `44a2720e6b6164cb96888a024b5fc068c4abe6e331cff54e3389c721890c4eb4` | One × four scales | Cannon pose preserved; orange border speckles and soil color need correction or contextual approval. |
|
||||
| Super Shield pickup `102d0` | `adf4b4823aee9bade62b755b170bc0393b73d84c02653e51827109016b3d97ea` | One × four scales | Independently approved; golden S glyph, pointed silhouette, transparency and source canvas preserved. |
|
||||
|
||||
All five isolated packs passed source hash, canvas, origin, PNG/crop and nonempty indexed validation without diagnostics. That result does not approve their artistry. TARGT5's outer three logical pixels change from source mean RGB 124/76/63 to indexed 1× 74/32/28. TARG10 changes from 104/65/55 to 4× 92/40/30 and introduces bright orange border pixels (1.63% of that band; source zero). These measurements explain the requested ground-neighbor check; they are not universal numeric art gates.
|
||||
|
||||
Evidence is under `.reference/hd-full-review/terrain-10360-1036f/`, `terrain-2010d-2011c/`, `held-targt5/`, `held-targ10/` and `equipment-102d0/`. Only source inspection and preview extraction were performed; no candidate pixels or global registration were changed.
|
||||
|
||||
## First terrain family's contextual acceptance
|
||||
|
||||
Candidate `10360–1036f` passed independent inspection of all **48 actual 3×3 contexts** and all **755 cardinal pairs** across 24 review pages: 170 HD/HD joins and 585 HD/original joins. Trees, dirt/grass boundaries, adjoining native coastlines, roads, structures and existing destroyed neighbors keep their source arrangement. No tile hole, crossing-material leakage or low-resolution interior edge grid was found. Texture contrast at the temporary HD/original boundary remains visible, so this approval is for the current partial pack and does not establish finished full-map consistency.
|
||||
|
||||
Four actual native scene captures show phase 0, phase 31, intact terrain and settled destruction. The engineer's runtime assertions compare cached and fresh composition at every one of 32 scroll phases, preserve simulation/pilot/script/RNG state across draw calls, and invalidate the prepared layer after mod hash or load-order changes. The original debug-profile isolated warmed scene averaged 2.97 ms with a 2.76 MiB retained strip and 2 MiB transition lookup. These scene results are distinct from the PNG gallery check.
|
||||
|
||||
This family has **no declared destroyed-state partners**. Its destruction capture exercises another native tile's intact/settled cache transition; it does not approve generated HD debris. Those 734 destroyed-only targets remain required in their own jobs.
|
||||
|
||||
Runtime proof SHA-256: `c00e11d26e2a49a6b0f2e156581a7a7b1fc217f922a34e6b70ed7dbd5b95a0de`. Independent evidence is `.reference/hd-full-review/terrain-10360-1036f/context-review.json`, its 24 cardinal pages and three 48-context contact parts. Original native captures are under `.reference/terrain-hd/terrain-10360-1036f/`.
|
||||
|
||||
## Runtime routes and remaining gates
|
||||
|
||||
The former original beam/effect routing gaps have dedicated native composition paths. Beam shafts, muzzle/power/impact companions and secondary smoke/sparks/glows preserve original frame IDs and original anchors. Original shadows deliberately retain the original logical masks and collision dimensions.
|
||||
|
||||
Code review of the newly extended HUD, timed currency pickup and light-mask routes confirms they request HD frames at the presentation scale while retaining source logical placement. Light masks call indexed `Canvas::shade` instead of painting RGBA colors. Generic HUD assets keep nearest-neighbor source fallback; bitmap font text remains sharply scaled. Focused runtime fixtures now pass at **1× and 4×**: generic HUD and equipped-weapon icons use supplied HD pixels at the original logical anchor; a sparse red RGBA shield replacement uses its own alpha holes and transforms background index **37→44** through the light table rather than painting its red foreground. World/pilot state remains unchanged. The broader 13 original-rendering checks also pass. Supplied-art currency pickup reachability still needs a dedicated runtime assertion or capture; no currency replacement is registered in the reviewed snapshot.
|
||||
|
||||
Canonical terrain candidate review and actual terrain composition checks are ongoing. Fresh isolated Rust review decoded all 32 tiles in the first two groups at scales 1–4 without diagnostics; all eight paged source/1×/4× contact sheets were inspected. The first group (`10360–1036f`) keeps the broad tree/grass/dirt placements. The second group is held for the concrete source drift below. Every candidate tile requires independent source geometry review plus neighboring placements, mixed HD/original joins, destroyed states and scroll phases. Opaque alpha-mask overlap alone is trivial and is not an artistic quality gate. Rendering test success adds no artwork coverage.
|
||||
|
||||
## Open findings
|
||||
|
||||
- Full art generation and registration are unfinished; preserve the per-ID checklist through every cohort.
|
||||
- Correct generic HD quantization to preserve original palette animation classes, then rerun every registered frame at scales 1–4. Hold new equipment registration pending that fix; no regeneration is required for a quantizer-only defect.
|
||||
- Firing-player frames require separate source-contour review, especially straight frame `2004f`; the dark/light banking review does not approve that sequence.
|
||||
- Generic HUD and lighting-mask routes now pass source-anchored indexed runtime fixtures at 1×/4×. Confirm the timed currency pickup route with supplied art and a runtime assertion or capture.
|
||||
- **Hold terrain `2010d–2011c`: picture `20111` invents a tree canopy.** Its original palette contains olive grass and brown dirt only; the remaster adds a bright green canopy at the upper-left covering about 15% of logical pixels. The source hash and opaque alpha-mask overlap pass, demonstrating why those checks do not establish source geometry. Regenerate that picture with the original grass/dirt arrangement before approving the family. Coordinator, artist and terrain engineer were notified. Proof: `.reference/hd-full-review/terrain-2010d-2011c/runtime-contact-sheet-002.png` and `20111-original-enlarged.png`.
|
||||
- First canonical terrain group `10360–1036f` is independently approved; complete the corrected second group contextual review before registration.
|
||||
|
||||
This record will be extended as new complete families arrive. Original-derived artwork and review renders remain in ignored `data/` and `.reference/`; authored mod artwork and sounds remain Git-tracked under `RustRaptor/mods/`.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Independent review of original HD artwork
|
||||
|
||||
Reviewed on 2026-10-10 against the original loose catalog, native artwork references, a fresh Rust decoder/HD-loader probe, and the registered pack. This review was performed separately from the artwork generation and registration jobs.
|
||||
|
||||
The original catalog inventory is complete. **The remaster is still partial:** the reviewed snapshot has 32 registered replacements out of 5,753 standalone pictures. All 32 load successfully and produce visible, correctly sized indexed sprites at presentation scales 1–4. No blocking clipping, background, source-hash or banking-order defect was found in those registered frames. This does not establish full-game visual parity or approve the unregistered drafts.
|
||||
|
||||
## Independent evidence
|
||||
|
||||
Every one of the 6,449 catalog entries passed its recorded byte-size check. A separate strict Python decoder, which does not import the existing artwork audit or source decoder, produced exactly the same picture IDs, dimensions and indexed pixels as a fresh Rust `Sprite::decode` pass. This verified all 5,753 standalone pictures, including **4,768 outside `sprites/`**: 232 in `tiles/` and 4,536 in `misc/`.
|
||||
|
||||
The fresh Rust probe also decoded and applied all **303 AGX delta frames across 13 streams**, checked the active HD pack with no diagnostics, and requested each registered frame at all four scales. Four bitmap fonts were inventoried separately. Their glyphs should retain sharp indexed scaling; cinematic delta streams require a dedicated treatment rather than ordinary sprite registration.
|
||||
|
||||
Original source SHA-256 hashes, logical dimensions and replacement anchors match every registered frame. All active player crop rectangles exclude **zero pixels with alpha at least 32**. The complete runtime sheets were inspected for light-player banking, the boss, two fighters and ground target, equipment, projectiles, and every energy-ring frame.
|
||||
|
||||
Local evidence is saved under `.reference/hd-independent-review/`: `runtime_probe.rs`, `runtime.json`, `audit.py` and `report.json`. The report records per-frame hashes, silhouettes, visible bounds and scale-specific results. It is ignored alongside the original assets. The reviewed manifest SHA-256 is `b75330698a7baac975988cc206f040e1addbe1bb873a210e032a343c78a9e582`; later registered families require another review.
|
||||
|
||||
## Coverage and omissions
|
||||
|
||||
The category correction below includes gameplay artwork that the earlier generation backlog had incorrectly grouped under UI/misc. The overall picture inventory was already complete; these were omissions from the dedicated animation job list.
|
||||
|
||||
| Original category | Picture frames | Registered HD in this snapshot | Remaining |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Player banking and firing flash | 21 | 7 | 14 |
|
||||
| Bosses | 249 | 1 | 248 |
|
||||
| Enemies and ground objects referenced by sprite libraries | 419 | 3 | 416 |
|
||||
| Main player/enemy projectile sprites | 51 | 4 | 47 |
|
||||
| Explosions, smoke, sparks, glows and beam companions | 281 | 12 | 269 |
|
||||
| Equipment and pickups | 47 | 5 | 42 |
|
||||
| Terrain | 4,535 | 0 | 4,535 |
|
||||
| Menus, portraits, HUD and other retained pictures | 150 | 0 | 150 |
|
||||
| **Total standalone pictures** | **5,753** | **32** | **5,721** |
|
||||
|
||||
Native animation registration and direct rendering references revealed **77 gameplay effect frames** missing from the explicit effect sequence backlog:
|
||||
|
||||
| Family | Original asset range / count | Original reference |
|
||||
| --- | --- | --- |
|
||||
| Laser impact | `0x10229`, 4 | `anims.cpp` |
|
||||
| Small smoke | `0x1020a`, 9 | `anims.cpp`; descending smoke reuses the last 5 |
|
||||
| Smoke trail | `0x1022d`, 4 | `anims.cpp` |
|
||||
| Lightning / energy grab | `0x1018e`, 14 | `anims.cpp`; two animation handles share these frames |
|
||||
| Blue sparks | `0x1019c`, 9 | `anims.cpp` |
|
||||
| Orange sparks | `0x101a5`, 9 | `anims.cpp` |
|
||||
| Gun/casing streaks | `0x101c7`, 4 | `anims.cpp`; muzzle and ejected-casing handles share them |
|
||||
| Super-shield glow | `0x10249`, 4 | `anims.cpp` |
|
||||
| Death Ray power | `0x10239`, 4 | `shots.cpp` |
|
||||
| Laser power | `0x1023d`, 4 | `shots.cpp` |
|
||||
| Beam impact | `0x101f1`, 4 | `shots.cpp` / `eshot.cpp` |
|
||||
| Enemy laser power | `0x10245`, 4 | `eshot.cpp` |
|
||||
| Pickup light mask | `0x10225`, 4 | `bonus.cpp` |
|
||||
|
||||
These ranges are counted once despite shared handles and overlapping smoke subsets. The coordinator has incorporated them into the maintained backlog. Pickup glow, descending smoke and super-shield masks use palette-table shading in the original renderer; a colored RGBA replacement alone does not reproduce their operation. The lightning family uses ordinary indexed sprite drawing.
|
||||
|
||||
The four native sprite libraries contain 528 enemy records sharing 262 art sequences and 668 picture frames. The 27 original mission layouts reference 512 records and 260 sequences. Static fighters do not represent the remaining animated bosses, ground targets or directional variants. An additional 33 enemy-shaped pictures remain in the retained miscellaneous inventory: the three unused eight-frame `WORMDN`, `WORMRITE` and `WORMLEFT` families, and nine `DSHP` pictures. No references to those pictures were found in the native sprite libraries or rendering code; retain their catalog entries and identify them explicitly before any cleanup.
|
||||
|
||||
The original `SPACE_AGX` stream contains 22 of the 303 cinematic frames and has no playback reference in the current native source. It remains cataloged and decodable. The classic intro, landing, death and campaign-ending streams remain original artwork; the new authored menu background is a separate packaged asset, not HD coverage for the original logo/menu/portrait pictures.
|
||||
|
||||
## Registered artwork quality
|
||||
|
||||
All seven light-player bank poses remain distinct, progress in the original order and keep coherent silver/gunmetal materials and red markings. The independent logical masks match the recorded 87.7–92.3% silhouette overlap. The nose, canopy, wing ends and engine exits remain visible; maximum logical bound difference is one pixel. The original mask still determines collision dimensions and shadow geometry. The dark-sector `DPLAYER` and firing-flash `FPLAYER` seven-frame families are not registered in this snapshot. The straight firing-flash frame has a different source mask and must receive separate review, even when other poses can reuse lighting edits.
|
||||
|
||||
The first industrial boss, two fighters and opaque ground target preserve their source orientation and canvas. The boss's interpreted metal machinery and both fighters' finer contours differ from the source. Neither their static-frame approval nor their shared palette establishes completion of other enemy/boss families.
|
||||
|
||||
Equipment retains readable visible metal geometry and the two Plasma icon frames remain aligned. The remaster removes nearly black opaque source outlines around the equipment. Consequently full alpha-mask overlap is only about 54–78%, while the separately measured visible-material masks have approximately 93–100% overlap. Both measurements should remain visible in provenance; the material result must not be reported as identical source transparency.
|
||||
|
||||
Small-scale resampling has two documented differences. At 1×, the Plasma projectile loses one tail row (original heights 8/6 visible pixels become 7/5); 4× retains its original logical extent. The first energy flash becomes narrower at 1×, with source bounds `[3,2,38,37]` becoming `[7,5,34,34]`; at 4× its bounds are approximately `[4.75,3.25,36.5,35.75]` in logical units. The later energy rings are smoother and thinner, so silhouette overlap alone is not a useful acceptance gate. These are presentation differences rather than discarded PNG crop pixels. Check the 1× option in motion and retain a crisp fallback if the shorter flash or projectile reduces readability.
|
||||
|
||||
## Remaining rendering and integration checks
|
||||
|
||||
The review found routing gaps that are independent of generation quality. The original equipped-weapon HUD path uses the raw catalog sprite, so registered base equipment HD is bypassed there even though the shop and pickups can use it. Native HUD digits, bomb/shield icons and warning labels also use an intentionally crisp path. New HD portraits already have an appropriate optional route, but no portrait replacements are registered.
|
||||
|
||||
The native beam renderer repeats the main laser sprite along its full line and draws power/impact companion frames. The reviewed Rust projectile path draws the main original projectile sprite without that native composite. Similarly, the current `EffectKind` models the eleven explosion/effect families, but the additional smoke, sparks, muzzle/casing, lightning and glow events found above have no corresponding original-art effect path in that snapshot. Registering their HD PNGs would not by itself make those native visual events appear. Restore and screenshot the original composition/event paths before claiming complete visual fidelity, then review their HD replacements against that output.
|
||||
|
||||
Terrain has four sets of 672, 1,437, 1,082 and 1,344 pictures. They contain 4,524 unique original payloads and decoded indexed images. The original missions require **3,483 intact frame IDs and 736 destroyed frame IDs**, a union of 4,217; **734 are destroyed-only** and therefore absent from an intact-map screenshot. There are 889 destructible intact frames. Those destruction states belong in the remaster jobs and seam tests.
|
||||
|
||||
The reviewed 3×3 shoreline draft is continuous internally and preserves the broad coast, but nine placements contain only six distinct original tile IDs. The generated copies of a repeated tile are not guaranteed identical. It cannot safely be split into global per-ID replacements without a reusable edge contract or context-aware terrain chunks. External neighbors, alternate placements, destroyed states, scrolling offsets and palette cycling remain unverified, and native terrain currently lacks a catalog-HD composition route. The terrain draft remains correctly unregistered.
|
||||
|
||||
Before wider opt-in release, prioritize complete dark/firing player families, the original effect/beam routing gaps, representative remaining animated enemy and boss sequences, and context-aware terrain including destroyed variants. Follow that with remaining equipment/projectiles, original portraits/HUD/logo assets where retained, and a separately approved cinematic treatment. Keep the complete ID inventory as the completion checklist; filenames, a single mission screenshot and the number of generated PNGs are insufficient coverage measures.
|
||||
|
||||
## Follow-up after this snapshot
|
||||
|
||||
The engine now restores native beam composition and secondary visual events, routes optional HD artwork through HUD pictures and indexed light masks, and composes canonical source-bound HD terrain with neighboring context and a bounded scrolling strip. The complete dark-player family has also passed review. These corrections do not complete the remaster: all 5,753 picture IDs remain required, including unused original pictures and destroyed terrain. The ongoing [complete-original review](hd-full-review.md) records subsequent exact manifest hashes, complete-family approvals and held corrections; the [completion ledger](hd-completion.json) is the current per-ID checklist. Classic original cinematics and sharp bitmap fonts keep their explicit preservation policies.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Full-catalog HD loading and caches
|
||||
|
||||
The runtime can register the complete original picture catalog without retaining every decoded replacement in RAM. Registration accepts up to 8 MiB of JSON metadata, 8,192 sequences and 16,384 distinct asset IDs. Each sequence still has at most 512 declared frames. These are validation ceilings, not coverage claims.
|
||||
|
||||
`HdAssets::load_lazy(root, catalog, cache_dir)` checks all sequence IDs before committing the manifest. Duplicate IDs, arithmetic overflow, unsupported formats and invalid sequence lengths reject the pack atomically. Each supplied frame must match the checked original payload SHA-256, logical canvas and `[0,0]` anchor. Its PNG path must resolve inside the pack, including symlinks. Missing files and individual source mismatches produce diagnostics and native-art fallback.
|
||||
|
||||
Registration does not imply a PNG has been decoded. `registered_count()` reports source-checked candidates that have not been rejected; `validated_count()` and the compatibility alias `coverage()` report frames whose artwork has passed validation. First use checks PNG format, crop bounds, matching aspect ratio, at least 2× logical resolution, visible pixels and source-aware transparency. Transparent originals require transparent negative space; opaque originals accept opaque art. A failed frame returns `Ok(None)` from `sprite`, records one diagnostic and keeps native fallback until the pack is reloaded.
|
||||
|
||||
`HdAssets::load` performs full eager validation using the same bounded caches. `validate_all()` can also validate a lazily registered pack; its report distinguishes registered, validated and rejected frames. Full validation can therefore be used in a CLI or audit without raising runtime memory limits. It validates supplied artwork and does not infer coverage for absent originals.
|
||||
|
||||
## Memory bounds
|
||||
|
||||
Each PNG is limited to 32 MiB on disk, 4,096 pixels per dimension, 4,194,304 total pixels and a 64 MiB decoder allocation limit. Reads are bounded even if a file grows after metadata inspection. A loaded frame retains at most its logical canvas at 4× resolution. Larger generated originals remain unchanged on disk.
|
||||
|
||||
Three least-recently-used caches each have a 32 MiB ceiling:
|
||||
|
||||
- Decoded atlas images, keyed by the exact source PNG SHA-256.
|
||||
- Cropped, resampled RGBA frames, keyed by PNG identity, original payload SHA-256, crop, canvas, anchor, alpha requirement and kernel version.
|
||||
- Indexed sprites, keyed by the derived frame identity, presentation scale and complete palette identity.
|
||||
|
||||
Resident cached pixels total at most 96 MiB, irrespective of catalog coverage. Decoder input, resampling work buffers and returned owned sprites are additional bounded working allocations; callers should retain only currently needed sprites. Eviction removes cached pixels, not frame registrations or validated coverage. Adjacent crops reuse the decoded atlas; an atlas is not cloned in full merely to extract a small crop. Repeated draws copy only the final indexed canvas. Palette changes cannot reuse stale indexed colors.
|
||||
|
||||
Source palette labels have a separate hard 32 MiB metadata ceiling. Frames using only ordinary colors need no label map. Frames containing animated labels retain one byte per original logical pixel; transparent and ordinary source locations use the ordinary class. Quantization restricts ordinary HD pixels to indices 0–239, water to 240–244 and fire to 245–249. Original glow/flash labels 250–255 remain exact. Newly visible HD pixels outside the original silhouette use ordinary colors. This prevents a bright static highlight from acquiring fire, water or blinking behavior. Cache color keys include the source class; equal RGB values in different classes cannot share an incorrect palette result.
|
||||
|
||||
## Derived disk cache
|
||||
|
||||
The optional cache belongs under the separate Rust profile cache, for example `RaptorReplica/cache/hd-art-v1`. It stores original-independent derived RGBA files; originals and raw generations are never overwritten. Each filename binds the source PNG hash, exact original payload hash, crop, logical dimensions, anchor, alpha policy and resampling kernel. Palette-dependent indexed sprites remain in memory only.
|
||||
|
||||
A derived file has a versioned header, exact frame identity, dimensions and payload SHA-256. Reads recheck dimensions, byte count, alpha policy and checksum. A corrupted or escaping symlink cache entry is discarded and rebuilt from the checked PNG. Artwork continues to work if the cache is unwritable. Files are written to a new temporary file and atomically renamed; interruption may lose a cache entry, which is safe to regenerate.
|
||||
|
||||
The disk cache is capped at 512 MiB. Older generated `.rgba` files are evicted when space is needed; unrelated files and original art are untouched. Delete this cache to reclaim space at any time. A changed PNG, original source payload, crop or kernel produces a different cache identity.
|
||||
|
||||
## Validation
|
||||
|
||||
`raptor-rust --data /absolute/path/to/data --validate-hd` fully validates the supplied pack and exits unsuccessfully if any supplied artwork fails. Its totals describe supplied frames; the full-catalog completion ledger separately records originals that are still missing replacements.
|
||||
|
||||
Portable synthetic tests exercise a 5,753-frame manifest, full validation, adjacent atlas crops, memory eviction, palette changes and source animation classes at every scale, 4× residency, malformed PNGs, original hash mismatches, corrupted caches, disk eviction and escaping artwork/cache symlinks.
|
||||
|
||||
Run the local original-data integration check with:
|
||||
|
||||
```sh
|
||||
RAPTOR_TEST_DATA=/absolute/path/to/data \
|
||||
cargo test --locked --lib hd::tests::original_hd_pack_validates_with_bounded_residency \
|
||||
-- --ignored --nocapture
|
||||
```
|
||||
|
||||
That check validates every supplied frame and renders it through `HdAssets::sprite` at scales 1–4 with the original palette. It checks dimensions, visible output and each cache ceiling. Visual review, animation alignment and terrain seams remain separate acceptance checks.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Original terrain remaster pipeline
|
||||
|
||||
HD terrain uses the original catalog ID of each currently displayed tile. Intact and destroyed pictures have separate IDs and source hashes; an intact replacement is never reused for a destroyed tile. Native maps keep intact art until their original destruction animation settles. JSON missions retain their existing immediate destroyed-state selection. Rendering does not change terrain health, rewards, collision dimensions, scrolling, update order or RNG.
|
||||
|
||||
The renderer first joins the original tiles at their logical dimensions on a row-aligned 320×268 indexed strip. The additional rows and two-pixel filtering gutter supply neighboring context for the selected Scale2x, Scale3x or Scale4x filter throughout all 32 native scroll phases. This original layer is assembled without consulting HD art, even when HD mode is enabled. Missing or rejected replacements therefore retain the complete original terrain treatment.
|
||||
|
||||
Reviewed native HD tiles are fetched lazily from `HdAssets` only for visible placements. Mod terrain can supply `hd_png`; a source PNG already at least twice its logical dimensions can also keep its supplied detail. Replacements are drawn directly at the selected presentation scale. Every placement uses its original logical anchor and the gutter offset. The completed layer is translated once for fractional scrolling before shadows, enemies, projectiles, effects and the ship are drawn. Individual tiles do not round their scroll offsets independently.
|
||||
|
||||
## Canonical tile and edge contract
|
||||
|
||||
Each original terrain ID needs one canonical full opaque 32×32 logical image, including IDs stored outside the named sprite folders. A generation job should include the exact original source, all cardinal neighbors encountered in mission layouts, actual 3×3 context samples and any intact/destroyed partner. Preserve coast crossings, road widths, building footprints, color families and edge geometry. Fine texture may improve; a displaced coastline or invented solid structure needs correction before acceptance.
|
||||
|
||||
A canonical tile may occur beside several different neighbors. The renderer prepares one pristine joined HD layer for all accepted visible tiles before resolving their edges. Adjacent HD tiles share a detailed indexed edge color where their original material class and source tones agree; four adjacent HD tiles share a common corner color. Distinct original coastline, road or material boundaries remain sharp. Each tile reaches its own full detail two logical pixels inward using integer smoothstep weights.
|
||||
|
||||
At an HD/original join, the replacement transitions to the unchanged joined original context instead. This preserves mixed coverage and source fallback. Full HD neighborhoods retain detailed colors across their internal edges, avoiding the grid produced by forcing every tile back to low-resolution source colors. Source geometry remains a generation and review requirement; edge blending does not repair a shifted coastline.
|
||||
|
||||
Opaque replacements must match `logical_width × scale` and `logical_height × scale` exactly. An unexpected transparent pixel or invalid canvas rejects the whole terrain overlay and adds a diagnostic. Fully opaque source pictures are accepted by the source-aware HD loader; artificial transparent holes are unnecessary.
|
||||
|
||||
The previous generated 3×3 shoreline prototype remains unregistered. It contains nine placements but only six unique original IDs, and its fine detail varies between repeated placements. Splitting it into global ID replacements would select arbitrary placement-specific art. Canonical jobs and source-bound review are required before those IDs count as remastered.
|
||||
|
||||
## Palette and memory behavior
|
||||
|
||||
The final terrain canvas remains indexed. Original water labels stay within the 240–244 cycling group; fire stays within 245–249. Original glow, flash and blink masks at 250–255 retain their exact labels. Ordinary terrain stays within 0–239, so quantizing an HD highlight cannot accidentally turn it into a blinking light. Palette cycling, blue flashes, fades, scanlines and subsequent indexed shadow shading therefore continue to apply to the complete scene.
|
||||
|
||||
The compositor retains a lazy 2 MiB palette transition table, invalidated when the base palette changes, and one prepared row-aligned viewport strip. At 4× its indexed image uses at most 2,744,320 bytes, plus a cache key capped at 64 KiB. It retains no individual HD tiles, atlases or mission map images. HD PNG, RGBA and indexed caches use the bounded lazy `HdAssets` provider. Working layers are temporary viewport images, so registering the complete terrain catalog does not require keeping every generated PNG in memory.
|
||||
|
||||
The prepared strip key includes the mission, row origin, selected intact/destroyed art at every visible placement, active mod hashes in priority order, base palette, presentation scale and HD setting. Terrain destruction invalidates it when the selected artwork changes; mod edits or load-order changes cannot reuse a previous content strip. Between those changes, integer scroll phases and interpolation translate the completed indexed strip without reconstructing or re-quantizing tiles. Palette cycling and fades continue at presentation time. Oversized keys or strips are discarded instead of retained.
|
||||
|
||||
## Verification and acceptance
|
||||
|
||||
Run the portable checks with:
|
||||
|
||||
```sh
|
||||
LD_LIBRARY_PATH=/home/quintine/.local/lib cargo test --locked --manifest-path RustRaptor/Cargo.toml --test hd_terrain
|
||||
```
|
||||
|
||||
The twelve portable tests cover shared edges and four-tile corners at all four scales, fully remastered neighborhoods without a source-pixel grid, hard material boundaries, mixed HD/original joins, overlay order independence, negative gutters, fractional scroll offsets, whole-tile rejection, palette masks, bounded cache memory, logical dimensions, native destruction settling and source-bound lazy loading of distinct intact/destroyed images. The row-aligned strip matches the previous joined SD viewport pixel-for-pixel at all 32 scroll phases, four scales, five interpolation values and negative row origins. Presentation work between simulation steps is compared against an untouched control for world state, RNG, damage and rewards.
|
||||
|
||||
Two additional original-data checks verified all 27 original missions and 4,217 distinct declared intact/destroyed terrain IDs are opaque 32×32 canvases, and produced a compositor comparison from the unregistered shoreline draft. The comparison is under ignored `.reference/terrain-hd/shoreline-draft-context-proof.png`: joined original Scale4x, the raw placement-specific HD draft, and the actual indexed HD composition. This corrected the visible grid found in an earlier source-color edge transition. A separate ignored release timing test exercises 72 visible HD placements with the 1280×816 gutter canvas. On the development machine, its 50 warm frames measured about 5.0 ms median and 6.6 ms p95 after edge/corner precomputation; cold composition took 8.9 ms. These timings cover the compositor only, excluding asset loading, SDL and the rest of the scene.
|
||||
|
||||
The completed strip cache was also measured in the real OpenGL SDL game with HD Stormwall terrain, enemies, projectiles, ship, shadows and HUD. Across 114 measured frames, 1280×800 presentation took 4.08 ms median and 9.86 ms p95 of game work, compared with 15.55/18.64 ms before strip reuse. Fullscreen 1920×1080 with aspect correction took 4.27/9.10 ms; total frame times were 5.86/9.28 ms. A separate cached original-mission `Host` scene averaged 2.97 ms across 60 draws. These local timings use a dummy audio output and describe this scene and machine, not every mission. Both SDL screenshots were inspected for clipping, coastline joins, low-resolution tile grids, foreground order and aspect presentation. Evidence is under `.reference/terrain-hd/stormwall-hd-prepared*.png` and the adjacent timing files.
|
||||
|
||||
Passing renderer tests makes the full catalog renderable; it does not count outstanding generation jobs as accepted artwork. Each supplied tile still needs source/HD review, every encountered neighboring edge and destroyed-state transition checked in real mission layouts, and live captures at the intended display size. Registration, decoded validity, visual acceptance and complete catalog coverage are separate statuses.
|
||||
|
||||
Canonical candidate review tooling uses an isolated manifest and the exact checked `HdAssets` loader, leaving the global manifest and coverage ledger untouched. The first 16-ID group, `terrain-10360-1036f`, produced all 48 source-layout contexts and 755 declared directional neighbor pairs: 170 HD/HD and 585 HD/original. The candidate has no declared destroyed partners. Its actual `Host` scene proof separately tests native destruction settling on a destructible placement, cached-versus-fresh equality at all 32 scroll phases and invalidation on changed mod order or content hash. Captures and JSON records are in `.reference/terrain-hd/terrain-10360-1036f/`. An independent reviewer inspected all context and pair sheets before artistic approval.
|
||||
|
||||
Use these ignored original-data checks for subsequent candidate groups:
|
||||
|
||||
```sh
|
||||
RAPTOR_HD_TERRAIN_JOB=terrain-10360-1036f LD_LIBRARY_PATH=/home/quintine/.local/lib cargo test --locked --release --manifest-path RustRaptor/Cargo.toml --test hd_terrain canonical_terrain_candidate -- --ignored --nocapture
|
||||
RAPTOR_HD_TERRAIN_JOB=terrain-10360-1036f LD_LIBRARY_PATH=/home/quintine/.local/lib cargo test --locked --release --manifest-path RustRaptor/Cargo.toml --lib canonical_terrain_live_scene -- --ignored --nocapture
|
||||
```
|
||||
|
||||
The context/pair check is reusable for any complete canonical terrain candidate. The live scene check selects its first supplied source context and requires that original mission to contain destructible terrain. Successful execution verifies render reachability and consistency; it does not automatically approve generated artwork.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Original opening cinematics
|
||||
|
||||
The normal startup sequence retains the original Apogee and Cygnus logo cards,
|
||||
their individual palettes, the Apogee fanfare and the original city chase,
|
||||
ship, cockpit, firing and explosion animations. **Watch intro** replays that
|
||||
sequence. The new menu background is used after the cinematics; it does not
|
||||
replace any of their artwork.
|
||||
|
||||
The cinematic player uses a separate 70 Hz clock. Original movie rates are
|
||||
converted using the native `GFX_WaitUpdate` rule, integer `70 / frame_rate`
|
||||
ticks per frame. This matters especially for the ship scenes: their native
|
||||
18 fps setting holds each frame for three service ticks. It previously held
|
||||
them for eighteen ticks in the Rust implementation. Frame sound effects now
|
||||
occur after each frame's timed display, as in `MOVIE_Play`.
|
||||
|
||||
| Sequence | Service ticks | Seconds |
|
||||
| --- | ---: | ---: |
|
||||
| Apogee and Cygnus cards, with local original fanfare | 966 | 13.80 |
|
||||
| Complete original introduction | 925 | 13.21 |
|
||||
| Complete startup sequence | 1,891 | 27.01 |
|
||||
| Landing | 288 | 4.11 |
|
||||
| Air and ground death | 1,384 | 19.77 |
|
||||
| Campaign 1 ending, base approach and landing | 1,426 | 20.37 |
|
||||
| Campaign 2 ending, base approach and landing | 1,171 | 16.73 |
|
||||
| Campaign 3 ending, base approach and landing | 1,058 | 15.11 |
|
||||
|
||||
The Apogee hold is derived from the decoded MUS end event, with the original
|
||||
120-tick minimum. The local original score ends at music tick 516, requiring
|
||||
517 music service ticks after it starts. Cygnus plays its two boss sound cues
|
||||
at hold ticks 3 and 120, and starts the looping introduction music at tick 135.
|
||||
The logo fades are 64 ticks in and 63 ticks out. The final landing record is
|
||||
decoded and checked, but its delta is not displayed: that native record fades
|
||||
the preceding frame rather than drawing another frame.
|
||||
|
||||
Jet background patches resolve through the original sound bank's digital
|
||||
variants. The zero-byte catalog FX markers are not decoded as audio. Their
|
||||
repeat intervals use the same quantized pitch table and DSP stepping as the
|
||||
real mixer. The duration helper is checked against actual voice expiry across
|
||||
72 combinations of sample rate, pitch and service clock.
|
||||
Clip transitions stop the outgoing background patch before starting the next
|
||||
one, so a long jet sample cannot spill into the cockpit or firing scene.
|
||||
Chronological play/stop events preserve that order even when an update crosses
|
||||
several scene boundaries. Tagged mixer stops remove the patch's held output
|
||||
immediately while preserving other sounds and their playback positions.
|
||||
|
||||
Run the portable decoder, timing and cue checks from the repository root:
|
||||
|
||||
```sh
|
||||
LD_LIBRARY_PATH="$HOME/.local/lib" cargo test --locked \
|
||||
--manifest-path RustRaptor/Cargo.toml --lib cinematics::tests
|
||||
```
|
||||
|
||||
Run the original catalog integration checks locally:
|
||||
|
||||
```sh
|
||||
LD_LIBRARY_PATH="$HOME/.local/lib" RAPTOR_TEST_DATA="$PWD/data" \
|
||||
cargo test --locked --manifest-path RustRaptor/Cargo.toml \
|
||||
--lib cinematics::tests -- --ignored
|
||||
```
|
||||
|
||||
The checks cover both card palettes, complete logo and introduction durations,
|
||||
music and sound cue times, all original cinematic delta decoding, all sequence
|
||||
rates and rendering-rate independence. Original assets and screenshot captures
|
||||
remain outside version control. The retained native source supplies the frame
|
||||
rates and cue placement; these checks are not a frame-by-frame native video or
|
||||
whole-sequence PCM comparison.
|
||||
@@ -1,34 +1,65 @@
|
||||
# Iron Coast content packages
|
||||
|
||||
Iron Coast is a local campaign assembled from seven independently toggleable
|
||||
mods. Enabling the mission package checks and selects its six installed
|
||||
dependencies automatically. Disabling it also deselects unused dependencies
|
||||
after a confirmation listing the affected packages. Dependencies still needed
|
||||
by another enabled mod remain selected. The Mods manager validates versions
|
||||
Iron Coast contains six independently toggleable mission mods, five shared
|
||||
weapon and enemy mods, a shared soundtrack, six stage boss mods, and an optional
|
||||
campaign assembly mod. Enable **Iron
|
||||
Coast — Complete Campaign** (`iron_coast_campaign`) to select the full campaign
|
||||
and its eighteen dependencies automatically. Each individual mission can also be
|
||||
enabled separately; it selects the five shared content mods, soundtrack and its
|
||||
own boss. Disabling a package also deselects unused dependencies. When this
|
||||
affects other packages, a confirmation lists all affected mods; a single-mod
|
||||
toggle with no dependency changes applies immediately. Dependencies still
|
||||
needed by another enabled mod remain selected. 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_campaign` | Links all six missions into one campaign without replacing mission scripts or artwork |
|
||||
| `iron_coast_mission_1` | Breakwater mission, campaign, supply depots, field repairs, terrain, briefing art and mission audio |
|
||||
| `iron_coast_mission_2` | Tidal Relay mission and its local communications targets, terrain, briefing art and audio |
|
||||
| `iron_coast_mission_3` | Saltworks Convoy mission and its local cargo transports, terrain, briefing art and audio |
|
||||
| `iron_coast_mission_4` | Stormwall mission, cliff batteries, terrain, briefing art and audio |
|
||||
| `iron_coast_mission_5` | Cinder Docks mission and its armored foundries, coolant repairs, terrain, briefing art and audio |
|
||||
| `iron_coast_mission_6` | Iron Crown finale, fortified batteries, dreadnoughts, terrain, briefing art and 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 |
|
||||
| `iron_coast_boss_2` | Relay Warden, authored movement and firing pattern, SD/HD art and audio |
|
||||
| `iron_coast_boss_3` | Saltbreaker VTOL, authored movement and firing pattern, SD/HD art and audio |
|
||||
| `iron_coast_boss_4` | Stormwall Arsenal, authored movement and firing pattern, SD/HD art and audio |
|
||||
| `iron_coast_boss_5` | Cinder Hammer, authored movement and firing pattern, SD/HD art and audio |
|
||||
| `iron_coast_boss_6` | Iron Crown Dreadnought, authored movement and firing pattern, SD/HD art and audio |
|
||||
| `iron_coast_music` | Coastal Steel, original 96-second MUS loop played through the game's Rust FM synthesizer |
|
||||
| `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.
|
||||
All packages are version 1.0.0 and use mod API 1. Every mission requires
|
||||
compatible 1.x versions of its five shared content mods, soundtrack and stage
|
||||
boss. The campaign assembly requires all six mission packages; shared dependencies are selected transitively.
|
||||
Save compatibility records each enabled package's exact version and content
|
||||
hash. A changed or missing mod prompts before loading. Unavailable saved
|
||||
equipment and progression are retained, and the first subsequent write backs
|
||||
up the original pilot.
|
||||
|
||||
The assembly explicitly overrides `iron_coast_mission_1:campaign`, changing its
|
||||
mission list from Breakwater alone to missions 1–6. The Mods manager reports
|
||||
this winning override. Campaign order supplies the next mission when a mission
|
||||
has no explicit `next` ID, so each mission keeps its own asset root and saved
|
||||
script identity. Finishing the sixth mission marks it completed and leaves it
|
||||
available for replay. Disabling the assembly leaves each mission independently
|
||||
playable. Disabling it while another enabled package needs one of its dependencies
|
||||
retains that dependency; the confirmation lists the actual affected packages.
|
||||
|
||||
## 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.
|
||||
turrets and the command ship to finish. Surviving turrets stop at y=72 and the boss stays in the playfield until
|
||||
killed; waiting out the mission duration cannot produce victory. Missing an
|
||||
early battery does not leave an unreachable required target.
|
||||
|
||||
When inventory space allows, the mission grants a trial Pulse Cannon, which
|
||||
fires alongside forward guns, and three field repairs. The cannon is a mission-only loan and is returned before a successful flight
|
||||
@@ -52,12 +83,46 @@ state and completed mission IDs are saved with the pilot:
|
||||
| `iron_coast_mission_1:complete` | All required targets destroyed |
|
||||
| `iron_coast_mission_1:trial_issued` | A loan or an already owned cannon is available |
|
||||
|
||||
## Six-mission route
|
||||
|
||||
The linked route progresses from the outer harbor to the fleet headquarters.
|
||||
Every mission requires explicit objectives; reaching its duration never awards
|
||||
victory. Every mission holds essential structures in the firing area so
|
||||
missed targets cannot make the rest of a flight impossible. Bosses patrol until
|
||||
destroyed. Optional scouts and additional enemies pay their normal rewards.
|
||||
|
||||
| Mission | Required targets | Completion bonus | Support |
|
||||
| --- | --- | ---: | --- |
|
||||
| 1 — Breakwater | Two turrets and one command cruiser | 5,000 | Pulse Cannon loan and three 25-shield field repairs when inventory fits; depot repairs |
|
||||
| 2 — Tidal Relay | Three radar relays and Relay Warden | 7,500 | Pulse Cannon loan when inventory fits; each destroyed relay restores 12 shield |
|
||||
| 3 — Saltworks Convoy | Three cargo transports, two escort turrets and Saltbreaker VTOL | 7,500 | Each intercepted transport restores 15 shield; no equipment grant |
|
||||
| 4 — Stormwall | Four cliff batteries and Stormwall Arsenal | 12,000 | Pulse Cannon loan when inventory fits |
|
||||
| 5 — Cinder Docks | Two armored foundries and Cinder Hammer | 15,000 | Three selectable 30-shield coolant injectors when inventory fits; outer-lane slag volleys have audible warnings |
|
||||
| 6 — Iron Crown | Six citadel batteries and two Iron Crown Dreadnoughts, including the final arrival | 30,000 | Ion Lance loan when inventory fits; 25 shield on entry and 20 at each dreadnought phase |
|
||||
|
||||
Loaned weapons are returned at mission end; owned weapons remain equipped.
|
||||
Inventory grants check the twenty-slot limit, including reserve super-shield
|
||||
modules, and never remove existing equipment to make room. Mission repair
|
||||
items use normal equipment selection and the **Special** control. Keep **Ctrl**
|
||||
held to fire, use **Alt** or **Shift** to cycle, **Z** for Special, and **Space**
|
||||
for a carried bomb with the default bindings.
|
||||
|
||||
Missions 2–6 reference original `base:enemy_*` definitions directly. Those foes
|
||||
retain the original sprite library's flight paths, firing types, animation and
|
||||
rewards; no mod override substitutes generic downward movement.
|
||||
|
||||
Progression records each completed mission and carries shield charge, credits,
|
||||
equipment and script state into the hangar. The player can buy an original
|
||||
Energy refill for 10,000 credits to recover 25 shield between sorties. Selecting
|
||||
a mission directly remains available for replay and independent level mods.
|
||||
|
||||
## 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
|
||||
Mission, enemy and weapon packages include generated masters, packaged
|
||||
SD/HD art, WAV files and prompt/provenance metadata in Git. The assembly owns
|
||||
its SD/HD campaign portrait; the soundtrack package owns its MUS composition. 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
|
||||
@@ -83,52 +148,99 @@ 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.
|
||||
audio service is used. The generators and output hashes are tracked. The soundtrack is an original
|
||||
MUS score played through the same FM synthesizer as the original game music;
|
||||
its source composition, generated score and provenance are included.
|
||||
|
||||
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
|
||||
the complete content 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 mods/iron_coast_mission_1/build_assets.py --palette ../data/misc/PALETTE_DAT
|
||||
cargo run --no-default-features -- --mods mods --validate-mods iron_coast_scout,iron_coast_turret,iron_coast_boss,pulse_cannon,seeker_swarm,ion_lance,iron_coast_mission_1
|
||||
cargo run --no-default-features -- --mods mods --validate-mods iron_coast_scout,iron_coast_turret,iron_coast_boss,iron_coast_music,pulse_cannon,seeker_swarm,ion_lance,iron_coast_mission_1
|
||||
```
|
||||
|
||||
From `RustRaptor/`, validating the full assembly uses all dependencies explicitly
|
||||
at the CLI (the in-game Mods manager selects them automatically):
|
||||
|
||||
```sh
|
||||
cargo run --no-default-features -- --mods mods --validate-mods iron_coast_scout,iron_coast_turret,iron_coast_boss,iron_coast_boss_2,iron_coast_boss_3,iron_coast_boss_4,iron_coast_boss_5,iron_coast_boss_6,iron_coast_music,pulse_cannon,seeker_swarm,ion_lance,iron_coast_mission_1,iron_coast_mission_2,iron_coast_mission_3,iron_coast_mission_4,iron_coast_mission_5,iron_coast_mission_6,iron_coast_campaign
|
||||
```
|
||||
|
||||
## Acceptance checks
|
||||
|
||||
`tests/iron_coast.rs` loads all seven real manifests, definitions and scripts
|
||||
with synthetic PNG/WAV fixtures. It checks dependency activation, artwork
|
||||
`tests/iron_coast.rs` loads the standalone Breakwater manifests, definitions
|
||||
and scripts with synthetic PNG/WAV fixtures and the mod-authored MUS score. 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.
|
||||
|
||||
Release acceptance passes all eight tests in **1.20 seconds**. The tests also
|
||||
check loan cleanup after victory, retention of an owned cannon, and starting
|
||||
with a full inventory including stacked super-shields. Synthetic seven-package
|
||||
activation takes 0.173–0.294 seconds including fixture creation and PNG/WAV
|
||||
validation. Local package validation also checks the generated SD/HD artwork
|
||||
and WAV files, with zero overrides.
|
||||
The two playable runs complete as follows:
|
||||
|
||||
| Loadout | Completion step | Final shield | Credits |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Starter forward guns | 882 | 94 | 7,715 |
|
||||
| Trial Pulse Cannon plus forward guns | 786 | 92 | 7,715 |
|
||||
|
||||
Both start at difficulty 2 with shield 100. The test holds fire, fixes the
|
||||
player at (144,160), selects field repairs on step 2 and presses the special
|
||||
button every 90 steps starting at step 61. The starter run removes the granted
|
||||
trial cannon after initial setup. Granted/dropped repairs heal through normal
|
||||
item callbacks; these runs use no direct shield refills, invulnerability,
|
||||
enemy damage commands or victory-objective mutations. They establish that
|
||||
the actual waves and boss can be completed under this controlled input, not
|
||||
that every normal-player route or loadout has been balanced.
|
||||
The standalone suite also checks mission-only loan cleanup, preservation of
|
||||
owned equipment, full-inventory starts, and required turrets remaining reachable
|
||||
after their scheduled entrance. The full campaign run below exercises the
|
||||
current Mission 1 package together with the later missions and shared soundtrack.
|
||||
|
||||
Release measurements exclude compilation.
|
||||
|
||||
`tests/iron_coast_campaign.rs` adds six portable structural checks and one
|
||||
original-data integration check for the complete route. The portable tests
|
||||
load the real nineteen manifests, content definitions and Rhai scripts with
|
||||
synthetic PNG/WAV resources. Referenced native IDs receive labeled registry
|
||||
placeholders only for these structural checks; their movement is tested using
|
||||
the original catalog in the separate integration run. Generated artwork and
|
||||
sound files are checked separately by local package validation. The six portable checks passed in **0.54 seconds** in release mode, excluding
|
||||
compilation. The suite checks automatic dependency selection, shared dependency retention, independent
|
||||
mission activation, one explicit campaign override, unchanged mission owners,
|
||||
full twenty-slot inventory starts, explicit-next priority, deterministic
|
||||
campaign ordering, terminal replay behavior and versioned pilot save/load.
|
||||
|
||||
The original-data headless campaign starts with the registered pilot's 75
|
||||
shield and 10,000 credits at difficulty 2. It holds fire, moves at most four horizontal
|
||||
pixels per step toward mission structures, turrets, cruisers and scouts, and
|
||||
uses actual consumable callbacks when shield is at most 70. It carries damage,
|
||||
equipment, credits and script state between all six flights and round-trips an
|
||||
atomic save after each victory. It then replays the finale with the recovered
|
||||
mission and boss state, verifies both boss deaths and keeps exactly six
|
||||
completed mission records after another save/load. The harness uses no direct shield refills, enemy-damage commands, invulnerability or
|
||||
objective mutations. Any Energy refills use the actual shop purchase and are
|
||||
recorded in the report.
|
||||
|
||||
The final nineteen-package release run passed in **19.44 seconds**, excluding
|
||||
compilation, after every package was frozen and ignored bytecode was removed.
|
||||
It needed no Energy refills or purchased weapons. All original enemy types
|
||||
authored into missions 2–6 appeared and moved through their native paths.
|
||||
|
||||
| Mission | Completion step | Starting shield | Final shield | Total credits | Native types seen moving |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| 1 — Breakwater | 785 | 75 | 92 | 18,480 | 0 |
|
||||
| 2 — Tidal Relay | 926 | 92 | 86 | 30,685 | 3 |
|
||||
| 3 — Saltworks Convoy | 1,290 | 86 | 82 | 43,425 | 4 |
|
||||
| 4 — Stormwall | 1,217 | 82 | 91 | 61,280 | 6 |
|
||||
| 5 — Cinder Docks | 1,818 | 91 | 89 | 84,510 | 6 |
|
||||
| 6 — Iron Crown | 2,428 | 89 | 100 | 122,705 | 6 |
|
||||
|
||||
The recovered finale replay also completed in **2,428 steps** with **100 shield**,
|
||||
bringing credits to **160,900**. Both dreadnought deaths were required again,
|
||||
and the completed mission list stayed at six entries. Pilot saves and loads
|
||||
preserved the exact active package versions, hashes, inventory, script state
|
||||
and script RNG after every flight and the replay. The local JSON report records
|
||||
these package pins, objectives, native movement metadata and state digests.
|
||||
|
||||
The machine-readable report can be captured locally with:
|
||||
|
||||
```sh
|
||||
RAPTOR_TEST_DATA=../data RAPTOR_IRON_COAST_REPORT=../.reference/iron-coast-six-level.json cargo test --locked --release --no-default-features --test iron_coast_campaign -- --ignored --nocapture
|
||||
```
|
||||
|
||||
A passing original-data run establishes a viable continuous route using
|
||||
ordinary controls and campaign-provided support. It does not establish balance
|
||||
for every loadout, difficulty or normal-player route. Each individual mission also includes its
|
||||
own acceptance runner and generated asset provenance.
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Default machine gun and ground collisions
|
||||
|
||||
The original Forward Guns remain an air-and-ground weapon: two banked muzzle
|
||||
positions, one damage per round, a two-step firing interval and the original
|
||||
8-to-16 projectile acceleration. They do not require another selected weapon.
|
||||
|
||||
The confirmed compatibility defect was in JSON mission terrain. Original guns
|
||||
used the native eight-row collision window even when a mod map was displayed
|
||||
at a different position. Their terrain damage also bypassed destruction: a
|
||||
two-health test tile reached minus 52 health without exploding or paying its
|
||||
reward. JSON terrain now uses its displayed row coordinates and completes
|
||||
destruction immediately, just as mod projectile terrain damage does. This
|
||||
includes short maps, continued scrolling and optional replacement artwork.
|
||||
Destroyed tiles cannot absorb more shots or award their reward again. Their
|
||||
destruction emits the original positional ground-explosion sound.
|
||||
|
||||
Original mission terrain keeps its delayed explosion, neighbor damage and
|
||||
replacement-art timing. Seven portable regressions in
|
||||
`tests/machine_gun_ground.rs` cover ground enemy damage/rewards, one-time terrain
|
||||
destruction and sound, native delayed destruction, short maps, continued
|
||||
scrolling, offscreen rejection and optional replacement artwork. The 15
|
||||
existing gameplay checks, nine enemy package checks and nine Breakwater mission
|
||||
checks also pass. After the correction, the complete 1,889-step native DEMO1
|
||||
trace still matches every recorded enemy, projectile, player and RNG snapshot.
|
||||
|
||||
The local original-data test isolates actual enemy definitions, movement,
|
||||
artwork and callbacks in a quiet test mission. It uses a fresh pilot, only the
|
||||
default guns and ordinary mouse-position/fire input; it never injects
|
||||
projectiles, changes health or grants equipment. It checks classic non-boss
|
||||
ground units from the original first mission and the Iron Coast turret, depot,
|
||||
relay, convoy and foundry, including their kill rewards.
|
||||
|
||||
That local combat check passed for all ten definitions (five classic and five
|
||||
Iron Coast), each starting with 75 shield and 10,000 credits. The 90-health
|
||||
foundry was destroyed in 99 steps with 71 shield remaining. Relay and convoy
|
||||
shield recovery came from their real destruction callbacks. The optional
|
||||
`RAPTOR_GROUND_REPORT` output records each unit's health, kill timing, shield and
|
||||
reward; this run wrote `.reference/machine-gun-ground.json` outside Git.
|
||||
Package PNG/WAV decoding remains covered by the separate mod validator; this
|
||||
collision test loads the actual definitions, logical canvases and Rhai sources.
|
||||
|
||||
Run the portable checks with:
|
||||
|
||||
```sh
|
||||
cargo test --locked --test machine_gun_ground --test gameplay
|
||||
```
|
||||
|
||||
After campaign package files are stable, run the local original-data check with:
|
||||
|
||||
```sh
|
||||
RAPTOR_TEST_DATA=../data cargo test --locked --test machine_gun_ground \
|
||||
real_forward_guns_damage_classic_and_iron_coast_ground_units -- --ignored
|
||||
```
|
||||
|
||||
Commands above run from `RustRaptor/`. Set the usual local SDL library path
|
||||
when needed. The native reference comparison is the ignored
|
||||
`complete_demo_1_matches_every_reference_gameplay_snapshot` test in
|
||||
`tests/campaigns.rs` and reads the untracked original catalog/reference trace.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Menu and classic-intro screenshot review
|
||||
|
||||
The full local-original-data audit captures 31 menu states and 11 frames of the original logos/intro. All 42 captures were inspected, including main menu, hangar, registration, shop purchase/sale details, equipment, missions, options, pilot slots, empty legacy archive, mod dependencies and disable confirmation, mismatched-save loading, pause and results.
|
||||
|
||||
The authored coastal-hangar background is packaged at `art/menu-background-v1.png`. Original bitmap labels remain sharp, panels preserve readable contrast, focus and feedback colors are consistent, and long text stays within panels. Main-menu and hangar actions stay visible together. The mission preview is outside the actionable list hitboxes. Shop cards display artwork, prices, resale value, quantity and automatic/selectable/passive behavior.
|
||||
|
||||
The audit also verifies the complete startup reaches its return screen at exactly 1,891 70 Hz service ticks and restores menu music. Cancelling a mod-mismatch prompt preserves the original save; accepting it displays unavailable equipment without silently discarding it.
|
||||
|
||||
Run the local audit from `RustRaptor/`:
|
||||
|
||||
```sh
|
||||
RAPTOR_TEST_DATA="$PWD/../data" LD_LIBRARY_PATH="$HOME/.local/lib" \
|
||||
cargo test --locked --lib every_menu_screen_and_original_intro_visual_audit -- --ignored
|
||||
```
|
||||
|
||||
Captures and the action-row manifest stay ignored in `../.reference/ui-all/`. The background was generated with the imagegen skill's built-in image generation tool; the exact prompt and provenance are tracked in `art/menu-background-v1.prompt.txt` and `art/provenance.json`. Original cinematic pixels remain outside Git.
|
||||
@@ -19,7 +19,7 @@ cargo run --no-default-features -- --data ../data --mods mods \
|
||||
--enabled aurora --headless aurora:field_trials --ticks 700
|
||||
```
|
||||
|
||||
The manager discovers the bundled `mods/` folder by default when no user mods folder exists. You can also copy `mods/aurora` into the `mods/` directory under `$XDG_DATA_HOME/RaptorReplica` (default `~/.local/share/RaptorReplica/mods/`), or start the Rust engine with `--mods mods`. Open **Mods**, toggle Aurora, then select **Apply selections**. Select its mission from the mission menu. Existing pilots can add mods while preserving required versions, hashes and relative load order; the next save records the expanded active set. Disabling, changing or reordering required mods blocks loading with details. Use another pilot for an incompatible set.
|
||||
The manager discovers the bundled `mods/` folder by default when no user mods folder exists. You can also copy `mods/aurora` into the `mods/` directory under `$XDG_DATA_HOME/RaptorReplica` (default `~/.local/share/RaptorReplica/mods/`), or start the Rust engine with `--mods mods`. Open **Mods**, toggle Aurora, then select **Apply selections**. Select its mission from the mission menu. Existing pilots can add mods while preserving required versions, hashes and relative load order; the next save records the expanded active set. Disabling, changing or reordering required mods shows compatibility details and asks whether to load anyway. Missing equipment and progression remain saved until their packages are restored.
|
||||
|
||||
In the Mods screen, Enter toggles a package or activates the selected action. Enabling a package immediately checks versions, API compatibility, cycles and conflicts, then selects its installed dependencies recursively. A failed check leaves the selection unchanged. Disabling includes dependent mods and dependencies that become unused, preserving dependencies required by other remaining packages. A single-package disable toggles immediately; changes affecting multiple packages show an **Are you sure?** popup listing every affected mod, with Cancel selected initially. Cancel or Back preserves the pending selection. Rescan after editing a package on disk. Dependencies load before dependents; independent packages retain your chosen priority. Later explicit overrides win. Changes made through the pause menu remain pending until the main menu. Candidate registries and scripts are validated before the active set changes.
|
||||
|
||||
@@ -56,7 +56,7 @@ All package paths are relative. Absolute paths, parent traversal, symlinks and s
|
||||
|
||||
## Content definitions and overrides
|
||||
|
||||
`content.json` has optional `weapons`, `items`, `enemies`, `missions`, `campaigns`, `sounds` and `overrides` arrays. Unknown fields are rejected. IDs have the form `namespace:name`, for example `aurora:pulse_cannon`. IDs are stable save references and unique across content types. Keep item and projectile IDs distinct: the item `aurora:pulse_cannon` refers to the weapon/projectile `aurora:pulse_projectile` through its `weapon` property.
|
||||
`content.json` has optional `weapons`, `items`, `enemies`, `missions`, `campaigns`, `sounds`, `music` and `overrides` arrays. Unknown fields are rejected. IDs have the form `namespace:name`, for example `aurora:pulse_cannon`. IDs are stable save references and unique across content types. Keep item and projectile IDs distinct: the item `aurora:pulse_cannon` refers to the weapon/projectile `aurora:pulse_projectile` through its `weapon` property.
|
||||
|
||||
Items may include an optional `description` of up to 4096 UTF-8 bytes for shop information. The shop displays the description, artwork, effective target types, firing mode, price, ownership and inventory limits.
|
||||
|
||||
@@ -91,12 +91,19 @@ New definitions must belong to the package namespace. An existing definition may
|
||||
| Weapon/projectile | Damage, integer speed, cooldown in simulation ticks, targeting (`air`, `ground`, `all`), behavior (`straight`, `homing`, `beam`, `spread`, `bomb`, `energy_grab`), firing offsets, optional animation and script. Scripts can emit additional projectiles with explicit velocity. |
|
||||
| Item | Price, maximum and initial count, optional weapon, shield/credit effects, consumable/special flags, artwork and script. `initial_count` controls how many units a purchase or pickup adds. |
|
||||
| Enemy | Health, speed, rewards, air/ground and boss flags, firing cooldown, path points and repeat flag, weapon, dropped item, animation and script. Scripts can query entities and change enemy movement. |
|
||||
| Mission | Duration, `auto_complete` (default true), optional briefing artwork, ordered tick-based spawns, terrain tile definitions and row-major layout, column count, next mission, optional music asset and script. Scripts can manage objectives, spawn waves and decide success/failure. |
|
||||
| Mission | Duration, `auto_complete` (default true), optional briefing artwork, ordered tick-based spawns, terrain tile definitions and row-major layout, column count, next mission, optional legacy music asset or namespaced `music_track`, and script. Scripts can manage objectives, spawn waves and decide success/failure. |
|
||||
| Campaign | Name, optional menu artwork and ordered mission IDs. Missions and campaigns appear through the same registries as original content. |
|
||||
| Music | Namespaced ID, display name, package-relative MUS file and `looping` (default true). Mission `music_track` references this ID; playback uses original GENMIDI FM and the Music volume control. |
|
||||
|
||||
For non-legacy missions, an explicit `next` takes priority. Otherwise progression
|
||||
uses the following entry in the first containing campaign, ordered by stable
|
||||
campaign ID. The last stage stays selected for replay, and completing it again
|
||||
does not duplicate its completion record. This lets an optional assembly mod
|
||||
join independently owned missions without overriding their scripts or artwork.
|
||||
|
||||
Simulation uses integer coordinates in the original 320×200 logical canvas and runs at 70/3 Hz: one tick is 3/70 seconds. Artwork detail does not enlarge collision boxes. Script movement and projectile speed are bounded to 128 logical pixels per tick; spawn and movement coordinates must stay within ±32,768.
|
||||
|
||||
JSON Schemas are checked into `schemas/`: `mod-v1.json`, `content-v1.json`, `hook-v1.json`, `weapon-projectile-v1.json`, `item-v1.json`, `enemy-v1.json`, `mission-v1.json` and `campaign-v1.json` and `sound-v1.json`. Schemas describe serialization shape; the validation CLI also checks references, numeric limits, package paths, dependencies, artwork and script compilation. Generate them from the Rust types with:
|
||||
JSON Schemas are checked into `schemas/`: `mod-v1.json`, `content-v1.json`, `hook-v1.json`, `weapon-projectile-v1.json`, `item-v1.json`, `enemy-v1.json`, `mission-v1.json`, `campaign-v1.json`, `sound-v1.json` and `music-v1.json`. Schemas describe serialization shape; the validation CLI also checks references, numeric limits, package paths, dependencies, artwork and script compilation. Generate them from the Rust types with:
|
||||
|
||||
```sh
|
||||
cargo run --no-default-features -- --schemas schemas
|
||||
@@ -196,7 +203,11 @@ Rhai uses an explicit package allowlist for arithmetic, strings, collections, it
|
||||
|
||||
Rust pilots use separate, versioned JSON files under `$XDG_DATA_HOME/RaptorReplica`, and settings use TOML under `$XDG_CONFIG_HOME/RaptorReplica`. Atomic writes replace completed saves after file and directory synchronization. Legacy saves are read and imported; their original bytes are not modified.
|
||||
|
||||
Each pilot records the ordered mod IDs, package versions, full content hashes, script states and script RNG positions. Loading requires the same active pins and all referenced equipment, missions and progression. Changing a package without changing its version still changes its hash and blocks that save. Disabling a mod preserves its saved inventory and progression; it never silently discards them. Restore the matching package/version and load order, or use another pilot. This API does not yet provide mod save migrations.
|
||||
Each pilot records the ordered mod IDs, package versions, full content hashes, script states and script RNG positions. The load screen reports missing packages, changed versions or hashes, changed required load order, and unavailable equipment or missions. Changing a package without changing its version still changes its hash and therefore requires confirmation. Cancel leaves the current pilot and original save untouched; choosing to load accepts the differences for that pilot session.
|
||||
|
||||
Unavailable equipment stays in the pilot's inventory with its saved quantity and occupies its original inventory slots, but cannot be fired, selected or traded until its definition is available. Unavailable completed missions remain recorded. If the saved current mission is missing, choose an available mission in the hangar before flying. Runtime saves retain inactive package pins, script state and script RNG alongside the active packages, so restoring a disabled package can recover its data. Current active versions and hashes are recorded after an accepted load is successfully saved; the original save is backed up before its first replacement. Structural corruption, unsupported save versions and invalid IDs still cannot be loaded. This API does not yet provide scripted mod save migrations, so a changed script must handle its earlier serializable state.
|
||||
|
||||
For Rust tooling, `Pilot::inspect_load` returns the unmodified pilot and a structured `SaveCompatibility` report. `Pilot::load` remains strict for callers that require a fully matching environment. Only consume `PilotLoad::confirm_load` after explicit acceptance. Use `validate_tolerant` and `complete_mission_tolerant` for that accepted session, and `merge_runtime_snapshot` to retain saved state and pins belonging to inactive packages.
|
||||
|
||||
## Validation tests
|
||||
|
||||
@@ -209,3 +220,19 @@ An artwork definition may add `hd_png`, a separate package-relative PNG with the
|
||||
`sounds` definitions have `id`, `name`, `file` (relative path) and optional `volume` (0–127, default127), `pan` (0–255, default127), `pitch` (0–255, default128) and `priority` (0–100000, default1). Lower numeric priority wins, matching the original DSP. Play one with `#{op:"play_sound",sound:"your_mod:shot"}`. WAV inputs must be uncompressed PCM8/16, mono/stereo, 8–48kHz, at most30seconds/8MiB. Stereo is mixed to mono and converted to the original unsigned8-bit digital format; channel/pan/pitch/volume use the original mixer. Original DMX patches are also accepted. Invalid/missing audio rejects activation.
|
||||
|
||||
For objective-driven missions set `auto_complete=false` and explicitly return `complete` or `fail`. Scripted endings dispatch `on_mission_end` in the same transaction. Commands can create further hook events, bounded by the shared execution budget and an8192-event tick limit.
|
||||
|
||||
|
||||
## Package mission music
|
||||
|
||||
Register a track in the optional `music` array. A music definition contains `id`, `name`, a package-relative `.mus` `file` and optional `looping` (default `true`). A mission selects it with `music_track`; setting both `music_track` and legacy numeric `music` rejects activation. Declare a dependency on a separate music package so enabling the mission also selects its installed soundtrack. Music IDs use the same namespace, duplicate and explicit-override rules as other content. Winning overrides resolve their score from the winning package.
|
||||
|
||||
```json
|
||||
{
|
||||
"music": [{"id":"your_mod:theme", "name":"Mission theme", "file":"music/theme.mus", "looping":true}],
|
||||
"missions": [{"id":"your_mod:mission", "name":"New mission", "duration":2000, "music_track":"your_mod:theme"}]
|
||||
}
|
||||
```
|
||||
|
||||
Only checked MUS scores are accepted, played through the original GENMIDI FM synthesizer and the **Music volume** control. Digital effects keep their separate volume. External MIDI and SoundFont playback remain deferred. Scores are limited to 128 KiB, 16,384 events, 256 events per service tick and 30 minutes of score time. The MUS header, event types, controller values, primary-channel count and end marker are checked by the existing decoder. Zero-duration scores, missing files, escaping paths, invalid scores and unsupported formats reject activation with the package/file identified. Score bytes participate in the normal mod hash and pilot compatibility checks. A bounded score cache is cleared when mods are applied.
|
||||
|
||||
The selected track starts with the mission, honors its loop flag, and can be scheduled before the SDL device is ready. Selecting the same active track preserves playback position; ordinary menus and classic cinematics select their original tracks. See [music validation](music-validation.md) for the registry, file, volume, loop and device-attachment checks.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Mod mission music
|
||||
|
||||
Package music uses namespaced `MusicDef` entries and a mission's optional
|
||||
`music_track`. It plays through the existing Rust MUS/GENMIDI FM synthesizer,
|
||||
with the ordinary Music volume and fade controls. Sound effects keep their
|
||||
separate digital bus. Original numeric mission tracks, menus and classic
|
||||
cinematics keep their existing music paths.
|
||||
|
||||
Tracks use package-relative `.mus` files. The registry checks references,
|
||||
duplicate IDs, namespaces and explicit overrides. Activation decodes the
|
||||
winning owner's file and rejects missing, escaping, malformed or oversized
|
||||
scores. The package's existing SHA-256 hash includes the score file, so pilot
|
||||
mod compatibility covers music changes. Applying mods clears the bounded
|
||||
decoded-score cache and returns an active mod track to main-menu music.
|
||||
|
||||
Limits apply to mod music without changing the legacy decoder: 128 KiB per
|
||||
file, 16,384 events, at most 256 events at any service tick, positive duration,
|
||||
and at most 126,000 score ticks (30 minutes at 70 Hz). Header ranges, supported
|
||||
event types, primary channel count, velocities, controllers and the final end
|
||||
event are checked by `MusScore::decode`. Cache allocation is bounded separately
|
||||
from digital effects. Only MUS is supported; external MIDI and SoundFonts
|
||||
remain deferred.
|
||||
|
||||
The host retains a namespaced desired-track ID and its loop flag. A mission
|
||||
preview can select its music before SDL audio is initialized; attaching the
|
||||
device applies that pending score. Re-selecting the same active ID/loop
|
||||
preserves playback position. Selecting original menu or cinematic music clears
|
||||
the mod identity. A failed score load leaves the pilot and menu state intact.
|
||||
|
||||
Synthetic checks cover registration and missing references, conflicting
|
||||
legacy/custom choices, explicit overrides and owner resolution, hash changes,
|
||||
cache invalidation, path and symlink containment, unsupported formats, file
|
||||
size, event density, duration and channel limits. FM output checks exercise the
|
||||
definition's actual loop flag and show that Music volume mutes the mod score
|
||||
while digital effects remain audible. A one-play score's ending tail has less
|
||||
than one twentieth the energy of the looping score in the tested fixture.
|
||||
|
||||
```sh
|
||||
cargo test --locked --no-default-features \
|
||||
--manifest-path RustRaptor/Cargo.toml --test music
|
||||
```
|
||||
|
||||
Portable host checks verify pending identity, original-menu transitions and
|
||||
rollback when music cannot load. The paused SDL test attaches a real device
|
||||
object and compares consecutive FM PCM blocks against uninterrupted reference
|
||||
playback, proving that selecting the same track does not restart it:
|
||||
|
||||
```sh
|
||||
LD_LIBRARY_PATH="$HOME/.local/lib" cargo test --locked \
|
||||
--manifest-path RustRaptor/Cargo.toml --lib mission_mod_music
|
||||
LD_LIBRARY_PATH="$HOME/.local/lib" cargo test --locked \
|
||||
--manifest-path RustRaptor/Cargo.toml --lib failed_mission_music_load
|
||||
LD_LIBRARY_PATH="$HOME/.local/lib" SDL_AUDIODRIVER=dummy cargo test --locked \
|
||||
--manifest-path RustRaptor/Cargo.toml --lib pending_mod_music_resumes -- --ignored
|
||||
```
|
||||
|
||||
These checks validate the engine path using synthetic scores. Composition,
|
||||
listening review and original-data in-game soundtrack captures are separate
|
||||
release checks for each music package.
|
||||
@@ -37,11 +37,11 @@ Portable fixtures use synthetic assets and the bundled authored mod art/audio. O
|
||||
|
||||
## Profiles and settings
|
||||
|
||||
Rust uses `$XDG_DATA_HOME/RaptorReplica/pilot-NNNN.json` and `mods/`, `$XDG_CONFIG_HOME/RaptorReplica/config.toml`, and `$XDG_CACHE_HOME/RaptorReplica/` for derived assets. Normal XDG defaults apply. Pilots record active package versions/hashes and script state/RNG. Missing or changed requirements block loading, without deleting equipment or replacing the save.
|
||||
Rust uses `$XDG_DATA_HOME/RaptorReplica/pilot-NNNN.json` and `mods/`, `$XDG_CONFIG_HOME/RaptorReplica/config.toml`, and `$XDG_CACHE_HOME/RaptorReplica/` for derived assets. Normal XDG defaults apply. Pilots record active package versions/hashes and script state/RNG. Missing, changed or reordered requirements show a compatibility prompt with Cancel selected and **Load anyway** available. Confirming keeps unavailable equipment, quantities, progression and inactive script state. The first subsequent write preserves the previous save in a hash-named `before-mod-change` backup. Corrupt files and unsafe structural values remain errors; inspecting or cancelling never modifies the file.
|
||||
|
||||
Legacy `~/.local/share/Raptor/SETUP.INI` and pilot files are imported read-only. All difficulties, waves, portrait and training metadata are retained. Rust writes only its separate versioned files. Explicit CLI import output cannot be the original file.
|
||||
|
||||
Options include scaling, scanlines, aspect correction, fullscreen/borderless, input bindings/controller, volumes and optional HD art. New/load pilot controls keep incompatible save slots separate. The Mods screen applies validated selections at the main menu; selections made during flight remain pending until returning there.
|
||||
Options include scaling, scanlines, aspect correction, fullscreen/borderless, input bindings/controller, volumes and optional HD art. New/load pilot controls show compatibility details for affected save slots. The Mods screen applies validated selections at the main menu; selections made during flight remain pending until returning there.
|
||||
|
||||
The main menu leads to the Hangar, new/load/import pilots, options, mods and
|
||||
extras. The Hangar keeps flying, mission selection, the supply room, inventory,
|
||||
@@ -71,7 +71,7 @@ SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy cargo run --locked -- \
|
||||
--screenshot ../.reference/menu.png
|
||||
```
|
||||
|
||||
Use real SDL video for interactive/display-performance checks. Dummy screenshots verify raster placement, not GPU/vsync performance. `--preview-mission ID` starts a mission; `--preview-cinematic intro|landing|death|end1|end2|end3` previews movies. Frame-limited runs do not save pilots/settings; cache stays within the selected profile.
|
||||
Use real SDL video for interactive/display-performance checks. Dummy screenshots verify raster placement, not GPU/vsync performance. `--preview-mission ID` starts a mission; `--preview-cinematic credits|intro|landing|death|end1|end2|end3` previews movies. Frame-limited runs do not save pilots/settings; cache stays within the selected profile.
|
||||
|
||||
## Controlled reference replays
|
||||
|
||||
@@ -96,6 +96,6 @@ All 1,889 available steps of DEMO1G1_REC match the captured fields: RNG, player
|
||||
|
||||
The Rust FM/DSP implementation has synthetic sample-by-sample native comparisons and a four-second original intro-music comparison at 44.1kHz. The checked sound bank also dispatches original and mod cues through the SDL callback; real-device output checks are documented in [sound validation](sound-validation.md). These establish those cases, not every track/input/output rate. The Rust core retains the original envelope/noise semantics and LGPL source provenance.
|
||||
|
||||
The campaign checks cover all waves and difficulties with assistance, and first-mission tests win without replenishing pilot health or editing enemy state. Wider ordinary playtesting and more complete native state comparisons remain useful. Original cinematics decode/play locally; exact fade/transition/custom-palette fidelity has limited comparison coverage. Some menu layouts differ. Base-game HD coverage is partial; bundled mods supply SD/HD art. See [display measurements](performance.md).
|
||||
The campaign checks cover all waves and difficulties with assistance, and first-mission tests win without replenishing pilot health or editing enemy state. Wider ordinary playtesting and more complete native state comparisons remain useful. Normal startup includes the original logos, fanfare and intro. Native timing, sound-marker resolution, scene stops and custom logo palettes are checked in [intro validation](intro-validation.md). The refined menu uses an authored hangar backdrop and has a [42-frame screenshot review](menu-validation.md). Base-game HD coverage is partial; bundled mods supply SD/HD art. See [display measurements](performance.md).
|
||||
|
||||
Scale2x/3x follows the [published algorithm](https://www.scale2x.it/algorithm); Scale4x applies Scale2x twice. WAV parsing follows the [RIFF chunk format](https://learn.microsoft.com/en-us/windows/win32/xaudio2/resource-interchange-file-format--riff-). Original catalog/reference assets remain outside version control. Authored mod artwork and sounds are tracked.
|
||||
@@ -50,8 +50,34 @@ The inspected light ship frames retain 97.2..98.3% of nearest-neighbor opaque co
|
||||
|
||||
The original terrain patch exposed 113 differing pixels at 2×, 297 at 3× and 727 at 4× when the tiles were filtered independently. Filtering the joined patch with the tested two-pixel neighbor gutter exactly matched the full-image center at each factor. The paired contact sheets show subtle texture discontinuities along independent tile boundaries; joined filtering retains the diagonal water transition. This review covers the crisp original terrain path; supplied HD terrain needs its own artistic edge review.
|
||||
|
||||
The optional HD player proof remains a separate review: it replaces only straight flight, so banking still changes visual style. The generated extreme-bank attempt remains excluded because it shortened the engines and failed the footprint check; crisp original fallback is retained for that frame.
|
||||
Optional HD replacements receive a separate source, canvas and silhouette review, recorded in `hd-art.md` and `hd-backlog.md`. The runtime selects accepted frames from the HD manifest and retains the original crisp fallback for unregistered or rejected frames. Original ScaleNx checks above do not certify generated HD artwork.
|
||||
|
||||
The frontend previously requested `AIRMED_BLK`, which does not exist in the original catalog. Effects now carry typed identities for the original air/ground explosions, flak, energy, people, flares and sparkles. The renderer selects each sequence's asset, duration, centered canvas origin and ground/air layer. Enemy deaths retain their original multiple bursts and RNG consumption; ground effects follow terrain scroll. This source-based port repairs the missing sequence and generic fallback. Exhaustive visual comparison of every effect remains outside the sampled checks above.
|
||||
|
||||
The new enemy packages include their generated SD/HD artwork and audio in Git. Their original style-reference pixels under `art/source/` remain excluded. Their per-package `art-qa.json` and `provenance.json` provide a separate record of supplied-art canvas alignment and generation prompts.
|
||||
|
||||
## Native effects and player presentation
|
||||
|
||||
The 2026-10-10 source review also compared `ANIMS_Register`, `SHOTS_Display`, `ESHOT_Display`, `BONUS_Display`, `RAP_GetShipPic` and `FLAME_Down` against their Rust render paths. It restored player and enemy laser shafts tiled every three logical pixels, their distinct power animations, native impact caps and three indexed turret-tracer lines. Emissions now include missile smoke, blue/orange hit sparks, energy-grab animations attached to enemies, world-space gun casings, missile-pod muzzle animations, shield lighting and delayed terrain flare/sparkle overlays. Gun casings use the original four-frame streak rather than an explosion at the muzzle. Native explosions and secondary animations jointly obey the original 100-animation allocation limit; visual allocation failure leaves weapon firing and RNG consumption intact.
|
||||
|
||||
Pickup halos, smoke masks and shield glow use the original +9 VGA light lookup to transform the existing background indices. Mask colors are not painted as foreground sprites. Colored companions use the normal optional HD selection and original fallback. The secondary queue and pickup presentation latches are excluded from saved gameplay serialization, advance on simulation steps and never consume the simulation RNG. Eight focused portable tests cover beam direction, cap offsets, tracer colors, lighting-mask holes, casing placement, smoke drift/lifetime, unchanged gameplay state and pool saturation. The complete retained native demo still matches all 1,889 gameplay snapshots, finishing with shield 3 and 60,825 credits.
|
||||
|
||||
Player presentation selects all seven `LPLAYER` poses normally. The two native dark missions, sector 2 wave 8 and sector 3 wave 3, select `DPLAYER` between shots and `FPLAYER` when the existing simulation firing-flash latch is set. Light missions retain `LPLAYER` during the flash. The player and its original shadow mask use the same selected canvas. Accepted HD frames are read by that asset ID, with original fallback for an unregistered dark or flash frame.
|
||||
|
||||
Engine flames are native procedural shading, not missing sprite files. Each simulation tick selects an eight-row or twelve-row phase; the two four-pixel-wide bands follow the original seven bank offsets and draw immediately before the ship. Eight original VGA remap tables add 16, 14, 12, 10, 8, 6, 4 and 2 to six-bit palette components. The ramp preserves the native 16.16 accumulator and recomputes its height after viewport clipping. The ship covers the bands at its rear edge, and both flame passes disappear when the player is hidden. Four portable tests cover all campaign/frame mappings, bank offsets, interpolated placement, exact ramp phases, clipping, overlapping shade passes, composition order and render-only behavior at 1× and 4×.
|
||||
|
||||
Local render captures are under ignored `../.reference/secondary-effects/`. `native-effects-{sd,hd}-{1,2,3}.png` show controlled beam and secondary-animation frames. `native-player-family-{0,1,2}-{sd,hd}.png` show the light, dark and flash families in their original mission scenes. `native-player-banking-flames-{sd,hd}.png` contain all seven banking poses, with rows ordered light/eight-row phase, light/twelve-row phase, dark/eight-row phase, dark/twelve-row phase, flash/eight-row phase and flash/twelve-row phase. Their fixtures assert that drawing does not mutate the world. These controlled captures assess placement and composition; they are not an exhaustive pixel comparison of every effect against C++.
|
||||
|
||||
Both player contact sheets and the light HD and dark firing-flash mission scenes were inspected after capture. Engines remain registered across the seven banks, both phases retain their indexed terrain colors and the ship covers the upper flame row. At capture time, the light family selected its supplied HD artwork; the dark and firing-flash frames retained the original crisp fallback. The earlier secondary-effect captures were also inspected for beam caps, pickup halos and shield composition. Subsequent complete-family approvals are recorded in the maintained HD ledger and independent full review.
|
||||
|
||||
```sh
|
||||
cargo test --lib original_
|
||||
cargo test --test secondary_effects --test transitions
|
||||
cargo test --lib original_secondary_effects_capture_sd_and_hd_without_gameplay_mutation -- --ignored
|
||||
cargo test --lib original_player_flames_and_flash_capture_complete_banking_families_in_sd_and_hd -- --ignored
|
||||
cargo test --test campaigns complete_demo_1_matches_every_reference_gameplay_snapshot -- --ignored
|
||||
```
|
||||
|
||||
The generic `A_SMALL_SMOKE` and `A_ENERGY_GRAB_HIT` handles have no C++ call sites, so no invented emitters were added. The restored render paths do not imply that every original effect has an HD remaster; remaining artwork is tracked in the HD backlog.
|
||||
|
||||
Generic HUD pictures, timed currency pickups and native light masks now consult the same source-bound optional HD pack. HUD and pickup artwork retains its original logical anchor and canvas. Smoke, shield and pickup glow use replacement alpha only to shade existing palette indices through the native light table. They do not paint the replacement's RGB colors onto the scene. Synthetic HUD and sparse HD-mask regressions at 1× and 4× verify the supplied artwork reaches these paths, indexed shading retains holes and centers, and drawing does not change world state. Original collision and projected shadow masks continue to use native geometry.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Iron Coast: Relay Warden
|
||||
|
||||
Standalone API 1 boss package used by the Tidal Relay mission. It has no dependencies and never decides when a mission finishes. Enable the mission to select this package automatically through its dependency.
|
||||
|
||||
- Enemy: `iron_coast_boss_2:unit`, health 160, reward 1,800 credits, airborne boss.
|
||||
- Projectile: `iron_coast_boss_2:projectile`, damage 3, speed 4, 8×8 logical canvas.
|
||||
- Completion observation: `iron_coast_boss_2:defeated` increases once for each actual death. Duplicate notifications are harmless; despawning or time passing does not count.
|
||||
- Art: one complete static frame on a 96×64 logical canvas. SD and HD preserve the same origin, full silhouette and true alpha. HD is 768×512; gameplay collision dimensions remain 96×64.
|
||||
- Audio: `shot`, `warning` and `death` namespaced sound IDs, each a generated, non-silent 22,050 Hz, mono, 16-bit PCM WAV.
|
||||
|
||||
The Warden enters at two logical pixels per tick, stops at y=24, and patrols x=80…144. Each entity tracks its own arrival timer and direction. An audible radar charge starts 12 ticks before each 54-tick volley. Odd volleys fire two parallel outer lanes; even volleys fire two diverging inner lanes. The opening below its central radar dish remains readable. Each queued spawn registers its life even if a projectile has already killed it before callback dispatch; that early kill still counts when the engine delivers its actual death. Timers and directions are removed when the aircraft dies, and dead units cannot emit movement, warnings or shots. Difficulty scaling comes from the engine, including its existing half-health rule on the two lower difficulties.
|
||||
|
||||
## Asset sources
|
||||
|
||||
The two masters were generated using the built-in imagegen tool from the saved prompts, with the existing generated Iron Coast cruiser inspected for material and color style. The Warden and cyan radar pulse have new silhouettes; no original game art or recordings are copied into this package. `provenance.json`, `art-qa.json` and `audio-recipe.json` record inputs, processing, alpha bounds and hashes. All PNGs, WAVs and prompts are intended for Git.
|
||||
|
||||
Recreate the SD/HD output and audio from the saved masters with:
|
||||
|
||||
```sh
|
||||
python3 RustRaptor/mods/iron_coast_boss_2/build_assets.py
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
The package contains a small Rust validation program that uses the game's public modding and deterministic simulation API. It validates package artwork and WAV decoding, both telegraphs and alternating volleys, separate entity timers, actual death rewards/objectives, repeated and dead callbacks, cleanup, a real lethal projectile during the spawn step, and reuse of an entity ID for a new life. It requires no original game assets.
|
||||
|
||||
```sh
|
||||
CARGO_TARGET_DIR=RustRaptor/target cargo run --offline \
|
||||
--manifest-path RustRaptor/mods/iron_coast_boss_2/validation/Cargo.toml \
|
||||
--bin validate-boss
|
||||
```
|
||||
|
||||
Its saved result is `validation/report.json`. Tidal Relay's validation also exercises this boss in the complete mission with the real original content registry.
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"logical_unit": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"logical_projectile": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"hd_scale": 8,
|
||||
"frames": 1,
|
||||
"alpha": "True RGBA; runtime palette mapping uses alpha >= 128",
|
||||
"conversion": "Full source canvas Lanczos HD, BOX SD; no sprite redraw or catalog copies",
|
||||
"files": [
|
||||
{
|
||||
"file": "art/unit-sd.png",
|
||||
"size": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"opaque_bounds": [
|
||||
3,
|
||||
3,
|
||||
93,
|
||||
62
|
||||
],
|
||||
"source": "art/unit-generated-v1.png",
|
||||
"alpha_min_max": [
|
||||
0,
|
||||
254
|
||||
],
|
||||
"sha256": "0d41d6bf34f7812b684c724aa44c9477187dfbe7a38faa958f0d3887d7e9770d"
|
||||
},
|
||||
{
|
||||
"file": "art/unit-hd.png",
|
||||
"size": [
|
||||
768,
|
||||
512
|
||||
],
|
||||
"opaque_bounds": [
|
||||
20,
|
||||
21,
|
||||
748,
|
||||
498
|
||||
],
|
||||
"source": "art/unit-generated-v1.png",
|
||||
"alpha_min_max": [
|
||||
0,
|
||||
255
|
||||
],
|
||||
"sha256": "272e1a206d40e06ddc54a0c9cb763db9a776647d3392c2a063cc551cfab939cb"
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-sd.png",
|
||||
"size": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"opaque_bounds": [
|
||||
3,
|
||||
2,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"source": "art/projectile-generated-v1.png",
|
||||
"alpha_min_max": [
|
||||
0,
|
||||
225
|
||||
],
|
||||
"sha256": "f871add4de5a44d6dff953d12369bcff56c2a1f65bdffc861d962b8d1b6616e4"
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-hd.png",
|
||||
"size": [
|
||||
64,
|
||||
64
|
||||
],
|
||||
"opaque_bounds": [
|
||||
24,
|
||||
13,
|
||||
40,
|
||||
49
|
||||
],
|
||||
"source": "art/projectile-generated-v1.png",
|
||||
"alpha_min_max": [
|
||||
0,
|
||||
255
|
||||
],
|
||||
"sha256": "abd22235128301f609feebcc73bc3910ef248b678002cea88cf655efd739ffb6"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 135 B |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"generator": "build_assets.py deterministic chirp/noise synthesis; no recordings",
|
||||
"files": [
|
||||
{
|
||||
"file": "audio/shot.wav",
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 4630,
|
||||
"duration": 0.21,
|
||||
"peak": 25558,
|
||||
"rms": 4171.3,
|
||||
"seed": 212,
|
||||
"sha256": "67c6b5b2dd9748584f9db50c3c251cc441218a3600b40f5c96407c2da5928d62"
|
||||
},
|
||||
{
|
||||
"file": "audio/warning.wav",
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 7938,
|
||||
"duration": 0.36,
|
||||
"peak": 25558,
|
||||
"rms": 8556.4,
|
||||
"seed": 222,
|
||||
"sha256": "915cf9a4e73f7d3a9b89e036640211e8f052bc490637c5244566f3c35f69080e"
|
||||
},
|
||||
{
|
||||
"file": "audio/death.wav",
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 20947,
|
||||
"duration": 0.95,
|
||||
"peak": 25558,
|
||||
"rms": 3815.13,
|
||||
"seed": 232,
|
||||
"sha256": "0b836bfcf01aea73587dcbb5a26460dbf6153acc03732fe0c047401a62ad2dfa"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resize saved generated masters and synthesize reproducible package WAVs.
|
||||
|
||||
No original assets are copied. Canvas resampling retains the master's true alpha;
|
||||
art is never redrawn. Run from any directory with Python 3 and Pillow installed.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import struct
|
||||
import wave
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
RATE = 22050
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def artwork():
|
||||
stats = []
|
||||
for name, size, scale in [('unit', (96, 64), 8), ('projectile', (8, 8), 8)]:
|
||||
path = ROOT / 'art' / (name + '-generated-v1.png')
|
||||
original = Image.open(path).convert('RGBA')
|
||||
assert original.getchannel('A').getextrema()[0] == 0
|
||||
if name == 'unit':
|
||||
assert original.width * size[1] == original.height * size[0]
|
||||
hd = original.resize((size[0] * scale, size[1] * scale), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
# Keep the complete square alpha canvas; this preserves every source pixel.
|
||||
side = max(original.size)
|
||||
canvas = Image.new('RGBA', (side, side), (0, 0, 0, 0))
|
||||
canvas.alpha_composite(original, ((side-original.width)//2, (side-original.height)//2))
|
||||
hd = canvas.resize((size[0] * scale, size[1] * scale), Image.Resampling.LANCZOS)
|
||||
sd = hd.resize(size, Image.Resampling.BOX)
|
||||
for mode, sprite in [('sd', sd), ('hd', hd)]:
|
||||
alpha = sprite.getchannel('A')
|
||||
mask = alpha.point(lambda a: 255 if a >= 128 else 0)
|
||||
bounds = mask.getbbox()
|
||||
assert bounds and bounds[0] > 0 and bounds[1] > 0
|
||||
assert bounds[2] < sprite.width and bounds[3] < sprite.height
|
||||
out = ROOT / 'art' / (name + '-' + mode + '.png')
|
||||
sprite.save(out)
|
||||
stats.append({'file': str(out.relative_to(ROOT)), 'size': list(sprite.size),
|
||||
'opaque_bounds': list(bounds), 'source': str(path.relative_to(ROOT)),
|
||||
'alpha_min_max': list(alpha.getextrema()), 'sha256': sha(out)})
|
||||
(ROOT / 'art-qa.json').write_text(json.dumps({
|
||||
'logical_unit': [96, 64], 'logical_projectile': [8, 8], 'hd_scale': 8,
|
||||
'frames': 1, 'alpha': 'True RGBA; runtime palette mapping uses alpha >= 128',
|
||||
'conversion': 'Full source canvas Lanczos HD, BOX SD; no sprite redraw or catalog copies',
|
||||
'files': stats}, indent=2) + '\n')
|
||||
|
||||
|
||||
def audio():
|
||||
stats = []
|
||||
for name, duration, seed in [('shot', 0.21, 212), ('warning', 0.36, 222), ('death', 0.95, 232)]:
|
||||
rng = random.Random(seed)
|
||||
count = int(duration * RATE)
|
||||
phase = 0.0
|
||||
low_noise = 0.0
|
||||
signal = []
|
||||
for n in range(count):
|
||||
t = n / RATE
|
||||
p = n / max(1, count-1)
|
||||
noise = rng.uniform(-1, 1)
|
||||
low_noise = low_noise * .86 + noise * .14
|
||||
if name == 'warning':
|
||||
# Two brief rising radar pips separated by a clear gap.
|
||||
local = t % .18
|
||||
frequency = 1450 + 900 * min(1, local / .11)
|
||||
phase += 2 * math.pi * frequency / RATE
|
||||
envelope = (math.sin(math.pi * min(1, local / .12)) ** 2 if local < .12 else 0)
|
||||
value = .68 * math.sin(phase) + .15 * math.sin(phase * 2)
|
||||
elif name == 'shot':
|
||||
phase += 2 * math.pi * (170 + 710 * (1-p)**2) / RATE
|
||||
value = .60*math.sin(phase) + .22*math.sin(phase*1.52) + .12*noise
|
||||
envelope = min(1, t / .003) * math.exp(-p*6.0)
|
||||
else:
|
||||
phase += 2 * math.pi * (80 - 55*p) / RATE
|
||||
value = .65*low_noise + .20*noise + .26*math.sin(phase)
|
||||
envelope = min(1, t / .004) * math.exp(-p*4.5)
|
||||
envelope *= min(1, (count-1-n) / (RATE*.005))
|
||||
signal.append(value * max(0, envelope))
|
||||
maximum = max(abs(x) for x in signal)
|
||||
pcm = [round(v / maximum * 0.78 * 32767) for v in signal]
|
||||
path = ROOT / 'audio' / (name + '.wav')
|
||||
with wave.open(str(path), 'wb') as wav:
|
||||
wav.setnchannels(1); wav.setsampwidth(2); wav.setframerate(RATE)
|
||||
wav.writeframes(struct.pack('<' + 'h'*len(pcm), *pcm))
|
||||
stats.append({'file': str(path.relative_to(ROOT)), 'sample_rate': RATE,
|
||||
'channels': 1, 'bits': 16, 'samples': count, 'duration': duration,
|
||||
'peak': max(abs(x) for x in pcm),
|
||||
'rms': round(math.sqrt(sum(x*x for x in pcm)/len(pcm)),2),
|
||||
'seed': seed, 'sha256': sha(path)})
|
||||
(ROOT / 'audio-recipe.json').write_text(json.dumps({
|
||||
'generator': 'build_assets.py deterministic chirp/noise synthesis; no recordings',
|
||||
'files': stats}, indent=2) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
artwork()
|
||||
audio()
|
||||
print('Generated Relay Warden SD/HD art and three non-silent WAVs')
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "iron_coast_boss_2:projectile",
|
||||
"name": "Relay Warden radar pulse",
|
||||
"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_2:unit",
|
||||
"name": "Relay Warden",
|
||||
"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": 1
|
||||
},
|
||||
"script": "scripts/ai.rhai"
|
||||
}
|
||||
],
|
||||
"sounds": [
|
||||
{ "id": "iron_coast_boss_2:shot", "name": "Relay Warden pulse volley", "file": "audio/shot.wav", "volume": 95, "pan": 127, "pitch": 128, "priority": 2 },
|
||||
{ "id": "iron_coast_boss_2:warning", "name": "Relay Warden radar charge", "file": "audio/warning.wav", "volume": 86, "pan": 127, "pitch": 128, "priority": 2 },
|
||||
{ "id": "iron_coast_boss_2:death", "name": "Relay Warden destruction", "file": "audio/death.wav", "volume": 120, "pan": 127, "pitch": 128, "priority": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "iron_coast_boss_2"
|
||||
name = "Iron Coast: Relay Warden"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
description = "A broad radar-aircraft boss for Tidal Relay, with an audible warning and alternating two-lane pulse volleys. Includes transparent SD/HD art and generated sounds."
|
||||
content = "content.json"
|
||||
scripts = ["scripts/ai.rhai"]
|
||||
@@ -0,0 +1,5 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: one transparent projectile sprite for Relay Warden radar-aircraft boss in Iron Coast top-down shooter.
|
||||
Primary request: small compact cyan radar pulse, a vertically pointed metallic dart with dark gunmetal center and bright cyan electrical collar arranged around it. Directly overhead, dart nose points DOWN. Flat orthographic view, narrow central dart inside a short cyan oval electrical sheath. Restrained bright cyan body, white small central core, dark gunmetal stub fins; the silhouette stays compact and readable at 8x8 logical pixels. No long trails. This is different from a fireball or explosion.
|
||||
Composition: one whole projectile centered in a square transparent canvas, with 18% blank margin all sides.
|
||||
Constraints: genuinely transparent RGBA background; no text, no logo, no scene, no checkerboard artwork, no exterior shadow, no second projectile. Preserve crisp self-contained alpha silhouette.
|
||||
@@ -0,0 +1,7 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: genuinely transparent HD boss sprite for top-down 2D vertical scrolling shooter; Relay Warden, new Iron Coast electronic warfare boss.
|
||||
Input reference image: inspected Iron Coast Command Cruiser is a STYLE reference only; create a DIFFERENT silhouette, a broad aircraft with wingspan rather than a narrow cruiser.
|
||||
Primary request: one large broad industrial radar aircraft, strict orthographic view directly above, pointed nose facing DOWN the image toward bottom edge. Strong symmetrical swept armored wing spanning horizontally, gunmetal grey metal plates, tiny dark red markings, a prominent circular radar dish centered on the upper fuselage, two orange engine exhaust slots facing up, two downward-facing cannon muzzles integrated into wing roots. Radar dish is a substantial physical armored circular fixture, not glowing energy. Heavy believable 1990s Raptor-inspired industrial aircraft design with crisp edge detail and restrained textures.
|
||||
Composition: single whole sprite centered, exact full canvas aspect 3:2, aircraft fills roughly 82% canvas width and 82% canvas height with clear transparent margin on every side, nose down, whole wings and tail included. Flat overhead lighting from upper left; no perspective, no horizon.
|
||||
Palette: gunmetal and slate grey, restrained dark red panels, amber indicators.
|
||||
Constraints: true RGBA transparent background, no shadow outside aircraft, no text, no labels, no logo, no watermark, no scene, no additional sprites, no tile grid. No white/matte fringe. Ship should remain readable when reduced to 96 by 64 pixels.
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"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",
|
||||
"style_reference": {
|
||||
"path": "../iron_coast_boss/art/unit-hd.png",
|
||||
"role": "Inspected generated Iron Coast boss artwork used only as material/style reference; new broad radar aircraft silhouette and unique projectile generated independently",
|
||||
"original_catalog_pixels_copied": false
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"file": "art/unit-generated-v1.png",
|
||||
"prompt": "prompts/unit-hd-v1.txt",
|
||||
"tool_output": "exec-28f23852-b44b-4461-89d2-eb72c6e150a0.png",
|
||||
"sha256": "ef00f35f6df18d5590e7c64168fa071daf31b116dca36f86542f1852865ce5ab"
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-generated-v1.png",
|
||||
"prompt": "prompts/projectile-hd-v1.txt",
|
||||
"tool_output": "exec-7200b663-a79e-4899-a062-3d06a7bae851.png",
|
||||
"sha256": "51fe35c238b299fbcac748278e6799c6e4bfe30439471ad064d8a696ca97b147"
|
||||
}
|
||||
],
|
||||
"pipeline": "build_assets.py: full-canvas alpha-preserving Lanczos HD / BOX SD, deterministic 22050Hz 16-bit mono sound synthesis",
|
||||
"version_control": "All generated masters, shipped SD/HD PNGs, WAVs, prompts and provenance tracked in Git per user request"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// All cursors are per entity. Arrival and attack timers use simulation callbacks.
|
||||
fn is_alive(event) {
|
||||
for unit in event.entities {
|
||||
if unit.entity == event.entity && unit.health > 0 { return true; }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn on_spawn(event, state, random) {
|
||||
// Register the life before checking current liveness: the engine can kill a
|
||||
// queued spawn during advance, before it dispatches this callback. Its later
|
||||
// actual death still needs one objective update, without movement or audio.
|
||||
if !state.contains("directions") { state.directions = #{}; }
|
||||
if !state.contains("ages") { state.ages = #{}; }
|
||||
let key = event.entity.to_string();
|
||||
state.directions[key] = 1;
|
||||
state.ages[key] = 0;
|
||||
if !is_alive(event) { return #{ commands: [], state: state }; }
|
||||
#{ 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();
|
||||
// An untracked unit cannot acquire another entity's attack timer.
|
||||
if !state.contains("ages") || !state.ages.contains(key) {
|
||||
return #{ commands: [], state: state };
|
||||
}
|
||||
let direction = state.directions[key];
|
||||
if event.x >= 144 { direction = -1; }
|
||||
if event.x <= 80 { direction = 1; }
|
||||
state.directions[key] = direction;
|
||||
let age = state.ages[key] + 1;
|
||||
state.ages[key] = age;
|
||||
let commands = [
|
||||
#{ op: "move", entity: event.entity, x: event.x, y: 24 },
|
||||
#{ op: "velocity", entity: event.entity, vx: direction, vy: 0 }
|
||||
];
|
||||
// Twelve ticks of warning precede the shot: about half a second at 70/3 Hz.
|
||||
if age % 54 == 42 {
|
||||
commands.push(#{ op: "play_sound", sound: "iron_coast_boss_2:warning" });
|
||||
}
|
||||
if age % 54 == 0 {
|
||||
// Alternate parallel outer lanes with a diverging inner fork.
|
||||
// Both leave a readable opening directly under the central radar dish.
|
||||
if (age / 54) % 2 == 1 {
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_2:projectile", x: event.x + 24, y: 75, vx: 0, vy: 4, hostile: true });
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_2:projectile", x: event.x + 72, y: 75, vx: 0, vy: 4, hostile: true });
|
||||
} else {
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_2:projectile", x: event.x + 36, y: 75, vx: -1, vy: 4, hostile: true });
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_2:projectile", x: event.x + 60, y: 75, vx: 1, vy: 4, hostile: true });
|
||||
}
|
||||
commands.push(#{ op: "play_sound", sound: "iron_coast_boss_2: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) {
|
||||
let key = event.entity.to_string();
|
||||
// A death callback must refer to a spawned unit that is absent from live queries.
|
||||
// Removing its cursor also makes repeated death notifications harmless.
|
||||
if is_alive(event) || !state.contains("ages") || !state.ages.contains(key) {
|
||||
return #{ commands: [], state: state };
|
||||
}
|
||||
state.ages.remove(key);
|
||||
state.directions.remove(key);
|
||||
state.destroyed = if state.contains("destroyed") { state.destroyed + 1 } else { 1 };
|
||||
let defeated = if event.objectives.contains("iron_coast_boss_2:defeated") { event.objectives["iron_coast_boss_2:defeated"] + 1 } else { 1 };
|
||||
#{ commands: [
|
||||
#{ op: "objective", key: "iron_coast_boss_2:defeated", value: defeated },
|
||||
#{ op: "effect", x: event.x + 32, y: event.y + 38 },
|
||||
#{ op: "effect", x: event.x + 64, y: event.y + 38 },
|
||||
#{ op: "play_sound", sound: "iron_coast_boss_2:death" }
|
||||
], state: state }
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.9.1",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iron-coast-boss-2-validation"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"raptor-replica",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "raptor-replica"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"image",
|
||||
"rhai",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0334639972c0ea5a3fd366aa36116754a11431b619fec3ed559b3f73bcbcebf5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"no-std-compat",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd3a7535e50bf36857e7be7bec276d334e8c2dfa469c2201226fd01638ea5ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6a4b9ba8738cb4a4f399d37e266becfd475e75eb73425b87a05a2f2039ba63e"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "iron-coast-boss-2-validation"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
raptor-replica = { path = "../../..", default-features = false }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
|
||||
[[bin]]
|
||||
name = "validate-boss"
|
||||
path = "validate.rs"
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"actual_cues": [
|
||||
"iron_coast_boss_2:shot",
|
||||
"iron_coast_boss_2:warning"
|
||||
],
|
||||
"actual_engine_tick": 271,
|
||||
"dead_or_repeated_callbacks": "no commands",
|
||||
"death_objective": 1,
|
||||
"first_volley": [
|
||||
[
|
||||
136,
|
||||
75,
|
||||
0,
|
||||
4,
|
||||
true,
|
||||
"iron_coast_boss_2:projectile"
|
||||
],
|
||||
[
|
||||
184,
|
||||
75,
|
||||
0,
|
||||
4,
|
||||
true,
|
||||
"iron_coast_boss_2:projectile"
|
||||
]
|
||||
],
|
||||
"mod": "iron_coast_boss_2",
|
||||
"outcome": "Playing",
|
||||
"package_validation": "passed; PNGs, sound decoding, manifest and script",
|
||||
"per_entity_state_cleanup": "passed",
|
||||
"queued_spawn_step_lethal_projectile": "passed",
|
||||
"reused_entity_id_new_life": "counts once",
|
||||
"reward": 1800,
|
||||
"second_volley": [
|
||||
[
|
||||
148,
|
||||
75,
|
||||
-1,
|
||||
4,
|
||||
true,
|
||||
"iron_coast_boss_2:projectile"
|
||||
],
|
||||
[
|
||||
172,
|
||||
75,
|
||||
1,
|
||||
4,
|
||||
true,
|
||||
"iron_coast_boss_2:projectile"
|
||||
]
|
||||
],
|
||||
"volley_ages": [
|
||||
54,
|
||||
108
|
||||
],
|
||||
"warning_ages": [
|
||||
42,
|
||||
96
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Standalone mod acceptance without original copyrighted game data.
|
||||
use anyhow::{Result, ensure};
|
||||
use raptor_replica::{
|
||||
content::{ContentId, ContentPack, Registry},
|
||||
modding::{self, ActiveMods, Command, ScriptRuntime},
|
||||
persistence::Pilot,
|
||||
simulation::{Input, Outcome, Projectile, World},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::PathBuf;
|
||||
const OWNER: &str = "iron_coast_boss_2";
|
||||
const SCRIPT: &str = "scripts/ai.rhai";
|
||||
const STATE: &str = "iron_coast_boss_2/scripts/ai.rhai";
|
||||
const OBJECTIVE: &str = "iron_coast_boss_2:defeated";
|
||||
|
||||
fn event(entity: u64, health: i32, x: i32, y: i32) -> Value {
|
||||
json!({"entity":entity,"health":health,"x":x,"y":y,"tick":900,
|
||||
"objectives":{},"entities":[{"entity":entity,"health":health,"x":x,"y":y}]})
|
||||
}
|
||||
fn invoke(runtime: &mut ScriptRuntime, hook: &str, event: Value) -> Result<Vec<Command>> {
|
||||
runtime.begin_tick();
|
||||
runtime.invoke(OWNER, SCRIPT, hook, &event, 31)
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let mods = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.canonicalize()?;
|
||||
let mut base = Registry::default();
|
||||
let content: ContentPack = serde_json::from_value(json!({
|
||||
"weapons":[{"id":"base:projectile_forward_guns","name":"Forward guns","damage":1000,"speed":8,"cooldown":2,"behavior":"straight","target":"all"}],
|
||||
"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":"Boss test","duration":10000,"auto_complete":false,
|
||||
"spawns":[{"tick":1,"enemy":"iron_coast_boss_2:unit","x":112,"y":-64}]}]
|
||||
}))?;
|
||||
base.apply(content, "base")?;
|
||||
let discovery = modding::discover(&mods);
|
||||
let mut active = ActiveMods::build(&base, &discovery, &[OWNER.into()], None)?;
|
||||
let def = &active.registry.enemies[&ContentId::from("iron_coast_boss_2:unit")];
|
||||
ensure!(def.health == 160 && def.reward == 1800 && def.boss && def.speed == 0);
|
||||
let rt = &mut active.scripts;
|
||||
let mut cues = Vec::new();
|
||||
invoke(rt, "on_spawn", event(1, 160, 112, 24))?;
|
||||
let mut first = Vec::new();
|
||||
let mut second = Vec::new();
|
||||
for age in 1..=108 {
|
||||
let commands = invoke(rt, "on_tick", event(1, 160, 112, 24))?;
|
||||
for command in &commands {
|
||||
if let Command::PlaySound { sound } = command {
|
||||
cues.push((age, sound.clone()));
|
||||
}
|
||||
}
|
||||
let shots: Vec<_> = commands
|
||||
.iter()
|
||||
.filter_map(|command| {
|
||||
if let Command::Projectile {
|
||||
x,
|
||||
y,
|
||||
vx,
|
||||
vy,
|
||||
hostile,
|
||||
weapon,
|
||||
} = command
|
||||
{
|
||||
Some((*x, *y, *vx, *vy, *hostile, weapon.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if age == 54 {
|
||||
first = shots;
|
||||
} else if age == 108 {
|
||||
second = shots;
|
||||
} else {
|
||||
ensure!(shots.is_empty(), "unexpected volley at age {age}");
|
||||
}
|
||||
}
|
||||
ensure!(first.len() == 2 && second.len() == 2 && first != second);
|
||||
ensure!(first.iter().all(|p| p.2 == 0 && p.3 == 4 && p.4));
|
||||
ensure!(second[0].2 == -1 && second[1].2 == 1);
|
||||
ensure!(
|
||||
cues == vec![
|
||||
(42, format!("{OWNER}:warning")),
|
||||
(54, format!("{OWNER}:shot")),
|
||||
(96, format!("{OWNER}:warning")),
|
||||
(108, format!("{OWNER}:shot"))
|
||||
]
|
||||
);
|
||||
invoke(rt, "on_spawn", event(2, 160, 112, 24))?;
|
||||
ensure!(rt.states[STATE]["ages"]["2"] == 0);
|
||||
ensure!(
|
||||
invoke(rt, "on_death", event(1, 160, 112, 24))?.is_empty(),
|
||||
"living unit cannot register a death"
|
||||
);
|
||||
ensure!(
|
||||
invoke(rt, "on_tick", event(1, 0, 112, 24))?.is_empty(),
|
||||
"dead unit cannot fire or warn"
|
||||
);
|
||||
let mut death = event(1, 0, 112, 24);
|
||||
death["entities"] = json!([{"entity":2,"health":160}]);
|
||||
let commands = invoke(rt, "on_death", death.clone())?;
|
||||
ensure!(
|
||||
commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::Objective{key,value} if key==OBJECTIVE && *value==1))
|
||||
);
|
||||
ensure!(commands.iter().all(|c| !matches!(c, Command::Complete)));
|
||||
ensure!(
|
||||
invoke(rt, "on_death", death)?.is_empty(),
|
||||
"duplicate death cannot increment objective"
|
||||
);
|
||||
ensure!(rt.states[STATE]["ages"].get("1").is_none());
|
||||
ensure!(
|
||||
rt.states[STATE]["ages"]["2"] == 0,
|
||||
"another entity retains independent cursor"
|
||||
);
|
||||
let mut death2 = event(2, 0, 112, 24);
|
||||
death2["entities"] = json!([]);
|
||||
death2["objectives"] = json!({OBJECTIVE:1});
|
||||
let commands = invoke(rt, "on_death", death2)?;
|
||||
ensure!(
|
||||
commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::Objective{key,value} if key==OBJECTIVE && *value==2))
|
||||
);
|
||||
ensure!(rt.states[STATE]["ages"].as_object().unwrap().is_empty());
|
||||
ensure!(
|
||||
rt.states[STATE]["directions"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// Run actual engine hooks and command application; no scripted mission completion.
|
||||
active = ActiveMods::build(&base, &discovery, &[OWNER.into()], None)?;
|
||||
let pilot = Pilot::default();
|
||||
let mut world = World::new(&pilot.mission, &pilot, &active.registry)?;
|
||||
let mut actual_cues = std::collections::BTreeSet::new();
|
||||
for _ in 0..150 {
|
||||
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
for cue in &world.mod_sounds {
|
||||
actual_cues.insert(cue.0.clone());
|
||||
}
|
||||
}
|
||||
ensure!(world.enemies.len() == 1 && world.enemies[0].y == 24);
|
||||
ensure!(world.enemies[0].x >= 79 && world.enemies[0].x <= 145);
|
||||
ensure!(
|
||||
actual_cues.contains("iron_coast_boss_2:warning")
|
||||
&& actual_cues.contains("iron_coast_boss_2:shot")
|
||||
);
|
||||
let initial_credits = world.player.credits;
|
||||
world.enemies[0].health = 0;
|
||||
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
ensure!(world.objectives[OBJECTIVE] == 1 && world.enemies.is_empty());
|
||||
ensure!(world.player.credits == initial_credits + 1800);
|
||||
ensure!(
|
||||
world.outcome == Outcome::Playing,
|
||||
"boss never completes the mission itself"
|
||||
);
|
||||
let shots = active.scripts.states[STATE]["shots"].clone();
|
||||
for _ in 0..120 {
|
||||
world.step(Input::default(), &active.registry, &mut active.scripts)?;
|
||||
}
|
||||
ensure!(world.objectives[OBJECTIVE] == 1 && active.scripts.states[STATE]["shots"] == shots);
|
||||
ensure!(
|
||||
active.scripts.states[STATE]["ages"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
// A projectile can kill a scheduled spawn during advance, before on_spawn dispatch.
|
||||
// Use an actual stationary projectile inside the arriving boss silhouette.
|
||||
let mut early_active = ActiveMods::build(&base, &discovery, &[OWNER.into()], None)?;
|
||||
let mut early_world = World::new(&pilot.mission, &pilot, &early_active.registry)?;
|
||||
early_world.step(
|
||||
Input::default(),
|
||||
&early_active.registry,
|
||||
&mut early_active.scripts,
|
||||
)?;
|
||||
early_world.projectiles.push(Projectile {
|
||||
entity: 9000,
|
||||
weapon: ContentId::from("base:projectile_forward_guns"),
|
||||
x: 160,
|
||||
y: -40,
|
||||
px: 160,
|
||||
py: -40,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
hostile: false,
|
||||
age: 0,
|
||||
legacy: None,
|
||||
legacy_enemy: None,
|
||||
});
|
||||
early_world.step(
|
||||
Input::default(),
|
||||
&early_active.registry,
|
||||
&mut early_active.scripts,
|
||||
)?;
|
||||
ensure!(
|
||||
early_world.enemies.len() == 1 && early_world.enemies[0].health <= 0,
|
||||
"lethal projectile must kill the boss in its spawn step"
|
||||
);
|
||||
early_world.step(
|
||||
Input::default(),
|
||||
&early_active.registry,
|
||||
&mut early_active.scripts,
|
||||
)?;
|
||||
ensure!(
|
||||
early_world.objectives.get(OBJECTIVE) == Some(&1),
|
||||
"queued spawn-step kill must increment its actual-death objective"
|
||||
);
|
||||
ensure!(
|
||||
early_active.scripts.states[STATE]["ages"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
// Reusing an entity ID in a new mission must count its new life, while another
|
||||
// death notification for that life must stay harmless.
|
||||
let rt = &mut early_active.scripts;
|
||||
invoke(rt, "on_spawn", event(1, 160, 112, 24))?;
|
||||
let mut reused = event(1, 0, 112, 24);
|
||||
reused["entities"] = json!([]);
|
||||
reused["objectives"] = json!({OBJECTIVE: 1});
|
||||
let reused_commands = invoke(rt, "on_death", reused.clone())?;
|
||||
ensure!(reused_commands.iter().any(|c| matches!(c,
|
||||
Command::Objective { key, value } if key == OBJECTIVE && *value == 2)));
|
||||
ensure!(invoke(rt, "on_death", reused)?.is_empty());
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"mod":OWNER,"package_validation":"passed; PNGs, sound decoding, manifest and script",
|
||||
"warning_ages":[42,96],"volley_ages":[54,108],"first_volley":first,"second_volley":second,
|
||||
"actual_engine_tick":world.tick,"death_objective":world.objectives[OBJECTIVE],
|
||||
"reward":world.player.credits-initial_credits,"outcome":format!("{:?}",world.outcome),
|
||||
"per_entity_state_cleanup":"passed","dead_or_repeated_callbacks":"no commands",
|
||||
"actual_cues":actual_cues,
|
||||
"queued_spawn_step_lethal_projectile":"passed",
|
||||
"reused_entity_id_new_life":"counts once"
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Saltbreaker
|
||||
|
||||
Standalone API 1 mod, version 1.0.0, with no dependencies. Saltworks Convoy requires this package and references `iron_coast_boss_3:unit` and the death objective `iron_coast_boss_3:defeated`.
|
||||
|
||||
The armored twin-fan escort enters vertically, then patrols between x=80 and x=144 at y=24. Its logical canvas remains 96×64 in SD and HD. It has 160 health and pays 1,800 credits when destroyed. Every 54 simulation steps after arrival, one nose cannon fires two three-damage shells. The cannons alternate and each warning sounds nine steps before firing. Shells move four pixels vertically, with a second shell angled one pixel toward the center. The boss never completes a mission or pays a separate scripted victory reward.
|
||||
|
||||
Every entity retains its own direction, clock and cannon side in serializable script state. Dead or absent entities cannot move or fire. A duplicate death callback cannot count twice. A new spawn resets its lifecycle even when a lethal collision precedes its queued callback, so restored state and reused entity IDs cannot suppress replay objectives. Mission-start objective reset and victory gating belong to the mission mod.
|
||||
|
||||
Generated master images, aligned SD/HD PNGs and synthesized mono 16-bit 22,050 Hz WAVs are included. `prompts/` contains the exact built-in image generation prompts, `provenance.json` records the sources, and `build_assets.py` reproduces derived artwork and audio. Use `--audio-only` to regenerate sounds without Pillow. `art-qa.json` records logical dimensions and clipping checks; `audio-recipe.json` records non-silent PCM peaks and hashes.
|
||||
|
||||
Validate content with `raptor-rust --data data --mods RustRaptor/mods --validate-mods iron_coast_boss_3` from the repository root. `validate.rs` is a package-local acceptance probe compiled against the RustRaptor library and serde_json. It checks cannon alternation, warning timing, independent entities, restoration, patrol bounds and death objectives. A real World regression kills the boss through collision before queued callbacks, restores script checkpoints and repeats three lifecycles with the same entity ID; each must count and pay exactly once without firing after death.
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"unit": {
|
||||
"logical_size": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"hd_size": [
|
||||
768,
|
||||
512
|
||||
],
|
||||
"hd_scale": 8,
|
||||
"sd_opaque_bounds": [
|
||||
6,
|
||||
4,
|
||||
90,
|
||||
58
|
||||
],
|
||||
"hd_opaque_bounds": [
|
||||
49,
|
||||
36,
|
||||
719,
|
||||
466
|
||||
],
|
||||
"source": "art/unit-generated-v1.png",
|
||||
"source_size": [
|
||||
1536,
|
||||
1024
|
||||
],
|
||||
"source_sha256": "6359953cbc845441f8e075488aaf0b9c0281f02938b895d43affdade668d8bef",
|
||||
"frames": 1,
|
||||
"origin": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"alpha": "Generated RGBA preserved; indexed threshold alpha>=128"
|
||||
},
|
||||
"projectile": {
|
||||
"logical_size": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"hd_size": [
|
||||
64,
|
||||
64
|
||||
],
|
||||
"hd_scale": 8,
|
||||
"sd_opaque_bounds": [
|
||||
3,
|
||||
2,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"hd_opaque_bounds": [
|
||||
23,
|
||||
11,
|
||||
41,
|
||||
53
|
||||
],
|
||||
"source": "art/projectile-generated-v1.png",
|
||||
"source_size": [
|
||||
1254,
|
||||
1254
|
||||
],
|
||||
"source_sha256": "6b8e12409793991fc374386acf8d5cb05fcdaad86257c1a24cbad1119f6e6df2",
|
||||
"frames": 1,
|
||||
"origin": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"alpha": "Generated RGBA preserved; indexed threshold alpha>=128"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 556 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"generator": "build_assets.py seeded chirp/noise synthesis; no recordings",
|
||||
"files": {
|
||||
"audio/shot.wav": {
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 4851,
|
||||
"seed": 303,
|
||||
"duration_seconds": 0.22,
|
||||
"peak": 25558,
|
||||
"sha256": "f3b0e9f304a6fa5beb25d6b2cf3378e35711af2825756aafd12d02fc4eace3c6"
|
||||
},
|
||||
"audio/warning.wav": {
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 3749,
|
||||
"seed": 304,
|
||||
"duration_seconds": 0.17002267573696145,
|
||||
"peak": 17694,
|
||||
"sha256": "906abb992bee7ea65e171d9d3e4e59bf6d75fdb7aa0df512347a3ac3025b645b"
|
||||
},
|
||||
"audio/death.wav": {
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 20948,
|
||||
"seed": 305,
|
||||
"duration_seconds": 0.9500226757369614,
|
||||
"peak": 25558,
|
||||
"sha256": "9762116be5e60f038a4f30c1f3e5d96e29088783d416473577c85981dc263a4e"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resize saved imagegen masters and reproduce seeded PCM effects.
|
||||
|
||||
Generated alpha is preserved. No image is painted or reconstructed here.
|
||||
Run with --audio-only to regenerate WAVs without Pillow.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import random
|
||||
import struct
|
||||
import wave
|
||||
|
||||
|
||||
def synthesize(root):
|
||||
rate = 22050
|
||||
reports = {}
|
||||
for name, duration, seed in [("shot", 0.22, 303), ("warning", 0.17, 304), ("death", 0.95, 305)]:
|
||||
rng = random.Random(seed)
|
||||
count = round(duration * rate)
|
||||
phase = smooth_noise = 0.0
|
||||
samples = []
|
||||
for index in range(count):
|
||||
t = index / rate
|
||||
u = index / max(1, count - 1)
|
||||
noise = rng.uniform(-1, 1)
|
||||
smooth_noise = smooth_noise * 0.88 + noise * 0.12
|
||||
if name == "shot":
|
||||
phase += 2 * math.pi * (95 + 520 * (1-u)**2) / rate
|
||||
value = 0.6 * math.sin(phase) + 0.24 * noise + 0.14 * math.sin(2.21 * phase)
|
||||
envelope = math.exp(-6*u)
|
||||
elif name == "warning":
|
||||
phase += 2 * math.pi * (640 + 740*u) / rate
|
||||
value = 0.6 * math.sin(phase) + 0.12 * math.sin(2*phase)
|
||||
envelope = math.sin(math.pi*u)**1.5 * (0.3 + 0.7 * (0.5 + 0.5*math.sin(2*math.pi*18*t)))
|
||||
else:
|
||||
phase += 2 * math.pi * (90 - 65*u) / rate
|
||||
value = 0.65 * smooth_noise + 0.17 * noise + 0.31 * math.sin(phase)
|
||||
envelope = math.exp(-4.9*u)
|
||||
envelope *= min(1, t/0.003) * min(1, (count-1-index)/(rate*0.005))
|
||||
samples.append(value * max(0, envelope))
|
||||
peak = max(abs(v) for v in samples)
|
||||
pcm = [round(v / peak * (0.54 if name == "warning" else 0.78) * 32767) for v in samples]
|
||||
path = root / "audio" / f"{name}.wav"
|
||||
with wave.open(str(path), "wb") as output:
|
||||
output.setnchannels(1)
|
||||
output.setsampwidth(2)
|
||||
output.setframerate(rate)
|
||||
output.writeframes(struct.pack("<" + "h" * count, *pcm))
|
||||
reports[str(path.relative_to(root))] = {
|
||||
"sample_rate": rate, "channels": 1, "bits": 16, "samples": count,
|
||||
"seed": seed, "duration_seconds": count/rate,
|
||||
"peak": max(abs(v) for v in pcm),
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
}
|
||||
(root / "audio-recipe.json").write_text(json.dumps({"generator": "build_assets.py seeded chirp/noise synthesis; no recordings", "files": reports}, indent=2) + "\n")
|
||||
|
||||
|
||||
def artwork(root):
|
||||
from PIL import Image, ImageOps
|
||||
reports = {}
|
||||
for name, logical, scale in [("unit", (96,64), 8), ("projectile", (8,8), 8)]:
|
||||
path = root / "art" / f"{name}-generated-v1.png"
|
||||
source = Image.open(path).convert("RGBA")
|
||||
if source.getchannel("A").getextrema()[0] != 0:
|
||||
raise ValueError(f"{path}: missing true transparent pixels")
|
||||
size = tuple(v*scale for v in logical)
|
||||
if name == "unit":
|
||||
hd = Image.new("RGBA", size, (0,0,0,0))
|
||||
fitted = ImageOps.contain(source, (size[0]-64, size[1]-42), Image.Resampling.LANCZOS)
|
||||
hd.alpha_composite(fitted, ((size[0]-fitted.width)//2, (size[1]-fitted.height)//2))
|
||||
else:
|
||||
hd = source.resize(size, Image.Resampling.LANCZOS)
|
||||
sd = hd.resize(logical, Image.Resampling.BOX)
|
||||
bounds = []
|
||||
for image in (sd,hd):
|
||||
bbox = image.getchannel("A").point(lambda v:255 if v>=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"{name}: silhouette clips or is missing: {bbox}")
|
||||
bounds.append(list(bbox))
|
||||
sd.save(root / "art" / f"{name}-sd.png")
|
||||
hd.save(root / "art" / f"{name}-hd.png")
|
||||
reports[name] = {"logical_size": list(logical), "hd_size": list(size), "hd_scale": scale,
|
||||
"sd_opaque_bounds": bounds[0], "hd_opaque_bounds": bounds[1],
|
||||
"source": str(path.relative_to(root)), "source_size": list(source.size),
|
||||
"source_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
"frames": 1, "origin": [0,0], "alpha": "Generated RGBA preserved; indexed threshold alpha>=128"}
|
||||
(root / "art-qa.json").write_text(json.dumps(reports, 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
|
||||
synthesize(root)
|
||||
if not args.audio_only:
|
||||
artwork(root)
|
||||
print("Saltbreaker assets validated and generated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"weapons": [{
|
||||
"id": "iron_coast_boss_3:projectile", "name": "Saltbreaker incendiary 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_3:unit", "name": "Saltbreaker armored escort VTOL",
|
||||
"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": 1 },
|
||||
"script": "scripts/ai.rhai"
|
||||
}],
|
||||
"sounds": [
|
||||
{ "id": "iron_coast_boss_3:shot", "name": "Saltbreaker cannon report", "file": "audio/shot.wav", "volume": 95, "pan": 127, "pitch": 128, "priority": 2 },
|
||||
{ "id": "iron_coast_boss_3:warning", "name": "Saltbreaker cannon charging warning", "file": "audio/warning.wav", "volume": 83, "pan": 127, "pitch": 128, "priority": 2 },
|
||||
{ "id": "iron_coast_boss_3:death", "name": "Saltbreaker turbine destruction", "file": "audio/death.wav", "volume": 120, "pan": 127, "pitch": 128, "priority": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "iron_coast_boss_3"
|
||||
name = "Iron Coast Saltbreaker"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
description = "An armored convoy-escort VTOL boss with alternating, audible side-cannon warnings, SD and HD artwork, and deterministic serializable AI."
|
||||
content = "content.json"
|
||||
scripts = ["scripts/ai.rhai"]
|
||||
@@ -0,0 +1 @@
|
||||
Use case: stylized-concept. Asset type: a single transparent production game projectile sprite for an industrial top-down vertical shoot-em-up. Exactly one small Saltbreaker boss cannon shell, straight top-down overhead orthographic view. Compact oval gunmetal projectile with a narrow amber incendiary slit and tiny red bands, nose pointing DOWN; no fins. Isolated centered on a square transparent canvas, occupies the central 60% height and 40% width. Crisp outlined high-definition painted sprite compatible with downsampling to 8x8 pixels; thick visible larger detail, restrained charcoal silver amber military palette. True transparent background and generous clear margins. No text, no watermark, no glow aura, no cast shadow, no explosion, no trail, no smoke, no surrounding scene. Exactly one projectile.
|
||||
@@ -0,0 +1 @@
|
||||
Use case: stylized-concept. Asset type: production transparent HD game sprite for a top-down 1990s Raptor-inspired vertical shoot-em-up. Use the reference only for the industrial gray metal, olive plates, restrained rust-red accents and crisp painted/pixel-compatible detail; do not copy its silhouette or multiple frames. Generate ONE distinct enemy boss called Saltbreaker: a heavy armored convoy-escort VTOL craft with a broad low-profile fuselage, two large circular ducted lift fans set into the short side wings, blunt armored nose pointing straight DOWN, and a cannon pod on each side of the nose. Canvas landscape 3:2, aircraft occupies 86% width and 80% height, completely visible and centered. Exact overhead orthographic view, bilateral symmetric static unbanked sprite, no perspective. Gunmetal and olive military industrial armor, small red warning strips, warm amber small windows, dark fan rotors, metallic rim highlights; no blue neon. Crisp clean edges and purposeful larger details remain readable when downsampled to 96x64 logical pixels. True transparent background, transparent margin all sides, no cast ground shadow, no grid, no surface, no smoke, no words, no labels, no border, no glowing aura, no additional objects. Exactly one craft, no sheet.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"asset_authoring": "Built-in image_gen tool, transparent_background=true; no CLI fallback",
|
||||
"original_game_art": "No original game assets copied into this package",
|
||||
"artworks": [
|
||||
{"master": "art/unit-generated-v1.png", "prompt": "prompts/unit-hd-v1.txt", "reference": "../iron_coast_boss/art/unit-hd.png", "reference_role": "Industrial materials and palette only; independently generated twin-fan silhouette", "output": ["art/unit-hd.png", "art/unit-sd.png"]},
|
||||
{"master": "art/projectile-generated-v1.png", "prompt": "prompts/projectile-hd-v1.txt", "reference": null, "output": ["art/projectile-hd.png", "art/projectile-sd.png"]}
|
||||
],
|
||||
"derivation": "build_assets.py applies only aspect-preserving fit, transparent padding and SD/HD resampling; original generated alpha is retained",
|
||||
"sounds": "Original deterministic procedural chirps and seeded noise; build_assets.py --audio-only; no recordings or external audio assets",
|
||||
"validation": ["art-qa.json logical and HD bounds", "audio-recipe.json PCM peaks and hashes", "validate.rs callback acceptance probe"]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Each boss has its own direction, cannon clock and alternating barrel.
|
||||
// No wall-clock time or global tick modulo affects restored behavior.
|
||||
fn is_alive(event) {
|
||||
for unit in event.entities {
|
||||
if unit.entity == event.entity && unit.health > 0 { return true; }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn on_spawn(event, state, random) {
|
||||
if !state.contains("units") { state.units = #{}; }
|
||||
if !state.contains("dead") { state.dead = #{}; }
|
||||
let key = event.entity.to_string();
|
||||
state.units[key] = #{direction: 1, clock: 0, side: 0};
|
||||
state.dead.remove(key);
|
||||
// IDs restart on replay. Even a spawn killed during advance must clear
|
||||
// its previous life's death guard before the queued death callback runs.
|
||||
if !is_alive(event) { return #{commands: [], state: state}; }
|
||||
#{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("units") { state.units = #{}; }
|
||||
let unit = if state.units.contains(key) { state.units[key] } else {#{direction: 1, clock: 0, side: 0}};
|
||||
if event.x >= 144 { unit.direction = -1; }
|
||||
if event.x <= 80 { unit.direction = 1; }
|
||||
unit.clock = (unit.clock + 1) % 54;
|
||||
let commands = [
|
||||
#{op: "move", entity: event.entity, x: event.x, y: 24},
|
||||
#{op: "velocity", entity: event.entity, vx: unit.direction, vy: 0}
|
||||
];
|
||||
// Audible warning lasts nine simulation steps before each side fires.
|
||||
if unit.clock == 45 {
|
||||
commands.push(#{op: "play_sound", sound: "iron_coast_boss_3:warning"});
|
||||
}
|
||||
if unit.clock == 0 {
|
||||
let muzzle = if unit.side == 0 {35} else {61};
|
||||
let inward = if unit.side == 0 {1} else {-1};
|
||||
commands.push(#{op: "projectile", weapon: "iron_coast_boss_3:projectile", x: event.x + muzzle, y: event.y + 54, vx: 0, vy: 4, hostile: true});
|
||||
commands.push(#{op: "projectile", weapon: "iron_coast_boss_3:projectile", x: event.x + muzzle, y: event.y + 54, vx: inward, vy: 4, hostile: true});
|
||||
commands.push(#{op: "play_sound", sound: "iron_coast_boss_3:shot"});
|
||||
unit.side = 1 - unit.side;
|
||||
state.shots = if state.contains("shots") {state.shots + 1} else {1};
|
||||
}
|
||||
state.units[key] = unit;
|
||||
#{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) {
|
||||
let key = event.entity.to_string();
|
||||
if !state.contains("dead") { state.dead = #{}; }
|
||||
if state.dead.contains(key) { return #{commands: [], state: state}; }
|
||||
state.dead[key] = true;
|
||||
if state.contains("units") { state.units.remove(key); }
|
||||
state.destroyed = if state.contains("destroyed") {state.destroyed + 1} else {1};
|
||||
let defeated = if event.objectives.contains("iron_coast_boss_3:defeated") {event.objectives["iron_coast_boss_3:defeated"] + 1} else {1};
|
||||
#{commands: [
|
||||
#{op: "objective", key: "iron_coast_boss_3:defeated", value: defeated},
|
||||
#{op: "effect", x: event.x + 35, y: event.y + 48},
|
||||
#{op: "effect", x: event.x + 61, y: event.y + 48},
|
||||
#{op: "play_sound", sound: "iron_coast_boss_3:death"}
|
||||
], state: state}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//! Package-local callback acceptance probe; compile against raptor_replica and serde_json.
|
||||
use raptor_replica::{
|
||||
content::{ContentPack, Registry},
|
||||
modding::{self, ActiveMods, Command, ScriptRuntime},
|
||||
persistence::Pilot,
|
||||
simulation::{Input, World},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::Path;
|
||||
|
||||
const OWNER: &str = "iron_coast_boss_3";
|
||||
const SCRIPT: &str = "scripts/ai.rhai";
|
||||
|
||||
fn runtime(path: &Path) -> ScriptRuntime {
|
||||
let mut result = ScriptRuntime::new();
|
||||
result
|
||||
.add(
|
||||
OWNER,
|
||||
SCRIPT,
|
||||
&std::fs::read_to_string(path.join(SCRIPT)).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
result
|
||||
}
|
||||
|
||||
fn event(entity: u64, x: i32, y: i32, health: i32) -> Value {
|
||||
json!({"entity":entity, "x":x, "y":y, "health":health, "tick":999,
|
||||
"entities":[{"entity":entity,"health":health}],"objectives":{}})
|
||||
}
|
||||
|
||||
fn call(runtime: &mut ScriptRuntime, hook: &str, event: Value) -> Vec<Command> {
|
||||
runtime.invoke(OWNER, SCRIPT, hook, &event, 303).unwrap()
|
||||
}
|
||||
|
||||
fn queued_spawn_replay(package: &Path) {
|
||||
let mut base = Registry::default();
|
||||
let content: 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"}],
|
||||
"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":"Spawn lifecycle collision regression","duration":500,"auto_complete":false,
|
||||
"spawns":[{"tick":0,"enemy":"iron_coast_boss_3:unit","x":112,"y":120}]}]
|
||||
})).unwrap();
|
||||
base.apply(content, "base").unwrap();
|
||||
let discovery = modding::discover(package.parent().unwrap());
|
||||
let mut active = ActiveMods::build(&base, &discovery, &[OWNER.to_string()], None).unwrap();
|
||||
// A real collision during advance kills this fragile boss before dispatch.
|
||||
// Its on_spawn callback is queued before on_death, and both see no live boss.
|
||||
active
|
||||
.registry
|
||||
.enemies
|
||||
.get_mut(&"iron_coast_boss_3:unit".into())
|
||||
.unwrap()
|
||||
.health = 1;
|
||||
let mut pilot = Pilot::default();
|
||||
pilot.difficulty = 2;
|
||||
pilot.credits = 0;
|
||||
let mut reused_id = None;
|
||||
for replay in 0..3 {
|
||||
let mut world = World::new(&pilot.mission, &pilot, &active.registry).unwrap();
|
||||
world
|
||||
.step(
|
||||
Input {
|
||||
absolute: Some([144, 160]),
|
||||
..Input::default()
|
||||
},
|
||||
&active.registry,
|
||||
&mut active.scripts,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
world.enemies.is_empty(),
|
||||
"collision must remove the boss before dispatch"
|
||||
);
|
||||
assert_eq!(
|
||||
world.objectives.get("iron_coast_boss_3:defeated"),
|
||||
Some(&1),
|
||||
"replay {replay} must count its new lifecycle"
|
||||
);
|
||||
assert_eq!(
|
||||
world.player.credits, 1800,
|
||||
"exactly one engine reward per lifecycle"
|
||||
);
|
||||
let checkpoints = active.scripts.checkpoints();
|
||||
let state = &checkpoints.0["iron_coast_boss_3/scripts/ai.rhai"];
|
||||
let ids = state["dead"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(ids.len(), 1);
|
||||
if let Some(ref previous) = reused_id {
|
||||
assert_eq!(previous, ids[0]);
|
||||
} else {
|
||||
reused_id = Some(ids[0].clone());
|
||||
}
|
||||
assert!(state["units"].as_object().unwrap().is_empty());
|
||||
assert!(world.projectiles.is_empty(), "dead spawn must not shoot");
|
||||
assert_eq!(world.mod_sounds, vec!["iron_coast_boss_3:death".into()]);
|
||||
world
|
||||
.step(Input::default(), &active.registry, &mut active.scripts)
|
||||
.unwrap();
|
||||
assert_eq!(world.player.credits, 1800);
|
||||
assert_eq!(world.objectives.get("iron_coast_boss_3:defeated"), Some(&1));
|
||||
// Reconstruct the runtime from saved state exactly as a pilot load does.
|
||||
let mut restored = runtime(package);
|
||||
restored.restore(checkpoints);
|
||||
active.scripts = restored;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("pass package directory");
|
||||
let path = Path::new(&path);
|
||||
let mut ai = runtime(path);
|
||||
call(&mut ai, "on_spawn", event(1, 112, -64, 160));
|
||||
for _ in 0..20 {
|
||||
let commands = call(&mut ai, "on_tick", event(1, 112, -64, 160));
|
||||
assert!(
|
||||
!commands
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Projectile { .. } | Command::PlaySound { .. }))
|
||||
);
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
let mut volleys = Vec::new();
|
||||
for tick in 1..=108 {
|
||||
let commands = call(&mut ai, "on_tick", event(1, 112, 24, 160));
|
||||
if commands
|
||||
.iter()
|
||||
.any(|c| matches!(c,Command::PlaySound{sound} if sound=="iron_coast_boss_3:warning"))
|
||||
{
|
||||
warnings.push(tick);
|
||||
}
|
||||
let shells = commands
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
Command::Projectile {
|
||||
weapon,
|
||||
x,
|
||||
y,
|
||||
vx,
|
||||
vy,
|
||||
hostile,
|
||||
} => {
|
||||
assert_eq!(weapon, "iron_coast_boss_3:projectile");
|
||||
assert_eq!((*y, *vy, *hostile), (78, 4, true));
|
||||
Some((*x, *vx))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !shells.is_empty() {
|
||||
volleys.push((tick, shells));
|
||||
}
|
||||
}
|
||||
assert_eq!(warnings, vec![45, 99]);
|
||||
assert_eq!(
|
||||
volleys,
|
||||
vec![
|
||||
(54, vec![(147, 0), (147, 1)]),
|
||||
(108, vec![(173, 0), (173, -1)])
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
call(&mut ai, "on_tick", event(1, 144, 24, 160))
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Velocity { vx: -1, vy: 0, .. }))
|
||||
);
|
||||
assert!(
|
||||
call(&mut ai, "on_tick", event(1, 80, 24, 160))
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Velocity { vx: 1, vy: 0, .. }))
|
||||
);
|
||||
// A second boss starts a separate clock and cannot inherit the first gun phase.
|
||||
call(&mut ai, "on_spawn", event(2, 112, 24, 160));
|
||||
assert!(
|
||||
!call(&mut ai, "on_tick", event(2, 112, 24, 160))
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Projectile { .. }))
|
||||
);
|
||||
let mut restored = runtime(path);
|
||||
restored.restore(ai.checkpoints());
|
||||
for _ in 0..111 {
|
||||
assert_eq!(
|
||||
serde_json::to_value(call(&mut ai, "on_tick", event(1, 112, 24, 160))).unwrap(),
|
||||
serde_json::to_value(call(&mut restored, "on_tick", event(1, 112, 24, 160))).unwrap()
|
||||
);
|
||||
}
|
||||
// A dead or absent entity cannot move or fire. Death reports once and never wins a mission itself.
|
||||
assert!(call(&mut ai, "on_tick", event(1, 112, 24, 0)).is_empty());
|
||||
let mut missing = event(1, 112, 24, 160);
|
||||
missing["entities"] = json!([]);
|
||||
assert!(call(&mut ai, "on_tick", missing).is_empty());
|
||||
let death = call(&mut ai, "on_death", event(1, 112, 24, 0));
|
||||
assert!(
|
||||
death.iter().any(
|
||||
|c| matches!(c,Command::Objective{key,value:1} if key=="iron_coast_boss_3:defeated")
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
!death
|
||||
.iter()
|
||||
.any(|c| matches!(c, Command::Complete | Command::Credits { .. }))
|
||||
);
|
||||
assert!(call(&mut ai, "on_death", event(1, 112, 24, 0)).is_empty());
|
||||
let mut second = event(2, 112, 24, 0);
|
||||
second["objectives"] = json!({"iron_coast_boss_3:defeated":1});
|
||||
assert!(
|
||||
call(&mut ai, "on_death", second).iter().any(
|
||||
|c| matches!(c,Command::Objective{key,value:2} if key=="iron_coast_boss_3:defeated")
|
||||
)
|
||||
);
|
||||
queued_spawn_replay(path);
|
||||
println!(
|
||||
"Saltbreaker: arrival/patrol, two-shell alternating volleys, nine-step warnings, independent entities, checkpoint replay, dead-unit safety and exact death objectives passed"
|
||||
);
|
||||
println!(
|
||||
"Real World lethal collision before queued spawn: three restored-state replays reuse the same entity ID and each count and reward exactly once"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Stormwall Arsenal
|
||||
|
||||
An independent Iron Coast boss mod for mission 4. The wide aerial arsenal carries two massive downward-facing batteries, red storm-warning stripes and four rear engines. It has its own true transparent SD/HD boss and shell artwork, alarm, cannon and destruction sounds.
|
||||
|
||||
Enable `iron_coast_boss_4` to register the boss; the Stormwall mission selects it as a dependency. This package has no dependencies, overrides or mission definitions.
|
||||
|
||||
| Definition | ID |
|
||||
| --- | --- |
|
||||
| Enemy | `iron_coast_boss_4:unit` |
|
||||
| Projectile | `iron_coast_boss_4:projectile` |
|
||||
| Death objective | `iron_coast_boss_4:defeated` |
|
||||
| Sounds | `iron_coast_boss_4:warning`, `iron_coast_boss_4:shot`, `iron_coast_boss_4:death` |
|
||||
|
||||
The 96×64 boss enters at two logical pixels per simulation tick, holds y=24 and patrols x=80–144. Every 54 ticks it pauses for 12 ticks with a two-note arming alarm, then fires three 3-damage shells at speed 4. The third shell alternates between batteries. Each boss stores its own direction and attack age, so concurrent bosses remain independent. Ordinary difficulty scaling applies to its 160 health; destruction awards 1,800 credits.
|
||||
|
||||
Actual death increments `iron_coast_boss_4:defeated` once and retires that entity's live state. A dead boss never keeps firing, and a fresh sortie safely resets reused entity IDs even when a lethal projectile hits before its queued spawn callback runs. The script does not declare mission success: the mission combines the death objective with its other targets.
|
||||
|
||||
`art/unit-sd.png` and `art/unit-hd.png` share a 96×64 logical canvas (HD is 384×256). `art/projectile-sd.png` and `art/projectile-hd.png` share an 8×8 logical canvas (HD is 64×64). Both use one static frame, consistent registration and preserved alpha. `art/preview.png` shows both modes. The first shell master is retained for provenance but is not loaded; v2 gives a readable SD core.
|
||||
|
||||
The built-in image generation tool supplied the artwork. Exact prompts are in `prompts/`, tool results and processing details in `provenance.json`, and image dimensions, bounds and hashes in `art-qa.json`. The package includes generated masters, runtime art and WAVs for Git tracking. Original catalog reference pixels are excluded.
|
||||
|
||||
`python3 build_assets.py` reproduces the SD/HD packaging and deterministic 22,050 Hz mono PCM16 sounds from saved masters. `audio-recipe.json` records seeds, nonzero peaks/RMS and hashes; no recordings or external sound downloads are used.
|
||||
|
||||
After building RustRaptor in release mode, run `python3 validate.py` for six tests through the real simulation and Rhai callbacks: warning timing and alternate batteries, independent multi-boss state, once-only destruction, saved state/RNG determinism, fresh sorties, lethal spawn-step hits with restored death state, duplicate death callbacks and persistent patrol bounds. Results are recorded in `acceptance-report.json`. The normal Rust mod validation CLI also validates this package independently.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"validation": "Package-local actual Rust simulation and Rhai, release library; python3 validate.py",
|
||||
"tests": {
|
||||
"warning_precedes_three_shells_and_batteries_alternate": "passed",
|
||||
"two_bosses_have_independent_state_and_death_counts_once": "passed",
|
||||
"saved_script_state_and_rng_reproduce_identical_future": "passed",
|
||||
"a_new_sortie_clears_reused_entity_death_guards": "passed",
|
||||
"live_boss_enters_and_persists_in_safe_bounds_without_auto_completion": "passed",
|
||||
"restored_dead_guard_does_not_hide_a_lethal_hit_on_the_reused_spawn_step": "passed"
|
||||
},
|
||||
"independent_cli": "Validated 1 mods; 0 overrides",
|
||||
"test_difficulty": 3,
|
||||
"full_definition_health": 160,
|
||||
"original_difficulty_scaling": "Boss health is halved by the engine on difficulties 0 and 1, as for existing bosses.",
|
||||
"sound_peak": 25558,
|
||||
"sounds_non_silent": true,
|
||||
"art_preview": "art/preview.png",
|
||||
"sd_boss_opaque_bounds": [
|
||||
3,
|
||||
6,
|
||||
93,
|
||||
54
|
||||
],
|
||||
"sd_shell_opaque_bounds": [
|
||||
3,
|
||||
2,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"safe_patrol_bounds": [
|
||||
80,
|
||||
145
|
||||
],
|
||||
"persistent_y": 24,
|
||||
"death_objective": "iron_coast_boss_4:defeated",
|
||||
"mission_completion": "Never completes a mission itself",
|
||||
"queued_spawn_regression": "Reproduced against old callback: missing death objective when restored dead flag survives a lethal spawn-step hit. Fixed lifecycle reset before survival guard; no dead commands; once-only objective/reward; duplicate callback safely ignored.",
|
||||
"script_sha256": "f3cc133f6cf858d7fa4e3a6c7e424ad81aa5a66abfb64407572622314fc58f00"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"unit": {
|
||||
"sd": {
|
||||
"dimensions": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"opaque_bounds": [
|
||||
3,
|
||||
6,
|
||||
93,
|
||||
54
|
||||
],
|
||||
"sha256": "2188403b5d39f8e8458514cf6c77c1089cd0eced658f9359747a637a62931c5d"
|
||||
},
|
||||
"hd": {
|
||||
"dimensions": [
|
||||
384,
|
||||
256
|
||||
],
|
||||
"opaque_bounds": [
|
||||
12,
|
||||
25,
|
||||
372,
|
||||
218
|
||||
],
|
||||
"sha256": "293b7f87531c1c23433cf53e539a8ab4d710803c0bd171af65680982b89af47d"
|
||||
},
|
||||
"source_sha256": "0d812437a71b65c34c467f892d918e70903c9f407ad2e6dc5e5d08d9b32828ec",
|
||||
"logical_dimensions": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"frames": 1
|
||||
},
|
||||
"projectile": {
|
||||
"sd": {
|
||||
"dimensions": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"opaque_bounds": [
|
||||
3,
|
||||
2,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"sha256": "bcfc46259ff65192b72013abcb182d965f7fb242b9cd796ad4e1940665ebed33"
|
||||
},
|
||||
"hd": {
|
||||
"dimensions": [
|
||||
64,
|
||||
64
|
||||
],
|
||||
"opaque_bounds": [
|
||||
24,
|
||||
12,
|
||||
40,
|
||||
52
|
||||
],
|
||||
"sha256": "3d7b6a7ffd9fcbc4d86433621574a74a773945ead6d7ed1038cfa3185fc4b0cb"
|
||||
},
|
||||
"source_sha256": "1f12188394c367ce531f79bf158140ac9a866b4ebb29131bd5f2b5009fa25a16",
|
||||
"logical_dimensions": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"frames": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 169 B |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"generator": "build_assets.py deterministic PCM synthesis; no recordings",
|
||||
"files": {
|
||||
"warning": {
|
||||
"seed": 404,
|
||||
"duration": 0.38,
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 8379,
|
||||
"peak": 25558,
|
||||
"rms": 8927.033991755772,
|
||||
"sha256": "8deebf980e3fda65ddc3a1fc905a7616368b25fdd6e9e0ad9a960bc14b43ce37"
|
||||
},
|
||||
"shot": {
|
||||
"seed": 405,
|
||||
"duration": 0.23,
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 5072,
|
||||
"peak": 25558,
|
||||
"rms": 3886.3418602855677,
|
||||
"sha256": "52494d9278450c77737c91eb75afaaba74a1f015d3a1d80a07b644c2dad200b8"
|
||||
},
|
||||
"death": {
|
||||
"seed": 406,
|
||||
"duration": 1.08,
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"samples": 23814,
|
||||
"peak": 25558,
|
||||
"rms": 3286.543804581382,
|
||||
"sha256": "8ac2c2846d14b185280be6d1a90835529b97342e9f305877ed11a24e558782fc"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preserve generated RGBA masters, derive SD/HD sprites and synthesize local WAVs.
|
||||
|
||||
This is an asset packaging pipeline, not an image redraw. Inputs are the saved
|
||||
built-in imagegen results documented in prompts/ and provenance.json.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import random
|
||||
import struct
|
||||
import wave
|
||||
from PIL import Image, ImageDraw, ImageOps
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def artwork():
|
||||
report = {}
|
||||
for kind, logical, scale in [("unit", (96, 64), 4), ("projectile", (8, 8), 8)]:
|
||||
source_file = f"{kind}-generated-v{2 if kind == 'projectile' else 1}.png"
|
||||
source = Image.open(ROOT / "art" / source_file).convert("RGBA")
|
||||
size = tuple(v * scale for v in logical)
|
||||
# Unit master already has matching 3:2 framing. The shell uses contain
|
||||
# with one logical pixel of padding, retaining its portrait aspect.
|
||||
if kind == "unit":
|
||||
hd = source.resize(size, Image.Resampling.LANCZOS)
|
||||
else:
|
||||
inset = ImageOps.contain(source, (size[0] - 2 * scale, size[1] - 2 * scale), Image.Resampling.LANCZOS)
|
||||
hd = Image.new("RGBA", size)
|
||||
hd.alpha_composite(inset, ((size[0] - inset.width) // 2, (size[1] - inset.height) // 2))
|
||||
sd = hd.resize(logical, Image.Resampling.BOX)
|
||||
entries = {}
|
||||
for mode, image in [("sd", sd), ("hd", hd)]:
|
||||
mask = image.getchannel("A").point(lambda value: 255 if value >= 128 else 0)
|
||||
bounds = mask.getbbox()
|
||||
assert bounds is not None, f"{kind}/{mode} is invisible"
|
||||
assert all([bounds[0] > 0, bounds[1] > 0, bounds[2] < image.width, bounds[3] < image.height]), f"{kind}/{mode} clips"
|
||||
path = ROOT / "art" / f"{kind}-{mode}.png"
|
||||
image.save(path)
|
||||
entries[mode] = {"dimensions": list(image.size), "opaque_bounds": list(bounds), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
||||
entries["source_sha256"] = hashlib.sha256((ROOT / "art" / source_file).read_bytes()).hexdigest()
|
||||
entries["logical_dimensions"] = list(logical)
|
||||
entries["frames"] = 1
|
||||
report[kind] = entries
|
||||
preview = Image.new("RGB", (768, 560), (30, 44, 53))
|
||||
draw = ImageDraw.Draw(preview)
|
||||
draw.text((16, 12), "Stormwall Arsenal / SD nearest scale (top), HD (bottom)", fill="white")
|
||||
for kind, sd_box, hd_box in [("unit", (16, 50), (16, 238)), ("projectile", (640, 70), (640, 320))]:
|
||||
sd = Image.open(ROOT / "art" / f"{kind}-sd.png").convert("RGBA")
|
||||
hd = Image.open(ROOT / "art" / f"{kind}-hd.png").convert("RGBA")
|
||||
sd = sd.resize(tuple(v * (2 if kind == "unit" else 8) for v in sd.size), Image.Resampling.NEAREST)
|
||||
preview.paste(sd, sd_box, sd)
|
||||
preview.paste(hd, hd_box, hd)
|
||||
preview.save(ROOT / "art/preview.png")
|
||||
(ROOT / "art-qa.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
|
||||
|
||||
def sounds():
|
||||
rate = 22050
|
||||
report = {}
|
||||
for name, seconds, seed in [("warning", 0.38, 404), ("shot", 0.23, 405), ("death", 1.08, 406)]:
|
||||
rng = random.Random(seed)
|
||||
count = round(seconds * rate)
|
||||
phase = low_noise = 0.0
|
||||
pcm = []
|
||||
for index in range(count):
|
||||
t = index / rate
|
||||
p = index / max(1, count - 1)
|
||||
noise = rng.uniform(-1, 1)
|
||||
low_noise = 0.85 * low_noise + 0.15 * noise
|
||||
if name == "warning":
|
||||
# Two distinct short rising notes; finishes before the salvo.
|
||||
note_t = t % 0.19
|
||||
phase += 2 * math.pi * (640 if t < 0.19 else 900) / rate
|
||||
value = math.sin(phase) * 0.66 + math.sin(2 * phase) * 0.12
|
||||
envelope = min(1.0, note_t / 0.006) * max(0.0, 1.0 - note_t / 0.15)
|
||||
elif name == "shot":
|
||||
phase += 2 * math.pi * (70 + 480 * (1 - p) ** 3) / rate
|
||||
value = 0.62 * math.sin(phase) + 0.22 * noise + 0.12 * math.sin(2.2 * phase)
|
||||
envelope = min(1, t / 0.002) * math.exp(-6 * p)
|
||||
else:
|
||||
phase += 2 * math.pi * (56 - 30 * p) / rate
|
||||
value = 0.62 * low_noise + 0.24 * noise + 0.35 * math.sin(phase)
|
||||
envelope = min(1, t / 0.003) * math.exp(-4 * p)
|
||||
envelope *= min(1, (count - 1 - index) / (rate * 0.005))
|
||||
pcm.append(value * max(0, envelope))
|
||||
peak = max(map(abs, pcm))
|
||||
pcm = [round(value / peak * 0.78 * 32767) for value in pcm]
|
||||
path = ROOT / "audio" / f"{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))
|
||||
report[name] = {"seed": seed, "duration": seconds, "sample_rate": rate, "channels": 1, "bits": 16,
|
||||
"samples": count, "peak": max(map(abs, pcm)), "rms": math.sqrt(sum(v*v for v in pcm) / count),
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
||||
(ROOT / "audio-recipe.json").write_text(json.dumps({"generator": "build_assets.py deterministic PCM synthesis; no recordings", "files": report}, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
artwork()
|
||||
sounds()
|
||||
print("Packaged Stormwall Arsenal SD/HD sprites and three non-silent WAVs")
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"weapons": [{
|
||||
"id": "iron_coast_boss_4:projectile",
|
||||
"name": "Stormwall Arsenal battery 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_4:unit",
|
||||
"name": "Stormwall Arsenal",
|
||||
"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
|
||||
},
|
||||
"script": "scripts/ai.rhai"
|
||||
}],
|
||||
"sounds": [{
|
||||
"id": "iron_coast_boss_4:warning",
|
||||
"name": "Stormwall battery arming warning",
|
||||
"file": "audio/warning.wav",
|
||||
"volume": 80, "pan": 127, "pitch": 128, "priority": 2
|
||||
}, {
|
||||
"id": "iron_coast_boss_4:shot",
|
||||
"name": "Stormwall battery discharge",
|
||||
"file": "audio/shot.wav",
|
||||
"volume": 95, "pan": 127, "pitch": 128, "priority": 2
|
||||
}, {
|
||||
"id": "iron_coast_boss_4:death",
|
||||
"name": "Stormwall Arsenal destruction",
|
||||
"file": "audio/death.wav",
|
||||
"volume": 120, "pan": 127, "pitch": 128, "priority": 4
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "iron_coast_boss_4"
|
||||
name = "Iron Coast: Stormwall Arsenal"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
description = "A storm-hardened twin-turret arsenal with alternating telegraphed battery fire. Includes independent SD/HD artwork and synthesized sounds."
|
||||
content = "content.json"
|
||||
scripts = ["scripts/ai.rhai"]
|
||||
@@ -0,0 +1,5 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: single transparent HD cannon-shell sprite for a vertical shooter, viewed directly from above.
|
||||
Primary request: one small heavy Stormwall Arsenal shell, steel/silver nose pointing straight DOWN, dark red short armored cylindrical casing, warm amber tail ignition at TOP. It belongs to an industrial gunmetal/red gun platform.
|
||||
Composition: strict orthographic top-down, centered single shell on a square canvas, full silhouette and generous transparent margin; approximately twice as tall as wide; occupies center 60% of height. No ground shadow, no background, no text, no border, no particles or large glow outside silhouette.
|
||||
Style: crisp classic Raptor mechanical game sprite remastered with clean HD metal shading, restrained specular edges, clear at 8x8 logical pixels. One sprite only, genuine RGBA transparent background.
|
||||
@@ -0,0 +1,3 @@
|
||||
Use case: precise-object-edit
|
||||
Asset type: transparent top-down vertical-shooter projectile sprite, one single frame.
|
||||
Edit this shell into a short, chunky heavy cannon round. Keep its red metal casing, silver nose pointing DOWN, small amber exhaust at TOP and industrial material treatment. Change only the proportions and framing: halve its current length and double its width, so the visible round is roughly 1.7 times as tall as wide. Make the object fill the central 70% height of a SQUARE transparent canvas, centered with matching margins. Strict orthographic top view, no perspective. Simple large shapes remain readable at only 8x8 logical pixels. Genuine RGBA transparency; no shadow, no haze or broad glow outside the small exhaust, no text, no extra objects.
|
||||
@@ -0,0 +1,7 @@
|
||||
Use case: stylized-concept
|
||||
Asset type: production HD enemy boss sprite for a top-down vertical shooter, one static frame.
|
||||
Input images: the original Raptor boss source is a style/orientation reference only; the other image is another Iron Coast boss, use its mechanical material treatment but do not copy its narrow silhouette.
|
||||
Primary request: create the distinct "Stormwall Arsenal", a broad storm-hardened aerial gun platform with two massive round cannon turrets facing DOWN toward the bottom of the image. It must read as a unique industrial arsenal flying fortress, not a conventional aircraft.
|
||||
Composition: strict orthographic top view, nose and paired cannon barrels point to screen bottom; body aspect roughly 3:2, centered on a landscape 3:2 full transparent canvas. Wide horizontal armored wings/platform, substantial central armored reactor spine, two large round turrets clearly separated on left/right, four small rear engine pods along the top. No perspective tilt. Silhouette entirely visible with transparent margin on all sides.
|
||||
Style: faithful classic Raptor industrial gunmetal mechanical shapes remastered with clean HD material definition; dark gray steel plates, restrained worn warm bronze seams, deep red warning stripes and small amber warning lamps. Crisp readable contrast at 96x64 logical pixels. Handcrafted game sprite, controlled small highlights, soft short shadows within the object only, no photorealistic scene.
|
||||
Constraints: one single isolated sprite, true RGBA transparency, no terrain or ground shadow, no text or numbers, no border, no decorations outside silhouette, no exploded diagram, no glow spilling outside silhouette. Exactly two principal gun turrets. Cannons extend downward; symmetrical silhouette, slight asymmetric wear permitted. Do not copy the prior command cruiser silhouette.
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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",
|
||||
"references": [
|
||||
{
|
||||
"asset": "sprites/SHIP10G1_PIC",
|
||||
"asset_id_hex": "10306",
|
||||
"role": "Inspected classic source for top-down orientation and industrial material language only; not copied into new art or tracked in Git",
|
||||
"review_image": "data/hd/bosses/ship10g1-source.png"
|
||||
},
|
||||
{
|
||||
"asset": "../iron_coast_boss/art/preview.png",
|
||||
"role": "Iron Coast style reference, new boss uses a distinctly wider twin-turret silhouette"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"file": "art/unit-generated-v1.png",
|
||||
"prompt": "prompts/unit-hd-v1.txt",
|
||||
"tool_output": "exec-b2e6eb12-8bd6-4c1a-8395-f990f837adff.png",
|
||||
"accepted": true,
|
||||
"sha256": "0d812437a71b65c34c467f892d918e70903c9f407ad2e6dc5e5d08d9b32828ec"
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-generated-v1.png",
|
||||
"prompt": "prompts/projectile-hd-v1.txt",
|
||||
"tool_output": "exec-c22fbaf6-60b7-4e57-a3fc-e2567d1159da.png",
|
||||
"accepted": false,
|
||||
"sha256": "c7bbc2e39911943398aaee11bcf381a6b466c83e631672f0299826373b195255"
|
||||
},
|
||||
{
|
||||
"file": "art/projectile-generated-v2.png",
|
||||
"prompt": "prompts/projectile-hd-v2.txt",
|
||||
"tool_output": "exec-f140bd43-cd64-4202-bf49-3af441a64dae.png",
|
||||
"accepted": true,
|
||||
"sha256": "1f12188394c367ce531f79bf158140ac9a866b4ebb29131bd5f2b5009fa25a16"
|
||||
}
|
||||
],
|
||||
"projectile_revision": "First shell was too slender at 8x8 SD. v2 broadens the body and uses square framing; only v2 supplies runtime art.",
|
||||
"pipeline": "build_assets.py preserves generated RGBA, resizes full boss canvas, contains shell canvas with padding, derives BOX SD and LANCZOS HD; no redraw or alpha reconstruction.",
|
||||
"alpha": "Alpha preserved; engine uses alpha>=128 as indexed visibility. No outline or ground shadow added.",
|
||||
"sounds": "Three deterministic, original synthesized 22050 Hz mono PCM16 WAVs; exact recipe and hashes in audio-recipe.json.",
|
||||
"version_control": "All generated masters, shipped SD/HD PNGs, previews, prompts and WAVs intended for Git. Classic reference pixels remain outside this package."
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
fn is_alive(event) {
|
||||
for unit in event.entities {
|
||||
if unit.entity == event.entity && unit.health > 0 { return true; }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Every live boss owns its own clock/direction. A fresh spawn clears reused IDs
|
||||
// from previous sorties; no wall clock or rendering state enters the simulation.
|
||||
fn on_spawn(event, state, random) {
|
||||
if !state.contains("units") { state.units = #{}; }
|
||||
if !state.contains("dead") { state.dead = #{}; }
|
||||
let key = event.entity.to_string();
|
||||
// A queued spawn still starts a new lifecycle if advance already killed it.
|
||||
// Clear saved guards first so its following death is counted for this sortie.
|
||||
state.dead.remove(key);
|
||||
state.units.remove(key);
|
||||
if !is_alive(event) { return #{ commands: [], state: state }; }
|
||||
state.units[key] = #{ direction: 1, age: 0 };
|
||||
#{ 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 }; }
|
||||
let key = event.entity.to_string();
|
||||
if !state.contains("units") { state.units = #{}; }
|
||||
if !state.units.contains(key) { state.units[key] = #{ direction: 1, age: 0 }; }
|
||||
if event.y < 24 {
|
||||
return #{ commands: [#{ op: "velocity", entity: event.entity, vx: 0, vy: 2 }], state: state };
|
||||
}
|
||||
let unit = state.units[key];
|
||||
if event.x >= 144 { unit.direction = -1; }
|
||||
if event.x <= 80 { unit.direction = 1; }
|
||||
let phase = unit.age % 54;
|
||||
let commands = [#{ op: "move", entity: event.entity, x: event.x, y: 24 }];
|
||||
// Twelve ticks of a visible pause plus a two-note alarm telegraph each salvo.
|
||||
commands.push(#{ op: "velocity", entity: event.entity, vx: if phase < 12 { 0 } else { unit.direction }, vy: 0 });
|
||||
if phase == 0 { commands.push(#{ op: "play_sound", sound: "iron_coast_boss_4:warning" }); }
|
||||
if phase == 12 {
|
||||
let left_active = (unit.age / 54) % 2 == 0;
|
||||
// The outer shells come from both batteries; the third shell alternates.
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_4:projectile", x: event.x + 24, y: 80, vx: -1, vy: 4, hostile: true });
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_4:projectile", x: event.x + 72, y: 80, vx: 1, vy: 4, hostile: true });
|
||||
commands.push(#{ op: "projectile", weapon: "iron_coast_boss_4:projectile", x: event.x + if left_active { 28 } else { 68 }, y: 80, vx: 0, vy: 4, hostile: true });
|
||||
commands.push(#{ op: "play_sound", sound: "iron_coast_boss_4:shot" });
|
||||
state.salvos = if state.contains("salvos") { state.salvos + 1 } else { 1 };
|
||||
}
|
||||
unit.age += 1;
|
||||
state.units[key] = unit;
|
||||
#{ commands: commands, state: state }
|
||||
}
|
||||
|
||||
fn on_death(event, state, random) {
|
||||
let key = event.entity.to_string();
|
||||
if !state.contains("dead") { state.dead = #{}; }
|
||||
if state.dead.contains(key) { return #{ commands: [], state: state }; }
|
||||
state.dead[key] = true;
|
||||
if state.contains("units") { state.units.remove(key); }
|
||||
let defeated = if event.objectives.contains("iron_coast_boss_4:defeated") { event.objectives["iron_coast_boss_4:defeated"] + 1 } else { 1 };
|
||||
#{ commands: [
|
||||
#{ op: "objective", key: "iron_coast_boss_4:defeated", value: defeated },
|
||||
#{ op: "effect", x: event.x + 24, y: event.y + 40 },
|
||||
#{ op: "effect", x: event.x + 72, y: event.y + 40 },
|
||||
#{ op: "play_sound", sound: "iron_coast_boss_4:death" }
|
||||
], state: state }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build package-local Rust acceptance against the latest release library."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parent
|
||||
project = root.parents[2]
|
||||
deps = project / "RustRaptor/target/release/deps"
|
||||
libraries = sorted(deps.glob("libraptor_replica-*.rlib"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if not libraries:
|
||||
raise SystemExit("Build RustRaptor in release mode before this package-local check")
|
||||
output = project / ".reference/iron-coast-boss-4-validation"
|
||||
output.parent.mkdir(exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["RAPTOR_PROJECT"] = str(project)
|
||||
local_lib = Path.home() / ".local/lib"
|
||||
env["LD_LIBRARY_PATH"] = f"{local_lib}:{env.get('LD_LIBRARY_PATH', '')}"
|
||||
env["LIBRARY_PATH"] = f"{local_lib}:{env.get('LIBRARY_PATH', '')}"
|
||||
subprocess.run(["rustc", "--edition=2024", "--test", str(root / "validate.rs"), "--extern", f"raptor_replica={libraries[0]}",
|
||||
"-L", f"dependency={deps}", "-L", str(local_lib), "-o", str(output)], check=True, env=env)
|
||||
subprocess.run([str(output), "--nocapture", "--test-threads=1"], check=True, env=env)
|
||||
@@ -0,0 +1,353 @@
|
||||
//! Package-local acceptance using the actual Rust simulation and Rhai runtime.
|
||||
use raptor_replica::{
|
||||
assets::Catalog,
|
||||
content::{ContentId, Registry},
|
||||
modding::{self, ActiveMods, Command},
|
||||
persistence::Pilot,
|
||||
simulation::{Input, Outcome, World},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const OWNER: &str = "iron_coast_boss_4";
|
||||
const UNIT: &str = "iron_coast_boss_4:unit";
|
||||
const PROJECTILE: &str = "iron_coast_boss_4:projectile";
|
||||
const MISSION: &str = "base:sector_1_wave_1";
|
||||
const STATE: &str = "iron_coast_boss_4/scripts/ai.rhai";
|
||||
|
||||
fn active() -> ActiveMods {
|
||||
let project = PathBuf::from(std::env::var("RAPTOR_PROJECT").unwrap());
|
||||
let catalog = Catalog::open(project.join("data")).unwrap();
|
||||
let base = Registry::base(&catalog).unwrap();
|
||||
let discovery = modding::discover(&project.join("RustRaptor/mods"));
|
||||
let mut active =
|
||||
ActiveMods::build(&base, &discovery, &[OWNER.to_owned()], Some(&catalog)).unwrap();
|
||||
// Make an isolated non-legacy arena while retaining the real original guns.
|
||||
let mission = active.registry.missions.get_mut(&MISSION.into()).unwrap();
|
||||
mission.legacy_map = None;
|
||||
mission.spawns.clear();
|
||||
mission.tiles.clear();
|
||||
mission.layout.clear();
|
||||
mission.columns = 0;
|
||||
mission.script = None;
|
||||
mission.auto_complete = false;
|
||||
mission.duration = 10_000;
|
||||
active
|
||||
}
|
||||
|
||||
fn world(active: &ActiveMods) -> World {
|
||||
let mut pilot = Pilot::default();
|
||||
pilot.difficulty = 3;
|
||||
pilot.inventory.insert("base:energy".into(), 100);
|
||||
World::new(&MISSION.into(), &pilot, &active.registry).unwrap()
|
||||
}
|
||||
|
||||
fn step(world: &mut World, active: &mut ActiveMods) {
|
||||
world
|
||||
.step(Input::default(), &active.registry, &mut active.scripts)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warning_precedes_three_shells_and_batteries_alternate() {
|
||||
let mut active = active();
|
||||
let mut world = world(&active);
|
||||
let id = world
|
||||
.spawn(&UNIT.into(), 112, 24, &active.registry)
|
||||
.unwrap();
|
||||
for tick in 0..12 {
|
||||
step(&mut world, &mut active);
|
||||
assert!(world.projectiles.iter().all(|p| p.weapon.0 != PROJECTILE));
|
||||
let unit = world.enemies.iter().find(|e| e.entity == id).unwrap();
|
||||
assert_eq!((unit.y, unit.vx, unit.vy), (24, 0, 0));
|
||||
if tick == 0 {
|
||||
assert!(
|
||||
world
|
||||
.mod_sounds
|
||||
.iter()
|
||||
.any(|id| id.0 == "iron_coast_boss_4:warning")
|
||||
);
|
||||
}
|
||||
}
|
||||
step(&mut world, &mut active);
|
||||
let shells = world
|
||||
.projectiles
|
||||
.iter()
|
||||
.filter(|p| p.weapon.0 == PROJECTILE)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(shells.len(), 3);
|
||||
assert_eq!(
|
||||
shells.iter().filter(|p| p.vx == 0).next().unwrap().x,
|
||||
112 + 28
|
||||
);
|
||||
assert!(shells.iter().all(|p| p.hostile && p.vy == 4));
|
||||
assert!(
|
||||
world
|
||||
.mod_sounds
|
||||
.iter()
|
||||
.any(|id| id.0 == "iron_coast_boss_4:shot")
|
||||
);
|
||||
for _ in 13..67 {
|
||||
step(&mut world, &mut active);
|
||||
}
|
||||
let unit = world.enemies.iter().find(|e| e.entity == id).unwrap();
|
||||
let latest = world
|
||||
.projectiles
|
||||
.iter()
|
||||
.filter(|p| p.weapon.0 == PROJECTILE && p.age == 0)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(latest.len(), 3);
|
||||
assert_eq!(
|
||||
latest.iter().filter(|p| p.vx == 0).next().unwrap().x,
|
||||
unit.x + 68
|
||||
);
|
||||
assert_eq!(active.scripts.states[STATE]["salvos"].as_i64(), Some(2));
|
||||
assert_eq!(world.outcome, Outcome::Playing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_bosses_have_independent_state_and_death_counts_once() {
|
||||
let mut active = active();
|
||||
let mut world = world(&active);
|
||||
let a = world.spawn(&UNIT.into(), 80, 24, &active.registry).unwrap();
|
||||
for _ in 0..20 {
|
||||
step(&mut world, &mut active);
|
||||
}
|
||||
let b = world
|
||||
.spawn(&UNIT.into(), 144, 24, &active.registry)
|
||||
.unwrap();
|
||||
step(&mut world, &mut active);
|
||||
assert_eq!(
|
||||
active.scripts.states[STATE]["units"][a.to_string()]["age"].as_i64(),
|
||||
Some(21)
|
||||
);
|
||||
assert_eq!(
|
||||
active.scripts.states[STATE]["units"][b.to_string()]["age"].as_i64(),
|
||||
Some(1)
|
||||
);
|
||||
world
|
||||
.apply_commands(
|
||||
&[Command::Damage {
|
||||
entity: a,
|
||||
amount: 100_000,
|
||||
}],
|
||||
&active.registry,
|
||||
)
|
||||
.unwrap();
|
||||
step(&mut world, &mut active);
|
||||
assert_eq!(world.objectives["iron_coast_boss_4:defeated"], 1);
|
||||
assert!(
|
||||
active.scripts.states[STATE]["units"]
|
||||
.get(a.to_string())
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
world
|
||||
.mod_sounds
|
||||
.iter()
|
||||
.any(|id| id.0 == "iron_coast_boss_4:death")
|
||||
);
|
||||
for _ in 0..10 {
|
||||
step(&mut world, &mut active);
|
||||
}
|
||||
assert_eq!(world.objectives["iron_coast_boss_4:defeated"], 1);
|
||||
assert!(world.enemies.iter().any(|e| e.entity == b && e.health > 0));
|
||||
world
|
||||
.apply_commands(
|
||||
&[Command::Damage {
|
||||
entity: b,
|
||||
amount: 100_000,
|
||||
}],
|
||||
&active.registry,
|
||||
)
|
||||
.unwrap();
|
||||
step(&mut world, &mut active);
|
||||
assert_eq!(world.objectives["iron_coast_boss_4:defeated"], 2);
|
||||
assert_eq!(world.outcome, Outcome::Playing);
|
||||
assert!(
|
||||
active.scripts.states[STATE]["units"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_script_state_and_rng_reproduce_identical_future() {
|
||||
let mut a = active();
|
||||
let mut first = world(&a);
|
||||
first.spawn(&UNIT.into(), 112, -40, &a.registry).unwrap();
|
||||
for _ in 0..90 {
|
||||
step(&mut first, &mut a);
|
||||
}
|
||||
let mut second = first.clone();
|
||||
let mut b = active();
|
||||
b.scripts.restore(a.scripts.checkpoints());
|
||||
for _ in 0..100 {
|
||||
step(&mut first, &mut a);
|
||||
step(&mut second, &mut b);
|
||||
assert_eq!(first.digest(), second.digest());
|
||||
assert_eq!(a.scripts.states, b.scripts.states);
|
||||
assert_eq!(
|
||||
format!("{:?}", a.scripts.rng),
|
||||
format!("{:?}", b.scripts.rng)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_sortie_clears_reused_entity_death_guards() {
|
||||
let mut active = active();
|
||||
let mut first = world(&active);
|
||||
let a = first
|
||||
.spawn(&UNIT.into(), 112, 24, &active.registry)
|
||||
.unwrap();
|
||||
step(&mut first, &mut active);
|
||||
first
|
||||
.apply_commands(
|
||||
&[Command::Damage {
|
||||
entity: a,
|
||||
amount: 100_000,
|
||||
}],
|
||||
&active.registry,
|
||||
)
|
||||
.unwrap();
|
||||
step(&mut first, &mut active);
|
||||
let mut second = world(&active);
|
||||
let b = second
|
||||
.spawn(&UNIT.into(), 112, 24, &active.registry)
|
||||
.unwrap();
|
||||
assert_eq!(a, b);
|
||||
step(&mut second, &mut active);
|
||||
second
|
||||
.apply_commands(
|
||||
&[Command::Damage {
|
||||
entity: b,
|
||||
amount: 100_000,
|
||||
}],
|
||||
&active.registry,
|
||||
)
|
||||
.unwrap();
|
||||
step(&mut second, &mut active);
|
||||
assert_eq!(second.objectives["iron_coast_boss_4:defeated"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_dead_guard_does_not_hide_a_lethal_hit_on_the_reused_spawn_step() {
|
||||
let mut prior = active();
|
||||
let mut previous_world = world(&prior);
|
||||
let old = previous_world
|
||||
.spawn(&UNIT.into(), 112, 24, &prior.registry)
|
||||
.unwrap();
|
||||
step(&mut previous_world, &mut prior);
|
||||
previous_world
|
||||
.apply_commands(
|
||||
&[Command::Damage {
|
||||
entity: old,
|
||||
amount: 100_000,
|
||||
}],
|
||||
&prior.registry,
|
||||
)
|
||||
.unwrap();
|
||||
step(&mut previous_world, &mut prior);
|
||||
assert_eq!(
|
||||
prior.scripts.states[STATE]["dead"][old.to_string()].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let saved_script_checkpoint = prior.scripts.checkpoints();
|
||||
let mut restored = active();
|
||||
restored.scripts.restore(saved_script_checkpoint);
|
||||
restored
|
||||
.registry
|
||||
.weapons
|
||||
.get_mut(&PROJECTILE.into())
|
||||
.unwrap()
|
||||
.damage = 100_000;
|
||||
let mut replay = world(&restored);
|
||||
let reused = replay
|
||||
.spawn(&UNIT.into(), 112, 24, &restored.registry)
|
||||
.unwrap();
|
||||
assert_eq!(old, reused);
|
||||
// Preserve the actual queued spawn event to invoke duplicate death later.
|
||||
let duplicate_event = replay
|
||||
.events
|
||||
.iter()
|
||||
.find(|e| e.hook == "on_spawn")
|
||||
.unwrap()
|
||||
.event
|
||||
.clone();
|
||||
let credits = replay.player.credits;
|
||||
replay
|
||||
.apply_commands(
|
||||
&[Command::Projectile {
|
||||
weapon: PROJECTILE.into(),
|
||||
x: 160,
|
||||
y: 56,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
hostile: false,
|
||||
}],
|
||||
&restored.registry,
|
||||
)
|
||||
.unwrap();
|
||||
// Advance hits before the queued on_spawn callback. No dead entity may
|
||||
// receive velocity, a warning or a projectile from that callback.
|
||||
step(&mut replay, &mut restored);
|
||||
assert!(
|
||||
replay
|
||||
.enemies
|
||||
.iter()
|
||||
.all(|e| e.entity != reused || e.health <= 0)
|
||||
);
|
||||
assert!(
|
||||
restored.scripts.states[STATE]["units"]
|
||||
.get(reused.to_string())
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
replay
|
||||
.mod_sounds
|
||||
.iter()
|
||||
.all(|id| id.0 != "iron_coast_boss_4:warning" && id.0 != "iron_coast_boss_4:shot")
|
||||
);
|
||||
assert!(replay.projectiles.iter().all(|p| !p.hostile));
|
||||
step(&mut replay, &mut restored);
|
||||
assert_eq!(replay.objectives["iron_coast_boss_4:defeated"], 1);
|
||||
assert_eq!(replay.player.credits, credits + 1_800);
|
||||
assert!(replay.enemies.iter().all(|e| e.entity != reused));
|
||||
|
||||
let state = restored.scripts.states[STATE].clone();
|
||||
let duplicate_commands = restored
|
||||
.scripts
|
||||
.invoke(OWNER, "scripts/ai.rhai", "on_death", &duplicate_event, 1)
|
||||
.unwrap();
|
||||
assert!(duplicate_commands.is_empty());
|
||||
assert_eq!(restored.scripts.states[STATE], state);
|
||||
for _ in 0..60 {
|
||||
step(&mut replay, &mut restored);
|
||||
}
|
||||
assert_eq!(replay.objectives["iron_coast_boss_4:defeated"], 1);
|
||||
assert_eq!(replay.player.credits, credits + 1_800);
|
||||
assert!(replay.projectiles.iter().all(|p| !p.hostile));
|
||||
assert_eq!(replay.outcome, Outcome::Playing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_boss_enters_and_persists_in_safe_bounds_without_auto_completion() {
|
||||
let mut active = active();
|
||||
let mut world = world(&active);
|
||||
let boss = world
|
||||
.spawn(&ContentId::from(UNIT), 112, -64, &active.registry)
|
||||
.unwrap();
|
||||
for _ in 0..600 {
|
||||
// Keep the player outside the cannon lane so the test measures persistence.
|
||||
world.player.x = 0;
|
||||
step(&mut world, &mut active);
|
||||
assert_eq!(world.outcome, Outcome::Playing);
|
||||
}
|
||||
let unit = world.enemies.iter().find(|e| e.entity == boss).unwrap();
|
||||
assert_eq!(unit.y, 24);
|
||||
assert!((80..=145).contains(&unit.x));
|
||||
assert_eq!(unit.health, 160);
|
||||
assert!(!world.objectives.contains_key("iron_coast_boss_4:defeated"));
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# Cinder Hammer
|
||||
|
||||
An independent boss mod for Iron Coast mission 5, **Cinder Docks**. The furnace
|
||||
gunship patrols persistently at the top of the combat area. A pressure-warning
|
||||
sound precedes three molten projectiles fired six simulation ticks apart. The
|
||||
center mortar travels vertically; the two plasma rounds spread outward.
|
||||
|
||||
- Enemy: `iron_coast_boss_5:unit`; health 160, reward 1,800, logical size 96×64.
|
||||
- Mortar and plasma: damage 3, speed 4; a 54-tick attack cycle.
|
||||
- Death objective: `iron_coast_boss_5:defeated`, incremented only on actual death.
|
||||
- Manifest API 1, package version 1.0.0, no dependencies or overrides.
|
||||
|
||||
The package never completes a mission itself. A mission must spawn the enemy,
|
||||
declare this package as a dependency, and require its death objective. Each
|
||||
enemy instance stores its own attack clock and patrol direction in serializable
|
||||
Rhai state. Dead or removed enemies cannot issue attacks or movement commands.
|
||||
|
||||
SD PNGs are derived from the same transparent generated masters as the 8× HD
|
||||
PNGs. Both modes retain the same logical canvas and collision dimensions.
|
||||
`art/preview.png` shows the HD hull and exact SD silhouette enlarged for review.
|
||||
The generation prompts are in `prompts/`; `provenance.json` records tool mode
|
||||
and source hashes. All artwork and audio belong in Git with this mod.
|
||||
|
||||
Run `python3 build_assets.py` to reproduce the resized art and synthesized mono
|
||||
22,050 Hz, 16-bit PCM warning, shot and destruction sounds. Python and Pillow
|
||||
are required. `audio-recipe.json` and `art-qa.json` record file hashes and checks.
|
||||
|
||||
Run `cargo test --locked --manifest-path validation/Cargo.toml` from this folder
|
||||
for five asset-independent checks of attack timing, multiple-instance state,
|
||||
save serialization, dead-entity guards, patrol turns, actual simulation
|
||||
persistence, rewards and the single death objective. The simulation test also
|
||||
validates the real package-local PNGs and WAVs.
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"alpha": "Preserved RGBA; runtime alpha>=128 indexed transparency",
|
||||
"registration": "Whole-canvas proportional resize, fixed padding; single idle frame",
|
||||
"files": [
|
||||
{
|
||||
"name": "unit",
|
||||
"logical_size": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"hd_scale": 8,
|
||||
"origin": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"frames": 1,
|
||||
"master_sha256": "e138d470db1b0a9fa268b32ec8f9b1d9db3dcf41fb9c951acea609dbba5025db",
|
||||
"master_size": [
|
||||
1536,
|
||||
1024
|
||||
],
|
||||
"files": {
|
||||
"art/unit-sd.png": {
|
||||
"physical_size": [
|
||||
96,
|
||||
64
|
||||
],
|
||||
"opaque_bounds": [
|
||||
7,
|
||||
5,
|
||||
89,
|
||||
57
|
||||
],
|
||||
"transparent_pixels": 3690,
|
||||
"sha256": "506094c4c1d1ad15c3f58484aab8057b829a53dea59f598a8632e6236dc4e21d"
|
||||
},
|
||||
"art/unit-hd.png": {
|
||||
"physical_size": [
|
||||
768,
|
||||
512
|
||||
],
|
||||
"opaque_bounds": [
|
||||
58,
|
||||
38,
|
||||
710,
|
||||
453
|
||||
],
|
||||
"transparent_pixels": 235914,
|
||||
"sha256": "01bf000620057a06ca85b5b5d59e5e1ef56962726212b5f901916dfad83c86b1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "projectile",
|
||||
"logical_size": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"hd_scale": 8,
|
||||
"origin": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"frames": 1,
|
||||
"master_sha256": "f16d4f48f61ffbdd1eac4102b856b5f3bfa3555dd0ae41784b9cd7499d52b955",
|
||||
"master_size": [
|
||||
1254,
|
||||
1254
|
||||
],
|
||||
"files": {
|
||||
"art/projectile-sd.png": {
|
||||
"physical_size": [
|
||||
8,
|
||||
8
|
||||
],
|
||||
"opaque_bounds": [
|
||||
2,
|
||||
3,
|
||||
6,
|
||||
6
|
||||
],
|
||||
"transparent_pixels": 56,
|
||||
"sha256": "c441ded21f3dcfa6666256ac2e5444b8de3e8ab3b52b22080b36a5ad4eacd3d8"
|
||||
},
|
||||
"art/projectile-hd.png": {
|
||||
"physical_size": [
|
||||
64,
|
||||
64
|
||||
],
|
||||
"opaque_bounds": [
|
||||
19,
|
||||
12,
|
||||
45,
|
||||
50
|
||||
],
|
||||
"transparent_pixels": 3490,
|
||||
"sha256": "39a7f5ed33f4d1e0d1683e7337b100ab4a04933f19b0939f2f26f4dfdbda588b"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 801 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 168 B |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"generator": "build_assets.py deterministic chirp/noise PCM synthesis; no recordings",
|
||||
"files": {
|
||||
"audio/warning.wav": {
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"seed": 5501,
|
||||
"duration_seconds": 0.28,
|
||||
"samples": 6174,
|
||||
"peak": 23592,
|
||||
"rms": 7402.96485860664,
|
||||
"sha256": "dcf32a1fc3cdcb734e6aaa3ed0cfeac854e2b0c89b92a209d3b77c53dc820604"
|
||||
},
|
||||
"audio/shot.wav": {
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"seed": 5502,
|
||||
"duration_seconds": 0.24,
|
||||
"samples": 5292,
|
||||
"peak": 23592,
|
||||
"rms": 4267.066582997498,
|
||||
"sha256": "16bb1b201c746b41b93baf6faa4e7612c90ebce2168d888d8da7e0e222157bd9"
|
||||
},
|
||||
"audio/death.wav": {
|
||||
"sample_rate": 22050,
|
||||
"channels": 1,
|
||||
"bits": 16,
|
||||
"seed": 5503,
|
||||
"duration_seconds": 0.95,
|
||||
"samples": 20948,
|
||||
"peak": 23592,
|
||||
"rms": 3232.3544431693276,
|
||||
"sha256": "91950d95cdd98a42359e9b65072904a15270a2a80308e1d93aa8a73295ee67d5"
|
||||
}
|
||||
}
|
||||
}
|
||||