Skip to content

Tutorials · Crypt Knight · Chapter 13

The beast

90 minutes Uses: Area, Collider, CameraRig, Spawner, Parallax, CharacterBody, emit_to, heard, show, spawn, destroy_self, translate, random, complete, kept_point, keep_point, set_flag, jump, play_clip

By the end

  • The Beast’s Hall, the Crypt’s scene made again with a new floor plan, one backdrop and one heart
  • An arena: an Area the knight walks into once, a portcullis that drops behind him, and a camera that pulls back and holds still
  • A boss bar in the HUD that shows the beast’s health
  • The Nightmare, with three moves, picked at random and never the same one three times in a row
  • A fury below half its health, a fireball that becomes three, and a death that wins the level
On this page

A fight in a room

The third level is one room and one fight. The knight walks in from the left, past a heart floating over a ledge, and into an arena. As he enters, a gate drops behind him, the camera pulls back and holds still, a health bar shows at the bottom of the screen, and the Nightmare, a horse of black smoke and blue fire, wakes. It has 20 health, and each slash that lands takes one.

The Nightmare rests, and then makes one of three moves, at random, never the same one three times in a row: it charges at where the knight was, to the wall; it leaps to where he was and lands with a shake; or it breathes a fireball at him. Each is announced by a red glow, a wind-up, long enough to see. Below half its health it is in a fury: it rests and winds up 40% shorter, charges faster and breathes three fireballs at once. When it is slain, it flashes and fades, and a moment later the level is won.

Everything here is chapters you have done again, and one new idea, which is how a number of nodes can be told to start together: a signal sent to a group. The arena sends one, "begin", to the group arena, and the gate, the bar, the camera and the beast, each a node in that group, each answer it in their own way.

The Hall’s room

The Hall is the Crypt’s scene, made again as the Crypt was the Gate’s: it keeps the camera, the knight, the HUD and the room’s bounds, and everything else is replaced.

  1. In Assets, open scenes, right-click crypt.ron, choose Duplicate, then right-click the copy, choose Rename, type hall.ron and press Enter. In art, do the same to crypt.tilemap, renaming its copy to hall.tilemap.
  2. Double-click hall.ron. Rename its root Crypt to Hall.
  3. In the Hierarchy, select hound_1, then Ctrl-click hound_2, skull, wolf, heart_pickup_2, sword, sign_sword, exit, pit, safe_1, safe_2, safe_3, safe_4 and safe_5, and press Delete. Keep the camera, ground, knight and its slash, bounds, hud and heart_pickup_1.
  4. Rename heart_pickup_1 to heart_pickup, and set its Position to 288 and 100.

The Hall’s ground is a new, smaller floor plan on its own tilemap: 58 cells across and 15 tall, a single long floor between two walls, with one ledge. The heart floats over the ledge, 48 units above the floor, which only a double jump reaches.

  1. Select ground, and drag art/hall.tilemap onto the Tilemap slot of its TileMapComponent. Open the Tilemap tab and turn on Paint mode. It still holds the Crypt.
  2. Choose Erase, set Size to 16, and, with the whole of the room in view, drag from cell (0, 6) to cell (101, 6) on each of the layers back, ground and ledges, clicking the layer’s name first. The room is empty.
  3. Choose the Rect tool and the picker’s #66, and on ground drag one rectangle for each row of the first table below. Pick #45 and drag the cap of the floor. Click ledges, keep #45, and drag the ledge.
  4. Press Save at the top of the Tilemap panel, and Ctrl+S.
Layer and tileStretchFrom cellTo cell
ground, #66The left wall(0, 0)(1, 14)
ground, #66The floor’s body(2, 11)(55, 14)
ground, #66The right wall(56, 0)(57, 14)
ground, #45The floor’s cap(2, 10)(55, 10)
ledges, #45The ledge(16, 7)(19, 7)

The room is now 928 wide and 240 tall, a row taller than the Gate’s and the Crypt’s, so that the camera, pulled back, fits it from floor to ceiling. The Confiner and the Hall’s own backdrop change to fit.

  1. Copy the picture above into art, and press Refresh in Assets.
  2. Select bounds. Set its Position to 464 and 120, and its Bounds to 928 and 240.
  3. Select Hall, press + Add node, and choose Parallax. Rename it backdrop. Drag art/old-dark-castle-interior-background.png onto its Sprite’s Texture, and set Size to 960 and 304, and Sort Layer to -40.
  4. In its Parallax, set Factor to 0.6 and 1, Offset to 0 and -40, and Repeat Period to 960 and 0. Leave Repeat X ticked and Repeat Y unticked.
  5. Press Ctrl+S.

One opaque backdrop, not four: a castle interior, a lantern, a window, behind the whole fight. Its picture is as tall as the room and a little more, so its offset lifts it 40 units to fit, and the Hall costs the machine one layer.

  1. Click the Levels tab. Type The Beast’s Hall into the box in the panel’s bar and press Add level. With its row selected, press Scene and choose hall.ron.
  2. Drag abandoned_castle_music_-_starninjas_6.ogg onto the Music button of its row.

The arena and its gate

The arena is an Area the knight walks into, at the room’s middle. It sends its signal the first time he does, and never again. Everything that starts with the fight listens for it.

  1. In Assets, press Create, choose New script, type arena, and press Enter.
  2. In the Code tab’s Scripts list, click arena.rgs, replace everything in it with the script below, and press Ctrl+S.
rgs arena.rgs
/// The arena's threshold: the first time the knight walks in, it sends
/// "begin" to the group "arena", which drops the portcullis behind the knight,
/// turns the camera onto the arena, shows the boss bar and wakes the beast.
component Arena : Area {
    /// Whether the fight has begun, so it begins once.
    #[hidden]
    begun: bool = false,

    fn on_enter(other: NodeId) {
        if in_group(other, "knight") && !begun {
            begun = true;
            emit_to("arena", "begin");
        }
    }
}
  1. Click the Viewport tab. Select Hall, press + Add node, choose Area, and rename it arena. Set its Position to 688 and 120.
  2. In its Area, set the Shape’s Size to 368 and 80. Leave its Layer and Mask: it is on layer 0 and looks for layer 0, where the knight is.
  3. Press Add component and choose Arena.
  4. Press Ctrl+S.
The arena node selected and framed in the Viewport: a wide outline over the floor of the Hall with the Nightmare standing at its right, a heart, a window and pillars behind; the Inspector narrowed to its Area, Size 368 by 80, Layer 0 and Mask 0
emit_to "arena", "begin"
Sends the signal "begin" to every node in the group arena. Each hears it on the next step, with heard or an await signal. The same call told the camera to "shake" in chapter 9.
begun
A flag the script keeps, so the signal goes once. The knight’s Area may leave the arena and enter it again, and would otherwise begin the fight twice. #[hidden] keeps it out of the Inspector.

The first thing to answer is the gate. A portcullis drops behind the knight so that he cannot leave: a dark bar, over the floor, 16 wide and 160 tall, with a Collider, a solid box that stops bodies. It is hidden until the signal. A hidden Collider blocks nothing, so showing it is what closes the gate. Both the portcullis and the boss bar, below, are shown by one script.

  1. In Assets, press Create, choose New script, type reveal, and press Enter. In the Code tab, replace everything in reveal.rgs with the script below, and press Ctrl+S.
rgs reveal.rgs
/// Hidden until its cue reaches the node, then shown: the portcullis, which
/// blocks once it shows, and the boss bar.
component Reveal {
    /// The signal that shows it.
    cue: String = "begin",

    fn loop() {
        if heard(cue) {
            show();
        }
    }
}
  1. Click the Viewport tab. Select Hall, press + Add node, choose Shape, and rename it portcullis. Set its Position to 472 and 160, and untick its Visible. Under Groups, type arena and press Enter.
  2. In its Shape, keep Kind on Rectangle, set Size to 16 and 160, Anchor to 0.5 and 1, Outline to 0, Color to 0.16, 0.15, 0.18 with an alpha of 1, and Sort Layer to 2.
  3. Press Add component and choose ColliderComponent. In its Shape, set Size to 16 and 160, Offset to 0 and -80, and tick Solid.
  4. Press Add component and choose Reveal. Its Cue is begin already.
  5. Press Ctrl+S.

The arena has three walls, which are not for the knight: they are for the beast’s fireballs. A fireball is an Area, not a body, so the room’s tiles do not stop it: it would fly through the walls. Three Areas, one for the left wall, one for the right and one for the floor, say where a fireball ends. They are in a group wall, on a layer of their own, 3.

  1. Select Hall, press + Add node, choose Area, and rename it wall_left. Set its Position to 472 and 120, and under Groups type wall. In its Area, set the Shape’s Size to 16 and 240, the Area’s own Layer to 3, and untick 0 in its Mask.
  2. Duplicate it: rename the copy wall_right, and set its Position to 904 and 120.
  3. Duplicate it again: rename the copy arena_floor, set its Position to 688 and 168, and its Shape’s Size to 416 and 16.
  4. Press Ctrl+S.

The layers are now four: 0 hears the knight, 1 is what hurts him, 2 is what the sword reaches, and 3 is a wall of the arena.

The camera pulls back

For the fight the camera lets go of the knight, pulls back and holds the whole arena still. That is a second framing in the Camera Rig of chapter 6: a shot of its own, with the arena as its subject, that the rig blends to when it is switched on.

  1. Select camera, and in its CameraRigComponent, next to Framings, press the +. Open the new framing [1] with its arrow.
  2. Set Name to arena. Click Subject, and choose arena: the Area’s node, which stands still at the room’s middle.
  3. Untick Active: the framing is off until the fight begins. Set Priority to 1, above the follow framing’s 0, Zoom to 0.75, Duration to 0.6 and Ease to EaseInOut.
  4. Set Dead Zone and Soft Zone to 0 and 0, Offset to 0 and 0, and Damping to 0.2 and 0.2. Leave Time at 0, Smoothing at 0.2 and Limit at 0.
  5. Under Groups, add the group arena beside camera.
The camera selected in the Hall, its Inspector narrowed to its Camera Rig: two framings, 1 follow closed and 2 arena open: Name arena, Subject arena, Active unticked, Priority 1, Zoom 0.75, Rotation 0, Duration 0.60 s, Ease EaseInOut, Dead Zone and Soft Zone 0 and 0, Offset 0 and 0, Damping 0.2 and 0.2; the Hierarchy lists the Hall’s nodes with the arena group on the camera, boss_bar, portcullis and beast
Priority
When two framings are on, the higher one wins. The follow framing, 0, is always on. When the arena framing is switched on, its 1 wins, and the camera blends to it over its Duration, 0.6 seconds, easing in and out. When it goes off again, the follow framing is the one left on.
Zoom 0.75
The shot shows a third more of the room: 320 divided by 0.75 is about 427 units across, and 180 divided by 0.75 is 240 up, which is the room’s height exactly. Every art pixel is drawn three screen pixels wide, which is still a whole number, so the art stays crisp.
A still subject
The arena’s node never moves, so the framing never does: the camera is locked, with no dead zone, no look-ahead and a gentle damping for the blend to settle with.

The camera is told to switch it on by the same signal, in a script of its own. A script cannot switch another node’s framing, but the camera’s own script writes to the camera’s own rig.

  1. In Assets, press Create, choose New script, type arena_view, and press Enter. In the Code tab, replace everything in arena_view.rgs with the script below, and press Ctrl+S.
rgs arena_view.rgs
/// The camera's arena shot: when "begin" reaches the group "arena" this node
/// is in, the rig's second framing, which holds the arena still and pulled
/// back, takes over from the one that follows the knight.
component ArenaView {
    fn loop() {
        if heard("begin") {
            // The rig's framings are counted from 0: 1 is the second.
            set_flag("CameraRigComponent", "framings.1.active", true);
        }
    }
}
  1. Click the Viewport tab, select camera, press Add component, and choose ArenaView.
  2. Press Ctrl+S.

set_flag("CameraRigComponent", "framings.1.active", true) writes the second framing’s Active, reached by its place in the rig’s list: the framings are counted from 0, so 1 is the second. set_flag writes a flag of one of this node’s components.

The boss bar

The bar is two Panels on the HUD: a dark slot along the bottom of the screen, hidden until the signal, and a red fill inside it, whose right edge is moved by the beast’s health. A Rect has no scale a script can reach, but it has an Offset Max, and the fill is as long as that says. The slot is shown by the same Reveal.

  1. In Assets, press Create, choose New script, type boss_health, and press Enter. In the Code tab, replace everything in boss_health.rgs with the script below, and press Ctrl+S.
rgs boss_health.rgs
/// The boss bar's fill: as long as the beast has health left, which its
/// Wounds keep as `boss_hp`.
component BossHealth {
    /// The beast's health when whole.
    full: f32 = 20.0,
    /// The fill's length at full health.
    #[suffix("u")]
    width: f32 = 158.0,

    fn loop() {
        // The fill's right edge, from the bar's left edge, 1 unit in.
        set_number("RectComponent", "offset_max.x", 1.0 + width * kept_number("boss_hp", full) / full);
    }
}
  1. Click the Viewport tab. Select hud, press + Add node, and choose Panel. Rename it boss_bar, untick its Visible, and under Groups type arena.
  2. Its Rect: Anchor Min and Anchor Max 0.5 and 1, the bottom centre; Offset Min -80 and -14; Offset Max 80 and -8; Pivot 0.5 and 0.5. That is 160 units across and 6 tall, 8 above the bottom edge. Its Panel Color: 0.05, 0.05, 0.08, alpha 1.
  3. Press Add component and choose Reveal.
  4. Select boss_bar, press + Add node, and choose Panel: it lands under the slot. Rename it boss_fill.
  5. Its Rect: Anchor Min 0 and 0, Anchor Max 0 and 1, so that it is as tall as the slot; Offset Min 1 and 1; Offset Max 159 and -1; Pivot 0 and 0. Its Panel Color: 0.6, 0.02, 0.06, alpha 1, the hearts’ red.
  6. Press Add component and choose BossHealth. Leave its Full at 20 and Width at 158.
  7. Press Ctrl+S.

The fill is 158 units long when the beast is whole, one unit in from each end of the slot, and its right edge is 1 + 158 × boss_hp / 20. At 10 health it is half as long, and at none it is the one unit of a slit. kept_number("boss_hp", full) reads the beast’s health, with 20 as its answer until the beast has kept any.

The Nightmare’s art

The beast is the Nightmare of the collection, a horse of black smoke, with two strips: idle and galloping. Its fireball is a small flame from another creature’s pack, the Hell Beast’s.

The two strips play from one sheet, so they are packed as the knight’s are: Make Animation…, then the Sprite sheet. The fireball is one strip, and is cut where it lies.

  1. Copy the three pictures above into art, and press Refresh in Assets.
  2. Right-click nightmare-galloping.png, choose Make Animation…, and tick nightmare-idle.png too. Set Name to nightmare and press Create. It writes art/nightmare.png, 2048 by 128, and art/nightmare.anim.
  3. Double-click art/nightmare.png in Assets. The Sprite sheet tab opens on it. The galloping strip is at x 0 and the idle strip at x 578.
  4. Idle. Set Cell to 128 and 96, Margin to 578 and 0. Drag from cell 0, 0 to cell 3, 0: Selected 4 x 1. Press From selection; name it idle, 8 fps, Loop; Generate 1 and Clear.
  5. Gallop. Set Cell to 144 and 96, Margin to 0 and 0. Drag from cell 0, 0 to cell 3, 0: Selected 4 x 1. From selection; name it gallop, 12 fps, Loop; Generate 1 and Clear.
  6. Press Save in the Animation panel.
  7. Double-click art/fire-ball.png. Set Cell to 19 and 16, drag from cell 0, 0 to cell 2, 0: Selected 3 x 1. From selection; name it burn, 12 fps, Loop; Generate 1. A file, art/fire-ball.anim, appears beside it.

The strips’ frames are not the same width, 128 and 144, so the beast’s Animated Sprite draws each frame at its own size, with Frame Size, as the knight’s does. The pivots stay at the middle of the bottom edge: both creatures’ feet are on the frames’ last row.

The beast

  1. Select Hall, press + Add node, choose Character, and rename it beast. Set its Position to 800 and 160. Under Groups, type enemy, then arena. Remove its Shape.
  2. In its CharacterBodyComponent, set Size to 64 and 56, Driven By to Script, Gravity to 900, Terminal Velocity to 400 and Jump Speed to 360.
  3. Add a SpriteComponent: drag art/nightmare.png onto its Texture, set Size to 128 and 96, Anchor to 0.5 and 1, Sort Layer to 4.
  4. Add an AnimatedSpriteComponent: drag art/nightmare.anim onto Animations, set Clip to idle, and tick Frame Size.
  5. Add an AreaComponent: its Shape Size 60 and 52, Offset 0 and -26, its own Layer 1, and in its Mask untick 0 and tick 2: it is an enemy that hurts the knight, and it looks for the sword’s reach.
  6. Press Ctrl+S.
The beast framed and selected, filling the Viewport, its Character Body the green box round it and its Area the yellow box just inside; its Inspector narrowed to its Character Body: Size 64 by 56, Driven By Script, Gravity 900, Terminal Velocity 400 and Jump Speed 360, with the Animation panel under the Viewport listing its clips, nightmare, idle and gallop, and the Hierarchy listing the Hall’s nodes

The beast is two scripts on one node: Beast, which makes the moves, and Wounds, which keeps its health. A node does not hear its own emit_to, so the two cannot signal each other on one node. They talk through a kept value, boss_hp, as the HUD talks to the knight through hearts. And the beast needs to know where the knight is: his script keeps his position every step, and a script cannot read another node’s.

In knight.rgs, two lines in the comment at the top, and one in loop, after the heard("heal") block, before the recoil:

rgs part of knight.rgs
/// With no hearts left, the level is lost. Each step it keeps where it is,
/// for the beast to aim at.
rgs part of knight.rgs
        // Where the knight is, kept for the beast to aim at a step later.
        keep_point("knight", position());

Press Ctrl+S. Now the beast’s health, which is short.

  1. In Assets, press Create, choose New script, type wounds, and press Enter. In the Code tab, replace everything in wounds.rgs with the script below, and press Ctrl+S.
rgs wounds.rgs
/// The beast's wounds: each slash of the knight's that lands takes one of its
/// health and pushes the knight back. Its health is kept as `boss_hp`, which
/// the boss bar shows and the beast's fury reads. At none it is slain: it
/// stops, hurts nothing more, and a moment later the level is won.
component Wounds : Area {
    /// How many slashes it takes to slay.
    health: f32 = 20.0,
    /// Seconds from the last slash to the level won.
    #[suffix("s")]
    dying_time: f32 = 2.0,

    fn on_create() {
        keep_number("boss_hp", health);
    }

    fn on_enter(other: NodeId) {
        if in_group(other, "slash") && health > 0.0 {
            health -= 1.0;
            keep_number("boss_hp", health);
            // The knight is in the group "knight", and is pushed back a step
            // later.
            emit_to("knight", "recoil");
            if health <= 0.0 {
                // It stops where it is, and its Area is on no layer the
                // knight's looks for.
                set_number("CharacterBodyComponent", "velocity.x", 0.0);
                set_number("AreaComponent", "layer", 0.0);
                await wait(dying_time);
                complete();
            }
        }
    }
}
on_create and boss_hp
The beast’s health, 20, is kept as boss_hp as the Hall opens, so the bar reads it from the first step.
on_enter
Each slash that lands is the reach of the knight, in the group slash, meeting the beast’s Area. It takes one health, keeps the new number, and tells the knight, in the group knight, to "recoil": he is pushed back, as he is by any creature. At none, the beast is slain.
The death
It stops where it is, velocity.x to 0, and its Area goes to layer 0, which the knight’s does not look for, so a body still sliding cannot hurt him. It waits two seconds, and calls complete: the level is won. Chapter 14 makes it flash and burst.
  1. Click the Viewport tab, select beast, press Add component, and choose Wounds. Leave its Health at 20 and Dying Time at 2.

Three moves, one at a time

The beast’s script is the longest in the game, and it is one idea: a loop that rests and makes a move, again and again, for as long as the beast lives. The loop is a hook that waits: it pauses in the middle, for time to pass and for things to happen, so that one pass through it is one rest and one move. It is typed in pieces, in order, one under another. Create beast.rgs and start with its top.

  1. In Assets, press Create, choose New script, type beast, and press Enter. Open it in the Code tab.
rgs part of beast.rgs
/// The beast: asleep until "begin" reaches the group "arena" it is in. Then,
/// while it lives, it rests facing the knight and makes one of three moves at
/// random, never the same one three times running: a charge to the wall, a
/// leap at the knight, or fire. Below half its health it is in a fury: its
/// rests and wind-ups are 40% shorter, it charges faster, and it breathes
/// three fireballs at once. Its health is its Wounds', kept as `boss_hp`.
component Beast : Body {
    /// Its health when whole: below half of it, the beast is in a fury.
    full: f32 = 20.0,
    /// Seconds it rests between moves.
    #[suffix("s")]
    rest: f32 = 1.0,
    /// Seconds it glows red before a move: the knight's warning.
    #[suffix("s")]
    windup: f32 = 0.6,
    /// How fast it charges.
    #[suffix("u/s")]
    charge_speed: f32 = 200.0,
    /// Seconds a leap stays in the air: twice its body's Jump Speed over its
    /// Gravity, 2 x 360 / 900.
    #[suffix("s")]
    air_time: f32 = 0.8,
    /// How far each side fireball turns from the middle one, in a fury.
    #[suffix("rad")]
    spread: f32 = 0.35,
    /// Whether "begin" has woken it.
    #[hidden]
    awake: bool = false,
    /// The way it faces: -1 for left, 1 for right. Its art faces left.
    #[hidden]
    way: f32 = -1.0,
    /// The move it made last: 0 a charge, 1 a leap, 2 fire, -1 none yet.
    #[hidden]
    last: f32 = -1.0,
    /// How many times running it has made that move.
    #[hidden]
    streak: f32 = 0.0,
    /// 1 once it is in a fury.
    #[hidden]
    fury: f32 = 0.0,
    /// Whether a charge has met a wall.
    #[hidden]
    walled: bool = false,
component Beast : Body
A script with a Body base, as the patrols’: it has the body’s hooks, such as on_hit_wall, and its own node’s body to jump and run.
The fields
The numbers it is tuned by, each in the Inspector: full, its health when whole, rest and windup in seconds, charge_speed, air_time and the fireballs’ spread. The hidden ones are its memory: awake, way it faces, the move it made last and how many times running, fury and whether a charge has met a wall.

Waking, and resting

Then loop, one pass of it a rest and a move. First the beast sleeps until it hears the signal, and wakes a second later. While it has health, it works out whether it is in its fury, rests facing the knight, and waits.

rgs part of beast.rgs
    fn loop() {
        if !awake {
            // The arena sends "begin" when the knight walks in.
            await signal("begin");
            awake = true;
            await wait(1.0);
        }
        // Its Wounds, beside this on the beast, keep its health.
        if kept_number("boss_hp", full) > 0.0 {
            // In a fury, a rest or a wind-up takes `calm` of its time, and a
            // charge or a fireball goes `haste` times as fast.
            let calm = 1.0;
            let haste = 1.0;
            if kept_number("boss_hp", full) < full * 0.5 {
                fury = 1.0;
            }
            if fury > 0.0 {
                calm = 0.6;
                haste = 1.3;
            }
            // It faces the knight, where the knight was a step ago.
            if kept_point("knight", position()).x < position().x {
                way = -1.0;
            } else {
                way = 1.0;
            }
            set_flag("SpriteComponent", "flip.x", way > 0.0);
            play_clip("idle");
            await wait(rest * calm);
await signal("begin") and await wait
An awaitable pauses the hook where it is, and carries on from the same place later. signal("begin") waits for that signal to reach the node. wait(1.0) waits a second. In between, the beast does nothing: it is asleep.
calm and haste
Two numbers for the fury. Whole, the beast rests and winds up for the whole of its time, calm of 1, and moves at its own speed, haste of 1. Below half its health, fury is set, and does not go away: calm is 0.6, so its rests and wind-ups are 40% shorter, and haste is 1.3. The fury begins strictly below half: at 10 of 20 it is not in one, at 9 it is.
kept_point("knight", position())
Where the knight was a step ago, kept by his script. The beast turns to face it: way is -1 for left and 1 for right, and its Sprite is mirrored to match. Its art faces left.

Choosing a move

One of three, at random, and never the same one three times running. A number from 0 to 3 is rolled, and its whole part picks the move: 0 a charge, 1 a leap, 2 fire. If the move is the one made last, and it has been made twice running, it is moved on to the next.

rgs part of beast.rgs
            // One of three at random; after two of one, never a third.
            let roll = random(0.0, 3.0);
            let pick = 0.0;
            if roll >= 1.0 {
                pick = 1.0;
            }
            if roll >= 2.0 {
                pick = 2.0;
            }
            if pick == last && streak >= 2.0 {
                pick = (pick + 1.0) % 3.0;
            }
            if pick == last {
                streak += 1.0;
            } else {
                last = pick;
                streak = 1.0;
            }

random answers a number from its first argument up to, but not including, its second, new at every call and different every Play. The % is the remainder of a division, so (pick + 1.0) % 3.0 goes round from fire back to the charge.

The wind-up

rgs part of beast.rgs
            // The wind-up: it glows red.
            pulse_colour("SpriteComponent", "color", vec4(1.0, 0.4, 0.4, 1.0), 0.1, .Linear);
            await wait(windup * calm);
            // A move begins only while it lives.
            if kept_number("boss_hp", full) > 0.0 {
                set_colour("SpriteComponent", "color", vec4(1.0, 1.0, 1.0, 1.0));

The beast glows red for windup seconds, a calm share of that in a fury. The glow is the same before all three moves, so it does not say which one is coming: it says one is, and gives the knight that long to get ready. Then the glow ends and the move begins, if the beast is still alive: a slash can land during the wind-up and slay it.

The charge

rgs part of beast.rgs
                if pick == 0.0 {
                    // The charge: the way it faces, until a wall stops it.
                    play_clip("gallop");
                    walled = false;
                    set_number("CharacterBodyComponent", "velocity.x", way * charge_speed * haste);
                    await wait_until(walled);
                    set_number("CharacterBodyComponent", "velocity.x", 0.0);

It runs the way it faces, toward where the knight was, at 200 units a second, 260 in a fury, until a wall stops it. on_hit_wall, at the bottom of the script, sets walled, and await wait_until(walled) waits for it. A charge that starts against its wall ends at once.

The leap

rgs part of beast.rgs
                } else if pick == 1.0 {
                    // The leap: up, and sideways as far as the knight was.
                    let reach = kept_point("knight", position()).x - position().x;
                    play_clip("gallop");
                    set_number("CharacterBodyComponent", "velocity.x", reach / air_time);
                    jump();
                    // A jump leaves the ground a step after it is asked.
                    await wait_frames(3);
                    await wait_until(grounded());
                    set_number("CharacterBodyComponent", "velocity.x", 0.0);
                    // The camera's node is in the group "camera".
                    emit_to("camera", "shake");

It jumps: jump asks its body for one, with a Jump Speed of 360 and a Gravity of 900, so it is in the air for 2 × 360 ÷ 900, 0.8 seconds, and in that time it runs sideways as far as the knight was, reach / air_time. A jump leaves the ground a step after it is asked, so it waits three frames before it waits to land, and when it lands, the camera shakes.

The fire

rgs part of beast.rgs
                } else {
                    // Fire, from its mouth at the knight. Each fireball flies
                    // away from the kept muzzle, so where it is made is the
                    // way it goes.
                    let mouth = position() + vec2(way * 40.0, -62.0);
                    let aim = (kept_point("knight", position()) + vec2(0.0, -15.0) - mouth).normalized();
                    keep_point("muzzle", mouth);
                    keep_number("haste", haste);
                    spawn("fireball", mouth + aim * 8.0);
                    if fury > 0.0 {
                        let c = spread.cos();
                        let s = spread.sin();
                        spawn("fireball", mouth + vec2(aim.x * c - aim.y * s, aim.x * s + aim.y * c) * 8.0);
                        spawn("fireball", mouth + vec2(aim.x * c + aim.y * s, aim.y * c - aim.x * s) * 8.0);
                    }
                    await wait(0.3);
                }
            }
        }
    }

It breathes from its mouth, 40 units out and 62 up, at the knight’s chest. A script cannot tell a copy where to go, so the beast keeps its mouth as muzzle and its speed as haste, and each fireball flies away from the muzzle: where it is made is the way it goes. In a fury, two more are made, spread radians either side of the middle one, by turning the aim: cos and sin are the turn’s two halves. Then it waits a third of a second, and the loop begins again.

Last, below loop, the hook that a wall calls:

rgs part of beast.rgs
    fn on_hit_wall() {
        walled = true;
    }
}
The Code panel open on beast.rgs, at the part that picks a move: the roll from random(0.0, 3.0), the pick it makes, and the line that moves a pick on when it is the move made last twice running, with the caret on it and Up to date in the status line

Press Ctrl+S. The panel says Up to date once it checks. The script is 143 lines, in the pieces above: the blank lines of the file sit between them. Click the Viewport tab, select beast, press Add component, and choose Beast, leaving its fields as they come.

The fireball

A fireball is an artifact, as Ship Evader’s laser is: a node the game makes copies of while it runs. It is an Area, in the group enemy, that flies away from the muzzle and ends on the knight, a wall, or after three seconds.

  1. In Assets, press Create, choose New script, type fireball, and press Enter. In the Code tab, replace everything in fireball.rgs with the script below, and press Ctrl+S.
rgs fireball.rgs
/// A fireball the beast breathes: it flies away from the beast's mouth, which
/// the beast keeps as `muzzle`, the way it was breathed, and is gone when it
/// meets the knight or a wall of the arena, or after `lifetime`.
component Fireball : Area {
    /// How fast it flies, before the beast's fury quickens it.
    #[suffix("u/s")]
    speed: f32 = 150.0,
    /// Seconds before it burns out.
    #[suffix("s")]
    lifetime: f32 = 3.0,
    /// How fast it flies, and which way, worked out as it is made.
    #[hidden]
    velocity: Vec2 = vec2(0.0, 0.0),

    fn on_create() {
        // Made a little way out from the muzzle, along its way. The beast keeps
        // `haste` as 1, or more in its fury.
        let way = (position() - kept_point("muzzle", position() - vec2(1.0, 0.0))).normalized();
        velocity = way * speed * kept_number("haste", 1.0);
        // Its art faces left.
        set_flag("SpriteComponent", "flip.x", velocity.x > 0.0);
        await wait(lifetime);
        destroy_self();
    }

    fn loop(dt: f32) {
        translate(velocity * dt);
    }

    fn on_enter(other: NodeId) {
        if in_group(other, "knight") || in_group(other, "wall") {
            destroy_self();
        }
    }
}
  1. Click the Viewport tab. Select Hall, press + Add node, choose Sprite, and rename it fireball. Under Groups, type enemy.
  2. Drag art/fire-ball.png onto its Sprite’s Texture, and set Size to 19 and 16, Anchor to 0.5 and 0.5 and Sort Layer to 7.
  3. Add an AnimatedSpriteComponent: drag art/fire-ball.anim onto Animations and set Clip to burn.
  4. Add an AreaComponent: its Shape Size 12 and 10, its own Layer 1, and in its Mask tick 0 and 3, and untick the rest: it looks for the knight, on layer 0, and the arena’s walls, on layer 3.
  5. Add Fireball. Leave its Speed at 150 and Lifetime at 3.
  6. Right-click fireball in the Hierarchy and choose Save as artifact…. In the save dialog, make a folder called artifacts, open it, and save the file as fireball.artifact. Delete the fireball left in the scene.
  7. Select beast, press Add component, and choose SpawnerComponent. Press + beside Entries, open the new [0] row, type fireball as its Name, and drag artifacts/fireball.artifact from Assets onto its Artifact.
  8. Press Ctrl+S.
on_create
Works out its velocity as it is made: the way from the muzzle, kept_point("muzzle", …), to where it is made, a little out from it along the aim, as a unit vector, times its speed and the beast’s kept haste. Its art faces left, and is mirrored to fly right. Then await wait(lifetime), and destroy_self(): a fireball that has met nothing burns out.
loop and translate
Moves the node by velocity × dt each step: a fireball is not a body, it just flies.
on_enter
Meeting the knight or a wall, it removes itself. The knight’s own Area hears it first, and counts a heart: a fireball is in the group enemy, like a hound.

Check it works

The Hall’s left end: the knight running along the floor under a ledge, with five hearts at the top left, a lantern and pillars behind him, a red heart floating just over the ledge, and no health bar yet
The arena as the fight begins: the camera pulled back so that the room shows from the floor to the vaulted ceiling, the gate a pale strip at the left edge, the knight beside it with five hearts, the Nightmare standing at the right under a lit window, and the boss bar, a dark slot with a full red fill, at the bottom centre
The Nightmare has breathed: a small red fireball flying left from its mouth toward the knight, over the floor, with the boss bar still full
The Nightmare galloping across the arena toward the knight in its gallop pose, its smoke streaming behind it, with four hearts left at the top left
The Nightmare in the air at the top left of the picture, mid-leap over the knight, who stands at the left edge with three hearts left

To fight the beast the knight needs the sword, which is at the far end of the Crypt, and a game opened on the Hall has none: a new game keeps no relics. Borrow it. Open scenes/crypt.ron, select sword, and press Ctrl+C. Open scenes/hall.ron, right-click Hall, choose Paste as child, and set its Position to 120 and 130: the knight takes it on his first steps. When you have tried the fight, select it and press Delete, and save.