+2 votes
asked by (320 points)

Hi, fellow twiners!

For a new game, I want to be able to display a game board which consists of hexagons in a grid. (Just as in the settlers board game) Before I start, I wanted to ask you for advice on how to go about that. I need to be able to display some numeric information in each hexagon (such as the number of trees groing there) as well as giving them individual styles according to certain vasriables. Each hexagon should also serve as a link to a passage.

I found two solutions I figured could get me there:

1. CSS hexagons (e.g. here)

I guess this is a light weight solution, that would allow me to render the grid by using a SugarCube widget. I guess this means more work, but also more control over the grid.

2. A jQuery based solution

This one comes with a nice grid rendering function and a lot of other stuff. But I wouldn't know how to use the provided event listeners with linking to passages. Would that be hard?

 

What would you suggest?

 

3 Answers

0 votes
answered by (26.7k points)

It looks like the CSS version is probably easier (especially either the first way they show a grid of hexes or the first "Addendum" example), because you can easily put text across the middle of the hex, including Twine/SugarCube links and such (you might want to add "text-align: center;" to that element's CSS though).  Give each middle DIV cell a unique ID, and then the <<replace>> macro can be used to update the text inside as well.

 

commented by (2.2k points)
This might come in handy: http://ondras.github.io/rot.js/manual/#hex/indexing (sorry, wrong branch).
commented by (320 points)
Thank you guys, I'll have a look into it!

I found out that I am also struggling with how to set up the game board itself, not only with making the cells hexagonal.

I thought the reasonable thing to do would be setting up the boards as an object and then "rendering" it by iterating through its content. But honestly, I do not know how to do that.

I put together an example how a single cell should behave: http://deutschdetektiv.net/exp/board.html

Players should be able to plant trees on each field. Once there are four trees on a field it is "protected" against future attacks. Protection can also be gained by using a spell.

Could you guys help me out on how one needs to structure a game board object and how to iterate through each cell? If we leave the hexagons out for the moment and go for square cells, I guess each cell needs a x and y position, the number of trees, two booleans for if it is protected by trees or spell.

Could somebody give me a hint on how to start?

Big thanks in advance!
0 votes
answered by (2.2k points)

I think it should be represented as 2-dimensional array of objects. Then x an y would be just indices in this array, and objects will contain amount of trees and if the spell used. Something like this:

var cells = [
    [ // first row
        { // first row first cell
            trees: 0,
            hasSpell: true,
        },
        { // first row second cell
            trees: 2,
            hasSpell: false,
        },
    ],
    [ // second row
        { // second row first cell
            trees: 1,
            hasSpell: false,
        },
        {
            trees: 0,
            hasSpell: true,
        },
    ],
];

Link I provided earlier should help you choose how exactly x and y are calculated from array indices (it depends on what you plan to do with this grid, do you need to find neighboring cells or drawing straight lines, etc).

0 votes
answered by (8.5k points)

Here's my simple solution. It assumes you have the map as a JavaScript object with attributes like "5x3" for 5th column, 3rd row (left-topmost hex has "1x1"), but can be adopted relatively easily to anything. In HTML, it produces "hexagon map cells" which look like this when empty:

<map-hex
    style="--x: 3; --y: 4;"
    data-row="4" data-col="3" data-cell="3x4">
        <span class="loc">3x4</span>
</map-hex>

The "data-row", "data-col" and "data-cell" values are for easier styling of specific cells via CSS attribute selectors, while the "--x" and "--y" style variables are so that it can be referenced in the CSS itself via the CSS var() function.

The widget creating the map might look a tiny bit scary at first, but it generally simply first creates the grid (as a CSS grid), then registers a JavaScript function which shifts half of the rows by a bit so they are "properly" staggered.

<<widget "Map">>
	<<set _size = $args[0]>>
	<<set _x = $args[1]>>
	<<set _y = $args[2]>>
	<<set _map = $args[3] || {}>>
	<<set _mapId = "map_" + Math.floor(Math.random() * 0x10000).toString(16)>>
	<<if _size && _x && _y && _x > 0 && _y > 0>>
	<<= "<html><div class='map' id='" + _mapId + "' style='--size: " + _size + "'>"
	  + Array.from({length: _x}).map((u, col) =>
	  	Array.from({length: _y}).map((u, row) => {
			var cell = (col + 1) + "x" + (row + 1);
			return "<map-hex style='--x: " + (col+1) + "; --y: " + (row+1) + ";' data-row='" + (row+1) + "' data-col='" + (col+1) + "' data-cell='" + cell + "'>" + (_map[cell] ? _map[cell] : "") + "<span class='loc'>" + cell + "</span></map-hex>"}
			).join("")).join("")
	  + "</div></html>">>
      <<script>>
	  var mapId = State.temporary.mapId;
	  jQuery(function() {
		  var map = jQuery("#" + mapId);
		  var size = map.get(0).style.getPropertyValue("--size").trim();
		  map.children("map-hex")
			  .filter((i, el) => 1 - Number(el.style.getPropertyValue("--y")) % 2)
			  .css("margin-left", "calc(" + size + " * 1.732 / 2)");
	  });
	  <</script>>
	<</if>>
<</widget>>

Calling it is then done with ...

<<Map 50px 7 7 $map>>

... for a 7x7 map with each cell being 100px wide (the 50px set is half the distance between the centre and the side of the hex). The "$map" parameter is optional.

The remainder is simply some CSS styling, with the hex shape being enforced via a "clip-path" property.

div.map {
    display: inline-grid;
    grid-auto-columns: calc(var(--size) * 1.732);
    grid-auto-rows: calc(var(--size) * 1.5);
    padding-bottom: calc(var(--size) * 0.5);
    padding-right: calc(var(--size) * 1.732 / 2);
    border: 2px solid white;
}

map-hex {
    box-sizing: border-box;
    position: relative;
    width: calc(var(--size) * 1.732);
    height: calc(var(--size) * 2);
    padding: calc(var(--size) * 0.5) 0;
    background: rgba(255,0,0,0.5);
    grid-area: var(--y) / var(--x);
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}

map-hex span.loc {
    position: absolute;
    display: block;
    bottom: calc(var(--size) * 0.5 - 1em);
    text-align: center;
    line-height: 1em;
    left: 0;
    right: 0;
    font-size: 80%;
    opacity: 0.5;
    z-index: -1;
}

Change (or remove) the "border" property of "div.map" and the "background" property of "map-hex" to suit your needs. They are here just so that you can see what's going on.

commented by (320 points)
Hi Akjosch,

thank you so much for the effort you  put into the solution and the helpful explanations.

I tried your coude and it is almost everything I expected. Could I bother you for one more thing? Could you give me an example of how $map would look like, just so I get an idea of how to adress individual cells within the map and alter their properties, like NotKostas  did in his example?

Thanks a lot in advance!
commented by (8.5k points)

The $map you give to the widget should look like this in the simplest case:

<<set $map = {
	"3x3": "Home",
	"4x5": "Dungeon",
	"2x1": "???",
}>>

If you want pure text, you probably want to add "text-align: center;" to the map-hex CSS rules.

You generally wouldn't - and shouldn't - put the raw game-relevant data into the map display widget, but first filter the relevant portions of the map to return something that represents it visually. For this example, I'll use a few images: A tree and a grassy hexagon. The remainder can be done with characters and numbers. I'll display a tree (along with a number if it's more than one) for each hex where there are any trees and link the whole hex to a passage named "Hex (X)x(Y)", which then can be used to dynamically link wherever they need to be.

The structure of the map itself is mostly the same:

<<set $map = {
	"3x3": {trees: 0, hasSpell: true},
	"4x5": {trees: 2, hasSpell: false},
	"2x1": {trees: 1, hasSpell: false},
	"5x2": {trees: 0, hasSpell: true},
}>>

I'd create a JavaScript function which would take the content of each hex cell and return the HTML structure needed to display it. Essentially, it creates three spans (with classes "tree", "treeAmount" and "spell") if the corresponding values are large enough respectively true. The "tree" span has the image of the tree linked, the "treeAmount" the number of trees if it is larger than 1. The remainder will be done via CSS.

setup.mapToDisplay = function(map) {
	return Object.keys(map || {})
		.reduce((val, key) => {
			var result = "";
			var hex = map[key];
			if(hex.trees > 0) {
				result += "<span class='tree'><img src='https://i.imgur.com/dQWof0s.png' /></span>";
				if(hex.trees > 1) {
					result += "<span class='treeAmount'>" + hex.trees + "</span>";
				}
			}
			if(hex.hasSpell) {
				result += "<span class='spell'></span>";
			}
			val[key] = result;
			return val;
		}, {});
};

Our map call looks slightly differently now.

<<Map 50px 7 7 setup.mapToDisplay($map)>>

And we have to write the CSS for the three new spans as well as add the background to the "map-hex" element and make the location text visible again, so the CSS is the one which changed most. In full.

div.map {
	display: inline-grid;
	grid-auto-columns: calc(var(--size) * 1.732);
    grid-auto-rows: calc(var(--size) * 1.5);
	padding-bottom: calc(var(--size) * 0.5);
	padding-right: calc(var(--size) * 1.732 / 2);
	border: 2px solid white;
}

map-hex {
    box-sizing: border-box;
	cursor: pointer;
	position: relative;
	width: calc(var(--size) * 1.732);
    height: calc(var(--size) * 2);
    padding: calc(var(--size) * 0.5) 0;
    background-image: url(https://i.imgur.com/PJdtzK4.png);
    background-size: cover;
	grid-area: var(--y) / var(--x);
	clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}

map-hex span.loc {
	position: absolute;
    display: block;
    bottom: calc(var(--size) * 0.5 - 1em);
    text-align: center;
    line-height: 1em;
    left: 0;
    right: 0;
    font-size: 80%;
	color: white;
	font-weight: bold;
    z-index: -1;
}

map-hex span.tree {
	position: absolute;
	top: calc(var(--size) / 4);
	left: calc(var(--size) / 2);
}

map-hex span.tree img {
	width: calc(var(--size) * 0.7);
}

map-hex span.treeAmount:before {
	content: "\d7";
}

map-hex span.treeAmount {
	position: absolute;
	left: var(--size);
	bottom: calc(var(--size) / 2);
	color: cyan;
	text-shadow: -2px -2px 1px black, 2px -2px 1px black, -2px 2px 1px black, 2px 2px 1px black;
}

map-hex span.spell:before {
	content: "\2606";
}

map-hex span.spell {
	position: absolute;
	left: calc(var(--size) * 0.5);
	top: calc(var(--size) * 0.7);
	color: purple;
	font-size: 200%;
	text-shadow: -2px -2px 1px violet, 2px -2px 1px violet, -2px 2px 1px violet, 2px 2px 1px violet;
}


You'll note I use "var(--size)" and "calc()" a lot. This way, the map stays somewhat scalable. Feel free to play with the values therein to position things however you like.

What's left is the "click on the map to get to the hex passage" part you wanted. It'd do it as "click" event handlers for the map cells, created at the same time the map is and in the same widget. This adds a total of four lines to the code.

<<widget "Map">>
	<<set _size = $args[0]>>
	<<set _x = $args[1]>>
	<<set _y = $args[2]>>
	<<set _map = $args[3] || {}>>
	<<set _mapId = "map_" + Math.floor(Math.random() * 0x10000).toString(16)>>
	<<if _size && _x && _y && _x > 0 && _y > 0>>
	<<= "<html><div class='map' id='" + _mapId + "' style='--size: " + _size + "'>"
	  + Array.from({length: _x}).map((u, col) =>
	  	Array.from({length: _y}).map((u, row) => {
			var cell = (col + 1) + "x" + (row + 1);
			return "<map-hex style='--x: " + (col+1) + "; --y: " + (row+1) + ";' data-row='" + (row+1) + "' data-col='" + (col+1) + "' data-cell='" + cell + "'>" + (_map[cell] ? _map[cell] : "") + "<span class='loc'>" + cell + "</span></map-hex>"}
			).join("")).join("")
	  + "</div></html>">>
      <<script>>
	  var mapId = State.temporary.mapId;
	  jQuery(function() {
		  var map = jQuery("#" + mapId);
		  var size = map.get(0).style.getPropertyValue("--size").trim();
		  map.children("map-hex")
		  	.on("click", function() {
				var target = this.getAttribute("data-cell").trim();
				Engine.play("Hex " + target);
			})
			.filter((i, el) => 1 - Number(el.style.getPropertyValue("--y")) % 2)
			.css("margin-left", "calc(" + size + " * 1.732 / 2)");
	  });
	  <</script>>
	<</if>>
<</widget>>

 

commented by (320 points)

Thank you so much again Akjosch, your code really helped me advance.

This is what I came up with for the moment, which reflects about half the game mechanic.

What I am struggling with is assigning classes to the individual hex fields.

Could you give me a hint what to add in your code

if(hex.trees > 0) {
				result += "<span class='tree'><img src='https://i.imgur.com/dQWof0s.png' /></span>";
				if(hex.trees > 1) {
					result += "<span class='treeAmount'>" + hex.trees + "</span>";
				}
			}

to add classes to the fields instead of additional elements? Like what would be the equivalent of the <<addclass>> macro?

My aim is to assign classes to fields which have "protected eq true" or "hasSpell eq true".

 

Best,

 

richVIE

Welcome to Twine Q&A, where you can ask questions and receive answers from other members of the community.

You can also find hints and information on Twine on the official wiki and the old forums archive.

See a spam question? Flag it instead of downvoting. A question flagged enough times will automatically be hidden while moderators review it.
...