Better way to handle <<if>> macros

Twine Version: 2.3.16
Story Format: Sugarcube 2.36.1

<<set $charGold to "[insert value]">>

<<if $charGold eq 1>><<set $GoldValueImg to $GoldCoinImg>><</if>>
<<if $charGold gte 2>><<set $GoldValueImg to $GoldCoinDoubleImg>><</if>>
<<if $charGold gte 10>><<set $GoldValueImg to $GoldCoinSmallStackImg>><</if>>
<<if $charGold gte 100>><<set $GoldValueImg to $GoldCoinMediumStackImg>><</if>>
<<if $charGold gte 1000>><<set $GoldValueImg to $GoldCoinLargeStackImg>><</if>>
<<if $charGold gte 10000>><<set $GoldValueImg to $TreasurePileImg>><</if>>

[img[$GoldValueImg]]$charGold

Wondering if there’s a better way to handle these if statements. The function works as intended, but it’s sloppy, and I’m not the best at doing arithmetic, especially in code.

The way you’re doing it is fine.

If you wanted to be super-efficient about it, you could do it this way:

<<set $charGold to [value]>>

<<if $charGold >= 10000>><<set _GoldValueImg to setup.TreasurePileImg>>
<<elseif $charGold >= 1000>><<set _GoldValueImg to setup.GoldCoinLargeStackImg>>
<<elseif $charGold >= 100>><<set _GoldValueImg to setup.GoldCoinMediumStackImg>>
<<elseif $charGold >= 10>><<set _GoldValueImg to setup.GoldCoinSmallStackImg>>
<<elseif $charGold >= 2>><<set _GoldValueImg to setup.GoldCoinDoubleImg>>
<<elseif $charGold == 1>><<set _GoldValueImg to setup.GoldCoinImg>>
<<else>><<set _GoldValueImg to setup.NoGoldCoinsImg>><</if>>

[img[_GoldValueImg]]$charGold

and you’d set all of those image values on the SugarCube setup object either in your JavaScript section or your StoryInit passage. (Note that I added a case for when $charGold < 1.)

IMPORTANT: Data on the setup object must never change during gameplay, because it doesn’t get included in the game’s save data. If you do change data on the setup object during gameplay, then saves will likely be broken since loading those saves would not also restore the changed setup data. This is also why the values on the setup object should be set initially in the JavaScript section and/or the StoryInit passage, since those are run first when a save is loaded.

That code is more efficient because it only sets the value of _GoldValueImg once. Also, by making _GoldValueImg a temporary variable and putting all of the image names onto the SugarCube setup object, you’re storing less data in the game’s history, which means faster saves, loads, and passage transitions. (It’s strongly recommended that, instead of using story variables, you use temporary variables whenever possible. Story variables should only be used for data which needs to be kept so that it can be used in future passages.)

Now, if your list was larger, then you could set the data up as an array of objects, with each object having the lower boundary value and the image name, going from largest to smallest. Something like this:

<<set setup.coinImages = []>>  /* Initialize the array. */
<<set setup.coinImages.push({ min: 10000, image: "images/TreasurePile.png" })>>
<<set setup.coinImages.push({ min: 1000, image: "images/GoldCoinLargeStack.png" })>>
etc...
<<set setup.coinImages.push({ min: Number.NEGATIVE_INFINITY, image: "images/NoGoldCoins.png" })>>

Then you could find the right image from that data by using a <<for>> loop, like this:

<<for _i = 0; _i < setup.coinImages.length; _i++>>
	<<if $charGold >= setup.coinImages.min>>
		<<set _GoldValueImg = setup.coinImages.image>>
		<<break>>
	<</if>>
<</for>>

However, for your current list size, doing that would be about the same amount of work as what you’re doing already.

Also, if you’re repeatedly using that code in various passages, I’d look at turning it into a widget or a macro that you could call instead.

Hope that helps! :slight_smile:

I appreciate the fast reply! Really what I was hoping for was a more streamlined snippet, but as I’m reading into Twine’s if/else/elseif macros it doesn’t seem to be possible to break it down any further than I already had. I’m coming from lower-level languages and expecting Twine to be as capable as they are, which obviously isn’t a fair comparison to make.

I use, essentially, a custom StoryInit passage called script.Init where I initialize all the important starting variables on load, so I can then modify them later without the fear of <<set>> looping. Essentially script.Init does all the initial variable loading, and, if I need to, I can recall it by going to that passage. I can also then modify any initialized variable, since they’re not technically being set in StoryInit, and therefore can be saved with SugarCubes save function.

For temporary variables, like you’re suggesting $GoldValueImg should be; if I’m calling that image reference multiple times throughout different passages (there isn’t only one passage where that image table will be displayed), then setting it as a temporary variable adds more stress than setting it as a global variable, no? I mean, on this scale it would be negligible and, I’d be willing to wager, practically immeasurable, but if we’re coming from a purely “as efficient as possible” route, I’d be curious which route is actually less efficient.

Well, SugarCube is basically “syntactic sugar” on top of of JavaScript (which I’d guess explains the name), so you should be able to do whatever is possible in JavaScript with it.

I would recommend against using a custom passage for that. You should either use the JavaScript section or the StoryInit special passage, since they’re both executed first before loading the story variables in a saved game. If your script.Init passage isn’t run properly when a save is loaded, then that may cause your saves to be broken.

I’m not quite clear what you mean there or what problem(s) you’re trying to avoid by using your method. If you could clarify, then I could perhaps suggest a safer method of doing what you want.

If you’re setting that value appropriately each time just before you use it, which I’m assuming is the case since $charGold may have changed since the last time you checked, then then you shouldn’t store it in a story variable.

Story variables take up space in the game’s history, as do any changes to those variables. The larger the game’s history is, the slower saves, loads, and passage transitions are, since the history is updated and stored/loaded in each of those cases. Thus, it’s more efficient to use temporary variables or the setup object whenever you can, since they don’t get stored in the game’s history.

Yes, the effect of any one variable may be negligible, however, the effects are cumulative, both across the number and size of story variables and in how many moments are stored in the history (each passage transition normally adds a moment to the history, up to the number of moments set in the Config.history.maxStates setting). This means that problems with history bloat are best avoided from the outset, rather than potentially having to clean your code up later if those cumulative effects become noticeable, which can happen in larger games.

Hopefully that make sense. :slight_smile:

Good point; not trying to talk shit about Twine/Sugarcube at all, it’s just a slight learning curve from what I’m used to.

The script.Init passage loads only on the story load; it basically acts as a formatter so that all the listed variables don’t display [undefined] initially. The save file contains all of the set variables that change during the experience, and so it doesn’t really matter if the .Init file loads or not once the save has been registered. At least, that’s been my experience every time I’ve messed around with it; I’m wary of using StoryInit because of this:

Perhaps I’m misunderstanding something, but all of the variables I’m using need to be saved in the save file to be referenced later; $charGold (actually $invCurrencyGold, but whatever) needs to be changed multiple times during gameplay. So from my understanding if I set $invCurrencyGold in StoryInit, then from what I’ve read I should not modify that variable anymore, because like you said;

I guess my contention with this point isn’t that I don’t believe it, just that modern computer hardware should be able to handle 5mb save files (the max Google Chrome can handle, by default, for example) without any noticeable hanging. 5mb is 5,000,000 characters. If you can’t store all the data you need in 5,000,000 characters, using Twine, you should be using a different application to write in. Doing some quick math, 5,000,000 characters is ~770,000 to 1,000,000 words. Or ~3000-4000 pages. That’s 12 point TNR, and double spaced. I think, for example, even with some of the higher stories I’ve seen that boast about 150-200,000 words, this becomes a non-issue. I could be wrong on that 150-200,000 part, but assuming this to be true, that leaves another 570,000 words for variables in a worst case scenario. I could set 1,000 global variables and not even come remotely close to this limit, especially considering my next reply.

My maxState is currently set to 2; I don’t need anymore since each passage calls the appropriate variable injector; I have one for each set of variables I plan on using. Each injector sets the variables for it’s specific category; it’s a chain function. We call something like script.VarInjector, for example, whenever we need to set variable data.

Also, I’m not trying to argue by the way, and I apologize if it comes across that way. I am, like I said, not used to this form of development, and being restricted by a literal file size limit during development is something that I have never experienced practically before, and I’m trying to understand if it’s a limit that I should actually be concerned about, or if it’s more a ghost myth that remains from the days of 32kb memory, where every byte matters.

HiEv was specifically referring to the setup object, not story variables—i.e., setup.foo vs. $foo—with the quote you’re replying to. Existing story variables are always recorded when a save is made.

The StoryInit special passage is specifically intended for initialization, especially of story variables. It’s not going to adversely affect your saves, so reinventing the wheel doesn’t make much sense here.

So, it does what StoryInit does, but only for story variables. This makes your method less functional than using the JavaScript section or the StoryInit passage.

OK, but in the StoryInit passage it’s also possible to set up variables which aren’t included in the save data, such as data you set up on the SugarCube setup object. You’re apparently giving up that option by needlessly using your own method.

For context, here’s the full sentence:

As you can see, I was only talking about variables on the setup object. That sentence has nothing to do with story variables.

Storing data which won’t change during gameplay on the setup object is a great way to cut down the size of the history data while still being able to work with those static variables.

If you put data on the setup object in the JavaScript section or the StoryInit passage, then that data will be able to be referenced later if you load a saved game. If you created them in your script.Init passage, then they wouldn’t be available. That’s why I’m recommending against reinventing the wheel, especially in a less useful form like that.

I’m sorry, but you’re getting a lot of things mixed up here.

To clarify, a browser’s localStorage, which is where game save data goes, is normally either 10 MB for desktop browsers or 5 MB for mobile devices (or 2 MB in some rare instances). The browser’s sessionStorage, which is used to track the current state of the game, is also generally limited to 5 MB. Note also that the size of the storage space includes the names of the storage objects. (See also the MDN “Web Storage” article.)

In other words, if your save data is 5 MB, then you might only be able to have one or two saves (SugarCube defaults to having eight save slots, plus an optional “autosave” slot). And that’s assuming that you aren’t overflowing the sessionStorage.

Worse, for HTML pages run directly off the user’s computer (as many Twine games are), the localStorage space in non-Firefox browsers is shared across all such local files (i.e. they’re all treated as having the same “origin”). Meaning that, if the person plays other Twine games on their computer, they might be left with no space at all for a 5 MB save slot. Hence the reason why I wrote “Local Storage Manager”, to help people deal with problems caused by running out of localStorage space for their save data. (You can use that tool to verify the maximum size of your browser’s localStorage and to see the size of your saves. Also, regarding Firefox-based browsers, they currently allocate localStorage space by path and filename, so you won’t have to worry about shared space for those browsers. However, most people currently use Chrome, thus shared localStorage space should be a definite concern.)

Beyond that, I have no idea why you’re bringing up the word count of stories, because it has nothing at all to do with the size of Twine/SugarCube save slots. The data in save slots usually only include a history of story variables, passages visited, and a few other details (see the “Save Objects” documentation). They certainly don’t include the story text (unless you’re storing that text in story variables, which you shouldn’t be).

Regardless, browsers aren’t known for their speed, so compressing/uncompressing 5 MB of data would take an amount of time that would be noticeable by players of your game. This noticeable lag would occur during saves, loads, and passage transitions, exactly like I stated. Hence the reason why I strongly recommend optimizing the data you use to minimize the amount of data stored in the game’s history if it risks getting large enough for this lag to be noticeable.

That’s a good way to help optimize the size of your history data. :+1:

This seems like unnecessary overkill in 99.99% of cases, but I assume you have a reason for doing things like this.

I’ll be honest, it feels like you’re going out of your way to overengineer things because of your history with other languages. The vast majority of the time you don’t need “custom initialization modules” or “variable injectors” for this kind of coding, as there are existing/simpler ways of doing these things.

And you still haven’t experienced that, because nobody here is talking about a “literal file size limit”. Nobody here has mentioned file sizes even once, prior to your comment.

You’re apparently confusing the size of the HTML file with the size of the history data, when the two have nothing to do with each other. You could have a huge HTML file with no save data, or you could have a tiny HTML file which completely fills localStorage with save data. Please don’t conflate these two very different things.

(Since you bring it up, I will mention that, from past experience, the breaking point of HTML file sizes is around 70 MB in desktop browsers, with Firefox being able to handle the largest file sizes. That said, it obviously varies by browser and things may have changed since then, since this was a couple of years back. However, unless you’re doing something seriously wrong, you’ll likely never hit that file size limit.)

The thing you might need to be concerned about is the amount of data in your game’s history. If it’s a small or medium sized game, you probably don’t need to worry about it much unless you’re cramming a ton of data in your story variables for some strange reason. However, for larger games where you may end up tracking lots of data, especially if the data frequently changes, then optimizing the data in your story variables is a good idea.

Most Twine games have save data which is less than 0.25 MB, and thus have nothing to worry about in this regard.

However, if you’re actually expecting to have save slots which are a megabyte or larger in size each, then this is definitely something worth paying attention to.

Remember, your original question was about efficiency, and I said you what you were doing was fine, but explained how to be “super-efficient” just as an example. I was not saying this is something that everyone has to worry about (though it certainly doesn’t hurt).

Hopefully that clears things up for you. :slight_smile:

I see, I was completely confused on what the setup object was; I hadn’t really read much into the documentation yet. Can you explain the process of how Sugarcube stores data? From the way it’s being described it appears as if what’s happening is that story variables ($var) are being treated as appendages to the data. I’ll see if I can describe what I mean, and hopefully explain some of my confusion.

It appears that as you change $var to anything, instead of being removed and rewritten as:

$var = 1

to

$var = 2

The engine appears to be appending the variables together;

$var = 1
$var = 2

And then just reading the most recent change as the newest information, which is what causes the memory issue after enough changes. At least, this is my interpretation, and I’m sure it’s wrong. I haven’t seen under the hood to know exactly how variables are treated. If this is the case (and it’s a strong if), why are they treated as linear objects? And if this isn’t the case then how are variable modifications being written to memory? Or… rather, what is the benefit of writing the actual changes to memory, rather than just editing the existing = 1?

Please be gentle, I’m not an expert on byte storage at all, and you’re obviously much more educated on the subject than I am, so forgive this horrible analogy, but it appears as if when changes to the variable are made what is actually happening is something like

$var = 10100110 + $var = 10100111 + $var = 10101111

Where the last appended dataset is the one that’s read, and the others are ignored; similarly to how trashbins work (the data isn’t actually being deleted, just flagged as writable).

If I’m completely wrong here, that’s totally fine too. I am more just curious about how it all operates so I can understand why using story variables is bad practice, because as I first read about $var in the documentation they appeared to just be standard variables.

I’m willing to take a gander that the issues I’m having with this concept seem to be from the fact that each passage is logged uniquely, instead of being re-written over itself. IE, if I go to a passage once, do some stuff, go to another passage, do some more stuff, and then go back to the first passage, it is treated as 3 unique sets of data, instead of 2; the first set would become null in most of the other set structures I’m familiar with.

note: The following description is not 100% technically correct.

The History System is made up of an array of Moments, each one representing a Passage that was visited during the playthrough of the project, in the order those Passages were visited.

Each Moment stores the details of the Passage it represents, including the current state of all Story Variables that were defined when that Passage was being visited. That current state does not include any changes made within that Passage itself.

A Save basically consists of the current state of the History System at the time the save was made.

The “current” Passage being visited is also represented by a Moment, however this one is not stored in History.

@vessi It’s worth reading this section of the sugarcube documentation and then greyelf’s post will make more sense.

http://www.motoslave.net/sugarcube/2/docs/#guide-state-sessions-and-saving

My understanding is that if the system crashes the last moment (ie. relating to the passage last visited) will be save and restored on reload. What happens on the last passage is saved as the project moves to the next passage.