0 votes
by (230 points)
edited by

I'm currently working on Hunt The Wumpus in Twine as an experiment (sugarcube 2). Eventually I plan to make a succinct tutorial that touches on a wide range of Twine features, while building a functional and polished game. The point is to focus on using Twine as naturally as possible, and not get bogged down too much with custom code.

Right now I've run into a snag that I think demands the use of some custom code, and I'm trying to figure out how to do it elegantly.

Each room is its own passage, and the Wumpus is assigned a room randomly in the Storyinit passage.

<<set $wumpus =
	{
	"room2" : either
		(
		"Boiler Room", "Heat Exchanger Room", "Water Reclaimation",
		"Dynamo Room", "Noisy Room", "Pipe Nightmare",
		"Breaker Room", "Hotel Lobby"
		)
	}
>>

I have a generic "Room" passage that checks for the presence of a Wumpus. This cuts down on the repeated code.

<<print passage()>><<set $player.room2 = passage()>>
$wumpus.room2
<<if ($player.room2 == $wumpus.room2)>>Wumpus is here!<</if>>

Each room consists of little more than an include for this generic passage, some description text, and a set of links to connecting rooms.

<<include "Room2">>

Steam billows forth from a tangle of pipes wrapped like vines around a massive round boiler.

---Nothing
Nothing<-|->[[Heat Exchanger Room]]
---------[[Dynamo Room]]

At this point in the game's build, I need to allow each room (the generic room code) to detect whether there is a Wumpus in the adjascent rooms. In order to do that, I need to detect which passages the current passage links to. I can check that information against the Wumpus's assigned room, and if it's present I can add the "I smell a Wumpus" text to the current room.

I currently have two ideas for how to do this. Both involve building a custom macro in Javascript.

  1. Break the room text apart, searching for all links and their passage text.
  2. Add tags to each room, defining which rooms it links to. Use Story.lookup() to list all rooms with the current room tagged.

Option 2 requires more work than I think is necessary. Every time a room is added, I'd not only need to repeat each link in the tags for that room. I'd also need to go to each connected room and add the new room there.

Option 1 seems clumsy and not very robust. The code would get very complicated if I wanted to check for different link types, or account for conditionals or <<include>> passages.

Is there a way to access a room's links through javascript? Is there some better way of doing this?

2 Answers

+1 vote
by (44.7k points)
selected by
 
Best answer

You might want to take a look at this sample code.  It uses jQuery to find all of the links displayed currently, and then modifies those links so you can use the number keys to navigate with them.

Based on that code, what you need is some code like this in your JavaScript section:

$(document).on(":passagerender", function (ev) {
	var Links = $(ev.content).find("a").toArray(), i;
	for (i=0; i<Links.length; i++) {
		if (Links[i].getAttribute("data-passage") == State.variables.wumpus.room2) {
			$(ev.content).find("#wumpus").wiki("I smell a Wumpus!<br>");
			break;
		}
	}
});

That code waits for the passage to render, and then creates an array of all links on the page (not including the UI bar).  It then checks the "data-passage" attribute of each link to see if it equals $wumpus.room2.  If it does, then it causes the text "I smell a Wumpus nearby." (followed by a linebreak) to appear in the passage wherever you put "<div id="wumpus"></div>" in your passages.

It will require a little work to add that <div> to all of your passages, but it should be simpler than the other two options you had.  (Or, at least this is a better version of Option 1.)

Hope that helps!  :-)

EDIT: Bug fixed.

by (230 points)

Thank you!

I'm playing around with this code now. I'm afraid I don't fully understand it.

What does the "#wumpus" part do? If I change this to ev.content, I can append that string to the end of my passage, but I'd rather insert it somewhere inside.

Thanks

 

What I'd really like to do is create my own function or macro to do this. Something that returns a boolean if a passage or passages (given as a string, or an array of strings) is linked to in the text.

Useage would be something like:

<<if isLinked($wumpus.room)>>I smell a Wumpus!<</if>>

or

<<if isLinked $wumpus.room>>I smell a Wumpus!<</if>>

Is this feasible?

I know that Macros.add lets you create macros like this, but I can't find any way to get access to the passage text the way ev.content can.

by (44.7k points)

"What does the "#wumpus" part do?"

The "#wumpus" part is supposed to look for an HTML element with an ID of "wumpus", which should be your "<div id="wumpus"></div>" part, and it inserts the given text inside of that <div>.  Like I said, you should insert "<div id="wumpus"></div>" into each passage in the place where you want "I smell a Wumpus!" to appear if it's needed.

That said, I screwed that code up a little (fixed it now though).  Instead of :

$("#wumpus").wiki("I smell a Wumpus!<br>");

it should actually be:

$(ev.content).find("#wumpus").wiki("I smell a Wumpus!<br>");

So use that line instead and then that should work.  See the jQuery API documentation on the .find() method and the .append() method for details.  (With SugarCube, the .wiki() method is the same as the jQuery .append() method, except it also wikifies the content, in the same way that text added to a passage normally is wikified (i.e. variables are converted into their values, macros are executed, etc...).)

"What I'd really like to do is create my own function or macro to do this."

Unfortunately, you can't turn that into a function, widget, or macro, because the passage has to render before the code can look for the links, and at that point, any code in the passage which executes immediately would have already executed.  Doing that inside the "passagerender" event, like in the code I gave you, is the soonest you could do the search for any links.

Hope that helps!  :-)

by (159k points)

HiEv's implementation is scaning through the HTML structure that was generated for the current Passage looking for link related HTML elements. HiEv is using the :passagerender event to delay that scanning until after the elements have actually been created.

The main issue with your idea of calling an isLinked() funtion within the Passage content is that the link related HTML elements (that the function needs to look for) don't exist at the time the function is called.

You could abstract the logic within HiEv's :passagerender event to a function like so.

/* Returns true if a link is found that references the location supplied. */
setup.isLinked = function (content, location) {

	/* Assume that no link was found. */
	var found = false;

	/* Find all the links that target passages. */
	const $links = $(content)
		.find("a.link-internal[data-passage]")
		.toArray();

	for (let i = 0; i < $links.length; i++) {
		if ($links[i].getAttribute("data-passage") === location) {
			found = true;
			break;
		}
	}
	return found;
};

note: I have used a more specific CSS selector that looks for internal links that have a data-passage attribute.

... and you could call the above function from within a :passagerender event like so

$(document).one(":passagerender", function (ev) {
	if (setup.isLinked(ev.content, State.variables.wumpus.room)) {
		/* Do something... */
		console.log('wumpus is nearby');
	}
});


The next issue to solve is what to do once it has been determined that one links references the location of the wumpus.

by (44.7k points)
@Greyelf - But what's the point of making the code a separate function like that, when there's no reason to call such a function anywhere other than within the "passagerender" event?  Especially considering that it wouldn't work properly if called in a passage before its "passagerender" event had occurred.  It looks like a needlessly complicated waste of code.

It makes more sense to just roll the the relevant bits of that function into the "passagerender" event and dump the rest, which essentially leaves you with the code I posted originally.
by (230 points)

HiEv,

Oh my goodness I feel foolish. I got so wrapped up in immediately diving into the code that I never finished reading your comment!

I've changed the code and added the "wumpus" ID (which I'll probably rename to "wumpsense" because it amuses me), and everything works perfectly! I don't even have to add it to every passage, as I've got a generic room passage that I <<include>> in each passage.

Thanks for re-explaining for me.

 

Greyelf,

Thank you for your suggestion!

Your suggestion does make the code more human readable. I think that I prefer to keep everything together for now. I'll keep your suggestion in mind if I add more functionality to the passagerender event. Which I might! This is a very handy tool!

Thank you both!

by (159k points)
@HiEv
The OP expressed a wish to have a boolean returning function they could call then wondered about the feasiblity of creating such a thing.

I explains what your implementation was doing and why such a function wouldn't work within the context they wanted to use it in.

For educational purposes I supplied them an example of what such a function could look like so that they now know one method they can use to define callable JavaScript functions within a Twine project, then showed how such a function is used within an event.

I also supplied them with a more specific selector to use within your find() function call so that the following for loop wouldn't need to include non-passage transition based links.

I then explained that they would still need to sovle the issue of updating the page, thus impling that using a function this way doesn't solve their original problem.
0 votes
by (159k points)

You stated you didn't want to use "custom code", so the following relies on you simply assigning a value to a known variable within each of your 'room' passages. The value is an array containing the Passage Names of the other rooms that this room links to.

<<set $linksTo to ["room 2", "room 4", "room 26"]>>

 

by (230 points)
Thank you for the help. Unfortunately I think I phrased things poorly, because what you're proposing doesn't quite do what I need.

Firstly, I'm not trying to avoid making custom macros in Javascript per se. In fact that's what I'm planning to do. I was just trying to figure out the most straightforward method for doing so. I haven't found any built-in functions that list links in a passage, and I'm not aware of any elements or methods for passage objects that do that either.

I'd like to avoid forcing writers to manually list links for each room/passage, because I feel that it's error-prone, and it restricts the ease of creating content (namely new rooms).

Thank you for your help, though!
...