Skip to content

jengine-shooter

Space-shooter mechanics: a wrapping field, inertial ships, shots, splitting rocks, saucers and an AI pilot.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-shooter
Version0.1.0
Entry pointjengine_shooter::ShooterPackage
Script namespaceShooter
Listed byprojects/arcade-classics, projects/engine-test-lab, projects/shooter-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-shooter", "source": { "type": "path", "path": "../../packages/jengine-shooter" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Shooter == nil then return end.

Scene keyRust type
shooter_fieldShooterFieldpackage only: needs a v2 scene
shooter_bodyShooterBodypackage only: needs a v2 scene
shooter_shipShooterShippackage only: needs a v2 scene
shooter_saucerShooterSaucerpackage only: needs a v2 scene
shooter_formationShooterFormationpackage only: needs a v2 scene
shooter_bunkerShooterBunkerpackage only: needs a v2 scene
shooter_pathShooterPathpackage only: needs a v2 scene
danmaku_emitterDanmakuEmitterpackage only: needs a v2 scene
bullet_poolBulletPoolpackage only: needs a v2 scene
shooter_terrainShooterTerrainpackage only: needs a v2 scene
shooter_podShooterPodpackage only: needs a v2 scene
shooter_linkShooterLinkpackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Shooter.ship_fire(e)ship_fire
Shooter.ship_drive(e, turn, thrust)ship_drive
Shooter.ship_configure(e, def)ship_configure
Shooter.ship_special(e)ship_special
Shooter.ship_hyperspace(e)ship_hyperspace
Shooter.ship_respawn(e, x, y)ship_respawn
Shooter.ship_set_ai(e, on)ship_set_ai
Shooter.ship_set_input(e, on)ship_set_input
Shooter.saucer_launch(e, side, y)saucer_launch
Shooter.saucer_set_aim(e, error)saucer_set_aim
Shooter.rocks_spawn(field, count, size)rocks_spawn
Shooter.rock_spawn(field, x, y, size)rock_spawn
Shooter.field_set_scroll(field, x, y)field_set_scroll
Shooter.ship_charge(e, on)ship_charge
Shooter.ship_set_missiles(e, count)ship_set_missiles
Shooter.ship_set_speed(e, speed)ship_set_speed
Shooter.body_set_active(e, on)body_set_active
Shooter.pod_toggle(e)pod_toggle
Shooter.pod_set_level(e, level)pod_set_level
Shooter.ship_set_focus(e, on)ship_set_focus
Shooter.emitter_set_phase(e, phase)emitter_set_phase
Shooter.emitter_set_scale(e, speed, rate)emitter_set_scale
Shooter.ship_set_shots(e, count, spread)ship_set_shots
Shooter.body_set_hp(e, hp, shield)body_set_hp
Shooter.body_reset(e, x, y, hp)body_reset
Shooter.emitter_set_active(e, on)emitter_set_active
Shooter.path_set(e, points, end)path_set
Shooter.bullets_clear(field, x, y, radius)bullets_clear
Shooter.shots_clear(field)shots_clear
Shooter.formation_reset(e, drop)formation_reset
Shooter.formation_pause(e, seconds)formation_pause
Shooter.bunker_reset(e)bunker_reset
Shooter.shooter_clear(field)shooter_clear
Shooter.body_configure(e, def)body_configure
Shooter.emitter_configure(e, def)emitter_configure
Shooter.body_set_polarity(e, polarity)body_set_polarity
Shooter.emitter_set_polarity(e, polarity)emitter_set_polarity
Shooter.ship_release(e, count)ship_release
Shooter.ship_set_advise(e, on)ship_set_advise
Shooter.bullets_fire(field, x, y, pattern, polarity, target)bullets_fire
  • projects/arcade-classics/assets/scenes/asteroids.scene
  • projects/arcade-classics/assets/scenes/bullethell.scene
  • projects/arcade-classics/assets/scenes/invaders.scene
  • projects/arcade-classics/assets/scenes/ironnebula.scene
  • projects/arcade-classics/assets/scenes/orbit.scene
  • projects/engine-test-lab/assets/scenes/73-shooter.scene
  • projects/engine-test-lab/assets/scenes/74-invaders.scene
  • projects/engine-test-lab/assets/scenes/77-danmaku.scene
  • projects/engine-test-lab/assets/scenes/78-scroller.scene
  • projects/shooter-classics/assets/scenes/spark.scene

From packages/jengine-shooter/README.md.

Space-shooter mechanics:

  • a field that wraps, with bodies that drift, spin and collide in it

  • ships that turn, thrust and fire, or slide along a lane as a cannon

  • rocks that break into smaller rocks

  • saucers that cross the field, shooting or not

  • a marching invader formation

  • bunkers that wear away

  • bullet patterns (“danmaku”) and a pool that moves thousands of bullets

  • ships that fly eight ways with a slow, focused mode, and enemies on flight paths

  • a field that scrolls over tile terrain, a charged beam, homing missiles, a detachable pod, and bosses built of linked parts

  • an AI for every kind of ship

  • polarity (Ikaruga): light and dark bodies, shots and bullets - a bullet of your own colour is absorbed, the other colour’s kills, and the other colour takes double damage - plus homing lasers released in a salvo and an autopilot that knows the rule

Asteroids, Space Invaders, a bullet hell, an R-Type-style scrolling shooter and an Ikaruga are built on it; Galaxian-style divers are the obvious next step.

Used by projects/arcade-classics (Asteroids, Space Invaders, Starfall, Iron Nebula, Pale Orbit), projects/shooter-classics (Dual Spark) and Engine Test Lab scenes 73, 74, 77 and 78.

The package spawns and despawns the rocks and shots itself, sprites included, and it moves and collides everything. A game’s script is the rules around it: score, lives, waves, when a saucer comes, and everything the player sees and hears happen, all from the events the field publishes.

One per scene, centred on its entity. Bodies that wrap do it inside this rectangle; bodies that don’t are removed when they leave it. It also says what rocks look like, and it is where each frame’s events are published.

"shooter_field": {
"half_width": 640, "half_height": 360, "safe_radius": 150, "seed": 1979,
"rock_color": [0.86, 0.93, 1, 1], "rock_z": 2, "split_into": 2,
"rock_sizes": [
{ "radius": 12, "draw_size": 30, "speed": [95, 175], "spin": 2.4, "points": 100, "textures": ["sprites/vector/rock_s1.png"] },
{ "radius": 25, "draw_size": 60, "speed": [65, 125], "spin": 1.5, "points": 50, "textures": ["..."] },
{ "radius": 48, "draw_size": 116, "speed": [35, 80], "spin": 0.8, "points": 20, "textures": ["..."] }
]
}
  • rock_sizes[0] is the smallest. A rock of size n breaks into split_into rocks of size n - 1, each in a random direction; the smallest just breaks.
  • A size’s textures are picked from at random. Leave them out and it uses the field’s rock_textures. Outline art wants one set per size, so its lines are equally thick on screen.
  • safe_radius: new rocks appear on the edges at least 1.5 times this far from anything they could hurt, and a ship coming back waits until nothing that can hurt it is this close.

Anything that moves and collides: ships, saucers, rocks, shots.

"shooter_body": { "radius": 13, "layer": "ship", "hits": ["rock", "saucer", "enemy_shot"],
"drag": 0.35, "max_speed": 520, "active": false }
  • Layers decide what hits what. Two bodies collide when either one’s layer is in the other’s hits. They are measured the short way round the field only when both wrap. Each then takes the other’s damage from its hp, and one at zero is destroyed.
  • damage is dealt on every frame two bodies touch, which suits a shot that ends on the hit. Something that lingers - a bomb’s blast - should use dps instead, damage per second of contact, so it burns the same at any frame rate.
  • A shot never hits whoever fired it.
  • A body with shield seconds left can’t be hit, and blinks.
  • A body the package spawned (a rock, a shot) is despawned when destroyed. One from the scene (a ship, a saucer) goes inactive instead: hidden, still, out of every collision, until a command brings it back. Its sprite’s alpha is restored when it does.
  • wrap: false bodies are removed when they leave the field. life is seconds to live (0 is forever); a shot fades out in its last 0.15 s.

A body that turns, thrusts and fires. Its sprite points up at rotation 0.

"shooter_ship": { "turn_speed": 4.6, "thrust": 480, "fire_cooldown": 0.12, "max_shots": 4,
"flame": "Flame", "flame_offset": 18, "respawn_shield": 2.5,
"shot": { "speed": 640, "life": 0.95, "texture": "sprites/vector/shot.png", "size": 8,
"layer": "shot", "hits": ["rock", "saucer"], "inherit": 1.0 } }
  • Steering: with input on, an input_axis on the same entity steers it: x turns (positive is clockwise) and positive y thrusts. Bind it to actions in the scene, as the arcade does with move_left / move_right / thrust.
  • Firing and hyperspace are commands (Shooter.ship_fire, Shooter.ship_hyperspace), so the script decides which button does what: tap to fire, or call it every frame for autofire.
  • flame names an entity that is drawn behind the ship, flickering, while it thrusts.
  • With ai on, the package’s pilot flies it:
    • It dodges whatever will pass closest soonest.
    • It hyperspaces when a hit is a frame or two away.
    • Otherwise it leads the nearest target and fires once lined up.

"mode": "cannon" makes a ship Space Invaders’ laser base. It slides along lane (world x) at speed, steered by its input_axis x, and fires straight up. It has no hyperspace. respawn_clear: false brings it back at once, since a cannon under a low formation would otherwise wait forever. With ai it uses the cannon pilot:

  • it steps out from under bombs about to land on it
  • it slides under the target it can reach soonest (the mystery ship first, then the lowest invaders)
  • it fires when it’s under one

"mode": "pilot" is a bullet hell’s ship. It flies eight ways at speed, steered by its input_axis, and stays inside the field. It fires straight up, shot_count shots at a time, fanned across shot_spread degrees and shot_gap px apart:

"shooter_ship": { "mode": "pilot", "speed": 290, "focus_speed": 120, "fire_cooldown": 0.075, "max_shots": 80,
"shot_count": 2, "shot_spread": 14, "shot_gap": 12, "respawn_shield": 3, "shot": { "..." } }
  • Shooter.ship_set_focus(e, true) slows it to focus_speed and gathers its fan into a narrow column, for threading a pattern.
  • Shooter.ship_set_shots(e, count, spread) changes the fan: power levels.
  • Give its body a tiny radius (the arcade’s is 3). The genre’s rule is that only the dot at the ship’s heart can be hit.
  • facing is the way it flies and fires, in degrees: 90 (up) for a vertical shooter, 0 (right) for a horizontal one. The sprite is not turned; draw it facing that way.
  • With ai, the pilot steers away from the bullets that will be nearest in a moment and from terrain, focuses when bullets are close, and otherwise lines up with the nearest target ahead, well back on the field, firing all the time.

A pilot can also carry:

  • A charged beam. With a charge_time, Shooter.ship_charge(e, true) charges and Shooter.ship_charge(e, false) lets go. At a quarter charge or more it fires beam, with its damage, hit points and size scaled by the charge. A shot with more than 1 hp pierces, and it strikes each body once however many frames it takes to pass through, so a beam’s damage does not depend on the frame rate. ShipState.charge is 0..1, for a meter.
  • Missiles. missiles (Shooter.ship_set_missiles) are fired as a fanned salvo every missile_cooldown while the ship fires. Give missile a homing rate in degrees a second and each one turns toward the nearest body it can hit.

Crosses the field once from the side it’s launched from, zig-zagging, and fires at target every fire_interval.

"shooter_saucer": { "speed": 135, "turn_interval": 1.2, "fire_interval": 0.9, "aim_error": 3.2,
"target": "Ship", "shot": { "layer": "enemy_shot", "hits": ["ship", "rock"], "..." } }

aim_error is its worst miss, in radians. π or more is the classic big saucer, shooting anywhere; a small one is the small saucer, which doesn’t miss. With no live target it fires at random. Once it has crossed it goes inactive and reports escaped.

turn_interval: 0 flies straight and fire_interval: 0 never fires: that’s Space Invaders’ mystery ship, which is worth whatever the script says.

A ship’s or saucer’s shot (and a formation’s bomb) is a ShotConfig:

  • speed, life, radius, damage
  • layer and hits
  • inherit: how much of the shooter’s velocity it keeps
  • how it looks: texture, size, color, z_order
  • wrap: false ends the shot at the field’s edge instead of wrapping, and with life: 0 it lives until then
  • region names an atlas region of texture; the shot then draws at the region’s own size

Space Invaders’ fleet: one entity that spawns a grid of invader bodies around its position and marches them.

"shooter_formation": {
"cols": 11, "rows": 5, "spacing": [48, 40], "row_kinds": [0, 1, 1, 2, 2],
"kinds": [ { "name": "Squid", "texture": "atlases/invaders.png", "regions": ["squid_a", "squid_b"],
"points": 30, "radius": 11 }, "..." ],
"step_x": 8, "step_y": 20, "step_per_invader": 0.0167, "lane": [-332, 332], "land_y": -268,
"bomb": { "layer": "bomb", "hits": ["ship"], "wrap": false, "..." },
"bomb_interval": [0.5, 1.4], "max_bombs": 3, "aimed_chance": 0.35, "target": "Cannon"
}
  • The march: each step moves the whole grid step_x sideways. When the next step would cross lane, it drops step_y and turns back instead.
  • Speed: a step takes step_per_invader for each invader alive. That’s the arcade’s one frame per invader, so the last one races.
  • Animation: each step switches every invader to its kind’s next region. A kind with no texture draws as a square.
  • Bombs: they fall from the bottom invader of a random column. With probability aimed_chance the column is the one nearest target instead. At most max_bombs are in the air at once.
  • Landing: an invader reaching land_y has landed. The formation stops and reports it.
  • Invaders are bodies on layer with hits, so the cannon’s shot kills them through the usual collision rule, and points travels on the destroyed event.

A shield of cols x rows cells of cell_size, centred on its entity, cut to layout (top row first; # is a cell). It’s drawn as one sprite per horizontal run of standing cells, about 30 per bunker rather than one per cell. A body on a layer in absorbs (shots, bombs) that touches a cell is destroyed and blows a ragged hole of radius blast cells. One on a layer in eroded_by (invaders) wears away the cells it touches and carries on.

Bullet patterns: danmaku_emitter and bullet_pool

Section titled “Bullet patterns: danmaku_emitter and bullet_pool”

Bullets are not bodies. A body is tested against every other body, which is right for a few ships and rocks and hopeless for three thousand bullets. The pool tests each bullet against its target_layer only, one distance check each, and draws them with one sprite entity per slot, made once and reused: a free slot is hidden, not despawned. (The renderer skips a fully transparent sprite, so a hidden slot costs nothing to draw.)

"bullet_pool": { "max": 3000, "texture": "atlases/bullethell.png", "z_order": 20,
"graze_radius": 16, "target_layer": "ship", "margin": 40 }
  • One pool per scene. A bullet is dropped margin px past the field’s edge. When all max slots are in use a new bullet is not fired and bullets_dropped counts it.
  • A bullet that touches a target_layer body costs it one hp and is removed. One that passes within graze_radius of it first counts once as a graze, which a game scores.
  • A body whose shield is up is not hit (a bomb, a respawn).

An emitter goes on any entity; bodies carry them (a fairy, the boss). Its phases are lists of patterns, and the phase in use fires all of its patterns at once: a boss’s cards, or a fairy’s variants that a script picks per wave. An empty phase fires nothing.

"danmaku_emitter": { "target": "Player", "phases": [
[ { "kind": "ring", "count": 20, "interval": 0.9, "rotate": 9,
"bullet": { "region": "orb_blue", "radius": 6, "speed": 130 } },
{ "kind": "aimed", "count": 5, "spread": 40, "interval": 1.8, "delay": 1.2,
"bullet": { "region": "rice_white", "radius": 3.5, "speed": 200, "orient": true } } ] ] }

A pattern:

FieldMeaning
kindring (all round), fan (across spread degrees about angle), aimed (a fan at target), random (count anywhere in the spread cone, at 70-130% speed)
count, spread, anglehow many per volley; the cone; the direction in degrees, 0 right and 90 up (default 270, down)
aimpoint angle at the target, for any kind
delay, interval, durationseconds before the first volley, between volleys, and in all (0: until the phase changes)
rotatedegrees the direction turns after each volley: spirals
layers, speed_stepfire each volley this many times, each speed_step faster: rings in layers
bulletregion (in the pool’s texture), color, radius, speed, accel (px/s², may be negative), max_speed, curve (degrees per second it turns), orient (turn the sprite to face its way)

An emitter fires only while its active is on and, if it has a body, the body is active. speed_scale and rate_scale (Shooter.emitter_set_scale) make it faster and denser: a second loop.

Moves an entity through points, each [dx, dy, speed], relative to where it was when the path started. end is "stop", "loop" or "despawn" (removed at the last point, with a left event). A point that barely moves at a slow speed is a wait: [0, -160, 170], [0, -165, 1] flies in and holds for five seconds. Shooter.path_set gives any entity a new path, starting from where it is.

Section titled “A scrolling shooter: scroll, terrain, pod, links”

The scroll. A field’s scroll (px/s, or Shooter.field_set_scroll) moves its entity each frame, and with it the entities named in carry (the camera) and every body with ride on. So the player and enemies flying in formation stay put on screen while terrain - and turrets fixed to it, which do not ride - goes by. A path on a riding body moves with it; a pool with ride carries its bullets too. margin lets a body that does not wrap sit that far past the edge before it is removed, so turrets can be spawned just off screen. FieldState.scrolled is how far it has gone.

Terrain. shooter_terrain on an entity with an engine tilemap makes the tiles of its layers solid. A body whose layer is in solid_for that touches one is destroyed (a shot just ends); pool bullets end there too with bullets on. Tiles of the breakable layer are solid and break under shots, reporting tile_broken with points. A sky of parallax sprites sorted below the tilemap draws beneath it.

"shooter_terrain": { "layers": ["terrain"], "breakable": "blocks",
"solid_for": ["ship", "shot", "enemy_shot"], "bullets": true, "break_points": 50 }

The pod (shooter_pod), R-Type’s Force. It docks offset px in front of its owner ship, or behind, and fires when the ship does - level x 2 - 1 shots fanned over shot_spread, backwards while behind. Name its body’s layer in the pool’s blockers and it soaks bullets; give its body a dps and it burns what it touches. Shooter.pod_toggle launches it along the ship’s facing (it stops at reach of the way to the edge and drifts to line up with the ship) and calls it back; it docks on the side it meets, with a pod_docked event. A pod left behind when its ship is destroyed stays where it was. Level 0 is no pod. PodState has state and level.

Linked parts (shooter_link). Kept at offset from the entity named parent, turning round it at orbit degrees a second: armour plates that soak shots (a layer the shots hit, lots of hp), guns that can be shot off. With die_with_parent (the default) a part is destroyed with its parent.

Every body, shot and bullet can be light (1) or dark (2). Polarity 0 - the default everywhere - is “none”, so a game that never names one plays exactly as before.

  • shooter_body.polarity. A pool bullet of the body’s own polarity is absorbed: it is removed, does no harm (through a shield too), and is reported as an absorbed event (name, count this frame, x, y). The other colour - and a bullet of no polarity - hits as before.
  • bullet.polarity in a pattern, or the emitter’s polarity for bullets of none: one pattern fired in either colour. The pool’s polarity_suffix (["", "_w", "_b"]) draws a light orb as the region orb_w and a dark one as orb_b.
  • A shot’s polarity is the firing body’s when its ShotConfig says 0, so a ship that changes colour fires its new colour at once. polarity_regions and polarity_colors (indexed by polarity) give a weapon its light and dark looks; trail gives a shot a ribbon (the engine’s trail component), coloured by polarity_colors.
  • shooter_field.polarity_damage: what a hit between two bodies of opposite polarities is worth (2 is the genre’s double damage; 1, the default, changes nothing). report_struck reports a struck event (name, hp, by, double) when a shot hits a body that lives - something to flash.
  • A destroyed event carries polarity (what died) and by_polarity (what killed it), which is all a script needs to keep a chain.
  • Homing lasers: a pilot’s release shot. Shooter.ship_release(e, n) fires n of them fanned out to the sides and back, each sent after its own target in reach (nearest first, round again when there are fewer targets than lasers); a laser whose target dies homes on the nearest.
  • The polarity pilot: a pilot with a polarity flown by ai (or advising, with advise) tries the eight directions and standing still against where every bullet that can hurt it - and every body it would die touching - will be over the next half second, after deciding whether the other colour would be safer where it is. It soaks up its own colour a little on purpose. With advise on and ai off it flies nothing and publishes its choice as ShipState.advice - { x, y, fire, switch }, x and y each -1, 0 or 1 - for a script to play through real input (an autopilot whose advice drives a test). A change of colour, by anyone, holds off the next for switch_gap seconds.
  • A pilot’s Shooter.ship_drive(e, x, y) steers it this frame (a direction at most 1 long), over its input_axis: a script flying it from the move actions, the mouse or the advice.

A pool of scene enemies, configured from data. A package component survives only on a v2 scene entity, so a game authors slots and makes each a particular enemy when it spawns: Shooter.body_configure(e, def) and Shooter.emitter_configure(e, def) lay a row’s fields over the component (only the fields named change; Lua’s float numbers fit integer fields). Shooter.bullets_fire(field, x, y, pattern, polarity, target) fires one volley of a pattern at a point - an enemy that bursts as it dies.

CallDoes
Shooter.ship_fire(e)fire, if the cooldown and max_shots allow
Shooter.ship_hyperspace(e)vanish for hyperspace_time, then reappear somewhere random
Shooter.ship_respawn(e, x, y)come back at world (x, y) once nothing dangerous is near, shielded
Shooter.ship_set_ai(e, on), Shooter.ship_set_input(e, on)let the AI fly it; read its input_axis or not
Shooter.saucer_launch(e, side, y)cross from the left (side -1) or right (1) at world height y
Shooter.saucer_set_aim(e, error)its worst miss, radians
Shooter.rocks_spawn(field, count, size)new rocks of size (1 is the smallest) on the edges, clear of danger
Shooter.rock_spawn(field, x, y, size)one rock at world (x, y)
Shooter.shooter_clear(field)remove every rock, shot and invader, and send saucers away
Shooter.shots_clear(field)remove every shot and bomb in flight
Shooter.formation_reset(e, drop)a new grid, drop px below the formation’s position
Shooter.formation_pause(e, seconds)hold the march and the bombs (a cannon was hit)
Shooter.bunker_reset(e)restore a bunker’s full shape
Shooter.ship_set_focus(e, on)a pilot’s focused mode
Shooter.ship_set_shots(e, count, spread)a pilot’s fan of shots
Shooter.emitter_set_phase(e, phase)which set of patterns fires (from 0); a new phase starts its patterns afresh
Shooter.emitter_set_active(e, on)fire or hold
Shooter.emitter_set_scale(e, speed, rate)bullet speed and fire rate multipliers
Shooter.path_set(e, points, end)a new flight path from where the entity is; points is a list of {dx, dy, speed}
Shooter.bullets_clear(field, x, y, radius)remove every bullet within radius of world (x, y), all of them with 0, and report bullets_cleared
Shooter.body_set_hp(e, hp, shield)set either or both; hp 0 destroys the body next frame (a boss’s card timing out)
Shooter.body_reset(e, x, y, hp)bring a body back at (x, y) with hp, active, still, and off any path
Shooter.body_set_active(e, on)take a body out of play, hidden, or put it back
Shooter.field_set_scroll(field, x, y)the scroll, px/s
Shooter.ship_charge(e, on)charge the beam, or let it go
Shooter.ship_set_missiles(e, count)missiles per salvo, 0 for none
Shooter.ship_set_speed(e, speed)a pilot’s speed
Shooter.pod_toggle(e)launch a docked pod, or call a flying one back
Shooter.pod_set_level(e, level)the pod’s level; 0 is none
Shooter.body_set_polarity(e, p), Shooter.emitter_set_polarity(e, p)0 none, 1 light, 2 dark
Shooter.body_configure(e, def), Shooter.emitter_configure(e, def)lay a table of fields over the component (a pool slot becoming an enemy)
Shooter.ship_release(e, count)loose count homing lasers (the ship’s release shot)
Shooter.ship_set_advise(e, on)publish the AI’s choice in ShipState.advice instead of, or as well as, flying it
Shooter.bullets_fire(field, x, y, pattern, polarity, target)one volley of pattern at (x, y); target names what an aimed one aims at

The field’s FieldState, rewritten every frame. Read it from a script that runs every frame (tick_rate = 60), since events is replaced, not appended:

FieldMeaning
events{ kind, name, x, y, ... } - also beam (level), missiles (count), tile_broken (points), pod_docked (side) - where kind is one of: destroyed (adds layer, points, size, by, by_layer), fired, hyperspace (arrived false going, true coming back), respawned, escaped, step (a formation marched; note counts 0-3 round, alive), landed, cleared (a formation has no invaders left), bunker_hit (layer of what hit it), hit (a pool bullet hit a body that survived; hp), graze (count this frame), volleys (count of volleys emitters fired this frame, one event: enough for a sound), bullets_cleared (count, and up to 300 points [x, y] where they were), left (an entity reached the end of a "despawn" path), absorbed (count bullets of its own colour a body soaked up), struck (with report_struck), released (count lasers), polarity (the AI changed colour)
rocksrocks left
shotsshots in flight
bullets, grazes, bullets_droppedpool bullets in the air; grazes so far; bullets not fired because the pool was full
scrolled, scrolled_yhow far the field has scrolled

A destroyed event’s fields:

  • size is a rock’s size, and 0 for anything else.
  • by names whoever fired the killing shot, or whatever was collided with, so by == "Ship" is the player scoring.
  • Shots aren’t reported as destroyed; what they hit is.

Each ship’s ShipState has these fields:

  • alive: not destroyed; in hyperspace counts as alive.
  • active, waiting (to respawn), thrusting, shield, shots, x, y, charge (0..1), missiles, speed, polarity.
  • advice: the polarity pilot’s choice while advise is on, else nil.

Each saucer’s SaucerState has active. An entity with an emitter and a body has EmitterState: hp, active, shield, phase (a boss’s health bar).

A body killed by a pool bullet is reported destroyed with by and by_layer both "bullet".

One system at ScriptExec, after the frame’s script commands, so a shot fires the frame its button is pressed. Like every package it runs natively and in the editor viewport, and not in web builds, which host no packages yet.

  • src/sim.rs: covers heading and rotation, wrapping and shortest distances, target leading, and the pilot turning, firing, dodging and hyperspacing.
  • tests/asteroids_scene.rs: the arcade’s Asteroids scene.
    • The AI breaks every rock size for a minute, with every kill credited and every rock staying in the field.
    • A player’s thrust, turn and shot move the ship and split a rock.
    • A saucer crosses, fires and leaves.
    • A ship waits to respawn until the centre is clear, then arrives shielded.
  • src/invaders.rs: covers slot layout, bunker shapes, their runs and a blast.
  • tests/invaders_scene.rs: the arcade’s Space Invaders scene.
    • The formation marches, animates, drops at the edge and turns, with the march notes cycling.
    • The last invader steps every frame.
    • The cannon AI shoots invaders for a minute while bunkers wear away.
    • A bomb is absorbed by a bunker and leaves a hole, not a wipe.
    • A low formation lands.
    • The mystery ship crosses straight without firing.
  • src/danmaku.rs: a ring’s count and spacing, an aimed fan centred on its target, a spiral turning, and a full pool dropping bullets and reusing cleared slots.
  • tests/bullethell_scene.rs: the arcade’s Starfall scene.
    • Every prefab loads with real package components.
    • The AI survives the boss’s first card, grazes, and wears her down.
    • A fairy flies its path, fires, and is dropped once it leaves.
    • A bomb clears the screen and kills the fairies under it.
    • An item is picked up at no cost to the ship.
    • The pool carries over 1,500 bullets, one sprite per slot.
    • A path stops at its last point.
  • src/polarity.rs: a bullet of your colour absorbed and the other killing; no polarity hitting as before; an emitter colouring bullets of none (and a bullet’s own polarity winning); double damage only between opposite colours and only when set; a salvo spread over the targets; Lua’s floats fitting integer fields; the pilot changing colour when surrounded; and two bodies that do not wrap never meeting across an edge (a boss leaving the top of a vertical field used to strike the ship at the bottom).
  • src/scroller.rs: which tiles a circle touches (and that outside the map is air), and a pod launching from the side it sits on.
  • tests/nebula_scene.rs: the arcade’s Iron Nebula scene.
    • The scene and every prefab load with real components.
    • The scroll carries the camera and the riders and leaves the terrain’s turrets.
    • Shots break blocks; the floor destroys the ship.
    • The AI flies the whole stage through the terrain.
    • A charged beam pierces two cruisers, striking each once.
    • The pod blocks a stream of bullets, launches, and docks again.
    • Missiles find a target the main gun misses.
    • The armour turns round the core and falls with it.
  • tests/test_lab_scene.rs: test-lab scenes 73 (whose field is off-centre), 74, 77 (every pattern, against the dodging AI) and 78 (the AI over scrolling terrain, the labels riding along).