"use strict" const THE_13_COLONIES = [ "NH", "NY", "MA", "CT", "RI", "PA", "NJ", "MD", "DE", "VA", "NC", "SC", "GA" ] const ALL_COLONIES = THE_13_COLONIES.concat([ "CA" ]) const AMERICAN_GENERALS = [ "Arnold", "Gates", "Greene", "Lafayette", "Lee", "Lincoln", "Washington" ] const CONTINENTAL_CONGRESS_DISPERSED = "Continental Congress Dispersed" const BRITISH = "British" const AMERICAN = "American" const BLOCKADE_ZONES = [ "Sea1", "Sea2", "Sea3", "Sea4", "Sea5", "Sea6", "Sea7" ] let ui = { cards: {}, spaces: {}, generals: {}, control: {}, blockade: {}, cu: [], } function on_log(text) { let p = document.createElement("div") text = text.replace(/&/g, "&") text = text.replace(//g, ">") text = text.replace(/\[(\d+)([^\]]*)\]/, '$1$2') if (text.match(/^\.h1 /)) { p.className = "h1" text = text.substring(4) } else if (text.match(/^\.h2.american /)) { p.className = "h2 american" text = text.substring(13) } else if (text.match(/^\.h2.british /)) { p.className = "h2 british" text = text.substring(12) } p.innerHTML = text return p } function clearList(container) { while (container.firstChild) container.removeChild(container.firstChild) } function onHoverCard(X) { let c = CARDS[X.id.split("+")[1]] document.getElementById("status").textContent = JSON.stringify(c) } function onFocusNode(evt) { let space = SPACES[evt.target.id] document.getElementById("status").textContent = space.name } function onBlurNode(evt) { let space = SPACES[evt.target.id] document.getElementById("status").textContent = "" } function clear_group(name) { let container = document.getElementById(name) while (container.firstChild) container.removeChild(container.firstChild) } function build_marker(container, id, x, y, w, h, classList) { let e = document.createElement("div") e.foo = { w: w, h: h } e.classList.add("marker") for (let c of classList) e.classList.add(c) e.setAttribute("id", id) e.style.left = ((x - w / 2) | 0) + "px" e.style.top = ((y - h / 2) | 0) + "px" document.getElementById(container).appendChild(e) return e } function update_marker(e, space) { let box = SPACES[space] || BOXES[space] e.style.left = ((box.x - e.foo.w / 2) | 0) + "px" e.style.top = ((box.y - e.foo.h / 2) | 0) + "px" } function update_marker_xy(e, x, y) { e.style.left = ((x - e.foo.w / 2) | 0) + "px" e.style.top = ((y - e.foo.h / 2) | 0) + "px" } function build_map() { function buildNode(type, x, y, colony) { let e = document.createElement("div") let w, h e.classList.add("space") e.classList.add(type) if (colony && colony != "SEA") e.classList.add("colony_" + colony) if (type === "regular-space") { w = 66 h = 66 } else if (type === "winter-quarters") { w = 66 h = 66 } else if (type === "fortified-port") { w = 54 h = 54 } else if (type === "blockade") { w = 96 h = 80 } else { w = 60 h = 60 } let b = 7 w -= 2 h -= 2 e.style.left = x - w / 2 - b + "px" e.style.top = y - h / 2 - b + "px" e.style.width = w + "px" e.style.height = h + "px" return e } for (let name in SPACES) { let space = SPACES[name] if (space.colony != null) { let e = buildNode(space.type, space.x, space.y, SPACES[name].colony) e.setAttribute("id", name) e.addEventListener("mouseenter", onFocusNode) e.addEventListener("mouseleave", onBlurNode) e.addEventListener("click", on_space) ui.spaces[name] = e document.getElementById("spaces").appendChild(e) build_marker("pc", name + "-pc", space.x, space.y, 67, 58.5, [ "pc" ]) } } for (let zone of BLOCKADE_ZONES) { let e = buildNode("blockade", BOXES[zone].x, BOXES[zone].y, "SEA") e.setAttribute("id", zone) e.addEventListener("click", on_space) document.getElementById("spaces").appendChild(e) ui.blockade[zone] = e } ui.turn = build_marker("markers", "GameTurn", BOXES["Game Turn 1775"].x, BOXES["Game Turn 1775"].y, 56.5, 56.5, [ "turn", ]) ui.french_alliance = build_marker( "markers", "FrenchAlliance", BOXES["French Alliance Track 0"].x, BOXES["French Alliance Track 0"].y, 56.5, 56.5, [ "french-alliance" ] ) ui.french_navy = build_marker( "markers", "FrenchNavy", BOXES["French Reinforcements"].x - 130 / 2 - 10, BOXES["French Reinforcements"].y - 32, 126 / 2, 252 / 2, [ "french-navy" ] ) ui.congress = build_marker( "markers", "Congress", SPACES["Philadelphia"].x, SPACES["Philadelphia"].y, 113 / 2, 113 / 2, [ "congress" ] ) for (let c in COLONIES) { ui.control[c] = build_marker("markers", "control_" + c, BOXES[c].x, BOXES[c].y, 38 + 8, 38 + 8, [ "control" ]) } for (let c = 1; c <= 110; ++c) { ui.cards[c] = document.getElementById("card+" + c) ui.cards[c].addEventListener("click", on_card) } for (let g in GENERALS) { let color = GENERALS[g].owner.toLowerCase() ui.generals[g] = build_marker("generals", g, 0, 0, 126 / 2, 252 / 2, [ "general", color, g, "offmap" ]) ui.generals[g].addEventListener("click", on_general) } } function update_units() { const unitW = 130 / 2 const unitH = 263 / 2 const generalX = -22 const generalY = 22 const cuX = 20 const cuY = 10 update_marker(ui.turn, "Game Turn " + view.year) if (view.regulars) ui.turn.classList.remove("no-regulars") else ui.turn.classList.add("no-regulars") update_marker(ui.congress, view.congress) update_marker(ui.french_alliance, "French Alliance Track " + view.french_alliance) if (view.european_war) ui.french_alliance.classList.add("european-war") else ui.french_alliance.classList.remove("european-war") if (view.french_navy == "French Reinforcements") { let x = BOXES["French Reinforcements"].x - 130 / 2 - 10 let y = BOXES["French Reinforcements"].y - 32 let w = 126 / 2 let h = 252 / 2 ui.french_navy.style.left = ((x - w / 2) | 0) + "px" ui.french_navy.style.top = ((y - h / 2) | 0) + "px" } else { update_marker(ui.french_navy, view.french_navy) } for (let space in SPACES) { let space_pc = view.pc[space] let e = document.getElementById(space + "-pc") if (e) { if (space_pc === BRITISH) { e.classList.remove("american") e.classList.add("british") } else if (space_pc === AMERICAN) { e.classList.add("american") e.classList.remove("british") } else { e.classList.remove("american") e.classList.remove("british") } } } for (let c in COLONIES) { let control = 0 for (let space of COLONIES[c]) { if (view.pc[space] == BRITISH) --control else if (view.pc[space] == AMERICAN) ++control } if (control < 0) ui.control[c].className = "marker control british" else if (control > 0) ui.control[c].className = "marker control american" else ui.control[c].className = "marker control" } let offset = {} for (let g in GENERALS) { let e = ui.generals[g] let unit = view.generals[g] let space = SPACES[unit.location] || BOXES[unit.location] if (space) { let o = offset[space.name] | 0 update_marker_xy(e, space.x + o * generalX, space.y + o * generalY - 32) e.classList.remove("offmap") offset[space.name] = ++o } else { e.classList.add("offmap") } if (view.who == g) e.classList.add("selected") else e.classList.remove("selected") } // TODO: reuse CU elements offset = {} clear_group("cu") for (let i = 0; i < view.cu.length; ++i) { let cu = view.cu[i] let space = SPACES[cu.location] || BOXES[cu.location] let o = offset[space.name] | 0 let x = space.x + o * cuX let y = space.y + o * cuY let e = build_marker("cu", "cu" + i, x, y, 122 / 2, 122 / 2, [ "cu", cu.owner.toLowerCase() ]) e.textContent = cu.count offset[space.name] = ++o } } build_map() function player_info(player, nc, nq) { let info = "" if (player == AMERICAN) { if (view.pennsylvania_and_new_jersey_line_mutinies || view.congress == CONTINENTAL_CONGRESS_DISPERSED) info += "\u{1f6ab} " } if (nq > 0) info += nq + "\u{231b} " info += nc + "\u{1f3b4}" return info } function on_update() { let e roles.American.stat.textContent = player_info(AMERICAN, view.a_cards, view.a_queue) roles.British.stat.textContent = player_info(BRITISH, view.b_cards, view.b_queue) if (!view.last_played) document.getElementById("last_played").className = "card show card_back" else document.getElementById("last_played").className = "card show card_" + view.last_played action_button("pickup_british_cu", "Pick up British CU") action_button("pickup_american_cu", "Pick up American CU") action_button("pickup_french_cu", "Pick up French CU") action_button("drop_british_cu", "Drop off British CU") action_button("drop_american_cu", "Drop off American CU") action_button("drop_french_cu", "Drop off French CU") action_button("british_first", "British") action_button("american_first", "American") action_button("surrender", "Surrender") action_button("pass", "Next") action_button("undo", "Undo") e = document.getElementById("war_ends") e.classList.remove("year_1779") e.classList.remove("year_1780") e.classList.remove("year_1781") e.classList.remove("year_1782") e.classList.remove("year_1783") if (view.war_ends) e.classList.add("year_" + view.war_ends) e = document.getElementById("played_british_reinforcements") e.classList.remove("ops_1") e.classList.remove("ops_2") e.classList.remove("ops_3") e.classList.add("ops_" + view.played_british_reinforcements) e = document.getElementById("played_american_reinforcements_1") e.classList.remove("ops_1") e.classList.remove("ops_2") e.classList.remove("ops_3") if (view.played_american_reinforcements.length >= 1) e.classList.add("ops_" + view.played_american_reinforcements[0]) e = document.getElementById("played_american_reinforcements_2") e.classList.remove("ops_1") e.classList.remove("ops_2") e.classList.remove("ops_3") if (view.played_american_reinforcements.length >= 2) e.classList.add("ops_" + view.played_american_reinforcements[1]) let cards = view.hand for (let c = 1; c <= 110; ++c) { ui.cards[c].classList.remove("enabled") if (cards && cards.includes(c)) ui.cards[c].classList.add("show") else ui.cards[c].classList.remove("show") } for (let space in SPACES) ui.spaces[space].classList.remove("enabled") for (let general in GENERALS) ui.generals[general].classList.remove("enabled") for (let zone of BLOCKADE_ZONES) ui.blockade[zone].classList.remove("enabled") update_units() if (player != view.active) return for (let action of Object.keys(view.actions)) { let args = view.actions[action] switch (action) { case "card_play_event": case "card_discard_event": case "card_campaign": case "card_ops_general": case "card_ops_pc": case "card_ops_reinforcements": case "card_ops_queue": case "card_battle_play": case "card_battle_discard": case "exchange_for_discard": for (let card of args) ui.cards[card].classList.add("enabled") break case "remove_cu": // TODO: target CU not space? case "move": case "sea_move": case "place_continental_congress": case "place_reinforcements": case "place_american_pc": case "place_british_pc": case "remove_pc": case "flip_pc": for (let space of args) ui.spaces[space].classList.add("enabled") break case "select_general": for (let general of args) ui.generals[general].classList.add("enabled") break case "place_navy": for (let zone of args) ui.blockade[zone].classList.add("enabled") } } } function is_action(action, card) { return view.actions && view.actions[action] && view.actions[action].includes(card) } function show_popup_menu(evt, menu_id, target_id, title) { let menu = document.getElementById(menu_id) let show = false for (let item of menu.querySelectorAll("li")) { let action = item.dataset.action if (action) { if (is_action(action, target_id)) { show = true item.classList.add("action") item.classList.remove("disabled") item.onclick = function () { send_action(action, target_id) hide_popup_menu() evt.stopPropagation() } } else { item.classList.remove("action") item.classList.add("disabled") item.onclick = null } } } if (show) { menu.onmouseleave = hide_popup_menu menu.style.display = "block" if (title) { let item = menu.querySelector("li.title") if (item) { item.onclick = hide_popup_menu item.textContent = title } } let w = menu.clientWidth let h = menu.clientHeight let x = Math.max(5, Math.min(evt.clientX - w / 2, window.innerWidth - w - 5)) let y = Math.max(5, Math.min(evt.clientY - 12, window.innerHeight - h - 40)) menu.style.left = x + "px" menu.style.top = y + "px" evt.stopPropagation() } else { menu.style.display = "none" } } function hide_popup_menu() { document.getElementById("popup").style.display = "none" } function on_card(evt) { if (view.actions) { let c = evt.target.id.split("+")[1] | 0 show_popup_menu(evt, "popup", c, CARDS[c].title) } } function get_action_from_arg(x) { for (let action of Object.keys(view.actions)) { let args = view.actions[action] if (Array.isArray(args) && args.includes(x)) return action } return null } function on_space(evt) { if (view.actions) { let space = evt.target.id let action = get_action_from_arg(space) if (action) send_action(action, space) } } function on_general(evt) { if (view.actions) { let general = evt.target.id let action = get_action_from_arg(general) if (action) send_action(action, general) } } function toggle_markers() { document.querySelector("#map").classList.toggle("hide_markers") }