[Sugarcube] Dynamically created objects/variables

I’m trying to work out the very basics of a system before I delve too deeply into what I can and cannot do, and one thing I can’t quite figure out is how to create new Objects and or Variables.

Suppose I have an initial empty array called $photos and as the player takes photographs, I would like to populate it with objects which for simplicity I’d like to call $photo1, $photo2 and so on. Each photo would have their own properties such as a description. The really important part is that the photos should not exist until they are taken.

I know how to manually create a new object, but the part I’m stuck with is trying to create that new $photo[num]. I tried to do something like

<<set _num = $photos.length++>>
<<set $photo[_num]>>
<<photos.push(“$photo[_num]”)>>

But that didn’t work at all. Is this impossible and/or too complex?

I though about simply creating a different storage array populated with $photo1, $photo2 and so on at the beginning of the game and then moving them to the $photos array actually accessible during the game as needed, but that felt like a fairly hacky solution so I wanted to see if my original idea was viable.

There are actually a few syntax errors in the code that you posted. I’ll just point them out real quick before giving you the answer.

<<set _num = $photos.length++>>

Don’t do ++ here. Do + 1 instead. The ++ operator does return 1 more than the value, BUT the issue is that it also increases that value by one after. Example:

<<set $a = 0>>
<<set $b = $a++>>

The result of both $a and $b will be 1. If you did $b = $a + 1, then $a would stay 0.

But in your code it’s especially wrong because $photos.length is supposed to be returning the length of the array, but you’re forcing it be increased by 1, so now the length isn’t accurate. That’s assuming modern JS even lets you overwrite that value (I believe it used to).

And now for more of a logic issue:

Arrays are zero based. The first entry of the array would be $photos[0]. So increasing _num by one is actually going the wrong direction. IOW, $photos.length - 1 will get you the index of the last item of an array, assuming the array has at least one item.

That being said, you’ll get an error if you attempt to access an array index that doesn’t exist, so your first two lines don’t really make sense in general. The only line that that makes sense is the .push line, and that one isn’t formatted correctly. :confused:

Okay then! Hopefully I didn’t insult you by trying to explain the errors in your code. Now I’ll show you how to do it:

The first way is using an array:

<<set $photos = []>>
<<set $photos.push({name: "some photo", description: "It's just some photo."})>>
You see <<= $photos[0].name>> here. <<= $photos[0].description>>

The downside with this method is that the photos are added to the end of the array. IOW, if you’re able to take a photo of a tree or of a bird in a random order, you won’t know if the bird photo is $photo[0] or $photo[1]. If the goal is just to collect photos and then display the ones you’ve taken in your status screen or something without checking whether a specific one exists, then this method works just fine.

However, if you want to easily check whether a specific one was taken, I’d suggest the object list method:

<<set $photos = {}>>
<<set $photos["bird"] = {name: "bluebird", description: "It's a pretty bird."}>>
There's photo of a <<= $photos["bird"].name>> here. <<= $photos["bird"].description>>

And if you want to see if you’ve taken the photo already you can do:

<<if ("bird" in $photos)>>
	You've taken the bird photo!
<</if>>

There. That should be all there is to it.

1 Like

One very minor correction, this:

<<$photos.push(...

should be done like this:

<<set $photos.push(...

or this:

<<run $photos.push(...

The <<set>> and <<run>> macros do the same thing, but it sometimes helps to think of things as being “run” instead of as being “set” when that better describes what is going on.

Also, instead of doing $photos["bird"] (using bracket notation) you could do $photos.bird (using dot notation). Generally speaking, dot notation is much quicker to type, however it does have some limitations on the kinds of property names you can use with it (i.e. only alphanumeric characters plus “$” and “_”, though the first character can’t be a number). Bracket notation is much more flexible in the kinds of names you can use, thus is generally the “safer” method, but it’s a lot more to type, especially if you have to do it over and over.

Enjoy! :grinning:

1 Like

Thank you for catching that typo. :slight_smile: I fixed it in my post now.

Thank you very much for both of your answers. I’m essentially running into this thing head on fully expecting to make a lot of mistakes, but I find it easier to learn like that than first memorizing a lot of syntax that I’ll forget by the time I actually do anything.

I got that working with some tweaks, but while I’m here, here’s something a bit more troublesome:

Elsewhere I set up some code to give random properties to each photo object, here simplified to just a name. What I would like to do is for the contents of the $photos[] array to be shown to player in particular passage as a list of links, each link allowing the player to open a different passage for that photo in which they could get more information.

So at some point the game runs something like this:

<<set _nameList = Story.get("PeopleNames")>>
<<set _peopleNames = _nameList.text>>
<<set _splitNames = _peopleNames.split("\n")>>
<<set _randomNumber = random(0, _splitNames.length)>>
<<set _name = _splitNames[_randomNumber]>>
<<run $photos.push({name: _name})>>

This will populate $photos[] with $photos[0], $photos[1], $photos[2] and so on, each one having a ‘name’ property randomly picked from a different PeopleNames passage. There would be a lot of repeated names which would get confusing but that’s another matter. What I would like to have is the player go to, say a [[Photo Inventory]] passage, and find in that passage a list of links to each photo taken thus far, each being their own dynamically created passage containing that photo’s properties (In this case, just a name).

I THOUGHT I had worked that out by doing this:

<<for _i = 0; _i < $photos.length; _i++>>
[[$photos[_i].name|$photos[_i] ]]
<</for>>

But the links created come off red and leading nowhere. Ideally, these should link to a passage containing just
<<$photos[_i].name>>
As a placeholder for a more robust description. I’m also wondering exactly what to do with that _i in there. If this is the (again, dynamically created) passage for $photos[0], how can I let twine know to read that _i as 0?

First of all, you shouldn’t use passages to store data. It’s inefficient (as you can see from the number of lines of code it takes for you to extract that data), and the more passages you have, the slower the Twine editor is.

Instead, for any data which never changes, you should store that on a property of the setup object in the JavaScript section and/or the StoryInit passage. So you might have something like this in your JavaScript section:

setup.peopleNames = ["Anne", "Bob", "Charlie"];

You could then add names to the $photos array like this:

<<run $photos.push({ name: setup.peopleNames.random() })>>

Though I should note that there’s a risk you’ll get the same name more than once.

Then, to display the links, you’d only need to do this in your passage:

<<for _i = 0; _i < $photos.length; _i++>>\
	<<capture _i>>\
		<<link $photos[_i].name $photos[_i].name>><<set $personIndex = _i>><</link>>\
	<</capture>>\
	<<if _i < $photos.length - 1>> | <</if>>\
<</for>>

That would create clickable links to the “person name” passages, each separated by a “pipe” ("|") character. After the user clicks the link, the code could then access the person’s info using $photos[$personIndex]. (The backslashes ("\") prevent line breaks.)

Note that you need to use the <<capture>> macro, in order to “capture” the value of _i at the point in the loop where the link is created, otherwise the <<set $personIndex = _i>> code would use the value that _i had at the point where the user clicked.

Also, using the <<link>> macro prevents the Twine editor from trying to show links to invalid passage names, like it would if you used the square brackets method of creating links.

That said, instead of making different passages for each person, you could simply store all of the data you need on the setup object, have the $photos objects refer to that data, and then send all links to the same passage where that information is displayed based on the $personIndex value.

For example, you might have this in your JavaScript section:

setup.peopleNames = ["Anne", "Bob", "Charlie"];
setup.peopleJobs = ["Architect", "Business Person", "Chef"];

Build up the $photos array like this:

<<set _tmpObj = {}>>
<<set _tmpObj.name = random(0, setup.peopleNames.length - 1)>>
<<set _tmpObj.job = random(0, setup.peopleJobs.length - 1)>>
<<run $photos.push(_tmpObj)>>

So now you’re only storing the indexes of the data on the setup objects, rather than the data itself. That would help minimize the amount of data you’re storing in the game history, which means faster saves, loads, and passage transitions.

You’d then display the links like this:

<<for _i = 0; _i < $photos.length; _i++>>\
	<<capture _i>>\
		<<link setup.peopleNames[$photos[_i].name] "Person Info">><<set $personIndex = _i>><</link>>\
	<</capture>>\
	<<if _i < $photos.length - 1>> | <</if>>\
<</for>>

That would point all of the links to the “Person Info” passage, and set $personIndex to indicate which person was clicked on.

Then, in the “Person Info” passage you could just do this:

''Name: '' <<= setup.peopleNames[$photos[$personIndex].name]>>
''Job: '' <<= setup.peopleJobs[$photos[$personIndex].job]>>

and that one passage would work for every person. You’d just need to create data for each of the fields you want to display there on the setup object. (<<=>> is shorthand for the <<print>> macro.)

Hope that helps! :grinning: