[SugarCube 2.29] Advice on Best Practices - Calculate or Store?

Hi again. Just looking for a little advice on optimization of a specific system:

I’ve written an inventory system into my current project. Each container is an object that stores information on how many slots it has available, how many are filled, its weight, etc… It also has an inventory, which is a Map object where the keys are references to the item or its template (depending on whether or not it stacks).

As the keys obviously need to be unique, this storage method doesn’t take into account the items stack size, so to actually work with an inventory - as they have limited slots - I have a widget called <<slottedinventory>> that accepts an inventory Map and converts it to an Array, which it stores in a temporary variable called _slottedInv.

Now, any time I really want to work with an inventory, (i.e. checking if it’s full or can hold a certain amount of an item), I have to call this widget. This is going to happen quite often in my game, obviously, so my question is this:

Should I simply store both the Map and Array versions of a container’s inventory and only call this widget to update the Array version, or should I just keep calling this widget every time I need the Array version? I have no need for backward navigation in my story, so my config.saves.maxStates is set to 1 to keep the save file small, but as its an in-depth RPG, there will be a lot of items and containers holding them. I’m just wondering whether it will eventually be better for performance if these Arrays are stored or if I should just keep calling this widget?

Sorry for the long explanation, but I wanted to be clear. Let me know if there are other factors I should provide. Thanks!

[quote=“Tilea, post:1, topic:43401”]so my config.saves.maxStates is set to 1 to keep the save file small
I will assume you actually meant the Config.history.maxStates setting.

Ideally you should only store values that change (dynamic data) during a play-through within the story variable system. Any values that don’t change from the value initially assigned during startup (static data) should be stored elsewhere, like on the special setup object.

The two main reasons for doing the above are:
1: Reducing the size of the History system, thus the size of the Save slots.

This is important because both are stored with the web-browser’s Local Storage cache and that area has a default maximum size of between 2MB and 10MB depending on the web-browser being used and the operating system/machine/device it’s being used on.

Your changing of the maxStates setting to 1 makes the above limitation less of an issue.

2: Reducing the cost of cloning the current state of all known story variables during the Passage Transition process.

Each time a Passage Transition occurs the current state of the story variable system is cloned, the original copy is added to the History system as part of a new Moment, and the cloned copy is made available to the Passage that is about to be shown. This cloning process breaks the integrity of object references, which can cause issues if you have two or more variables (object properties/array elements) referencing the same object.

Changing the maxStates setting to 1 only controls how many States/Moments are stored within the History system, it doesn’t stop the cloning process.
eg. The following TWEE Notation example demonstrates the above.

:: Story Javascript [script]
Config.history.maxStates = 1;

:: Start
/* Initialise two references to the same object. */
<<set $first to {name: "Aaaa"}>>\
<<set $second to $first>>\
/* Display initial values, both the same! */
name (first): $first.name 
name (second): $second.name

/* Alter name for one of the references. */
<<set $first.name to "Bbbb">>\
/* Display values again, both have new value! */
name (first): $first.name 
name (second): $second.name

[[Next|After Cloning]]

:: After Cloning
/* Display values, both appear the same! */
name (first): $first.name
name (second): $second.name

/* Alter name for one of the references again. */
<<set $first.name to "Cccc">>\
/* Display values again, now different! */
name (first): $first.name 
name (second): $second.name

Personally I would store the (static) Item Definitions on the setup object, which you can do either within your project’s Story JavaScript area.

/* Initialise the items property on the setup object. */
setup.items = {
	apple: {id: 'apple', name: 'Apple'},
	banana: {id: 'banana', name: 'Banana'}
/* Add more items. */
setup.items.pear = {id: 'pear', name: 'Pear'};

…or within your project’s StoryInit special passage…

/* Initialise the items property on the setup object. */
<<set setup.items to {
	apple: {id: 'apple', name: 'Apple'},
	banana: {id: 'banana', name: 'Banana'}
/* Add more items. */
<<set setup.items.pear to {id: 'pear', name: 'Pear'}>>

(obviously I don’t know what properties you have associated with your items, so I will only include an ID and Name in my example.)

You can use a fixed length Array based story variable to track which item (if any) is currently associated with a specific slot.

/* Initialise an Array with 5 slots at startup. */
<<set $slots to Array.from({length: 5})>>

/* Associate an Item with a Slot using the item definition's key. */
<<set $slots[3] to 'banana'>>

/* Display the current contents of the slots
slot count: $slots.length
slot contents:
<<for _i, _slot range $slots>>
	_i: <<= _slot ? setup.items[_slot].name : ''>>\

You can use a Generic Object / Map based story variable to track the ID and quantity of any item being carried.

/* Initialise the variable at startup. */
<<set $carring to {}>>

/* Add item to inventory using the Item Definition's key. */
<<set $carring['banana'] to 1>>
/* Increase quantity being carried. */
<<set $carring['banana'] += 5>>
/* Remove item from inventory. */
<<run delete $carring['banana']>>

Yes, I did mean the config.history.maxStates setting, and all of my defined items are stored in the setup object as defined in my story JavaScript.

I will look into the changes you mentioned, but this system won’t work for me as-is. Items such as weapons and armor will have durability and other properties that can change, so storing them as merely an ID won’t really work, at least not in that way.

The fact that you are tracking things like “durability” implies that you need to uniquely identify each instance of a specific item, so that you can track the state of each instance separately.
eg. if the player is carrying 2 bananas, and one of them is partly eaten, then each banana instance needs to be uniquely identified.

In database theory this is usually done by associating both an ID and a Type with each instance being tracked, and the Type would be used to lookup the information that is common to all instances of a specific type.

Item Definitions:
id: "banana", name: "Banana", colour: "Yellow"

Item Instances:
id: "banana-1", type: "banana", durability: 100
id: "banana-2", type: "banana", durability: 50

(obviously the naming of the property representing the “Type” association depends on the individual designing the scheme.)

How you implement the above structure (and related functionality) within your project greatly depends on exactly what you want to do (display, manipulate, etc…) with the data being stored, in some use-cases it would make more sense to replace the Generic Object in my $carrying example with an Array.

Yes, I am using separate instances for items that have durability or properties that can change (by creating clones of their base in the setup object). I was using a reference to the setup object for items that are stackable and thus will not have altered properties, but that seems to be one aspect I will have to change if the objects are cloned anyway.

However, this doesn’t really answer my original question. The system I have is working so far, aside from - for example - needing to change the reference to setup.Ammo.Arrow in the Map key to an id. However, I still need to create an array to represent the inventories slots and how many items/stacks are stored. I can’t really store it just as an array, because then I would need to search through the entire array every single time I needed to know if a certain item was present, which would be even more processing than I already have.

I was just hoping to get some advice on whether running functions more often or storing a bit more data to avoid this additional processing might be a better practice. This could apply to more than just my inventory system in the future.

Okay, so to see what would happen, I did a small test on what you were saying about object references:

In the Story JavaScript, I have this:

setup.banana = {
	"name" : "banana",
	"color" : "yellow"

In my start passage, I create a Map object with a reference to the banana object as its key and an amount:

<<set $bunch = new Map([
	[setup.banana, 2]
[[Next|Passage 2]]

And then I output the value in “Passage 2” like this:

<<print $bunch.get(setup.banana)>>

The result of the output is “2”. So, in this instance, it still recognizes the the Map key as a reference to setup.banana despite navigating to a new passage/moment. if the key had been cloned, this reference would no longer work as it would be considered a different object, or am I mistaken?

EDIT: Alright, I tested the integrity over a save and load and this seems to be where it breaks, which I suppose makes sense as it is converting everything to a JSON string then parsing it again.

Additionally. The player reloading the page, which reloads the active/current play session, would also break your code, since sessions are stored very similarly to saves.

I’ve solved this by storing stackable items as string/ids in the Map and unique (non-stackable) items as objects, as it doesn’t matter whether or not they’ve been cloned or rebuilt. I’ve also created an is(item2) method that I’ve attached to all of the base item types in my JavaScript, which will compare the primitive properties of two items if I - for some reason - need to check them that way.

So, what this leaves me with is the need to convert the Map into a temporary array when I want to display the inventory or update how many slots of it are filled. If I do this in JavaScript rather than trying to use widgets/Twine script for as much as possible, it could probably be done efficiently enough to keep the game running smoothly.

At least, that’s my current assumption. This calculation will still need to be run frequently, but it will ensure the save file is a little smaller.

Thanks for the pointers. They didn’t directly answer my question, but led me to a redesign that led me to an answer of my own and prevented me from having to do this redesign later on when it would have taken a lot more work.