Undertale background

Welcome to Unitale's documentation!

NOTE: There will be casual spoilers all over the place. If you haven't played or completed Undertale yet, it's very recommended that you do so first! Don't say you weren't warned.

You probably want to get right in, so let's keep it short. Honestly, the fastest way to get started is to check out the existing encounters and tinker with them. The documentation is for reference if you want to know the specifics of everything. Help is provided on the /r/Unitale subreddit. These are the categories on the left, and what to expect in them:

Basic setup
Details folder structure of mods, how the game reads folders and expects files, that kind of stuff. Recommended to start with.

Terminology
This is a short page on how what things are called in the documentation. For instance, the white box that the bullet dodging occurs in is referred to as the "arena". Recommended read so you don't get lost in later parts of the documentation.

API - Text commands
This is where all the special commands are that you can use in your dialogue boxes. Text effects, colours, character voices, automatic skipping, you name it. You can even add commands to run Lua code!

API - Game events
If you want to get your hands dirty with Lua, these are the functions the game will use from your script, and at what point they're used. Useful for if you want to have certain events occur during specific times, such as before/after using an act command or item, or just before the fight starts. A must-read for interactive fights.

API - Functions & Objects
These are most custom functions and objects that you can use in your scripts, detailing how to use them and what they do. Want to control the music, check for game events or handle keyboard input? Global variables so you can communicate between scripts? Need to know how much HP the player has, damage them, heal them? This is where to go! Projectile management and sprite management are split off into separate sections below.

API - Projectile management
This is where you can read details about how to create projectiles, what you can do with them and some short examples.

API - Sprites & Animation
This section is about creating sprites and how to control them. By combining the sprite functions, you can create animations.

Controls

Arrow keys, Z (or Enter), X (or left/right Shift) - The same as in Undertale.

Esc - Resets to the Mod Selection screen to assist in quickly testing mods. Will not work on the disclaimer/Game Over screen.

F9 - Toggles the debug console. Currently, you can write text to this with DEBUG("your text here") in your scripts.

Alt+Enter - Fullscreens the game. F4 works too, but not on the disclaimer/Game Over screens.

Basic setup

IMPORTANT NOTE: Due to the hasty implementation of file loading, your encounter will currently break if certain folders are missing. To make sure this doesn't happen, please preserve the folder structure of the example mods, even if you have no files in a certain folder. It's an early alpha and we're working on it!

By this point you'll probably want to set up an encounter of your own. Right now, we only have encounters implemented, and even that's still a work in progress! Planning ahead for the future, scripts will eventually be set up as follows:

  • Monster scripts - will contain information about your enemies. Here, you'll set things like their ATK, DEF, HP, random comments that might show up as encounter texts, random dialogue, and what ACT commands they have.
  • Encounter scripts - will contain a set of monsters, a set of wave scripts (that you can modify at any point), size of the arena, custom interactions for items, etc.

    A lot of Unitale's functions will be run on the Encounter scripts first. If there's no custom behaviour on the encounter, many of these functions will then also be run on the Monster scripts. More information on this can be found in the API - Game events section.
  • Wave scripts - will contain an update function. You may use these to spawn, track, modify and otherwise interact with bullets during the defending phase of the game.

Eventually, you will be able to call up your encounter scripts from various places in the overworld. Seeing as we are missing an overworld, the current alpha of the game lets you select which encounter you want to play.


Files and directories

It's fairly self-explanatory. If you just want to move on fast, feel free to skip this section and go to the next one. If for any reason some of your files don't work, you might want to read this anyway.

Scripts

The Encounter scripts are located in YOUR MOD/Lua/Encounters/. The Monster scripts are in YOUR MOD/Lua/Monsters/, and your wave scripts at YOUR MOD/Lua/Waves/. If you're getting started, check out these files in example encounters to see how they're put together. Starting from 0.2, there's also an optional YOUR MOD/Lua/Libraries/ folder. You can put libraries other people have made in here (or create your own) for use in your other scripts. Libraries/modules are more Lua functionality than they are Unitale functionality, so please read up about them here instead. There is an example encounter included making use of one such library.

Music

Music can be put in YOUR MOD/Audio/. Your music must be in .ogg or .wav format. Audacity can export to .ogg if you're missing the appropriate software.

Sounds

Sounds can be put in YOUR MOD/Sounds/. They must be in .ogg or .wav format. You can play them with Audio.PlaySound(filename); more on this in API - Functions & Objects.

Voices

Voices can be put in YOUR MOD/Voices/. They must be in .ogg or .wav format, although .wav is generally recommended. You can use them with the [voice] text command; more on this in API - Text commands.

Sprites

Sprites can be put in YOUR MOD/Sprites/. They must be in the .png format. Note that most vanilla Undertale monster sprites start with a small base resolution, then resize the sprite to 2x its original resolution for an oldschool look. To add a background you can have one file titled bg.png in the sprites folder. This image will stretch over the entire background, so 640x480 resolution is recommended. This is not the final solution for backgrounds; it just beats not having one.


The Default directory

Starting from 0.2 Unitale now has a default directory. This is where resources from Undertale reside. It is not advised to modify files in this directory, as they are expected to be the same across all installations.

If you wish to replace any of the files for your mod, create a file with the same name at the same location instead. For instance, if you want to change the player soul hurt sound, don't replace Default/Sounds/hurtsound.wav. Instead, create a new file located at YOUR MOD/Sounds/hurtsound.wav.


Basic variables and their usage

Now that all of that's out of the way, it's time to set up the basics of an encounter! The fastest way to get started is to copy the 'Encounter Skeleton' mod and play with the values in it, then either copying over existing examples' code, or writing your own. This section serves to explain the variables you see.

Encounter script variables

music = "yourmusicname_without_extension"
encountertext = "Vegetoid came out of\rthe earth!"
nextwaves = {"bullettest_wavy", "bullettest_homing"}
wavetimer = 4.0
arenasize = {155, 130}

enemies = { "vegetoid" }

enemypositions = {
{0, 50},
{-70, 30},
{70, 30}
}

music - Name of your encounter's starting music, without the file extension. If this variable isn't present, it'll play Undertale's default battle theme. If you don't want any music, call Audio.Stop() in the EncounterStarting() function. For more information see API - Game events.

encountertext - Set the initial text of your encounter here. After that, you can modify it at any time in preparation for the next turn. encountertext gets read out at the start of every new turn (i.e. you going back to the FIGHT/ACT/ITEM/MERCY selection).

nextwaves - A list of all simultaneous attack waves you want when the monsters start their attacks. You can modify this at any time, and it'll get read out before the enemies start their attack. For most boss-type encounters, you'll likely only want one wave simultaneously - but you can get creative here.

wavetimer - How long it takes for the defending step to end. If this isn't set anywhere, it'll be the default 4.0 seconds.

arenasize - The inner size of the box the player's constrained to. {155, 130} is the default size for a lot of basic Undertale encounters. Papyrus' battle, for instance, has this at {245, 130} most of the time. You may modify this at any time - it'll only get read out before the enemies start their attack.
Note: lowest possible setting is {16, 16} - this is the size of the player's soul. Anything lower will be set to 16 anyway.

enemies - Defines the names of your enemy scripts that will be used in your encounter. In this example. vegetoid.lua will be used from the Monsters folder. After initialization, the names will be replaced by Script controller objects you can use to control your monster scripts. Refer to API - Functions & Objects for more information.

enemypositions - Defines where the enemies are on the screen. {0, 0} means they're centered just above the arena, with 1 pixel of space inbetween. {-30, 0} means above the arena to the left; {50, 80} means 50 pixels to the right and 80 pixels above that center.
You will always need at least as many enemy positions as enemies in your encounter. In this example we have 3 enemy positions set to show you how you can define more than one, but since this example only contains Vegetoid you only really need one position.


Monster script variables

comments = {"Vegetoid cackles softly.", "Vegetoid's here for your health."}
commands = {"Talk", "Devour", "Dinner"}
randomdialogue = {"Fresh\nMorning\nTaste", "Farmed\nLocally,\nVery\nLocally"}
currentdialogue = {'Eat\nYour\nGreens'}
cancheck = true
canspare = false

sprite = "vegetoid_sprite"
dialogbubble = "rightshort"
name = "Vegetoid"
hp = 20
atk = 6
def = 6
xp = 6
gold = 1
check = "Serving Size: 1 Monster\nNot monitored by the USDA"

comments - A list of random comments attached to this monster. You can retrieve one at random using the RandomEncounterText() function in your Encounter script. See API - Functions & Objects for details.

commands - A list of ACT commands you can do. Listed in the ACT menu and used in HandleCustomCommand(). See API - Game events for details. Note that the behaviour for Check is built-in, and shows you the monster's name followed by the ATK and DEF, and then the check variable you'll see all the way down.

randomdialogue - A list of random dialogue the monster can have. One of these is selected at random if currentdialogue is nil (i.e. has no value).

currentdialogue - The next dialogue for this monster. This overrides the random dialogue and is meant for special actions (e.g. you hit Vegetoid's green carrots after selecting Dinner from the ACT menu). This variable gets cleared every time after it's read out in the monster dialogue phase. This is done so you don't have to take care of managing it manually.

cancheck - Either true or false. You can leave this line out; it will be true by default. If set to false, it will disable the default Check action that shows up in your ACT menu. If you want a custom Check action, you can add it back into your commands table, and handle it like any other custom command. See API - Game events for details.

canspare - Either true or false. If you leave this line out, it'll be set to false by default. If you change this to true, your monster's name will turn yellow and it will be spareable.


sprite - Name of the sprite in your Sprites folder, without the .PNG extension. This is the initial sprite for your monster. It can be changed using SetSprite(name); see API - Functions & Objects for details.

dialogbubble - What dialogue bubble will be used for the monster's dialogue. You can change this at any time, but this must be initially set to something. For a list of all possible options, check the dialog bubble names chart; it's also in the sidebar.

Positioning of the bubbles is done automatically.

name - Monster name. Fairly self-explanatory; shows up in the FIGHT/ACT menus. Can also be changed at any time.

hp - Your monster's max HP, initially. After the fight has started this value will always accurately reflect your monster's current HP. You can then modify this value to change your monster's current HP.

atk - Your monster's ATK. Only used in the default Check handler; bullet damage is set through wave scripts. If you're not using the default Check you can leave this out.

def - Your monster's DEF.

xp - Your monster's XP upon actually defeating them. You only get this by killing the monster. Currently unused.

gold - Gold you get from either killing or sparing this monster. Since the gold can change based on whether you kill or spare the monster, you can modify this at any time up until the fight ends. Currently unused.

check - When checking with the default Check option, this is what's listed under the monster's name, ATK and DEF.


Wave script variables

Wave scripts don't have any variables that are read out from the start, but you can define your own. An instance of a wave script is made when you start defending, and is destroyed when the defending step ends. As such, you can't store variables in a wave script for reusing later. Use the Encounter script to keep track of things, or the SetGlobal/GetGlobal functions (API - Functions & Objects for details).

Terminology

arena - The inside of the white box in which the player is allowed to move.

bullet - Everything in a wave that can collide with you. Flowey's pellets would be referred to as bullets, but so would Papyrus' bones, anything Woshua can shoot (of any colour), even the dancing Migosp.

encounter text - The text that shows up before you've selected FIGHT/ACT/ITEM/MERCY.

monster dialogue - Text from monsters in an encounter, often seen before attacking. Can also be multiple dialogue boxes for special encounters.

dialog - A user interface component that contains text. For example, the battle dialog window. The distinction between dialog and dialogue is that dialog refers to interface windows containing text, and dialogue refers to the speech content of monsters.

wave - A single attack behaviour (or attack 'wave', to say), measured from when you start defending until when it stops. Vegetoid's bouncing vegetables attack would count as a wave. Papyrus' special Cool Dude attack would also count as a wave. Unitale works with 'wave scripts' for attacks; you can use multiple wave scripts at the same time for when you have various monsters.

Text commands

There are two types of text commands: commands that get executed instantly, like text color and effects; and commands that get executed inline, as they're displayed, like wait commands and character voices. Note that currently, if you skip a text command (with X), it'll also skip all inline commands that were still in your text.

On line breaks: there are actually two different kinds. In UI messages where asterisks are used, you can use \n to start a new line with an asterisk. If you want a new line without an asterisk, use \r.

This is different for monster dialogue that isn't prefixed with asterisks: Always use \n for line breaks here.

Instant commands

[color:rrggbb] This sets the text color for all text after this command to the specified hex code. It resets per dialogue. [color:ff0000] would be red, [color:555555] a dark grey. The main colours the game uses are as follows:

[color:ff0000]Determination
[color:003cff]Integrity
[color:00c000]Kindness
[color:ffff00]Justice
[color:d535d9]Perseverance
[color:fca600]Bravery
[color:42fcff]Patience

If you have to use colours, try to stick to these. While the option for any colour is offered, actual usage in Undertale is very limited.

The default UI text is plain white and the default enemy dialogue text is plain black; [color:ffffff] and [color:000000] respectively.

[starcolor:rrggbb] Same usage as color but only affects the first asterisk in a dialogue box that has asterisks. This is a dirty workaround as there's no other way to change the colour of the first asterisk otherwise. It'll be changed at some point.

[noskip] Prevents this dialogue from being skipped by pressing X. Has no effect in encounter texts as you don't control that.

[instant] Instantly shows the entire text without having to wait or press anything.

[effect:x] or [effect:x,intensity] Sets the text effect for the entire message, regardless of position. You can use the following effects:

  • none: No effect on the text.
  • rotate: Rotating text, most random monsters have this by default. Intensity sets how far letters rotate, in pixels. Default is 1.5.
  • shake: Shaking text. Flowey uses this sometimes. Intensity sets how far the letters offset, in pixels. Default is 1.0.
  • twitch: Letters twitch occasionally, battle UI has this by default. Intensity sets how far a letter should shake. Default is 2.0.

Note that the twitch effect should, at a later point, let you set shake frequency. Unfortunately you can't do this yet.

[font:x] Sets the font for this dialogue. Usually includes a default voice. As the [font] command can change both text colour and voice, if you want to have your own voice/text color do it after the font change. Possible options:

  • uidialog: Default large pixel font for UI.
  • monster: Default font for almost all monsters in the game.
  • sans: sans. use lowercase. uppercase works, but... sans.
  • papyrus: PAPYRUS! USE UPPERCASE ONLY. LOWERCASE WON'T WORK.
  • wingdings: Wingdings.
  • uibattlesmall: The font used for the character name, HP and level.

For all default fonts, check out the Default/Sprites/UI/Fonts folder. Every font with a matching .xml file is mapped.

Inline commands

[w:x] The wait command. This will pause your textbox for x frames.

[waitall:x] Like the wait command, but applies to all letters after this command. It resets per dialogue. Useful for slow text.

[voice:filename] Sets the voice (sound per letter) to a sound located in YOUR MOD/Sounds/Voices. Applies to all letters after the command. It resets per dialogue. This has to be a .wav file, and you shouldn't include the file extension when using [voice]. If your voice sound is YOUR MOD/Sounds/Voices/mettaton.wav, you can use it with [voice:mettaton].
[voice:default] resets to the default voice (beeps). If you have a voice sound named 'default', it will be ignored.

[novoice] Removes the voice for the letters after this command. It resets per dialogue. Useful for when you should be burning in hell.

[next] Skips to the next dialogue automatically. You can also use this for textbox trickery. Here's an example to replicate Flowey's text-changing effect if you dodge the Friendliness Pellets(tm) twice.

first line:  "[noskip][voice:flowey][effect:none]RUN. [w:30]INTO. [w:30]THE.\n[w:30]BULLETS!![w:30][next]"
second line: "[instant][effect:none]RUN. INTO. THE.\nfriendliness\npellets"

[func:x] or [func:x,argument] The most powerful command. If the previous text commands were established official characters, [func] is some kid's deviantArt original character that never dies and has all the superpowers.

In all seriousness, [func] allows you to execute any function from your script in line with the text. Refer to the examples below.

your dialogue: "hoi hoi this is dog [func:dog] and now the music changed" 
   
function dog()
    Audio.LoadFile("dog_music") --plays dog_music.ogg (or .wav) from your Audio folder! for built-in functions like this, refer to section API - Functions & Objects
    --insert more code here, any code!
end
your dialogue: "dog with arguments!! [func:newmusic,temietheme] so intense!" 
   
function newmusic(yourargumentname)
    Audio.LoadFile(yourargumentname) --with this example, it'll load 'temietheme.ogg (or .wav)'...
    --and then play it! THE FUTURE IS NOW! By using an argument, your function can be more versatile.
end

Game events

This section is all about game events. Game events are functions in your scripts that the Unitale engine runs at various points in the game. By changing up your behaviour depending on the actions the player takes, you can go beyond a basic encounter and make it great.

Waves (currently) only have one function; Update(). Most of the information on programming waves is in the Functions section of the documentation.

There are two kinds of events. We'll refer to them as inherited events and script-specific events. An inherited event is a function that gets run first on your Encounter script. If the function is not found in the Encounter script, it will try to run the same function on all monsters. If it's not found there either, it will resort to a default, built-in handler. Script-specific events are, as the name implies, functions that only happen for this specific script type. We'll start with the first type.


Inherited events (Encounter -> Monster)

EncounterStarting() Happens once when everything's done initializing but before any encounter actions start. You should do things like stopping the music here, or using State() if you want to start the fight off with some dialogue.

EnemyDialogueStarting() Happens when you go to the monster dialogue state. You're still free to modify monster dialogue here.

EnemyDialogueEnding() Happens when you go from the monster dialogue state to the defending state.

DefenseEnding() Happens when you go from the defending state of the game to any other state. If you read up on the RandomEncounterText() function, you'll want to use it here.

HandleItem(item_ID) Happens when you select an item from the item menu. In this alpha you will only have TestDog1 through TestDog7. The item IDs for these, if you want to use them for some reason, are DOGTEST1 through DOGTEST7. Note that if you do have a custom item handler, using BattleDialog() is mandatory, as selecting an item will not change the game's state by default. Currently, having a custom handler prevents the regular item's handler from working. This will be fixed later, but has been delayed as you can't create custom items yet.

function HandleItem(ItemID)
    if ItemID == "DOGTEST2" then
        BattleDialog({"You selected The Second Dog.", "You are truly great."})
    else
        BattleDialog({"You didn't select The Second Dog.", "You could've picked better."})
    end
end

HandleSpare() Happens when you select the Spare option from the Mercy menu, regardless of whether a monster is spareable or not. This event fires after the sparing of monsters is completed. If you spare the last enemy in the encounter, this function will not happen - the encounter is over at that point.

Script-specific events

Encounter script events

EnteringState(newstate, oldstate) [E] A new, more flexible way of handling state changes. When you enter a new state, this function will fire with newstate containing the new state's name, and oldstate containing the previous state's name. Both are in all caps. One of the most powerful things about it is that you can use State() here to interrupt state changes initiated by the engine itself.
Possible states and when they execute are below:

  • ACTIONSELECT - Returning to the main part of the battle, where you can select FIGHT/ACT/ITEM/MERCY.
  • ATTACKING - When you've selected a target with the FIGHT option.
  • DEFENDING - When the enemy/enemies finish dialogue, and one or more waves start.
  • ENEMYSELECT - When you've selected either FIGHT or ACT, and need to select an enemy.
  • ACTMENU - When you've selected an ACT target, and must now select an ACT command.
  • ITEMMENU - When you've selected ITEM.
  • MERCYMENU - When you've selected MERCY.
  • ENEMYDIALOGUE - When your enemy/enemies start their dialogue.
  • DIALOGRESULT - When you call BattleDialog(), or when the UI shows text on its own (e.g. when using an item).
For a clearer example, here's a code snippet replicating the older events above.
function EnteringState(newstate, oldstate)
    if newstate == "ENEMYDIALOGUE" then
        --same as EnemyDialogueStarting()
    elseif newstate != "ENEMYDIALOGUE" and oldstate == "ENEMYDIALOGUE" then
        --same as EnemyDialogueEnding(). Alternatively, check for newstate == "DEFENDING"
    elseif newstate != "DEFENDING" and oldstate == "DEFENDING" then
        --same as DefenseEnding()
    end
end

Update() This function runs for every frame for all of the encounter, even during waves. For advanced users: the Update function is required to exist at the beginning of the encounter. If you define it at a later point in time, it will not get executed.

Monster script events

HandleAttack(damage) Happens the moment the player's attack has applied damage - this is when you hear the hitting sound after the slash animation. damage will be -1 if the player pressed Fight, but didn't press any buttons and let it end by itself. The monster's hp variable will have updated at this time, too. Don't call BattleDialog() here, it's a bit buggy right now.

OnDeath() Happens after your attack's shaking animation has completed and the monster's HP is 0. If you implement OnDeath(), your monster will not die automatically, and you will have to do it manually with the Kill() function. OnDeath() will only happen through monster kills that happened with the FIGHT command; scripted Kill() calls will not trigger it. Calling BattleDialog() here will probably screw up the battle UI.

HandleCustomCommand(actcommand) Happens when you select an Act command on this monster. actcommand will be the same as how you defined it in the commands list, except it will be in all caps. Intermediate example below, showing how you can use it and spice it up a little.

commands = {"Sing", "Dance", "Wiggle"} --somewhere at the beginning
wigglecounter = 0 --let's keep a counter to check how often we've wiggled
  
function HandleCustomCommand(command)
    if command == "SING" then
        BattleDialog({"You sing your heart out. It's in the arena now."})
    elseif command == "DANCE" then
        BattleDialog({"You busted out your best moves."})
    elseif command == "WIGGLE" then
        if wigglecounter == 0 then --you can use variables to make commands more exciting!
            BattleDialog({"You just kind of stood there and wiggled."})
        elseif wigglecounter == 1 then
            BattleDialog({"You're still kind of standing there and wiggling."})
        else
            BattleDialog({"Your wiggled so often that your wiggling technique\ris now legendary."})
        end
        wigglecounter = wigglecounter + 1 --be sure to increase the wiggle counter, or it'll stay at 0
    end
end

Wave script events

Update() Happens every frame (usually at 60FPS) while monsters are attacking (the defense step). That's pretty much it. Update your bullets here - more on bullet creation and control is on the API - Projectile management page.

Functions & Objects

This section details functions Unitale adds to your Lua scripts to interact with the game in various ways. All functions will have a suffix in square brackets to denote in which scripts they may be used. The key for this is E for Encounter, M for Monster and W for Wave. If, for example, a function can be used in all scripts, it will be written as functionName() [E/M/W].

DEBUG(text) [E/M/W] Write text to the debug console (toggleable with F9). It will appear automatically the first time you write text to it. You can use this to check values in your code, or make sure some pieces of code are actually running.

SetGlobal(your_variable_name, value) [E/M/W] Set a global variable. After setting, you can retrieve it from all your scripts at any time with GetGlobal(variable_name).

GetGlobal(your_variable_name) returns variable [E/M/W] Get a global variable that you previously set using SetGlobal().

BattleDialog(list_of_strings) [E/M/W] This makes the list of strings you give the function appear in the UI dialog box. After skipping through them, you will automatically go to the monster dialogue step by default. Below is a working example of how you could use it for a Vegetoid encounter.

function HandleCustomCommand(command)
    if command == "DINNER" then
        if ate_greens then -- ate_greens is a non-default variable, of course
            currentdialogue = {"Ate\nYour\nGreens"}
        else
            currentdialogue = {"Eat\nYour\nGreens"}
        end
        BattleDialog({"You pat your stomach.\nVegetoid offers a healthy meal."})
    end
end

(For the Lua specialists; it is indeed a table of strings rather than a list of strings.)

State(state_to_go_to) [E/M/W] A powerful function that immediately skips the battle to the specified state, rather than following the default conventions. Below is a list of valid strings you can pass to it, what state you'll be going to and what that state entails. The states you pass to it are case-invariant, but uppercase is recommended for readability.

  • ACTIONSELECT - While in this state, you can select FIGHT/ACT/ITEM/MERCY.
  • ENEMYDIALOGUE - When starting this state, currentdialogue gets read from the Encounter script for every monster and their dialogue is displayed. If that doesn't exist it'll return something at random from the randomdialogue list.
  • DEFENDING - When starting this state, nextwaves is read out from the Encounter script and all scripts in the Waves folder with matching names will be part of this defense step. wavetimer is also read from the Encounter script at this time.
  • DONE - Currently, this immediately returns you to the mod selection screen.

Arguably the best part about State is that it can be used in conjunction with the text command [func] to change the state from within your dialogue. An example below; this will have a monster say that he will not fight you, then return to the action select screen rather than launching an attack.

currentdialogue = {"I won't fight you.", "[func:State,ACTIONSELECT]"}

There are other states available, but they can't be properly used. For the sake of completeness I will list them here and you are free to attempt something with them, but will not explain further as they'll likely lock up the battle and their use is entirely unsupported. A better way to manage the state of the battle will be added eventually.

The other states are as follows: ATTACKING, ENEMYSELECT, ACTMENU, ITEMMENU, MERCYMENU, RESETTING, DIALOGRESULT.

The Player object [E/M/W] You can use this object to obtain information about the player. Since the player is always 16x16 pixels in Undertale, you can add/subtract 8 from the player's horizontal/vertical position to get the player's edges if you need that for anything.

  • Player.sprite - the Player's soul sprite component. See the Sprites & Animation section for usage details.
  • Player.hp - get or set the player's current HP. Can't exceed max HP. If set to 0, game over triggers.
  • Player.name - get or set the player's current name. 6 letters max. Random name by default.
  • Player.lv - get or set the player's current level. 1 by default/minimum. 20 max. Player starts with 20HP / 10 ATK and gets 4 HP / 2 ATK per level. Leveling up the player through code doesn't automatically heal them; you'll have to do this manually.
  • Player.x (readonly) - get the X position of the player's center relative to the arena's center.
  • Player.y (readonly) - get the Y position of the player's center relative to the arena's center.
  • Player.absx (readonly) - get the X position of the player's center relative to the bottom left corner of the screen.
  • Player.absy (readonly) - get the Y position of the player's center relative to the bottom left corner of the screen.
  • Player.isHurting (readonly) - true if the player's currently blinking due to being hurt, false otherwise.
  • Player.isMoving (readonly) - true if the player is currently moving in battle, false otherwise. Will always be false while not in a wave script.
  • Player.Hurt(damage) - deals damage to the player, and makes them invincible for the default time.
  • Player.Hurt(damage, invul_time) - deals damage to the player, and makes them invincible for invul_time seconds.
  • Player.Heal(value) - heals the player for this amount. This is exactly the same as Player.Hurt(-value, 0).
  • Player.SetControlOverride(boolean) - either true or false. Only useable in waves. If true, this will prevent the player soul from doing its regular movement/keyboard control for the rest of the wave. Used for if you want to implement your own controls for a wave.
  • Player.MoveTo(x, y, ignoreWalls) - Moves to player soul to this position relative to the arena's center. If ignoreWalls is false, it will make sure the player doesn't go outside of the arena, otherwise it ignores the boundaries. If you want to move the player out of bounds in a wave, you'll have to call Player.SetControlOverride(true) as the player's default movement keeps the soul inside the arena.
  • Player.MoveToAbs(x, y, ignoreWalls) - Moves player soul to this position relative to the bottom left of the screen. If ignoreWalls is false, it will make sure the player doesn't go outside of the arena, otherwise it ignores the boundaries. If you want to move the player out of bounds in a wave, you'll have to call Player.SetControlOverride(true) as the player's default movement keeps the soul inside the arena.

The Script object (and the Encounter object) [varies] Script objects are a bit of a special case. They're used to refer to other scripts that were loaded by the engine itself. In the encounter script, the enemies table is filled with Script objects after the encounter starts. The Encounter object is also a script object that refers to the current encounter, and is accessible from anywhere. Take note of the different way of accessing variables.

  • script.GetVar("variable_name") - gets variable_name from the script.
  • script["variable_name"] - same as the above.
  • script.SetVar("variable_name", value) - sets variable_name on the script.
  • script["variable_name"] = value - same as the above.
  • script.Call("function_name") - runs function_name from within the script.

So for instance, you can do enemies[1].Call("Kill") to kill the first monster from your encounter. You can do Encounter.SetVar("wavetimer", 1.0) from anywhere to change the wave timer to 1 second.

The Audio object [E/M/W] The Audio object allows you to control music in the game and play sounds. Here's the ways in which you can use it.

  • Audio.Play() - Play the currently loaded music. Done automatically at the beginning of a fight.
  • Audio.Stop() - Stops the music. If you want a battle not to have music, call this in EncounterStarting().
  • Audio.Pause() - Pause the music.
  • Audio.Unpause() - Unpause the music if you previously paused it.
  • Audio.Volume(value) - Set music to given volume. value should be between 0.0 (muted) and 1.0 (full volume). This is 0.75 by default.
  • Audio.Pitch(value) - Set music pitch to given value. 1.0 is default, 2.0 is twice the regular speed. Negative values play the music backwards. value should be between -3.0 and 3.0.
  • Audio.LoadFile(filename) - Load music from the Audio folder titled filename.ogg or filename.wav and play it immediately. If you don't want immediate playback, call Audio.Stop() after this.
  • Audio.PlaySound(filename) - Play the sound from the Sounds folder titled filename.ogg or filename.wav.
  • Audio.playtime - Get the current play position of the current music in seconds.
  • Audio.totaltime - Get the total length of the current music in seconds.

As it's an object, you can't directly use it with [func], but you can make your own function if you want to, say, stop the music mid-dialogue.

currentdialogue = {"but then I realized...\n[w:30][func:drama]the butler did it!!!"}
   
function drama()
    Audio.Stop()
    Audio.PlaySound(dramatic_sound_effect)
end

The Input object [E/M/W] The Input object allows you to retrieve input status. All keys will return a number; 0 when not pressed, 1 on the first frame the key is pressed, 2 while it's being held, and -1 while it's released. You can check if a key's value is greater than 0 to see if it's pressed, or if it's exactly 1/-1 to only have an action if it was just pressed/released. Possible key options are below.

Note: do not rely on the Input object to replace proper UI controls. Changing game state in the UI based on input will likely cause a fair share of issues and is not supported at this moment (but feel free to see what does and doesn't work).

  • Input.Confirm - Z or Enter keys
  • Input.Cancel - X or left Shift keys
  • Input.Menu - C or left Control keys (currently unused anywhere else)
  • Input.Up - Up arrow
  • Input.Down - Down arrow
  • Input.Left - Left arrow
  • Input.Right - Right arrow

The Time object [E/M/W] The Time object serves as a way to retrieve game timing without having to keep track of it yourself, or using a frame counter. Whenever possible, try using the Time object over a frame-based timing method to ensure equal behaviour across all framerates.

  • Time.time - Time (in seconds) since the game application started. If you want to time specific events, store Time.time in a variable of your own at the start of what you want to time, then subtract Time.time from your stored time to calculate the difference.
  • Time.dt - Delta time (in seconds). This is the time it took for the last game update to complete.
  • Time.mult - Multiplier to ensure equal movement across all framerates (this is essentially deltatime*framerate). This will be around 1.0 when the application runs at 60FPS, ~0.5 at 120FPS and ~2.0 at 30FPS, etc. By multiplying your movement by this value, your waves will be consistent on lower framerates as well.

SetSprite(filename) [M] Sets the monster's sprite from the Sprites folder to filename.png. Can be used with [func] to change sprites mid-dialogue.

SetActive(active) [M] Either true or false. If false, this monster will stay on screen but will not show up in menus, do its dialogue or run any of its events. You can use this to introduce monsters to an encounter at a later point. The battle will end when a monster is killed or spared and there are no active monsters left. Having no active monsters at all will likely cause a bunch of errors right now.

Kill() [M] Kills the monster immediately. If this was the last monster, the battle ends.

Spare() [M] Spares the monster immediately. Similar to Kill(), if this was the last monster, the battle ends.

RandomEncounterText() returns string [E] Select a random monster from the encounter, then get a random entry from the comments specified. You'll want to use this to replicate default encounter behaviour. See code below (or one of the example encounters).

function DefenseEnding() --This built-in function fires after the defense round ends.
    encountertext = RandomEncounterText()
end

Wave scripts

The following section is dedicated exclusively to wave scripts. We'll go over special functions and the arena object. As you can now create projectiles from the encounter as well, information about projectiles and their examples have been moved to the API - Projectile management section.

The Arena object [W] You can use this object to obtain information about the arena, or resize it.

  • Arena.width (readonly) - the width of the arena in pixels, after resizing. Since the reference for the player and bullets is the arena's center, you can get the left/right side of the arena with -Arena.width/2 and Arena.width/2 respectively.
  • Arena.height (readonly) - the height of the arena in pixels, after resizing. Like with width, you can get the bottom/top with -Arena.height/2 and Arena.height/2 respectively.
  • Arena.currentwidth (readonly) - the current width of the arena in pixels. Differs from width in that it will accurately reflect the arena size in the middle of resizing, too.
  • Arena.currentheight (readonly) - the current height of the arena in pixels. Differs from height in that it will accurately reflect the arena size in the middle of resizing, too.
  • Arena.Resize(width, height) - Resizes the arena to the new size. Currently, monsters stay on top of the arena. This will be changed around the animation update.
  • Arena.ResizeImmediate(width, height) - Resizes the arena without the animation.

EndWave() [W] Ends this wave immediately. You can only call this function from the Update function.

Projectile management

Projectile management is, starting from 0.2.0, available from both the encounter and the wave scripts. As a result it is now in its own section.

CreateProjectile(spritename, initial_x, initial_y) returns Bullet [E/W] Create a bullet that you can store and modify, with its spawn position relative to the center of the arena. The hitbox of it will be, in this alpha build, a rectangle around your sprite. As the hitbox code is being rewritten very soon (it's high priority!) try to stick to bullets without extravagant shapes sticking out for now.

CreateProjectileAbs(spritename, initial_x, initial_y) returns Bullet [E/W] Same as CreateProjectile, but spawn position is relative to the bottom left of the screen instead of the arena's center.

The Bullet object This is what you use to move around the bullet and store values in it. You can store a bunch in a table and modify them. The functions and variables you can use on a Bullet are as follows.

  • Bullet.sprite - the Bullet's sprite component. See the Sprites & Animation section for usage details. NOTE: Currently, modifying the sprite does not change the bullet's hitbox yet; it's always the original square of the bullet when it was created.
  • Bullet.x (readonly) - The X position of this bullet, relative to the arena's center. A bullet at x=0 and y=0 will be at the center of the arena.
  • Bullet.y (readonly) - The Y position of this bullet, relative to the arena's center.
  • Bullet.absx (readonly) - The X position of this bullet, relative to the bottom left corner of the screen.
  • Bullet.absy (readonly) - The Y position of this bullet, relative to the bottom left corner of the screen.
  • Bullet.isactive (readonly) - Used to check if the bullet is still active. If the bullet has been removed, this will be false; otherwise true.
  • Bullet.Move(x, y) - Move this bullet x pixels to the right and y pixels up. Negative x will move it to the left, and negative y will move it downwards.
  • Bullet.MoveTo(x, y) - Move this bullet to this position immediately, relative to the arena's center.
  • Bullet.MoveToAbs(x, y) - Move this bullet to this position immediately, relative to the bottom left corner of the screen.
  • Bullet.Remove() - destroys this bullet. You can continue retrieving its x, y, absx and absy properties. Trying to move a removed bullet will give you a Lua error. If you're not sure your bullet still exists, check Bullet.isactive first.
  • Bullet.SetVar(your_variable_name, value) - Set a variable on this bullet that you can retrieve with Bullet.GetVar(your_variable_name). Similar in use to SetGlobal(), but you can use this to store specific variables on a per-bullet basis.
  • Bullet.GetVar(your_variable_name) - Get a variable that you previously set using Bullet.SetVar().
  • Bullet.SendToTop() - Moves this bullet on top of all currently existing projectiles. Note that newly spawned projectiles are always on top by default; this function is mostly to move existing bullets to the top.
  • Bullet.SendToBottom() - Moves this bullet below all currently existing projectiles.

Here is an example of a bullet that chases you pretty fast, but slows down as it gets closer. You have to keep moving to dodge it. This is a fairly basic example that makes use of the Player object.

oursprite = "hOI!!!!"
--Create a new bullet, starting in the upper right corner.
chasingbullet = CreateProjectile(oursprite, Arena.width/2, Arena.height/2)
--Set initial speed of 0 in both directions.
chasingbullet.SetVar('xspeed', 0)
chasingbullet.SetVar('yspeed', 0)

function Update()
    -- Get the x/y difference between the player and bullet
    local xdifference = Player.x - chasingbullet.x
    local ydifference = Player.y - chasingbullet.y
    -- We create a new speed by first halving the original speed
    -- Then we add a small fraction of the position difference between the player and bullet.
    -- The result is a bullet that moves faster as it's further away, and slower when it's closer.
    -- The value we're dividing by is experimental. Experimenting with numbers is essential!
    local xspeed = chasingbullet.GetVar('xspeed') / 2 + xdifference / 100
    local yspeed = chasingbullet.GetVar('yspeed') / 2 + ydifference / 100
    -- Now move the bullet...
    chasingbullet.Move(xspeed, yspeed)
    -- ...and store our new speeds.
    chasingbullet.SetVar('xspeed', xspeed)
    chasingbullet.SetVar('yspeed', yspeed)
end

Below is an example of a fully scripted Wave using most of these functions. It will spawn a projectile above the arena (assuming a width/height of 155/130), give it a random speed in the X direction, and drop it downwards. If it hits the bottom border of the arena, it'll bounce back up. Otherwise it'll continue falling off the screen.

spawntimer = 0
bullets = {}

-- This happens every frame while you're defending. --
function Update()
    spawntimer = spawntimer + 1 --Add 1 to the counter every frame
    
    -- This part takes care of bullet spawning. --
    if spawntimer%30 == 0 then  --This happens every 30 frames.
        local posx = 30 - math.random(60) --Set a random X position between -30 and 30
        local posy = 65 --and set the Y position to 65, on the top edge of the arena.
        local bullet = CreateProjectile('hOI!!!!', posx, posy) -- Create projectile with sprite hOI!!!!.png
        bullet.SetVar('velx', 1 - 2*math.random()) -- We'll use this for horizontal speed. Random between -1/1
        bullet.SetVar('vely', 0) -- We'll use this for fall speed. We're starting without downward movement.
        table.insert(bullets, bullet) -- Add this new Bullet object to the bullets table up there.
    end
    
    -- This part updates every bullet in the bullets table. --
    for i=1,#bullets do -- #bullets in Lua means 'length of bullets table'.
        local bullet = bullets[i] -- For convenience, so we don't have to use bullets[i]
        local velx = bullet.GetVar('velx') -- Get the X/Y velocity we just set
        local vely = bullet.GetVar('vely')
        local newposx = bullet.x + velx -- New position will be old position + velocity
        local newposy = bullet.y + vely
        if(bullet.x > -Arena.width/2 and bullet.x < Arena.width/2) then -- Are we inside the arena (horizontally)?
            if(bullet.y < -Arena.height/2 + 8) then -- And did we go past the bottom edge?
                bullet.MoveTo(bullet.x, -Arena.height/2 + 8) -- Don't move it past the edge!
                -- Note the +8; I know the bullet sprite I'm using is 16x16.
                -- Without adding 8 it'll be inside the edge.
                vely = 4 --reverse bounce direction
            end
        end
        vely = vely - 0.04 -- Apply gravity
        bullet.MoveTo(newposx, newposy) -- and finally, move our bullet
        bullet.SetVar('vely', vely) -- and store our new fall speed into the bullet again.
    end
end

OnHit(bullet) Every time a bullet collides with a player, this function gets called on the script that created the projectile. The bullet object in this function can be modified if you feel like it. For more information on the bullet object, see the documentation above.

If you implement this function in your script, you have to define manually what should happen after bullet collision. This is what allows you to create orange, cyan and green projectiles, and much much more. If you don't implement this function in your wave script, it'll stick to the default of dealing 3 damage on hit. Below are multiple examples of how to use this function.

--Defining your own damage for this wave
function OnHit(bullet)
    Player.Hurt(10)
end
  
--Replicating cyan bullet functionality
function OnHit(bullet)
    if Player.isMoving then
        Player.Hurt(5)
    end
end

--Replicating orange bullet functionality; opposite condition of cyan
function OnHit(bullet)
    if not Player.isMoving then
        Player.Hurt(5)
    end
end

--Replicating green bullet functionality
function OnHit(bullet)
    Player.Heal(1)
    bullet.Remove()
end

Sprites & Animation

Projectile management is, starting from 0.2.0, available from both the encounter and the wave scripts. As a result it is now in its own section.

CreateSprite(spritename) returns Sprite [E/W] Create a sprite in the far bottom left of the screen (at 0, 0) that you can modify in many ways.

The Sprite object The Sprite object has many controls intended for animation. There is a working intermediate example included in the Examples folder.

  • sprite.isactive (readonly) - true if sprite has been removed, false otherwise.
  • sprite.x - horizontal position of sprite relative to bottom left of screen, measured from its pivot/anchor point (center by default). If parented, x position is relative to parent
  • sprite.y - vertical position of sprite relative to bottom left of screen, measured from its pivot/anchor point (center by default). If parented, y position is relative to parent
  • sprite.xscale - horizontal scaling of sprite (1.0 by default). 2.0 is twice as large, 0.5 half as large
  • sprite.yscale - vertical scaling of sprite (1.0 by default). 2.0 is twice as large, 0.5 half as large
  • sprite.width (readonly) - width of this sprite's image in pixels. This never changes until the sprite itself is swapped
  • sprite.height (readonly) - height of this sprite's image in pixels. This never changes until the sprite itself is swapped
  • sprite.color - get or set the color, as a table of 3 values from 0 to 1. For example, sprite.color = {1.0, 0.0, 0.0} makes it red. This actually overlays the sprite's original color, so if you want full control over the color make sure your sprite is white. Black areas are not affected by coloration.
  • sprite.alpha - get or set sprite transparency, from 0 to 1.
  • sprite.rotation - get or set sprite rotation, in degrees. It's clamped between 0 and 360, so if you change it to 365 it becomes 5.
  • sprite.Set("new_sprite") - change the sprite's image. It retains its scaling and rotation. If you have an animation running with SetAnimation, the animation will override your sprite change.
  • sprite.SetParent(other_sprite_object) - parents sprite to other_sprite_object. This will make the original sprite move along with the object it's parented to.
  • sprite.SetPivot(x, y) - changes the point the sprite rotates/scales around; (0,0) is bottom left of sprite, (1,1) is top right. You can have values outside of the 0-1 range, too.
  • sprite.SetAnchor(x, y) - change the point your sprite anchors to when moving. Mainly noticeable when rescaling the parent sprite, and you want to have this sprite stick to a certain edge of it. x/y should be in 0-1 range.
  • sprite.MoveTo(x, y) - same as setting x and y simultaneously.
  • sprite.MoveToAbs(x, y) - used to move a sprite to an absolute screen position, regardless of parent settings. The sprite itself will remain parented.
  • sprite.Scale(xscale, yscale) - same as setting xscale and yscale simultaneously
  • sprite.SetAnimation(sprite_table) to do frame-by-frame animation at 30FPS. Example: sprite.SetAnimation({"sans_head1", "sans_head2", "sans_head3"})
  • sprite.SetAnimation(sprite_table, time_per_frame) to do frame-by-frame animation with your own time between frames, in seconds. If time_per_frame is 1, it takes 1 second to move to the next sprite.
  • sprite.StopAnimation() - stop the frame-by-frame animation if it was running. Also changes back to the sprite it had before the animation, or whenever you last called sprite.Set().
  • sprite.SendToTop() - sends this sprite to the top of its layer's hierarchy. If a sprite has 5 children for instance, you can use this to rearrange them internally. However, child sprites will always appear on top of their parents, regardless of this function being called.
  • sprite.SendToBottom() sends this sprite to the bottom of its layer's hierarchy. Similar rules apply as with SendToTop().
  • sprite.Remove() - remove this sprite. Calling anything other than isactive after this will give you an error.

The animation script used in the example is shown below for reference.

-- First, we can create the torso, legs and head.
sanstorso = CreateSprite("sans/sanstorso")
sanslegs = CreateSprite("sans/sanslegs")
sanshead = CreateSprite("sans/sanshead1")

--We parent the torso to the legs, so when you move the legs, the torso moves too. 
--We do the same for attaching the head to the torso.
sanstorso.SetParent(sanslegs)
sanshead.SetParent(sanstorso)

--Now we adjust the height for the individual parts so they look more like a skeleton and less like a pile of bones.
sanslegs.y = 240
sanstorso.y = -5 --The torso's height is relative to the legs they're parented to.
sanshead.y = 40 --The head's height is relative to the torso it's parented to.

--We set the torso's pivot point to halfway horizontally, and on the bottom vertically, 
--so we can rotate it around the bottom instead of the center.
sanstorso.SetPivot(0.5, 0)

--We set the torso's anchor point to the top center. Because the legs are pivoted on the bottom (so rescaling them only makes them move up),
--we want the torso to move along upwards with them.
sanstorso.SetAnchor(0.5, 1)
sanslegs.SetPivot(0.5, 0)

--Finally, we do some frame-by-frame animation just to show off the feature. You put in a list of sprites,
--and the time you want a sprite change to take. In this case, it's 1/2 of a second.
sanshead.SetAnimation({"sans/sanshead1", "sans/sanshead2", "sans/sanshead3"}, 1/2)

function AnimateSans()
    sanslegs.Scale(1, 1+0.1*math.sin(Time.time*2))
    sanshead.MoveTo(2*math.sin(Time.time), 40 + 2*math.cos(Time.time))
    sanshead.rotation = 10*math.sin(Time.time + 1)
    sanstorso.rotation = 10*math.sin(Time.time + 2)
end

* YOU WON!
* You earned 0 EXP and 0 gold.
* also the nav menu is broken now

Undertale fake button
Undertale fake button
Undertale fake button
Undertale fake button