jengine-shooter
Space-shooter mechanics: a wrapping field, inertial ships, shots, splitting rocks, saucers and an AI pilot.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-shooter |
| Version | 0.1.0 |
| Entry point | jengine_shooter::ShooterPackage |
| Script namespace | Shooter |
| Listed by | projects/arcade-classics, projects/engine-test-lab, projects/shooter-classics |
Using it
Section titled “Using it”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.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
shooter_field | ShooterField | package only: needs a v2 scene |
shooter_body | ShooterBody | package only: needs a v2 scene |
shooter_ship | ShooterShip | package only: needs a v2 scene |
shooter_saucer | ShooterSaucer | package only: needs a v2 scene |
shooter_formation | ShooterFormation | package only: needs a v2 scene |
shooter_bunker | ShooterBunker | package only: needs a v2 scene |
shooter_path | ShooterPath | package only: needs a v2 scene |
danmaku_emitter | DanmakuEmitter | package only: needs a v2 scene |
bullet_pool | BulletPool | package only: needs a v2 scene |
shooter_terrain | ShooterTerrain | package only: needs a v2 scene |
shooter_pod | ShooterPod | package only: needs a v2 scene |
shooter_link | ShooterLink | package only: needs a v2 scene |
Script commands
Section titled “Script commands”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.
| Call | Flat 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 |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/asteroids.sceneprojects/arcade-classics/assets/scenes/bullethell.sceneprojects/arcade-classics/assets/scenes/invaders.sceneprojects/arcade-classics/assets/scenes/ironnebula.sceneprojects/arcade-classics/assets/scenes/orbit.sceneprojects/engine-test-lab/assets/scenes/73-shooter.sceneprojects/engine-test-lab/assets/scenes/74-invaders.sceneprojects/engine-test-lab/assets/scenes/77-danmaku.sceneprojects/engine-test-lab/assets/scenes/78-scroller.sceneprojects/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.
The field: shooter_field
Section titled “The field: shooter_field”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 sizenbreaks intosplit_intorocks of sizen - 1, each in a random direction; the smallest just breaks.- A size’s
texturesare picked from at random. Leave them out and it uses the field’srock_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.
Bodies: shooter_body
Section titled “Bodies: shooter_body”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
layeris in the other’shits. They are measured the short way round the field only when both wrap. Each then takes the other’sdamagefrom itshp, and one at zero is destroyed. damageis dealt on every frame two bodies touch, which suits a shot that ends on the hit. Something that lingers - a bomb’s blast - should usedpsinstead, damage per second of contact, so it burns the same at any frame rate.- A shot never hits whoever fired it.
- A body with
shieldseconds 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: falsebodies are removed when they leave the field.lifeis seconds to live (0 is forever); a shot fades out in its last 0.15 s.
Ships: shooter_ship
Section titled “Ships: shooter_ship”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
inputon, aninput_axison 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 withmove_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. flamenames an entity that is drawn behind the ship, flickering, while it thrusts.- With
aion, 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.
Cannon mode
Section titled “Cannon mode”"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
Pilot mode
Section titled “Pilot mode”"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 tofocus_speedand 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. facingis 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 andShooter.ship_charge(e, false)lets go. At a quarter charge or more it firesbeam, with its damage, hit points and size scaled by the charge. A shot with more than 1hppierces, 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.chargeis 0..1, for a meter. - Missiles.
missiles(Shooter.ship_set_missiles) are fired as a fanned salvo everymissile_cooldownwhile the ship fires. Givemissileahomingrate in degrees a second and each one turns toward the nearest body it can hit.
Saucers: shooter_saucer
Section titled “Saucers: shooter_saucer”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,damagelayerandhitsinherit: how much of the shooter’s velocity it keeps- how it looks:
texture,size,color,z_order wrap: falseends the shot at the field’s edge instead of wrapping, and withlife: 0it lives until thenregionnames an atlas region oftexture; the shot then draws at the region’s own size
The formation: shooter_formation
Section titled “The formation: shooter_formation”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_xsideways. When the next step would crosslane, it dropsstep_yand turns back instead. - Speed: a step takes
step_per_invaderfor 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 notexturedraws as a square. - Bombs: they fall from the bottom invader of a random column. With
probability
aimed_chancethe column is the one nearesttargetinstead. At mostmax_bombsare in the air at once. - Landing: an invader reaching
land_yhas landed. The formation stops and reports it. - Invaders are bodies on
layerwithhits, so the cannon’s shot kills them through the usual collision rule, andpointstravels on thedestroyedevent.
Bunkers: shooter_bunker
Section titled “Bunkers: shooter_bunker”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
marginpx past the field’s edge. When allmaxslots are in use a new bullet is not fired andbullets_droppedcounts it. - A bullet that touches a
target_layerbody costs it one hp and is removed. One that passes withingraze_radiusof it first counts once as a graze, which a game scores. - A body whose
shieldis 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:
| Field | Meaning |
|---|---|
kind | ring (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, angle | how many per volley; the cone; the direction in degrees, 0 right and 90 up (default 270, down) |
aim | point angle at the target, for any kind |
delay, interval, duration | seconds before the first volley, between volleys, and in all (0: until the phase changes) |
rotate | degrees the direction turns after each volley: spirals |
layers, speed_step | fire each volley this many times, each speed_step faster: rings in layers |
bullet | region (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.
Flight paths: shooter_path
Section titled “Flight paths: shooter_path”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.
A scrolling shooter: scroll, terrain, pod, links
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.
Polarity (Ikaruga)
Section titled “Polarity (Ikaruga)”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 anabsorbedevent (name,countthis frame,x,y). The other colour - and a bullet of no polarity - hits as before.bullet.polarityin a pattern, or the emitter’spolarityfor bullets of none: one pattern fired in either colour. The pool’spolarity_suffix(["", "_w", "_b"]) draws a lightorbas the regionorb_wand a dark one asorb_b.- A shot’s
polarityis the firing body’s when itsShotConfigsays 0, so a ship that changes colour fires its new colour at once.polarity_regionsandpolarity_colors(indexed by polarity) give a weapon its light and dark looks;trailgives a shot a ribbon (the engine’strailcomponent), coloured bypolarity_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_struckreports astruckevent (name,hp,by,double) when a shot hits a body that lives - something to flash.- A
destroyedevent carriespolarity(what died) andby_polarity(what killed it), which is all a script needs to keep a chain. - Homing lasers: a pilot’s
releaseshot.Shooter.ship_release(e, n)firesnof 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, withadvise) 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. Withadviseon andaioff it flies nothing and publishes its choice asShipState.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 forswitch_gapseconds. - A pilot’s
Shooter.ship_drive(e, x, y)steers it this frame (a direction at most 1 long), over itsinput_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.
Script commands
Section titled “Script commands”| Call | Does |
|---|---|
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 |
What scripts read
Section titled “What scripts read”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:
| Field | Meaning |
|---|---|
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) |
rocks | rocks left |
shots | shots in flight |
bullets, grazes, bullets_dropped | pool bullets in the air; grazes so far; bullets not fired because the pool was full |
scrolled, scrolled_y | how far the field has scrolled |
A destroyed event’s fields:
sizeis a rock’s size, and 0 for anything else.bynames whoever fired the killing shot, or whatever was collided with, soby == "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 whileadviseis 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".
Where it runs
Section titled “Where it runs”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).