The bs cookbook
Common shmup patterns written in bs. Each example is a full program with a main, and on
bs.ell.dev each one has a link that opens it in the testbed. The
guide covers the language itself.
The enemy starts near the top of the field and the player near the bottom. The field is 448
units tall. Its width depends on the window, so examples that use the side walls read it with
(input 'field-w 384). The dodge bot gets through every example here.
- Aiming
- Spirals
- Bullet scripts
- Lasers
- Shapes and symmetry
- Time and phases
- Telegraphs
- Familiars and options
- Difficulty
- Randomness that stays fair
- Music
- Writing helpers
Aiming
Aimed and locked streams
The first stream aims every shot, so it follows the player. The second aims once, so every shot goes where the player was and a small step clears it.
(defn main []
(forever
; Aimed every shot: the stream bends after the player.
(dotimes [i 12]
(fire :speed 4 :kind 'needle-cyan)
(wait 4))
(wait 30)
; Aimed once: the stream goes where the player was.
(stream 12 :speed 4 :gap 4 :kind 'needle-yellow)
(wait 30)))fire aims at the player when it has no :dir. stream works
out its :dir once, so every shot in it goes the same way. :gap and :speed control how
hard each stream is to avoid.
Odd and even fans
An odd fan puts its middle shot on the player, so they have to move. An even fan puts the player in the middle gap, so standing still is safe. This example alternates the two.
(defn main []
(forever
; Odd: the middle shot is aimed, so move.
(fan 5 :step 14 :speed 2.5 :kind 'big-red)
(wait 30)
; Even: the player sits in the middle gap, so stay.
(fan 6 :step 14 :speed 2.5 :kind 'big-blue)
(wait 30)))fan centres its shots on (aim), :step degrees apart. Keep
the step wide enough that the player fits between two shots by the time they arrive.
Leading shots
(aim :lead s) aims where the player will be when a shot at speed s reaches them, if they keep
moving the same way. Fired with a plain aimed shot, it catches a player who stands still and one
who keeps moving. Changing speed or direction gets past both.
(defn main []
(forever
(dotimes [i 6]
(fire :speed 3 :kind 'needle-purple)
(fire :dir (aim :lead 3) :speed 3
:kind 'needle-yellow)
(wait 8))
(wait 40)))Give :lead the same speed the shot flies at. Moving the enemy
lists the other queries about the player.
Random spray
Bursts of shots at random angles and speeds, centred on the player. The shots are spread thin enough to get through.
(def colours ['small-orange 'small-yellow 'small-red])
(defn main []
(forever
(dotimes [i 40]
(burst 2 :width 70 :speed [1.5 3.5]
:kind (rand-nth colours))
(wait 3))
(wait 45)))burst picks each direction within :width degrees of :dir,
the player by default, and each speed from the :speed range. rand-nth picks the colour.
Wider ranges spread the shots further.
A wall to weave through
Rows of shots fall across the whole width of the field. Each row is shifted half a step from the last, so the player moves a little for each one.
(def spacing 24)
; A row of shots across the top of the field, falling.
; shift moves it along by a share of the spacing.
(defn row [shift kind]
(let [w (input 'field-w 384)]
(dotimes [i (floor (/ w spacing))]
(let [wx (* spacing (+ i shift))]
(fire :at [(- wx (x)) (- 440 (y))] :dir 270
:speed 2 :kind kind)))))
(defn main []
(forever
(row 0.25 'big-orange)
(wait 24)
(row 0.75 'big-yellow)
(wait 24))):at places a shot relative to the shooter, so (- wx (x)) turns a field position into an
offset. The number of shots in a row comes from field-w, which keeps the spacing the same on
any width. spacing sets how much room the player has between shots.
Spirals
Spiral
One shot a tick, each turned a few degrees further than the last. The player moves through the gaps between passes of the arm.
(defn main []
(loop [a 0]
(fire :dir a :speed 2.5 :kind 'rice)
(wait 1)
(recur (+ a 11))))loop and recur carry the angle from shot to shot (Loops and
lists). The step sets how tightly it winds: 11 degrees a tick makes a
full turn every 33 ticks. A step that divides 360 evenly lines the shots up into straight spokes
instead.
Double spiral
Two arms half a turn apart, one colour each.
(defn main []
(loop [a 0]
(fire :dir a :speed 2 :kind 'big-cyan)
(fire :dir (+ a 180) :speed 2 :kind 'big-pink)
(wait 2)
(recur (+ a 7))))For more arms, fire more shots per step, (/ 360 arms) degrees apart. A smaller step or a
shorter wait packs the arms tighter.
Counter-rotating spirals
Two three-armed spirals turning opposite ways. Where the arms cross, the gaps between them keep opening and closing.
(defn spiral [step kind]
(loop [a 0]
(dotimes [arm 3]
(fire :dir (+ a (* arm 120)) :speed 2 :kind kind))
(wait 4)
(recur (+ a step))))
(defn main []
(par (spiral 9 'needle-cyan)
(spiral -9 'needle-violet)))par runs both spirals at once, each in its own thread (Doing things at
once). One function makes both spirals. Only the sign of the
step and the kind differ.
Rotating rings in two colours
A ring every few ticks, each turned 5 degrees from the last, so the shots line up into curved arms. The colours alternate by ring.
(def colours ['big-blue 'big-pink])
(defn main []
(loop [i 0]
(ring 18 :dir (* i 5) :speed 1.6
:kind (nth colours (mod i 2)))
(wait 16)
(recur (inc i))))(mod i 2) goes 0, 1, 0, 1, and nth uses it to pick a kind from the list. A turn of half the
ring's spacing, 10 degrees for 18 shots, makes each ring fill the gaps of the one before.
Bullet scripts
A bullet fired with :run runs a script of its own, and the
script can steer it, fire from it or remove it.
Stop and re-aim
A ring flies out and stops. Then its shots turn toward the player and fly at them one at a time.
(defn reaim [i]
(fn []
(change-speed 0 :over 30)
(wait (+ 40 (* i 3)))
(change-dir (aim))
(change-speed 3 :over 20)))
(defn main []
(forever
(let [a (rand 0 360)]
(dotimes [i 16]
(fire :dir (+ a (* i 22.5)) :speed 3 :kind 'big-pink
:run (reaim i))))
(wait 90)))reaim takes i and returns a script, so each shot waits a little longer than the one before.
change-speed with :over runs in the background, so the script needs its own wait before it
turns.
Split
Big shots slow down, then each one bursts into a ring with one shot aimed at the player.
(defn split []
(change-speed 0 :over 40)
(wait 40)
(ring 10 :dir (aim) :speed 1.5 :kind 'small-orange)
(vanish))
(defn main []
(forever
(fan 3 :step 50 :speed 3 :kind 'huge-orange :run split)
(wait 70)))A bullet's script can fire too, and its shots start where the bullet is. (aim) inside the
script aims from the bullet, so each ring's first shot goes at the player. vanish removes the
big shot once it has split.
Bounce off the walls
A wide spread whose outer shots bounce off the side walls and come back across the field.
(defn bounce []
(let [w (input 'field-w 384)]
(wait-until (or (< (x) 0) (> (x) w)))
(change-dir (- 180 (heading)))))
(defn main []
(loop [i 0]
(spread 8 :dir (+ 270 (* 6 (mod i 2))) :width 160
:speed 2.5 :kind 'big-cyan :run bounce)
(wait 30)
(recur (inc i))))wait-until checks its test once a tick. Once the shot is past a wall, (- 180 (heading))
flips its direction left to right. It bounces once, then flies on. Every other spread is turned
6 degrees, so the shots don't all fly down the same tracks.
Curving shots
Rings whose shots all turn the same way, so each ring bends into a pinwheel.
(defn curl [rate]
(fn [] (turn rate :for 90)))
(defn main []
(forever
(ring 6 :speed 2 :kind 'leaf :run (curl 0.7))
(wait 6)))turn adds its rate to the heading every tick, for :for ticks. curl returns a script with
the rate built in. A negative rate turns the other way. Firing both directions together makes a
lattice.
Weak homing
Missiles turn toward the player for a while, then fly straight. A sidestep late in their flight gets away from them.
(defn seek []
(dotimes [i 5]
(change-dir (aim) :over 30)
(wait 15)))
(defn main []
(forever
(ring 6 :dir 30 :speed 2 :kind 'missile :run seek)
(wait 50)))change-dir with :over 30 would turn the missile all the way to the player in 30 ticks. The
next aim cuts it off after 15, so it only gets halfway round each time. Fewer rounds or a longer
:over make the homing weaker.
Gravity
Shots are thrown upward and fall back down in arcs.
(defn fall []
(accel 0 -3 :over 40))
(defn main []
(forever
(fire :dir (+ 90 (rand -70 70)) :speed (rand 1 2.5)
:kind 'gem :run fall)
(wait 3)))accel adds a velocity on top of the shot's own heading and speed. Here it ramps to 3 units a
tick downward over 40 ticks and stays there. Give it an x part, like (accel 0.5 -3 :over 40),
for wind.
Speeding up
A fast ring, then a slow ring that speeds up and catches the first one near the player.
(defn surge []
(change-speed 4 :over 90))
(defn main []
(forever
(ring 24 :dir (rand 0 360) :speed 0.3
:kind 'needle-orange
:run surge)
(ring 24 :dir (rand 0 360) :speed 2.5 :kind 'big-teal)
(wait 45)))change-speed with :over ramps the speed in the background. Each ring gets its own random
angle, so the two rings' shots don't line up.
Lasers
A laser shows a warning line, fires, then fades. It can only hit while the beam is on.
Sweeping lasers
Two beams start pointing out to the sides and sweep down toward the middle, stopping short of it. Aimed fans come down the space between them.
(defn beam []
(laser :dir 190 :turn 0.4 :warn 40 :active 125 :fade 20
:width 14 :kind 'laser-blue))
(defn main []
(forever
(beam)
(mirror (beam))
(wait 40)
(dotimes [i 8]
(fan 3 :step 10 :speed 2.5 :kind 'big-yellow)
(wait 15))
(wait 85))):turn turns the beam that many degrees each tick while it's active, so 0.4 for 125 ticks takes
it from 190 to 240. mirror makes the second beam from the first. A sweep across the whole
field would leave no space, so each beam stops 30 degrees short of straight down.
Laser cage
Two beams come down on either side of the player, then aimed shots fall between them.
; A bar down the whole height of the field, at field x.
(defn bar [bx]
(laser :at [(- bx (x)) (- 448 (y))] :dir 270 :length 448
:width 10 :warn 50 :active 200 :fade 20
:attach false :kind 'laser-red))
(defn main []
(let [w (input 'field-w 384)]
(forever
(let [px (clamp (target-x) 70 (- w 70))]
(bar (- px 60))
(bar (+ px 60)))
(wait 60)
(dotimes [i 12]
(spread 5 :width 40 :speed 2 :kind 'small-yellow)
(wait 15))
(wait 60)))):attach false leaves a beam where it was fired instead of following the boss. (target-x) is
the player's x, and clamp keeps the cage inside the field.
Warning line, then beam
Beams aimed at the player. Each one shows a thin warning line for 45 ticks before it fires.
(defn main []
(forever
(dotimes [i 3]
(laser :warn 45 :active 20 :fade 10 :width 16
:kind 'laser-cyan)
(wait 25))
(wait 60)))A laser aims at the player unless given a :dir. :warn sets how long the line shows. The
25-tick wait between beams sets the rhythm.
Curvy lasers
Two arcs curl down either side of the boss while a wavy beam snakes toward the player.
(defn arc []
(curvy-laser :dir 330 :curve 'head :speed 2.5 :turn -0.8
:segments 32 :window 50 :width 8
:attach false :kind 'laser-green))
(defn main []
(forever
(arc)
(mirror (arc))
(wait 20)
(curvy-laser :curve 'wave :speed 2.5 :amplitude 30
:period 50 :segments 40 :window 60
:attach false :kind 'laser-thin)
(wait 40)))curvy-laser draws a trail behind a moving head. With 'head the head flies at :speed,
turning :turn degrees a tick. With 'wave it sways :amplitude units to each side every
:period ticks. :segments and :window set how long and smooth the trail is, and
:attach false lets the beams fly off on their own.
Shapes and symmetry
Polygons
Outlines from a triangle up to a hexagon and round again, each turned a little.
(defn main []
(loop [sides 3]
(polygon sides :per-side 6 :dir (* sides 20) :speed 1.5
:kind 'big-cyan)
(wait 40)
(recur (if (= sides 6) 3 (inc sides)))))polygon fires the corners at :speed and the rest slower, so
the outline keeps its shape as it grows. :per-side sets how many shots make each side, and so
how wide the gaps are.
Roses
Rings that open into five petals, alternating colours, each turned half a petal from the last.
(defn main []
(loop [a 0]
(rose 40 :petals 5 :dir a :speed 2.5
:kind 'small-magenta)
(wait 35)
(rose 40 :petals 5 :dir (+ a 36) :speed 2.5
:kind 'small-yellow)
(wait 35)
(recur (+ a 15))))rose fires the tip of each petal at :speed and the shots
between petals at a quarter of it. Those slow shots stay near the boss for a long time, so keep
the count to about 40 shots a rose.
Mirrored wings
A sweep from one side and its mirror image from the other. Each shot curls a little, and the mirrored shots curl the other way.
(defn wing []
(dotimes [i 12]
(fire :dir (+ 190 (* i 6)) :speed 2.5 :kind 'rice
:run (fn [] (turn 0.5 :for 60)))
(wait 3)))
(defn main []
(forever
(par (wing) (mirror (wing)))
(wait 30)))mirror flips everything fired inside it left to right, including the
turns in the bullets' scripts. par fires the two wings together.
Rotational symmetry
One arm, made of a small fan and a slow big shot, repeated five times round the circle and turned a little more each volley.
(defn arm []
(fan 3 :dir 0 :step 5 :speed 2 :kind 'needle-violet)
(fire :dir 0 :speed 1.4 :kind 'big-purple))
(defn main []
(loop [a 0]
(dotimes [i 5]
(rotate (+ a (* i 72)) (arm)))
(wait 10)
(recur (+ a 7))))rotate turns everything fired inside it, so the arm is written pointing at 0 degrees and
rotate puts it in place. For n arms, space them (/ 360 n) apart.
Time and phases
Phases on a timer
Three attacks, each running for a set time before the next takes over.
(defn pinwheel []
(forever
(ring 5 :dir (* (age) 3) :speed 2 :kind 'rice)
(wait 4)))
(defn fans []
(forever
(fan 7 :step 9 :speed 2.5 :kind 'big-orange)
(wait 24)))
(defn rain []
(forever
(burst 3 :dir 270 :width 140 :speed [1 2.5]
:kind 'small-blue)
(wait 5)))
(def phases
[[240 pinwheel]
[180 fans]
[240 rain]])
(defn main []
(forever
(doseq [[ticks attack] phases]
(race (wait ticks) (attack)))))race stops all its forms as soon as one ends, so (race (wait ticks) (attack)) runs an
endless attack for that many ticks (Doing things at once). The
phases are a list of [ticks attack] rows, so reordering or retiming them doesn't touch the
code. Bullets from one phase carry over into the next, and A clean break between
phases shows how to clear them.
Phases on health
The boss changes attacks as its health drops, and clears the field at each change.
(defn hp [] (input 'var1 1))
(defn calm []
(forever
(ring 12 :dir (* (age) 2) :speed 1.6 :kind 'big-teal)
(wait 25)))
(defn angry []
(forever
(ring 16 :dir (* (age) 2) :speed 2 :kind 'big-red)
(fan 5 :step 12 :speed 3 :kind 'needle-orange)
(wait 20)))
(defn desperate []
(forever
(burst 4 :width 360 :speed [1.5 3] :kind 'small-red)
(wait 3)))
(defn main []
(until (< (hp) 0.6) (calm))
(cancel)
(emit 'angry)
(until (< (hp) 0.25) (angry))
(cancel)
(emit 'desperate)
(desperate))The host passes health in as an input. In the testbed, var1 in the settings menu stands in for
it: 1 is full health, 0 is dead. Until it's set, (input 'var1 1) reads 1 and the first attack
keeps running. until stops its body when the test turns true, checking once a tick.
Attack cycle
The boss moves to a new spot, runs an attack, then moves again for the next one.
(defn reposition []
(let [w (input 'field-w 384)]
(move-to (rand (* w 0.25) (* w 0.75)) (rand 330 380)
:over 40)
(wait 50)))
(defn spiral-burst []
(dotimes [i 60]
(ring 3 :dir (* i 13) :speed 2.2 :kind 'needle-cyan)
(wait 2)))
(defn aimed-volley []
(dotimes [i 4]
(fan 5 :step 10 :speed 3 :kind 'big-yellow)
(wait 18)))
(defn big-ring []
(dotimes [i 3]
(ring 24 :dir (* i 7.5) :speed 1.5 :kind 'big-purple)
(wait 20)))
(defn main []
(forever
(doseq [attack [spiral-burst aimed-volley big-ring]]
(reposition)
(attack))))Functions are values, so a list can hold attacks and doseq can call each in turn. Each attack
here ends on its own, which is what lets the cycle move on. move-to works in field
coordinates, and field-w keeps the boss away from the walls.
A clean break between phases
At the end of each phase the bullets are cleared, then there's a one-second pause. The big
purple shots are fired with :cancellable false, so they stay.
(defn swirl []
(forever
(ring 5 :dir (* (age) 4) :speed 1.5 :kind 'small-cyan)
(wait 4)))
(defn anchors []
(forever
(fire :speed 2 :kind 'huge-purple :cancellable false)
(wait 40)))
(defn phase [ticks attack]
(race (wait ticks) (attack))
(emit 'phase-end (cancel))
(wait 60))
(defn main []
(forever
(phase 240 swirl)
(phase 240 (fn [] (par (swirl) (anchors))))))(cancel) removes every cancellable bullet and returns how many went, which emit passes to
the host for a score or a sound. The testbed turns cancelled bullets into items. To clear only
some, tag shots with :tags and call (cancel :tags 'soft) (Placing
shots).
Telegraphs
Marked lines
Three lines of ghost shots appear along the paths the shots will take. After 40 ticks they can hit, and they move along those lines.
(defn go []
(wait 40)
(change-speed 3 :over 10))
(defn marked-line [d]
(dotimes [i 14]
(fire :at [(* i 18 (cos d)) (* i 18 (sin d))] :dir d
:speed 0 :delay 40 :kind 'big-red :run go)))
(defn main []
(forever
(let [d (aim)]
(marked-line d)
(marked-line (+ d 25))
(marked-line (- d 25)))
(wait 80))):delay keeps a shot from hitting for that many ticks, and the testbed draws it as a ghost
until then. Fired at speed 0 with a script that waits out the delay, the ghosts sit still as a
warning and then go.
Safe lanes
Rows of ghosts form at the top of the field with one lane left open, then fall. The open lane moves by one each row, so the player always has time to reach it.
(def lanes 8)
(defn fall []
(wait 30)
(change-speed 2 :over 10))
(defn row [open]
(let [w (input 'field-w 384)
lane-w (/ w lanes)]
(dotimes [i (* lanes 4)]
(let [wx (* (+ i 0.5) (/ lane-w 4))
at [(- wx (x)) (- 440 (y))]]
(when (not= open (floor (/ wx lane-w)))
(fire :at at :dir 270 :speed 0 :delay 30
:kind 'small-purple :run fall))))))
(defn main []
(loop [open 3]
(row open)
(wait 45)
(let [step (rand-nth [-1 1])]
(recur (clamp (+ open step) 0 (dec lanes))))))The lane only ever moves one step, and clamp keeps it on the field. More lanes makes
narrower ones, and a shorter wait between rows gives less time to move.
Familiars and options
Orbiting options
Three drones circle the boss and follow it as it moves, each firing aimed fans.
(defn gun []
(forever
(wait 40)
(fan 3 :step 12 :speed 2.2 :kind 'small-yellow)))
(defn main []
(dotimes [i 3]
(let [a (* i 120)]
(fire :at [(* 70 (cos a)) (* 70 (sin a))] :speed 0
:motion 'polar :spin 1.2 :attach true
:cancellable false :kind 'drone :run gun)))
(let [w (input 'field-w 384)]
(forever
(move-to (* w 0.3) (y) :over 90)
(wait 120)
(move-to (* w 0.7) (y) :over 90)
(wait 120)))):motion 'polar with :spin circles a shot round the point it was fired from, and
:attach true keeps that point on the boss. :at sets where on the circle each drone starts.
They're fired :cancellable false so that a phase change doesn't take them away.
Free drones
Drones fly out to the sides, fire a few rings, then leave. The boss keeps firing meanwhile.
(defn drone [tx ty]
(move-to tx ty :over 50)
(wait 60)
(dotimes [i 4]
(ring 12 :dir (* i 7.5) :speed 1.5 :kind 'small-cyan)
(wait 30))
(change-dir 90)
(change-speed 3 :over 40))
(defn main []
(let [w (input 'field-w 384)]
(forever
(doseq [tx [(* w 0.2) (* w 0.8)]]
(fire :speed 0 :kind 'drone
:run (fn [] (drone tx 300))))
(dotimes [i 8]
(fan 3 :step 15 :speed 2.5 :kind 'needle-violet)
(wait 30)))))A drone is a shot with a script, fired at speed 0 with the 'drone kind. move-to glides it to
a spot on the field. After its last ring it turns upward and speeds off the top, where it's
culled.
Difficulty
Difficulty and rank
Difficulty changes the pattern itself: bigger rings, and an aimed fan from Hard up. Rank changes the speed and timing.
(defn main []
(let [n (by-diff 10 14 18 22)
aimed (by-diff 0 0 3 5)]
(loop [a 0]
(let [speed (lerp 1.5 2.4 rank)
gap (lerp 40 26 rank)]
(ring n :dir a :speed speed :kind 'big-blue)
(fan aimed :step 12 :speed (* speed 1.5)
:kind 'needle-yellow)
(wait gap)
(recur (+ a 7))))))(by-diff easy normal hard lunatic) picks one of four values by the host's difficulty setting.
That doesn't change mid-fight, so it's read once. rank runs from 0 to 1 and can change during
play, so it's read every volley. (fan 0 ...) fires nothing, which leaves the aimed layer out
on Easy and Normal. See Talking to the host.
Randomness that stays fair
Each actor draws from its own random stream, seeded by the VM (Randomness). The same seed plays out the same way every time, so replays work.
Variety without repeats
The boss picks its next attack at random but never the same one twice in a row, and each attack varies its own details.
(defn petals []
(rose 32 :petals (rand-nth [3 4 5]) :dir (rand 0 360)
:speed 2 :kind 'small-magenta))
(defn wave []
(dotimes [i 20]
(fire :dir (+ 270 (* 50 (sin (* i 18)))) :speed 2.5
:kind 'needle-cyan)
(wait 2)))
(defn volley []
(fan (rand-nth [3 5 7]) :step 12 :speed 2.5
:kind 'big-orange))
(def attacks [petals wave volley])
(defn main []
(loop [prev -1]
(let [i (rand-nth (filter (fn [j] (not= j prev))
(range (count attacks))))
attack (nth attacks i)]
(attack)
(wait 50)
(recur i))))filter drops the last attack's index before rand-nth picks from the rest. Inside the
attacks, rand-nth and rand vary the petal count, the angle and the size of the fan.
A gap that's always there
Random sprays toward the lower half of the field, always with one gap. The gap moves a little each volley, so the player can always reach it.
(defn main []
(loop [gap (mod (aim) 360)]
(dotimes [i 30]
(let [d (rand 200 340)]
(when (> (abs (- d gap)) 12)
(fire :dir d :speed (rand 2 2.3)
:kind 'small-red))))
(wait 20)
(recur (clamp (+ gap (rand -8 8)) 215 325))))Random shots can line up into a wall by chance. Leaving out every shot near the gap rules that
out. (aim) comes back between -180 and 180, so mod moves it into the 0 to 360 range the
other angles use. clamp keeps the gap away from the edges of the spray.
Music
On the beat
A ring on every beat, an aimed pair on the off-beats, and a burst every fourth beat.
(defn main []
(par
; A ring on every beat, turning by a step each beat.
(forever
(ring 20 :dir (* 9 (beat (/ (tick) 30))) :speed 1.6
:kind 'big-pink)
(wait-beats 1 30))
; An aimed pair on the off-beats.
(forever
(wait-beats 0.5 15)
(fan 2 :step 16 :speed 3 :kind 'needle-yellow)
(wait-beats 0.5 15))
; A burst every fourth beat.
(forever
(wait-beats 4 120)
(burst 16 :width 90 :speed [2 3.5]
:kind 'small-yellow))))(wait-beats n) waits for the host's beat clock. A whole number waits n beats from the start of
the current one, and a fraction waits for the next step on that grid. The second number is the
wait in ticks when there's no clock, and 30 ticks is a beat at 120 bpm. (beat d) is the beat
now, or d without a clock. The testbed's beat clock is in the settings menu.
Writing helpers
Passing keys on
A helper that fires two interlocking rings, called twice with different keys.
; Two rings, the second turned half a step and :gap ticks
; later, so they interlock. Any other key goes to ring.
(defn ring-pair [n :dir 0 :gap 8 & opts]
(ring n :dir dir & opts)
(wait gap)
(ring n :dir (+ dir (/ 180 n)) & opts))
(defn surge []
(change-speed 3 :over 60))
(defn main []
(forever
(ring-pair 16 :dir (age) :speed 0.5 :kind 'big-teal
:run surge)
(wait 30)
(ring-pair 20 :gap 12 :speed 1.5 :kind 'tiny-pink
:delay 20)
(wait 40)))& opts collects the keys ring-pair has no parameter for and
passes them on to ring, so a caller can add :run, :delay, :tags or anything else fire
takes. A key that nothing takes is an error when the file loads. Keys the helper sets itself
aren't passed on, which is why ring-pair takes :dir as a parameter of its own.