"use strict" // === SYNC with rules.js === const TRIBE_COUNT = [ 0, 5, 3, 4, 5 ] const ITALIA = 0 const ASIA = 1 const GALLIA = 2 const MACEDONIA = 3 const PANNONIA = 4 const THRACIA = 5 const AEGYPTUS = 6 const AFRICA = 7 const HISPANIA = 8 const BRITANNIA = 9 const GALATIA = 10 const SYRIA = 11 const ALAMANNI_HOMELAND = 12 const FRANKS_HOMELAND = 13 const GOTHS_HOMELAND = 14 const NOMADS_HOMELAND = 15 const SASSANIDS_HOMELAND = 16 const MARE_OCCIDENTALE = 17 const MARE_ORIENTALE = 18 const OCEANUS_ATLANTICUS = 19 const PONTUS_EUXINUS = 20 const AVAILABLE = 21 const UNAVAILABLE = 22 const ARMY = 23 const first_barbarian = [ 0, 10, 20, 30, 40 ] const first_governor = [ 0, 6, 12, 18 ] const first_general = [ 0, 6, 12, 18 ] const REGION_NAME = [ "Italia", "Asia", "Gallia", "Macedonia", "Pannonia", "Thracia", "Aegyptus", "Africa", "Hispania", "Britannia", "Galatia", "Syria", "Alamanni Homeland", "Franks Homeland", "Goths Homeland", "Nomads Homeland", "Sassanids Homeland", "Mare Occidentale", "Mare Orientale", "Oceanus Atlanticus", "Pontus Euxinus", "Available", "Unavailable", ] function is_no_place_governor(province) { return province >= view.support.length } function get_support(province) { return view.support[province] } function get_barbarian_location(id) { return view.barbarians[id] & 63 } function is_barbarian_inactive(id) { return view.barbarians[id] & 64 } function get_legion_location(ix) { return view.legions[ix] & 63 } function is_legion_reduced(ix) { return view.legions[ix] & 64 } function is_legion_unused(ix) { return view.legions[ix] === AVAILABLE } function get_governor_location(id, loc) { return view.governors[id] & 63 } function get_general_location(id) { return view.generals[id] & 63 } function is_general_inside_capital(id) { return view.generals[id] & 64 } function has_general_castra(id) { return view.castra & (1 << id) } function has_militia_castra(province) { return view.mcastra & (1 << province) } function has_quaestor(province) { return view.quaestor & (1 << province) } function has_militia(province) { return view.militia & (1 << province) } function get_mobs(province) { return view.mobs[province] } function has_amphitheater(province) { return view.amphitheater & (1 << province) } function has_basilica(province) { return view.basilica & (1 << province) } function has_limes(province) { return view.limes & (1 << province) } function is_breakaway(province) { return view.breakaway & (1 << province) } function is_seat_of_power(province) { return view.seat_of_power & (1 << province) } // === END SYNC === 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 } const PLAYER_CLASS = [ "red", "blue", "yellow", "green" ] const BOXES = { "Thracia Support": [1502,720,258,52], "Syria Support": [2034,1280,258,52], "Pannonia Support": [1154,626,258,53], "Macedonia Support": [1384,936,258,53], "Hispania Support": [154,980,258,53], "Gallia Support": [460,507,258,53], "Galatia Support": [1954,931,258,53], "Britannia Support": [231,260,258,52], "Asia Support": [1679,1000,258,52], "Africa Support": [647,1290,258,53], "Aegyptus Support": [1700,1468,258,53], "Italia Support 2": [1054,887,258,52], "Italia Support 1": [1028,835,258,52], "Thracia Capital": [1594,631,70,70], "Syria Capital": [2174,1193,70,70], "Pannonia Capital": [1214,536,70,70], "Macedonia Capital": [1477,850,70,70], "Italia Capital": [1038,743,70,70], "Hispania Capital": [249,892,70,70], "Gallia Capital": [554,418,70,70], "Galatia Capital": [2048,842,70,70], "Britannia Capital": [325,177,70,70], "Asia Capital": [1790,908,70,70], "Africa Capital": [741,1204,70,70], "Aegyptus Capital": [1793,1380,70,70], "Pontus Euxinus XY": [1970,620,130,60], "Mare Orientale XY": [1770,1170,100,60], "Mare Occidentale XY": [720,890,90,60], "Oceanus Atlanticus XY": [130,495,80,50], "Nomads XY": [705,1495,165,25], "Sassanids XY": [2295,980,190,25], "Goths XY": [1840,235,130,15], "Alamanni XY": [1370,200,195,15], "Franks XY": [900,200,135,15], "SCORE TRACK": [40,40,2469,80], "CRISIS TABLE": [2195,189,262,326], } const LAYOUT_XY = [ BOXES["Italia Capital"], BOXES["Asia Capital"], BOXES["Gallia Capital"], BOXES["Macedonia Capital"], BOXES["Pannonia Capital"], BOXES["Thracia Capital"], BOXES["Aegyptus Capital"], BOXES["Africa Capital"], BOXES["Hispania Capital"], BOXES["Britannia Capital"], BOXES["Galatia Capital"], BOXES["Syria Capital"], BOXES["Alamanni XY"], BOXES["Franks XY"], BOXES["Goths XY"], BOXES["Nomads XY"], BOXES["Sassanids XY"], BOXES["Mare Occidentale XY"], BOXES["Mare Orientale XY"], BOXES["Oceanus Atlanticus XY"], BOXES["Pontus Euxinus XY"], ] const LAYOUT_SUPPORT = [ BOXES["Italia Support 1"], BOXES["Asia Support"], BOXES["Gallia Support"], BOXES["Macedonia Support"], BOXES["Pannonia Support"], BOXES["Thracia Support"], BOXES["Aegyptus Support"], BOXES["Africa Support"], BOXES["Hispania Support"], BOXES["Britannia Support"], BOXES["Galatia Support"], BOXES["Syria Support"], ] let ui = { cards: [], militia: [], body: document.querySelector("body"), header: document.querySelector("header"), hand: document.getElementById("hand"), draw: document.getElementById("draw"), discard: document.getElementById("discard"), played: document.getElementById("played"), market: document.getElementById("market"), pieces: document.getElementById("pieces"), legacy: [ document.getElementById("red_legacy"), document.getElementById("blue_legacy"), document.getElementById("yellow_legacy"), document.getElementById("green_legacy"), ], emperor_turns: [ document.getElementById("red_emperor_turns"), document.getElementById("blue_emperor_turns"), document.getElementById("yellow_emperor_turns"), document.getElementById("green_emperor_turns"), ], regions: [ document.getElementById("mapsvg").getElementById("region_italia"), document.getElementById("mapsvg").getElementById("region_asia"), document.getElementById("mapsvg").getElementById("region_gallia"), document.getElementById("mapsvg").getElementById("region_macedonia"), document.getElementById("mapsvg").getElementById("region_pannonia"), document.getElementById("mapsvg").getElementById("region_thracia"), document.getElementById("mapsvg").getElementById("region_aegyptus"), document.getElementById("mapsvg").getElementById("region_africa"), document.getElementById("mapsvg").getElementById("region_hispania"), document.getElementById("mapsvg").getElementById("region_britannia"), document.getElementById("mapsvg").getElementById("region_galatia"), document.getElementById("mapsvg").getElementById("region_syria"), document.getElementById("mapsvg").getElementById("region_alamanni"), document.getElementById("mapsvg").getElementById("region_franks"), document.getElementById("mapsvg").getElementById("region_goths"), document.getElementById("mapsvg").getElementById("region_nomads"), document.getElementById("mapsvg").getElementById("region_sassanids"), document.getElementById("mapsvg").getElementById("region_mare_occidentale"), document.getElementById("mapsvg").getElementById("region_mare_orientale"), document.getElementById("mapsvg").getElementById("region_oceanus_atlanticus"), document.getElementById("mapsvg").getElementById("region_pontus_euxinus"), ], capital: [], quaestor: [], amphitheater: [], basilica: [], limes: [], dice: [ document.getElementById("crisis_die_1"), document.getElementById("crisis_die_2"), document.getElementById("barbarian_die_1"), document.getElementById("barbarian_die_2"), ], neutral_governors: [], barbarian_leaders: [], rival_emperors: [], legions: [], barbarians: [ [], [], [], [], [] ], generals: [ [], [], [], [] ], governors: [ [], [], [], [] ], castra: [ [], [], [], [] ], mcastra: [], } function get_province_governor_player(where) { let np = view.legacy.length for (let p = 0; p < np; ++p) for (let i = 0; i < 6; ++i) if (get_governor_location(first_governor[p] + i) === where) return p return -1 } function is_neutral_province(where) { if (is_no_place_governor(where)) return false return get_province_governor_player(where) < 0 } function show(elt) { elt.classList.remove("hide") } function hide(elt) { elt.classList.add("hide") } function toggle_pieces() { ui.pieces.classList.toggle("hide") } function create(t, p, ...c) { let e = document.createElement(t) Object.assign(e, p) e.append(c) if (p.my_action) register_action(e, p.my_action, p.my_id) return e } function create_thing(p) { let e = create("div", p) ui.pieces.appendChild(e) return e } function create_piece(id, action, css_class, dom_id) { if (dom_id) return create_thing({ className: css_class + " hide", id: dom_id, my_action: action, my_id: id }) return create_thing({ className: css_class + " hide", my_action: action, my_id: id }) } let action_register = [] function register_action(target, action, id) { target.my_action = action target.my_id = id target.onmousedown = (evt) => on_click_action(evt, target) action_register.push(target) } function on_click_action(evt, target) { if (evt.button === 0) { if (target.my_stack) { evt.stopPropagation() if (focus_stack(target.my_stack)) if (!send_action(target.my_action, target.my_id)) blur_stack() } else { if (send_action(target.my_action, target.my_id)) evt.stopPropagation() } } } document.getElementById("map").addEventListener("mousedown", function (evt) { if (evt.button === 0) blur_stack() }) function create_building(region, className, xoff, yoff) { let [ x, y, w, h ] = LAYOUT_SUPPORT[region] let e = create_thing({ className }) e.style.left = x + (w>>1) + xoff - 46 + "px" e.style.top = y + h + yoff + "px" return e } 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_init() { let c = 1 function init_cards(n, className) { for (let i = 0; i < n; ++i) ui.cards[c + i] = create("div", { className, my_action: "card", my_id: c + i }) c += n } init_cards(12, "card influence_m1") init_cards(12, "card influence_s1") init_cards(12, "card influence_p1") init_cards(9, "card influence_m2") init_cards(9, "card influence_s2") init_cards(9, "card influence_p2") init_cards(8, "card influence_m3") init_cards(8, "card influence_s3") init_cards(8, "card influence_p3") init_cards(6, "card influence_m4") init_cards(6, "card influence_s4") init_cards(6, "card influence_p4") for (let i = 0; i < 33; ++i) ui.legions[i] = create_piece(i, "legion", "legion", "legion_" + i) ui.barbarian_leaders[0] = create_piece(0, "barbarian_leader", "goths", "cniva") ui.barbarian_leaders[1] = create_piece(1, "barbarian_leader", "sassanids", "ardashir") ui.barbarian_leaders[2] = create_piece(2, "barbarian_leader", "sassanids", "shapur") ui.rival_emperors[0] = create_piece(0, "rival_emperor", "rival_emperor", "postumus") ui.rival_emperors[1] = create_piece(1, "rival_emperor", "rival_emperor", "priest_king") ui.rival_emperors[2] = create_piece(2, "rival_emperor", "rival_emperor", "zenobia") for (let i = 0; i < 10; ++i) { ui.barbarians[0][i] = create_piece(0 + i, "barbarian", "alamanni") ui.barbarians[1][i] = create_piece(10 + i, "barbarian", "franks") ui.barbarians[2][i] = create_piece(20 + i, "barbarian", "goths") ui.barbarians[3][i] = create_piece(30 + i, "barbarian", "nomads") ui.barbarians[4][i] = create_piece(40 + i, "barbarian", "sassanids") } for (let p = 0; p < 4; ++p) { for (let g = 0; g < 6; ++g) { ui.castra[p][g] = create_thing({ className: "castra hide" }) ui.governors[p][g] = create_piece(p * 6 + g, "governor", PLAYER_CLASS[p] + " governor n" + g) ui.generals[p][g] = create_piece(p * 6 + g, "general", PLAYER_CLASS[p] + " general n" + g) } } for (let region = 0; region < 12; ++region) { ui.mcastra[region] = create_thing({ className: "castra hide" }) ui.militia[region] = create_thing({ className: "militia hide", my_action: "militia", my_id: region }) ui.capital[region] = document.getElementById(REGION_NAME[region] + "_Capital") ui.quaestor[region] = document.getElementById(REGION_NAME[region] + "_Quaestor") if (true) { ui.amphitheater[region] = create_building(region, "amphitheater hide", -48 - 3, 6) ui.basilica[region] = create_building(region, "basilica hide", 48 + 3, 6) ui.limes[region] = create_building(region, "limes hide", 0, 6+25) } else { ui.amphitheater[region] = create_building(region, "amphitheater hide", -96 - 5, 6) ui.basilica[region] = create_building(region, "basilica hide", 0, 6) ui.limes[region] = create_building(region, "limes hide", 96 + 5, 6) } register_action(ui.capital[region], "capital", region) register_action(ui.regions[region], "region", region) ui.neutral_governors[region] = create_thing({ className: "neutral governor hide" }) } for (let region = 12; region < 21; ++region) { register_action(ui.regions[region], "region", region) } } let stack_count = new Array(12 + 5).fill(0) let stack_focus = 0 let stack_cache = {} function focus_stack(id) { if (stack_focus !== id) { stack_focus = id on_update() let stack = stack_cache[id] return stack && stack.length <= 1 } return true } function blur_stack() { if (stack_focus !== 0) { stack_focus = 0 on_update() } } function layout_stack(id, list, region, in_capital) { let [ x, y, w, h ] = LAYOUT_XY[region] let dx = 8 let dy = 8 let z = 1 stack_cache[id] = list if (in_capital) { x += 5 y += 5 } else { x += stack_count[region] * 80 stack_count[region] += 1 } if (list.length > 5) { dx = 5 dy = 5 } if (stack_focus === id) { dx = 24 dy = 16 } for (let item of list) { item.style.left = x + "px" item.style.top = y + "px" item.style.zIndex = z item.my_stack = id x -= dx y -= dy z += 1 } } function layout_available(list, dx, x0, y0) { let y = 1650 + 45 - y0 let x = 25 + x0 for (let item of list) { item.style.left = x + "px" item.style.top = y + "px" item.style.zIndex = 1 item.my_stack = 0 x += dx } } function layout_governor(e, color, region) { e.className = color + " governor s" + get_support(region) e.style.left = (LAYOUT_SUPPORT[region][0] - 1) + "px" e.style.top = (LAYOUT_SUPPORT[region][1] - 1) + "px" } function layout_governor_available(e, color) { e.className = color + " governor" } function layout_governor_unavailable(e, color, ix) { e.className = color + " governor n" + ix } function on_update() { let player_count = view.legacy.length stack_cache = {} ui.body.classList.toggle("p1", view.solo === 1) ui.body.classList.toggle("p2", player_count === 2) ui.body.classList.toggle("p3", player_count === 3) ui.body.classList.toggle("p4", player_count === 4) ui.header.classList.toggle("player_red", view.current === 0) ui.header.classList.toggle("player_blue", view.current === 1) ui.header.classList.toggle("player_yellow", view.current === 2) ui.header.classList.toggle("player_green", view.current === 3) if (player_count < 4) hide(document.getElementById("role_Green")) else show(document.getElementById("role_Green")) if (player_count < 3) hide(document.getElementById("role_Yellow")) else show(document.getElementById("role_Yellow")) ui.hand.replaceChildren() ui.draw.replaceChildren() ui.discard.replaceChildren() ui.market.replaceChildren() for (let pi = 0; pi < player_count; ++pi) { let legacy = view.legacy[pi] let turns = view.emperor_turns[pi] if (legacy > 40) { legacy -= 40 ui.legacy[pi].classList.toggle("legacy_40", true) } else { ui.legacy[pi].classList.toggle("legacy_40", false) } let y = 30 for (let k = 0; k < pi; ++k) { let k_legacy = view.legacy[k] if (k_legacy > 40) k_legacy -= 40 if (legacy === k_legacy) y += 20 } show(ui.legacy[pi]) ui.legacy[pi].style.left = Math.round(43 + legacy * 60.2) + "px" ui.legacy[pi].style.top = (2 + y) + "px" y = 30 for (let k = 0; k < pi; ++k) { let k_turns = view.emperor_turns[k] if (turns === k_turns) y += 20 } show(ui.emperor_turns[pi]) ui.emperor_turns[pi].style.left = Math.round(41 + turns * 60.2) + "px" ui.emperor_turns[pi].style.top = (0 + y) + "px" } for (let pi = player_count; pi < 4; ++pi) { hide(ui.legacy[pi]) hide(ui.emperor_turns[pi]) } for (let region = 0; region < 12; ++region) { if (has_quaestor(region)) show(ui.quaestor[region]) else hide(ui.quaestor[region]) if (has_amphitheater(region)) show(ui.amphitheater[region]) else hide(ui.amphitheater[region]) if (has_basilica(region)) show(ui.basilica[region]) else hide(ui.basilica[region]) if (has_limes(region)) show(ui.limes[region]) else hide(ui.limes[region]) if (has_militia(region)) show(ui.militia[region]) else hide(ui.militia[region]) } for (let i = 0; i < 33; ++i) { if (is_legion_unused(i)) hide(ui.legions[i]) else show(ui.legions[i]) if (is_legion_reduced(i)) ui.legions[i].classList.toggle("reduced", true) else ui.legions[i].classList.toggle("reduced", false) } // TODO: Cniva, Ardashir, and Shapur // TODO: Zenobia, Postumus, Priest King let tribe_count = TRIBE_COUNT[player_count] for (let tribe = 0; tribe < TRIBE_COUNT[player_count]; ++tribe) { for (let i = 0; i < 10; ++i) { show(ui.barbarians[tribe][i]) if (is_barbarian_inactive(first_barbarian[tribe] + i)) ui.barbarians[tribe][i].classList.toggle("inactive", true) else ui.barbarians[tribe][i].classList.toggle("inactive", false) } } for (let tribe = TRIBE_COUNT[player_count]; tribe < 5; ++tribe) { for (let i = 0; i < 10; ++i) { hide(ui.barbarians[tribe][i]) } } stack_count.fill(1) for (let region = 0; region < 12 + 5; ++region) { for (let tribe = 0; tribe < tribe_count; ++tribe) { let active_barbarians = [] let inactive_barbarians = [] // TODO: Cniva, Ardashir, and Shapur for (let i = 0; i < 10; ++i) { let loc = get_barbarian_location(first_barbarian[tribe], i) let inactive = is_barbarian_inactive(first_barbarian[tribe], i) if (loc === region) { if (inactive) inactive_barbarians.push(ui.barbarians[tribe][i]) else active_barbarians.push(ui.barbarians[tribe][i]) } } if (active_barbarians.length > 0) layout_stack(region * 10 + tribe * 2 + 0, active_barbarians, region, false) if (inactive_barbarians.length > 0) layout_stack(region * 10 + tribe * 2 + 1, inactive_barbarians, region, false) } } for (let region = 0; region < 12; ++region) { if (has_militia(region)) { let lone_militia = true for (let pi = 0; pi < player_count; ++pi) { for (let i = 0; i < 6; ++i) { let loc = get_general_location(first_general[pi] + i) let inside = is_general_inside_capital(first_general[pi] + i) if (loc === region && inside) lone_militia = false } } if (lone_militia) { let mcastra = has_militia_castra(region) if (mcastra) { show(ui.mcastra[region]) layout_stack(0, [ ui.militia[region], ui.mcastra[region] ], region, true) } else { hide(ui.mcastra[region]) layout_stack(0, [ ui.militia[region] ], region, true) } } } if (is_no_place_governor(region)) { hide(ui.neutral_governors[region]) } else { if (is_neutral_province(region)) { show(ui.neutral_governors[region]) layout_governor(ui.neutral_governors[region], "neutral", region) } else { hide(ui.neutral_governors[region]) } } } for (let pi = 0; pi < player_count; ++pi) { let avail_stack = [] for (let ai = 0; ai < 6; ++ai) { let id = ARMY + first_general[pi] + ai let region = get_general_location(first_general[pi] + ai) let inside = is_general_inside_capital(first_general[pi] + ai) let castra = has_general_castra(first_general[pi] + ai) let e = ui.generals[pi][ai] show(e) if (region < 21) { let stack = [] if (has_militia(region) && inside) stack.push(ui.militia[region]) for (let tribe = 0; tribe < tribe_count; ++tribe) { for (let i = 0; i < 10; ++i) { let loc = get_barbarian_location(first_barbarian[tribe] + i) if (loc === id) stack.push(ui.barbarians[tribe][i]) } } for (let i = 0; i < 33; ++i) { let loc = get_legion_location(i) if (loc === id) stack.push(ui.legions[i]) } stack.push(e) if (castra) { show(ui.castra[pi][ai]) stack.push(ui.castra[pi][ai]) } else { hide(ui.castra[pi][ai]) } if (inside) layout_stack(id, stack, region, true) else layout_stack(id, stack, region, false) } else { avail_stack.push(e) } e.classList.toggle("unavailable", region === UNAVAILABLE) } layout_available(avail_stack, 63, pi * 625 + 0, 30) } for (let pi = 0; pi < player_count; ++pi) { let avail_stack = [] for (let ai = 0; ai < 6; ++ai) { let id = 100 + 100 * pi + ai let region = get_governor_location(first_governor[pi] + ai) let e = ui.governors[pi][ai] if (region >= 1 && region < 12) { layout_governor(e, PLAYER_CLASS[pi], region) } else { if (region === AVAILABLE) layout_governor_available(e, PLAYER_CLASS[pi]) else layout_governor_unavailable(e, PLAYER_CLASS[pi], ai) avail_stack.push(e) } e.classList.toggle("unavailable", region === UNAVAILABLE) } layout_available(avail_stack, 58, pi * 625 + 325, 27) } ui.dice[0].className = "dice black d" + view.dice[0] ui.dice[1].className = "dice white d" + view.dice[1] ui.dice[2].className = "dice black d" + view.dice[2] ui.dice[3].className = "dice white d" + view.dice[3] if (view.events) { for (let c of view.events) ui.played.appendChild(ui.cards[c]) } ui.played.replaceChildren() if (view.played) { for (let c of view.played) ui.played.appendChild(ui.cards[c]) } ui.hand.replaceChildren() if (view.hand) { for (let c of view.hand) ui.hand.appendChild(ui.cards[c]) } ui.draw.replaceChildren() if (view.draw) { for (let c of view.draw) ui.draw.appendChild(ui.cards[c]) } ui.discard.replaceChildren() if (view.discard) { for (let c of view.discard) ui.discard.appendChild(ui.cards[c]) } ui.market.replaceChildren() for (let pile of view.market) { if (pile.length > 0) ui.market.appendChild(ui.cards[pile[0]]) } for (let e of action_register) e.classList.toggle("action", is_action(e.my_action, e.my_id)) action_button("enter", "Enter Capital") action_button("leave", "Leave Capital") action_button("spend", "Spend") action_button("roll", "Roll") action_button("militia", "Place Militia") action_button("support", "Increase Support Level") action_button("hold_games", "Hold Games") action_button("build_improvement", "Build an Improvement") action_button("disperse_mob", "Disperse Mob") action_button("train_legions", "Train Legions") action_button("add_legion_to_army", "Add Legion to Army") action_button("move_army", "Move Army") action_button("initiate_battle", "Initiate Battle") action_button("amphitheater", "Amphitheater") action_button("basilica", "Basilica") action_button("limes", "Limes") action_button("recall", "Recall") action_button("recruit", "Recruit") action_button("place", "Place") action_button("create_army", "Create Army") action_button("end_actions", "End Actions") action_button("done", "Done") action_button("undo", "Undo") } on_init() scroll_with_middle_mouse("main")