+1 vote
by (2.9k points)
edited by
Hi,

I'm making a game but i'm stuck on getting the arrow keys to move the map, i also need to get a working map aswel.

 

Here is what i want my map to look like.

https://usercontent1.hubstatic.com/8108494_f520.jpg

2 Answers

0 votes
by (6.2k points)
Please could you give some detail and actually tell us what the problem is, as well as your story format.
by (2.9k points)
Sugarcube, and I can't figure out how to do it
by (750 points)
Wasn't there a TWINE tutorial on creating maps and navigation somewhere? On Video? Youtube?

Here it is.

https://www.youtube.com/watch?v=5_gbzdRVdAc

Hope that's got what you're at least 'somewhat' looking for.
by (8.6k points)

It's worth noting that the video has quite some errors and misconceptions in it. In JavaScript, your arrays absolutely can have negative indices ($mapArray[-1][-5] would work just fine, assuming $mapArray[-1] is an array), using 0 for "no room here" is unneeded when you can just check for undefined, and it's best to encapsulate the access and movement check into methods, in case you want to expand the system later to allow for multiple maps, three-dimensional movement or whatever else you want to be there and not recode your whole game.

window.GameMap = {
	get: function(x, y) {
		return ((State.variables.mapArray || [])[x] || [])[y];
	},
	walkable: function(x, y) {
		return !!GameMap.get(x, y);
		// alternative check for just 0: return GameMap.get(x, y) === 0
		// alternative check for just undefined: return GameMap.get(x, y) === void 0
	},
};

This lets you check for walkability with <<if GameMap.walkable($positionX, $positionY -1)>> and not ever care about the values in it, or if the map is "columns first" or "rows first" in memory.

by (68.6k points)
edited by

[...]  In JavaScript, your arrays absolutely can have negative indices ($mapArray[-1][-5] would work just fine, assuming $mapArray[-1] is an array) [...]

Arrays proper cannot, actually.  Per the specification, array indices are integer indices whose numeric value i is in the range +0 ≤ i < 2**32-1.  Depending on the JavaScript engine in question, some ignore the addition of members with negative indices (e.g. Firefox), while others allow them as expando properties (e.g. Chrome).  Edit: Firefox's behavior is apparently now inline with other browsers and all allow invalid indices as expando properties.

In the engines which do allow them, the negative "indices" expando properties are not counted by the array's length property and are not exposed via normal array iteration (though you can do it manually) because they're not proper indices (i.e. there's no technical difference between using -2 and "foo", neither are array indices).

Tangentially.  Several Array methods do accept negative indices, in which case they're treated as offsets (generally, from the end of the array).

by (8.6k points)
My Firefox (developer edition) has no problems dealing with things like arr[-3] - I can set values there and read them back and they (when they are references) retain their identity. Same goes for very large indexes - I can set arr[1e12] and it will work just fine, I can even retrieve it using arr[1000000000000].

The value arr.length returns has only a tangential relation to  the actual number of elements in the array, by definition.
by (68.6k points)

The length property has significantly more than a tangential relationship to the array members, per the specification.  Expando properties added to an array do not count as members of the array.  They are properties of the object (because arrays are objects), however, they will never be enumerated by any of the normal array iteration mechanics (i.e. no Array methods will see them, nor will an array iterator enumerate them; e.g. neither for...of loops nor the forEach() method will see them).

Any out of range index (whether < 0 or > 2**32-1) will be treated as an expando property access (i.e. a regular own property of the object) and not as an array index access of the array.  Array indices are a case of special treatment and there are limitations to that.

by (8.6k points)

There is significantly more weirdness going on with array.length than just "doesn't deal with non-integer indices outside its defined range".

It doesn't deal with sparse arrays.

It doesn't deal with delete array[lastIndex]

It doesn't actually add any elements to the array when setting it to a number larger than the current length.

It's usefulness is basically limited to a specific kind of array (dense, zero-based positive index). As long as you can ensure that the arrays you're dealing with are only ever such, you're fine.

However, since the topic is 2D maps for a game, they really really benefit from the arrays holding the maps being sparse, which means using "length" blindly with them is problematic at best. The video originally linked also claimed you can't access a JavaScript array with a negative index, which is plainly false as well.

by (68.6k points)

It doesn't deal with sparse arrays.

It does, in fact.  It doesn't tell you how many members have genuine values, no, but that's not its purpose.  It's simply a count of the members.

To be clear, sparse array members in JavaScript arrays are non-existent by nature (i.e. they're notional and exist only because the array's length property says they do).

 

It doesn't deal with delete array[lastIndex]

Because the delete operator removes properties from objects, not arrays.  Arrays are, again, really just objects which receive some special treatment.  If you do an end run around that special treatment, then things will get weird.

In the case of using delete on an array, which you shouldn't be, all you're really doing is making the deleted member sparse.  To actually manually pop the last member, you must also change the value of length.  That is simply how arrays work in JavaScript.

 

It doesn't actually add any elements to the array when setting it to a number larger than the current length.

It adds sparse members, which, as noted, are notional.  That's simply how JavaScript handles them.

 

It's usefulness is basically limited to a specific kind of array (dense, zero-based positive index). As long as you can ensure that the arrays you're dealing with are only ever such, you're fine.

All arrays are zero-base positive index, as I've explained twice already.  As I noted in previous replies, you may certainly use invalid array indices on an array, however, they'll be treated as regular object property names, not array indices which are special sauce.  Again, an "index" of -2 or 1e12 is no different from using "foo".  You're using property names at that point, not array indices, and none of the special treatment is in effect.

 

The video originally linked also claimed you can't access a JavaScript array with a negative index, which is plainly false as well.

It is not, actually.  Again, you're conflating regular object property access with the special array index access.

 

At this point, the only thing I can suggest is for you to read the ECMAScript specification, because you are confusing the limited special treatment array objects receive with generic object access.

by (68.6k points)
Let me try saying it this way.  Arrays in JavaScript are simply sugar on top of objects with the proper prototype.  If you violate the limitations of the array sugar, then you fall back to dealing with the array object as a regular object, not as an array.
by (8.6k points)

There is not much "special" about array index access. When you write stuff[x], it works almost exactly the same for arrays and objects with the one exception of x being an integer between 0 and 2^32-1 (inclusive) and when writing to that index, which updates the "length" property on arrays but not objects.

Even then, you can have plain objects act like arrays in this regard as well.

var notAnArray = {};
notAnArray.__proto__ = Array.prototype;

Everything else is identical. In particular, obj[-1] and arr[-1] work the same way and are valid access methods for properties of both.

by (68.6k points)

There is not much "special" about array index access.

I never said there was much special about the handling of arrays.  Where are you getting that from?

The handling of array indices and treatment of the length property is basically it.  My point is, has ever been, that if you violate the limitations of that special handling, and there are limitations, then you're dealing with the array object as a regular object and not as an array (i.e. without the special handling).  I do not see what's hard to understand about that.

 

Even then, you can have plain objects act like arrays in this regard as well.

var notAnArray = {};
notAnArray.__proto__ = Array.prototype;

Everything else is identical. In particular, obj[-1] and arr[-1] work the same way and are valid access methods for properties of both.

That is not doing what you think it is.  All you've done there is add Array as your object's prototype.  The object will get access to the Array methods, sure enough.  It will not, however, receive any of the special array index/length handling.

For example, try the following:

var notAnArray = {};
notAnArray.__proto__ = Array.prototype;
console.log(notAnArray.length); // yields: 0
notAnArray.push('foo');
console.log(notAnArray.length); // yields: 1
notAnArray[9] = 'bar';
console.log(notAnArray.length); // yields: 1 (not 10)

var isAnArray = [];
console.log(isAnArray.length); // yields: 0
isAnArray.push('foo');
console.log(isAnArray.length); // yields: 1
isAnArray[9] = 'bar';
console.log(isAnArray.length); // yields: 10

Notice how adding the far member to both, thus attempting to making them sparse, did not update the length property of your object, while doing so with the array object did.

Additionally.  You can take that example further to show that out-of-range indices do not trigger the special array handling.   Like so:

var isAnArray = [];
console.log(isAnArray.length); // yields: 0
isAnArray.push('foo');
console.log(isAnArray.length); // yields: 1
isAnArray[1e12] = 'bar';
console.log(isAnArray.length); // yields: 1 (not 1e12+1)

/*
	Both of the following show only 'foo' because 1e12 is an
	invalid index and does not trigger the special array handling.
*/
isAnArray.forEach(m => console.log(m));
for (var m of isAnArray) console.log(m);

Does the 1e12 property exist on the object and have the value 'bar'?  Yes, certainly.  Is it considered part of the array by JavaScript?  Absolutely not.

 

As I've said multiple times now, while you may certainly use invalid array indices with an array, if you do, then you're not dealing with the array object as an array but as a regular object (i.e. the special array handling is not applied).

by (8.6k points)
As I've said multiple times now, this doesn't mean you can't use those array indices just fine in JavaScript, as opposed to what the video is claiming.

You don't get the benefit of the array functions (though that's relatively easy to "fix" using a Proxy if you really want to), but it won't break anything either.
by (68.6k points)
edited by

As I've said multiple times now, this doesn't mean you can't use those array indices just fine in JavaScript, as opposed to what the video is claiming.

They're regular object properties, not array indices, and claiming otherwise is both disingenuous and harmful to people without a firm grasp of JavaScript (likely most of the people who'll read this).

 

You don't get the benefit of the array functions (though that's relatively easy to "fix" using a Proxy if you really want to), but it won't break anything either.

It breaks both the length the property and thus iterators ("break" here meaning failing to work as intended).  That may not matter to you, but do not claim that it doesn't break anything.  That's disingenuous malarkey.

Beginners thinking that they'll be able to use invalid array indices as they can valid indices are going to be sadly surprised when anything other than absolutely basic access fails for no reason they'll be able to figure out.

These distinctions matter and you're not doing anyone any favors by claiming that they don't.

by (8.6k points)
If iterators matter to you, you can override the array's Symbol.iterator. If the other methods should work differently, you can override most of it too, in doubt packing it all in a Proxy. That's like basic JavaScript nowadays ...
by (68.6k points)
Don't shift the goalposts.  And do you honestly expect beginners, or even intermediates, to have any clue what you're talking about?

Beyond that, the entire idea that spawned this, that you even need negative indices, is silly.  If you really want to use negative Cartesian coordinates while using an array, it's both correct and easier (than overriding every bloody thing) to simply write a function or method which offsets for the size of the map, so the array indices need not be negative (which they cannot actually be anyway).  Or, if you really wanted to apply negative coordinates directly to the object access, just use a generic object (at least then, there's no mistaking that you're not using an array and/or using it properly).
0 votes
by (8.6k points)

Here's a partial answer, about how to make a map.

First the necessary style sheets: In my case, I decided to have the maps displayed in a separate div with the class "map", and each of the map "icons" be a 32x32 px in size <i> element with the background image being the map content - so you can still put something inside the <i>. The main work is done by the following style sheet declarations.

div.map {
  line-height: 0;
  font-size: 0;
}

div.map > i {
  display: inline-block;
  width: 32px;
  height: 32px;
}

Following that, each tile type gets their own small CSS with the "background" property set to an image. I won't paste them here, because of the size, but it's simply four of them for now: i.map-grass, i.map-hills, i.map-mountain and i.map-woods.

Now it's time to define our map. I created a separate passage for this (named "world map"), which will be read as text and contains the map icons represented by ASCII characters. One map line per map row. Accessible via Story.get("world map").text if you need it for further processing.

.........
...H.....
W..HH....
W...HMHH.
.W...HH..
W.WW...H.
.WW..W...

Now we just need a few widgets and some static data initialisers to finish our rendering off. One helper widget for rendering a single map icon (in case we ever need it separately), and one for rendering a given map ("world map" per default, but it can work with any given as an argument). All of this packed into a single passage tagged "widget" and "nobr".

<<set setup.MapIcons = {
	".": "grass",
	"M": "mountain",
	"H": "hills",
	"W": "woods",
}>>

<<widget "icon">>
<<= "<i class='map-" + $args[0] + "'></i>">>
<</widget>>

<<widget "showmap">>
  <<set _mapName = $args[0] || "world map", _mapResult = "">>
  <<if Story.has(_mapName)>>
	<<script>>
	  State.temporary.mapResult = "@@.map;\n";
	  for(var m of Story.get(State.temporary.mapName).text) {
	    if(m === "\n") {
		  State.temporary.mapResult += "<br>";
		} else {
		  State.temporary.mapResult += "<<icon '" + setup.MapIcons[m] + "'>>";
		}
	  }
	  State.temporary.mapResult += "\n@@";
	<</script>>
	<<= _mapResult>>
  <</if>>
<</widget>>

And that's it. Just include <<showmap>> (or <<showmap "world map">> or <<showmap "tamriel">> ...) anywhere in a passage and it will drop a <div class="map"> element with the map in that spot.

Showcase: standalone working example, Twine source file

...