0 votes
by (1.6k points)
edited by

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
by (159k points)
edited by
 
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))

 

NOTES:

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.

by (1.6k 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
by (63.1k 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. 

by (1.6k points)
Thanks for the confirmation.

Yeah, I think the main thing I've overlooked is that Datamaps aren't really ordered -- they just appear to be. Not the answer I want, but the truth is that I'm sort of misusing the data type. Even a variation of the (datanames:) macro that didn't alphabetize the result would be a false solution, since the ordering isn't technically guaranteed.

Still trying to think of a more elegant solution, but I'm probably still looking at the first option. (piggy-backing a datamap and a names array, and using the data names as the index between them).
by (63.1k points)
edited by

There are JavaScript methods that you could potentially use that won't autosort the array, like Object.keys(), but there's two problems there: 

  1. They aren't officially supported as a part of Harlowe, so their use can be sort of iffy since Harlowe makes an attempt, at least from what I can tell, to discourage or distance itself from the use of JavaScript inside macros. 
  2. The order of this data is still really not guaranteed in any way. In my experience, it seems fairly consistent, but most documentation I've seen makes it a point to tell you you can't count on it. 

That said, depending on what your data map contains, and does, you could workaround this issue in a slightly more integrated way by defining a passage system that serves this purpose instead of a data map.

 

If you need some code examples, let me know, but on a high level: 

  • You'd define a series of passages, each having a name similar to your data map names, so 'incapacitated', 'healthUp', etc. 
  • Within the passage's body you'd define the code that you would've defined in the for each hooks. You can also set other variable and such to configure your timers. 
  • You'd create an array of these passage names. 
  • You'd have your for each macro run through the array, and have each hook call that passage's code via a display macro. 
  • You'd probably want to ignore any output from these passages, you could use CSS and tags to do this, or collapsing white space markup, though the former is better. 

I don't know if it'll help much to use something like this over your current plan; it largely depends on your code. 

by (1.6k points)
The passage system process you describe is pretty close to how I'm handling game event management. I can definitely imagine making it work for this application as well.

Thanks again for the thoughtful replies.
by (68.6k points)

Datamaps in harlowe, and objects in general in JavaScript are all about unordered data collections;

Do not confuse generic key iteration on objects in JavaScript, which is not guaranteed to be ordered, with JavaScript Map objects, iteration upon which is guaranteed to be ordered.  Harlowe's datamaps are simply Map objects wrapped by the (datamap:) macro, so they are indeed ordered—this is easily verified.

That knowledge doesn't help much, unfortunately, if you're using the datamap-to-list macros—(datanames:), (datavalues:), & (dataentries:)—as they all, bizarrely, explicitly sort the array they return by the keys.

You could attempt to use the Map methods directly, but Harlowe is, basically, designed to make that difficult.

by (63.1k points)

Do not confuse generic key iteration on objects in JavaScript, which is not guaranteed to be ordered, with JavaScript Map objects, iteration upon which is guaranteed to be ordered.  

I was definitely confusing/conflating the two.  Thanks. 

by (1.6k points)

Harlowe's datamaps are simply Map objects wrapped by the (datamap:) macro, so they are indeed ordered—this is easily verified.

Interesting. I suppose I could make a proposal regarding unsorted versions of the datamap-to-list macros. Probably not a very high priority request however. 

by (159k 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)

 

by (1.6k 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.
by (1.1k 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.

by (1.6k 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.
by (1.6k 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.

...