"use strict" /* global view, data, roles, send_action, action_button */ /* COMMON */ 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 } /* DATA */ const PC_BRITISH = 1 const PC_AMERICAN = 2 const CU_BRITISH_SHIFT = 2 const CU_AMERICAN_SHIFT = 8 const CU_FRENCH_SHIFT = 14 const CU_BRITISH_MASK = 63 << CU_BRITISH_SHIFT const CU_AMERICAN_MASK = 63 << CU_AMERICAN_SHIFT const CU_FRENCH_MASK = 7 << CU_FRENCH_SHIFT const CARDS = data.cards const P_BRITAIN = "Britain" const P_AMERICA = "America" const F_REGULARS = 2 const F_EUROPEAN_WAR = 4 const F_MUTINIES = 16 const general_count = data.generals.length const CONTINENTAL_CONGRESS_DISPERSED = data.space_index["Continental Congress Dispersed"] const FRENCH_REINFORCEMENTS = data.space_index["French Reinforcements"] const NOWHERE = data.spaces.length /* ACCESSORS */ function get_space_pc(space) { return view.cupc[space] & 3 } function has_british_pc(space) { return get_space_pc(space) === PC_BRITISH } function has_american_pc(space) { return get_space_pc(space) === PC_AMERICAN } function count_british_cu(s) { return (view.cupc[s] & CU_BRITISH_MASK) >>> CU_BRITISH_SHIFT } function count_american_cu(s) { return (view.cupc[s] & CU_AMERICAN_MASK) >>> CU_AMERICAN_SHIFT } function count_french_cu(s) { return (view.cupc[s] & CU_FRENCH_MASK) >>> CU_FRENCH_SHIFT } /* ANIMATION */ var animation_list = [] function remember_position(e) { if (e.parentElement) { animation_list.push(e) let prect = e.parentElement.getBoundingClientRect() let rect = e.getBoundingClientRect() e.my_visible = 1 e.my_parent = e.parentElement e.my_px = prect.x e.my_py = prect.y e.my_x = rect.x e.my_y = rect.y } else { e.my_visible = 0 e.my_parent = null e.my_x = 0 e.my_y = 0 e.my_z = 0 } } function animate_position(e) { if (e.parentElement && e.my_visible) { let prect = e.parentElement.getBoundingClientRect() let rect = e.getBoundingClientRect() let dx, dy if (e.parentElement === e.my_parent) { dx = (e.my_x - e.my_px) - (rect.x - prect.x) dy = (e.my_y - e.my_py) - (rect.y - prect.y) } else { dx = e.my_x - rect.x dy = e.my_y - rect.y } if (dx !== 0 || dy !== 0) { e.animate( [ { transform: `translate(${dx}px, ${dy}px)`, }, { transform: "translate(0, 0)", }, ], { duration: 333, easing: "ease" } ) } } } function animate_positions() { for (let e of animation_list) animate_position(e) animation_list.length = 0 } /* BUILD UI */ let ui = { favicon: document.getElementById("favicon"), header: document.querySelector("header"), status: document.getElementById("status"), last_played: document.getElementById("last_played"), hand: document.getElementById("hand"), spaces_element: document.getElementById("spaces"), pieces_element: document.getElementById("pieces"), cards: [], spaces: [], seas: [], a_pc: [], b_pc: [], a_colony: [], b_colony: [], a_cu: [], b_cu: [], f_cu: [], layout_static: [], layout_move: null, layout_react: null, generals: [], } let action_register = [] function register_action(target, action, id) { target.my_action = action target.my_id = id target.onmousedown = on_click_action action_register.push(target) } function is_action(action, arg) { if (arg === undefined) return !!(view.actions && view.actions[action] === 1) return !!(view.actions && view.actions[action] && set_has(view.actions[action], arg)) } function on_click_action(evt) { if (evt.button === 0) send_action(evt.target.my_action, evt.target.my_id) } const SPACE_W = { fortified_port: 68, winter_quarters: 78, regular: 80, box: 100 } function build_marker(cn, x, y, w, h) { let e = document.createElement("div") e.className = cn e.style.top = Math.round(y - h/2) + "px" e.style.left = Math.round(x - w/2) + "px" return e } function build_piece(cn, w, h) { let e = document.createElement("div") e.className = cn e.my_dx = w >> 1 e.my_dy = h >> 1 return e } function on_init() { for (let c = 0; c <= 110; ++c) { let e = ui.cards[c] = document.createElement("div") e.className = "card card_" + c if (c > 0) { e.my_id = c e.onclick = on_click_card register_action(e, "card", c) } } for (let s = 0; s < data.spaces.length; ++s) { let info = data.spaces[s] let e = ui.spaces[s] = document.createElement("div") let x = info.x let y = info.y let w = SPACE_W[info.type] let h = SPACE_W[info.type] if (info.type === "box") { x = data.layout.box[info.name][0] y = data.layout.box[info.name][1] w = data.layout.box[info.name][2] h = data.layout.box[info.name][3] } e.className = "space " + info.type + " " + data.colony_name[info.colony] e.style.left = x - w/2 + "px" e.style.top = y - h/2 + "px" e.style.width = w - 10 + "px" e.style.height = h - 10 + "px" register_action(e, "space", s) ui.spaces_element.appendChild(e) ui.b_pc[s] = build_marker("marker pc british", x, y, 58, 66) ui.a_pc[s] = build_marker("marker pc american", x, y, 58, 66) ui.b_cu[s] = build_piece("marker cu british", 60+2, 60+2) ui.a_cu[s] = build_piece("marker cu american", 60+2, 60+2) ui.f_cu[s] = build_piece("marker cu french", 60+2, 60+2) } ui.b_mcu = build_piece("marker cu british", 60+2, 60+2) ui.a_mcu = build_piece("marker cu american", 60+2, 60+2) ui.f_mcu = build_piece("marker cu french", 60+2, 60+2) ui.b_rcu = build_piece("marker cu british", 60+2, 60+2) ui.a_rcu = build_piece("marker cu american", 60+2, 60+2) ui.f_rcu = build_piece("marker cu french", 60+2, 60+2) for (let s = 0; s < 7; ++s) { let e = ui.seas[s] = document.createElement("div") let [ x, y, w, h ] = data.layout.sea[s] e.className = "space sea" e.style.left = x + "px" e.style.top = y + "px" e.style.width = w - 14 + "px" e.style.height = h - 14 + "px" register_action(e, "sea", s) ui.spaces_element.appendChild(e) } for (let g = 0; g < general_count; ++g) { let e = ui.generals[g] = build_piece("marker general small " + data.generals[g].name, 45+2, 45+2) register_action(e, "general", g) } for (let i = 0; i < 14; ++i) { let [ x, y ] = data.layout.colony[i] ui.a_colony[i] = build_marker("marker small control american", x, y, 45+2, 45+2) ui.b_colony[i] = build_marker("marker small control british", x, y, 45+2, 45+2) } ui.congress = build_piece("marker large congress", 55+2, 55+2) ui.french_navy = build_piece("marker large french_navy", 55+2, 55+2) ui.turn_marker = build_piece("marker large turn", 55+2, 55+2) ui.french_alliance = build_piece("marker large french_alliance", 55+2, 55+2) ui.combat = document.createElement("div") ui.combat.id = "combat" } on_init() /* UPDATE UI */ function lerp(a, b, t) { return a + t * (b - a) } function show_combat_marker() { let x = Math.round(lerp(data.spaces[view.move.from].x, data.spaces[view.move.to].x, 0.75)) let y = Math.round(lerp(data.spaces[view.move.from].y, data.spaces[view.move.to].y, 0.75)) ui.combat.style.left = x - 20 + "px" ui.combat.style.top = y - 20 + "px" ui.pieces_element.appendChild(ui.combat) } function show_marker(e) { ui.pieces_element.appendChild(e) } function toggle_marker(e, cond) { if (cond) show_marker(e) } function show_marker_at(e, x, y) { show_marker(e) e.style.left = x - e.my_dx + "px" e.style.top = y - e.my_dy + "px" } function show_marker_at_xy(e, xy) { show_marker_at(e, xy[0], xy[1]) } function toggle_marker_with_number_at(e, n, x, y) { if (n > 0) { show_marker(e) e.textContent = n e.style.left = x - e.my_dx + "px" e.style.top = y - e.my_dy + "px" } } function player_info(player, nc, nq) { let info = "" if (player == P_AMERICA) { if ((view.flags & F_MUTINIES) || view.congress === CONTINENTAL_CONGRESS_DISPERSED) info += "\u{1f6ab} " } if (nq > 0) info += nq + "\u{231b} " info += nc + "\u{1f3b4}" return info } function general_offset(g) { let n = 0 for (let i = 0; i < g; ++i) { if (view.move && view.move.who === i) continue if (view.react && view.react.who === i) continue if (view.loca[i] === view.loca[g]) ++n } return n } function general_total(g) { let n = 0 for (let i = 0; i < general_count; ++i) { if (view.move && view.move.who === i) continue if (view.react && view.react.who === i) continue if (view.loca[i] === view.loca[g]) ++n } return n } function get_army_xy_lerp(s1, s2) { if (s1 === s2) return get_army_xy(s2) let dx = data.spaces[s2].x - data.spaces[s1].x let dy = data.spaces[s2].y - data.spaces[s1].y let x, y if (s1 >= 66) { x = data.spaces[s1].x y = data.spaces[s1].y - 40 } else { x = data.spaces[s1].x + (dx * 1/2) | 0 y = data.spaces[s1].y + (dy * 1/2) | 0 } if (Math.abs(dx) < 180 && Math.abs(dy) < 60) y -= 20 return [ x - 15, y ] } function get_army_xy(s) { let x, y if (s >= 66) { x = data.spaces[s].x y = data.spaces[s].y - 120 } else { x = data.spaces[s].x y = data.spaces[s].y - 40 } return [ x, y ] } function get_static_xy(s) { let x, y if (s >= 66) { x = data.spaces[s].x y = data.spaces[s].y + 20 } else { x = data.spaces[s].x - 20 y = data.spaces[s].y + 10 } return [ x, y ] } function get_static_acu(s) { let cu = count_american_cu(s) if (view.move && view.move.to === s) cu -= view.move.carry_american if (view.react && view.react.from === s) cu -= view.react.carry_american return cu } function get_static_fcu(s) { let cu = count_french_cu(s) if (view.move && view.move.to === s) cu -= view.move.carry_french if (view.react && view.react.from === s) cu -= view.react.carry_french return cu } function get_static_bcu(s) { let cu = count_british_cu(s) if (view.move && view.move.to === s) cu -= view.move.carry_british if (view.react && view.react.from === s) cu -= view.react.carry_british return cu } function layout_all_cu(an, fn, bn, ae, fe, be, pos) { let [ x, y ] = pos toggle_marker_with_number_at(ae, an, x, y) if (an > 0) x += 30 toggle_marker_with_number_at(fe, fn, x, y) if (fn > 0) x += 30 toggle_marker_with_number_at(be, bn, x, y) if (bn > 0) x += 30 // if (an > 0 || fn > 0 || bn > 0) x -= 10 return [ x, y ] } function on_update() { let e remember_position(ui.turn_marker) remember_position(ui.french_alliance) remember_position(ui.french_navy) remember_position(ui.congress) remember_position(ui.b_mcu) remember_position(ui.a_mcu) remember_position(ui.f_mcu) remember_position(ui.b_rcu) remember_position(ui.a_rcu) remember_position(ui.f_rcu) for (let g = 0; g < general_count; ++g) remember_position(ui.generals[g]) roles.America.stat.textContent = player_info(P_AMERICA, view.a_cards, view.a_queue) roles.Britain.stat.textContent = player_info(P_BRITAIN, view.b_cards, view.b_queue) ui.last_played.className = "card shrink card_" + view.last_played ui.pieces_element.replaceChildren() for (let s = 0; s < data.spaces.length; ++s) { toggle_marker(ui.b_pc[s], has_british_pc(s)) toggle_marker(ui.a_pc[s], has_american_pc(s)) } if (view.congress === CONTINENTAL_CONGRESS_DISPERSED) show_marker_at(ui.congress, data.spaces[view.congress].x, data.spaces[view.congress].y) else show_marker_at(ui.congress, data.spaces[view.congress].x+20, data.spaces[view.congress].y-10) if (view.french_navy < 0) show_marker_at(ui.french_navy, data.spaces[FRENCH_REINFORCEMENTS].x-65, data.spaces[FRENCH_REINFORCEMENTS].y) else if (view.french_navy > 1700) show_marker_at_xy(ui.french_navy, data.layout.turn[view.french_navy]) else show_marker_at(ui.french_navy, data.layout.sea[view.french_navy][0]-15, data.layout.sea[view.french_navy][1]+42) for (let s = 0; s < data.spaces.length; ++s) { ui.layout_static[s] = layout_all_cu( get_static_acu(s), get_static_fcu(s), get_static_bcu(s), ui.a_cu[s], ui.f_cu[s], ui.b_cu[s], get_static_xy(s) ) } if (view.move) { let xy = null if (view.move.from === view.move.to && view.move.carry_american === count_american_cu(view.move.from) && view.move.carry_french === count_french_cu(view.move.from) && view.move.carry_british === count_british_cu(view.move.from) ) xy = get_static_xy(view.move.from) else xy = get_army_xy_lerp(view.move.from, view.move.to) ui.layout_move = layout_all_cu( view.move.carry_american | 0, view.move.carry_french | 0, view.move.carry_british | 0, ui.a_mcu, ui.f_mcu, ui.b_mcu, xy ) if (view.move.from !== view.move.to) show_combat_marker() } if (view.react) { ui.layout_react = layout_all_cu( view.react.carry_american | 0, view.react.carry_french | 0, view.react.carry_british | 0, ui.a_rcu, ui.f_rcu, ui.b_rcu, get_army_xy_lerp(view.react.from, view.react.to) ) } for (let g = 0; g < general_count; ++g) { let s = view.loca[g] if (s === NOWHERE) continue let [ x, y ] = (s < 66) ? ui.layout_static[s] : get_static_xy(s) if (s >= 66) y -= 60 if (view.move && view.move.who === g) [ x, y ] = ui.layout_move if (view.react && view.react.who === g) [ x, y ] = ui.layout_react ui.generals[g].classList.toggle("selected", view.selected_general === g) if (view.move && view.move.who === g) { // already laid out } else if (view.react && view.react.who === g) { // already laid out } else { if (s < 66) { let offset = general_offset(g) x += offset * (45 + 9) } else { let wrap = (s === 66 ? 3 : 4) let total = general_total(g) let offset = general_offset(g) let off_x = offset % wrap let off_y = offset / wrap | 0 let ctr_x = total > wrap ? wrap : total off_x -= (ctr_x - 1) / 2 x += off_x * (45 + 9) y += off_y * (45 + 9) y -= 15 } } show_marker_at(ui.generals[g], x, y) } for (let i = 0; i < 14; ++i) { let control = 0 for (let s of data.colonies[i]) { if (has_british_pc(s)) --control else if (has_american_pc(s)) ++control } toggle_marker(ui.b_colony[i], control < 0) toggle_marker(ui.a_colony[i], control > 0) } ui.turn_marker.classList.toggle("no_regulars", !(view.flags & F_REGULARS)) ui.french_alliance.classList.toggle("european_war", !!(view.flags & F_EUROPEAN_WAR)) show_marker_at_xy(ui.turn_marker, data.layout.turn[view.year]) show_marker_at_xy(ui.french_alliance, data.layout.french_alliance_track[view.french_alliance]) ui.hand.replaceChildren() if (view.hand) { for (let c of view.hand) { ui.cards[c].classList.toggle("selected", view.selected_card === c) ui.hand.appendChild(ui.cards[c]) } } e = document.getElementById("war_ends") if (view.war_ends) e.className = "year_" + CARDS[view.war_ends].year else e.className = "" e = document.getElementById("played_british_reinforcements") if (view.reinforcements[0] > 0) e.className = "reinforcements ops_" + CARDS[view.reinforcements[0]].count else e.className = "" e = document.getElementById("played_american_reinforcements_1") if (view.reinforcements[1] > 0) e.className = "reinforcements ops_" + CARDS[view.reinforcements[1]].count else e.className = "" e = document.getElementById("played_american_reinforcements_2") if (view.reinforcements[2] > 0) e.className = "reinforcements ops_" + CARDS[view.reinforcements[2]].count else e.className = "" for (let e of action_register) e.classList.toggle("action", is_action(e.my_action, e.my_id)) animate_positions() action_button("exchange", "Exchange") action_button("queue", "Queue") action_button("reinforce", "Reinforce") action_button("activate", "Activate") action_button("pc_action", "PC Action") action_button("event", "Event") action_button("campaign", "Campaign") action_button("landing_party", "Landing Party") action_button("no_general", "No General") action_button("pickup_french_cu", "Take French CU") action_button("pickup_british_cu", "Take CU") action_button("pickup_american_cu", "Take CU") action_button("drop_french_cu", "Drop French CU") action_button("drop_british_cu", "Drop CU") action_button("drop_american_cu", "Drop CU") action_button("britain_first", "Britain") action_button("america_first", "America") action_button("surrender", "Surrender") action_button("stop", "Stop") action_button("roll", "Roll") action_button("next", "Next") action_button("done", "Done") action_button("pass", "Pass") action_button("undo", "Undo") } /* POPUP MENU */ function show_popup_menu(evt, menu_id, card, 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, card)) { show = true item.classList.add("action") item.classList.remove("disabled") item.onclick = function () { send_action(action, card) 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_click_card(evt) { if (view.actions) { let c = evt.target.my_id if (is_action("card", c)) send_action("card", c) else show_popup_menu(evt, "popup", c, data.cards[c].title) } } /* LOG */ function sub_space(_match, p1) { let x = p1 | 0 let n = data.spaces[x].name if (n === "Wilmington DE") n = "Wilmington" n = n.replaceAll(" ", "\xa0") let co = data.spaces[x].colony if (co) n += "\xa0(" + data.colony_name[data.spaces[x].colony] + ")" return `${n}` } function sub_general(_match, p1) { let x = p1 | 0 let n = data.generals[x].name return `${n}` } function sub_card(_match, p1) { let x = p1 | 0 let n = data.cards[x].title return `${n}` } function on_click_space_tip(s) { ui.spaces[s].scrollIntoView({ block: "center", inline: "center", behavior: "smooth" }) } function on_focus_space_tip(s) { ui.spaces[s].classList.add("tip") } function on_blur_space_tip(s) { ui.spaces[s].classList.remove("tip") } function on_click_general_tip(s) { ui.generals[s].scrollIntoView({ block: "center", inline: "center", behavior: "smooth" }) } function on_focus_general_tip(s) { ui.generals[s].classList.add("tip") } function on_blur_general_tip(s) { ui.generals[s].classList.remove("tip") } function on_focus_card_tip(s) { ui.last_played.className = "card shrink card_" + s } function on_blur_card_tip() { ui.last_played.className = "card shrink card_" + view.last_played } function on_log(text) { let p = document.createElement("div") text = text.replace(/&/g, "&") text = text.replace(//g, ">") if (text.startsWith("=t")) { p.className = "h turn" text = text.substring(2) } else if (text.startsWith("=a")) { p.className = "h america" text = text.substring(3) } else if (text.startsWith("=b")) { p.className = "h britain" text = text.substring(3) } text = text.replace(/C(\d+)/g, sub_card) text = text.replace(/S(\d+)/g, sub_space) text = text.replace(/G(\d+)/g, sub_general) p.innerHTML = text return p }