"use strict"
function set_has(set, item) {
let a = 0
let b = set.length - 1
while (a <= b) {
let m = (a + b) >> 1
let x = set[m]
if (item < x)
b = m - 1
else if (item > x)
a = m + 1
else
return true
}
return false
}
function map_get(map, key, missing) {
let a = 0
let b = (map.length >> 1) - 1
while (a <= b) {
let m = (a + b) >> 1
let x = map[m<<1]
if (key < x)
b = m - 1
else if (key > x)
a = m + 1
else
return map[(m<<1)+1]
}
return missing
}
const FRANKS = "Franks"
const SARACENS = "Saracens"
const ENEMY = { Saracens: "Franks", Franks: "Saracens" }
const NOWHERE = 0
const DEAD = 1
const F_POOL = 2
const S_POOL = 3
const SEA = 4
const ENGLAND = 5
const FRANCE = 6
const GERMANIA = 7
const SHIELD_NAMES = {}
SHIELD_NAMES[town_index["Antioch"]] = "Bohemond, Templars, Turcopoles"
SHIELD_NAMES[town_index["Latakia"]] = "Bohemond"
SHIELD_NAMES[town_index["Sa\xf4ne"]] = "Josselin"
SHIELD_NAMES[town_index["Margat"]] = "Hospitallers"
SHIELD_NAMES[town_index["Krak"]] = "Hospitallers"
SHIELD_NAMES[town_index["Tartus"]] = "Templars"
SHIELD_NAMES[town_index["Tripoli"]] = "Bohemond, Raymond"
SHIELD_NAMES[town_index["Beirut"]] = "Turcopoles, King Guy"
SHIELD_NAMES[town_index["Sidon"]] = "Reynald (Sidon)"
SHIELD_NAMES[town_index["Beaufort"]] = "Reynald (Sidon)"
SHIELD_NAMES[town_index["Tyre"]] = "Conrad, King Guy"
SHIELD_NAMES[town_index["Acre"]] = "Turcopoles, Hospitallers, King Guy"
SHIELD_NAMES[town_index["Tiberias"]] = "Turcopoles, Raymond"
SHIELD_NAMES[town_index["Baisan"]] = "Hospitallers"
SHIELD_NAMES[town_index["Caesarea"]] = "Walter"
SHIELD_NAMES[town_index["Nablus"]] = "Balian"
SHIELD_NAMES[town_index["Amman"]] = "Templars"
SHIELD_NAMES[town_index["Jaffa"]] = "King Guy"
SHIELD_NAMES[town_index["Jerusalem"]] = "King Guy, Hospitallers, Templars"
SHIELD_NAMES[town_index["Ascalon"]] = "Balian, King Guy"
SHIELD_NAMES[town_index["Hebron"]] = "King Guy"
SHIELD_NAMES[town_index["Gaza"]] = "Templars"
SHIELD_NAMES[town_index["Kerak"]] = "Reynald (Kerak)"
SHIELD_NAMES[town_index["Egypt"]] = "Saladin, Qara-Qush, Yuzpah"
SHIELD_NAMES[town_index["Aleppo"]] = "Saladin, Sanjar, Zangi"
SHIELD_NAMES[town_index["Ashtera"]] = "Yazkuj"
SHIELD_NAMES[town_index["Artah"]] = "Sulaiman"
SHIELD_NAMES[town_index["Damascus"]] = "Saladin, Keukburi, Al Mashtub"
SHIELD_NAMES[town_index["Homs"]] = "Tuman, Shirkuh"
SHIELD_NAMES[town_index["Zerdana"]] = "Jurdik"
SHIELD_NAMES[town_index["Baalbek"]] = "Bahram"
SHIELD_NAMES[town_index["Hama"]] = "Taqi al Din"
SHIELD_NAMES[town_index["Banyas"]] = "Qaimaz"
const KINGDOM = {
"Syria": "Syria",
"Jerusalem": "Kingdom of Jerusalem",
"Antioch": "Principality of Antioch",
"Tripoli": "County of Tripoli",
}
const VICTORY_TOWNS = [
town_index["Aleppo"],
town_index["Damascus"],
town_index["Egypt"],
town_index["Antioch"],
town_index["Tripoli"],
town_index["Acre"],
town_index["Jerusalem"]
]
// :r !node tools/genroads.js
const ROADS_XY = {
"Germania / Aleppo": [975,64],
"Germania / St. Simeon": [297,100],
"Germania / Antioch": [512,54],
"Aleppo / Artah": [939,147],
"Aleppo / Zerdana": [1045,218],
"Artah / Zerdana": [941,227],
"Artah / Harim": [761,127],
"Zerdana / Hama": [1028,388],
"Zerdana / Albara": [942,342],
"Hama / Homs": [1133,571],
"Hama / Albara": [950,450],
"Hama / Monterrand": [964,554],
"Homs / Lacum": [980,780],
"Homs / Qaddas": [1156,780],
"Homs / Monterrand": [986,643],
"Homs / Krak": [927,726],
"Lacum / Qaddas": [1063,906],
"Lacum / Baalbek": [889,953],
"Lacum / Krak": [867,794],
"Qaddas / Damascus": [1170,1038],
"Baalbek / Anjar": [812,1082],
"Baalbek / Tripoli": [776,953],
"Anjar / Damascus": [869,1177],
"Anjar / Beirut": [635,1137],
"Anjar / Beaufort": [718,1198],
"Damascus / Banyas": [936,1267],
"Damascus / Ashtera": [1051,1339],
"Banyas / Ashtera": [907,1411],
"Banyas / Beaufort": [666,1389],
"Banyas / Tiberias": [730,1455],
"Ashtera / Ajlun": [1046,1491],
"Ajlun / Tiberias": [911,1554],
"Ajlun / Amman": [1035,1711],
"St. Simeon / Antioch": [412,208],
"Antioch / Harim": [600,127],
"Antioch / Kassab": [477,260],
"Harim / Shughur": [684,214],
"Kassab / Latakia": [406,388],
"Shughur / Saône": [635,374],
"Shughur / Albara": [704,345],
"Latakia / Saône": [524,432],
"Latakia / Margat": [492,488],
"Saône / Albara": [735,421],
"Margat / Tartus": [547,654],
"Monterrand / Krak": [851,664],
"Tartus / Krak": [689,722],
"Tartus / Tripoli": [669,786],
"Krak / Tripoli": [757,807],
"Tripoli / Botron": [571,933],
"Botron / Beirut": [567,1052],
"Beirut / Sidon": [503,1219],
"Sidon / Tyre": [460,1330],
"Sidon / Beaufort": [542,1324],
"Tyre / Beaufort": [546,1388],
"Tyre / Acre": [427,1492],
"Acre / Tiberias": [601,1572],
"Acre / Legio": [518,1653],
"Acre / Caesarea": [390,1658],
"Tiberias / Baisan": [759,1632],
"Legio / Baisan": [653,1685],
"Legio / Nablus": [619,1724],
"Baisan / Nablus": [675,1736],
"Baisan / Damiya": [811,1723],
"Caesarea / Nablus": [520,1777],
"Caesarea / Jaffa": [409,1837],
"Nablus / Damiya": [745,1797],
"Nablus / Jerusalem": [653,1883],
"Damiya / Amman": [934,1781],
"Damiya / Jericho": [845,1883],
"Amman / Jericho": [958,1892],
"Amman / Kerak": [1130,1973],
"Jaffa / Ramallah": [463,1932],
"Jaffa / Ascalon": [391,2004],
"Ramallah / Jerusalem": [586,1972],
"Ramallah / Ascalon": [448,2014],
"Jerusalem / Jericho": [789,1954],
"Jerusalem / Hebron": [677,2050],
"Jericho / Kerak": [952,1976],
"Ascalon / Lachish": [439,2128],
"Ascalon / Gaza": [330,2133],
"Lachish / Hebron": [556,2151],
"Lachish / Gaza": [430,2172],
"Hebron / Dimona": [658,2242],
"Hebron / Zoar": [779,2195],
"Kerak / Zoar": [998,2140],
"Gaza / Beersheba": [369,2265],
"Gaza / Egypt": [225,2266],
"Beersheba / Dimona": [539,2293],
"Beersheba / Egypt": [415,2312],
"Dimona / Zoar": [788,2291],
}
const ROADS_BG = {
"Germania / Aleppo": "hsl(36, 72%, 76%)",
"Germania / St. Simeon": "hsl(37, 79%, 77%)",
"Germania / Antioch": "hsl(61, 30%, 58%)",
"Aleppo / Artah": "hsl(37, 80%, 78%)",
"Aleppo / Zerdana": "hsl(36, 81%, 81%)",
"Artah / Zerdana": "hsl(37, 82%, 78%)",
"Artah / Harim": "hsl(35, 68%, 70%)",
"Zerdana / Hama": "hsl(36, 78%, 77%)",
"Zerdana / Albara": "hsl(36, 76%, 77%)",
"Hama / Homs": "hsl(36, 78%, 77%)",
"Hama / Albara": "hsl(39, 69%, 74%)",
"Hama / Monterrand": "hsl(34, 76%, 67%)",
"Homs / Lacum": "hsl(42, 57%, 70%)",
"Homs / Qaddas": "hsl(36, 82%, 78%)",
"Homs / Monterrand": "hsl(34, 78%, 70%)",
"Homs / Krak": "hsl(36, 74%, 72%)",
"Lacum / Qaddas": "hsl(36, 60%, 75%)",
"Lacum / Baalbek": "hsl(45, 45%, 68%)",
"Lacum / Krak": "hsl(38, 59%, 65%)",
"Qaddas / Damascus": "hsl(36, 80%, 78%)",
"Baalbek / Anjar": "hsl(48, 43%, 68%)",
"Baalbek / Tripoli": "hsl(56, 30%, 55%)",
"Anjar / Damascus": "hsl(42, 59%, 73%)",
"Anjar / Beirut": "hsl(51, 39%, 61%)",
"Anjar / Beaufort": "hsl(47, 43%, 67%)",
"Damascus / Banyas": "hsl(38, 58%, 78%)",
"Damascus / Ashtera": "hsl(38, 65%, 74%)",
"Banyas / Ashtera": "hsl(38, 56%, 75%)",
"Banyas / Beaufort": "hsl(53, 36%, 59%)",
"Banyas / Tiberias": "hsl(54, 35%, 62%)",
"Ashtera / Ajlun": "hsl(37, 70%, 75%)",
"Ajlun / Tiberias": "hsl(46, 48%, 68%)",
"Ajlun / Amman": "hsl(37, 85%, 80%)",
"St. Simeon / Antioch": "hsl(66, 32%, 58%)",
"Antioch / Harim": "hsl(46, 48%, 69%)",
"Antioch / Kassab": "hsl(66, 32%, 58%)",
"Harim / Shughur": "hsl(41, 61%, 74%)",
"Kassab / Latakia": "hsl(66, 32%, 58%)",
"Shughur / Saône": "hsl(47, 48%, 69%)",
"Shughur / Albara": "hsl(41, 53%, 72%)",
"Latakia / Saône": "hsl(60, 31%, 61%)",
"Latakia / Margat": "hsl(63, 32%, 59%)",
"Saône / Albara": "hsl(41, 60%, 74%)",
"Margat / Tartus": "hsl(50, 39%, 61%)",
"Monterrand / Krak": "hsl(40, 57%, 68%)",
"Tartus / Krak": "hsl(62, 31%, 59%)",
"Tartus / Tripoli": "hsl(55, 35%, 65%)",
"Krak / Tripoli": "hsl(52, 39%, 64%)",
"Tripoli / Botron": "hsl(72, 33%, 57%)",
"Botron / Beirut": "hsl(44, 43%, 57%)",
"Beirut / Sidon": "hsl(42, 57%, 79%)",
"Sidon / Tyre": "hsl(45, 42%, 71%)",
"Sidon / Beaufort": "hsl(70, 33%, 57%)",
"Tyre / Beaufort": "hsl(69, 32%, 56%)",
"Tyre / Acre": "hsl(58, 33%, 63%)",
"Acre / Tiberias": "hsl(56, 27%, 60%)",
"Acre / Legio": "hsl(61, 31%, 57%)",
"Acre / Caesarea": "hsl(52, 40%, 70%)",
"Tiberias / Baisan": "hsl(49, 44%, 67%)",
"Legio / Baisan": "hsl(56, 38%, 64%)",
"Legio / Nablus": "hsl(58, 32%, 60%)",
"Baisan / Nablus": "hsl(48, 50%, 71%)",
"Baisan / Damiya": "hsl(51, 40%, 65%)",
"Caesarea / Nablus": "hsl(59, 32%, 60%)",
"Caesarea / Jaffa": "hsl(51, 40%, 70%)",
"Nablus / Damiya": "hsl(39, 64%, 77%)",
"Nablus / Jerusalem": "hsl(45, 51%, 71%)",
"Damiya / Amman": "hsl(46, 47%, 69%)",
"Damiya / Jericho": "hsl(48, 48%, 69%)",
"Amman / Jericho": "hsl(40, 66%, 75%)",
"Amman / Kerak": "hsl(36, 72%, 79%)",
"Jaffa / Ramallah": "hsl(58, 34%, 61%)",
"Jaffa / Ascalon": "hsl(46, 44%, 69%)",
"Ramallah / Jerusalem": "hsl(47, 46%, 68%)",
"Ramallah / Ascalon": "hsl(49, 43%, 66%)",
"Jerusalem / Jericho": "hsl(41, 52%, 70%)",
"Jerusalem / Hebron": "hsl(41, 62%, 75%)",
"Jericho / Kerak": "hsl(40, 58%, 72%)",
"Ascalon / Lachish": "hsl(44, 57%, 72%)",
"Ascalon / Gaza": "hsl(45, 51%, 74%)",
"Lachish / Hebron": "hsl(42, 56%, 71%)",
"Lachish / Gaza": "hsl(41, 65%, 74%)",
"Hebron / Dimona": "hsl(40, 60%, 73%)",
"Hebron / Zoar": "hsl(38, 59%, 72%)",
"Kerak / Zoar": "hsl(38, 78%, 77%)",
"Gaza / Beersheba": "hsl(36, 82%, 76%)",
"Gaza / Egypt": "hsl(50, 44%, 63%)",
"Beersheba / Dimona": "hsl(36, 71%, 78%)",
"Beersheba / Egypt": "hsl(34, 80%, 71%)",
"Dimona / Zoar": "hsl(36, 78%, 77%)",
}
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")
}
let ui = {
cards: [],
card_backs: [],
towns: [],
roads: [],
blocks: [],
battle_menu: [],
battle_block: [],
present: new Set(),
}
function remember_position(e) {
if (e.classList.contains("show")) {
let rect = e.getBoundingClientRect()
e.my_parent = true
e.my_x = rect.x
e.my_y = rect.y
} else {
e.my_parent = false
e.my_x = 0
e.my_y = 0
}
}
function animate_position(e) {
if (e.parentElement) {
if (e.my_parent) {
let rect = e.getBoundingClientRect()
let dx = e.my_x - rect.x
let dy = e.my_y - rect.y
if (dx !== 0 || dy !== 0) {
e.animate(
[
{ transform: `translate(${dx}px, ${dy}px)`, },
{ transform: "translate(0, 0)", },
],
{ duration: 250, easing: "ease" }
)
}
} else {
e.animate(
[
{ opacity: 0 },
{ opacity: 1 }
],
{ duration: 250, easing: "ease" }
)
}
}
}
function on_focus_space_tip(x) {
ui.towns[x].classList.add("tip")
}
function on_blur_space_tip(x) {
ui.towns[x].classList.remove("tip")
}
function on_click_space_tip(x) {
scroll_into_view(ui.towns[x])
}
function sub_space_name(match, p1, offset, string) {
let x = p1 | 0
let n = TOWNS[x].name
return `${n}`
}
function on_log(text) {
let p = document.createElement("div")
if (text.match(/^>/)) {
text = text.substring(1)
p.className = "i"
}
text = text.replace(/&/g, "&")
text = text.replace(//g, ">")
text = text.replace(/\u2192 /g, "\u2192\xa0")
text = text.replace(/^([A-Z]):/, ' $1 ')
text = text.replace(/#(\d+)/g, sub_space_name)
if (text.match(/^\.h1 /))
p.className = 'h1', text = text.substring(4)
if (text.match(/^\.h2 F/))
p.className = 'h2 F', text = text.substring(4)
if (text.match(/^\.h2 S/))
p.className = 'h2 S', text = text.substring(4)
if (text.match(/^\.h3 /))
p.className = 'h3', text = text.substring(4)
if (text.match(/^\.h4 /))
p.className = 'h4', text = text.substring(4)
p.innerHTML = text
return p
}
function on_focus_town(evt) {
let where = evt.target.town
let text = TOWNS[where].name
if (where in SHIELD_NAMES)
text += " \u2014 " + SHIELD_NAMES[where]
let kingdom = KINGDOM[TOWNS[where].region]
if (kingdom)
text += " \u2014 " + kingdom
if (VICTORY_TOWNS.includes(where))
text += " \u2014 1 VP"
document.getElementById("status").textContent = text
}
function on_blur_town(evt) {
document.getElementById("status").textContent = ""
}
function on_click_town(evt) {
let where = evt.target.town
send_action('town', where) || send_action('townb', where)
}
const STEP_TEXT = [ 0, "I", "II", "III", "IIII" ]
const HEIR_TEXT = [ 0, '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075' ]
function block_name(who) { return BLOCKS[who].name; }
function block_home(who) { return BLOCKS[who].home; }
function block_owner(who) { return BLOCKS[who].owner; }
function on_focus_map_block(evt) {
let info = BLOCKS[evt.target.block]
let where = view.location[evt.target.block]
if ((info.owner === player || info.owner === "Assassins") && where !== S_POOL && where !== F_POOL) {
let text = info.name + " "
if (info.move)
text += info.move + "-"
text += STEP_TEXT[info.steps] + "-" + info.initiative + info.fire_power
document.getElementById("status").textContent = text
} else {
document.getElementById("status").textContent = info.owner
}
}
function on_blur_map_block(evt) {
document.getElementById("status").textContent = ""
}
function on_click_map_block(evt) {
let b = evt.target.block
if (!view.battle)
send_action('block', b)
}
function on_focus_battle_block(evt) {
let b = evt.target.block
let msg
if (!evt.target.classList.contains("known")) {
if (block_owner(b) === FRANKS)
msg = "Franks"
else if (block_owner(b) === SARACENS)
msg = "Saracens"
} else {
msg = block_name(b)
}
if (view.actions && view.actions.fire && view.actions.fire.includes(b))
msg = "Fire with " + msg
else if (view.actions && view.actions.storm && view.actions.storm.includes(b))
msg = "Storm with " + msg
else if (view.actions && view.actions.sally && view.actions.sally.includes(b))
msg = "Sally with " + msg
else if (view.actions && view.actions.withdraw && view.actions.withdraw.includes(b))
msg = "Withdraw with " + msg
else if (view.actions && view.actions.hit && view.actions.hit.includes(b))
msg = "Take hit on " + msg
document.getElementById("status").textContent = msg
}
function on_blur_battle_block(evt) {
document.getElementById("status").textContent = ""
}
function on_click_battle_block(evt) {
let b = evt.target.block
send_action('block', b)
}
function on_focus_fire(evt) {
document.getElementById("status").textContent =
"Fire with " + block_name(evt.target.block)
}
function on_focus_retreat(evt) {
if (view.battle.storming.includes(evt.target.block))
document.getElementById("status").textContent =
"Withdraw with " + block_name(evt.target.block)
else
document.getElementById("status").textContent =
"Retreat with " + block_name(evt.target.block)
}
function on_focus_harry(evt) {
document.getElementById("status").textContent =
"Harry with " + block_name(evt.target.block)
}
function on_focus_charge(evt) {
document.getElementById("status").textContent =
"Charge with " + block_name(evt.target.block)
}
function on_focus_withdraw(evt) {
document.getElementById("status").textContent =
"Withdraw with " + block_name(evt.target.block)
}
function on_focus_storm(evt) {
document.getElementById("status").textContent =
"Storm with " + block_name(evt.target.block)
}
function on_focus_sally(evt) {
document.getElementById("status").textContent =
"Sally with " + block_name(evt.target.block)
}
function on_focus_hit(evt) {
document.getElementById("status").textContent =
"Take hit on " + block_name(evt.target.block)
}
function on_blur_battle_button(evt) {
document.getElementById("status").textContent = ""
}
function on_click_hit(evt) { send_action('hit', evt.target.block); }
function on_click_fire(evt) { send_action('fire', evt.target.block); }
function on_click_retreat(evt) { send_action('retreat', evt.target.block); }
function on_click_charge(evt) { send_action('charge', evt.target.block); }
function on_click_harry(evt) { send_action('harry', evt.target.block); }
function on_click_withdraw(evt) { send_action('withdraw', evt.target.block); }
function on_click_storm(evt) { send_action('storm', evt.target.block); }
function on_click_sally(evt) { send_action('sally', evt.target.block); }
function on_click_card(evt) {
let c = evt.target.id.split("+")[1] | 0
send_action('play', c)
}
function build_battle_button(menu, b, c, click, enter, img_src) {
let img = new Image()
img.draggable = false
img.classList.add("action")
img.classList.add(c)
img.setAttribute("src", img_src)
img.addEventListener("click", click)
img.addEventListener("mouseenter", enter)
img.addEventListener("mouseleave", on_blur_battle_button)
img.block = b
menu.appendChild(img)
}
function battle_block_class_name(block) {
return `block block_${block.image} ${block.owner}`
}
function build_battle_block(b, block) {
let element = document.createElement("div")
element.className = battle_block_class_name(block)
element.addEventListener("mouseenter", on_focus_battle_block)
element.addEventListener("mouseleave", on_blur_battle_block)
element.addEventListener("click", on_click_battle_block)
element.block = b
ui.battle_block[b] = element
let menu_list = document.createElement("div")
menu_list.className = "battle_menu_list"
build_battle_button(menu_list, b, "hit",
on_click_hit, on_focus_hit,
"/images/cross-mark.svg")
build_battle_button(menu_list, b, "charge",
on_click_charge, on_focus_charge,
"/images/mounted-knight.svg")
build_battle_button(menu_list, b, "fire",
on_click_fire, on_focus_fire,
"/images/pointy-sword.svg")
build_battle_button(menu_list, b, "harry",
on_click_harry, on_focus_harry,
"/images/arrow-flights.svg")
build_battle_button(menu_list, b, "retreat",
on_click_retreat, on_focus_retreat,
"/images/flying-flag.svg")
build_battle_button(menu_list, b, "withdraw",
on_click_withdraw, on_focus_withdraw,
"/images/stone-tower.svg")
build_battle_button(menu_list, b, "storm",
on_click_storm, on_focus_storm,
"/images/siege-tower.svg")
build_battle_button(menu_list, b, "sally",
on_click_sally, on_focus_sally,
"/images/doorway.svg")
let menu = document.createElement("div")
menu.className = "battle_menu"
menu.appendChild(element)
menu.appendChild(menu_list)
menu.block = b
ui.battle_menu[b] = menu
}
function build_map_block(b, block) {
let element = document.createElement("div")
element.classList.add("block")
element.classList.add("known")
element.classList.add(BLOCKS[b].owner)
element.classList.add("block_" + block.image)
element.addEventListener("mouseenter", on_focus_map_block)
element.addEventListener("mouseleave", on_blur_map_block)
element.addEventListener("click", on_click_map_block)
element.block = b
return element
}
function build_town(t, town) {
let element = document.createElement("div")
element.town = t
element.classList.add("town")
element.addEventListener("mouseenter", on_focus_town)
element.addEventListener("mouseleave", on_blur_town)
element.addEventListener("click", on_click_town)
ui.towns_element.appendChild(element)
return element
}
function build_map() {
let element
ui.blocks_element = document.getElementById("blocks")
ui.offmap_element = document.getElementById("offmap")
ui.towns_element = document.getElementById("towns")
for (let c = 1; c <= 27; ++c) {
ui.cards[c] = document.getElementById("card+"+c)
ui.cards[c].addEventListener("click", on_click_card)
}
for (let c = 1; c <= 6; ++c)
ui.card_backs[c] = document.getElementById("back+"+c)
for (let t = SEA; t < TOWNS.length; ++t) {
let town = TOWNS[t]
let name = town.name
if (t === SEA) {
element = document.getElementById("svgmap").getElementById("sea")
element.town = SEA
element.addEventListener("mouseenter", on_focus_town)
element.addEventListener("mouseleave", on_blur_town)
element.addEventListener("click", on_click_town)
ui.towns[t] = element
} else {
element = ui.towns[t] = build_town(t, town)
let xo = Math.round(element.offsetWidth/2)
let yo = Math.round(element.offsetHeight/2)
element.style.left = (town.layout.x - xo) + "px"
element.style.top = (town.layout.y - yo) + "px"
}
}
for (let b = 0; b < BLOCKS.length; ++b) {
let block = BLOCKS[b]
ui.blocks[b] = build_map_block(b, block)
build_battle_block(b, block)
}
for (let name in ROADS_XY) {
let [x, y] = ROADS_XY[name]
let [a, b] = name.split(" / ")
let id = town_index[a] * 100 + town_index[b]
let e = document.createElement("div")
e.my_id = id
e.my_bgnd = ROADS_BG[name]
e.my_show = "road " + ROADS[id]
e.className = "hide"
e.style.left = (x - 12) + "px"
e.style.top = (y - 12) + "px"
ui.roads.push(e)
document.getElementById("roads").appendChild(e)
}
}
function update_steps(b, steps, element) {
element.classList.remove("r0")
element.classList.remove("r1")
element.classList.remove("r2")
element.classList.remove("r3")
element.classList.add("r"+(BLOCKS[b].steps - steps))
}
function layout_blocks(location, secret, known) {
if (label_layout === 'stack')
document.getElementById("map").classList.add("stack_layout")
else
document.getElementById("map").classList.remove("stack_layout")
if (label_layout === 'spread' ||
(location === S_POOL || location === F_POOL || location === DEAD ||
location === ENGLAND || location === FRANCE || location === GERMANIA))
layout_blocks_spread(location, secret, known)
else
layout_blocks_stacked(location, secret, known)
}
function layout_blocks_spread(town, north, south) {
let wrap = TOWNS[town].layout.wrap
let rows = []
if ((north.length > wrap || south.length > wrap) || (north.length + south.length <= 3)) {
north = north.concat(south)
south = []
}
function wrap_row(input) {
while (input.length > wrap) {
rows.push(input.slice(0, wrap))
input = input.slice(wrap)
}
if (input.length > 0)
rows.push(input)
}
wrap_row(north)
wrap_row(south)
if (TOWNS[town].layout.minor > 0.5)
rows.reverse()
for (let r = 0; r < rows.length; ++r) {
let cols = rows[r]
for (let c = 0; c < cols.length; ++c)
position_block(town, r, rows.length, c, cols.length, cols[c])
}
}
function position_block(town, row, n_rows, col, n_cols, element) {
let space = TOWNS[town]
let block_size = 60+6
let padding = 4
if (town === ENGLAND || town === FRANCE || town === GERMANIA)
padding = 21
let offset = block_size + padding
let row_size = (n_rows-1) * offset
let col_size = (n_cols-1) * offset
let x = space.layout.x
let y = space.layout.y
if (space.layout.axis === 'X') {
x -= col_size * space.layout.major
y -= row_size * space.layout.minor
x += col * offset
y += row * offset
} else {
y -= col_size * space.layout.major
x -= row_size * space.layout.minor
y += col * offset
x += row * offset
}
element.style.left = ((x - block_size/2)|0)+"px"
element.style.top = ((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 = space.layout.x + (i - c) * 16 + k * 12
let y = space.layout.y + (i - c) * 16 - k * 12
element.style.left = ((x - block_size/2)|0)+"px"
element.style.top = ((y - block_size/2)|0)+"px"
}
function show_block(element) {
if (element.parentElement !== ui.blocks_element)
ui.blocks_element.appendChild(element)
}
function hide_block(element) {
if (element.parentElement !== ui.offmap_element)
ui.offmap_element.appendChild(element)
}
function is_known_block(info, who, town) {
if (view.game_over && player === 'Observer')
return true
if (town === DEAD)
return true
if ((town === S_POOL || town === F_POOL) && who !== view.who)
return false
if (info.owner === player || info.owner === "Assassins" || who === view.assassinate)
return true
return false
}
function is_in_battle(b) {
if (view.battle) {
if (view.battle.FR.includes(b)) return true
if (view.battle.FC.includes(b)) return true
if (view.battle.FF.includes(b)) return true
if (view.battle.SR.includes(b)) return true
if (view.battle.SC.includes(b)) return true
if (view.battle.SF.includes(b)) return true
}
return false
}
function update_map() {
let layout = {}
document.getElementById("frank_vp").textContent = view.f_vp + " VP"
document.getElementById("saracen_vp").textContent = view.s_vp + " VP"
document.getElementById("timeline").className = "year_" + view.year
if (view.turn < 1)
document.getElementById("turn_info").textContent =
"Year " + view.year
else if (view.turn < 6)
document.getElementById("turn_info").textContent =
"Turn " + view.turn + " of Year " + view.year
else
document.getElementById("turn_info").textContent =
"Winter Turn of Year " + view.year
for (let t = 0; t < TOWNS.length; ++t)
layout[t] = { north: [], south: [] }
let is_battle_open = document.getElementById("battle").getAttribute("open") !== null
for (let b = 0; b < BLOCKS.length; ++b) {
let info = BLOCKS[b]
let element = ui.blocks[b]
let town = view.location[b]
let moved = (set_has(view.moved, b) && b !== view.who) ? " moved" : ""
let battle = (is_battle_open && is_in_battle(b)) ? " battle" : ""
if (town === DEAD) {
moved = " moved"
}
if (town === NOWHERE) {
town = DEAD
moved = " removed"
}
if (is_known_block(info, b, town)) {
let image = " block_" + info.image
let steps = " r" + (info.steps - view.steps[b])
let known = " known"
element.classList = info.owner + known + " block" + image + steps + moved + battle
} else {
let besieging = ""
if (view.sieges[town] === info.owner) {
if (view.winter_campaign === town)
besieging = " winter_campaign"
else
besieging = " besieging"
}
let jihad = ""
if (view.jihad === town && info.owner === view.p1)
jihad = " jihad"
element.classList = info.owner + " block" + moved + besieging + jihad + battle
}
if (town !== DEAD) {
if (info.owner === FRANKS)
layout[town].north.push(element)
else
layout[town].south.push(element)
}
show_block(element)
}
for (let b = 0; b < BLOCKS.length; ++b) {
let info = BLOCKS[b]
let element = ui.blocks[b]
let town = view.location[b]
if (town === DEAD) {
if (info.owner === FRANKS)
layout[F_POOL].north.unshift(element)
else
layout[S_POOL].south.unshift(element)
}
}
for (let b = 0; b < BLOCKS.length; ++b) {
let info = BLOCKS[b]
let element = ui.blocks[b]
let town = view.location[b]
if (town === NOWHERE) {
if (info.owner === FRANKS)
layout[F_POOL].north.unshift(element)
else
layout[S_POOL].south.unshift(element)
}
}
for (let t = 0; t < TOWNS.length; ++t)
layout_blocks(t, layout[t].north, layout[t].south)
for (let t = SEA; t < TOWNS.length; ++t) {
if (ui.towns[t]) {
ui.towns[t].classList.remove('highlight')
ui.towns[t].classList.remove('muster')
ui.towns[t].classList.remove('bad')
ui.towns[t].classList.remove('battle')
}
}
if (view.actions && view.actions.town) {
for (let t of view.actions.town) {
ui.towns[t].classList.add('highlight')
}
}
if (view.actions && view.actions.townb) {
for (let t of view.actions.townb) {
ui.towns[t].classList.add('highlight')
ui.towns[t].classList.add('bad')
}
}
if (view.muster)
ui.towns[view.muster].classList.add('muster')
if (!view.battle) {
if (view.actions && view.actions.block)
for (let b of view.actions.block)
ui.blocks[b].classList.add('highlight')
} else {
ui.towns[view.battle.town].classList.add('battle')
}
if (view.who >= 0 && !view.battle)
ui.blocks[view.who].classList.add('selected')
for (let b of view.castle)
ui.blocks[b].classList.add('castle')
for (let e of ui.roads) {
let u = map_get(view.last_used, e.my_id, 0)
let n = map_get(view.road_limit, e.my_id, "")
if (view.main_road && set_has(view.main_road, e.my_id))
n += "*"
switch (u) {
case 1:
e.style.backgroundColor = null
e.className = "road Franks"
e.textContent = n
break
case 2:
e.style.backgroundColor = null
e.className = "road Saracens"
e.textContent = n
break
case 0:
if (n) {
e.style.backgroundColor = e.my_bgnd
e.className = e.my_show
e.textContent = n
} else {
e.style.backgroundColor = null
e.className = "hide"
e.textContent = ""
}
break
}
}
}
function update_card_display(element, card, prior_card) {
if (!card && !prior_card) {
element.className = "show card card_back"
} else if (prior_card) {
element.className = "show card prior " + CARDS[prior_card].image
} else {
element.className = "show card " + CARDS[card].image
}
}
function update_cards() {
update_card_display(document.getElementById("frank_card"), view.f_card, view.prior_f_card)
update_card_display(document.getElementById("saracen_card"), view.s_card, view.prior_s_card)
for (let c = 1; c <= 27; ++c) {
let element = ui.cards[c]
if (view.hand.includes(c)) {
element.classList.add("show")
if (view.actions && view.actions.play) {
if (view.actions.play.includes(c)) {
element.classList.add("enabled")
element.classList.remove("disabled")
} else {
element.classList.remove("enabled")
element.classList.add("disabled")
}
} else {
element.classList.remove("enabled")
element.classList.remove("disabled")
}
} else {
element.classList.remove("show")
}
}
let n = view.hand.length
for (let c = 1; c <= 6; ++c)
if (c <= n && player === 'Observer')
ui.card_backs[c].classList.add("show")
else
ui.card_backs[c].classList.remove("show")
}
function compare_blocks(a, b) {
let aa = BLOCKS[a].initiative + BLOCKS[a].fire_power
let bb = BLOCKS[b].initiative + BLOCKS[b].fire_power
if (aa === bb)
return (a < b) ? -1 : (a > b) ? 1 : 0
return (aa < bb) ? -1 : (aa > bb) ? 1 : 0
}
function insert_battle_block(root, node, block) {
for (let i = 0; i < root.children.length; ++i) {
let prev = root.children[i]
if (compare_blocks(prev.block, block) > 0) {
root.insertBefore(node, prev)
return
}
}
root.appendChild(node)
}
function show_battle() {
let box = document.getElementById("battle")
let space = TOWNS[view.battle.town].layout
let rotate = false
// reset position
box.classList.add("show")
box.style.top = null
box.style.left = null
box.style.bottom = null
box.style.right = null
box.setAttribute("open", true)
// calculate size
let w = box.clientWidth
let h = box.clientHeight
/* LANDSCAPE selector */
if (window.matchMedia("(min-width: 2000px)").matches) {
let x = space.y - w / 2
if (x < 80)
x = 80
if (x > 2475 - w - 80)
x = 2475 - w - 80
let y = space.x + h + 120
if (y > 1275)
y = space.x - 120
box.style.top = x + "px"
box.style.left = y + "px"
} else {
// place above town if possible, else below
let y = space.y - h - 120
if (y < 80)
y = space.y + 120
// place centered above town if possible, clamp to edges
let x = space.x - w / 2
if (x < 80)
x = 80
if (x > 1275 - w - 80)
x = 1275 - w - 80
box.style.top = y + "px"
box.style.left = x + "px"
}
scroll_into_view_if_needed(box)
}
function update_battle() {
function fill_cell(name, list, show) {
let cell = document.getElementById(name)
ui.present.clear()
for (let block of list) {
ui.present.add(block)
if (!cell.contains(ui.battle_menu[block]))
insert_battle_block(cell, ui.battle_menu[block], block)
ui.battle_menu[block].className = "battle_menu"
if (view.actions && view.actions.fire && view.actions.fire.includes(block))
ui.battle_menu[block].classList.add('fire')
if (view.actions && view.actions.retreat && view.actions.retreat.includes(block))
ui.battle_menu[block].classList.add('retreat')
if (view.actions && view.actions.harry && view.actions.harry.includes(block))
ui.battle_menu[block].classList.add('harry')
if (view.actions && view.actions.charge && view.actions.charge.includes(block))
ui.battle_menu[block].classList.add('charge')
if (view.actions && view.actions.withdraw && view.actions.withdraw.includes(block))
ui.battle_menu[block].classList.add('withdraw')
if (view.actions && view.actions.storm && view.actions.storm.includes(block))
ui.battle_menu[block].classList.add('storm')
if (view.actions && view.actions.sally && view.actions.sally.includes(block))
ui.battle_menu[block].classList.add('sally')
if (view.actions && view.actions.charge && view.actions.charge.includes(block))
ui.battle_menu[block].classList.add('charge')
if (view.actions && view.actions.treachery && view.actions.treachery.includes(block))
ui.battle_menu[block].classList.add('treachery')
if (view.actions && view.actions.hit && view.actions.hit.includes(block))
ui.battle_menu[block].classList.add('hit')
let class_name = battle_block_class_name(BLOCKS[block])
if (view.actions && view.actions.block && view.actions.block.includes(block))
class_name += " highlight"
if (set_has(view.moved, block))
class_name += " moved"
if (block === view.who)
class_name += " selected"
if (block === view.battle.halfhit)
class_name += " halfhit"
if (view.jihad === view.battle.town && block_owner(block) === view.p1)
class_name += " jihad"
if (view.battle.sallying.includes(block))
show = true
if (view.battle.storming.includes(block))
show = true
if (show || block_owner(block) === player) {
class_name += " known"
ui.battle_block[block].className = class_name
update_steps(block, view.steps[block], ui.battle_block[block], false)
} else {
ui.battle_block[block].className = class_name
}
}
for (let b = 0; b < BLOCKS.length; ++b) {
if (!ui.present.has(b)) {
if (cell.contains(ui.battle_menu[b]))
cell.removeChild(ui.battle_menu[b])
}
}
}
if (player === FRANKS) {
fill_cell("ER", view.battle.SR, false)
fill_cell("EC", view.battle.SC, view.battle.show_castle)
fill_cell("EF", view.battle.SF, view.battle.show_field)
fill_cell("FF", view.battle.FF, view.battle.show_field)
fill_cell("FC", view.battle.FC, view.battle.show_castle)
fill_cell("FR", view.battle.FR, false)
document.getElementById("FC").className = "c" + view.battle.FCS
document.getElementById("EC").className = "c" + view.battle.SCS
} else {
fill_cell("ER", view.battle.FR, false)
fill_cell("EC", view.battle.FC, view.battle.show_castle)
fill_cell("EF", view.battle.FF, view.battle.show_field)
fill_cell("FF", view.battle.SF, view.battle.show_field)
fill_cell("FC", view.battle.SC, view.battle.show_castle)
fill_cell("FR", view.battle.SR, false)
document.getElementById("EC").className = "c" + view.battle.FCS
document.getElementById("FC").className = "c" + view.battle.SCS
}
}
let flash_timer = 0
function start_flash() {
let element = document.getElementById("battle_message")
let tick = true
if (flash_timer)
return
flash_timer = setInterval(function () {
if (!view.flash_next) {
element.textContent = view.battle ? view.battle.flash : ""
clearInterval(flash_timer)
flash_timer = 0
} else {
element.textContent = tick ? view.battle.flash : view.flash_next
tick = !tick
}
}, 1000)
}
function on_update() {
action_button("eliminate", "Eliminate")
action_button("winter_campaign", "Winter campaign")
action_button("sea_move", "Sea move")
action_button("end_sea_move", "End sea move")
action_button("group_move", "Group move")
action_button("end_group_move", "End group move")
action_button("muster", "Muster")
action_button("end_muster", "End muster")
action_button("end_retreat", "End retreat")
action_button("end_regroup", "End regroup")
action_button("end_move_phase", "End move phase")
action_button("assign", "Assign hits")
action_button("pass", "Pass")
action_button("next", "Next")
action_button("undo", "Undo")
document.getElementById("frank_vp").textContent = view.f_vp
document.getElementById("saracen_vp").textContent = view.s_vp
for (let c = 1; c <= 27; ++c)
remember_position(ui.cards[c])
update_cards()
update_map()
if (view.battle) {
document.getElementById("battle_header").textContent = view.battle.title
document.getElementById("battle_message").textContent = view.battle.flash
if (view.flash_next)
start_flash()
update_battle()
if (!document.getElementById("battle").classList.contains("show"))
show_battle()
} else {
document.getElementById("battle").classList.remove("show")
}
for (let c = 1; c <= 27; ++c)
animate_position(ui.cards[c])
}
window.addEventListener("resize", function (evt) {
if (view && view.battle)
setTimeout(function () {
document.getElementById("battle").classList.remove("show")
show_battle()
}, 1)
})
document.getElementById("battle").addEventListener("toggle", on_update)
build_map()