Issue with Arrays Containing Variables

Please specify version and format if asking for help, or apply optional tags above:
Twine Version:2.3.13
Story Format: Sugarcube 2.34.1

So my problem is that I’m trying to store a variables with attached properties inside an array. I can only sort of get it to work though and I can’t figure out why. I can get it to print the items with their associated properties using a for loop and it works just fine. For some reason I can’t call the variable by name when I want to make a change to one of its property, I can only call it by using the index number.

::Story Init::
<<set $arr to []>>
<<set $lemon to {
name: "Lemon",
description: "A yellow fruit that's very sour.",
category: "consumable",
value: 2,
number: 1,
}>>
<<set $bottle to {
name: "Bottle of Water",
description: "Cold and refreshing!",
category: "consumable",
value: 2,
number: 1,
}>>
<<set $guitar to {
name: "Guitar",
description: "A string instrument.",
category: "tool",
value: 80,
number: 1,
}>>

The first print function returns Bottle of Water, but the second one doesn’t. What I want to do is to be able to find the index number of the variables so I can modify the properties but because it’ll be part of an inventory and shop system the items wont always be in predictable places in the array so I wont always be able to know what index number to use.

The second piece of code is where I’m trying to do that and I’ve done it before with strings instead of variables but now it’s just returning “<>: bad evaluation: Cannot read property ‘number’ of undefined” and I cannot for the life of me figure out why.

::Passage::
<<set $arr.push($lemon, $bottle, $guitar)>>

<<print $arr[1].name>>
<<print $arr[$bottle].name>>

<<set _i to $arr.indexOf($bottle)>>
<<set $arr[_i].number++>>
<<print $arr[_i].number>>

The thing that’s causing a problem is <<print $arr[$bottle].name>>

$bottle is an object, you can’t use that as array index (well, you sort of can, but in this case you’ll get back undefined). So then you can’t get the .name of something that doesn’t exist. Your <<set _i to $arr.indexOf($bottle)>> code seems to work fine.

@Boyish_Wonder (and @JoshGrams )

In your example you are both adding the object instances to an Array, and then using the <array>.indexOf() function to determine the index of one of the object instances contained in that array within the same Passage.
eg. pushing the object instance contained within the $bottle variable into the $arr Array, and then searching for the same object instance later on.

Currently the above is working because both the $bottle variable and the second element of the Array stored within the $arr variable are referencing the same object instance, however that won’t be the case once a Passage Transition occurs.

reason: Each time a Passage Transition occurs a ‘copy’ of the current state of all known Story Variables is made, the original state is added to the History system and the new ‘copy’ is made available to ‘next’ Passage being shown, and this copying action breaks Object Referential Integrity.

Which in your use-case means that the $bottle variable and the second element of the Array will no longer be reference the same instance of the ‘original’ object, they will both be reference their own unique ‘copy’ of that object. Which means that the $arr.indexOf($bottle) function call will not find the object instance you are looking for.

1 Like

That’s the thing, I don’t think <<set _i to $arr.indexOf($bottle)>> is working because it only returns -1, which, as far as I’m aware, means it’s not in the arry.

Oh butts. That makes sense. I will have to find another way around. Thanks!

If you add the following code to your JavaScript section:

Variable Utility Functions and the Array.indexOfObject() prototype
setup.VarUtils = { /* Variable Utility Functions */

	/* isArray: Returns if a value is an array. */
	isArray: function (Value) {
		return Array.isArray(Value);
	},

	/* isBoolean: Returns if a value is a boolean. */
	isBoolean: function (Value) {
		return typeof Value === "boolean";
	},

	/* isDate: Returns if value is a date object. */
	isDate: function (Value) {
		return setup.VarUtils.isObject(Value) && Value instanceof Date;
	},

	/* isFunction: Returns if a value is a function. */
	isFunction: function (Value) {
		return typeof Value === "function";
	},

	/* isGenericObject: Returns if a value is a generic object. */
	isGenericObject: function (Value) {
		return setup.VarUtils.isObject(Value) && Value.constructor === Object;
	},

	/* isInteger: Returns if a value is an integer. */
	isInteger: function (Value) {
		return Number.isInteger(Value);
	},

	/* isMap: Returns if a value is a map. */
	isMap: function (Value) {
		return setup.VarUtils.isObject(Value) && Value instanceof Map;
	},

	/* isNull: Returns if a value is null. */
	isNull: function (Value) {
		return Value === null;
	},

	/* isNumber: Returns if a value is a number. */
	isNumber: function (Value) {
		return (typeof Value === "number") && Number.isFinite(Value) && (!Number.isNaN(Value));
	},

	/* isObject: Returns if a value is an object (not including "null"). */
	isObject: function (Value) {
		return !!Value && typeof Value === "object";
	},

	/* isProperty: Returns if Prop is a property of the object Obj. */
	isProperty: function (Obj, Prop) {
		var result = false;
		if (setup.VarUtils.isObject(Obj)) {
			result = Obj ? hasOwnProperty.call(Obj, Prop) : false;
			if (!result) {				 /* if not pass... */
				try {
					if (Obj[Prop] === undefined) {
						result = false;  /* double-check fail */
					} else {
						result = true;   /* double-check pass */
					}
				} catch(error) {
					result = false;		 /* error fail */
				}
			}
		}
		return result;
	},

	/* Returns if a value is a regexp. */
	isRegExp: function (Value) {
		return setup.VarUtils.isObject(Value) && Value.constructor === RegExp;
	},

	/* isSet: Returns if a value is a set. */
	isSet: function (Value) {
		return setup.VarUtils.isObject(Value) && Value instanceof Set;
	},

	/* isString: Returns if a value is a string. */
	isString: function (Value) {
		return (typeof Value === "string") || (Value instanceof String);
	},

	/* isUndefined: Returns if a value is undefined. */
	isUndefined: function (Value) {
		return typeof Value === "undefined";
	},

	/* spread: Returns a Map, Set, or String converted to an array.
			If the second parameter is an Array, Map, Set, or String, then
			the two objects are spread and returned as a single array.
			If a function is passed as the second parameter, this calls the
			function with the spread array as parameters and returns that
			function's value. */
	spread: function (Value, Funct) {
		var arr = [];
		if (setup.VarUtils.isArray(Value)) {
			arr = clone(Value);
		} else if (setup.VarUtils.isMap(Value)) {
			Value.forEach(function(val, key, map) {
				arr.push([key, val]);
			});
		} else if (setup.VarUtils.isSet(Value)) {
			Value.forEach(function(val, key, set) {
				arr.push(val);
			});
		} else if (setup.VarUtils.isString(Value)) {
			arr = Value.split('');
		}
		if (setup.VarUtils.isFunction(Funct)) {
			return Funct.apply(null, arr);
		} else if (setup.VarUtils.isObject(Funct)) {
			arr = arr.concat(setup.VarUtils.spread(Funct));
		}
		return arr;
	},

	/* arraysAreEqual: Check two arrays to see if they're identical.
			IgnoreObjectPairs is for internal use to prevent infinite loops
			of objects. */
	arraysAreEqual: function (Array1, Array2, IgnoreObjectPairs) {
		if (setup.VarUtils.isArray(Array1) && setup.VarUtils.isArray(Array2)) {
			var i = 0;
			if (setup.VarUtils.isUndefined(IgnoreObjectPairs)) {
				IgnoreObjectPairs = [];
			}
			if (IgnoreObjectPairs.length > 0) {
				for (i = 0; i < IgnoreObjectPairs.length; i++) {
					if (((IgnoreObjectPairs[i][0] === Array1) && (IgnoreObjectPairs[i][1] === Array2)) ||
						((IgnoreObjectPairs[i][0] === Array2) && (IgnoreObjectPairs[i][1] === Array1))) {
							return true;  /* Ignores object pairs that have already been checked to prevent infinite loops. */
					}
				}
			}
			IgnoreObjectPairs.push([Array1, Array2]);
			if (Array1.length !== Array2.length) {
				return false;  /* Arrays are not the same length. */
			}
			if (Array1.length > 0) {
				for (i = 0; i < Array1.length; i++) {
					if (!setup.VarUtils.valuesAreEqual(Array1[i], Array2[i], IgnoreObjectPairs)) {  /* OOO function call. */
						return false;  /* Values or types do not match. */
					}
				}
			}
			return true;  /* All values match. */
		}
		return false;  /* Both are not arrays. */
	},

	/* mapsAreEqual: Returns if two maps contain the same values in the
			same order.
			IgnoreObjectPairs is for internal use to prevent infinite loops
			of objects. */
	mapsAreEqual: function (Map1, Map2, IgnoreObjectPairs) {
		if (setup.VarUtils.isMap(Map1) && setup.VarUtils.isMap(Map2)) {
			if (Map1.size === Map2.size) {
				if (setup.VarUtils.isUndefined(IgnoreObjectPairs)) {
					IgnoreObjectPairs = [];
				}
				if (IgnoreObjectPairs.length > 0) {
					for (var i = 0; i < IgnoreObjectPairs.length; i++) {
						if (((IgnoreObjectPairs[i][0] === Map1) && (IgnoreObjectPairs[i][1] === Map2)) ||
							((IgnoreObjectPairs[i][0] === Map2) && (IgnoreObjectPairs[i][1] === Map1))) {
								return true;  /* Ignores object pairs that have already been checked to prevent infinite loops. */
						}
					}
				}
				IgnoreObjectPairs.push([Map1, Map2]);
				var a = setup.VarUtils.spread(Map1), b = setup.VarUtils.spread(Map2);
				return setup.VarUtils.arraysAreEqual(a, b, IgnoreObjectPairs);  /* Compares maps. */
			}
		}
		return false;  /* Both are either not maps or are maps of different sizes. */
	},

	/* objectsAreEqual: Check two objects to see if they're identical.
			IgnoreObjectPairs is for internal use to prevent infinite loops
			of objects. */
	objectsAreEqual: function (Obj1, Obj2, IgnoreObjectPairs) {
		if (setup.VarUtils.isObject(Obj1) && setup.VarUtils.isObject(Obj2)) {
			var i = 0;
			if (setup.VarUtils.isUndefined(IgnoreObjectPairs)) {
				IgnoreObjectPairs = [];
			}
			if (IgnoreObjectPairs.length > 0) {
				for (i = 0; i < IgnoreObjectPairs.length; i++) {
					if (((IgnoreObjectPairs[i][0] === Obj1) && (IgnoreObjectPairs[i][1] === Obj2)) ||
						((IgnoreObjectPairs[i][0] === Obj2) && (IgnoreObjectPairs[i][1] === Obj1))) {
							return true;  /* Ignores object pairs that have already been checked to prevent infinite loops. */
					}
				}
			}
			IgnoreObjectPairs.push([Obj1, Obj2]);
			if (setup.VarUtils.isGenericObject(Obj1) && setup.VarUtils.isGenericObject(Obj2)) {
				var Keys1 = Object.keys(Obj1).sort(), Keys2 = Object.keys(Obj2).sort();
				if (!setup.VarUtils.arraysAreEqual(Keys1, Keys2)) {
					return false;  /* Objects have a different number of keys or have different keys. */
				}
				if (Keys1.length > 0) {
					var Key;
					for (i = 0; i < Keys1.length; i++) {
						Key = Keys1[i];
						if (!setup.VarUtils.valuesAreEqual(Obj1[Key], Obj2[Key], IgnoreObjectPairs)) {  /* OOO function call. */
							return false;  /* Values do not match. */
						}
					}
				}
				return true;  /* All values match. */
			} else {
				return setup.VarUtils.valuesAreEqual(Obj1, Obj2, IgnoreObjectPairs);  /* Return whether objects match. OOO function call. */
			}
		}
		return false;  /* Both are not objects. */
	},

	/* setsAreEqual: Returns if two sets contain the same values in the
			same order.
			IgnoreObjectPairs is for internal use to prevent infinite loops
			of objects. */
	setsAreEqual: function (Set1, Set2, IgnoreObjectPairs) {
		if (setup.VarUtils.isSet(Set1) && setup.VarUtils.isSet(Set2)) {
			if (Set1.size === Set2.size) {
				if (setup.VarUtils.isUndefined(IgnoreObjectPairs)) {
					IgnoreObjectPairs = [];
				}
				if (IgnoreObjectPairs.length > 0) {
					for (var i = 0; i < IgnoreObjectPairs.length; i++) {
						if (((IgnoreObjectPairs[i][0] === Set1) && (IgnoreObjectPairs[i][1] === Set2)) ||
							((IgnoreObjectPairs[i][0] === Set2) && (IgnoreObjectPairs[i][1] === Set1))) {
								return true;  /* Ignores object pairs that have already been checked to prevent infinite loops. */
						}
					}
				}
				IgnoreObjectPairs.push([Set1, Set2]);
				var a = setup.VarUtils.spread(Set1), b = setup.VarUtils.spread(Set2);
				return setup.VarUtils.arraysAreEqual(a, b, IgnoreObjectPairs);  /* Compares sets. */
			}
		}
		return false;  /* Both are either not sets or are sets of different sizes. */
	},

	/* setsMatch: Returns if two sets contain matches for all values in
			any order. */
	setsMatch: function (Set1, Set2) {
		if (setup.VarUtils.isSet(Set1) && setup.VarUtils.isSet(Set2)) {
			if (Set1.size === Set2.size) {
				if (Set1.size > 0) {  /* Compare sets. */
					var setIterator = Set1.values();
					var result = setIterator.next();
					while (!result.done) {
						if (!Set2.has(result.value)) return false;  /* Sets do not match. */
						result = setIterator.next();
					}
				}
				return true;  /* Sets match. */
			}
		}
	},

	/* valuesAreEqual: Check two variables to see if they're identical.
			This function does not support comparing symbols, functions, or
			custom types.
			IgnoreObjectPairs is for internal use to prevent infinite loops
			of objects. */
	valuesAreEqual: function (Var1, Var2, IgnoreObjectPairs) {
		if (typeof Var1 === typeof Var2) {
			switch (typeof Var1) {
				/* String */
				case "string":
				/* Number */
				case "number":  // eslint-disable-line no-fallthrough
				/* Boolean */
				case "boolean":  // eslint-disable-line no-fallthrough
					return Var1 === Var2;  /* Returns whether variables are equal or not. */
				/* Undefined */
				case "undefined":
					return true;  /* Variables are both undefined. */
				/* Object */
				case "object":
					/* Array Object */
					if (setup.VarUtils.isArray(Var1) && setup.VarUtils.isArray(Var2)) {
						return setup.VarUtils.arraysAreEqual(Var1, Var2, IgnoreObjectPairs);  /* Return whether arrays are equal. */
					/* Generic Object */
					} else if (setup.VarUtils.isGenericObject(Var1) && setup.VarUtils.isGenericObject(Var2)) {
						return setup.VarUtils.objectsAreEqual(Var1, Var2, IgnoreObjectPairs);  /* Return whether generic objects are equal. */
					/* Date Object */
					} else if (setup.VarUtils.isDate(Var1) && setup.VarUtils.isDate(Var2)) {
						return (Var1 - Var2) == 0;  /* Returns whether dates are equal. */
					/* Map Object */
					} else if (setup.VarUtils.isMap(Var1) && setup.VarUtils.isMap(Var2)) {
						return setup.VarUtils.mapsAreEqual(Var1, Var2, IgnoreObjectPairs);  /* Return whether maps are equal. */
					/* Set Object */
					} else if (setup.VarUtils.isSet(Var1) && setup.VarUtils.isSet(Var2)) {
						return setup.VarUtils.setsAreEqual(Var1, Var2, IgnoreObjectPairs);  /* Return whether sets are equal. */
					/* Null Object */
					} else if ((Var1 === null) && (Var2 === null)) {
						return true;  /* Objects are both null. */
					}
					return false;  /* Objects either don't match or are of an unsupported type. */
				default:
					return false;  /* Unsupported type. */
			}
		} else {
			return false;  /* Variables are not of the same type. */
		}
	},

	/* indexOfObject: Returns the index of the first matching Obj object
			in the Arr array or -1 if not found.
			Starts at index Idx (or 0 if Idx not included).
			If Idx is negative, then it starts at that offset from the end of
			the array (to a minimum of 0).
			Returns undefined if Arr is not an array. */
	indexOfObject: function (Arr, Obj, Idx) {
		if (!setup.VarUtils.isArray(Arr)) {
			return undefined;  // Not an array.
		}
		if (Arr.length === 0) {
			// Empty array.
			return -1;  // No match.
		}
		var i;
		if (setup.VarUtils.isUndefined(Idx) || !setup.VarUtils.isInteger(Idx)) {
			Idx = 0;
		}
		if (Idx < 0) {
			Idx = Arr.length + Idx;
			if (Idx < 0) {
				Idx = 0;
			}
		}
		for (i = Idx; i < Arr.length; i++) {
			if (setup.VarUtils.valuesAreEqual(Obj, Arr[i])) {
				return i;  // Found a match.
			}
		}
		return -1;  // No match.
	}
};

Array.prototype.indexOfObject = function(obj, fromIndex) {
	return setup.VarUtils.indexOfObject(this, obj, fromIndex);
};

then, instead of .indexOf(), you can use the .indexOfObject() method on your arrays to find objects with matching values, even if they have different references. Other than that, the .indexOfObject() method should work the same as the normal .indexOf() method.

Additionally, you can use the various setup.VarUtils methods if you need to check the types of your variables.

Enjoy! :slight_smile:

Thank you! That’s so helpful!