"use strict"
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
}
function map_get(map, key, missing) {
let a = 0
let b = (map.length >> 1) - 1
while (a <= b) {
let m = (a + b) >> 1
let x = map[m<<1]
if (key < x)
b = m - 1
else if (key > x)
a = m + 1
else
return map[(m<<1)+1]
}
return missing
}
const CAESAR = "Caesar"
const POMPEIUS = "Pompeius"
const DEAD = 0
const LEVY = 1
const B_CLEOPATRA = 31
const ENEMY = { "Caesar": "Pompeius", "Pompeius": "Caesar" }
for (let s of SPACES)
s.nbname = s.name.replace(/ /g, '\xa0')
const block_count = BLOCKS.length
const space_count = SPACES.length
let label_style = window.localStorage['julius-caesar/label-style'] || 'columbia'
let label_layout = window.localStorage['julius-caesar/label-layout'] || 'spread'
function toggle_blocks() {
document.getElementById("blocks").classList.toggle("hide_blocks")
}
function set_simple_labels() {
label_style = 'simple'
document.getElementById("blocks").classList.remove("columbia-labels")
document.getElementById("battle").classList.remove("columbia-labels")
document.getElementById("blocks").classList.add("simple-labels")
document.getElementById("battle").classList.add("simple-labels")
window.localStorage['julius-caesar/label-style'] = label_style
update_map()
}
function set_columbia_labels() {
label_style = 'columbia'
document.getElementById("blocks").classList.remove("simple-labels")
document.getElementById("battle").classList.remove("simple-labels")
document.getElementById("blocks").classList.add("columbia-labels")
document.getElementById("battle").classList.add("columbia-labels")
window.localStorage['julius-caesar/label-style'] = label_style
update_map()
}
function set_spread_layout() {
label_layout = 'spread'
window.localStorage['julius-caesar/label-layout'] = label_layout
update_map()
}
function set_stack_layout() {
label_layout = 'stack'
window.localStorage['julius-caesar/label-layout'] = label_layout
update_map()
}
// Levy and hit animations for 'simple' blocks.
const step_down_animation = [
{ transform: 'translateY(0px)' },
{ transform: 'translateY(10px)' },
{ transform: 'translateY(0px)' },
]
const step_up_animation = [
{ transform: 'translateY(0px)' },
{ transform: 'translateY(-10px)' },
{ transform: 'translateY(0px)' },
]
let ui = {
cards: [],
card_backs: [],
spaces: [],
roads: [],
blocks: [],
battle_menu: [],
battle_block: [],
old_steps: null,
old_location: null,
present: new Set(),
}
// :r !node tools/genroads.js
const ROADS_XY = {
"Aenos / Byzantium": [ 1953, 440 ],
"Aenos / Pergamum": [ 1912, 491 ],
"Aenos / Serdica": [ 1813, 388 ],
"Aenos / Thessalonica": [ 1806, 453 ],
"Alexandria / Catabathmus": [ 1962, 1102 ],
"Alexandria / Memphis": [ 2100, 1138 ],
"Alexandria / Pelusium": [ 2145, 1069 ],
"Ambracia / Athenae": [ 1648, 636 ],
"Ambracia / Dyrrachium": [ 1571, 555 ],
"Ancyra / Appia": [ 2137, 508 ],
"Ancyra / Eusebia": [ 2286, 506 ],
"Ancyra / Nicomedia": [ 2143, 461 ],
"Ancyra / Sinope": [ 2286, 426 ],
"Antiochia / Damascus": [ 2420, 787 ],
"Antiochia / Tarsus": [ 2348, 666 ],
"Appia / Ephesus": [ 2026, 601 ],
"Appia / Nicomedia": [ 2064, 496 ],
"Appia / Perga": [ 2086, 626 ],
"Aquileia / Ravenna": [ 1176, 230 ],
"Aquileia / Salona": [ 1314, 257 ],
"Aquileia / Sirmium": [ 1382, 180 ],
"Asculum / Ravenna": [ 1210, 340 ],
"Asculum / Roma": [ 1219, 410 ],
"Asculum / Sipontum": [ 1297, 440 ],
"Asturica / Bilbilis": [ 369, 556 ],
"Asturica / Emerita": [ 253, 677 ],
"Asturica / Portus": [ 191, 623 ],
"Asturica / Toletum": [ 332, 642 ],
"Athenae / Pylos": [ 1687, 700 ],
"Athenae / Thessalonica": [ 1704, 571 ],
"Badias / Iomnium": [ 856, 908 ],
"Badias / Tacape": [ 1001, 1010 ],
"Badias / Utica": [ 1016, 905 ],
"Bilbilis / Burdigala": [ 499, 452 ],
"Bilbilis / Tarraco": [ 536, 580 ],
"Bilbilis / Toletum": [ 429, 643 ],
"Brundisium / Neapolis": [ 1360, 519 ],
"Brundisium / Sipontum": [ 1402, 496 ],
"Burdigala / Cenabum": [ 589, 224 ],
"Burdigala / Narbo": [ 607, 406 ],
"Byzantium / Nicomedia": [ 2034, 427 ],
"Carthago Nova / Corduba": [ 415, 825 ],
"Carthago Nova / Gades": [ 390, 885 ],
"Carthago Nova / Tarraco": [ 542, 708 ],
"Carthago Nova / Toletum": [ 448, 749 ],
"Catabathmus / Cyrene": [ 1712, 1035 ],
"Cenabum / Lugdunum": [ 743, 191 ],
"Cenabum / Treviri": [ 768, 82 ],
"Corduba / Gades": [ 294, 865 ],
"Corduba / Toletum": [ 389, 778 ],
"Cyrene / Thubactus": [ 1431, 1173 ],
"Damascus / Jerusalem": [ 2375, 923 ],
"Dyrrachium / Salona": [ 1516, 387 ],
"Dyrrachium / Thessalonica": [ 1616, 486 ],
"Emerita / Gades": [ 239, 838 ],
"Emerita / Olisipo": [ 178, 774 ],
"Ephesus / Perga": [ 2024, 712 ],
"Ephesus / Pergamum": [ 1931, 566 ],
"Eusebia / Sinope": [ 2371, 474 ],
"Eusebia / Tarsus": [ 2298, 617 ],
"Gades / Olisipo": [ 186, 864 ],
"Gades / Tingis": [ 281, 964 ],
"Genua / Lugdunum": [ 916, 268 ],
"Genua / Massilia": [ 944, 359 ],
"Genua / Ravenna": [ 1107, 279 ],
"Genua / Roma": [ 1093, 385 ],
"Iomnium / Siga": [ 632, 903 ],
"Iomnium / Utica": [ 937, 834 ],
"Jerusalem / Pelusium": [ 2295, 1058 ],
"Lilybaeum / Messana": [ 1261, 721 ],
"Lilybaeum / Syracusae": [ 1257, 766 ],
"Lugdunum / Massilia": [ 811, 321 ],
"Lugdunum / Treviri": [ 851, 159 ],
"Massilia / Narbo": [ 746, 405 ],
"Memphis / Pelusium": [ 2136, 1118 ],
"Messana / Rhegium": [ 1344, 708 ],
"Messana / Syracusae": [ 1315, 740 ],
"Narbo / Tarraco": [ 700, 527 ],
"Neapolis / Rhegium": [ 1375, 624 ],
"Neapolis / Roma": [ 1263, 497 ],
"Neapolis / Sipontum": [ 1325, 499 ],
"Nicomedia / Pergamum": [ 1992, 493 ],
"Nicomedia / Sinope": [ 2197, 377 ],
"Olisipo / Portus": [ 118, 720 ],
"Perga / Tarsus": [ 2206, 727 ],
"Ravenna / Roma": [ 1175, 366 ],
"Sala / Siga": [ 373, 1071 ],
"Sala / Tingis": [ 253, 1044 ],
"Salona / Sirmium": [ 1426, 267 ],
"Serdica / Sirmium": [ 1631, 247 ],
"Serdica / Thessalonica": [ 1693, 403 ],
"Siga / Tingis": [ 382, 1009 ],
"Tacape / Thubactus": [ 1167, 1086 ],
"Tacape / Utica": [ 1092, 917 ],
}
const ROADS_BG = {
"Aenos / Byzantium": "hsl(83, 50%, 73%)",
"Aenos / Pergamum": "hsl(189, 64%, 75%)",
"Aenos / Serdica": "hsl(112, 43%, 67%)",
"Aenos / Thessalonica": "hsl(86, 43%, 69%)",
"Alexandria / Catabathmus": "hsl(77, 56%, 79%)",
"Alexandria / Memphis": "hsl(92, 46%, 67%)",
"Alexandria / Pelusium": "hsl(91, 52%, 74%)",
"Ambracia / Athenae": "hsl(77, 58%, 77%)",
"Ambracia / Dyrrachium": "hsl(77, 47%, 78%)",
"Ancyra / Appia": "hsl(79, 49%, 68%)",
"Ancyra / Eusebia": "hsl(99, 48%, 69%)",
"Ancyra / Nicomedia": "hsl(76, 58%, 71%)",
"Ancyra / Sinope": "hsl(80, 55%, 70%)",
"Antiochia / Damascus": "hsl(71, 42%, 65%)",
"Antiochia / Tarsus": "hsl(81, 50%, 73%)",
"Appia / Ephesus": "hsl(78, 58%, 71%)",
"Appia / Nicomedia": "hsl(76, 48%, 66%)",
"Appia / Perga": "hsl(78, 55%, 71%)",
"Aquileia / Ravenna": "hsl(92, 45%, 74%)",
"Aquileia / Salona": "hsl(79, 52%, 80%)",
"Aquileia / Sirmium": "hsl(118, 40%, 66%)",
"Asculum / Ravenna": "hsl(79, 50%, 80%)",
"Asculum / Roma": "hsl(99, 43%, 68%)",
"Asculum / Sipontum": "hsl(73, 55%, 82%)",
"Asturica / Bilbilis": "hsl(119, 44%, 70%)",
"Asturica / Emerita": "hsl(87, 53%, 71%)",
"Asturica / Portus": "hsl(82, 46%, 66%)",
"Asturica / Toletum": "hsl(104, 51%, 72%)",
"Athenae / Pylos": "hsl(75, 40%, 63%)",
"Athenae / Thessalonica": "hsl(80, 53%, 75%)",
"Badias / Iomnium": "hsl(75, 55%, 70%)",
"Badias / Tacape": "hsl(66, 45%, 67%)",
"Badias / Utica": "hsl(74, 56%, 71%)",
"Bilbilis / Burdigala": "hsl(106, 42%, 69%)",
"Bilbilis / Tarraco": "hsl(62, 52%, 71%)",
"Bilbilis / Toletum": "hsl(87, 49%, 68%)",
"Brundisium / Neapolis": "hsl(83, 49%, 69%)",
"Brundisium / Sipontum": "hsl(72, 53%, 81%)",
"Burdigala / Cenabum": "hsl(118, 37%, 66%)",
"Burdigala / Narbo": "hsl(112, 44%, 68%)",
"Byzantium / Nicomedia": "hsl(189, 65%, 75%)",
"Carthago Nova / Corduba": "hsl(72, 54%, 70%)",
"Carthago Nova / Gades": "hsl(62, 44%, 68%)",
"Carthago Nova / Tarraco": "hsl(74, 53%, 78%)",
"Carthago Nova / Toletum": "hsl(80, 59%, 72%)",
"Catabathmus / Cyrene": "hsl(81, 52%, 78%)",
"Cenabum / Lugdunum": "hsl(114, 43%, 67%)",
"Cenabum / Treviri": "hsl(116, 28%, 60%)",
"Corduba / Gades": "hsl(80, 59%, 72%)",
"Corduba / Toletum": "hsl(82, 54%, 70%)",
"Cyrene / Thubactus": "hsl(77, 51%, 81%)",
"Damascus / Jerusalem": "hsl(69, 49%, 68%)",
"Dyrrachium / Salona": "hsl(80, 46%, 69%)",
"Dyrrachium / Thessalonica": "hsl(81, 45%, 66%)",
"Emerita / Gades": "hsl(79, 58%, 72%)",
"Emerita / Olisipo": "hsl(74, 64%, 73%)",
"Ephesus / Perga": "hsl(77, 45%, 75%)",
"Ephesus / Pergamum": "hsl(78, 59%, 81%)",
"Eusebia / Sinope": "hsl(103, 44%, 74%)",
"Eusebia / Tarsus": "hsl(86, 45%, 68%)",
"Gades / Olisipo": "hsl(78, 64%, 78%)",
"Gades / Tingis": "hsl(189, 65%, 75%)",
"Genua / Lugdunum": "hsl(81, 33%, 78%)",
"Genua / Massilia": "hsl(81, 40%, 72%)",
"Genua / Ravenna": "hsl(119, 41%, 66%)",
"Genua / Roma": "hsl(83, 50%, 75%)",
"Iomnium / Siga": "hsl(84, 53%, 76%)",
"Iomnium / Utica": "hsl(82, 53%, 77%)",
"Jerusalem / Pelusium": "hsl(76, 54%, 77%)",
"Lilybaeum / Messana": "hsl(70, 49%, 81%)",
"Lilybaeum / Syracusae": "hsl(78, 56%, 82%)",
"Lugdunum / Massilia": "hsl(105, 37%, 63%)",
"Lugdunum / Treviri": "hsl(115, 43%, 67%)",
"Massilia / Narbo": "hsl(85, 55%, 71%)",
"Memphis / Pelusium": "hsl(98, 46%, 68%)",
"Messana / Rhegium": "hsl(189, 64%, 75%)",
"Messana / Syracusae": "hsl(77, 49%, 74%)",
"Narbo / Tarraco": "hsl(83, 48%, 73%)",
"Neapolis / Rhegium": "hsl(76, 53%, 81%)",
"Neapolis / Roma": "hsl(81, 54%, 75%)",
"Neapolis / Sipontum": "hsl(87, 51%, 74%)",
"Nicomedia / Pergamum": "hsl(76, 57%, 74%)",
"Nicomedia / Sinope": "hsl(76, 57%, 79%)",
"Olisipo / Portus": "hsl(74, 42%, 78%)",
"Perga / Tarsus": "hsl(76, 45%, 73%)",
"Ravenna / Roma": "hsl(102, 43%, 72%)",
"Sala / Siga": "hsl(83, 46%, 66%)",
"Sala / Tingis": "hsl(77, 54%, 78%)",
"Salona / Sirmium": "hsl(105, 38%, 64%)",
"Serdica / Sirmium": "hsl(114, 39%, 67%)",
"Serdica / Thessalonica": "hsl(98, 34%, 76%)",
"Siga / Tingis": "hsl(78, 51%, 81%)",
"Tacape / Thubactus": "hsl(68, 53%, 76%)",
"Tacape / Utica": "hsl(88, 50%, 74%)",
}
function remember_position(e) {
if (e.classList.contains("show")) {
let rect = e.getBoundingClientRect()
e.my_parent = true
e.my_x = rect.x
e.my_y = rect.y
} else {
e.my_parent = false
e.my_x = 0
e.my_y = 0
}
}
function animate_position(e) {
if (e.parentElement) {
if (e.my_parent) {
let rect = e.getBoundingClientRect()
let dx = e.my_x - rect.x
let dy = e.my_y - rect.y
if (dx !== 0 || dy !== 0) {
e.animate(
[
{ transform: `translate(${dx}px, ${dy}px)`, },
{ transform: "translate(0, 0)", },
],
{ duration: 250, easing: "ease" }
)
}
} else {
e.animate(
[
{ opacity: 0 },
{ opacity: 1 }
],
{ duration: 250, easing: "ease" }
)
}
}
}
function on_focus_space_tip(x) {
ui.spaces[x].classList.add("tip")
}
function on_blur_space_tip(x) {
ui.spaces[x].classList.remove("tip")
}
function on_click_space_tip(x) {
scroll_into_view(ui.spaces[x])
}
function sub_space_name(match, p1, offset, string) {
let x = p1 | 0
let n = SPACES[x].nbname
return `${n}`
}
function on_log(text) {
let p = document.createElement("div")
if (text.match(/^>/)) {
text = text.substring(1)
p.className = "i"
}
text = text.replace(/&/g, "&")
text = text.replace(//g, ">")
text = text.replace(/\u2192 /g, "\u2192\xa0")
text = text.replace(/^([A-Z]):/, ' $1 ')
text = text.replace(/#(\d+)/g, sub_space_name)
if (text.match(/^\.h1 /))
p.className = 'h1', text = text.substring(4)
if (text.match(/^\.h2 /))
p.className = 'h2', text = text.substring(4)
if (text.match(/^\.h3 C/))
p.className = 'h3 C', text = text.substring(4)
if (text.match(/^\.h3 P/))
p.className = 'h3 P', text = text.substring(4)
if (text.match(/^\.h4 /))
p.className = 'h4', text = text.substring(4)
if (text.match(/^\.h5 /))
p.className = 'h5', text = text.substring(4)
p.innerHTML = text
return p
}
function on_focus_space(evt) {
document.getElementById("status").textContent = SPACES[evt.target.space].name
}
function on_blur_space(evt) {
document.getElementById("status").textContent = ""
}
function on_click_space(evt) {
send_action('space', evt.target.space)
}
const STEPS = [ 0, "I", "II", "III", "IIII" ]
function block_description(b) {
if (is_known_block(b)) {
let s = BLOCKS[b].steps
let c = BLOCKS[b].initiative + BLOCKS[b].firepower
let levy = BLOCKS[b].levy
if (levy)
return BLOCKS[b].name + " (" + SPACES[levy].name + ") " + STEPS[s] + "-" + c
return BLOCKS[b].name + " " + STEPS[s] + "-" + c
}
return block_owner(b)
}
function block_color(who) {
if (who < B_CLEOPATRA)
return CAESAR
if (who > B_CLEOPATRA)
return POMPEIUS
return "Cleopatra"
}
function block_original_owner(who) {
if (who >= B_CLEOPATRA)
return POMPEIUS
return CAESAR
}
function block_owner(who) {
if (set_has(view.traitor, who))
return ENEMY[block_original_owner(who)]
return block_original_owner(who)
}
function block_name(b) {
return BLOCKS[b].name
}
function on_focus_map_block(evt) {
document.getElementById("status").textContent = block_description(evt.target.block)
}
function on_blur_map_block(evt) {
document.getElementById("status").textContent = ""
}
function on_focus_battle_block(evt) {
let b = evt.target.block
let msg = block_name(b)
if (!evt.target.classList.contains("known"))
msg = "Reserves"
if (view.actions && view.actions.battle_fire && view.actions.battle_fire.includes(b))
msg = "Fire with " + msg
else if (view.actions && view.actions.battle_retreat && view.actions.battle_retreat.includes(b))
msg = "Retreat with " + msg
else if (view.actions && view.actions.battle_hit && view.actions.battle_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_focus_battle_fire(evt) {
document.getElementById("status").textContent =
"Fire with " + block_name(evt.target.block)
}
function on_focus_battle_retreat(evt) {
document.getElementById("status").textContent =
"Retreat with " + block_name(evt.target.block)
}
function on_focus_battle_pass(evt) {
document.getElementById("status").textContent =
"Pass with " + block_name(evt.target.block)
}
function on_focus_battle_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_battle_block(evt) { send_action('block', evt.target.block) }
function on_click_battle_hit(evt) { send_action('battle_hit', evt.target.block) }
function on_click_battle_fire(evt) { send_action('battle_fire', evt.target.block) }
function on_click_battle_retreat(evt) { send_action('battle_retreat', evt.target.block) }
function on_click_battle_pass(evt) {
if (window.confirm("Are you sure that you want to PASS with " + block_name(evt.target.block) + "?"))
send_action('battle_pass', evt.target.block)
}
function on_click_map_block(evt) {
let b = evt.target.block
let s = view.location[b]
if (!view.battle)
send_action('block', b)
}
function build_map() {
// These must match up with the sizes in play.html
const city_size = 60+10
const sea_size = 70+10
ui.blocks_element = document.getElementById("blocks")
ui.offmap_element = document.getElementById("offmap")
ui.spaces_element = document.getElementById("spaces")
for (let s = 0; s < space_count; ++s) {
let space = SPACES[s]
let element = document.createElement("div")
element.classList.add("space")
let size = (space.type === 'sea') ? sea_size : city_size
if (space.type === "sea")
element.classList.add("sea")
else
element.classList.add("city")
element.setAttribute("draggable", "false")
element.addEventListener("mouseenter", on_focus_space)
element.addEventListener("mouseleave", on_blur_space)
element.addEventListener("click", on_click_space)
element.style.left = (space.layout.x - size/2) + "px"
element.style.top = (space.layout.y - size/2) + "px"
if (space.type !== 'pool')
document.getElementById("spaces").appendChild(element)
element.space = s
ui.spaces[s] = element
}
function build_map_block(b, block) {
let element = document.createElement("div")
element.classList.add("block")
element.classList.add("known")
element.classList.add(block_color(b))
element.classList.add("block_"+b)
element.classList.add("block_"+block.row+"_"+block.col)
element.addEventListener("mouseenter", on_focus_map_block)
element.addEventListener("mouseleave", on_blur_map_block)
element.addEventListener("click", on_click_map_block)
element.block = b
ui.blocks[b] = element
}
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 build_battle_block(b, block) {
let element = document.createElement("div")
element.classList.add("block")
element.classList.add(block_color(b))
element.classList.add("block_"+b)
element.classList.add("block_"+block.row+"_"+block.col)
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 action_list = document.createElement("div")
action_list.classList.add("battle_menu_list")
action_list.appendChild(element)
build_battle_button(action_list, b, "hit",
on_click_battle_hit, on_focus_battle_hit,
"/images/cross-mark.svg")
build_battle_button(action_list, b, "fire",
on_click_battle_fire, on_focus_battle_fire,
"/images/pointy-sword.svg")
build_battle_button(action_list, b, "retreat",
on_click_battle_retreat, on_focus_battle_retreat,
"/images/flying-flag.svg")
build_battle_button(action_list, b, "pass",
on_click_battle_pass, on_focus_battle_pass,
"/images/sands-of-time.svg")
let menu = document.createElement("div")
menu.classList.add("battle_menu")
menu.appendChild(element)
menu.appendChild(action_list)
menu.block = b
ui.battle_menu[b] = menu
}
for (let b = 0; b < block_count; ++b) {
let block = BLOCKS[b]
build_map_block(b, block)
build_battle_block(b, block)
}
for (let c = 1; c <= 27; ++c)
ui.cards[c] = document.getElementById("card+" + c)
for (let c = 1; c <= 6; ++c)
ui.card_backs[c] = document.getElementById("back+" + c)
for (let name in ROADS_XY) {
let [x, y] = ROADS_XY[name]
let [a, b] = name.split(" / ")
let id = SPACES.findIndex(s=>s.name===a) * 100 + SPACES.findIndex(s=>s.name===b)
let e = document.createElement("div")
e.my_id = id
e.my_bgnd = ROADS_BG[name]
e.my_show = "road " + EDGES[id]
e.className = "hide"
e.style.left = (x - 12) + "px"
e.style.top = (y - 12) + "px"
ui.roads.push(e)
document.getElementById("roads").appendChild(e)
}
}
function update_steps(block, element, animate) {
let old_steps = ui.old_steps[block] || view.steps[block]
let steps = view.steps[block]
if (view.location[block] !== ui.old_location[block])
animate = false
if (label_style === 'simple' && steps !== old_steps && animate) {
let options = { duration: 700, easing: 'ease', iterations: Math.abs(steps-old_steps) }
if (steps < old_steps)
element.animate(step_down_animation, options)
if (steps > old_steps)
element.animate(step_up_animation, options)
}
element.classList.remove("r0")
element.classList.remove("r1")
element.classList.remove("r2")
element.classList.remove("r3")
element.classList.add("r"+(BLOCKS[block].steps - steps))
}
function layout_blocks(location, north, south) {
if (label_layout === 'spread' || (location === LEVY || location === DEAD))
layout_blocks_spread(location, north, south)
else
layout_blocks_stacked(location, north, south)
}
function layout_blocks_spread(location, north, south) {
let wrap = SPACES[location].layout.wrap
let s = north.length
let k = south.length
let n = s + k
let row, rows = []
let i = 0
function new_line() {
rows.push(row = [])
i = 0
}
new_line()
while (north.length > 0) {
if (i === wrap)
new_line()
row.push(north.shift())
++i
}
// Break early if north and south fit in exactly two rows and more than two blocks.
if (s > 0 && s <= wrap && k > 0 && k <= wrap && n > 2)
new_line()
while (south.length > 0) {
if (i === wrap)
new_line()
row.push(south.shift())
++i
}
if (SPACES[location].layout.minor > 0.5)
rows.reverse()
for (let j = 0; j < rows.length; ++j)
for (i = 0; i < rows[j].length; ++i)
position_block_spread(location, j, rows.length, i, rows[j].length, rows[j][i])
}
function position_block_spread(location, row, n_rows, col, n_cols, element) {
let space = SPACES[location]
let block_size = (label_style === 'columbia') ? 60+6 : 48+6
let padding = (location === LEVY || location === DEAD) ? 6 : 3
let offset = block_size + padding
let row_size = (n_rows-1) * offset
let col_size = (n_cols-1) * offset
let x = space.layout.x - block_size/2
let y = space.layout.y - block_size/2
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|0)+"px"
element.style.top = (y|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 = SPACES[location]
let block_size = (label_style === 'columbia') ? 56+6 : 48+4
let x = space.layout.x + (i - c) * 16 + k * 12
let y = space.layout.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 is_known_block(who) {
if (block_owner(who) === player)
return true
let where = view.location[who]
if (where === DEAD)
return true
return false
}
function is_visible_block(where, who) {
if (where === LEVY)
return block_owner(who) === player
return true
}
function is_in_battle(b) {
if (view.battle) {
if (view.battle.CR.includes(b)) return true
if (view.battle.PR.includes(b)) return true
if (view.battle.CF.includes(b)) return true
if (view.battle.PF.includes(b)) return true
}
return false
}
function update_map() {
let layout = {}
for (let s = 0; s < space_count; ++s)
layout[s] = { north: [], south: [] }
let is_battle_open = document.getElementById("battle").getAttribute("open") !== null
for (let b in view.location) {
b = b | 0
let info = BLOCKS[b]
let element = ui.blocks[b]
let space = view.location[b]
if (is_visible_block(space, b)) {
let moved = set_has(view.moved, b) ? " moved" : ""
if (space === DEAD && info.type !== 'leader')
moved = " moved"
let battle = ""
if (is_battle_open && is_in_battle(b))
battle = " battle"
if (is_known_block(b)) {
let image = " block_" + b
image += " block_"+info.row+"_"+info.col
let known = " known"
element.classList = block_color(b) + known + " block" + image + moved + battle
update_steps(b, element, true)
} else {
let jupiter = ""
let mars = ""
let neptune = ""
if (set_has(view.traitor, b))
jupiter = " jupiter"
if (block_owner(b) === view.mars && space === view.surprise)
mars = " mars"
if (block_owner(b) === view.neptune && space === view.surprise)
neptune = " neptune"
element.classList = block_color(b) + " block" + moved + jupiter + mars + neptune + battle
}
if (block_owner(b) === CAESAR)
layout[space].north.push(element)
else
layout[space].south.push(element)
show_block(element)
} else {
hide_block(element)
}
}
for (let s = 0; s < space_count; ++s)
layout_blocks(s, layout[s].north, layout[s].south)
// Mark selections and highlights
for (let s = 0; s < space_count; ++s) {
if (ui.spaces[s]) {
ui.spaces[s].classList.remove('highlight')
ui.spaces[s].classList.remove('where')
ui.spaces[s].classList.remove('battle')
}
}
if (view.actions && view.actions.space)
for (let where of view.actions.space)
ui.spaces[where].classList.add('highlight')
for (let b = 0; b < block_count; ++b) {
ui.blocks[b].classList.remove('highlight')
ui.blocks[b].classList.remove('selected')
}
if (!view.battle) {
if (view.actions && view.actions.block)
for (let b of view.actions.block)
ui.blocks[b].classList.add('highlight')
if (view.who >= 0)
ui.blocks[view.who].classList.add('selected')
} else {
ui.spaces[view.battle.where].classList.add('battle')
}
for (let b = 0; b < block_count; ++b) {
let s = view.location[b]
if (view.actions && view.actions.secret && view.actions.secret.includes(s))
ui.blocks[b].classList.add('highlight')
}
for (let e of ui.roads) {
let cu = set_has(view.last_used[0], e.my_id)
let pu = set_has(view.last_used[1], e.my_id)
let n = map_get(view.limits, e.my_id, "")
if (n && view.main_road && set_has(view.main_road, e.my_id))
n += "*"
if (cu) {
e.style.backgroundColor = null
e.className = "road Caesar"
e.textContent = n
} else if (pu) {
e.style.backgroundColor = null
e.className = "road Pompeius"
e.textContent = n
} else {
if (n) {
e.style.backgroundColor = e.my_bgnd
e.className = e.my_show
e.textContent = n
} else {
e.style.backgroundColor = null
e.className = "hide"
e.textContent = ""
}
}
}
}
function compare_blocks(a, b, ballista) {
let aa = BLOCKS[a].initiative
let bb = BLOCKS[b].initiative
if (aa === 'X') aa = ballista
if (bb === 'X') bb = ballista
if (aa === bb) {
aa = a
bb = b
}
return (aa < bb) ? -1 : (aa > bb) ? 1 : 0
}
function sort_battle_row(root, ballista) {
let swapped
let children = root.children
do {
swapped = false
for (let i = 1; i < children.length; ++i) {
if (compare_blocks(children[i-1].block, children[i].block, ballista) > 0) {
children[i].after(children[i-1])
swapped = true
}
}
} while (swapped)
}
function show_battle() {
let box = document.getElementById("battle")
let space = SPACES[view.battle.where].layout
// reset position
box.classList.add("show")
box.style.top = null
box.style.left = null
box.setAttribute("open", true)
// calculate size
let w = box.clientWidth
let h = box.clientHeight
// center where possible
let x = space.x - w / 2
if (x < 140)
x = 140
if (x > 2475 - w - 60)
x = 2475 - w - 60
let y = space.y - h - 120
if (y < 50)
y = space.y + 120
box.style.top = y + "px"
box.style.left = x + "px"
scroll_into_view_if_needed(box)
}
function update_battle() {
function fill_cell(name, list, reserve, ballista) {
let cell = window[name]
ui.present.clear()
for (let block of list) {
ui.present.add(block)
if (!cell.contains(ui.battle_menu[block]))
cell.appendChild(ui.battle_menu[block])
if (block === view.who)
ui.battle_menu[block].classList.add("selected")
else
ui.battle_menu[block].classList.remove("selected")
ui.battle_block[block].classList.remove("highlight")
ui.battle_menu[block].classList.remove('hit')
ui.battle_menu[block].classList.remove('fire')
ui.battle_menu[block].classList.remove('retreat')
ui.battle_menu[block].classList.remove('pass')
if (view.actions && view.actions.block && view.actions.block.includes(block))
ui.battle_block[block].classList.add("highlight")
if (view.actions && view.actions.battle_fire && view.actions.battle_fire.includes(block))
ui.battle_menu[block].classList.add('fire')
if (view.actions && view.actions.battle_retreat && view.actions.battle_retreat.includes(block))
ui.battle_menu[block].classList.add('retreat')
if (view.actions && view.actions.battle_pass && view.actions.battle_pass.includes(block))
ui.battle_menu[block].classList.add('pass')
if (view.actions && view.actions.battle_hit && view.actions.battle_hit.includes(block))
ui.battle_menu[block].classList.add('hit')
update_steps(block, ui.battle_block[block], true)
if (reserve)
ui.battle_block[block].classList.add("secret")
else
ui.battle_block[block].classList.remove("secret")
if (set_has(view.moved, block) || reserve)
ui.battle_block[block].classList.add("moved")
else
ui.battle_block[block].classList.remove("moved")
if (reserve)
ui.battle_block[block].classList.remove("known")
else
ui.battle_block[block].classList.add("known")
if (set_has(view.traitor, block))
ui.battle_block[block].classList.add("jupiter")
else
ui.battle_block[block].classList.remove("jupiter")
}
for (let b = 0; b < block_count; ++b) {
if (!ui.present.has(b)) {
if (cell.contains(ui.battle_menu[b]))
cell.removeChild(ui.battle_menu[b])
}
}
sort_battle_row(cell, ballista)
}
if (player === CAESAR) {
fill_cell("FR", view.battle.CR, true, 'B')
fill_cell("FF", view.battle.CF, false, view.battle.A === CAESAR ? 'D' : 'B')
fill_cell("EF", view.battle.PF, false, view.battle.A === CAESAR ? 'B' : 'D')
fill_cell("ER", view.battle.PR, true, 'B')
} else {
fill_cell("FR", view.battle.PR, true, 'B')
fill_cell("FF", view.battle.PF, false, view.battle.A === POMPEIUS ? 'D' : 'B')
fill_cell("EF", view.battle.CF, false, view.battle.A === POMPEIUS ? 'B' : 'D')
fill_cell("ER", view.battle.CR, true, 'B')
}
}
function update_card_display(element, card, prior_card) {
if (!card && !prior_card) {
element.className = "show card card_back"
} else if (prior_card) {
element.className = "show card prior card_" + CARDS[prior_card].image
} else {
element.className = "show card card_" + CARDS[card].image
}
}
function update_cards() {
update_card_display(document.getElementById("caesar_card"), view.c_card, view.prior_c_card)
update_card_display(document.getElementById("pompeius_card"), view.p_card, view.prior_p_card)
for (let c = 1; c <= 27; ++c) {
let element = ui.cards[c]
if (view.hand.includes(c)) {
element.classList.add("show")
if (view.actions && view.actions.card) {
if (view.actions.card.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")
}
}
let n = view.hand.length
for (let c = 1; c <= 6; ++c)
if (c <= n && player === 'Observer')
ui.card_backs[c].classList.add("show")
else
ui.card_backs[c].classList.remove("show")
}
function on_update() {
if (!ui.old_steps) {
ui.old_steps = view.steps
ui.old_location = view.location
}
document.getElementById("turn").className = "year_" + view.year
document.getElementById("caesar_vp").textContent = view.c_vp + " VP"
document.getElementById("pompeius_vp").textContent = view.p_vp + " VP"
if (view.turn < 1)
document.getElementById("turn_info").textContent = `Year ${view.year}`
else
document.getElementById("turn_info").textContent = `Turn ${view.turn} of Year ${view.year}`
action_button("surprise", "Surprise!")
action_button("assign", "Assign hits")
action_button("pass")
action_button("undo", "Undo")
for (let c = 1; c <= 27; ++c)
remember_position(ui.cards[c])
update_cards()
if (view.battle) {
document.getElementById("battle_header").textContent = view.battle.title
document.getElementById("battle_message").textContent = view.battle.flash
update_battle()
if (!document.getElementById("battle").classList.contains("show"))
show_battle()
document.getElementById("battle").classList.toggle("sea", SPACES[view.battle.where].type === "sea")
} else {
document.getElementById("battle").classList.remove("show")
}
update_map()
ui.old_location = Object.assign({}, view.location)
ui.old_steps = Object.assign({}, view.steps)
for (let c = 1; c <= 27; ++c)
animate_position(ui.cards[c])
}
function select_card(c) {
send_action('card', c)
}
document.getElementById("battle").addEventListener("toggle", on_update)
document.getElementById("blocks").classList.add(label_style+'-labels')
document.getElementById("battle").classList.add(label_style+'-labels')
build_map()