If you're looking for an inventory system in Twine 2's default Harlowe Story Format, I have here a fairly simple system which I wrote for my incomplete Myst Jam twine game.
There are a lots of Twine code snippets available out there. The way to make an inventory outlined here is definitely not the only, or even the best, way to do it. I just wanted to write my own version, and keep it simple. I also wanted an inventory I could access as its own discrete passage, as well as give each item its own Twine passage so that I could sometimes call on items using the (display:) macro, without having to rely on calling to the umbrella inventory passage.
Edit 28/01/16: Squinky reminded me that I didn't write a way of checking the inventory's length and making sure it's greater than zero. I'm adding that in as step 4.2
The steps outlined in this post are:
- Desired behaviour, basic setup, and structure
- Setting up an empty inventory array ➝
2.1 Adding objects to the inventory ➝
- Linking to the inventory in the footer ➝
3.1 Using if/else to control when links are displayed in the footer ➝
- Writing the inventory passage ➝
4.1 How to exit the inventory, i.e. return to the "current" passage ➝
4.2 Edit 28/01/16: How to check the inventory's length in case of an empty inventory ➝
- Using true/false variables with your inventory to influence in-game events ➝
- Removing items from the inventory ➝
1. Desired behaviour
1.1 Basic setup
Harlowe is the default story format, and is focused on making it easy to add basic interaction to your stories in a readable, concise way. — source: Twinery Wiki
I wanted to create a twine game in which you could explore spaces and pick up certain items, so that if you're missing certain items like the player's keys you could get locked out of the player's home, or if the player has a phone in their inventory, the playable character can call their contacts or check their voicemails.
The passages setup of my inventory, as I envision it.
This is what the player should see when they're actually in the inventory passage.
In my inventory passage, I'm going to want to list all the objects in my inventory. Depending on the game, that list will grow (and from time to time, shrink) as the game progresses. Using arrays to keep track of what's in your inventory seemed to make sense to me. An array is a list of different words or text, referred to as strings in this blog post.
This array keeps a list of all the things that get put in the inventory. A decision I made was that all the individual strings in the array will also match the object's passage names. In these macros, we create variables (such as arrays, booleans, or strings) using the
Put this macro in the start passage (for more information on the start passage, check out the official Twine docs):
(set: $inv to (a:))
(a:) sets a blank array, and gives it the variable name
Every time the character finds an object, create a passage with the name of that object, and then add the name of the object passage to the $inv. For example:
Say in Passage 1, the player finds a smartphone. In Passage 1, there should be a line of code that reads:
(set: $inv to $inv + (a: "smartphone"))
Using the same
set notation as before, this macro says that you are creating an
$inv which is whatever had been saved to
$inv before, plus the
Bonus: if you want to add more than one object at a time, you would write:
(set: $inv to $inv + (a: "smartphone", "keys"))
Or, say you want to initialize a brand new array with several items in it (such as at the start of the story, you want your character to have certain objects on them already):
(set: $inv to (a: "smartphone", "keys", "cyberdeck"))
Note: I'll be covering how to remove items from an array at the end of this blog post.
The link to the inventory shows up in every story passage (with some exceptions) so it makes sense to put it in the footer passage in your Twine game.
A footer passage is a passage which appears at the end of every other passage in the game. So if you need text that appears (or might appear) in any passage, it's a good passage to learn how to manipulate.
The footer passage in Harlowe is actually any passage which has "footer" in its tags (as demonstrated in the screenshot above). It will show up at the bottom of every passage. So when creating the footer, you'll want to add a new passage, give it whichever name you prefer (the name "footer" works for me, to keep it simple), and make sure to give it the tag "footer".
In the footer, I am going to write:
Using that code in the footer means that in every passage, no matter what, there is going to be a hyperlink to the inventory passage in the text "Check inventory."
So, I don't actually want a link to the inventory at the bottom of every single passage. Some passages should not have a link to "Check the inventory"—such as the inventory passage itself, but also on the introduction (or title screen) of the twine game, and in a handful of other special passages where I've decided it won't be possible to check the inventory for game reasons.
So, in order to control exactly when the
Check [[inventory]]. hyperlink is displayed in the footer, I am going to surround that link to the inventory with an if/else script.
In these macros, if and else are written using the following syntax:
(if: something is true)[Do something]. The
() brackets are important, and have to be in this order. This basic syntax is reused for a lot of Harlowe macros.
In the first
(if:) statement, if the passage's name is inventory, don't display the link to the inventory passage (because the current passage is already the inventory!):
(if: (passage:)'s name is "inventory")[<!--Do nothing-->]
Note: I use HTML comments in Twine.
<!-- This is a comment in HTML -->. Comments in code are ignored by the browser and the interpreter but useful to leave for future you. The
<!--Do nothing --> comment is something I leave whenever I want to make sure not to delete the code in the future, but I know that the code is not going to output or visibly do anything if it works.
The second if statement is actually an
(else-if:) statement which is evaluated (or checked) only after the first
(if:) passage above is evaluated. This statement checks if the current passage has a tag called donotshowinventory, and then does not display the link to the inventory passage :
(else-if: (passage:)'s tags contains "donotshowinventory")[<!--Do nothing-->]
For all other passages, display the link to the inventory using the
(else:) statement, which only runs if both the first if and the second else-if both turn out to be false:
I gave the inventory passage the name inventory for simplicity. Inside the body of the passage I wrote
<h2>Inventory</h2> and used the array $inv to list all the objects in the inventory like so:
Your inventory contains a (print: $inv.join(", ")).
(print:) macro in Harlow is used to print the values of variables.
$inv is an array, so the print macro is going to fish out the individual strings in the array and print them out: the
$inv.join(", ") signifies that you want a
, to be printed out in between the different strings.
In step 2.1, I added a smartphone to the list, in the example screenshot below, the smartphone is printed out.
Say you want to be able to be able to display something from the smartphone passage. You'll want the player to click on the smartphone text and access the information in the smartphone passage. The way I wrote this is :
(click: "smartphone")[(display: "smartphone")]
(click: "smartphone")[<!-- Do something here-->] macro uses the same syntax as the if/else macros, and works by searching through all the text printed out before it, to see if the string "smartphone" (or any other text you want it to find) exists. In my case, I want whatever is in the "smartphone" passage to display when the text is clicked, so I also use the
(display: "smartphone") macro inside the
(click:) macro. For the player, clicking on the "smartphone" text yields this result:
(click:) macro is safe to insert into a passage even if the text you want it to target hasn't been added to the inventory array
$inv. It'll only highlight text that has been already added. So if you've only progressed far enough in your twine game to have a smartphone in your inventory, but your player character hasn't found their housekeys yet, you can still put
(click: "housekeys")[Do something] and it will only activate once the player has found their housekeys and the string "housekeys" has been added to the inventory array.
If you take a close look at the screenshots in section 4, you'll notice that at the very bottom of the passage displayed in the browser is the hyperlink "Return".
This is a link that takes the player out of the inventory and back into the game, back to the passage they were in before they clicked on "Check inventory". Instead of return, you could also call it "Exit inventory".
I don't recommend urging players to use the "back" arrow button made available by the Harlowe theme, as that is technically supposed to reset any variables or decisions that may have been set by the player.
There are a lot of ways to write the code that powers exiting the inventory. The simplest I've found to write is:
(link-goto: "Return", (history:)'s last)
Note: A foreseeable issue that may arise is that if your inventory allows you to access different passages in that inventory, then
(history:)'s last will come to mean those other passages you visited. This is one of the reasons why, in section 4, I favoured the use of the
(display:) macro to display information in an object's passage instead of actually visiting that passages' page. There are a few workarounds to this problem, which involve setting variables of passage names and some if/else-if/else loops carefully placed in footer passages but for the sake of brevity I won't get into them in this blog post.
Say you have a game in which it is possible for the inventory to become empty.
Using the code outlined so far in the inventory passage—specifically
Your inventory contains a (print: $inv.join(", ")).— checking an empty inventory will result in this string being displayed in the browser:
Your inventory contains a .
This is an incomplete sentence which looks clumsy, or like a bug.
To remedy this, we can put this that sentence inside an if/else loop which calculates the length of the
$inv array to make sure there is actually something in the inventory.
(if: $inv's length > 0)[Your inventory contains a (print: $inv.join(", ")).] (else:)[Your inventory is empty.]
It's actually pretty easy to use objects in your inventory to affect in-game actions (for instance, needing a key to unlock or lock a door). What you need are if/else statements.
With the example with the housekeys, I would want to check the
$inv array to see if "housekeys" are in the array. For instance, you could place the following in a passage:
(if: $inv contains "housekeys")[Oh! You can use your keys to [[unlock your front door]].] (else:)[You cannot open the door. The door is locked. If only you had your housekeys!]
The above code affects what passage links become available to the player. Without the "housekeys" in the inventory, it isn't possible to access the passage titled "unlock your front door"—you can use the inventory to control what paths become available to the player. You can also use these if/else (or even else-if) statements to set up variables or other code which have consequences for the player and change aspects of the gameplay.
So far we've used the array to list the inventory contents in section 4, and we've used the array to set booleans that affect in game events in section 5.
Now we're going to figure out how to drop items by manipulating the array
In section 2.1, we learned how to add objects into an array as follows:
(set: $inv to $inv + (a: "smartphone", "keys"))
Removing items from an inventory actually uses a very similar method.
Say you have the an inventory with a cyberdeck, a smartphone, and keys:
(set: $inv to (a: "smartphone", "keys", "cyberdeck"))
Say you want to drop your keys, you would write:
(set: $inv to $inv - (a: "keys"))
If you asked Twine to print out the values of $inv, it would show that keys have been removed from the array and
$inv is now just
(a: "smartphone", "cyberdeck").
Inventory: (print: $inv.join(", ")). <!--This prints out the following line:--> Inventory: smartphone, cyberdeck.
Because we removed the object from the array, it no longer shows up in the inventory passage, and any if statements which check $inv for that object will also evaluate to false! Nifty.
This upcoming weekend is the Global Game Jam and let me know if this inspires you to make cool things in Twine.
If you catch any mistakes or have any suggestions, find me on twitter @gersandelf.