From 4605ba9ebd7a63bbf2cc3bc14d72fb84d3dc1b64 Mon Sep 17 00:00:00 2001
From: Tor Andersson <tor@ccxvii.net>
Date: Sun, 20 Jun 2021 00:19:57 +0200
Subject: crusader: Stacked layout.

---
 data.js   |   2 +-
 play.html |   6 +++-
 rules.js  | 102 +++++++++++++++++++++++++++++++++++++-------------------------
 ui.js     |  60 ++++++++++++++++++++++++++++++++++--
 4 files changed, 124 insertions(+), 46 deletions(-)

diff --git a/data.js b/data.js
index 7b89d3f..dd77abc 100644
--- a/data.js
+++ b/data.js
@@ -154,7 +154,7 @@ const PORTS = [];
 	frank(33, "Leopold",		"Germania",	2,	3,	"B3",	"Crusaders");
 
 	frank(11, "Richard",		"England",	3,	4,	"B4",	"Crusaders");
-	frank(21, "Robert",		"England",	2,	3,	"B3",	"Crusaders");
+	frank(21, "Robert",		"Normandy",	2,	3,	"B3",	"Crusaders");
 	frank(31, "Crossbows",		"Aquitaine",	2,	3,	"A2",	"Crusaders");
 
 	frank(12, "Philippe",		"France",	2,	4,	"B3",	"Crusaders");
diff --git a/play.html b/play.html
index 80b931b..a2d760f 100644
--- a/play.html
+++ b/play.html
@@ -133,7 +133,7 @@ body.shift .block.known:hover {
 .map .block { position: absolute; z-index: 2; }
 .map .block.highlight { z-index: 3; }
 .map .block.selected { z-index: 4; }
-.map .block:hover { z-index: 5; }
+.map .block.known:hover { z-index: 5; }
 
 .block.highlight { cursor: pointer; box-shadow: 0px 0px 4px 1px white; }
 
@@ -303,9 +303,13 @@ body.shift .block.known:hover {
 			<div class="menu_title"><img src="/images/cog.svg"></div>
 			<div class="menu_popup">
 				<div class="menu_item" onclick="toggle_fullscreen()">Fullscreen</div>
+				<div class="menu_separator"></div>
 				<div class="menu_item" onclick="wide_map()">Wide Map</div>
 				<div class="menu_item" onclick="tall_map()">Tall Map</div>
 				<div class="menu_separator"></div>
+				<div class="menu_item" onclick="set_spread_layout()">Spread blocks</div>
+				<div class="menu_item" onclick="set_stack_layout()">Stack blocks</div>
+				<div class="menu_separator"></div>
 				<div class="menu_item" onclick="window.open('info/notes.html', '_blank')">Notes</div>
 				<div class="menu_item" onclick="window.open('info/rules.html', '_blank')">Rules</div>
 				<div class="menu_item" onclick="window.open('info/cards.html', '_blank')">Cards</div>
diff --git a/rules.js b/rules.js
index 4cc8f8a..b8e85fd 100644
--- a/rules.js
+++ b/rules.js
@@ -3,6 +3,9 @@
 // TODO: frank seat adjustment at setup
 // TODO: saladin seat adjustment at setup
 
+// TODO: optional rule - iron bridge
+// TODO: optional rule - force marches
+
 exports.scenarios = [
 	"Third Crusade"
 ];
@@ -15,6 +18,7 @@ const ASSASSINS = "Assassins";
 const ENEMY = { Frank: "Saracen", Saracen: "Frank" };
 const OBSERVER = "Observer";
 const BOTH = "Both";
+const DEAD = "Dead";
 const F_POOL = "F. Pool";
 const S_POOL = "S. Pool";
 
@@ -185,6 +189,7 @@ function block_type(who) {
 
 function block_home(who) {
 	let home = BLOCKS[who].home;
+	if (home == "Normandy") return "England";
 	if (home == "Aquitaine") return "England";
 	if (home == "Bourgogne") return "France";
 	if (home == "Flanders") return "France";
@@ -222,8 +227,13 @@ function block_max_steps(who) {
 	return BLOCKS[who].steps;
 }
 
+function is_saladin_family(who) {
+	return who == "Saladin" || who == "Al Adil" || who == "Al Aziz" || who == "Al Afdal" || who == "Al Zahir";
+}
+
 function is_block_on_map(who) {
-	return game.location[who] && game.location[who] != F_POOL && game.location[who] != S_POOL;
+	let location = game.location[who];
+	return location && location != DEAD && location != F_POOL && location != S_POOL;
 }
 
 function can_activate(who) {
@@ -273,7 +283,7 @@ function count_enemy_excluding_reserves(where) {
 	let count = 0;
 	for (let b in BLOCKS)
 		if (game.location[b] == where && block_owner(b) == p)
-			if (!game.reserves.includes(b))
+			if (!is_battle_reserve(b))
 				++count;
 	return count;
 }
@@ -312,7 +322,7 @@ function count_pinned(where) {
 	let count = 0;
 	for (let b in BLOCKS)
 		if (game.location[b] == where && block_owner(b) == game.active)
-			if (!game.reserves.includes(b))
+			if (!is_battle_reserve(b))
 				++count;
 	return count;
 }
@@ -487,18 +497,18 @@ function can_muster_to(muster) {
 }
 
 function is_battle_reserve(who) {
-	return game.reserves.includes(who);
+	return game.reserves1.includes(who) || game.reserves2.includes(who);
 }
 
 function is_attacker(who) {
 	if (game.location[who] == game.where && block_owner(who) == game.attacker[game.where])
-		return !game.reserves.includes(who);
+		return !is_battle_reserve(who);
 	return false;
 }
 
 function is_defender(who) {
 	if (game.location[who] == game.where && block_owner(who) != game.attacker[game.where])
-		return !game.reserves.includes(who);
+		return !is_battle_reserve(who);
 	return false;
 }
 
@@ -510,7 +520,10 @@ function disband(who) {
 
 function eliminate_block(who) {
 	log(block_name(who) + " is eliminated.");
-	game.location[who] = null;
+	if (is_saladin_family(who) || block_type(who) == 'crusaders' || block_type(who) == 'military_orders')
+		game.location[who] = null; // permanently eliminated
+	else
+		game.location[who] = DEAD; // into to the pool next year
 	game.steps[who] = block_max_steps(who);
 }
 
@@ -570,7 +583,8 @@ function start_game_turn() {
 	reset_road_limits();
 	game.last_used = {};
 	game.attacker = {};
-	game.reserves = [];
+	game.reserves1 = [];
+	game.reserves2 = [];
 	game.moved = {};
 
 	goto_card_phase();
@@ -708,7 +722,7 @@ function goto_event_card(event) {
 	end_player_turn();
 }
 
-// ACTION PHASE
+// MOVE PHASE
 
 function move_block(who, from, to) {
 	game.location[who] = to;
@@ -724,7 +738,7 @@ function move_block(who, from, to) {
 			// Attacker main attack or reinforcements
 			if (game.attacker[to] == game.active) {
 				if (game.main_road[to] != from) {
-					game.reserves.push(who);
+					game.reserves1.push(who);
 					return RESERVE_MARK_1;
 				}
 				return ATTACK_MARK;
@@ -735,10 +749,10 @@ function move_block(who, from, to) {
 				game.main_road[to] = from;
 
 			if (game.main_road[to] == from) {
-				game.reserves.push(who);
+				game.reserves1.push(who);
 				return RESERVE_MARK_1;
 			} else {
-				game.reserves.push(who);
+				game.reserves2.push(who);
 				return RESERVE_MARK_2;
 			}
 		}
@@ -845,6 +859,23 @@ states.group_move_to = {
 	undo: pop_undo
 }
 
+function end_move() {
+	if (game.distance > 0) {
+		let to = game.location[game.who];
+		if (!game.activated.includes(game.origin)) {
+			logp("activates " + game.origin + ".");
+			game.activated.push(game.origin);
+			game.moves --;
+		}
+		game.moved[game.who] = true;
+	}
+	log_move_end();
+	game.who = null;
+	game.distance = 0;
+	game.origin = null;
+	game.state = 'group_move';
+}
+
 function can_sea_move_anywhere() {
 	if (game.moves > 0) {
 		for (let b in BLOCKS)
@@ -974,19 +1005,6 @@ states.muster_who = {
 	undo: pop_undo,
 }
 
-function end_muster_move() {
-	let muster = game.where;
-	log_move_end();
-	game.moved[game.who] = true;
-	game.who = null;
-	game.state = 'muster_who';
-	if (!game.mustered.includes(muster)) {
-		logp("musters to " + muster + ".");
-		game.mustered.push(muster);
-		--game.moves;
-	}
-}
-
 states.muster_move_1 = {
 	prompt: function (view, current) {
 		if (is_inactive_player(current))
@@ -1088,26 +1106,23 @@ states.muster_move_3 = {
 	undo: pop_undo,
 }
 
-function end_move() {
-	if (game.distance > 0) {
-		let to = game.location[game.who];
-		if (!game.activated.includes(game.origin)) {
-			logp("activates " + game.origin + ".");
-			game.activated.push(game.origin);
-			game.moves --;
-		}
-		game.moved[game.who] = true;
-	}
+function end_muster_move() {
+	let muster = game.where;
 	log_move_end();
+	game.moved[game.who] = true;
 	game.who = null;
-	game.distance = 0;
-	game.origin = null;
-	game.state = 'group_move';
+	game.state = 'muster_who';
+	if (!game.mustered.includes(muster)) {
+		logp("musters to " + muster + ".");
+		game.mustered.push(muster);
+		--game.moves;
+	}
 }
 
 // BATTLE PHASE
 
 function goto_battle_phase() {
+	game.moved = {};
 	if (have_contested_towns()) {
 		game.active = game.p1;
 		game.state = 'battle_phase';
@@ -1166,10 +1181,12 @@ function end_battle() {
 }
 
 function bring_on_reserves(round) {
-	// TODO: defender reserves in round 3...
 	for (let b in BLOCKS) {
 		if (game.location[b] == game.where) {
-			remove_from_array(game.reserves, b);
+			if (round == 2)
+				remove_from_array(game.reserves1, b);
+			else if (round == 3)
+				remove_from_array(game.reserves2, b);
 		}
 	}
 }
@@ -1631,7 +1648,8 @@ exports.setup = function (scenario, players) {
 		moved: {},
 		moves: 0,
 		prompt: null,
-		reserves: [],
+		reserves1: [],
+		reserves2: [],
 		show_cards: false,
 		steps: {},
 		who: null,
@@ -1697,6 +1715,8 @@ exports.view = function(state, current) {
 		let a = game.location[b];
 		if (!a)
 			continue;
+		if (a == DEAD)
+			continue;
 		if (a == F_POOL) // && current != FRANK)
 			continue;
 		if (a == S_POOL) // && current != SARACEN)
diff --git a/ui.js b/ui.js
index aa48ca1..b78e413 100644
--- a/ui.js
+++ b/ui.js
@@ -5,6 +5,21 @@ const SARACEN = "Saracen";
 const ASSASSINS = "Assassins";
 const ENEMY = { Saracen: "Frank", Frank: "Saracen" }
 const POOL = "Pool";
+const DEAD = "Dead";
+
+let label_layout = window.localStorage['crusader-rex/label-layout'] || 'spread';
+
+function set_spread_layout() {
+	label_layout = 'spread';
+	window.localStorage['crusader-rex/label-layout'] = label_layout;
+	update_map();
+}
+
+function set_stack_layout() {
+	label_layout = 'stack';
+	window.localStorage['crusader-rex/label-layout'] = label_layout;
+	update_map();
+}
 
 function toggle_blocks() {
 	document.getElementById("map").classList.toggle("hide_blocks");
@@ -249,8 +264,9 @@ function build_known_block(b, block) {
 	return element;
 }
 
-function build_secret_block(b, block) {
+function build_secret_block(b, block, secret_index) {
 	let element = document.createElement("div");
+	element.secret_index = secret_index;
 	element.classList.add("block");
 	element.classList.add("secret");
 	element.classList.add(BLOCKS[b].owner);
@@ -344,7 +360,8 @@ function build_map() {
 		let block = BLOCKS[b];
 		build_battle_block(b, block);
 		ui.known[b] = build_known_block(b, block);
-		ui.secret[BLOCKS[b].owner].offmap.push(build_secret_block(b, block));
+		let e = build_secret_block(b, block, ui.secret[BLOCKS[b].owner].offmap.length);
+		ui.secret[BLOCKS[b].owner].offmap.push(e);
 	}
 
 	update_map_layout();
@@ -357,7 +374,14 @@ function update_steps(b, steps, element) {
 	element.classList.add("r"+(BLOCKS[b].steps - steps));
 }
 
-function layout_blocks(town, secret, known) {
+function layout_blocks(location, secret, known) {
+	if (label_layout == 'spread' || (location == POOL || location == DEAD))
+		layout_blocks_spread(location, secret, known);
+	else
+		layout_blocks_stacked(location, secret, known);
+}
+
+function layout_blocks_spread(town, secret, known) {
 	let wrap = TOWNS[town].wrap;
 	let s = secret.length;
 	let k = known.length;
@@ -424,6 +448,33 @@ function position_block(town, row, n_rows, col, n_cols, element) {
 	element.style.top = ((flip_y(x,y) - block_size/2)|0)+"px";
 }
 
+function layout_blocks_stacked(location, secret, known) {
+	let s = secret.length;
+	let k = known.length;
+	let both = secret.length > 0 && known.length > 0;
+	let i = 0;
+	while (secret.length > 0)
+		position_block_stacked(location, i++, (s-1)/2, both ? 1 : 0, secret.shift());
+	i = 0;
+	while (known.length > 0)
+		position_block_stacked(location, i++, (k-1)/2, 0, known.shift());
+}
+
+function position_block_stacked(location, i, c, k, element) {
+	let space = TOWNS[location];
+	let block_size = 60+6;
+	let x, y;
+	if (map_orientation == 'tall') {
+		x = space.x + (i - c) * 12 + k * 12;
+		y = space.y + (i - c) * 16 - k * 8;
+	} else {
+		x = space.x - (i - c) * 12 + k * 12;
+		y = space.y + (i - c) * 16 + k * 8;
+	}
+	element.style.left = ((flip_x(x,y) - block_size/2)|0)+"px";
+	element.style.top = ((flip_y(x,y) - block_size/2)|0)+"px";
+}
+
 function show_block(element) {
 	if (element.parentElement != ui.blocks_element)
 		ui.blocks_element.appendChild(element);
@@ -507,6 +558,9 @@ function update_map() {
 				n = game.secret[color][town][0];
 				m = game.secret[color][town][1];
 			}
+			// Preserve block stacking order, but lets the user track block identities...
+			if (label_layout == 'stack')
+				ui.secret[color][town].sort((a,b) => b.secret_index - a.secret_index);
 			for (let element of ui.secret[color][town]) {
 				if (i++ < n - m)
 					element.classList.remove("moved");
-- 
cgit v1.2.3