Skip to content

Lua Badeline Boss Starter Guide

JaThePlayer edited this page Nov 3, 2024 · 7 revisions

Frost Helper 1.45.0 added a new Lua Badeline Boss entity, with which you can make badeline bosses with fully custom patterns!

Setup

Create a .lua file in your mod with a unique path. In this tutorial, the file will be at Assets/testMod/myCoolBoss.lua

image

In a map editor, set the Filename field of the Lua Boss entity to this path, without the file extension. For our path, this will be Assets/testMod/myCoolBoss. Remember to ALWAYS use forward slashes (/) instead of backslashes (\), even on Windows.

image

Note: If the game can't find the file, try restarting Celeste - Everest doesn't recognize new assets added at runtime.

First Code

Open the .lua file. This file has to declare a global function ai(), which will be called once after each hit. You can paste this example code into it, then test in-game:

function ai()
    while true do
        beginCharge()
        wait(0.8)
        shoot()
        wait(1)
        beam()
        wait(0.5)
    end
end

This simple ai demonstrates the 4 most common helper functions used in lua bosses:

  • beginCharge() plays an animation for charging up a shot. This is optional and has 0 gameplay impact, but can be nice for indicating what the boss does.
  • wait(timeInSeconds) delays code execution for the given amount of time.
  • shoot() makes Badeline shoot a normal bullet at the player. This does NOT pause code execution!
  • beam() makes Badeline start the beam attack, and pauses code execution until it finishes.

Most of the time, you'll want the ai function to contain an infinite loop (done here with the while true do ... end)

Checking vanilla patterns

This file contains a port of all vanilla boss patterns to Lua. Feel free to check out this file for inspiration.

Most of that code uses the same concepts the previous example showed, but there is one new thing. Let's look at this snippet from vanillaPatterns.sequence03():

-- this for loop makes the code inside of it run 4 times.
for i = 0, 4, 1 do
    if player then
        -- creates a local variable named `at`, which stores the player's center location.
        local at = player.Center
        shootAt(at)
        wait(0.15)
        shootAt(at)
        wait(0.15)
    end
    if i < 4 then
        beginCharge()
        wait(0.5)
    end
end
  • player is a global variable available in lua bosses, which stores a reference to the player. If the player dies, this variable might be nil, so always checks whether it exists first by doing if player then ... end, like in this code snippet.
  • shootAt(location) makes Badeline shoot a bullet, while aiming at the given location (a Vector2) instead of the player. In this case, it is used to make sure that both of the bullets are shot at the same angle, regardless of whether the player moved between the shots.

Custom Bullets and Beams

The shoot, shootAt and beam functions actually accept an optional argument, which allows customizing the bullets/beams.

Custom Bullets

The shoot and shootAt functions accept a table containing any number of these values:

  • angle (number?, default: nil) - Specifies the angle (in degrees) to shoot at, instead of targeting the player.
  • angleOffset (number, default: 0) - The offset in angle (in degrees) to apply to the shot. You can use this to shoot bullets not exactly at the player.
  • speed (number, default: 100) - Specifies the movement speed of the bullet (in pixels/second)
  • waveStrength (number, default: 3) - How much (in pixels) the bullet waves around perpendicularly to the angle.

This example code makes Badeline shoot bullets in 4 cardinal directions, then the next time, in 4 diagonals.

function ai()
    local offset = 0
    while true do
        shoot({
            -- this argument specifies the angle to shoot at, instead of targeting the player
            angle = 0 + offset,
        })
        shoot({
            angle = 90 + offset,
        })
        shoot({
            angle = 180 + offset,
        })
        shoot({
            angle = 270 + offset,
        })
        wait(0.50)
        offset = offset + 45
    end
end
Celeste.2023-08-20.18-25-23.mp4

Another example:

function ai()
    while true do
        beginCharge()
        wait(0.15)

        shoot({
            angleOffset = 30,
        })
        shoot({
            angleOffset = -30,
        })
        shoot({
            waveStrength = 12,
        })

        wait(1)
    end
end
Celeste.2023-08-20.18-34-35.mp4

Custom Beams

The beam function accepts a table with these values as arguments:

  • rotationSpeed (number, default: 200) - How fast the beam rotates to follow the player. Very high values might make the beam unavoidable.
  • followTime (number, default: 0.9) - How long (in seconds) the beam follows the player.
  • lockTime (number, default: 0.5) - How long (in seconds) the beam stays locked to a certain angle after followTime expires, before the beam actualy gets shot and becomes deadly.
  • angle (number?, default: nil, requires Frost Helper 1.47.0) - Angle (in degrees) to shoot at. When present, the beam is no longer shot at the player, and followTime gets ignored completely.
function ai()
    while true do
        beam({
            followTime = 0.1,
            rotationSpeed = 2000,
        })
        beam({
            followTime = 0.5,
            lockTime = 0,
            rotationSpeed = 300
        })
    end
end
Celeste.2023-08-20.18-41-42.mp4

Multiple beams at once

To shoot multiple beams at once, as of Frost Helper 1.47.0, you can pass multiple arguments at once to the beam function, and all the beams will be shot at the same time.

function ai()
   while true do
        beam({
            angle = 0
        },
        {
            angle = 45
        },
        {
            angle = 90
        },
        {
            angle = 180
        },
        {
            angle = 270
        },
        {
            angle = 315
        })
        wait(0.3)
    end
end
Celeste.2023-12-21.19-53-51.mp4

Changing AI based on current node

If you want your boss to act differently based on how many times it has been hit, you can use the nodeIndex variable. It stores which node the boss is currently on.

function ai()
    if nodeIndex == 0 then
        while true do
            shoot()
            wait(1)
        end
    else
        while true do
            beam()
            wait(0.5)
        end
    end
end
Celeste.2023-08-20.18-58-10.mp4

If you have many different ai's at once, it might be nice to organize the code a bit, like so:

local function firstNode()
    shoot()
    wait(1)
end

local function secondNode()
    beam()
    wait(0.5)
end

-- stores a list of all ai's to use, in order
-- for each node you have in the boss, there should be one more function stored here
local ais = {
    firstNode, -- will be used when the boss is in the starting position
    secondNode, -- will be used after the boss gets hit once
    secondNode, -- will be used after the boss gets hit twice
}

function ai()
    local func = ais[nodeIndex + 1]
    while func do
        func()
    end
end

This code yields the exact same result as the previous example, but it's much simpler to add new ai's based on the current node.

Callbacks

On top of the ai function, your boss can also provide additional functions to call on certain events.

onHit

The onHit function gets called immediately after the boss gets hit.

function onHit()
    wait(0.25)

    -- isFinalNode is a global variable which tells you whether the boss is about to move to the final node and stop attacking.
    if isFinalNode then
        -- helper function we didn't talk about earlier.
        -- It shatters all spinners on screen, like in the final badeline fight room in 6a.
        -- Supports many custom spinners from mods.
        shatterSpinners()

        -- Another helper function available globally, it allows you to enable/disable a session flag. `getFlag` also exists.
        setFlag("bossKilled", true)
    else
        -- More complicated use of the function - only shatter frost helper custom spinners with specific options
        shatterSpinners({
            -- A list of entity SID's that will get shattered by this function. Technically, they don't even need to be spinners!
            types = { "FrostHelper/IceSpinner" },
            -- This function gets called for each entity that matches the `types` list. If it returns false, that entity wont get shattered.
            filter = function (spinner)
                return spinner.AttachGroup == nodeIndex - 1
            end
        })
    end
end
Celeste.2023-08-20.15-25-58.mp4

Misc Code Snippets

Seeded Randomness

Lua's built-in math.random() can be used to get random values, and math.randomseed(seed) to seed it. To make a map with random attacks remotely TAS-able, you can seed the randomness using the current session timer: (requires Frost Helper 1.47.2)

-- seed rng with the session timer
seedRandomWithSessionTime()
-- get random value in range [1, 4]
math.random(1, 4)

Doing multiple things at once

If you want to perform multiple actions at once, even if they're blocking, you can use the startCoroutine(func) function. This example allows the boss to shoot beams constantly while also toggling the dark flag each second

function ai()
    -- the code inside the callback will run in the background without blocking the attacking ai.
    -- the coroutine will automatically end once the boss gets hit.
    startCoroutine(function ()
        while true do
            setFlag("dark", true)
            wait(1)
            setFlag("dark", false)
            wait(1)
        end
    end)

    while true do
        beam()
        wait(0.5)
    end
end