"use strict" /* global view, data, roles, send_action, action_button */ // TODO: show "reshuffle" flag next to card deck display // TODO: improve animations by having one CU stack per general and one CU stack per space function toggle_counters() { // Cycle between showing everything, only markers, and nothing. if (ui.map.classList.contains("hide_markers")) { ui.map.classList.remove("hide_markers") ui.map.classList.remove("hide_pieces") } else if (ui.map.classList.contains("hide_pieces")) { ui.map.classList.add("hide_markers") } else { ui.map.classList.add("hide_pieces") } } /* COMMON */ function lerp(a, b, t) { return a + t * (b - a) } 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"), map: document.getElementById("map"), spaces_element: document.getElementById("spaces"), markers_element: document.getElementById("markers"), pieces_element: document.getElementById("pieces"), cards: [], spaces: [], seas: [], a_pc: [], b_pc: [], a_colony: [], b_colony: [], a_cu: [], b_cu: [], f_cu: [], a_mcu: [], b_mcu: [], f_mcu: [], 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() { document.getElementById("war_ends").addEventListener("mouseenter", on_focus_war_ends) document.getElementById("war_ends").addEventListener("mouseleave", on_blur_war_ends) document.getElementById("last_played").addEventListener("mouseenter", on_focus_last_played) document.getElementById("last_played").addEventListener("mouseleave", on_blur_last_played) 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 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) } for (let g = 0; g < general_count; ++g) { ui.b_mcu[g] = build_piece("marker cu british", 60+2, 60+2) ui.a_mcu[g] = build_piece("marker cu american", 60+2, 60+2) ui.f_mcu[g] = 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 show_move_marker(marker, s1, s2) { let x1 = data.spaces[s1].x let y1 = data.spaces[s1].y let x2 = data.spaces[s2].x let y2 = data.spaces[s2].y let dx = x2 - x1 let dy = y2 - y1 let n = Math.hypot(dx, dy) let a = Math.atan2(dy, dx) let x, y if (n < 150) { x = x2 - (dx/n) * 36 y = y2 - (dy/n) * 36 } else { x = lerp(x1, x2, 3/4) y = lerp(y1, y2, 3/4) } marker.style.left = (x|0) - 24 + "px" marker.style.top = (y|0) - 24 + "px" marker.style.transform = "rotate(" + (a + Math.PI/2) + "rad)" ui.markers_element.appendChild(marker) } function toggle_marker(e, cond) { if (cond) show_marker(e) } function show_marker(e) { ui.markers_element.appendChild(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 show_piece(e) { ui.pieces_element.appendChild(e) } function show_piece_at(e, x, y) { show_piece(e) e.style.left = x - e.my_dx + "px" e.style.top = y - e.my_dy + "px" } function show_piece_at_xy(e, xy) { show_piece_at(e, xy[0], xy[1]) } function show_piece_with_number_at(e, n, x, y) { if (n > 0) { show_piece(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.from !== view.move.to && 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) //return get_army_xy(s2) let x, y if (s1 >= 66) { x = data.spaces[s1].x - 15 y = data.spaces[s1].y - 40 } else { let x1 = data.spaces[s1].x let y1 = data.spaces[s1].y let x2 = data.spaces[s2].x let y2 = data.spaces[s2].y let dx = x2 - x1 let dy = y2 - y1 let n = Math.hypot(dx, dy) x = lerp(x1, x2, 1/2) y = lerp(y1, y2, 1/2) if (n < 150) { if (Math.abs(dx) > Math.abs(dy)) { if (dx > 0) x -= 10 else x += 10 y -= 35 } } } return [ (x|0) , (y|0) ] } 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 show_piece_with_number_at(ae, an, x, y) if (an > 0) x += 30 show_piece_with_number_at(fe, fn, x, y) if (fn > 0) x += 30 show_piece_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) for (let g = 0; g < general_count; ++g) { remember_position(ui.generals[g]) remember_position(ui.b_mcu[g]) remember_position(ui.a_mcu[g]) remember_position(ui.f_mcu[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.markers_element.replaceChildren() 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_piece_at(ui.congress, data.spaces[view.congress].x, data.spaces[view.congress].y) else show_piece_at(ui.congress, data.spaces[view.congress].x+20, data.spaces[view.congress].y-10) if (view.french_navy < 0) show_piece_at(ui.french_navy, data.spaces[FRENCH_REINFORCEMENTS].x-65, data.spaces[FRENCH_REINFORCEMENTS].y) else if (view.french_navy > 1700) show_piece_at_xy(ui.french_navy, data.layout.turn[view.french_navy]) else show_piece_at(ui.french_navy, data.layout.sea[view.french_navy][0]-15, data.layout.sea[view.french_navy][1]+42) if (view.move && view.move.from !== view.move.to) show_move_marker(ui.combat, view.move.from, view.move.to) for (let s = 0; s < data.spaces.length; ++s) { let found = -1 for (let g = 0; g < general_count; ++g) { if (view.move && view.move.who === g) continue if (view.react && view.react.who === g) continue if (view.loca[g] === s) { found = g break } } if (found >= 0) { ui.layout_static[s] = layout_all_cu( get_static_acu(s), get_static_fcu(s), get_static_bcu(s), ui.a_mcu[found], ui.f_mcu[found], ui.b_mcu[found], get_static_xy(s) ) } else { 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.to) && view.move.carry_french === count_french_cu(view.move.to) && view.move.carry_british === count_british_cu(view.move.to) ) xy = get_static_xy(view.move.to) else xy = get_army_xy_lerp(view.move.from, view.move.to) if (view.move.who >= 0) { ui.layout_move = layout_all_cu( view.move.carry_american | 0, view.move.carry_french | 0, view.move.carry_british | 0, ui.a_mcu[view.move.who], ui.f_mcu[view.move.who], ui.b_mcu[view.move.who], xy ) } else { ui.layout_move = layout_all_cu( view.move.carry_american | 0, view.move.carry_french | 0, view.move.carry_british | 0, ui.a_cu[view.move.to], ui.f_cu[view.move.to], ui.b_cu[view.move.to], xy ) } } 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_mcu[view.react.who], ui.f_mcu[view.react.who], ui.b_mcu[view.react.who], 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 === 68) y -= 20 else 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_piece_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") } /* 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_minus(_match, p1) { return "\u2212" + p1 } 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(c) { document.getElementById("tooltip").className = "card card_" + c } function on_blur_card_tip() { document.getElementById("tooltip").classList = "hide" } function on_focus_last_played() { if (view.last_played) document.getElementById("tooltip").className = "card card_" + view.last_played } function on_blur_last_played() { document.getElementById("tooltip").classList = "hide" } function on_focus_war_ends() { if (view.war_ends) document.getElementById("tooltip").className = "card card_" + view.war_ends } function on_blur_war_ends() { document.getElementById("tooltip").classList = "hide" } const ICONS = { D1: '', D2: '', D3: '', D4: '', D5: '', D6: '', A1: '1', A2: '2', A3: '3', A4: '4', A5: '5', A6: '6', AB1: '1', AB2: '2', AB3: '3', AB4: '4', AB5: '5', AB6: '6', BB1: '1', BB2: '2', BB3: '3', BB4: '4', BB5: '5', BB6: '6', FB1: '1', FB2: '2', FB3: '3', FB4: '4', FB5: '5', FB6: '6', } function sub_icon(match) { return ICONS[match] || match } function on_log(text) { let p = document.createElement("div") if (text.startsWith(">>")) { p.className = "ii" text = text.substring(2) } else if (text.startsWith(">")) { p.className = "i" text = text.substring(1) } else 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) } else if (text.startsWith("=!")) { p.className = "h" text = text.substring(3) } if ( text.startsWith("Played ") || text.startsWith("Discarded ") || text.startsWith("Exchanged ") || text.startsWith("Retreated ") || text.startsWith("Surrendered ") || text === "Removed card." ) p.className = "n" if (text.startsWith("Moved G") || text.startsWith("Landing Party")) p.className = "m" text = text.replace(/&/g, "&") text = text.replace(//g, ">") text = text.replace(/-(\d)/g, sub_minus) text = text.replace(/\b[D][1-6]\b/g, sub_icon) text = text.replace(/\b[A][1-6]\b/g, sub_icon) text = text.replace(/\bAB[1-6]\b/g, sub_icon) text = text.replace(/\bBB[1-6]\b/g, sub_icon) text = text.replace(/\bFB[1-6]\b/g, sub_icon) 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 }