Hey there,
Using SugarCube 2.1x.x
Twine 2.1.0
I tried recently to implement a Quest system that works with "templates" of different quest types (fetch quests, put given number of items into a specified container, visit a location and check game time while doing so, defeat an enemy type etc.), where I would have a MissionLog object for each mission these would be elements of a set. These sets would represent accomplished and still on going missions.
When a mission is complete I would move the mission from the on-going mission into the accomplished set.
The problem is that I can only call add() and delete() member functions inside JavaScript, since there's no equivalent in SugarCube 2. Now, of course in order to initialize the Set, I call the constructor inside SugarCube 2:
<<set accomplished_mission = new Set()>>
which yields "something" (I'm not sure what it is, probably a malformed array.)
I refer to this set in my JS code as follows:
State.variables.accomplished_mission
When I try to add() new elements to this "set" using JS code, for some reason, the system ignores the fact that I requested to work with a Set() data structure and creates multiple instances of the same object inside accomplished_mission. I checked for the "isArray()" property to find out what I'm working with but it yielded a big undefined for me. So, I'm not sure what is going on here. I found my self opening the console next to Twine 2 and trying everything that came to mind to find out more about the data structure that I got from SugarCube 2.
Now, of course it's needless to say that there is an easy workaround for this kind of problem in JS (simply check if the "set" already holds the item with the same Id and filter any new additions), but the fact that things don't work the way they supposed to, kind of bothers me. I'm afraid things like these could come back and bite me in the a$$ later down the road as I develop my game in SugarCube 2. I never thought I'd run into so many problems, which have delayed me significantly.
Can anyone explain to me what is going on here and why are data structures and some standard JS functions behave so strangely?
Comments
The accomplished_mission variable in your first example is missing a dollar-sign, if you cut-n-pasted that example from your story's code then try adding one.
Let's say:
But this time try and do this:
I played around with sets in my chrome browser and they worked just fine with objects like the ones stated above. I also don't use SugarCube to call add(), but instead I use macros where additional checks are being carried out. Let's see what happens here?
Also: I'm pretty sure it wasn't the dollar sign. This example was not copied from my own code.
Please read carefully below:
As I said. I tried out the code in my browser before (where it worked flawlessly, no duplicates appeared inside the created set, which is correct) applying it to my SugarCube story and it did not work (meaning duplicates were added, which strange and it should not be the case).
The objective of the experiment was to see if a duplicate of the same object will appear inside the set. In the browser it did not. In my twine game it did and I had to implement filtering in order to handle the data structure correctly.
eg. Are you doing something like the following? (using TWEE Notation)
Each time the story traverses between passages a copy of all known variables is made and this copy is what is made available to the next passage being rendered. This means that the $apple variable in the second passage does not reference the same object as the $apple variable in the first passage, it references a new object of the 'same' value.
Because the two object references are different they can both exist in a Set object at the same time.
If you meant that you think you cannot use the <Set>.add() and <Set>.delete() methods on an instance of Set within SugarCube's TwineScript, then you are mistaken. In SugarCube, TwineScript is essentially a layer of syntactical sugar atop standard JavaScript, so virtually anything you can do in raw/pure JavaScript, you can do in TwineScript, because it is JavaScript. SugarCube does not define a custom Set API, so the only one available normally is the standard JavaScript Set—or a standard polyfill, in case the browser is ancient enough to not have a native Set (EDIT: Unlike SugarCube v2, v1 does not include the polyfill, so the existence of Set cannot be guaranteed when using it).
If you meant something else, then you're going to need to make a better attempt at an explanation.
That example cannot work as written. You forgot to use the story variable sigil within the <<set>>, so accomplished_mission will be treated as a variable local to that <<set>> invocation and since no such variable exists an error will be returned.
Since the error would be kind of obvious and you didn't mention seeing one, I'm going to assume that was a transcription error. (ASIDE: Based upon past experience alone, you tend to make transcription errors routinely. Policing that more stringently would really be helpful. Seriously. For realsies.)
You're attempting this how, exactly? By choosing to not show the code you're having issues with, we cannot tell you what's going on.
That said, I'm going to go out on a limb and guess that you're attempting this across multiple turns/moments and have forgotten that the contents of story variables are cloned with each new turn/moment and referential relationships between objects are not kept—this was explained to you within the inventory thread you participated in and was the whole reason unique IDs were necessary there.
That caveat with story variables and objects—and everything in JavaScript which isn't a primitive type is an object—limits the usefulness of Set instances when attempting to store story variable based objects within them, since there's no way to override its native uniqueness test—which is, basically, strict equality.
The Set API contains no isArray() method—because instances of Set are not arrays—so of course Set.isArray() is undefined, if that's what you tried. While instances of Array and Set share some commonality, they are distinct data types.
Even if you used Array.isArray() on an instance of Set it would return false, since Set instances are not arrays.
Frankly, I'm curious as to how you even thought that was something that you could do in the first place.
As that example is written, the $accomplished_mission Set instance will contain a single entry which is a copy of the object reference contained within $apple. It will not contain two references as Set enforces uniqueness amongst its entries.
Beyond that, and as explained up post, what you're doing there is perilous as $apple is a story variable containing an object. If you attempt the above across multiple turns/moments, then the Set will contain references to distinct copies of the $apple object as the references will no longer be the same. For example: (using Twee notation here; assume $apple is defined as above)
Thank you! Exactly the info I needed!
I'm unclear on why you're storing the quest objects within story variables and then storing them again within a Set instance, which is within a story variable. Even if new turn/moment cloning didn't occur you'd be double dipping their storage.
It would probably be easier to store the quest objects within the story variable store, as it seems you were planning, and simply use a property to record their states—e.g. 'unstarted', 'active', 'finished', 'failed', etc.
For example:
Writing some functions to handle the state manipulation for you—to guard against fat fingering—would be easily enough done. For example: Example usage—based on the original example:
Thank you for the code examples. It is not the states that I'm storing in those extra objects. It is of course my fault not going into more details about the system I've gone for. So... here's what I did:
Now, I've created two sets, to utilize one to store the missions that are still active and another one to store missions that were already finished. So this way I can iterate through missions quickly and not to get the two kind (finished and active) mixed up and print their objective details separately (active and finished missions printed separately). I also chose sets because I wanted to spare my self from working on checks to filter duplicates and to re-use missions when they get reactivated (I simply move the ones that are repeatable and are triggered by the player back into the active mission set). Now, using sets as you mentioned made no sense since they would have not worked the way I thought they would.
Another thing that I thought about is composing mission/quest types by having a template for the basic mission types, but be able to modify the objective details easily. Say for example, you have a mission where you need to put a trouser into the cabinet, this can be grabbed and easily turned into a mission where you put two pistols under your pillow (you just copy the template of the "placement" mission type and change some parameters, assign it to a mission log, finally, add the mission log to the active missions). Now, this would work better if I could instantiate these objects and create them dynamically, which is something I have not figured out yet.
You could use Object.assign(), either by itself, within a factory function of sorts, or within a constructor function.
By itself example: I used the setup object to store the template object in the example because you do not want to be storing invariants within the story variable store—it bloats the history for no good purpose.
Factory function example: Usage: As with the first example, the template object is again stored within the setup object to keep the story variable store, and thus the history, free of bloat.
Constructor function example: Usage: The template object here is built into its constructor, so there's no external template to worry about.
Found more info here. I'll look into that and start experimenting with it.
I'm not sure if I'll have the dynamic mission creation implemented in this story, but I want to at least understand what is going on with copies and instances.
I'm not sure what you meant when you mentioned the setup object and history. I have not looked into these yet. I assume they are SugarCube 2 specific things. If you could post a link to the documentation or to an article about these, or explain them, I'd really appreciate that.
I did provide that very link in my post, when I first mentioned it.
SugarCube v2 documentation.
EDIT:
The setup object is simply a generic object which is guaranteed to be in scope and available to user code. It's briefly mentioned within Special Names.
The history is chiefly described within API: State.