HTML [Twine 2.0/SugarCube/Tweego] Updating Default Player Stats on Load

Jan 22, 2020
59
157
I'm a self taught hobbyist programmer, for what it's worth. My latest project is a JavaScript based choose your own adventure game using Twine (Tweego) as the framework. If you're not familiar with Twine, I don't believe it will hinder your understanding. I'll outline the issue in relation to they framework.

Twine's recommended variable storage objects are as follows: the setup object for variables that will load every time the game starts (refreshes), and the state object for variables that will be saved to file when the user saves their game. Since my game is planned to be early access, I need to be clever about how I do updates. If I add a new feature to the player, I don't want to force them to start the game over. Rather, handle it in a way that "auto" updates. The obvious way, to me, was leveraging Twine's setup object for default values. For instance, if the player's default hacking skill is 15 and during development I reason that value too low, I can increase it with minimal fuss. I accomplished this by doing the following:
JavaScript:
setup.PLAYER_BASE = {
    // primary stats
    charisma: 0,
    manipulation: 0,
    wits: 0,

    // skills
    hacking: 15,
    lockpicking: 0,
    negotiation: 0,
    sneaking: 0,

    // perks
    appearance: 0,
    goodness: 0,
    leadership: 0,
    wickedness: 0
}

setup.PLAYER_INIT = {
    // primary stats
    charisma: 0,
    manipulation: 0,
    wits: 0,

    // skills
    hacking: 0,
    lockpicking: 0,
    negotiation: 0,
    sneaking: 0,

    // perks
    appearance: 0,
    goodness: 0,
    leadership: 0,
    wickedness: 0,

    // Inventory
    credits: 0,
    items: [],
    laptop: {}
}
In the above code I created a constant variables for the PLAYER_BASE stats, so the program knows what the player minimum values are. Then I created a constant variable for PLAYER_INIT, which is only used when the game starts. When a new game begins, the following code is run:
JavaScript:
:: InitStory
<<set $player = setup.PLAYER_INIT>>
Before I explain the Twine syntax, I'll explain why I store $player with 0 values for all stats. As the game progresses, the player will gain points to apply to these stats, so this object tracks what the player has changed, not what the values originated as, which is what setup.PLAYER_BASE defines.
In Twine, the $ signals that a value will be stored to the state object (things that will be stored when the game saves). The :: tells Twine that what follows is a passage. I mention this, because there is another character I'll use in the next bit of code that represents a variable local to the passage, which is _. Next I'll demonstrate how this all ties together.
JavaScript:
:: chapterOne
<<set _playerObj = new setup.PlayerObj($player, setup.PLAYER_BASE)>>
Now, here is that `PlayerObj` code to make sense of it all.
JavaScript:
window.setup = window.setup || {};

setup.PlayerObj = function(playerData, playerBase) {
    this.firstName = playerData.firstName;
    this.lastName = playerData.lastName;

    // primary stats
    this.charisma = playerData.charisma + playerData.charisma;
    this.manipulation = playerData.manipulation + playerData.manipulation;
    this.wits = playerData.wits + playerData.wits;

    // skills
    this.hacking = playerData.hacking + playerBase.hacking;
    this.lockpicking = playerData.lockpicking + playerBase.lockpicking;
    this.negotiation = playerData.negotiation + playerBase.negotiation;
    this.sneaking = playerData.sneaking + playerBase.sneaking;

    // perks
    this.appearance = playerData.appearance + playerBase.appearance;
    this.goodness = playerData.goodness + playerBase.appearance;
    this.leadership = playerData.leadership + playerBase.leadership;
    this.wickedness = playerData.wickedness + playerBase.wickedness;

    // items
    this.items = playerData.items;

    // equipment
    this.laptop = playerData.laptop;

    // methods
    this.fullName = function () {
        return this.firstName + " " + this.lastName;
    }
}
Okay, so now every time a game is loaded from a save file, first everything in `setup` is defined, then the passage the player left off on is loaded. So, if I decided the player's hacking skill should be 30, it will now be 15 points higher than it was when the player saved. This to me feels incredibly sloppy and not DRY whatsoever. First, the `setup.PLAYER_INIT` feels redundant, because everything except the hacking skill value has no relevance. I'm sure this is a common task among programmers, which is why I'm seeking wisdom to expand my knowledge. Thank you in advance for the reviews.
 

outsider artisan

Developer of Succubus Stories.
Game Developer
Jun 14, 2020
348
587
This is incredibly over-engineered. There's also potentially a lot of practical issues with how you've set this all up, particularly with the prototype you've got on setup, which contains just outright errors, and does not work how I think you expect. If the player values are stateful, they should just be part of the state. Period.

Handling updates is as easy as checking a stateful variable for the version number, comparing it to a nonstateful version number, and using the `Config.saves.onLoad` handler to adjust game data as needed, something I've now done dozens of times in an incredibly complex game with no issues; my players have never had to start over after an update.
 
  • Like
Reactions: Heroine Hunter X
Jan 22, 2020
59
157
This is incredibly over-engineered. There's also potentially a lot of practical issues with how you've set this all up, particularly with the prototype you've got on setup, which contains just outright errors, and does not work how I think you expect. If the player values are stateful, they should just be part of the state. Period.

Handling updates is as easy as checking a stateful variable for the version number, comparing it to a nonstateful version number, and using the `Config.saves.onLoad` handler to adjust game data as needed, something I've now done dozens of times in an incredibly complex game with no issues; my players have never had to start over after an update.
The `Config.saves.onLoad` function was how I started my updates, but then I over thought it I suppose. When you do your updates, do you loop through the history array and apply them all states retroactively?

With regard to your statement on the prototype PlayerObj, I agree it would become impractical to use over time. I'm curious which errors you saw. To me, it does look over-engineered, but it appears to work as I expected. It would be helpful to my understanding if you're up for explaining.
 

outsider artisan

Developer of Succubus Stories.
Game Developer
Jun 14, 2020
348
587
The `Config.saves.onLoad` function was how I started my updates, but then I over thought it I suppose. When you do your updates, do you loop through the history array and apply them all states retroactively?

With regard to your statement on the prototype PlayerObj, I agree it would become impractical to use over time. I'm curious which errors you saw. To me, it does look over-engineered, but it appears to work as I expected. It would be helpful to my understanding if you're up for explaining.
I have the history off, but it is simple enough to loop through the states and add the required data.

One issue with your custom setup object is that some instances are just adding playerData to playerData instead of playerBase. Some values are just passed directly through which seems bizarre and inefficient. On top of that, when you do just pass, for example, an array through, you're passing a reference, you should probably be cloning it or not passing it through at all.

You also define a method in the constructor, which is not efficient, it's better to define the method on the prototype. On top of all that, with only one method, using a constructor in the first place seems totally unnecessary, I think this whole thing could be vastly simplified into a simple function that just does a simple `Object.assign()` (you may need to assign sub objects separately, though), so you take the base and map the data over it.

It seems like you're also doing all this on every passage instead of on game loads, and that is extremely inefficient. Doing potentially complex data checks and recreating an object in every passage just to patch saved data is kinda nuts when you could just do it once, when a save is loaded, and never again.

I call this over-engineered because it's solving the problem, but not even just in an inefficient way. It's like burning the whole house down to kill a spider in my opinion, like way over the top.

I think the code and the idea actually betray a certain amount of cleverness on your part, just wrongly applied in this case.

Edit: About passing data through being bizarre, I mean that if the inventory is an array and never going to need updated, it shouldn't be here in the first place taking up processing power just to get passed through with no changes. Take it off the player object if this is how you want to do things.
 
Last edited:
  • Like
Reactions: Heroine Hunter X
Jan 22, 2020
59
157
I have the history off, but it is simple enough to loop through the states and add the required data.

One issue with your custom setup object is that some instances are just adding playerData to playerData instead of playerBase. Some values are just passed directly through which seems bizarre and inefficient. On top of that, when you do just pass, for example, an array through, you're passing a reference, you should probably be cloning it or not passing it through at all.
Ah, ha! I see that now. That was my relying too much on auto complete without verifying. Also, probably a dumb idea to have two variables so similar in naming.

You also define a method in the constructor, which is not efficient, it's better to define the method on the prototype. On top of all that, with only one method, using a constructor in the first place seems totally unnecessary, I think this whole thing could be vastly simplified into a simple function that just does a simple `Object.assign()` (you may need to assign sub objects separately, though), so you take the base and map the data over it.
I've never used `Object.assign()` before, but read about it. Interesting and good to know! I need to brush up on prototyping methods as well. I keep moving between languages and some things get a little fuzzy.

It seems like you're also doing all this on every passage instead of on game loads, and that is extremely inefficient. Doing potentially complex data checks and recreating an object in every passage just to patch saved data is kinda nuts when you could just do it once, when a save is loaded, and never again.
It was the intent with the redesign, as originally I had it updating through the Save.onLoad() function. I guess I'm exploring options because I don't really know what I'm doing, just taking things I understand (or think I understand) and putting them together. I know what I want ultimately, just unsure how to get there. So, I explore. A big reason I reached out for advice is to prevent myself from spending countless hours on something then giving up when I can't figure out what way is up later on. lol

I call this over-engineered because it's solving the problem, but not even just in an inefficient way. It's like burning the whole house down to kill a spider in my opinion, like way over the top.

I think the code and the idea actually betray a certain amount of cleverness on your part, just wrongly applied in this case.
This is kind of my MO, lmao. Overthinking shit to the point of burning down the house. xD

Edit: About passing data through being bizarre, I mean that if the inventory is an array and never going to need updated, it shouldn't be here in the first place taking up processing power just to get passed through with no changes. Take it off the player object if this is how you want to do things.
I get where you're coming from. I have more methods planned, just wanted to make sure I was on a good path before making too much. I tossed in the `fullName()` method because it was simple and showed how I was going about my prototypes. You're wisdom is clearly well past my own on the subject. If you've got suggesting on material to read or study, I'd love to hear about them. Being creative is my passion, I just grew tired of all the books out there that rehash the same old boring basic stuff. I feel on the brink of learning something that will expand my perspective, just haven't found that yet.
 

outsider artisan

Developer of Succubus Stories.
Game Developer
Jun 14, 2020
348
587
If you've got suggesting on material to read or study, I'd love to hear about them. Being creative is my passion, I just grew tired of all the books out there that rehash the same old boring basic stuff. I feel on the brink of learning something that will expand my perspective, just haven't found that yet.
I learned about how the engine works by essentially rebuilding it. My game is made in a heavily customized version of the engine. However, I actually think a lot of what I wanted to achieve was possible without doing all of that, but I did learn quite a lot about the design of the SugarCube engine during that process.

For general JavaScript, MDN is the best place to learn stuff. The SugarCube documentation is also rather comprehensive, if a bit basic occasionally. I can provide you with some examples from my game of the helper functions and general approach I use for patching save games, but this data may not be as useful to you since I am not using the game history at all, players can't rewind, so I don't have to worry about that.

As a hint, you can also decompile most Twine games with Tweego to look at the code. Most Twine games are not particularly well coded though, for example, most games in general around here, Twine or not, have no issue breaking user saves on almost every update. Not doing that is laudable, and I don't think it's all that difficult, so I'd encourage you to stay the path, but probably avoid the complicated system you have now and just patch the saves when they're loaded and be done with it.
 
  • Like
Reactions: Heroine Hunter X
Jan 22, 2020
59
157
I learned about how the engine works by essentially rebuilding it. My game is made in a heavily customized version of the engine. However, I actually think a lot of what I wanted to achieve was possible without doing all of that, but I did learn quite a lot about the design of the SugarCube engine during that process.

For general JavaScript, MDN is the best place to learn stuff. The SugarCube documentation is also rather comprehensive, if a bit basic occasionally. I can provide you with some examples from my game of the helper functions and general approach I use for patching save games, but this data may not be as useful to you since I am not using the game history at all, players can't rewind, so I don't have to worry about that.

As a hint, you can also decompile most Twine games with Tweego to look at the code. Most Twine games are not particularly well coded though, for example, most games in general around here, Twine or not, have no issue breaking user saves on almost every update. Not doing that is laudable, and I don't think it's all that difficult, so I'd encourage you to stay the path, but probably avoid the complicated system you have now and just patch the saves when they're loaded and be done with it.
I'll check out MDN. I've gone through the SugarCube documentation and it is fairly simple, but I have not read it top to bottom. I would appreciate some code samples, I think seeing would help me understand a whole lot. Not sure if I'll keep history in the long run, hand't considered turning it off.

Yeah, as a player I always found it annoying and avoidable when it comes to broken save files. I played many Ren'Py games that forced you to button mash through so much "dialogue". Killed the game and story for me. I have decompiled many of those to look at the code and boy... it was a mess. No wonder so many are buggy. Ever since I learned about DRY, I've kept it in mind while coding. I might not be great at it, but I mean well. lol

My game is in very early development, but the story I've had for a long while. I think maybe making a mind map or something would help me. I need to see it so I can figure out how to simplify. Otherwise, I make overly complicated things like the above.
 

outsider artisan

Developer of Succubus Stories.
Game Developer
Jun 14, 2020
348
587
I'll check out MDN. I've gone through the SugarCube documentation and it is fairly simple, but I have not read it top to bottom. I would appreciate some code samples, I think seeing would help me understand a whole lot. Not sure if I'll keep history in the long run, hand't considered turning it off.

Yeah, as a player I always found it annoying and avoidable when it comes to broken save files. I played many Ren'Py games that forced you to button mash through so much "dialogue". Killed the game and story for me. I have decompiled many of those to look at the code and boy... it was a mess. No wonder so many are buggy. Ever since I learned about DRY, I've kept it in mind while coding. I might not be great at it, but I mean well. lol

My game is in very early development, but the story I've had for a long while. I think maybe making a mind map or something would help me. I need to see it so I can figure out how to simplify. Otherwise, I make overly complicated things like the above.
I will get together some code samples tomorrow, I'm actually prepping a release right now, so I won't get around to it until then. DRY is good but sometimes repeating yourself is a rational thing to do, if the simplest and most efficient solution is to copy and paste and chunk of code over, do it. But as a general approach it is wise, and if you are repeating yourself it's always worth checking to make sure there isn't a better way to solve the problem.

I think for what you're doing here a more relevant piece of advice would be separation of concerns. You've got sort of an OOP setup where you're instantiating a custom object type to create the player instance, but really, there's only one player, right? So that "class" is never going to be used again, and it's forcing you to mix data and methods, making them co-dependent and harder to alter separately. What I like to do is separate my functionality as far from my data as I can, so I can change the underlying data structure if I have to at anytime. For example, instead of checking $player.sneaking >= 10, I'd have something like p.sneaking(10), which would let me abstract away from the data, so if I decide later I need to move the sneaking property or somehow change the way the data is tracked, I can alter one function, the `p.sneaking()` function instead of a million checks across the game code. This is a less efficient line of code in terms of performance, but it's a valuable approach that I try to use everywhere in my game.

It also allows fast balance adjustments. Is every sneaking check in the game too hard? Have the function lower the 10 or something before checking.

Flatter data structures are also best. There's nothing wrong with nested objects, except that they are hard to manage in terms of cloning, assigning, etc. You should make your objects as flat as you possibly can for maximum ease of use with SugarCube. I wish I had known this earlier, I am constantly forced to work around unnecessarily complex data structures in my code. For example, the main thing in your code that would prevent you from being able to just use Object.assign() to simplify your process of combining the state and setup objects is that there are nested arrays and objects inside.
 
  • Like
Reactions: Heroine Hunter X
Jan 22, 2020
59
157
I will get together some code samples tomorrow, I'm actually prepping a release right now, so I won't get around to it until then. DRY is good but sometimes repeating yourself is a rational thing to do, if the simplest and most efficient solution is to copy and paste and chunk of code over, do it. But as a general approach it is wise, and if you are repeating yourself it's always worth checking to make sure there isn't a better way to solve the problem.

I think for what you're doing here a more relevant piece of advice would be separation of concerns. You've got sort of an OOP setup where you're instantiating a custom object type to create the player instance, but really, there's only one player, right? So that "class" is never going to be used again, and it's forcing you to mix data and methods, making them co-dependent and harder to alter separately. What I like to do is separate my functionality as far from my data as I can, so I can change the underlying data structure if I have to at anytime. For example, instead of checking $player.sneaking >= 10, I'd have something like p.sneaking(10), which would let me abstract away from the data, so if I decide later I need to move the sneaking property or somehow change the way the data is tracked, I can alter one function, the `p.sneaking()` function instead of a million checks across the game code. This is a less efficient line of code in terms of performance, but it's a valuable approach that I try to use everywhere in my game.

It also allows fast balance adjustments. Is every sneaking check in the game too hard? Have the function lower the 10 or something before checking.

Flatter data structures are also best. There's nothing wrong with nested objects, except that they are hard to manage in terms of cloning, assigning, etc. You should make your objects as flat as you possibly can for maximum ease of use with SugarCube. I wish I had known this earlier, I am constantly forced to work around unnecessarily complex data structures in my code. For example, the main thing in your code that would prevent you from being able to just use Object.assign() to simplify your process of combining the state and setup objects is that there are nested arrays and objects inside.
Hope all goes well with you release! And, thank you for sharing some of your wisdom. I'm going to take a step back and look at my planned game mechanics again with simplicity in mind (something I had in mind before, but drifted from). I don't want it overly complicated, just enough that the player has something to work towards when it comes to progress.
 

outsider artisan

Developer of Succubus Stories.
Game Developer
Jun 14, 2020
348
587
Here are the utilities I'm using to patch my players' saved games:
You don't have permission to view the spoiler content. Log in or register now.
Note how I have separate functions for several different types of patches because my data structure is so nested. If it were a flatter data structure, I could use something like Object.assign() and be done with it.

Here's an example of how a my last two patches looked, but I have patches like this for nearly every major update:
You don't have permission to view the spoiler content. Log in or register now.
There is some redundancy and repetition, as well as some code I don't really use, so this could use some cleanup, but the general idea stands. You'll note that I actually use a prerender to wait until the saved state is loaded, but before any code is executed, instead of accessing the saved state directly, this is just a mistake on my part and I plan to refactor it, but it does work. For some reason I misunderstood the docs and thought the save parameter was doing something else when I first wrote this code, and I never bothered to fix it up.

This is what production code tends to look like, though, so it doesn't always make the best examples, since a lot of times, as long as something's working, you need to keep moving forward, especially if you're on a platform like patreon where you need to keep churning out updates. I think it should be good enough to sort of understand the idea, though.
 
Last edited:
  • Like
Reactions: Heroine Hunter X

HiEv

Member
Sep 1, 2017
384
778
I'm a little late to the party, but I'll add for clarity that only data which never changes from one passage to another throughout gameplay should be stored on the setup object (or otherwise outside of SugarCube story variables). If it's data that can change from one passage to another, then it should be stored in a SugarCube story variable (the variables which start with a $), so that way it will be tracked properly in the game's history, and can be saved and loaded properly.

This is less of a problem if your game doesn't use the history buttons so that players can go back (you should set Config.history.maxStates = 1; in your JavaScript section in that case), but you're still making extra work for yourself since you'd have to add code to manually store and retrieve that data, when it could be done automatically in a story variable.

I'm mainly bringing this up because I've often seen people screw things up this way when they start using the setup object, because they don't realize that all of the variable changes need to be tracked in the game's history, otherwise they risk breaking their game when players load saved games and/or use the history buttons.

Also, instead of Object.assign(), you can use the SugarCube to copy SugarCube supported objects.

the state object for variables that will be saved to file when the user saves their game.
Minor quibble, but it's the (capitalization matters), and only the story variables on the State object will be saved; the temporary variables are never saved.
 
  • Like
Reactions: Heroine Hunter X
Jan 22, 2020
59
157
Here are the utilities I'm using to patch my players' saved games:
You don't have permission to view the spoiler content. Log in or register now.
Note how I have separate functions for several different types of patches because my data structure is so nested. If it were a flatter data structure, I could use something like Object.assign() and be done with it.

Here's an example of how a my last two patches looked, but I have patches like this for nearly every major update:
You don't have permission to view the spoiler content. Log in or register now.
There is some redundancy and repetition, as well as some code I don't really use, so this could use some cleanup, but the general idea stands. You'll note that I actually use a prerender to wait until the saved state is loaded, but before any code is executed, instead of accessing the saved state directly, this is just a mistake on my part and I plan to refactor it, but it does work. For some reason I misunderstood the docs and thought the save parameter was doing something else when I first wrote this code, and I never bothered to fix it up.

This is what production code tends to look like, though, so it doesn't always make the best examples, since a lot of times, as long as something's working, you need to keep moving forward, especially if you're on a platform like patreon where you need to keep churning out updates. I think it should be good enough to sort of understand the idea, though.
Thank you for this example! It's very clean and highly comprehensive. I was considering a JSON version check myself, because it feels more professional and it's also informative to the player. Nice touch! Of all the code above, there was only one part I didn't quite understand, which is the beginning of your onLoad function:

JavaScript:
    prerender['save-update'] = function (_, t) {
    delete prerender[t];
What is this doing?
 
Jan 22, 2020
59
157
I'm a little late to the party, but I'll add for clarity that only data which never changes from one passage to another throughout gameplay should be stored on the setup object (or otherwise outside of SugarCube story variables). If it's data that can change from one passage to another, then it should be stored in a SugarCube story variable (the variables which start with a $), so that way it will be tracked properly in the game's history, and can be saved and loaded properly.
This makes perfect sense to me. I took a small break from coding my game to study more JavaScript so that my mind is refreshed on the language. I was coding in Python a while before trying to use Ren'Py to code this, but it didn't make sense to use that framework to make a text based game. Twine is better equipped and closer to what I need. Thank you for this advice!

This is less of a problem if your game doesn't use the history buttons so that players can go back (you should set Config.history.maxStates = 1; in your JavaScript section in that case), but you're still making extra work for yourself since you'd have to add code to manually store and retrieve that data, when it could be done automatically in a story variable.

I'm mainly bringing this up because I've often seen people screw things up this way when they start using the setup object, because they don't realize that all of the variable changes need to be tracked in the game's history, otherwise they risk breaking their game when players load saved games and/or use the history buttons.
I am considering removing the history, as I want decisions to be more impactful. Though, players can always save before choices if they choose, and I won't have many (if at all) 'Game Over' screens. Perhaps a few for dramatic effect, but overall everything else will branch.

Also, instead of Object.assign(), you can use the SugarCube to copy SugarCube supported objects.
Excellent tip, thank you! It will likely work for me as I plan to greatly simplify my objects.

Minor quibble, but it's the (capitalization matters), and only the story variables on the State object will be saved; the temporary variables are never saved.
It's all fun and games until you spend hours trying to figure out what went wrong only to discover it was a capitalization error. lol

This is great information, than you again!
 

outsider artisan

Developer of Succubus Stories.
Game Developer
Jun 14, 2020
348
587
What is this doing?
I do mention it here:
You'll note that I actually use a prerender to wait until the saved state is loaded, but before any code is executed, instead of accessing the saved state directly, this is just a mistake on my part and I plan to refactor it, but it does work. For some reason I misunderstood the docs and thought the save parameter was doing something else when I first wrote this code, and I never bothered to fix it up.
Instead of accessing the saved state on the parameter passed to the onLoad method, what I was doing was using a prerender to basically wait until after the save was loaded and the active state was the one from the saved game, but before any passage content was run. The delete part just immediately removes the task after it is run, making it a single use task.

Tasks themselves are an outdated and (I believe) deprecated feature, you would use passage navigation events now, and you would use something like this instead:
JavaScript:
$(document).one(":passagestart", function () {
  // code 
});
You can access the incoming state on the parameter passed to the onLoad() method, though, so you actually don't need the event or the task. I thought that my changes to the incoming save were not actually being kept because of a mistake I made when I first wrote this code, basically I thought that the saved data was read only, which didn't seem right, but was sort of plausible, so I did it this way. When you write your version, definitely alter the incoming save. If you need an example of what that would look like, I can write you one later when I have time.
 
  • Like
Reactions: Heroine Hunter X

HiEv

Member
Sep 1, 2017
384
778
You can access the incoming state on the parameter passed to the onLoad() method, though, so you actually don't need the event or the task.
Correct, there's no need to use an event handler within the Config.saves.onLoad function, since it's redundant at best, potentially harmful at worst if it fails to fix a variable before the code attempts to use it.

One other thing you might want to do is set to an integer (i.e. 1), and then increment that number each time you add or change the initialization for a variable, so that the loading code can to "patch" those variables into old saves. (Why integers? Because they're easier to check against than strings and safer to check against than numbers with decimal places.) Then you can simply check the save version to determine what patches to apply to the save data. Thus your code might look something like this:
JavaScript:
Config.saves.version = 5;  //v0.23
// Handle loading saves for various game versions and patching in missing variables.
Config.saves.onLoad = function (save) {
    if ((save.version === undefined) || (save.version < 3)) {  // Saves prior to v0.15 won't work anymore.
        alert("Sorry, this save file is too old to be used with the current version of the game.");
        return false;  // Cancels loading the save.
    }
    var missing_variables = {};
    if (save.version < 4) {  // Patch saves prior to v0.20.
        missing_variables["variable_name"] = 0;
    }
    if (save.version < 5) {  // Patch saves prior to v0.23.
        missing_variables["anotherVariable"] = "Some text";
    }
    if (Object.keys(missing_variables).length > 0) {
        // Add missing variables to history.
        var i, key;
        for (i = 0; i < save.state.history.length; i++) {
            for (key in missing_variables) {
                save.state.history[i].variables[key] = missing_variables[key];
            }
        }
    }
    save.version = Config.saves.version;
};
For example, let's say that, in the "Config.saves.version = 4;" version of your game's code (which equals v0.20 of the game in the example above), it now needs $variable_name to be set to an integer, otherwise the game breaks. For the current version of the game that's not a problem, since you initialize $variable_name in your StoryInit passage. However, that variable didn't exist in prior versions of your game, so old saves would normally be broken now. Instead of writing code to check to see if that variable is initialized every time you go to use it, the above code just patches old saves to add that variable as being set to zero throughout the game's history.

You may need to write a bit more complex patching code in some cases, such as initializing a variable only after a certain passage has been visited, but the above code should cover the basics. See the " " documentation to see the structure of the object passed to the Config.saves.onLoad function if you need to do anything complex like that to patch old saves.

Enjoy! :)
 
Last edited:
May 21, 2022
219
509
I'm a self taught hobbyist programmer, for what it's worth. My latest project is a JavaScript based choose your own adventure game using Twine (Tweego) as the framework. If you're not familiar with Twine, I don't believe it will hinder your understanding. I'll outline the issue in relation to they framework.

Twine's recommended variable storage objects are as follows: the setup object for variables that will load every time the game starts (refreshes), and the state object for variables that will be saved to file when the user saves their game. Since my game is planned to be early access, I need to be clever about how I do updates. If I add a new feature to the player, I don't want to force them to start the game over. Rather, handle it in a way that "auto" updates. The obvious way, to me, was leveraging Twine's setup object for default values. For instance, if the player's default hacking skill is 15 and during development I reason that value too low, I can increase it with minimal fuss. I accomplished this by doing the following:
JavaScript:
setup.PLAYER_BASE = {
    // primary stats
    charisma: 0,
    manipulation: 0,
    wits: 0,

    // skills
    hacking: 15,
    lockpicking: 0,
    negotiation: 0,
    sneaking: 0,

    // perks
    appearance: 0,
    goodness: 0,
    leadership: 0,
    wickedness: 0
}

setup.PLAYER_INIT = {
    // primary stats
    charisma: 0,
    manipulation: 0,
    wits: 0,

    // skills
    hacking: 0,
    lockpicking: 0,
    negotiation: 0,
    sneaking: 0,

    // perks
    appearance: 0,
    goodness: 0,
    leadership: 0,
    wickedness: 0,

    // Inventory
    credits: 0,
    items: [],
    laptop: {}
}
In the above code I created a constant variables for the PLAYER_BASE stats, so the program knows what the player minimum values are. Then I created a constant variable for PLAYER_INIT, which is only used when the game starts. When a new game begins, the following code is run:
JavaScript:
:: InitStory
<<set $player = setup.PLAYER_INIT>>
Before I explain the Twine syntax, I'll explain why I store $player with 0 values for all stats. As the game progresses, the player will gain points to apply to these stats, so this object tracks what the player has changed, not what the values originated as, which is what setup.PLAYER_BASE defines.
In Twine, the $ signals that a value will be stored to the state object (things that will be stored when the game saves). The :: tells Twine that what follows is a passage. I mention this, because there is another character I'll use in the next bit of code that represents a variable local to the passage, which is _. Next I'll demonstrate how this all ties together.
JavaScript:
:: chapterOne
<<set _playerObj = new setup.PlayerObj($player, setup.PLAYER_BASE)>>
Now, here is that `PlayerObj` code to make sense of it all.
JavaScript:
window.setup = window.setup || {};

setup.PlayerObj = function(playerData, playerBase) {
    this.firstName = playerData.firstName;
    this.lastName = playerData.lastName;

    // primary stats
    this.charisma = playerData.charisma + playerData.charisma;
    this.manipulation = playerData.manipulation + playerData.manipulation;
    this.wits = playerData.wits + playerData.wits;

    // skills
    this.hacking = playerData.hacking + playerBase.hacking;
    this.lockpicking = playerData.lockpicking + playerBase.lockpicking;
    this.negotiation = playerData.negotiation + playerBase.negotiation;
    this.sneaking = playerData.sneaking + playerBase.sneaking;

    // perks
    this.appearance = playerData.appearance + playerBase.appearance;
    this.goodness = playerData.goodness + playerBase.appearance;
    this.leadership = playerData.leadership + playerBase.leadership;
    this.wickedness = playerData.wickedness + playerBase.wickedness;

    // items
    this.items = playerData.items;

    // equipment
    this.laptop = playerData.laptop;

    // methods
    this.fullName = function () {
        return this.firstName + " " + this.lastName;
    }
}
Okay, so now every time a game is loaded from a save file, first everything in `setup` is defined, then the passage the player left off on is loaded. So, if I decided the player's hacking skill should be 30, it will now be 15 points higher than it was when the player saved. This to me feels incredibly sloppy and not DRY whatsoever. First, the `setup.PLAYER_INIT` feels redundant, because everything except the hacking skill value has no relevance. I'm sure this is a common task among programmers, which is why I'm seeking wisdom to expand my knowledge. Thank you in advance for the reviews.
Is there a way to access the variables like setup.variables using the console?