Twine SugarCube state, objects and the back button

If you are requesting technical assistance with Twine, please specify:
Twine Version: Tweego 2.1.1
Story Format: SugarCube 2.34.1

Hello everyone. Thank you for taking the time to read this, and help out if you can!

I’ve decided to try my hand at writing in Twine and come from a background of some experience with JavaScript and lots of PHP/HTML/CSS. I understand the basic flow of the stories and passages and all seems to be working. However, I’ve run into a “gotcha!” with the back button and I’d like to sort it out before I go any further.

The basic problem is that objects within the Player object seem to be keeping the state they had at the end of the passage instead of at the beginning. So if a passage adds one to a stat and increases it from 9 to 10, then if the user goes back to the passage (using the SugarCube back button) the stat is seems to start at 10 then is boosted to 11.

If I understand the state system correctly, the state is saved at the beginning of each passage (before the “:passagestart” event). So, when you go “back” everything should be reset to how it was at the beginning of the passage and then the passage happens. That all makes sense - but let me know if I’ve got any of that wrong!

OK. To the problem in hand.

I have a Stat class which keeps track of values, progress to the next level, level caps, buffs and all manner of stat-ly things. It looks a bit like this:

    MyGame.Stat = function (initObject) {
        this.progress   = 0;        // 100 progress to get to the next level
        // and more properties here, they're all strings, ints or bools

        $.extend(this, initObject);
    }
    MyGame.Stat.prototype = {
        toJSON:         function() {
            var ownData = {
                progress:   this.progress,
                // and so on with other properties
            };
            return JSON.reviveWrapper('new MyGame.Stat($ReviveData$)', ownData);
        },
        clone: function() {
            return new MyGame.Stat(this);
        },
        // then lots more functions
    }

I then have a Player class which does Player-ly things and also contains some stats. It looks a bit like this:

    MyGame.Player = function (initObject) {
        this.testSkill = new MyGame.Stat({});
        // and more properties here, they're all strings, ints, bools or Maps

        $.extend(true, this, initObject);
    }
    MyGame.Player.prototype = {
        toJSON:         function() {
            var ownData = {
                'testSkill':   this.testSkill, // I have also tried "this.testSkill.toJSON()" here
                // and so on with other properties
            };

            return JSON.reviveWrapper('new MyGame.Player($ReviveData$)', ownData);
        },
        clone: function() {
            return new MyGame.Player(this);
        },
        // then lots more functions
    }

For testing purposes I also created a standalone stat in _config.twee to try and isolate the problem and see if it was a problem with the Stat itself or with it being part of the Player. In _config.twee:

<<set $player to new MyGame.Player({})>>
<<set $rawSkill to new MyGame.Stat({})>>

This is all sweetness and light. It all works fine and I can progress the stat when I want to. The Player object seems happy, state seems to work fine when the games are saved and loaded. Until the dreaded BACK button rears its ugly head.

Passage 1:
$player.testSkill.progress is 0
$rawSkill.progress is 0

Passage 2:
On entry the values are:
$player.testSkill.progress is 0
$rawSkill.progress is 0
The passage then adds 1 to the progress of both Stats. At the end the values are:
$player.testSkill.progress is 1
$rawSkill.progress is 1

Passage 3:
On entry the values are:
$player.testSkill.progress is 1
$rawSkill.progress is 1

Press the back button:
On entry the values are:
$player.testSkill.progress is 1 ← this should be 0
$rawSkill.progress is 0
The passage then adds 1 to the progress of both Stats. At the end the values are:
$player.testSkill.progress is 2 ← this should be 1
$rawSkill.progress is 1

So… any idea where I’m going wrong?

How do I ensure that the Player object properly keeps its state if the user goes “back” in the story? Should I be doing something different in Player.toJSON()?

I’ve spent north of 10 hours trying to debug this so far… it’s killing me. I hope somebody can help! At this point I’d even be happy with some pointers of where to look or what to try!

1 Like

I finally cracked this… I’m posting here in case it helps somebody else!

   MyGame.Player.prototype = {
        clone: function() {
            let clonedObj = new MyGame.Player(this);

            // deep clone objects in the stats Map:
            clonedObj.stats = new Map();
            this.stats.forEach(function (value, key) {
                clonedObj.stats.set(key, value.clone());
            });

            return clonedObj;
        },
        // then lots more functions
    }

What I’m doing here is looping through the Map and ensuring that the clone of Player gets a cloned version of every Mapped Stat object. If you don’t do this then it seems that the cloned Player is linked to the original Player’s stats and then weird things happen when those stats change.

Well, weird to me. Probably obvious to a skilled JavaScripter!

I hope this helps somebody!

(And if anybody looks at this and thinks “why are you doing it that way? It would be much simpler/more elegant to do it this way instead” - then don’t be shy! Let me know how to improve and I’ll be forever grateful!)

2 Likes

If I understand correctly you should be using the Engine.backward() function instead of the back button.

<<link "Back">><<run Engine.backward()>><</link>>

If you have some variables you want to keep and others that you don’t, I’m not sure how that would work. There is the memorize() function and related things.

This is when the reader clicks the native sugarcube back button (top left of the sidebar in the default sugarcube template)

As noted in the documentation the .clone() method should result in a clone of the appropriate values. For primitive values you generally don’t need to do anything special. For object/reference values you generally need to create new instances, otherwise you’re simply passing a reference to the original value.

Making custom non-generic object types fully compatible requires that two methods be added to their prototype, .clone() and .toJSON() , to support cloning—i.e., deep copying—instances of the type.

  • The .clone() method needs to return a clone of the instance.
  • The .toJSON() method needs to return a code string that when evaluated will return a clone of the instance.

In both cases, since the end goal is roughly the same, this means creating a new instance of the base object type and populating it with clones of the original instance’s data. There is no one size fits all example for either of these methods because an instance’s properties, and the data contained therein, are what determine what you need to do.