Why does this code for adding an item to an inventory work?

If you are requesting technical assistance with Twine, please specify:
Twine Version: 2.3.16
Story Format: Sugarcube 2.36.1

Hi,
I’ve been working on my inventory system today. Inventories for each character are saved as a list like [[‘item1’,count],[‘item2’,count],…] under the ‘$inventory’ object, as ‘$inventory.Name’, and they’re all loaded in from an external JS file at game start, because I found it easier to keep that (and the plethora of other data I’m storing in JS) as separate files. It’s largely all my own work within the widgets system, so it’s not the greatest (and there probably are better ways to do this, but I’m most comfortable with it), but after getting my ‘read inventory in several ways’ widgets working, I added an ‘add item’ widget.

Except that I got it to work, in that it still set inventories how I wanted, but it was throwing an error almost every time. As far as I could tell, something in the ‘increase item count by 1’ portion was amiss, as any time I was creating a new item entry, nothing went wrong. It was also referencing an input that shouldn’t have even been there as the reason it threw an error. So, I removed the main line for setting the main inventory to the temporary inventory to see if that got rid of the error. And, lo and behold, it did.

Except the code still works. Despite never asking the code to actually set it as such, or even referencing it after the original temporary inventory setting. And I have no idea why this is. I know that some programming languages link variables together, so that changing one changes the other, but I can’t seem to reproduce that elsewhere so I don’t think it’s the case.

If this is fine and there’s no hitches, I am more than happy to use it, but I just want to make sure there’s nothing untoward happening here that will come back to bite me later. I’ve included below a sort of example of the code that shows that it works.

For readability, inventoryRead takes the character’s name as an argument, and just sets the temporary variable _inventoryRead to whatever the character’s inventory is. So for James below, it returns the nested list [[‘coins’,5],[‘busPass’,1]]. It sets the variable _inventoryRead.

getInventoryContents takes the character’s name as an argument, and returns a list of all the items in the character’s inventory. So for James down below, it returns the list [‘coins’,‘busPass’]. It calls inventoryRead, and sets the variable _getInventoryContents.

addItem takes the character’s name, the item you wish to add, and the amount as arguments. The amount just decides how many times the widget runs on the same inventory. It calls getInventoryContents, checks to see if the item you wish to add is in _getInventoryContents, and if so it checks through the list until it gets to where the item is, then increases the value of _inventoryRead by 1. It used to have something which then set $inventory.Name to _inventoryRead, but it would throw errors for some reason. If _getInventoryContents doesn’t contain the item you’re adding, it simply pushes a new list for the item to the end of $inventory.Name.

Many thanks.

<<set $inventory to {}>>
<<set $inventory.James to []>>
<<run $inventory.James.push(['coins',5])>>
<<run $inventory.James.push(['busPass',1])>>

$inventory.James

<<widget "inventoryRead">>\
	<<print '<<set _inventoryRead to $inventory.'+_args[0]+'>>'>>\
<</widget>>\

<<widget "getInventoryContents">>\
	<<inventoryRead _args[0]>>\
	<<set _getInventoryContents to []>>\
	<<for _i to 0; _i < _inventoryRead.length; _i++>>\
		<<run _getInventoryContents.push(_inventoryRead[_i][0])>>\
	<</for>>\
<</widget>>\

<<widget "addItem">>\
	<<for _j to 0; _j < _args[2]; _j++>>\
	  <<getInventoryContents _args[0]>>\
	  <<if _getInventoryContents.contains(_args[1])>>\
		  <<for _i to 0; _i < _getInventoryContents.length; _i++>>\
			  <<if _getInventoryContents[_i] is _args[1]>>\
			  	<<set _inventoryRead[_i][1] to _inventoryRead[_i][1]+1>>\
				<<break>>\
			  <</if>>\
		  <</for>>\
	  <<else>>\
		  <<print '<<run $inventory.'+_args[0]+'.push(["'+_args[1]+'",1])>>'>>\
	  <</if>>\
	<</for>>\
	<<unset _i>>\
	<<unset _j>>\
	<<unset _getInventoryContents>>\
	<<unset _inventoryRead>>\
<</widget>>\

<<addItem 'James' 'busPass' 1>>
$inventory.James
<<addItem 'James' 'coins' 5>>
$inventory.James
<<addItem 'James' 'phone' 1>>
$inventory.James
2 Likes

Objects are reference types. In other words, when you simply assign an existing object to a new variable, you’re creating a new reference to the existing object rather than a copy of said object.

Arrays are objects, so are a reference type. Your <<inventoryRead>> widget assigns a reference to the associated array within the $inventory object to the _inventoryRead temporary variable. For example, <<inventoryRead 'James'>> makes _inventoryRead refer to the same array referenced by $inventory.James.

WARNING: SugarCube clones all story variable values upon passage navigation—i.e., a new turn. This will cause references to the same object to become copies of equivalent but distinct objects. What you’re doing here seems to happen within the same turn, so isn’t affected by this, but it’s something you should know about.


On to some code comments.

1. The following code is what’s known as the Stupid Print Trick™:

<<print '<<set _inventoryRead to $inventory.'+_args[0]+'>>'>>

As a general rule, never do this. There’s almost always a better way to do something.

In this case, instead of printing code to use the dot-notation of property access, simply use the bracket-notation. For example:

<<set _inventoryRead to $inventory[_args[0]]>>

 
2. Similarly, the following SPT™:

<<print '<<run $inventory.'+_args[0]+'.push(["'+_args[1]+'",1])>>'>>

Would be better as:

<<run $inventory[_args[0]].push([_args[1],1])>>

 
3. The <Array>.contains() method has been deprecated for years now:

<<if _getInventoryContents.contains(_args[1])>>

You should be using the <Array>.includes() method at this point:

<<if _getInventoryContents.includes(_args[1])>>
1 Like

Thank you, hopefully that shouldn’t pose too much of an issue as, like you said, all of my widgets run within a single passage here. In fact, everything that should use _inventoryRead actually unsets it afterwards (which I don’t think should delete the referenced object?), and the one that doesn’t I actually don’t intend to really call at all, just use it in others.

On your code comments: I had come across the SPT elsewhere, and have been using it throughout my project. The reason is mostly that tracking fluid variables (of which there are a lot, like health, equipped items and the like) for each character is setup under it’s own variable for each character. So, $James.equipped.head holds whatever James is wearing on his head, for instance. If that’s the ‘almost’ you’re referring to, then the only way I could really fix it (I think) is by having a ‘$characters’ variable instead that holds the same information, but I’m not very far into writing the characters or fleshing out those systems so it won’t be too hard to change, hopefully. I wasn’t keeping immutable stats as .js files when I wrote that, so it might actually wind up being better.

And I didn’t realise .contains() was deprecated, I’ll replace that as well.

Thank you!