Skip to content

Enemy Attacks

vitellaryjr edited this page Feb 16, 2023 · 34 revisions

Relevant files: wave.lua, arena.lua, bullet.lua, solid.lua, soul.lua

Waves

A wave is an attack that an enemy uses. Wave files are responsible for containing information about a wave and spawning the bullets and handling all behavior for the attack. Wave files are made by extending the Wave class, and should go in scripts/battle/waves. A basic wave file looks something like this:

local WaveName, super = Class(Wave)

function WaveName:init()
  super:init(self)
  self.time = 5
end

function WaveName:onStart()
  -- do stuff here
end

return WaveName

As per most classes, waves need certain information to be defined in their init(). Variables and functions that should be used for initiation are:

time: How long the wave will last, in seconds. If it's set to -1, the wave will last forever.
setArenaPosition(x, y): Sets the initial position of the arena, relative to the topleft of the screen.
setArenaOffset(x, y): Sets the initial position of the arena, relative to its default position.
setArenaSize(width, height): Sets the initial size of the arena. If only one argument is provided, it will set the arena to a square with the side length specified. The default size of the arena is 142x142.
setArenaShape(...): Sets the initial shape of the arena, taking in a series of tables with 2 values each, referring to the x and y position of each point of a polygon. For example, the default shape of the arena is {0, 0}, {142, 0}, {142, 142}, {0, 142}.
setSoulPosition(x, y): Sets the soul's starting position, relative to the topleft of the screen.
setSoulOffset(x, y): Sets the soul's starting position, relative to its default starting position.

Waves also have the following variables and functions for usage:

timer: An instance of a timer from the hump libraries. It is recommended to do actions based on this timer instead of Game.battle.timer, as the wave's timer will only update while the wave is active.
bullets: A table of all bullets added to the wave through spawnBullet().
objects: A table of all objects added to the wave through either spawnBullet(), spawnSprite(), or spawnObject().
encounter: Refers to the instance of the encounter for the battle. Gets defined after init(), and thus cannot be used within that function.
finished: Whether the wave is finished or not. Can be set to true to manually end a wave.
spawnBullet(bullet, ...): Spawns a bullet parented to Game.battle. bullet can be either an existing instance of a Bullet class, a string referring to a bullet ID (see Bullets below), or a path to a sprite for the new bullet to use. If bullet refers to a bullet ID, ... will be passed into the init() function for the new bullet class; if bullet refers to a sprite path, the first two arguments of ... refer to the coordinates of the bullet, relative to the topleft of the screen. After successfully spawning a bullet, it adds the bullet to the wave's bullets table. Returns the bullet if it succeeds in making one.
spawnBulletTo(parent, bullet, ...): Spawns a bullet and parents it to the specified object. Follows the same rules as spawnBullet().
spawnSprite(texture, x, y, layer): Spawns a sprite parented to Game.battle that will be automatically removed when the wave ends. texture is the path to the sprite image, x and y are coordinates relative to the topleft of the screen, and layer is the layer of the sprite. Automatically sets the sprite's origin to 0.5, 0.5 (meaning its position refers to its center), and its scale to 2. If layer is not specified, it defaults to LAYERS.above_arena (see Global Variables for a list of layer constants). After successfully making a sprite, it adds the sprite to the wave's objects table. Returns the sprite.
spawnSpriteTo(parent, texture, x, y, layer): Spawns a sprite and parents it to the specified object. Follows the same rules as spawnSprite().
spawnObject(object, x, y): Parents the specified object to Game.battle, and automatically removes the object when the wave ends. object is any Object instance, and x and y are optional coordinates of where the object should spawn. Adds the object to the wave's objects table. Returns the object.
spawnObjectTo(parent, object, x, y): Parents the object to the specified parent. Follows the same rules as spawnObject().
getAttackers(): Returns a table of enemies that the attack belongs to.

Overriding functions is necessary for waves to work. Wave extends Object, meaning it has the usual overridable functions like update() and draw(). The following functions can be overridden:

onStart(): Called when the attack starts, after the arena finishes transitioning. Any objects that need to be set up at the beginning (eg. bullets, sprites, timer loops, etc) should be done here, not in init(), since the wave will not update until after onStart() is called.
onEnd(): Called when the attack ends, before the arena transitions out.
canEnd(): Returns whether the wave is allowed to end. Returns true by default. Can be overridden to prevent a wave from ending.
onArenaEnter(): Called when the arena is first created for a wave, before it transitions. If this returns true, the wave will begin updating immediately, instead of waiting for the arena to finish transitioning.
onArenaExit(): Called when the arena finishing transitioning after an attack ends.

There are many, many valid ways to code a wave. To give some pointers, a few examples of how you could code a wave include:

  • Using self.timer, create a repeating function that occurs every x seconds to spawn an instance of a bullet extension. An example of this is the Basic:onStart() function in the example mod.
  • Similar to above, use self.timer to create instances of the bullet class, and update the bullets in update(), or set their physics table to have them move automatically.
  • Using self.timer, create a coroutine that handles creating and moving bullets. Kristal implements extra functionality for the wait(delay) function: if delay is not provided, it will wait for a single frame.

Arena

The arena refers to the shape that the player is confined to during a wave. You can get the current instance of the encounter's Arena by getting Game.battle.arena. The following variables and functions are available for usage during a wave:

color: Color of the arena border.
bg_color: Color of the background of the arena.
x, y: Refers to the center of the arena. May not be accurate when transformations occur, see getCenter() below.
left, right: Refers to the horizontal position of the furthest points in each direction. May not be accurate when transformations occur, see getLeft() and getRight() below.
top, bottom: Refers to the vertical position of the furthest points in each direction. May not be accurate when transformations occur, see getTop() and getBottom() below.
shape: A table containing a list of tables with 2 values each, referring to the x and y position of each point of a polygon.
line_width: A number referring to the thickness of the arena borders. If changed, you must call setShape() to update the arena.
mask: An Object that masks its children to the arena. Its position is at the center of the arena.
collider: The ColliderGroup used to check collision. Usually unnecessary to check, as Game.battle:checkSolidCollision(collider) exists.

getCenter(): Returns x and y, properly accounting for transformations.
getLeft(), getRight(), getTop(), getBottom(): Same as left, etc, but properly accounts for transformations.
getTopLeft(), getTopRight(), getBottomLeft(), getBottomRight(): Returns x and y coordinates similarly to combining the above functions.
getBackgroundColor(): Returns bg_color.
setSize(width, height): Changes the size of the arena if it's rectangular.
setShape(shape): Sets the shape of the arena to any polygon. shape is a table, containing a list of tables with 2 values each, referring to the x and y position of each point of the polygon.
setBackgroundColor(r, g, b, a): Sets bg_color to the color specified. a is an optional argument, defaulting to 1.

If an object is parented to either Game.battle.mask or Arena.mask, then the object will only render inside the arena. The layer of both mask objects is equivalent to the arena's layer, and thus object layers will be relative to that.

Bullets and Solids

Bullets are the most important part of any attack, and as such, they can be complicated to set up. This section will first explain creating generic bullets without making new files for them, then detail how to extend them.

By default, all bullets have a scale of 2, and an origin of 0.5, 0.5. This means its collider, sprite, and any other children will be relative to the topleft of the bullet's width and height, which are automatically set to the dimensions of its sprite if the bullet is initiated with a path specified (in other words, making all children relative to the topleft of the bullet's sprite). When making any bullet's collider, remember that it will be scaled 2x because of this.

Generic

Generic bullets can be spawned through Wave:spawnBullet(sprite, x, y). The Bullet class has the following variables and functions which can be changed during initiation or after spawning:

wave: A reference to the current Wave class that is active. Gets defined after init(), but only if spawned through spawnBullet(); otherwise, it is never defined.
tp: The amount of TP (in percentage) the player gains from grazing the bullet. Defaults to 1.6 (1/10th of a defend).
time_bonus: The number of frames, based on 30fps, that the wave's length will be reduced by when grazing the bullet. Apparently this is a mechanic in Deltarune.
damage: Amount of damage the bullet does. If not provided, the game will calculate damage based on the enemy's attack.
inv_timer: How long the player will be invulnerable after being hit by the bullet.
destroy_on_hit: Whether the bullet will be removed when it collides with the player. True by default.
remove_offscreen: Whether the bullet will be removed when it goes offscreen. True by default.

setSprite(texture, speed, loop, on_finished): Sets the sprite of the bullet to the specified path, and changes the bullet's width and height variables to the dimensions of the sprite. speed, loop, and on_finished will be passed into the sprite's play() function.
getDirection(): Returns direction if it's defined, otherwise returns rotation.
isBullet(id): Returns whether the bullet either is the bullet with the specified ID, or extends it.

Extensions

All files in scripts/battle/bullets will be loaded as bullet files. By default, each bullet's ID is its file path after that point (eg. scripts/battle/bullets/sub/attack.lua will have the ID sub/attack). You can change this ID by passing in an extra table to the Class() function, setting id to the string you'd prefer, though this is not necessary. A basic bullet file doing this looks something like this:

local BulletName, super = Class(Bullet, {id="bulletID"})

function BulletName:init(x, y)
  -- the original Bullet:init takes x and y as arguments,
  -- with an optional texture argument
  super:init(self, x, y)

  self:setHitbox(-8,-8,16,16)
end

return BulletName

When extending a bullet class, some extra functions are available for overriding:

onCollide(soul): Called when the player collides with the bullet, regardless of invincibility frames. By default, calls onDamage() if the player does not have active invincibility frames, and removes the bullet if destroy_on_hit is true.
onDamage(soul): Called when the player collides with the bullet without invincibility frames. By default, damages the player and sets their invincibility frames.

A reminder: Since global variables aren't created for bullet classes, you'll need to use the alternate extension method mentioned in the extra notes for Objects if you want to extend an existing extension of a bullet. Another important thing to note is that wave for bullets is set after initialization, and thus init() will not be able to reference it. If you need to refer to wave for setting up the bullet, you can override onAdd() (see Objects for more detail).

Solids

Solids can also be added during waves, though the processes for spawning and extending them are different. Solids are simply Objects, and must be instantiated by calling the global Solid class, in the form of Solid(filled, x, y, width, height). filled is a boolean determining whether the solid should draw its collider with the arena's color, x and y are the coordinates of the topleft of the solid, and width and height are optional arguments determining the size of the solid if it's rectangular. If you want the solid to be a shape other than rectangular, you can set its collider directly (see Colliders for more detail). Once you instantiate a solid, you can add it to the wave via Wave:spawnObject().

If you want to extend a Solid, you'll need to create a new Object for it (see Creating new Objects for more detail). Solids have a function called onSquished(soul) that can be overridden if extended, which is called when the solid crushes the soul against another solid or the arena. By default, this function takes the value of the solid's squish_damage variable (which defaults to 80), damages a random party member by that amount, explodes the soul, and ends the wave immediately. This behavior may be temporary, and solids moving is not yet fully functional, so expect potential bugs if you try to do so.

Soul

The soul is the object that the player controls during waves. You can retrieve the current instance of the soul through Game.battle.soul. The Soul class has a few variables and functions available:

speed: The speed the soul moves in pixels per frame at 30 fps. Defaults to 4 normally, and 2 when cancel is held.
transitioning: True while the soul is moving to and from the arena between turns.
last_collided_x, last_collided_y: Numbers referring to whether the soul collided with a solid the last time it moved. 0 means it didn't collide, 1 means it collided to the right or down respectively, and -1 means it collided to the left or up respectively.
moving_x, moving_y: Numbers referring to how far the soul moved last time it moved.
noclip: Whether the soul ignores solid collision.
slope_correction: Whether the soul will be moved along slopes when it moves into them.
isMoving(): Returns whether the soul is moving.
move(x, y, speed): Moves the soul x pixels horizontally and y pixels vertically, multiplied by speed if provided. Returns whether the soul successfully moved and whether the soul collided with something.

The primary reason a coder may need to know about souls is to create new soul modes. Unlike most objects that extend classes, new soul classes don't get their own folder; instead, soul extensions should go in scripts/objects, creating a global variable for them (see Creating new objects for more detail). When extending Soul, the following functions can be overridden:

doMovement(): Called in update. Handles moving the player.
onCollide(bullet): Called when the player collides with a bullet. Handles calling the bullet's onCollide().
onDamage(bullet, amount): Called when the soul takes damage from a bullet. Does not handle actually hurting party members.
onSquished(solid): Called when the soul is squished by a solid. Handles calling the solid's onSquished().

There are two primary ways to use a custom soul mode. The first is by overriding Encounter:createSoul() to set soul mode at the start of a wave; the Soul object returned by that function will be used for the wave. The other method is by calling Game.battle:swapSoul() with a new soul instance, if the player soul already exists, which can be used to change soul mode in the middle of a wave. For example, if you have a custom soul mode at scripts/objects/BlueSoul.lua, you would call Game.battle:swapSoul(BlueSoul()) to set the battle's soul mode to that.

Clone this wiki locally