diff options
Diffstat (limited to 'play.js')
-rw-r--r-- | play.js | 801 |
1 files changed, 801 insertions, 0 deletions
@@ -0,0 +1,801 @@ +"use strict"; + +const FRANKS = "Franks"; +const SARACENS = "Saracens"; +const ASSASSINS = "Assassins"; +const ENEMY = { Saracens: "Franks", Franks: "Saracens" } +const DEAD = "Dead"; +const F_POOL = "FP"; +const S_POOL = "SP"; +const ENGLAND = "England"; +const FRANCE = "France"; +const GERMANIA = "Germania"; + +const KINGDOM = { + "Syria": "Syria", + "Jerusalem": "Kingdom of Jerusalem", + "Antioch": "Principality of Antioch", + "Tripoli": "County of Tripoli", +}; + +const VICTORY_TOWNS = [ + "Aleppo", "Damascus", "Egypt", + "Antioch", "Tripoli", "Acre", "Jerusalem" +]; + +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: {}, + blocks: {}, + battle_menu: {}, + battle_block: {}, + present: new Set(), +} + +create_log_entry = function (text) { + let p = document.createElement("div"); + text = text.replace(/&/g, "&"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + + text = text.replace(/\u2192 /g, "\u2192\xa0"); + + text = text.replace(/^([A-Z]):/, '<span class="$1"> $1 </span>'); + + if (text.match(/^~ .* ~$/)) + p.className = 'br', text = text.substring(2, text.length-2); + else if (text.match(/^Start Frank turn/)) + p.className = 'F'; + else if (text.match(/^Start Saracen turn/)) + p.className = 'S'; + else if (text.match(/^Start /)) + p.className = 'st', text = text.replace(/\.$/, ""); + else if (text.match(/^(Battle in)/)) + p.className = 'bs'; + + if (text.match(/^Start /)) + text = text.substring(6); + + p.innerHTML = text; + return p; +} + +function on_focus_town(evt) { + let where = evt.target.town; + let text = where; + if (where in SHIELDS) + text += " \u2014 " + SHIELDS[where].join(", "); + 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); +} + +const STEP_TEXT = [ 0, "I", "II", "III", "IIII" ]; +const HEIR_TEXT = [ 0, '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075' ]; + +function block_name(who) { return who; } +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 = game.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.combat; + 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 (!game.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 (game.actions && game.actions.fire && game.actions.fire.includes(b)) + msg = "Fire with " + msg; + else if (game.actions && game.actions.storm && game.actions.storm.includes(b)) + msg = "Storm with " + msg; + else if (game.actions && game.actions.sally && game.actions.sally.includes(b)) + msg = "Sally with " + msg; + else if (game.actions && game.actions.withdraw && game.actions.withdraw.includes(b)) + msg = "Withdraw with " + msg; + else if (game.actions && game.actions.hit && game.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 (game.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 name in TOWNS) { + let town = TOWNS[name]; + if (name === F_POOL || name === S_POOL || name === DEAD) + continue; + if (name === "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[name] = element; + } else { + element = ui.towns[name] = build_town(name, town); + let xo = Math.round(element.offsetWidth/2); + let yo = Math.round(element.offsetHeight/2); + element.style.left = (town.x - xo) + "px"; + element.style.top = (town.y - yo) + "px"; + } + } + + for (let b in BLOCKS) { + let block = BLOCKS[b]; + ui.blocks[b] = build_map_block(b, block); + build_battle_block(b, block); + } +} + +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].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.x; + let y = space.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.x + (i - c) * 16 + k * 12; + let y = space.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 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) { + if (game_over) + return true; + if (info.owner === player || info.owner === ASSASSINS || who === game.assassinate) + return true; + let town = game.location[who]; + if (town === DEAD) + return true; + return false; +} + +function update_map() { + let layout = {}; + + document.getElementById("frank_vp").textContent = game.f_vp + " VP"; + document.getElementById("saracen_vp").textContent = game.s_vp + " VP"; + document.getElementById("timeline").className = "year_" + game.year; + if (game.turn < 1) + document.getElementById("turn_info").textContent = + "Year " + game.year; + else if (game.turn < 6) + document.getElementById("turn_info").textContent = + "Turn " + game.turn + " of Year " + game.year; + else + document.getElementById("turn_info").textContent = + "Winter Turn of Year " + game.year; + + for (let town in TOWNS) + layout[town] = { north: [], south: [] }; + + for (let b in game.location) { + let info = BLOCKS[b]; + let element = ui.blocks[b]; + let town = game.location[b]; + if (town in TOWNS) { + let moved = game.moved[b] ? " moved" : ""; + if (town === DEAD) + moved = " moved"; + if (is_known_block(info, b)) { + let image = " block_" + info.image; + let steps = " r" + (info.steps - game.steps[b]); + let known = " known"; + if ((town === S_POOL || town === F_POOL) && b !== game.who && !game_over) + known = ""; + element.classList = info.owner + known + " block" + image + steps + moved; + } else { + let besieging = ""; + if (game.sieges[town] === info.owner) { + if (game.winter_campaign === town) + besieging = " winter_campaign"; + else + besieging = " besieging"; + } + let jihad = ""; + if (game.jihad === town && info.owner === game.p1) + jihad = " jihad"; + element.classList = info.owner + " block" + moved + besieging + jihad; + } + if (town !== DEAD) { + if (info.owner === FRANKS) + layout[town].north.push(element); + else + layout[town].south.push(element); + } + show_block(element); + } else { + hide_block(element); + } + } + + for (let b in game.location) { + let info = BLOCKS[b]; + let element = ui.blocks[b]; + let town = game.location[b]; + if (town === DEAD) { + if (info.owner === FRANKS) + layout[F_POOL].north.unshift(element); + else + layout[S_POOL].south.unshift(element); + } + } + + for (let town in TOWNS) + layout_blocks(town, layout[town].north, layout[town].south); + + for (let where in TOWNS) { + if (ui.towns[where]) { + ui.towns[where].classList.remove('highlight'); + ui.towns[where].classList.remove('muster'); + } + } + if (game.actions && game.actions.town) + for (let where of game.actions.town) + ui.towns[where].classList.add('highlight'); + if (game.muster) + ui.towns[game.muster].classList.add('muster'); + + if (!game.battle) { + if (game.actions && game.actions.block) + for (let b of game.actions.block) + ui.blocks[b].classList.add('highlight'); + } + if (game.who && !game.battle) + ui.blocks[game.who].classList.add('selected'); + for (let b of game.castle) + ui.blocks[b].classList.add('castle'); +} + +function update_card_display(element, card, prior_card) { + if (!card && !prior_card) { + element.className = "small_card card_back"; + } else if (prior_card) { + element.className = "small_card prior " + CARDS[prior_card].image; + } else { + element.className = "small_card " + CARDS[card].image; + } +} + +function update_cards() { + update_card_display(document.getElementById("frank_card"), game.f_card, game.prior_f_card); + update_card_display(document.getElementById("saracen_card"), game.s_card, game.prior_s_card); + + for (let c = 1; c <= 27; ++c) { + let element = ui.cards[c]; + if (game.hand.includes(c)) { + element.classList.add("show"); + if (game.actions && game.actions.play) { + if (game.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"); + } + } + + if (player === 'Observer') { + let n = game.hand.length; + for (let c = 1; c <= 6; ++c) + if (c <= n) + ui.card_backs[c].classList.add("show"); + else + ui.card_backs[c].classList.remove("show"); + } +} + +function compare_blocks(a, b) { + let aa = BLOCKS[a].combat; + let bb = BLOCKS[b].combat; + 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 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 (game.actions && game.actions.fire && game.actions.fire.includes(block)) + ui.battle_menu[block].classList.add('fire'); + if (game.actions && game.actions.retreat && game.actions.retreat.includes(block)) + ui.battle_menu[block].classList.add('retreat'); + if (game.actions && game.actions.harry && game.actions.harry.includes(block)) + ui.battle_menu[block].classList.add('harry'); + if (game.actions && game.actions.charge && game.actions.charge.includes(block)) + ui.battle_menu[block].classList.add('charge'); + if (game.actions && game.actions.withdraw && game.actions.withdraw.includes(block)) + ui.battle_menu[block].classList.add('withdraw'); + if (game.actions && game.actions.storm && game.actions.storm.includes(block)) + ui.battle_menu[block].classList.add('storm'); + if (game.actions && game.actions.sally && game.actions.sally.includes(block)) + ui.battle_menu[block].classList.add('sally'); + if (game.actions && game.actions.charge && game.actions.charge.includes(block)) + ui.battle_menu[block].classList.add('charge'); + if (game.actions && game.actions.treachery && game.actions.treachery.includes(block)) + ui.battle_menu[block].classList.add('treachery'); + if (game.actions && game.actions.hit && game.actions.hit.includes(block)) + ui.battle_menu[block].classList.add('hit'); + + let class_name = battle_block_class_name(BLOCKS[block]); + if (game.actions && game.actions.block && game.actions.block.includes(block)) + class_name += " highlight"; + if (game.moved[block]) + class_name += " moved"; + if (block === game.who) + class_name += " selected"; + if (block === game.battle.halfhit) + class_name += " halfhit"; + if (game.jihad === game.battle.town && block_owner(block) === game.p1) + class_name += " jihad"; + + if (game.battle.sallying.includes(block)) + show = true; + if (game.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, game.steps[block], ui.battle_block[block], false); + } else { + ui.battle_block[block].className = class_name; + } + + } + + for (let b in BLOCKS) { + if (!ui.present.has(b)) { + if (cell.contains(ui.battle_menu[b])) + cell.removeChild(ui.battle_menu[b]); + } + } + } + + if (player === FRANKS) { + fill_cell("ER", game.battle.SR, false); + fill_cell("EC", game.battle.SC, game.battle.show_castle); + fill_cell("EF", game.battle.SF, game.battle.show_field); + fill_cell("FF", game.battle.FF, game.battle.show_field); + fill_cell("FC", game.battle.FC, game.battle.show_castle); + fill_cell("FR", game.battle.FR, false); + document.getElementById("FC").className = "c" + game.battle.FCS; + document.getElementById("EC").className = "c" + game.battle.SCS; + } else { + fill_cell("ER", game.battle.FR, false); + fill_cell("EC", game.battle.FC, game.battle.show_castle); + fill_cell("EF", game.battle.FF, game.battle.show_field); + fill_cell("FF", game.battle.SF, game.battle.show_field); + fill_cell("FC", game.battle.SC, game.battle.show_castle); + fill_cell("FR", game.battle.SR, false); + document.getElementById("EC").className = "c" + game.battle.FCS; + document.getElementById("FC").className = "c" + game.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 (!game.flash_next) { + element.textContent = game.battle ? game.battle.flash : ""; + clearInterval(flash_timer); + flash_timer = 0; + } else { + element.textContent = tick ? game.battle.flash : game.flash_next; + tick = !tick; + } + }, 1000); +} + +function on_update() { + action_button("eliminate_button", "Eliminate"); + action_button("winter_campaign_button", "Winter campaign"); + action_button("sea_move_button", "Sea move"); + action_button("end_sea_move_button", "End sea move"); + action_button("group_move_button", "Group move"); + action_button("end_group_move_button", "End group move"); + action_button("muster_button", "Muster"); + action_button("end_muster_button", "End muster"); + action_button("end_retreat_button", "End retreat"); + action_button("end_regroup_button", "End regroup"); + action_button("end_move_phase_button", "End move phase"); + action_button("pass", "Pass"); + action_button("next", "Next"); + action_button("undo", "Undo"); + + document.getElementById("frank_vp").textContent = game.f_vp; + document.getElementById("saracen_vp").textContent = game.s_vp; + + update_cards(); + update_map(); + + if (game.battle) { + document.getElementById("battle_header").textContent = game.battle.title; + document.getElementById("battle_message").textContent = game.battle.flash; + if (game.flash_next) + start_flash(); + document.getElementById("battle").classList.add("show"); + update_battle(); + } else { + document.getElementById("battle").classList.remove("show"); + } +} + +build_map(); + +drag_element_with_mouse("#battle", "#battle_header"); +scroll_with_middle_mouse("main", 3); +init_map_zoom(); +init_shift_zoom(); +init_client(["Franks", "Saracens"]); |