+2 votes
by (230 points)

So, I'm trying to make a separate Twine 2 story that serves as an Encyclopedia for the main story; with entries being unlocked throughout the story's core progression.  When you hit the Encyclopedia button it links to another Twine story, opening in a new tab, that then takes variables from the main story to decide what entries and information are unlocked.

I've been thinking of trying to save the game when they click the button and then having the Encyclopedia load the save game to get at the necessary variables... but how would I do that? Is that even realistic? Or can I pass the values I need as arguments somehow? Perhaps some type of object? I'm a noob when it comes to these types of languages.

The linkage occurs through the file system, so I can't use a URL argument as far as I'm aware...

Also. I want to have it in a separate Twine story not only for organizational means but also for speed and design reasons, just in case you were wondering.

Thanks for any help in advance smiley

2 Answers

+1 vote
by (63.1k points)
edited by
 
Best answer

Here's a minimal test case that I'll delete at some point.

First, a warning: A system like this can only work if both the twine html files are hosted at the same domain.  It also will not work locally (i.e. when both files are run from the computer, I hope this isn't what you mean by file system).  This is for security reasons; there are no workarounds (or rather, if you find one, you found a bug in your browser).

The Code.  

First I made this function in the JavaScript area of my main file (the game):

setup.openOther = function () {
    var args = [].slice.call(arguments);
    var url  = args.shift();

    var newWindowTwine = open(url);
    newWindowTwine.passParams = args;
};

Usage:

<<run setup.openOther('url.html', $var1, $var2, $var3, ...etc)>>

/% complete code of the test.html file, for reference %/
<<set $var1 to 'hello', $var2 to 'blue', $var3 to ''>>
<<textbox '$var3' '' autofocus>>

<<link 'Open the Encycopedia'>>
    <<run setup.openOther('encyc.html', $var1, $var2, $var3)>>
<</link>>

Then, in the receiving html file (the encyclopedia):

<<set $params to window.passParams>>\
<<print $params>>

You can send as many variables as you want, and the whole package is passed over as 'window.passParams' as an array.  You can then do whatever you need to with the given data (you'll probably want to 'unpack' it back into individual story variables for easy access).

Note that you can link to another Twine story's play mode by copying the URL you receive when you press the play button and use that for testing, but only in the online version of Twine 2.

Edit: Or use local storage, which works offline. The postMessage API can also work to send the data both ways

by (159k points)
@Chapel

Would it not be easier to just use web/local storage to save a key/value pair in the first story, which the other story could open as long as its on the same domain?
...and this should works on both web & locally hosted HTML files.

eg. The same way a save made in one Harlowe based story can be loaded in a different Harlowe based story.
by (230 points)
@Chapel

Wow, thank you!  That really helps. Very impressive work around without being overly complicated... someday I hope my understanding will be closer to your level. Also, thank you for showing me the example too. I really appreciate it.

And yes, unfortunately, I was referring to the local system. It was a pipe dream... but, I suppose this does make sense.  I wanted to try making the game into one convenient package for distribution.

Anyways, Thanks again!

P.S. From the example I found your Holy Land Dev Log. Very impressive stuff. I'm looking forward to playing the demo. Keep up the great work!

 

@Greyelf

That'd be pretty awesome if it's possible. Thanks for the input!
by (63.1k points)

Actually, @greyelf raises a good point.  Here's another pair of functions.  First one is for the main game:

setup.storeState = function () {
	var store = JSON.stringify(State.variables);
	try {
		localStorage.setItem('state-send', store);
	} catch (e) {
		UI.alert('Local storage is inaccessible!');
		console.log(e);
	}
};

Second one is for the receiving html, the encyclopedia:

setup.unpackVars = function () {
	try {
		var store = localStorage.getItem('state-send');
		return JSON.parse(store);
	} catch(e) {
		UI.alert('Local storage is inaccessible!');
		console.log(e);
	}
};

This example saves the entire variable store to local storage; you may want to create a smaller object and use that instead, but that should only be a problem if you have lots of variables and are developing for a more limited platform like mobile.

In the main game, just call the function when you need it:

<<run setup.storeState()>>

In the receiving file, you'll probably want to save it to a variable:

<<set $store to setup.unpackVars()>>

Then you can access each variable inside as a property:

/% if you have a variable called $name in the main game, access it like this: %/
<<set $store to setup.unpackVars()>>
<<print $store.name>>

Thanks for the suggestion greyelf, that's a much better idea, I think.

by (230 points)

Thank you! It works perfectly! laugh 

You mentioned saving it as a smaller object. Would I just create a Javascript object and then pass it to the JSON.stringify(State.MyObject) or something? 

Thanks again :D

by (63.1k points)

You can create a story variable object: 

<<set $myObj to {
    name: $name,
    data: 'value', 
    blah: 4
}>>

Then send it like: 

JSON.stringify(State.variables.myObj);

Or something like that. You could also send a JavaScript object if you wanted, but it probably won't be part of the State if you do. I'd just stick to story variables, personally. 

It's important you remember not to have any trailing commas, i.e. 

<<set $myObj to {
    name: $name,
    data: 'value', 
    blah: 4,
}>>

That comma after the last property, the 4, would cause an error when converting to JSON. 

by (230 points)

Thanks Chapel! You've been a huge help. I really appreciate it smiley

+2 votes
by (8.6k points)
edited by

There are a few pieces to the puzzle. You can easily find out which window opened your encyclopædia (it's in window.opener) and send a message that way, and you can then intercept it there with a pre-registered event listener and react to it.

On the "encyclopædia" side:

if(window.opener) {
	window.opener.postMessage("GET_DATA", "*");
}

On the "game" side (and using the "openOther" function similar to @Chapel's answer):

setup.openOther = function () {
	var args = [].slice.call(arguments);
	var url  = args.shift();

	var newWindowTwine = open(url);
	// TODO: What to do with the arguments now?
};

function receiveMessage(event)
{
	if(event.data === "GET_DATA") {
		// TODO: Implement this
	}
}

window.addEventListener("message", receiveMessage, false);

Now ... this is one-way communication. You can send data, but you can't receive any, and the receiving side can mutate the data all it wants, it won't be reflected on the sending side. There are also limits to what kind of data you can send, though plain JavaScript values, objects and arrays work fine. Still - with just this you can already implement dynamic loading of passages from external files, for example. Just send the passage's HTML code over as a string.

To get data from the "game" window back to "encyclopædia" window, we need to send a message back. Thankfully, the event already tells us where to do it, via event.source. We also need to register some event handler on the "encyclopædia" side of things, so let's start with this:

function receiveMessage(event)
{
	State.variables.params = event.data;
	// TODO: How to make the current passage aware of the changed data?
}

window.addEventListener("message", receiveMessage, false);

if(window.opener) {
	window.opener.postMessage("GET_DATA", "*");
}

The order is important here; we need to have the event listener registered before we ask for data to be sent to that event listener. Now let's implement the data sending. We need to temporarily save the arguments-to-send, then send them when the loaded page requests them (but only once, in case the user reloads the other page):

setup.openOtherData = new Map();

setup.openOther = function () {
	var args = [].slice.call(arguments);
	var url  = args.shift();

	var newWindowTwine = open(url);
	setup.openOtherData.set(newWindowTwine, args);
};

function receiveMessage(event)
{
	if(event.data === "GET_DATA") {
		if(setup.openOtherData.has(event.source)) {
			event.source.postMessage(setup.openOtherData.get(event.source), "*");
			setup.openOtherData.delete(event.source);
		}
	}
}

window.addEventListener("message", receiveMessage, false);

Thankfully, the last TODO on our list is easy: Just put in Engine.show() in there to re-render the current passage, or Engine.play(...) if you want to go to some passage based on the data received from the other window.

This works just fine directly from the file system and across the network. It works without problems even when the two stories are on different domains, or when one's a local file and one on a server somewhere else. The amount of data transferred is only really limited by your available RAM; you can easily push gigabytes of data either way.

...