"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()