Importing attributes from one game to another

We had a thread a few months back about importing choices from one Squiffy game to another (at http://textadventures.co.uk/forum/squiffy/topic/5paercpdb0wazp1v5lwpbg/importing-attributes-from-one-game-to-its-sequel )- that thread's been closed now, but I'm resurrecting the concept as I've almost finished my game and would love to have options for carrying a character forward to subsequent games!

mrangel came up with an excellent solution whereby user choices are saved into a password at the end of the first game which can be decoded by the second game to set the attributes there so the player can continue with the character they develoepd over the first game.

That's a great solution for users who have changed devices or browsers or cleared their cache between games, but Belén suggested that a more user-friendly alternative would be for the character to be saved in the LocalStorage of the browser at the end of game 1, and for game 2 to detect such saved characters and offer them to a player starting game 2.

Ideally the player could either

  • select pre-existing characters stored in the borwser (potentially more than once, if they want to start game 2 with the same character multiple times)
  • use the password (if they've changed device or browser/cleared their cache, and therefore their characters aren't stored in their current browser)
  • or start from scratch with a new character.

And as there could be a game 3 (and 4, and 5...) it would be good if their game 2 character could also then be saved separately to their game 1 character (a game 1 character can import into game 2, but not into game 3, and a game 2 character imported OR made from scratch in game 2 can import into game 3...and so on...)


OK… here's my attempt at modifying what we had last time to give the option of saving to localstorage.
(2nd edit… added a timeout to the load function to ensure that the selector is displayed before attempting to populate it)

I've realised that there's an unnecessary delay when loading the lzstring library, which can probably be avoided. So I'm poking a weird improvised "load if not already loaded" construct:

    (LZString ? ((i,f) => f()) : jQuery.getScript)("https://mrangel.info/lz-string.js", function () {
      // function body goes here
    });

That should load the LZString library and then run the function body; or just run the function if LZString is already loaded. I think.
EDIT: Doesn't work. Corrected version in the post below. I've corrected it in the code snippets below -

So then, as the absolute basics, we'll want to modify the script so that it saves to localstorage:

[[save]]:
    var attributes_to_save = "name haircolor fish_size money pet";
    (typeof(LZString) == "undefined" ? jQuery.getScript : (i,f) => f())("https://mrangel.info/lz-string.js", function () {
      var values = {};
      // change this name if you want something different:
      var defaultname = squiffy.get('name') + ' ' + (new Date()).toLocaleString();
      var suffix = 0;
      while (window.localStorage.getItem('lssv_' + defaultname + (suffix ? (' ('+suffix+')') : ''))) {
        suffix++;
      }
      attributes_to_save.split(/\s+/).forEach (attr => values[attr] = squiffy.get(attr));
      var password = LZString.compressToBase64(JSON.stringify(values));
      jQuery("input#saveString").val(password);
      jQuery("input#saveTitle").last().val(defaultname + (suffix ? (' ('+suffix+')') : '')).prop('readonly', false).on('input', function  () {
        $(this).css('color', window.localStorage.getItem($(this).val()) ? 'red' : 'inherit');
        jQuery("button#saveButton").last().prop('disabled', false).text(window.localStorage.getItem('lssv_' + $(this).val()) ? 'overwrite' : 'save');
      });
      jQuery("button#saveButton").last().click(function () {
        window.localStorage.setItem('lssv_' + $(this).siblings('#saveTitle').val(), password);
        $(this).prop('disabled', true).text('Saved');
      });
    });

<ul><li>You can copy this password and save it somewhere in order to restore your game later: <input readonly type="text" id="saveString" value="Generating password; please wait" /></li>
<li>Or enter a name to save your game within the browser <input readonly type="text" id="saveTitle" value="Generating password; please wait" /><button type="button" id="saveButton" disabled>Save!</button></li></ul>

And the load part:

[[Load]]:
    var updateLoadSelector = function () {
      if (jQuery('#saveSelector').length) {
        for (var i=0 ; i < window.localStorage.length ; i++) {
          var key = window.localStorage.key(i);
          if (key.match(/^lssv_/)) {
            jQuery('<option>', {value: window.localStorage.getItem(key)}).text(key.replace(/^lssv_/, '')).appendTo('#saveSelector');
          }
        }
        if (jQuery('#saveSelector').children().length) {
          jQuery('#saveSelector').click(function() {
            jQuery('input#loadString').last().val(jQuery(this).val());
          }).parent().show();
        }
      } else {
        setTimeout (updateLoadSelector, 100);
      }
    };
    updateLoadSelector();

<p>You can <span style="display: none;">choose a previously saved game <select id="saveSelector"></select> or </span>paste your save password from the previous game here: <input type="text" id="loadString" /></p>
[[Next]]

Or there might be a link here to start a new game without loading a previously saved one.

[[Next]]:

    var attributes_to_load = "name pet haircolor fish_size money";
    jQuery("input#loadString").last().prop("readonly", true).each(function () {
      this.value && (typeof(LZString) == "undefined" ? jQuery.getScript : (i,f) => f())("https://mrangel.info/lz-string.js", function () {
        var values = JSON.parse(LZString.decompressFromBase64(this.value));
        attributes_to_load.split(/\s+/).forEach (attr => squiffy.set(attr, values[attr]));
        squiffy.story.go("after load");
      });
    });
    

[[after load]]:
By getting to this point, the game should be loaded.

In this case, I've prefixed all the names with lssv_, for LocalStorage SaVe. Just so it doesn't end up displaying anything saved to localstorage by other scripts on the same site. It would probably be a good idea to change this to something different, especially if you have multiple games hosted on the same site, so that the game can tell which saves actually belong to it.

I've not really attempted to handle malformed saves. If the string (pasted or from localstorage) is invalid, I think it will just go to 'after load' without doing anything. This is the bare basics of a script, and could use a little polishing.

This is pretty ugly, and I haven't tested it yet. This was just written on the forum while I was trying to distract myself from panic, so if there's problems with it please let me know. I'm sure anyone with any JS knowledge should be able to fix it; my brain isn't in the best place right now. But I'll do my best.


Thanks mrangel - I'm not expert enough to debug it, but I can definitely shove it into Squiffy and see what happens!

At present the first part isn't working for me (nothing appears once I click Save, not even the plain HTML markup below your script) - looking at the console in Firefox it's throwing "Uncaught ReferenceError: LZString is not defined"?


Sorry, careless assumption about how JS handles undefined variables.
The check if the library is loaded should be:

    (typeof(LZString) == "undefined" ? jQuery.getScript : (i,f) => f())("https://mrangel.info/lz-string.js", function () {
      // function body goes here
    });

(I'm used to a few languages that allow an undefined value to be considered false; and JS used to do that in some browsers)

So change (LZString ? ((i,f) => f()) : jQuery.getScript) to (typeof(LZString) == "undefined" ? jQuery.getScript : (i,f) => f()) in both places and it should be a step closer.

And yet again, I'm on my phone so not in a place where I can test this myself.


Great, that's looking good on the Save side! The only thing I can see is that it gives a default value for the Save box - {name} plus the date and time - which would be perfectly acceptable to go ahead and save with, but unless you actually edit that value in some way, the Save button is greyed out.

On the Load side, I'm getting an error when running it: SyntaxError: missing ; after for-loop initializer


That was a typo, I had == instead of =.

Updated the code in the post above, and also added a timeout loop so that if the dropdown list hasn't been displayed yet, it will wait a tenth of a second and then try again rather than adding options to a list that isn't there.


OK, trying this out now!

I've created the Save and Load sides in two separate Squiffy files (using the desktop version rather than the in-browser one) to replicate game 1 and game 2. I've then Built each one and uploaded them - you can have a look at https://www.dragonchoice.com/character/index.html (to save the character, and there's a link to go to the character load page).

Save side:

  • still have the issue where the default name - date - time can't be saved without editing it in some way (Save button greyed out)

Load side:

  • clicking Next (after entering a password or selecting from the dropdown) just goes to a blank section

OK, finally on a computer. Have given a cursory test this time:

(EDIT: Seems to be a race condition; sometimes the password fails to display, if the LZString library finishes loading before Squiffy finishes rendering the page. Fixed, I think)

[[save]]:
    var attributes_to_save = "name haircolor fish_size money pet";
    (typeof(LZString) == "undefined" ? jQuery.getScript : (i,f) => f())("https://mrangel.info/lz-string.js", function () {
      var values = {};
      // change this name if you want something different:
      var defaultname = squiffy.get('name') + ' ' + (new Date()).toLocaleString();
      var suffix = 0;
      while (window.localStorage.getItem('lssv_' + defaultname + (suffix ? (' ('+suffix+')') : ''))) {
        suffix++;
      }
      attributes_to_save.split(/\s+/).forEach (attr => values[attr] = squiffy.get(attr));
      var password = LZString.compressToBase64(JSON.stringify(values));
      var showPassword = function() {
        if ($('#saveString').length) {
          jQuery("input#saveString").val(password);
          jQuery("input#saveTitle").last().val(defaultname + (suffix ? (' ('+suffix+')') : '')).prop('readonly', false).on('input', function  () {
            $(this).css('color', window.localStorage.getItem('lssv_' + $(this).val()) ? 'red' : 'inherit');
            jQuery("button#saveButton").last().text(window.localStorage.getItem('lssv_' + $(this).val()) ? 'overwrite' : 'save');
          });
          jQuery("button#saveButton").last().click(function () {
            window.localStorage.setItem('lssv_' + $(this).siblings('#saveTitle').val(), password);
            $(this).prop('disabled', true).text('Saved');
          }).prop('disabled', !defaultname);
        } else {
          setTimeout (showPassword, 100);
        }
      };
      showPassword();
    });

<ul><li>You can copy this password and save it somewhere in order to restore your game later: <input readonly type="text" id="saveString" value="Generating password; please wait" /></li>
<li>Or enter a name to save your game within the browser <input readonly type="text" id="saveTitle" value="Generating password; please wait" /><button type="button" id="saveButton" disabled>Save!</button></li></ul>

and to load:

[[Load]]:
    var updateLoadSelector = function () {
      if (jQuery('#saveSelector').length) {
        for (var i=0 ; i < window.localStorage.length ; i++) {
          var key = window.localStorage.key(i);
          if (key.match(/^lssv_/)) {
            jQuery('<option>', {value: window.localStorage.getItem(key)}).text(key.replace(/^lssv_/, '')).appendTo('#saveSelector');
          }
        }
        if (jQuery('#saveSelector').children().length > 1) {
          jQuery('#saveSelector').change(function() {
            if ($(this).val() != "null") {
              jQuery('input#loadString').last().val(jQuery(this).val());
              $(this).children('.nulloption').remove();
            }
          }).parent().show();
        }
      } else {
        setTimeout (updateLoadSelector, 100);
      }
    };
    updateLoadSelector();

<p>You can <span style="display: none;">choose a previously saved game <select id="saveSelector"><option name="null" class="nulloption" style="color: grey">pick a save</option></select> or </span>paste your save password from the previous game here: <input type="text" id="loadString" /></p>
[[Next]]

Or there might be a link here to start a new game without loading a previously saved one.

[[Next]]:

    var attributes_to_load = "name pet haircolor fish_size money";
    var field = jQuery("input#loadString").last().prop("readonly", true);
    (typeof(LZString) == "undefined" ? jQuery.getScript : (i,f) => f())("https://mrangel.info/lz-string.js", function () {
      var values;
      try {
        values = JSON.parse(LZString.decompressFromBase64(field.val()));
      } catch (e) {
        values = {};
        squiffy.set("loaderror", e.message);
      }
      if (values) {
        attributes_to_load.split(/\s+/).forEach (attr => squiffy.set(attr, values[attr]));
        squiffy.story.go("after load");
      } else {
        squiffy.story.go("load error");
      }
    });

[[load error]]:
The player will be sent here if there's something wrong with the string in the save box.
If there is an error message, it was: {loaderror}.

[[after load]]:
By getting to this point, the game should be loaded.

I think this is working, unless there's something I messed up by accident when tweaking it to work with my test data.


(I've gotten lazy and started using $ instead of jQuery. I don't know why the convention in Squiffy is to use the long version, but they're interchangeable)

If you want to have multiple compatible games (for example, part 2 can load a character from part 1, but part 3 can load a character from part 1 or 2), you can use regular expressions for the prefix. For example, change /^lssv_/ in the load function (I think it appears twice) to /^lssv[1-2]_/ to allow saves created using 'lssv1_' or 'lssv2_' in the save script.


Absolutely spot on! Thanks so much mrangel, this is going to be instrumental in my series of games (I'll put your in the credits, let me know if you want me to use a name that isn't your screenname here!

I'll do some more testing around the last bit - I need to set up a third game and see about importing characters from the first and second - but this is just awesome and I'm incredibly grateful for your help and expertise!

I hope other Squiffy users will find this helpful too. Personally I'm more a writer than a coder, so writing the interactive story is the easy part - getting it to function the way I want it is that bit harder!


Log in to post a reply.

Support

Forums