Add scripted lightning weapon mods with interpolated anchored effects
This commit is contained in:
1 parent
98465133aa
commit
c0a6fd3431
50 files changed
+3500
-65
No files matched your search
@@ -0,0 +1,25 @@
|
||||
# Instructions for AI agents authoring mods
|
||||
|
||||
Read [the mod authoring checklist](../docs/mod-authoring-checklist.md) and
|
||||
[the version 1 API](../docs/modding-api.md) before creating or changing a mod.
|
||||
Follow the user's requested behavior and controls, and preserve other agents'
|
||||
work in the shared checkout.
|
||||
|
||||
- Implement gameplay through content definitions, Rhai callbacks and documented
|
||||
commands. Keep a standalone mod independent unless its content needs a real
|
||||
dependency. Do not select legacy engine behavior for new content.
|
||||
- If the API lacks a required capability, define the smallest general addition
|
||||
and coordinate engine work with the responsible agent when authorized. Keep
|
||||
content-specific targeting, damage and tuning in the mod.
|
||||
- Ship actual SD and HD PNG assets for every artwork reference, plus required
|
||||
sounds/music. Keep dimensions, frame alignment and transparency consistent.
|
||||
Store generation prompts, provenance and reproduction recipes with the mod.
|
||||
- Price equipment against original and shipped equipment, using controlled
|
||||
simulation observations as well as targeting, range, cooldown and loadout
|
||||
utility. Do not copy cheap placeholder prices from an example.
|
||||
- Finish independent activation, gameplay and coexistence tests, real package
|
||||
validation where the original catalog is available, and a visual check of
|
||||
both SD and HD. Document any checks that cannot run.
|
||||
- Document enabling, controls, prices and behavior limits. Deliver complete
|
||||
local packages without changing the user's pilot, selected mods or unrelated
|
||||
content unless requested.
|
||||
@@ -0,0 +1,10 @@
|
||||
# lightning_cannon artwork and sound provenance
|
||||
|
||||
The transparent equipment master was created with the built-in imagegen tool and the imagegen skill. No original game pixels or external reference artwork were used. `prepare_art.rs` fits the master onto transparent 16×16 SD and 64×64 HD canvases. Collision and targeting dimensions are independent of image resolution. Actual connecting lightning is drawn through the public `arc` command rather than projectile sprites.
|
||||
|
||||
Final prompt:
|
||||
|
||||
Use case: stylized-concept. Asset type: transparent inventory equipment sprite for a top-down 1994-inspired arcade aircraft shooter, supplied as a square master image for a 16x16 logical icon and 64x64 HD icon. Primary request: a high-powered Lightning Cannon weapon module. Single compact industrial gun assembly viewed directly from above, long forward-facing barrel at top, two sturdy brushed steel rails around a vivid cyan electrical coil, white-blue zigzag electricity confined between the rails, dark navy housing with small copper contacts. Hand-painted retro game sprite with strong readable silhouette, simple broad material planes and restrained crisp highlights. Center the entire object with a little transparent padding. Genuinely transparent background and no ground shadow. No text, letters, numbers, frame, UI, watermark, or surrounding scene. The gun occupies most of the canvas and should still read at tiny size.
|
||||
|
||||
The master, SD/HD images and sound WAVs are included in the package. SHA-256 hashes are recorded in `art-provenance.json`. `generate_audio.py` reproduces deterministic PCM16 mono sounds at 22,050 Hz. Palette conversion occurs in the runtime loader.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Lightning Cannon
|
||||
|
||||
An independent, reusable heavy lightning special for **825,000 credits**.
|
||||
Enable **Lightning Cannon** in the Mods menu, apply selections at the main menu,
|
||||
then buy it in the supply room. Select it in Inventory or cycle specials;
|
||||
**Fire or Special** discharges the cannon while selected. It consumes no ammo,
|
||||
and you can own one.
|
||||
|
||||
The bolt instantly hits the nearest visible air or ground target ahead of the
|
||||
muzzle, within 180 logical pixels and a 36-pixel horizontal aiming allowance.
|
||||
It deals **32 damage**, then jumps from the struck ship to the nearest unhit
|
||||
enemy within **48 pixels**, for **20, 12 and 8 damage**. A discharge hits at most
|
||||
four different enemies. Chaining can change direction and continues from a
|
||||
destroyed target's last position. An isolated target takes only the initial hit;
|
||||
off-axis targets require an adjacent chain target. Cooldown is **18 ticks**,
|
||||
about 0.77 seconds. A missed shot shows a bolt and firing sound without damage.
|
||||
|
||||
The 30-second controlled simulation measured **41.60 damage/s** on an isolated
|
||||
aligned target and **93.60 combined damage/s** across a four-target chain. The
|
||||
price places it above the original Pulse Cannon and below the Death Ray. See
|
||||
the package [balance report](balance-report.json) for the recorded observations.
|
||||
|
||||
This package uses the public mod API's `scripted` weapon behavior, read-only
|
||||
entity snapshots, `damage`, `arc` and `play_sound` commands. All acquisition,
|
||||
chaining and damage rules live in `scripts/weapon.rhai`. It has no dependencies
|
||||
or overrides. It coexists with the other independent weapons and Iron Coast.
|
||||
|
||||
The equipment icon ships in **16×16 SD** and **64×64 HD**, alongside the
|
||||
transparent source master and deterministic synthesized audio. Connecting bolts
|
||||
use the game's arc renderer at both scales. See [provenance](ART_PROVENANCE.md)
|
||||
for the exact built-in imagegen prompt, asset hashes and reproduction recipes.
|
||||
|
||||
From `RustRaptor/`:
|
||||
|
||||
```sh
|
||||
cargo run --locked --no-default-features -- --mods mods --validate-mods lightning_cannon
|
||||
cargo test --locked --no-default-features --test weapons_mods lightning_cannon
|
||||
```
|
||||
|
||||
See [weapon pricing and measurements](../../docs/weapon-balance.md),
|
||||
[the API](../../docs/modding-api.md) and
|
||||
[the AI mod checklist](../../docs/mod-authoring-checklist.md).
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"package": "lightning_cannon",
|
||||
"tool": "built-in imagegen",
|
||||
"use_case": "stylized-concept",
|
||||
"art": {
|
||||
"art/equipment-generated-source.png": {
|
||||
"sha256": "96bddda748c25a70dc3a4be85badb524fe29a77c35ec82bb3f50efe529876ba6",
|
||||
"bytes": 727812
|
||||
},
|
||||
"art/equipment-hd.png": {
|
||||
"sha256": "b17a0d4d4af5f049bc2b02abce6e57570d8217a29dc76f4bf43df3bc2e51c703",
|
||||
"bytes": 2974
|
||||
},
|
||||
"art/equipment-sd.png": {
|
||||
"sha256": "3e43886e382ee9d3014c8a50700602b0499356b6b01a42ba8da965f18af17855",
|
||||
"bytes": 420
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"audio/fire.wav": {
|
||||
"sha256": "139b19dc294cdb256618c381eba3d1f7d0f6b0f77e463c0fda9dd579a8a457d0",
|
||||
"bytes": 10628
|
||||
},
|
||||
"audio/impact.wav": {
|
||||
"sha256": "636543d96b04a8c52e5ade37a5bce1d3f77e3bb6889ff4ffd6675ad9938d6415",
|
||||
"bytes": 4894
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 711 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 420 B |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"difficulty": 2,
|
||||
"fire_steps": 700,
|
||||
"rate": "70/3 Hz",
|
||||
"target_health": 1000000,
|
||||
"target_size": [
|
||||
32,
|
||||
32
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"damage": [
|
||||
1248
|
||||
],
|
||||
"damage_per_second": [
|
||||
41.6
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "aligned_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
1248
|
||||
],
|
||||
"damage_per_second": [
|
||||
41.6
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "aligned_ground",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "off_axis_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "off_axis_ground",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
1248,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
41.6,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "column",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
780,
|
||||
1248,
|
||||
468
|
||||
],
|
||||
"damage_per_second": [
|
||||
26.0,
|
||||
41.6,
|
||||
15.6
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "cluster",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "close_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "close_ground",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
1248,
|
||||
780,
|
||||
468,
|
||||
312,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
41.6,
|
||||
26.0,
|
||||
15.6,
|
||||
10.4,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "chain",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0,
|
||||
0,
|
||||
1248,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0,
|
||||
0.0,
|
||||
41.6,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "close_cluster",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
1942
|
||||
],
|
||||
"damage_per_second": [
|
||||
64.73333333333333
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "aligned_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": true
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_cannon:equipment",
|
||||
"price": 825000,
|
||||
"scene": "close_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": true
|
||||
}
|
||||
],
|
||||
"method": "Real World::step and shipped scripts; 700 Fire steps (30 seconds); stationary 32x32 surviving targets; no player damage. Reproduce with tests/weapon_balance.rs.",
|
||||
"limitations": "Controlled fixture observations, not a claim of equal campaign difficulty. Original catalog and preview pixels remain outside Git."
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "lightning_cannon:projectile",
|
||||
"name": "Lightning Cannon Discharge",
|
||||
"damage": 32,
|
||||
"speed": 0,
|
||||
"cooldown": 18,
|
||||
"behavior": "scripted",
|
||||
"target": "all",
|
||||
"script": "scripts/weapon.rhai"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "lightning_cannon:equipment",
|
||||
"name": "Lightning Cannon",
|
||||
"description": "Selectable reusable special. Fires a powerful lightning bolt into the nearest air or ground target in a narrow lane ahead, then chains through up to three nearby enemies with decreasing damage. Select it in Inventory or cycle specials, then use Fire or Special. No ammunition is consumed.",
|
||||
"price": 825000,
|
||||
"max_count": 1,
|
||||
"initial_count": 1,
|
||||
"weapon": "lightning_cannon:projectile",
|
||||
"special": true,
|
||||
"consumable": false,
|
||||
"art": {
|
||||
"png": "art/equipment-sd.png",
|
||||
"hd_png": "art/equipment-hd.png",
|
||||
"logical_width": 16,
|
||||
"logical_height": 16,
|
||||
"nearest": true
|
||||
},
|
||||
"script": "scripts/equipment.rhai"
|
||||
}
|
||||
],
|
||||
"sounds": [
|
||||
{
|
||||
"id": "lightning_cannon:fire",
|
||||
"name": "Lightning Cannon Discharge",
|
||||
"file": "audio/fire.wav",
|
||||
"volume": 92,
|
||||
"pan": 127,
|
||||
"pitch": 128,
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"id": "lightning_cannon:impact",
|
||||
"name": "Lightning Cannon Contact",
|
||||
"file": "audio/impact.wav",
|
||||
"volume": 78,
|
||||
"pan": 127,
|
||||
"pitch": 128,
|
||||
"priority": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce deterministic PCM16 mono electrical discharge sounds."""
|
||||
import math
|
||||
import struct
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE = "lightning_cannon"
|
||||
RATE = 22050
|
||||
|
||||
def render(kind):
|
||||
heavy = PACKAGE == "lightning_cannon"
|
||||
duration = (0.24 if heavy else 0.09) if kind == "fire" else (0.11 if heavy else 0.06)
|
||||
count = int(RATE * duration)
|
||||
seed = 0x5A17C0DE
|
||||
phase = 0.0
|
||||
samples = []
|
||||
for n in range(count):
|
||||
t = n / count
|
||||
seed ^= (seed << 13) & 0xffffffff
|
||||
seed ^= seed >> 17
|
||||
seed ^= (seed << 5) & 0xffffffff
|
||||
seed &= 0xffffffff
|
||||
noise = seed / 2147483648.0 - 1.0
|
||||
frequency = (2100 - 1450 * t) if heavy else (3100 - 1650 * t)
|
||||
phase += 2 * math.pi * frequency / RATE
|
||||
crackle = noise * (0.45 + 0.25 * math.sin(2 * math.pi * 61 * n / RATE) ** 8)
|
||||
tone = math.sin(phase) * 0.24 + math.sin(phase * 1.503) * 0.15
|
||||
if kind == "impact":
|
||||
tone = math.sin(phase * 0.18) * 0.25
|
||||
attack = min(1.0, n / (RATE * 0.002))
|
||||
envelope = attack * (1 - t) ** (2.2 if kind == "fire" else 3)
|
||||
samples.append(round(max(-1, min(1, (crackle + tone) * envelope)) * 21000))
|
||||
return struct.pack("<" + "h" * len(samples), *samples)
|
||||
|
||||
if __name__ == "__main__":
|
||||
output = Path(__file__).resolve().parent / "audio"
|
||||
output.mkdir(exist_ok=True)
|
||||
for kind in ["fire", "impact"]:
|
||||
with wave.open(str(output / (kind + ".wav")), "wb") as wav:
|
||||
wav.setparams((1, 2, RATE, 0, "NONE", "not compressed"))
|
||||
wav.writeframes(render(kind))
|
||||
print(output / (kind + ".wav"))
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "lightning_cannon"
|
||||
name = "Lightning Cannon"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
description = "Selectable reusable special. Fires a powerful lightning bolt into the nearest air or ground target in a narrow lane ahead, then chains through up to three nearby enemies with decreasing damage. Select it in Inventory or cycle specials, then use Fire or Special. No ammunition is consumed."
|
||||
content = "content.json"
|
||||
scripts = ["scripts/weapon.rhai", "scripts/equipment.rhai"]
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Fit the transparent equipment master to the 16px SD and 64px HD canvases.
|
||||
use image::{Rgba, RgbaImage, imageops};
|
||||
use std::{env, fs, path::Path};
|
||||
fn canvas(source: &RgbaImage, width: u32, height: u32, hd: bool) -> RgbaImage {
|
||||
// Only the solid silhouette defines its fitting box; faint outer glows stay bounded.
|
||||
let mut bounds = (source.width(), source.height(), 0, 0);
|
||||
for (x, y, p) in source.enumerate_pixels() {
|
||||
if p[3] >= 128 {
|
||||
bounds.0 = bounds.0.min(x);
|
||||
bounds.1 = bounds.1.min(y);
|
||||
bounds.2 = bounds.2.max(x + 1);
|
||||
bounds.3 = bounds.3.max(y + 1);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
bounds.2 > bounds.0 && bounds.3 > bounds.1,
|
||||
"source has no opaque silhouette"
|
||||
);
|
||||
let cropped = imageops::crop_imm(
|
||||
source,
|
||||
bounds.0,
|
||||
bounds.1,
|
||||
bounds.2 - bounds.0,
|
||||
bounds.3 - bounds.1,
|
||||
)
|
||||
.to_image();
|
||||
let margin = if hd { 4 } else { 1 };
|
||||
let available = (
|
||||
width.saturating_sub(margin * 2).max(1),
|
||||
height.saturating_sub(margin * 2).max(1),
|
||||
);
|
||||
let factor = (available.0 as f64 / cropped.width() as f64)
|
||||
.min(available.1 as f64 / cropped.height() as f64);
|
||||
let size = (
|
||||
(cropped.width() as f64 * factor).round().max(1.) as u32,
|
||||
(cropped.height() as f64 * factor).round().max(1.) as u32,
|
||||
);
|
||||
let filter = if hd {
|
||||
imageops::FilterType::Lanczos3
|
||||
} else {
|
||||
imageops::FilterType::Nearest
|
||||
};
|
||||
let mut scaled = imageops::resize(&cropped, size.0, size.1, filter);
|
||||
for p in scaled.pixels_mut() {
|
||||
if p[3] < (if hd { 64 } else { 128 }) {
|
||||
*p = Rgba([0, 0, 0, 0]);
|
||||
} else {
|
||||
p[3] = 255;
|
||||
}
|
||||
}
|
||||
let mut out = RgbaImage::new(width, height);
|
||||
imageops::overlay(
|
||||
&mut out,
|
||||
&scaled,
|
||||
((width - size.0) / 2) as i64,
|
||||
((height - size.1) / 2) as i64,
|
||||
);
|
||||
out
|
||||
}
|
||||
fn main() {
|
||||
let args: Vec<_> = env::args_os().collect();
|
||||
assert_eq!(
|
||||
args.len(),
|
||||
3,
|
||||
"usage: prepare_art EQUIPMENT_SOURCE PACKAGE_DIRECTORY"
|
||||
);
|
||||
let output = Path::new(&args[2]).join("art");
|
||||
fs::create_dir_all(&output).unwrap();
|
||||
let source = image::open(&args[1]).unwrap().into_rgba8();
|
||||
fs::copy(&args[1], output.join("equipment-generated-source.png")).unwrap();
|
||||
for (mode, scale) in [("sd", 1), ("hd", 4)] {
|
||||
let sprite = canvas(&source, 16 * scale, 16 * scale, scale == 4);
|
||||
let path = output.join(format!("equipment-{mode}.png"));
|
||||
sprite.save(&path).unwrap();
|
||||
println!("{}x{} {}", sprite.width(), sprite.height(), path.display());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Pickups use the same reusable equipment as the shop.
|
||||
fn on_pickup(event, state, random) {
|
||||
let recovered = if state.contains("recovered") { state.recovered % 100000 + 1 } else { 1 };
|
||||
#{ commands: [#{ op: "objective", key: "lightning_cannon:recovered", value: recovered }],
|
||||
state: #{ recovered: recovered } }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// The scripted discharge uses only public snapshots and checked API commands.
|
||||
// A narrow forward acquisition lane preserves aiming; later hops may go sideways.
|
||||
fn on_fire(event, state, random) {
|
||||
if event.hostile {
|
||||
return #{ commands: [], state: state };
|
||||
}
|
||||
let bursts = if state.contains("bursts") { state.bursts % 100000 + 1 } else { 1 };
|
||||
let hits = if state.contains("hits") { state.hits } else { 0 };
|
||||
let commands = [#{ op: "play_sound", sound: "lightning_cannon:fire" }];
|
||||
let struck = [];
|
||||
let from_x = event.x;
|
||||
let from_y = event.y;
|
||||
let damages = [32, 20, 12, 8];
|
||||
|
||||
for hop in 0..4 {
|
||||
let selected = #{};
|
||||
let nearest = 1000000;
|
||||
for enemy in event.entities {
|
||||
let cx = enemy.x + enemy.width / 2;
|
||||
let cy = enemy.y + enemy.height / 2;
|
||||
if enemy.health <= 0 || struck.contains(enemy.entity)
|
||||
|| cx < 0 || cx >= 320 || cy < 0 || cy >= 200 {
|
||||
continue;
|
||||
}
|
||||
let dx = cx - from_x;
|
||||
let dy = cy - from_y;
|
||||
let distance = dx * dx + dy * dy;
|
||||
let reachable = if hop == 0 {
|
||||
abs(dx) <= 36 && dy < 0 && distance <= 180 * 180
|
||||
} else {
|
||||
distance <= 48 * 48
|
||||
};
|
||||
// Entity order provides a stable tie break for equally near targets.
|
||||
if reachable && distance < nearest {
|
||||
selected = #{ entity: enemy.entity, x: cx, y: cy };
|
||||
nearest = distance;
|
||||
}
|
||||
}
|
||||
if !selected.contains("entity") { break; }
|
||||
commands.push(#{ op: "arc", x: from_x, y: from_y,
|
||||
to_x: selected.x, to_y: selected.y, ticks: 4 });
|
||||
commands.push(#{ op: "damage", entity: selected.entity, amount: damages[hop] });
|
||||
struck.push(selected.entity);
|
||||
from_x = selected.x;
|
||||
from_y = selected.y;
|
||||
hits = hits % 100000 + 1;
|
||||
}
|
||||
if struck.len() > 0 {
|
||||
commands.push(#{ op: "play_sound", sound: "lightning_cannon:impact" });
|
||||
} else {
|
||||
// A missed shot remains visible without spawning a damaging carrier.
|
||||
commands.push(#{ op: "arc", x: event.x, y: event.y,
|
||||
to_x: event.x, to_y: event.y - 180, ticks: 3 });
|
||||
}
|
||||
#{ commands: commands, state: #{ bursts: bursts, hits: hits } }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# lightning_shield artwork and sound provenance
|
||||
|
||||
The transparent equipment master was created with the built-in imagegen tool and the imagegen skill. No original game pixels or external reference artwork were used. `prepare_art.rs` fits the master onto transparent 16×16 SD and 64×64 HD canvases. Collision and targeting dimensions are independent of image resolution. Actual connecting lightning is drawn through the public `arc` command rather than projectile sprites.
|
||||
|
||||
Final prompt:
|
||||
|
||||
Use case: stylized-concept. Asset type: transparent inventory equipment sprite for a top-down 1994-inspired arcade aircraft shooter, supplied as a square master image for a 16x16 logical icon and 64x64 HD icon. Primary request: a Lightning Shield proximity zap weapon module. Single compact circular industrial coil unit viewed directly from above, a vivid cyan ring enclosing a dark central steel capacitor, four small copper electrode terminals evenly spaced, tiny white-blue lightning arcs contained inside the ring. Dark navy and brushed steel housing. Hand-painted retro game sprite with strong readable silhouette, simple broad material planes and restrained crisp highlights. Center the entire object with a little transparent padding. Genuinely transparent background and no ground shadow. No text, letters, numbers, frame, UI, watermark, spaceship, or surrounding scene. The circular emitter occupies most of the canvas and should still read at tiny size.
|
||||
|
||||
The master, SD/HD images and sound WAVs are included in the package. SHA-256 hashes are recorded in `art-provenance.json`. `generate_audio.py` reproduces deterministic PCM16 mono sounds at 22,050 Hz. Palette conversion occurs in the runtime loader.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lightning Shield
|
||||
|
||||
An independent, reusable proximity weapon for **155,000 credits**. Enable
|
||||
**Lightning Shield** in the Mods menu, apply selections at the main menu, then
|
||||
buy it in the supply room. It is **always equipped** while owned and fires
|
||||
alongside the other primary guns when you hold **Fire**. It needs no special
|
||||
selection, consumes no ammo, and you can own one.
|
||||
|
||||
Each pulse deals **2 damage** to up to **four close airborne ships** in any
|
||||
direction. Range is **28 logical pixels from your ship's center to the nearest
|
||||
point of the enemy hull**: approximately 12 pixels beyond your 32-pixel hull.
|
||||
It picks the nearest ships first, with a stable tie break. Ground structures
|
||||
are excluded. The **8-tick cooldown** gives about 2.92 pulses per second and
|
||||
5.87 measured damage per second against a stationary close ship.
|
||||
The price places this short-range supplement below the Micro Missile primary
|
||||
upgrade. See the [balance report](balance-report.json) for the 30-second
|
||||
controlled observations, including ground exclusions and combined loadouts.
|
||||
|
||||
It runs only while Fire is held, and makes no sound when nothing is in range.
|
||||
It supplements close combat; it does not absorb collision damage, restore energy
|
||||
or block bullets. It continues working when you select a different special.
|
||||
|
||||
Firing charges a flickering electric ring just outside the hull. The short zaps
|
||||
start from the edge facing each target, with a blue glow around the ship. The
|
||||
ring follows your movement and fades within one recharge after releasing Fire;
|
||||
it remains silent when no aircraft are close. The ring and its forks are cosmetic.
|
||||
|
||||
This package uses the public mod API's `scripted` weapon behavior, read-only
|
||||
entity snapshots with `ground`, and `damage`, `arc` and `play_sound` commands.
|
||||
All targeting and damage rules live in `scripts/weapon.rhai`. It has no
|
||||
dependencies or overrides, and coexists with the other weapons and Iron Coast.
|
||||
|
||||
The equipment icon ships in **16×16 SD** and **64×64 HD**, alongside the
|
||||
transparent source master and deterministic synthesized audio. The halo and
|
||||
connecting bolts use anchored `arc` commands and the game's renderer at both
|
||||
scales. See [provenance](ART_PROVENANCE.md)
|
||||
for the exact built-in imagegen prompt, asset hashes and reproduction recipes.
|
||||
|
||||
From `RustRaptor/`:
|
||||
|
||||
```sh
|
||||
cargo run --locked --no-default-features -- --mods mods --validate-mods lightning_shield
|
||||
cargo test --locked --no-default-features --test weapons_mods lightning_shield
|
||||
```
|
||||
|
||||
See [weapon pricing and measurements](../../docs/weapon-balance.md),
|
||||
[the API](../../docs/modding-api.md) and
|
||||
[the AI mod checklist](../../docs/mod-authoring-checklist.md).
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"package": "lightning_shield",
|
||||
"tool": "built-in imagegen",
|
||||
"use_case": "stylized-concept",
|
||||
"art": {
|
||||
"art/equipment-generated-source.png": {
|
||||
"sha256": "c0b52bc0ac198f3de77bf4ed562ab1ab4b46b9015c05852043d93ab34826c360",
|
||||
"bytes": 1732192
|
||||
},
|
||||
"art/equipment-hd.png": {
|
||||
"sha256": "62254d2663c27ffd27979276cbef06d14c9f8f972062a2cd7aa83f6e50c0b224",
|
||||
"bytes": 8665
|
||||
},
|
||||
"art/equipment-sd.png": {
|
||||
"sha256": "43801918f5d71ccd5d6798c684b945a0b90b6fe28541450d49adc6bb7eed2c17",
|
||||
"bytes": 710
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"audio/fire.wav": {
|
||||
"sha256": "21466ed125226870afe59a1cbaf1bc7448ae19258d362371e4f9c25768c4e462",
|
||||
"bytes": 4012
|
||||
},
|
||||
"audio/impact.wav": {
|
||||
"sha256": "74c57d492d87b2d4f5d34d52d2925da0bb1664b270ca407c685e63f83109f2db",
|
||||
"bytes": 2690
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 710 B |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"difficulty": 2,
|
||||
"fire_steps": 700,
|
||||
"rate": "70/3 Hz",
|
||||
"target_health": 1000000,
|
||||
"target_size": [
|
||||
32,
|
||||
32
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "aligned_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "aligned_ground",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "off_axis_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "off_axis_ground",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "column",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "cluster",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
176
|
||||
],
|
||||
"damage_per_second": [
|
||||
5.866666666666666
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "close_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "close_ground",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "chain",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
176,
|
||||
176,
|
||||
176,
|
||||
176,
|
||||
0
|
||||
],
|
||||
"damage_per_second": [
|
||||
5.866666666666666,
|
||||
5.866666666666666,
|
||||
5.866666666666666,
|
||||
5.866666666666666,
|
||||
0.0
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "close_cluster",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": false
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
694
|
||||
],
|
||||
"damage_per_second": [
|
||||
23.133333333333333
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "aligned_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": true
|
||||
},
|
||||
{
|
||||
"damage": [
|
||||
176
|
||||
],
|
||||
"damage_per_second": [
|
||||
5.866666666666666
|
||||
],
|
||||
"item": "lightning_shield:equipment",
|
||||
"price": 155000,
|
||||
"scene": "close_air",
|
||||
"seconds": 30.0,
|
||||
"ticks": 700,
|
||||
"with_forward_guns": true
|
||||
}
|
||||
],
|
||||
"method": "Real World::step and shipped scripts; 700 Fire steps (30 seconds); stationary 32x32 surviving targets; no player damage. Reproduce with tests/weapon_balance.rs.",
|
||||
"limitations": "Controlled fixture observations, not a claim of equal campaign difficulty. Original catalog and preview pixels remain outside Git."
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"weapons": [
|
||||
{
|
||||
"id": "lightning_shield:projectile",
|
||||
"name": "Lightning Shield Discharge",
|
||||
"damage": 2,
|
||||
"speed": 0,
|
||||
"cooldown": 8,
|
||||
"behavior": "scripted",
|
||||
"target": "air",
|
||||
"script": "scripts/weapon.rhai"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "lightning_shield:equipment",
|
||||
"name": "Lightning Shield",
|
||||
"description": "Always-equipped reusable proximity weapon. Hold Fire to charge a glowing electric ring that follows your hull and zaps up to four airborne ships within a very short range from its edge for 2 damage each. Fires alongside your other primary guns and selected special; no selection or ammunition needed. Does not absorb damage or stop enemy bullets.",
|
||||
"price": 155000,
|
||||
"max_count": 1,
|
||||
"initial_count": 1,
|
||||
"weapon": "lightning_shield:projectile",
|
||||
"special": false,
|
||||
"consumable": false,
|
||||
"art": {
|
||||
"png": "art/equipment-sd.png",
|
||||
"hd_png": "art/equipment-hd.png",
|
||||
"logical_width": 16,
|
||||
"logical_height": 16,
|
||||
"nearest": true
|
||||
},
|
||||
"script": "scripts/equipment.rhai"
|
||||
}
|
||||
],
|
||||
"sounds": [
|
||||
{
|
||||
"id": "lightning_shield:fire",
|
||||
"name": "Lightning Shield Discharge",
|
||||
"file": "audio/fire.wav",
|
||||
"volume": 68,
|
||||
"pan": 127,
|
||||
"pitch": 128,
|
||||
"priority": 2
|
||||
},
|
||||
{
|
||||
"id": "lightning_shield:impact",
|
||||
"name": "Lightning Shield Contact",
|
||||
"file": "audio/impact.wav",
|
||||
"volume": 54,
|
||||
"pan": 127,
|
||||
"pitch": 128,
|
||||
"priority": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce deterministic PCM16 mono electrical discharge sounds."""
|
||||
import math
|
||||
import struct
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE = "lightning_shield"
|
||||
RATE = 22050
|
||||
|
||||
def render(kind):
|
||||
heavy = PACKAGE == "lightning_cannon"
|
||||
duration = (0.24 if heavy else 0.09) if kind == "fire" else (0.11 if heavy else 0.06)
|
||||
count = int(RATE * duration)
|
||||
seed = 0x5A17C0DE
|
||||
phase = 0.0
|
||||
samples = []
|
||||
for n in range(count):
|
||||
t = n / count
|
||||
seed ^= (seed << 13) & 0xffffffff
|
||||
seed ^= seed >> 17
|
||||
seed ^= (seed << 5) & 0xffffffff
|
||||
seed &= 0xffffffff
|
||||
noise = seed / 2147483648.0 - 1.0
|
||||
frequency = (2100 - 1450 * t) if heavy else (3100 - 1650 * t)
|
||||
phase += 2 * math.pi * frequency / RATE
|
||||
crackle = noise * (0.45 + 0.25 * math.sin(2 * math.pi * 61 * n / RATE) ** 8)
|
||||
tone = math.sin(phase) * 0.24 + math.sin(phase * 1.503) * 0.15
|
||||
if kind == "impact":
|
||||
tone = math.sin(phase * 0.18) * 0.25
|
||||
attack = min(1.0, n / (RATE * 0.002))
|
||||
envelope = attack * (1 - t) ** (2.2 if kind == "fire" else 3)
|
||||
samples.append(round(max(-1, min(1, (crackle + tone) * envelope)) * 21000))
|
||||
return struct.pack("<" + "h" * len(samples), *samples)
|
||||
|
||||
if __name__ == "__main__":
|
||||
output = Path(__file__).resolve().parent / "audio"
|
||||
output.mkdir(exist_ok=True)
|
||||
for kind in ["fire", "impact"]:
|
||||
with wave.open(str(output / (kind + ".wav")), "wb") as wav:
|
||||
wav.setparams((1, 2, RATE, 0, "NONE", "not compressed"))
|
||||
wav.writeframes(render(kind))
|
||||
print(output / (kind + ".wav"))
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "lightning_shield"
|
||||
name = "Lightning Shield"
|
||||
version = "1.0.0"
|
||||
api_version = 1
|
||||
description = "Always-equipped reusable proximity weapon. Hold Fire to charge an electric ring around your hull and zap up to four nearby airborne ships from its edge for a little damage. Fires alongside your forward guns and selected special; no selection or ammunition needed. Does not absorb damage or stop enemy bullets."
|
||||
content = "content.json"
|
||||
scripts = ["scripts/weapon.rhai", "scripts/equipment.rhai"]
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Fit the transparent equipment master to the 16px SD and 64px HD canvases.
|
||||
use image::{Rgba, RgbaImage, imageops};
|
||||
use std::{env, fs, path::Path};
|
||||
fn canvas(source: &RgbaImage, width: u32, height: u32, hd: bool) -> RgbaImage {
|
||||
// Only the solid silhouette defines its fitting box; faint outer glows stay bounded.
|
||||
let mut bounds = (source.width(), source.height(), 0, 0);
|
||||
for (x, y, p) in source.enumerate_pixels() {
|
||||
if p[3] >= 128 {
|
||||
bounds.0 = bounds.0.min(x);
|
||||
bounds.1 = bounds.1.min(y);
|
||||
bounds.2 = bounds.2.max(x + 1);
|
||||
bounds.3 = bounds.3.max(y + 1);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
bounds.2 > bounds.0 && bounds.3 > bounds.1,
|
||||
"source has no opaque silhouette"
|
||||
);
|
||||
let cropped = imageops::crop_imm(
|
||||
source,
|
||||
bounds.0,
|
||||
bounds.1,
|
||||
bounds.2 - bounds.0,
|
||||
bounds.3 - bounds.1,
|
||||
)
|
||||
.to_image();
|
||||
let margin = if hd { 4 } else { 1 };
|
||||
let available = (
|
||||
width.saturating_sub(margin * 2).max(1),
|
||||
height.saturating_sub(margin * 2).max(1),
|
||||
);
|
||||
let factor = (available.0 as f64 / cropped.width() as f64)
|
||||
.min(available.1 as f64 / cropped.height() as f64);
|
||||
let size = (
|
||||
(cropped.width() as f64 * factor).round().max(1.) as u32,
|
||||
(cropped.height() as f64 * factor).round().max(1.) as u32,
|
||||
);
|
||||
let filter = if hd {
|
||||
imageops::FilterType::Lanczos3
|
||||
} else {
|
||||
imageops::FilterType::Nearest
|
||||
};
|
||||
let mut scaled = imageops::resize(&cropped, size.0, size.1, filter);
|
||||
for p in scaled.pixels_mut() {
|
||||
if p[3] < (if hd { 64 } else { 128 }) {
|
||||
*p = Rgba([0, 0, 0, 0]);
|
||||
} else {
|
||||
p[3] = 255;
|
||||
}
|
||||
}
|
||||
let mut out = RgbaImage::new(width, height);
|
||||
imageops::overlay(
|
||||
&mut out,
|
||||
&scaled,
|
||||
((width - size.0) / 2) as i64,
|
||||
((height - size.1) / 2) as i64,
|
||||
);
|
||||
out
|
||||
}
|
||||
fn main() {
|
||||
let args: Vec<_> = env::args_os().collect();
|
||||
assert_eq!(
|
||||
args.len(),
|
||||
3,
|
||||
"usage: prepare_art EQUIPMENT_SOURCE PACKAGE_DIRECTORY"
|
||||
);
|
||||
let output = Path::new(&args[2]).join("art");
|
||||
fs::create_dir_all(&output).unwrap();
|
||||
let source = image::open(&args[1]).unwrap().into_rgba8();
|
||||
fs::copy(&args[1], output.join("equipment-generated-source.png")).unwrap();
|
||||
for (mode, scale) in [("sd", 1), ("hd", 4)] {
|
||||
let sprite = canvas(&source, 16 * scale, 16 * scale, scale == 4);
|
||||
let path = output.join(format!("equipment-{mode}.png"));
|
||||
sprite.save(&path).unwrap();
|
||||
println!("{}x{} {}", sprite.width(), sprite.height(), path.display());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Pickups use the same reusable equipment as the shop.
|
||||
fn on_pickup(event, state, random) {
|
||||
let recovered = if state.contains("recovered") { state.recovered % 100000 + 1 } else { 1 };
|
||||
#{ commands: [#{ op: "objective", key: "lightning_shield:recovered", value: recovered }],
|
||||
state: #{ recovered: recovered } }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// This is an ordinary non-special weapon: it runs only while Fire is held.
|
||||
// Measure range to the nearest point of an enemy hull, not its center, so large
|
||||
// ships can be zapped at the edge too. The reach is only 12px past our 32px hull.
|
||||
fn on_fire(event, state, random) {
|
||||
if event.hostile {
|
||||
return #{ commands: [], state: state };
|
||||
}
|
||||
let bursts = if state.contains("bursts") { state.bursts % 100000 + 1 } else { 1 };
|
||||
let hits = if state.contains("hits") { state.hits } else { 0 };
|
||||
let commands = [];
|
||||
let struck = [];
|
||||
let from_x = event.player.x + 16;
|
||||
let from_y = event.player.y + 16;
|
||||
|
||||
// An electric halo just outside the hull follows the ship throughout the
|
||||
// recharge. Both endpoints use the public arc anchoring API; it is visual
|
||||
// only, so even an empty pulse still lights the ring without playing audio.
|
||||
let ring = [[19, 0], [18, 7], [13, 13], [7, 18], [0, 19], [-7, 18],
|
||||
[-13, 13], [-18, 7], [-19, 0], [-18, -7], [-13, -13],
|
||||
[-7, -18], [0, -19], [7, -18], [13, -13], [18, -7]];
|
||||
for i in 0..ring.len() {
|
||||
let a = ring[i];
|
||||
let b = ring[(i + 1) % ring.len()];
|
||||
commands.push(#{ op: "arc", x: from_x + a[0], y: from_y + a[1],
|
||||
to_x: from_x + b[0], to_y: from_y + b[1],
|
||||
anchor: 0, to_anchor: 0, ticks: 8 });
|
||||
}
|
||||
|
||||
// Bound each pulse's work and sound: at most four close airborne ships.
|
||||
for target in 0..4 {
|
||||
let selected = #{};
|
||||
let nearest = 1000000;
|
||||
for enemy in event.entities {
|
||||
if enemy.health <= 0 || enemy.ground || struck.contains(enemy.entity) {
|
||||
continue;
|
||||
}
|
||||
let x = max(enemy.x, min(from_x, enemy.x + enemy.width - 1));
|
||||
let y = max(enemy.y, min(from_y, enemy.y + enemy.height - 1));
|
||||
if x < 0 || x >= 320 || y < 0 || y >= 200 { continue; }
|
||||
let dx = x - from_x;
|
||||
let dy = y - from_y;
|
||||
let distance = dx * dx + dy * dy;
|
||||
if distance <= 28 * 28 && distance < nearest {
|
||||
selected = #{ entity: enemy.entity, x: x, y: y };
|
||||
nearest = distance;
|
||||
}
|
||||
}
|
||||
if !selected.contains("entity") { break; }
|
||||
// Start at the halo point facing this hull, not at the player's center.
|
||||
let dx = selected.x - from_x;
|
||||
let dy = selected.y - from_y;
|
||||
let origin = ring[0];
|
||||
let best = -1000000;
|
||||
for point in ring {
|
||||
let facing = point[0] * dx + point[1] * dy;
|
||||
if facing > best { best = facing; origin = point; }
|
||||
}
|
||||
commands.push(#{ op: "arc", x: from_x + origin[0], y: from_y + origin[1],
|
||||
to_x: selected.x, to_y: selected.y,
|
||||
anchor: 0, to_anchor: selected.entity, ticks: 3 });
|
||||
commands.push(#{ op: "damage", entity: selected.entity, amount: 2 });
|
||||
struck.push(selected.entity);
|
||||
hits = hits % 100000 + 1;
|
||||
}
|
||||
if struck.len() > 0 {
|
||||
commands.push(#{ op: "play_sound", sound: "lightning_shield:fire" });
|
||||
commands.push(#{ op: "play_sound", sound: "lightning_shield:impact" });
|
||||
}
|
||||
#{ commands: commands, state: #{ bursts: bursts, hits: hits } }
|
||||
}
|
||||
Reference in new issue
Block a user