Undertale Battle System Twine Story

This Twine story template, intended for Harlowe 3, creates a text-based version of the Undertale battle system. Just copy and paste the content below into your Twine passages, replacing the highlighted parts with the content of your choice. You can also download a sample Twine file here (and then import it into your Twine dashboard).

This is intended to be used with the Undertale Twine CSS Theme.

Starting Passage (Variable Reset)


<!-- Your starting passage should initialize all of the global variables you are going to use. Specific enemy HP and item variables can be added and removed at your discretion. The maxHP and HP variables should be the same, and it is up to you whether or not the player has the items, or how much gold they get to start with. -->

(set: $kills to 0)
(set: $victories to 0)
(set: $losses to 0)
(set: $gold to 0)
(set: $LV to 1)
(set: $EXP to 0)
(set: $maxHP to 20)
(set: $HP to 20)
(set: $FIRST_ENEMY_MAX_HP to 129)
(set: $SECOND_ENEMY_MAX_HP to 90)
(set: $THIRD_ENEMY_MAX_HP to 100)
(set: $ITEM1 to true)
(set: $ITEM2 to true)
(set: $ITEM3 to false)
(set: $X_ENEMY_KILLED to false) <!-- this type of enemy-tracking variable is optional, and should be used only if you want to keep track of if a certain enemy has been killed. -->
(goto: "EITHER NAME SELECTION OR NEXT PASSAGE") <!-- the player doesn’t need to know that these all got set, so bring them directly to your name selection passage, or first story passage (whatever that may be) -->
                      

Name Selection (Optional)


<!-- this optional passage simply prompts the player to choose their name, before moving on to the next passage, which is probably your first story passage or whatever. -->
(set: $name to (prompt: "Choose a Name.", "Chara"))(goto: "NEXT PASSAGE")
                      

Home Page (Optional)


<!-- if your battle system is standalone (not part of a story) and lets players battle multiple enemies, you might want to add a passage like this. NOTE: if you are going to have more than one battle happen in your Twine story, whether it’s a standalone battle system OR part of a larger story, you MUST reset the $turns variable to 0 after the battle is done to avoid errors!!! It’s up to you whether you want to reset the player back to full HP. If you’re going to make it so that the different enemies can be battled more than once (e.g. minor enemies that there are multiple of), you should reset their HP to their max as well. -->
\(set: $turns to 0)
\(set: $HP to $maxHP)
\(set: $ENEMYX_HP to $ENEMYX_MAX_HP)
\(set: $ACT1 to 0)
\(set: $ACT2 to 0)
\(set: $ACT3 to 0)
\(set: $togeno to 13)
\(set: $topacifist to 10)
\(if: $kills >= 3 and $kills < 13)[
\	(color: red)[(print: $togeno - $kills) left.]

]
\(elseif: $kills >= 13)[
\	(color: red)[[[Determination.|Genocide Route Home]]]

]
\(if: $victories >= 3 and $victories < 10 and $kills is 0)[
\	(color: blue + white)[(print: $topacifist - $victories) more...]

]
\(if: $victories >= 10 and $kills is 0)[
\	(color: blue + white)[[[Dona nobis pacem...|Pacifist Route Home]]]

] <!-- This optional code, highlighted in green adds a genocide kill counter that leads to another set of fights (under its own home screen, like this one) and a pacifist victory counter that works just like the genocide kill counter. -->
\$name <!-- displays the player’s name -->

Victories: $victories <!-- displays the number of victories -->

Losses: $losses <!-- displays the number of losses -->

Kills: $kills <!-- displays the number of enemies you’ve killed -->

EXP: $EXP <!-- displays your current EXP -->

LV: $LV <!-- displays your current LOVE -->

Gold: $gold <!-- displays the amount of gold you have -->

(set: $link to (random: 1, 4))
\(if: $link > 2)[
\	[[Start a battle|ENEMY1]]]
\(else:)[[[Start a battle|ENEMY2]]] <!-- My system features two battles, picked at a random chance. If you don't want this, omit the $link variable and if statement and just have a link. -->
\
\<!-- (if: $X_ENEMY_KILLED is false)[
\	[[Start a battle|ENEMY1]]]
\(else:)[[[Start a battle|BUT NOBODY CAME]]]--> <!-- Another option is to have the battle stop working if you kill the enemy, going to a passage where the player can reset/reload the game. -->

[Buy an (ITEM) (10 G)]<buy|(click: ?buy)[

(if: $gold >= 10 and $ITEM is false)[<!-- if you have enough gold and don’t already have that item, remove the appropriate amount of gold from your inventory and indicate that you now have that item -->
\	(set: $gold to it - 10)(set: $ITEM to true)
\	You have bought an (ITEM).]
\(elseif: $ITEM is true)[<!-- if you already have that item, you can’t buy another. Currently this system doesn’t allow you to have more than one of an item using the same variable, although you COULD have more than one item of a certain name if you give them different variable names. -->
\	You already have an (ITEM).]
\(elseif: $gold < 10)[<!-- if you don’t have enough gold you can’t buy the item -->
\	You don't have enough gold.]
\(else:)[... They're out of (ITEM)s.] <!-- just in case something goes weird. -->
\] <!-- This green section adds the option to buy an item if the player doesn't have one and has enough gold. -->
                      

Main Battle Sequence


(if: $HP is 0 or $HP < 0)[ <!-- This is the game over screen if you lose all of your HP. You always want to check for this first! -->
\	(set: $losses to it + 1) <!-- notes that you’ve lost a battle -->
\	<h1>GAME OVER</h1> 

	You cannot give up hope! $name... [[Stay determined!|HOME OR WHEREVER THEY GO IF THEY DIE]]
\]
\(elseif: $ENEMYX_HP <= 0)[ <!-- This is the win screen if the enemy's HP is 0. -->
\	(set: $ENEMYX_KILLED to true) <!-- notes that this has been killed so you can't fight them anymore. This line is optional -->
\	(set: $kills to it + 1)
\	(set: $victories to it + 1)
\	(set: _gained to (random: 40, 60))
\	(set: _goldgain to (random: 1, 10))
\	(set: $gold to it + _goldgain)
\	(set: $EXP to it + _gained) <!-- $kills counts the number of enemies you’ve killed, so we add this kill to it. It also counts as a victory. “gained” and “goldgain” give you a randomized amount of EXP and gold, respectively, and they are added to the global EXP and gold counters. --> 
\	[[You Won!|HOME OR WHEREVER THEY GO IF THEY WIN]]

	You've gained _gained EXP and _goldgain gold. 
\	(if: $EXP % 3 is 0 or $kills is 1)[<!-- EXP gain is a lot simpler in this battle system than in actual Undertale. Basically, if the amount of EXP you currently have can be divided by 3, or if this is your first kill, your LOVE will increase. -->
\		(set: $LV to it + 1)(set: $maxHP to it + 4)
\		Your LOVE has increased!]
\]
\(else:)[<!-- otherwise, if no one has won yet, continue to your turn. -->
\	(if: $turns is 0)[ENEMY approaches!]<!-- if this is the first turn/start of the encounter -->
\	(elseif: {SPARE CONDITION IS TRUE})[ENEMY doesn't want to fight anymore.]<!-- if the player has done whatever necessary to spare this particular enemy, show this dialogue to hint that they can spare. This goes ahead of the other elseif’s so that it’s checked first when it’s not the first turn. -->
\	(elseif: $ENEMYX_HP <= $ENEMYX_MAX_HP/3)[ENEMY has low HP.]<!-- if has less than a third of their HP, let the player know. This goes ahead of the other elseifs so that it’s checked second when it’s not the first turn (prioritization is enemy sparable -> enemy about to die -> other) -->
\	(elseif: $turns >= 1 and $ACT1 is 0 and $ACT2 is 0 and $ACT3 is 0)[ENEMY does something.]<!-- if you’ve done something, but NOT one of the act options -->
\	(elseif: $ACT1 > 1 and $ACT1 < 4)[ENEMY does something else.]<!-- if you’ve acted a certain number of times, this can be used to change the dialogue shown. Feel free to change the ACT variables and the counters. This is just visual customization of the battle screen. -->
\	(elseif: $ACT2 > 1 and $ACT2 < 7)[ENEMY does another thing.]<!-- same kind of thing as above -->
\	(else:)[Smells like something.] <!-- catch-all dialogue in case none of the above conditions are met. -->

	$name&nbsp; &nbsp; &nbsp; &nbsp;LV $LV&nbsp; &nbsp; &nbsp; &nbsp;HP $HP/$maxHP
	[[Fight|FightENEMYX]]&nbsp; &nbsp;[Act]<act|&nbsp; &nbsp;[Item]<item|&nbsp; &nbsp;[Mercy]<mercy| <!-- This section holds the players name, LV and current HP, as well as the FIGHT, ACT, ITEM, and MERCY buttons, separated by blank space (the &nbsp;s). It is important that the fight, spare, and flee buttons direct you to a fight/spare/flee passage SPECIFIC TO THE CURRENT ENEMY, unless you only have one UT battle in your Twine game. -->
	(click: ?act)[<!-- if the player clicks on the ACT button, show the ACT menu. You can have as many ACT buttons as you want, with two to a line. -->
\		[[Check|CheckENEMYX]]&nbsp; &nbsp; &nbsp;[[ACT1|Act1ENEMYX]]<!-- if an enemy uses the same action as a different enemy, you'll want to differentiate them like this so that the player goes to the correct passage for the current enemy. -->
		[[ACT2]]&nbsp; &nbsp; &nbsp;[[ACT3]]<!-- if the ACTs are unique to this enemy, you don't HAVE to differentiate them like above, but when in doubt, it's good practice to do so. -->
	]
\	(click: ?item)[<!-- if the player chooses to use an item, show the item menu. You should have as many ITEM options as you have ITEM variables, unless there’s a specific item you really don’t want the player to be able to use in a given battle. To add more, add a new line with NO slash (since you want a new line to be shown) and copy-paste the four lines with ITEM3 and ITEM4’s stuff-->
\		(if: $ITEM1 is true)[
\			[[ITEM1|ITEM1ENEMYX]]]&nbsp; &nbsp; &nbsp;
\		(if: $ITEM2 is true)[
\			[[ITEM2|ITEM2ENEMYX]]]
		(if: $ITEM3 is true)[
\			[[ITEM3|ITEM3ENEMYX]]]&nbsp; &nbsp; &nbsp;
\   (if: $ITEM4 is true)[
\     [[ITEM4|ITEM4ENEMYX]]]
\		]
\	(click: ?mercy)[<!-- if the player chooses to use mercy, show the mercy menu -->
\		(if: {SPARE CONDITION IS TRUE})[<!-- if the spare conditions are met, the spare button will be yellow. -->
\			(color: yellow)[[[Spare|SpareENEMYX]]]
\		]
\		(else:)[<!-- otherwise the spare button is the normal color. -->
\			[[Spare|SpareENEMYX]]
\		]&nbsp; &nbsp; &nbsp;[[Flee|FleeENEMYX]]
\	]
\]
                      

Fight Passage


<!-- if there is more than one battle in your Twine game, they must all have unique fight/act/item passages, even if the content is the same! Otherwise, you won’t be removing HP from the correct enemy or redirecting back to the correct main fight passage. -->
\(set: _clicked to false) <!-- this tracks whether the player has clicked on the hook for the purposes of the event macro. -->
\ENEMY's HP: $ENEMYX_HP/$ENEMYX_MAX_HP

[ATTACK]<ATTACK|<!-- This hook is clicked to attack, and is set to where faster attacks have a higher chance for larger amounts of damage. -->

\(click-replace: ?ATTACK)[
\	(if: time >= .1s and time <= 2s)[
\		(set: _clicked to true) <!-- notes that the player clicked on the hook -->
\		(set: _damage to (random: 15, 30)) <!-- sets how much damage the player did -->
\		(set: $ENEMYX_HP to it - _damage) <!-- this removes the appropriate amount from the enemy’s HP. -->
\		[[_damage|DodgeENEMYX]]
\	](elseif: time > 2s and time <= 4s)[
\		(set: _clicked to true)
\		(set: _damage to (random: 5, 15))
\		(set: $ENEMYX_HP to it - _damage)
\		[[_damage|DodgeENEMYX]]
\	](elseif: time > 4s and time <= 6s)[
\		(set: _clicked to true)
\		(set: _damage to (random: 1, 5))
\		(set: $ENEMYX_HP to it - _damage)
\		[[_damage|DodgeENEMYX]]
\	](else:)[<!-- if you wait more than 6 seconds, you'll miss. -->
\		(set: _damage to 0)<!-- if you didn’t do any damage, there’s no reason to remove HP from the enemy.-->
\		[[MISS|DodgeENEMYX]]
\	]
\]
\(event: when time > 6s and _clicked is false)[<!-- if the player has waited more than 6 seconds and has not clicked to attack, it times out. -->
\	(replace: ?ATTACK)[
\		(set: _damage to 0)
\		[[MISS|DodgeENEMYX]]
\	]
\]
                      

Act Passage Template


(set: $ACTX to it + 1)<!-- this helps you track how many times the player has done this particular action -->
\(if: $ACTX < {SPARE CONDITION})[
\ You do X.

[[Enemy responds thusly.|DodgeENEMYX]]
\](elseif: $ACTX >= {SPARE CONDITION})[
\ You continue Xing. 

  [[It seems satisfied.|DodgeENEMYX]]] <!-- Your spare condition is just how many times the player needs to perform a certain act before an enemy can be spared. If the act has no effect on whether or not you can spare, just remove the if/then statement and have "You do X" and a link something like "[[Enemy does Y|DodgeENEMYX]]". -->
                      

Check Passage


ENEMY - ATK X DEF X
A perfectly normal description.

[[Z|DodgeENEMYX]] <!-- Z simply represents the player pressing z in Undertale to continue on. Change that to your liking. Like all of the other passages, the “dodge” passage you direct the player to must be unique to this particular battle/enemy. -->
                      

Item Passage Template


(if: $HP <= $maxHP - 10)[<!-- if using this item is not going to push you over your max HP, just add the number of HP recovered to the current HP. -->
\	(set: $HP to it + 10)]
\(if: $HP > $maxHP - 10)[<!-- if using this item is going to push you over your max HP, just set the player’s current HP to their max. -->
\	(set: $HP to $maxHP)]
\(set: $ITEM to false)
\You used the ITEM.

Something happened.

(if: $HP is $maxHP)[<!-- like above, if you recovered all of your HP, it shows this link; otherwise it tells you how much HP you recovered. -->
\	[[HP fully restored!|DodgeENEMYX]]
\](else:)[
\	[[Recovered 10 HP!|DodgeENEMYX]]
\] <!-- if an item does not restore any HP, you can skip all of this and just do something like "You use the X... but nothing happened." and then have a link to the dodging passage for this particular enemy. -->
                      

Flee Passage


(set: _escape to (either: true, false))
\(if: _escape is true)[
\[[Escaped...|{WHEREVER YOU GO NEXT}]]
\](else:)[
\(goto: "DodgeENEMYX")]
\
\<!-- The Flee passage just randomizes whether or not you can flee and shows the link accordingly. If you want escape to be less likely, you can set _escape to a random number and only let the player flee if it's above a certain number.-->
                      

Spare Passage


(if: {PLAYER ACTED ENOUGH TIMES/SPARE CONDITION IS TRUE})[<!-- if the player has done whatever they need to spare the enemy, then they win and the battle ends. If there are multiple ways the player can spare the enemy, use another elseif: with the same content in the brackets, or add ORs to the conditions above.-->
\	(set: $victories to it + 1)<!-- count this as a victory -->
\	(set: _goldgain to (random: 5, 15))<!-- "goldgain” is a temporary variable that will be used to add a random amount of gold to the player’s inventory. -->
\	(set: $gold to it + _goldgain)
\	[[You Won!|{WHEREVER THIS GOES NEXT}]]

You've gained 0 EXP and _goldgain gold.

\](else:)[<!-- if the player hasn’t waited enough turns/used the right items/acted the right way the right number of times to spare the enemy, then it won’t work and they’ll go to dodging attacks. -->
\	[["Your mercy isn't what I want!"|DodgeENEMYX]]
]
                      

Dodge Passage


(if: $ENEMYX_HP > 0)[<!-- if the enemy isn't dead, this passage will actually run -->
\	(set: $turns to it + 1)<!-- the turns are counted with each dodging, rather than in the fight/act/item/etc. passages because you will ALWAYS come back to the dodging passage. -->
\	(set: _youdamage to 0)<!-- initializes this value, used to determine how much damage the player took. -->
\	(set: _clicked to false)<!-- initializes this value, used to determine if the player has clicked on the dodge link. -->
\	(if: {SPARE CONDITION IS TRUE})[<!-- this bit can be used if you want the enemy to stop attacking once they're spareable. It's optional. Everything in the (else:) statement can be taken out and used on its own if you don't want thsi feature, but DO want the enemy's dialogue to change based on the player's actions. -->
\		"We can stop fighting now."

		[[Z|MAIN BATTLE SEQUENCE]]
\ ](else:)[<!-- if the enemy isn't spareable, this produces their line of dialogue. -->
\		(if: $turns is 1 and (history:)'s last is not "ACT1" and (history:)'s last is not "ACT2")[“The fight is just starting!”]<!-- if this is the first turn/start of the encounter and the player hasn't done specific actions -->
\		(elseif: $turns >= 1 and $ACT1 is 0 and $ACT2 is 0 and $ACT3 is 0)[“Generic dialogue!”]<!-- if you’ve done something, but NOT one of the act options -->
\		(elseif: $ACT1 > 1 and $ACT1 <= 3)[“I love doing X!”]<!-- if you’ve acted a certain number of times, this can be used to change the dialogue shown. Feel free to change the ACT variables and the counters. This is just visual customization of the battle screen. You can add as many of these as you want.-->
\		(else:)[“I'm saying something...”] <!-- catch-all dialogue in case none of the above conditions are met. This section of code is optional and replicates enemies talking to you during their attacks. -->

		[DODGE]<dodging|<!-- This hook is clicked to dodge, and is set to where the faster you click it, the less damage you’ll take. -->
\
\		(click-replace: ?dodging)[
\			(if: time >= .1s and time <= 1.5s)[<!-- you can change the timings on these to make it easier or harder for players to dodge. -->
\				(set: _clicked to true) <!-- this signals that you clicked on the link so that the time out event down below doesn't happen. -->
\				(set: _youdamage to 0) <!-- if you clicked it really quickly, the enemy does no damage -->
\				You've dodged all of the attacks. [[0 HP lost!|MAIN BATTLE SEQUENCE]] <!-- this returns you to the main fight sequence. -->
\			](elseif: time > 1.5s and time <= 4s)[
\				(set: _clicked to true)
\				(set: _youdamage to (random: 1, 5))
\				(set: $HP to it - _youdamage) <!-- this removes the proper amound of HP from the player -->
\				You've dodged most of the attacks. [[_youdamage HP lost.|MAIN BATTLE SEQUENCE]]
\			](elseif: time > 4s and time <= 6s)[
\				(set: _clicked to true)
\				(set: _youdamage to (random: 5, 15))
\				(set: $HP to it - _youdamage)
\				You've dodged some of the attacks. [[_youdamage HP lost.|MAIN BATTLE SEQUENCE]]
\			](else:)[
\				(set: _clicked to true)
\				(set: _youdamage to (random: 15, 19))
\				(set: $HP to it - _youdamage)
\				You didn't dodge any of the attacks. [[_youdamage HP lost.|MAIN BATTLE SEQUENCE]]
\			]
\		]
\		(event: when time > 6s and _clicked is false)[<!-- if the player waits too long without clicking the link, it times out and they take the max damage. -->
\			(replace: ?dodging)[]
\			(show: ?dodging)[<!-- this is set up like this (as a separate replace and show) so that only the X HP lost part is a link, not the whole hook. -->
\				(set: _youdamage to (random: 15, 19))
\				(set: $HP to it - _youdamage)
\				You didn't dodge any of the attacks. [[_youdamage HP lost.|MAIN BATTLE SEQUENCE]]
\			]
\		]
\	]
\](else:)[<!-- if the enemy's HP is 0, they can't attack... -->
\	(redirect: "MAIN BATTLE SEQUENCE")
\]