Harlowe, Datamaps, and For-loops, oh my.

0 votes
asked Jul 9 by geekdragon (700 points)
edited Jul 9 by geekdragon

I'm using Twine 2.1.3 and Harlowe 2.0.1 for a turn-based text RPG game. I have datamap with a list of character status effects (incapacitated, health regen. up, etc.), paired with a countdown timer for turns remaining.

I'm using a (for) loop each turn to check each effect, count down their timers, and apply active effect modifiers. This works well, but I have a minor hitch and I'd like suggestions.

The "spread out" operator only works on normal arrays, so to work with a datamap I need to provide an array of the data names. Luckily there's a macro for that!

(for: each _status, ...(datanames:($statusEffects)))[do stuff

This works fine, except for one thing: The datanames macro automatically sorts the array into alphabetical order. As it turns out, I NEED the status effects evaluated in their original order so that I can control the display order and priority.

I've come up with a couple possible workarounds:

1) Create a redundant array of names to match the datamap, and use it to control the loop

2) Rename the status effects with an alphabetical prefix (a_incapacitate, b_healthUp)

I'm designing around the first option right now, but it grates on me to have the same data in two places and risk mistakes while I'm updating these structures. Any other ideas?

2 Answers

+1 vote
answered Jul 14 by greyelf (10,220 points)
edited Jul 14 by greyelf
Best answer

WARNING: The following solution is based on knowledge of the internal implementation of the story format, and may stop working if the implementation changes in a future release of Harlowe 2.

The following Javascript prototype creates a custom Namespace and Function which will return an Array of datanames in their Natural order, the code needs to be placed within your story's Story Javascript area.

if (! window.GE) {
	window.GE = {};

/* Returns an Array of datanames in Natural order. */
window.GE.datanamesNatural = function (value) {
	var map = null;

	if (value === null || typeof value !== "object") {
		throw new TypeError('value is not an object');
	} else if (value instanceof Map) {
		map = value;
	} else if (value.get) {
		map = value.get();
	} else {
		throw new TypeError('value is not a VarRef or a Map');

	return Array.from(map.keys());

The following test example shows how to use the new GE.datanamesNatural() function in a standard Passage.:

(set: $map to (datamap: "C", 1, "A", 2, "B", 3))

(set: $namesAtoZ to (datanames: $map))
(set: $namesNatural to GE.datanamesNatural($map))

A-Z: (print: $namesAtoZ)
Natural: (print: $namesNatural)
Within print: (print: GE.datanamesNatural($map))



a. For some reason Harlowe passes the story variable to the Javascript function as a VarRef object when reference within a (set:) macro, and passes the same value as Map when referenced within a (print:) macro.

b. I was not able to think of a better method for checking if the value passed was a VarRef, other than checking for the existence of the get function, because VarRef is not a known class so instanceof did not work.

c. I suggest you change the GE Namespace name to another unique name that makes sense to your particular project, just remember to also change every usage of the GE namespace in your TwineScript to the new unique name.

A more experienced Javascript programmer may be able to create a better implementation of this function.

commented Jul 14 by geekdragon (700 points)

Super awesome. Now I can use your function as a direct replacement in the example I originally posted. Thanks again.

(for: each _status, ...(Custom.datanamesNatural($statusEffectState)))[
       Timer for _status is currently (print: _status of $statusEffectState)


+1 vote
answered Jul 10 by Chapel (7,390 points)

Datamaps in harlowe, and objects in general in JavaScript are all about unordered data collections; only arrays really provide reliable ordering, at least as far as I know. If ordering is important, having an array copy of the data names is pretty much the way to go, in my opinion, though option two isn't a bad way to force harlowe to maintain ordering. Either way, it's not really redundant if you need it. 

commented Jul 11 by greyelf (10,220 points)

If you were using Harlowe 1.x then I would of suggested code like the following, but unfortunately Harlowe 2.x is stricter about where you can use Javascript in it's version of TwineScript.

Harlowe 1.x only example:

(set: $map to (datamap: "C", 1, "A", 2, "B", 3))

(set: $namesAtoZ to (datanames: $map))
(set: $namesNatural to Array.from($map.keys()))

A-Z: (print: $namesAtoZ)
Natural: (print: $namesNatural)


commented Jul 11 by geekdragon (700 points)
Thanks for the tease. Unfortunately there's no going back from Harlowe 2 now that I've incorporated so many for-loops. Huge speed and convenience increase from when I started out using recursive passages, but I still have an event that lags so badly I implemented interim screen refreshes to show progress.
commented Jul 12 by wyrde (1,070 points)

I wouldn't think of option 1 as a redundant array, but more as a checklist. The array would contain all the possible effects, while the datamap would contain the effects which are currently "active".

The array might have

(set: $EffectList to (array: StubbedToe, MajorWound, MinorDizzy, Sickened, Confused))

While the datamap may contain

(set: $CharacterEffects to (dm: Sickened, 300, StubbedToe, 100))

The (for: ) loop with the checklist will maintain not only the display order, but all the possible conditions.

The code for adding, updating, and removing conditions from the datamap only has to be concerned with the current condition.

It also means a new condition type is easily added, by placing it in the array.

For debugging, a (for: ) loop can also be written to check if there's a pair in the datamap which doesn't match the values in the array, which can be handy for catching typos or a forgotten effect.

And there's also option 3) design the display of conditions so order isn't important. cool


Harlowe: making the simple easier, and the difficult harder.

commented Jul 12 by geekdragon (700 points)
Thanks. There will always be ways to do thing differently, and I appreciate the suggestions.

By way of explanation, my code currently determines which effects are of concern by their datamap value. A '0' value implies an inactive effect. This is why I'm irked at using a separate checklist array.

As far as display order, I'm constructing a message string within the loop for display in a later passage.
commented Jul 13 by geekdragon (700 points)

Thank you greyelf. You should get answer credit for that solution, since I think I'll be using it. (Obviously with some comments in my code to remind me how to reverse it, should your warning prove true and everything breaks!)

I assume it's probably a limitation of javascript/twinescript interplay, but I noticed that I can't do something like:

(print: GE.datanamesNatural($map))

as it throws a "Cannot read property 'keys' of undefined" javascript error. It works fine when the custom function sets a variable first, then I work with the variable. My first instinct was to nest the function with other Twinescript macros to git-r-done in one step.