Developing a Twine game; random assorted questions

Hello!
I’m developing a rather ambitious Twine game using Sugarcube 2.35. I’ve run into some roadblocks, so I’d love to post here when I need help!

Currently, I’m dealing with two main issues:
First, I can’t figure out how to make <<links>> type out text. I’m looking for a sort of middle ground between a link-replace and a normal link. What I’m attempting to accomplish is a button to pick up an item; <<link "Pick up item.">>. It runs an <<if>> statement when clicked, checking if you have enough inventory space. If you have space, you’ll pick it up. If you don’t have space, then it’ll say “You don’t have enough inventory space!,” yet the link still remains (hence why it can’t be a link-replace). Any ideas on how I can do this? Placing simple text or even a print function into the <<else>> of the if statement inside the link doesn’t seem to do anything.

Secondly, I’m trying to make a digestion system. If you eat something, it increases your “fullness;” if you’re too full, you can’t eat something that goes over your fullness (an idea that also suffers from the previous issue). When you eat something, it’s added to an array, with an HP stat and a weight stat. Every digestion tick (custom system I’ve already made), the first item in the aforementioned array will lose 1 point of HP, and will add 1x its weight stat to the player’s weight. My question here is, how can I make an array, where each index has its own stats? My idea was to simply make a nested array, but then I don’t really know how to select the first index of the digestion array, and then the first index of whatever’s being digested. Would it be something like $digestion[0[0]]? I also need to be able to read the first value of all these nested indexes, as the HP will be added up in order to calculate your fullness. Though this seems pretty straightforward, as once I know how to read nested arrays, I just need to take digestion.length and use a <<for>> loop to add the HP values of each item inside digestion.length. EDIT: I’ve figured this one out! Although I’m leaving it here in case anyone has feedback to give on it–maybe I’m making some huge mistake programming it this way? ;0

I greatly appreciate any help I can get! Thank you so much for your time :slight_smile:

1 Like

My basic advice would be not to fight with the basic operation of Twine:

  1. Parse/evaluate the passage including variables and conditions
  2. Render the reslting passage to the user by replacing the contents of the viewed HTML
  3. Wait for interaction
  4. Choose the next passage and loop back to step 1

Anything that changes the current page without going through the loop is challenging to do.

For both the inventory and the eating a simple method is to test whether the item can be used and then either create text that says “Object: you are carrying to much to pick it up” or a link “Object: pick up”. This is actually better interaction for the user too: there’s no frustrating clicking on things and “computer says no” moments.

For the digestion thing I’d suggest using objects rather than multi dimensional arrays. This may be easier by dipping into JavaScript though. A food object could have attributes of name, hp, weight, and then you’d just need a simple array of those. You can create them like this:

<<set $food = {
    name: "donut",
    hp: 2,
    weight: 20
} >>
1 Like

Thank you for the inventory idea! I’ll definitely go ahead and add that :slight_smile: (I actually think I tried it before and still have the code, but was just really stubborn on doing this one thing. Oops!).

Don’t worry, I’m using an object to track the player’s stats and features (e.g $plrFeats.weight or $plrStats.STR). As for digestion, I’d considered that, but my thought process is that you can eat multiples of the same object. For instance, you can eat multiple “Test Apples.” So when digesting, if I said “subtract testApple.hp,” it’d subtract it across all test apples within the digestion array, regardless of whether or not they’re the one being currently digested.
I suppose I could make this system work via when you eat an item, the name of that item is put into the ‘digesting’ array, and then every digestion tick, it simply takes the name that’s in the array and inserts it into a (NAME).hp and does stuff to it; and then once the item is fully digested, the (NAME).hp resets, but that’d be more work than it’s worth, since I’d have to create a new object or object perimeter for each edible item.
Also if you’re wondering, I did get the nested arrays idea put together, and it works without issue! :slight_smile:

Multiple objects of the same type are always interesting. The simple answer is to have separate objects for each one, each with their own distinct attributes. Having a copy constructor to clone them from an original helps. This means anything that lists objects has to then count objects by “type” - in general you don’t want the player’s inventory or a location to read “an apple, an apple, an apple…”.

The other way is to have an object that is a count, and a reference to the original. It makes the descriptions simpler, but get/drop/consume more complex. If you then want to then mutate one item, you probably need to follow a copy-on-write pattern: once you use a setter on a value it creates a copy of the original items attributes locally before changing the local value.

Most Twine games tend to be simple enough that there’s only one of a thing, or they just keep a count of a very small number of items. Having said that there are pre-built inventory systems you can use instead of rolling your own.

I’m afraid I have…absolutely no idea what you’re talking about here :sweat_smile: I’m sorta new to Sugarcube and have never touched Javascript, so I only know about half the stuff that’s present in the Sugarcube documentation.

Could you explain a little? What’s a copy constructor; are you implying I can clone variables on command? (i.e <<clone $variable>> results in $variable and $variableClone1)

I’m not exactly sure what you mean by “Doing this means anything that lists objects will have to count objects by ‘type,’” as in how doing whatever will make the entire system work differently; though on an off note, I do get that you’re implying that the inventory should say something like “You have: Apple (x6), Torch (x2).” I agree that’s a much cleaner interface that I stupidly hadn’t considered!

For your second paragraph…you’ve completely lost me. :fearful: So you’re suggesting I make a generic object that somehow counts the amount of a specific item thats in my inventory, and somehow references the original, then somehow mutates an item (I assume this means “if you wanted to reduce the HP of one of the items”) using a…“copy-on-write” pattern? And what do you mean by ‘locally;’ why wouldn’t it be global? Etc etc. If I’m not making any sense it’s because I have no idea what’s going on, haha!

Also, as for using a pre-built inventory system…I’ve got a weird kind of laziness. I’d rather build my own from the ground up just so I don’t have to learn how to understand someone else’s, haha! It comes with the bonus of being able to tailor it exactly how I need it. Of course, I understand doing things in this way kind of stifles my growth, since it’s probably a good idea to study other peoples’ code so I can learn better habits, but…I’ll save that for a better day ;).

Okay, sorry. It might be worth looking into JavaScript, as it feels like you are writing something more complex than the typical Twine game.

In general terms a constructor is used to generate a new instance of an object with parameters. A copy constructor takes a single parameter of the same type as the object being created. The implementation usually assigns the fields of the original to it’s own fields. You end up with two objects, the original and the copy, and changing fields in one won’t affect the other. You can find more about this specific to Twine/SugarCube/JavaScript here.

The added complexity comes in that section of the manual comes from the way Twine uses JSON to create save files. These save the variables and fields, but not any methods associated with the object.

SugarCube has a clone() function that you can use to make copies of variables.

<<set $foodCopy to clone($food)>>

If later in the code you change the foodCopy.hp value, the food.hp remains unchanged as they are seperate objects now.

On inventory systems: I’m trying to describe two approaches:-

The obvious way: The inventory is simply a list (or array) of all the things the player is carrying. If they have picked up 10 apples it’s 10 entries long. It’s easy to program get/drop/eat. However if you want a passage that lists the inventory it has to do some work to count how many of the entries are apples (or whatever) - probably using a Map of name to count.

The other disadvantage is that if the player has 1,000 apples the list is 1,000 entries. That’s may be a problem as the save game will be larger, and you may run up against browser local storage restrictions (Chrome tends to be the worst in this regard).

The other way: Instead of a list of items, you have a list of objects that have a count and an item. A player carrying 10 apples has a single entry in the list and that entry has a count of 10 and the item is the apple object. When the player picks up an apple you have to first check if they are carrying any apples. If they are you just increment the count, if not you have to add another entry to the list with a count of one. When they drop/eat one then you have to check if the count is > 1. If it is you decrement the count, but if it is one you have to remove the entry (or be really careful about entries with a count of zero). It does make the inventory passage easier though.

In this second approach you have to be careful if your game has say one “poisoned apple”, and the player doesn’t know it’s different from the other apples. You don’t want the inventory passage to read “10 apples, 1 poisoned apple”, rather “11 apples”.

Copy-on-write (COW) is a general programming technique. In your digestion system, the original un-digested items can just be the original objects - nothing has happened to them yet. Once you start disgesting them and changing their values you need to create a copy so as not to alter all the apples - this would be an example of copy on write. The alternative is to create copies (clones) as soon as you eat them.

Variable scope is a bit odd in Twine. There’s the global State object which carries with it all the “global” and “local” variables in the story and which passages have been visited. But is also carries the history (for when you use the back button), so it also has older copies of those variables too (sometimes called the skein in other IF systems). Then there’s the browser’s Window object which is global to the whole game and isn’t rewound by going back, or persisted in a saved game. The other big difference between the two is that the State is largely updated during the evaluation phase before the passage is rendered to the user, while Window is “live” and affected immediately.

If/when you start messing with JavaScript you’ll often see the game set-up adding functions and other things to Window rather than State as loading a saved game will silently remove any functions added to State due to the way save games work.

I like Twine, but it’s unfortunate that once you try and go beyond simple to more complex stuff there’s suddenly much more to learn and understand.

Yo! Just wanted to let you know that I’m taking a quick little break from this, but I’ll get back to it soon and respond to your message :slight_smile: sorry to keep you waiting!