"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()