"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
}