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.
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.
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:
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.
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.
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 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 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 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 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.
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.
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.
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.
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 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).
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.
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.
[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:
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:
For all default fonts, check out the Default/Sprites/UI/Fonts folder. Every font with a matching .xml file is mapped.
[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
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.
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.
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:
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.
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
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.
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.
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.
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.
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.
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).
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.
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
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.
EndWave() [W] Ends this wave immediately. You can only call this function from the Update function.
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.
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
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.
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