Changing a stat in a Twine/SugarCube game is simple on paper but often complicated in practice.
If your game is very simple, you can do something like this:
<<set $stat -= 5>>
If you want to get fancy, you can even prevent the stat from becoming less than 0 by doing this:
<<set $stat to Math.max($stat - 5, 0)>>
This works fine until you add something like an inventory passage that the player can visit at any time. When they come back to the passage with the stat change, the change will happen again.
One way to prevent this is to do the following:
<<if visited() is 1>>
<<set $stat -= 5>>
<</if>>
Again, this works if your game is structured in a straightforward way, but it stops working once you introduce some <> macros that reveal passage content after some interaction.
If this interaction happens after you’ve visited the inventory passage and come back, then the visited value won’t be 1 anymore… it will be 2 or more.
To solve this problem, I’ve developed a more robust system for handling stat changes in Twine/SugarCube games. This system ensures that stat changes occur only once, regardless of how many times a player visits a passage or interacts with certain elements. Here’s how it works:
We create a custom widget called statChange to manage these stat changes:
<<widget "statChange">>
<<if ndef $statChanges>>
<<set $statChanges to {}>>
<</if>>
<<if not $statChanges.hasOwnProperty(_args[0])>>
<<set _tempStatValue to 0>>
_args[1]
<<set $statChanges[_args[0]] to _tempStatValue>>
<</if>>
<</widget>>
Let’s break down the statChange widget line by line:
This checks if the $statChanges variable is not defined. If it’s not, it initializes it as an empty object. This object will store all the stat changes that have occurred:
<<if ndef $statChanges>>
<<set $statChanges to {}>>
<</if>>
This checks if the $statChanges object doesn’t have a property with the name of the first argument passed to the widget (which should be the name of the stat being changed):
<<if not $statChanges.hasOwnProperty(_args[0])>>
If the stat hasn’t been changed before, this initializes a temporary variable _tempStatValue to 0:
<<set _tempStatValue to 0>>
This executes the macro:
_args[1]
This records the stat change in the $statChanges object, using the stat name as the key and the change value as the value:
<<set $statChanges[_args[0]] to _tempStatValue>>
We modify our stat-changing widgets to work with this system. For example, here’s one widget from MindWare:
<<widget "moreFeminine">>
<<set _amount to _args[0]>>
<<set _playerGender to State.variables.playerGender>>
<<set _newGender to Math.max(_playerGender - _amount, 0)>>
<<if _newGender == _playerGender>>
<div class="gender-change"><span class="gender-decrease">♀️ Your feminine gender identity is already at its minimum.</span></div><br>
<<else>>
<<set State.variables.playerGender to _newGender>>
<<set _pointText to _amount == 1 ? "1 point" : (_amount + " points")>>
<div class="gender-change"><span class="gender-decrease">♀️ Your gender identity has become more feminine by <<print _pointText>>.</span></div><br>
<</if>>
<<set _tempStatValue to _amount>>
<</widget>>
To use this system, we simply call the statChange widget with a unique identifier and the stat-changing widget:
<<statChange "ugly_bastard_quest_2_AVA_submit" "<<moreFeminine 5>>">>
This system solves several problems:
<<replace>>
macros and other dynamic content, as it’s not dependent on the visited()
count.By using this system, you can create more complex and dynamic games without worrying about unintended repeated stat changes.