"use strict" // https://www.redblobgames.com/grids/hexagons/ const svgNS = "http://www.w3.org/2000/svg" const round = Math.round const sqrt = Math.sqrt // refit and queue hexes const MALTA = 4 const hex_special = [ 47, 48, 49, 102, 127, MALTA ] const unit_count = 94 const EXIT_HEXES = [ 99, 148, 197 ] // [ 99, 123, 148 ] //, 172, 197 ] const REFIT_HEXES = [ 48, 102 ] const BENGHAZI = 54 const TOBRUK = 37 const BARDIA = 40 const SS_NONE = 0 const SS_BASE = 1 const SS_BARDIA = 2 const SS_BENGHAZI = 3 const SS_TOBRUK = 4 const SS_OASIS = 5 const DEPLOY = 1 const ELIMINATED = 2 const ARMOR = 0 const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 function is_axis_unit(u) { return (u >= 0 && u <= 33) } function is_italian_unit(u) { return (u >= 0 && u <= 13) } function is_german_unit(u) { return (u >= 14 && u <= 33) } function is_allied_unit(u) { return (u >= 34 && u <= 93) } function is_elite_unit(u) { return unit_elite[u] } function is_armor_unit(u) { return unit_class[u] === ARMOR } function is_infantry_unit(u) { return unit_class[u] === INFANTRY } function is_antitank_unit(u) { return unit_class[u] === ANTITANK } function is_artillery_unit(u) { return unit_class[u] === ARTILLERY } function is_recon_unit(u) { return unit_speed[u] === 4 } function is_mechanized_unit(u) { return unit_speed[u] === 3 } function is_motorized_unit(u) { return unit_speed[u] === 2 } function is_leg_unit(u) { return unit_speed[u] === 1 } function fortress_bit(fortress) { if (fortress === BARDIA) return 1 if (fortress === BENGHAZI) return 2 if (fortress === TOBRUK) return 4 return 0 } function is_fortress_axis_controlled(fortress) { return (view.fortress & fortress_bit(fortress)) === 0 } function set_has(set, item) { if (!set) return false 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 } let ui = { hexes: [], sides: [], lines: [], hex_x: [], hex_y: [], units: [], battle_units: [], cards: [], minefields: [], months: [], axis_supply: document.getElementById("axis_supply"), allied_supply: document.getElementById("allied_supply"), hand: document.getElementById("hand"), battle: document.getElementById("battle"), battle_hits: [ document.getElementById("hits_armor"), document.getElementById("hits_infantry"), document.getElementById("hits_antitank"), document.getElementById("hits_artillery") ], battle_buttons: [ document.getElementById("target_armor_button"), document.getElementById("target_infantry_button"), document.getElementById("target_antitank_button"), document.getElementById("target_artillery_button") ], battle_header: document.getElementById("battle_header"), battle_message: document.getElementById("battle_message"), battle_line_1: document.getElementById("battle_line_1"), battle_line_2: document.getElementById("battle_line_2"), battle_line_3: document.getElementById("battle_line_3"), battle_line_4: document.getElementById("battle_line_4"), pursuit: document.getElementById("pursuit"), pursuit_hits: document.getElementById("pursuit_hits"), pursuit_header: document.getElementById("pursuit_header"), pursuit_message: document.getElementById("pursuit_message"), pursuit_line_1: document.getElementById("pursuit_line_1"), pursuit_line_2: document.getElementById("pursuit_line_2"), units_holder: document.getElementById("units"), minefields_holder: document.getElementById("minefields"), focus: null, loaded: false, } const AXIS = 'Axis' const ALLIED = 'Allied' function is_map_hex(x) { return x < hex_exists.length && hex_exists[x] === 1 } // === UNIT STATE === const UNIT_DISRUPTED_SHIFT = 0 const UNIT_DISRUPTED_MASK = 1 << UNIT_DISRUPTED_SHIFT const UNIT_STEPS_SHIFT = 1 const UNIT_STEPS_MASK = 3 << UNIT_STEPS_SHIFT const UNIT_SUPPLY_SHIFT = 3 const UNIT_SUPPLY_MASK = 7 << UNIT_SUPPLY_SHIFT const UNIT_HEX_SHIFT = 6 const UNIT_HEX_MASK = 255 << UNIT_HEX_SHIFT function is_unit_disrupted(u) { return (view.units[u] & UNIT_DISRUPTED_MASK) === UNIT_DISRUPTED_MASK } function is_unit_undisrupted(u) { return !is_unit_disrupted(u) } function unit_hex(u) { return (view.units[u] & UNIT_HEX_MASK) >> UNIT_HEX_SHIFT } function is_unit_supplied(u) { return ((view.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) !== 0 } function unit_supply(u) { return (view.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT } function is_unit_unsupplied(u) { if (is_map_hex(unit_hex(u))) return ((view.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) === 0 return false } function unit_lost_steps(u) { return (view.units[u] & UNIT_STEPS_MASK) >> UNIT_STEPS_SHIFT } function unit_steps(u) { return unit_max_steps[u] - unit_lost_steps(u) } function is_unit_moved(u) { return set_has(view.moved, u) } function is_unit_fired(u) { return set_has(view.fired, u) || set_has(view.retreat, u) } function is_unit_revealed(u) { let reinf = hexmonth + view.month + 1 if (player === AXIS) return is_axis_unit(u) || set_has(view.revealed, u) || unit_hex(u) > reinf else if (player === ALLIED) return is_allied_unit(u) || set_has(view.revealed, u) || unit_hex(u) > reinf else return set_has(view.revealed, u) || unit_hex(u) > reinf } function is_unit_action(unit) { return !!(view.actions && view.actions.unit && view.actions.unit.includes(unit)) } function is_unit_hit_action(unit) { return !!(view.actions && view.actions.unit_hit && view.actions.unit_hit.includes(unit)) } function is_unit_selected(unit) { if (Array.isArray(view.selected)) return view.selected.includes(unit) return view.selected === unit } function is_any_hex_action(hex) { if (view.actions && view.actions.hex && view.actions.hex.includes(hex)) return true if (is_hex_forced_march_action(hex)) return true return false } function is_hex_forced_march_action(hex) { return !!(view.actions && view.actions.forced_march && view.actions.forced_march.includes(hex)) } function is_hex_axis_supply(hex) { return view.axis_supply[hex] > 0 } function is_hex_axis_controlled(hex) { return set_has(view.axis_hexes, hex) } function is_hex_allied_controlled(hex) { return set_has(view.allied_hexes, hex) } function is_side_axis_controlled(side) { return set_has(view.axis_sides, side) } function is_side_allied_controlled(side) { return set_has(view.allied_sides, side) } function is_side_axis_supply_line(side) { return view.axis_supply_line[side] > 0 } function is_hex_allied_supply(hex) { return view.allied_supply[hex] > 0 } function is_side_allied_supply_line(side) { return view.allied_supply_line[side] > 0 } function is_hex_selected(hex) { if (hex === view.pursuit || hex === view.battle || hex === view.selected_hexes) return true if (Array.isArray(view.selected_hexes) && view.selected_hexes.includes(hex)) return true return false } function is_setup_hex(hex) { return hex === DEPLOY } function is_month_hex(hex) { return hex >= hexmonth } function focus_stack(stack, hex) { if (ui.focus !== stack) { ui.focus = stack update_map() return stack.length <= 1 || is_setup_hex(hex) } return true } function blur_stack() { if (ui.focus !== null) { ui.focus = null update_map() } } function on_blur(evt) { document.getElementById("status").textContent = "" } function on_click_real_card(evt) { send_action("real_card") } function on_click_dummy_card(evt) { send_action("dummy_card") } function on_click_hex(evt) { if (evt.button === 0) { hide_supply() if (send_action('hex', evt.target.hex)) evt.stopPropagation() if (send_action('forced_march', evt.target.hex)) evt.stopPropagation() } } function on_click_unit(evt) { if (evt.button === 0) { hide_supply() evt.stopPropagation() if (focus_stack(evt.target.stack, evt.target.hex)) if (!send_action('unit', evt.target.unit)) blur_stack() } } function on_click_battle_unit(evt) { if (evt.button === 0) { send_action('unit', evt.target.unit) send_action('unit_hit', evt.target.unit) } } document.getElementById("map").addEventListener("mousedown", function (evt) { if (evt.button === 0) { hide_supply() blur_stack() } }) function for_each_side_in_path(path, fn) { for (let i = 1; i < path.length; ++i) fn(to_side_id(path[i-1], path[i])) } function on_focus_hex(evt) { let hex = evt.target.hex let text = hex_name[hex] if (view) { if (hex === BARDIA || hex === BENGHAZI || hex === TOBRUK) { if (is_fortress_axis_controlled(hex)) text += " - Axis control" else text += " - Allied control" } else { if (is_hex_axis_controlled(hex)) text += " - Axis control" if (is_hex_allied_controlled(hex)) text += " - Allied control" } } document.getElementById("status").textContent = text } const unit_description = [] for (let u = 0; u <= 93; ++u) { let t = "" if (is_italian_unit(u)) t += " Italian" if (is_german_unit(u)) t += " German" if (is_allied_unit(u)) t += " Allied" if (is_elite_unit(u) && !is_german_unit(u)) t += " Elite" if (is_armor_unit(u)) { if (is_recon_unit(u)) t += " Recon" t += " Armor" } if (is_infantry_unit(u)) { if (is_mechanized_unit(u)) t += " Mechanized" if (is_motorized_unit(u)) t += " Motorized" t += " Infantry" } if (is_antitank_unit(u)) { if (is_mechanized_unit(u)) t += " Mobile" if (is_motorized_unit(u)) t += " Motorized" t += " Anti-tank" } if (is_artillery_unit(u)) { if (is_mechanized_unit(u)) t += " Self Propelled" t += " Artillery" } unit_description[u] = t } function on_focus_unit(evt) { let u = evt.target.unit let t = "" if (is_unit_revealed(u)) { t += " " + unit_description[u] t += " \"" + unit_name[u] + "\"" } else { t = is_allied_unit(u) ? "Allied unit" : "Axis unit" } if (is_unit_disrupted(u)) t += " - disrupted" if (is_unit_unsupplied(u)) t += " - unsupplied" else { if (unit_supply(u) === SS_BARDIA) t += " - Bardia supply" if (unit_supply(u) === SS_TOBRUK) t += " - Tobruk supply" if (unit_supply(u) === SS_BENGHAZI) t += " - Benghazi supply" if (unit_supply(u) === SS_OASIS) t += " - Oasis supply" } if (is_unit_moved(u)) t += " - moved" if (is_unit_fired(u)) t += " - fired" if (unit_hex(u) === ELIMINATED) t += " - eliminated" document.getElementById("status").textContent = t } function on_focus_battle_unit(evt) { let u = evt.target.unit let t = unit_description[u] if (is_unit_fired(u)) t += " - fired" document.getElementById("status").textContent = t } function toggle_units() { document.getElementById("units").classList.toggle("hide") } let showing_supply = false function toggle_supply() { if (!showing_supply) send_query('supply') else hide_supply() } function show_supply(reply) { showing_supply = true view.axis_supply = reply.axis_supply view.axis_supply_line = reply.axis_supply_line view.allied_supply = reply.allied_supply view.allied_supply_line = reply.allied_supply_line for (let x of all_hexes) { ui.hexes[x].classList.toggle("axis_supply", is_hex_axis_supply(x)) ui.hexes[x].classList.toggle("allied_supply", is_hex_allied_supply(x)) for (let s = 0; s < 3; ++s) { if (ui.lines[x*3+s]) { ui.lines[x*3+s].classList.toggle("axis_supply", is_side_axis_supply_line(x*3+s)) ui.lines[x*3+s].classList.toggle("allied_supply", is_side_allied_supply_line(x*3+s)) } } } } function hide_supply() { if (showing_supply) { showing_supply = false for (let x of all_hexes) { ui.hexes[x].classList.toggle("axis_supply", false) ui.hexes[x].classList.toggle("allied_supply", false) for (let s = 0; s < 3; ++s) { if (ui.lines[x*3+s]) { ui.lines[x*3+s].classList.toggle("axis_supply", false) ui.lines[x*3+s].classList.toggle("allied_supply", false) } } } } } const CLEAR = 2 const PASS = 1 const ROUGH = 0 const TRAIL = 1 const TRACK = 2 const HIGHWAY = 4 // visible map width = 22 hexes: el agheila -> alexandria // visible map height = 9 hexes: oasis to derne const map_w = 25 const map_h = 9 const hexmonth = map_w * map_h let hexnext = [ 1, map_w, map_w-1, -1, -map_w, -(map_w-1) ] function to_side_id(a, b) { if (a > b) { let c = b b = a a = c } if (a + hexnext[0] === b) return a * 3 + 0 else if (a + hexnext[1] === b) return a * 3 + 1 else if (a + hexnext[2] === b) return a * 3 + 2 throw new Error("not a hexside " + a + " to " + b) } function build_hexes() { let yoff = 4 let xoff = 62 let hex_w = 121.5 let hex_r = hex_w / sqrt(3) let hex_h = hex_r * 2 let w = hex_w / 2 let a = hex_h / 2 let b = hex_h / 4 function add_line(x, y, s, side_id) { let x1, y1, x2, y2 switch (s) { case 0: x1 = (x+w); y1 = (y+b); x2 = (x+w); y2 = (y-b); break; // E case 1: x1 = (x+0); y1 = (y+a); x2 = (x+w); y2 = (y+b); break; // SE case 2: x1 = (x-w); y1 = (y+b); x2 = (x+0); y2 = (y+a); break; // SW case 3: x1 = (x-w); y1 = (y+b); x2 = (x-w); y2 = (y-b); break; // W case 4: x1 = (x-w); y1 = (y-b); x2 = (x+0); y2 = (y-a); break; // NW case 5: x1 = (x+0); y1 = (y-a); x2 = (x+w); y2 = (y-b); break; // NE } path.push("M", x1, y1, x2, y2) let side = ui.sides[side_id] = document.createElementNS(svgNS, "line") document.getElementById("mapsvg").getElementById("sides").appendChild(side) let cn = "side" if (side_limit[side_id] === 0) cn += " rough" else if (side_limit[side_id] === 1) cn += " gap" else if (side_limit[side_id] === 2) cn += " clear" if (side_road[side_id] === 1) cn += " trail" else if (side_road[side_id] === 2) cn += " track" else if (side_road[side_id] === 4) cn += " highway" side.setAttribute("class", cn) side.setAttribute("x1", x1) side.setAttribute("y1", y1) side.setAttribute("x2", x2) side.setAttribute("y2", y2) side.side = side_id } function add_path(x1, y1, x2, y2, side_id) { let line = ui.lines[side_id] = document.createElementNS(svgNS, "line") line.setAttribute("class", "path") line.setAttribute("x1", x1) line.setAttribute("y1", y1) line.setAttribute("x2", x2) line.setAttribute("y2", y2) document.getElementById("mapsvg").getElementById("lines").appendChild(line) } function add_hex(x, y) { let sm_hex_w = hex_w - 8 let sm_hex_h = sm_hex_w / sqrt(3) * 2 let ww = sm_hex_w / 2 let aa = sm_hex_h / 2 let bb = sm_hex_h / 4 return [ [ round(x), round(y-aa) ], [ round(x+ww), round(y-bb) ], [ round(x+ww), round(y+bb) ], [ round(x), round(y+aa) ], [ round(x-ww), round(y+bb) ], [ round(x-ww), round(y-bb) ] ].join(" ") } let path = [] for (let y = 0; y < map_h+1; ++y) { for (let x = 0; x < map_w+1; ++x) { let hex_id = y * map_w + x let xx = x + y/2 - 4.5 let hex_x = (xoff + hex_w * xx + hex_w/2) let hex_y = (yoff + hex_h * 3 / 4 * y + hex_h/2) ui.hex_x[hex_id] = round(hex_x) ui.hex_y[hex_id] = round(hex_y) // Add hex cell if (hex_exists[hex_id] || hex_special.includes(hex_id)) { let hex = ui.hexes[hex_id] = document.createElementNS(svgNS, "polygon") hex.setAttribute("class", "hex") hex.setAttribute("points", add_hex(hex_x, hex_y)) hex.addEventListener("mousedown", on_click_hex) hex.addEventListener("mouseenter", on_focus_hex) hex.addEventListener("mouseleave", on_blur) hex.hex = hex_id if (EXIT_HEXES.includes(hex_id)) hex.classList.add("exit") if (REFIT_HEXES.includes(hex_id)) hex.classList.add("refit") document.getElementById("mapsvg").getElementById("hexes").appendChild(hex) } } } for (let hex_id = 0; hex_id < map_w * map_h; ++hex_id) { // Add hex sides { for (let s = 0; s < 3; ++s) { let next_id = hex_id + hexnext[s] let side_id = hex_id * 3 + s add_line(ui.hex_x[hex_id], ui.hex_y[hex_id], s, side_id) if (hex_exists[hex_id] && hex_exists[next_id]) add_path(ui.hex_x[hex_id], ui.hex_y[hex_id], ui.hex_x[next_id], ui.hex_y[next_id], side_id) } } } for (let month = 1; month <= 10; ++month) { ui.hex_y[map_w * map_h + month] = 25 + 24 ui.hex_x[map_w * map_h + month] = 25 + 1956 + (month-1) * 70 ui.months[month] = document.getElementById("month" + month) } for (let month = 11; month <= 20; ++month) { ui.hex_y[map_w * map_h + month] = 25 + 24 ui.hex_x[map_w * map_h + month] = 25 + 1956 + (month-11) * 70 ui.months[month] = document.getElementById("month" + month) } document.getElementById("mapsvg").getElementById("grid").setAttribute("d", path.join(" ")) ui.benghazi = document.getElementById("mapsvg").getElementById("fortress_benghazi") ui.bardia = document.getElementById("mapsvg").getElementById("fortress_bardia") ui.tobruk = document.getElementById("mapsvg").getElementById("fortress_tobruk") ui.loaded = true } function build_units() { function build_unit(u) { let nationality = is_axis_unit(u) ? "axis" : "allied" let elt = ui.units[u] = document.createElement("div") elt.className = `unit ${nationality} u${u} r0 m` elt.addEventListener("mousedown", on_click_unit) elt.addEventListener("mouseenter", on_focus_unit) elt.addEventListener("mouseleave", on_blur) elt.unit = u elt = ui.battle_units[u] = document.createElement("div") elt.className = `unit ${nationality} u${u} r0` elt.addEventListener("mousedown", on_click_battle_unit) elt.addEventListener("mouseenter", on_focus_battle_unit) elt.addEventListener("mouseleave", on_blur) elt.unit = u } for (let u = 0; u < unit_count; ++u) { build_unit(u) } } function build_cards() { function build_card(i, real) { let elt = ui.cards[i] = document.createElement("div") if (real) { elt.className = "card real hide" elt.addEventListener("mousedown", on_click_real_card) ui.hand.appendChild(elt) } else { elt.className = "card dummy hide" elt.addEventListener("mousedown", on_click_dummy_card) ui.hand.appendChild(elt) } } for (let i = 0; i < 28; ++i) build_card(i, true) for (let i = 28; i < 42; ++i) build_card(i, false) } build_units() build_cards() function update_unit(e, u) { if (is_unit_revealed(u)) { let r = unit_lost_steps(u) e.classList.toggle("r0", r === 0) e.classList.toggle("r1", r === 1) e.classList.toggle("r2", r === 2) e.classList.toggle("r3", r === 3) e.classList.toggle("revealed", true) if (is_italian_unit(u)) e.classList.toggle("italian", true) } else { e.classList.toggle("r0", false) e.classList.toggle("r1", false) e.classList.toggle("r2", false) e.classList.toggle("r3", false) e.classList.toggle("revealed", false) if (is_italian_unit(u)) e.classList.toggle("italian", false) } } let stack_list = new Array(map_w * map_h + 21) for (let i = 0; i < stack_list.length; ++i) stack_list[i] = [[],[]] function layout_stack(stack, hex, start_x, start_y, wrap, xdir) { for (let i = 0; i < stack.length; ++i) { let u = stack[i] let e = ui.units[u] let x, y, z if (is_setup_hex(hex)) { if (view.month == 8) wrap = 14 else wrap = 12 x = start_x - 25 + ((i % wrap) | 0) * 56 y = start_y - 25 + ((i / wrap) | 0) * 56 z = 201 } else if (stack === ui.focus) { if (start_x > 2000) xdir = -1 if (start_x < 600) xdir = 1 x = start_x - 25 + xdir * ((i / wrap) | 0) * 56 y = start_y - 25 + (i % wrap) * 56 z = 200 } else { if (is_month_hex(hex)) { x = start_x - 25 + i * 3 y = start_y - 25 + i * 3 } else if (stack.length <= 1) { x = start_x - 25 + i * 11 y = start_y - 25 + i * 14 } else if (stack.length <= 4) { x = start_x - 30 + i * 11 y = start_y - 30 + i * 14 } else if (stack.length <= 8) { x = start_x - 30 + i * 4 y = start_y - 30 + i * 4 } else { x = start_x - 35 + i * 3 y = start_y - 35 + i * 3 } if (is_axis_unit(u)) z = 100 + i else z = i } e.style.top = y + "px" e.style.left = x + "px" e.style.zIndex = z update_unit(e, u) e.classList.toggle("disrupted", is_unit_disrupted(u)) e.classList.toggle("unsupplied", is_unit_unsupplied(u)) e.classList.toggle("action", !view.battle && is_unit_action(u)) e.classList.toggle("selected", !view.battle && is_unit_selected(u)) e.classList.toggle("moved", is_unit_moved(u)) e.classList.toggle("eliminated", unit_hex(u) === ELIMINATED) } } function cmp_unit_stack(a, b) { let as = a let bs = b as += is_unit_revealed(a) ? 800 : 0 bs += is_unit_revealed(b) ? 800 : 0 as += is_unit_undisrupted(a) ? 400 : 0 bs += is_unit_undisrupted(b) ? 400 : 0 as += is_unit_supplied(a) ? 200 : 0 bs += is_unit_supplied(b) ? 200 : 0 as += (is_unit_moved(a) && !is_unit_revealed(a)) ? 0 : 100 bs += (is_unit_moved(b) && !is_unit_revealed(a)) ? 0 : 100 return as - bs } function update_map() { ui.bardia.classList.toggle("axis", (view.fortress & 1) === 0) ui.benghazi.classList.toggle("axis", (view.fortress & 2) === 0) ui.tobruk.classList.toggle("axis", (view.fortress & 4) === 0) ui.bardia.classList.toggle("allied", (view.fortress & 1) !== 0) ui.benghazi.classList.toggle("allied", (view.fortress & 2) !== 0) ui.tobruk.classList.toggle("allied", (view.fortress & 4) !== 0) for (let i = 0; i < stack_list.length; ++i) { stack_list[i][0].length = 0 stack_list[i][1].length = 0 } for (let u = 0; u < unit_count; ++u) { let e = ui.units[u] let hex = unit_hex(u) if (hex === 2) { if (is_axis_unit(u)) hex = 170 else hex = 171 } if (hex >= hexmonth + view.month + 10) hex = 0 if (is_setup_hex(hex)) { if (player === "Axis" && !is_axis_unit(u)) hex = 0 if (player === "Allied" && !is_allied_unit(u)) hex = 0 if (player !== "Axis" && player !== "Allied") hex = 0 } if (view.month <= 10 && hex === MALTA) hex = 0 if (hex) { if (!ui.units_holder.contains(e)) ui.units_holder.appendChild(e) if (is_axis_unit(u)) { stack_list[hex][0].push(u) e.stack = stack_list[hex][0] } else { stack_list[hex][1].push(u) e.stack = stack_list[hex][1] } e.hex = hex } else { e.remove() } } for (let i = 0; i < stack_list.length; ++i) { stack_list[i][0].sort(cmp_unit_stack) stack_list[i][1].sort(cmp_unit_stack) } for (let i = 1; i <= 20; ++i) { ui.months[i].classList.toggle("show", (i >= view.start && i <= view.end) && (i < view.month + 10)) ui.months[i].classList.toggle("now", i === view.month) } if (view.minefields) { for (let i = ui.minefields.length; i < view.minefields.length; ++i) { let elt = ui.minefields[i] = document.createElement("div") elt.className = "minefield" ui.minefields_holder.appendChild(elt) } for (let i = 0; i < view.minefields.length; ++i) { let hex = view.minefields[i] ui.minefields[i].style.left = (ui.hex_x[hex] - 40) + "px" ui.minefields[i].style.top = (ui.hex_y[hex] + 4) + "px" ui.minefields_holder.appendChild(ui.minefields[i]) } for (let i = view.minefields.length; i < ui.minefields.length; ++i) { ui.minefields[i].remove() } } else { for (let i = 0; i < ui.minefields.length; ++i) ui.minefields[i].remove() } for (let hex = 0; hex < stack_list.length; ++hex) { let start_x = ui.hex_x[hex] let start_y = ui.hex_y[hex] let wrap = 6 if (is_setup_hex(hex)) { start_x = 1095 start_y = 25 + 8 } let shared = (stack_list[hex][0].length > 0) && (stack_list[hex][1].length > 0) for (let aa = 0; aa < 2; ++aa) { let this_y = start_y if (stack_list[hex][aa] === ui.focus) { let height = Math.min(wrap, stack_list[hex][aa].length) * 56 if (this_y + height + 25 > 960) this_y = 960 - height - 25 } if (shared) { if (aa === 0) layout_stack(stack_list[hex][aa], hex, start_x - 28, this_y + 2, wrap, -1) else layout_stack(stack_list[hex][aa], hex, start_x + 28, this_y - 2, wrap, 1) } else { layout_stack(stack_list[hex][aa], hex, start_x, this_y, wrap, 1) } } if (ui.hexes[hex]) { ui.hexes[hex].classList.toggle("action", is_any_hex_action(hex)) ui.hexes[hex].classList.toggle("forced_march", is_hex_forced_march_action(hex)) ui.hexes[hex].classList.toggle("selected", is_hex_selected(hex)) ui.hexes[hex].classList.toggle("axis_control", is_hex_axis_controlled(hex)) ui.hexes[hex].classList.toggle("allied_control", is_hex_allied_controlled(hex)) for (let s = 0; s < 3; ++s) { ui.sides[hex*3+s].classList.toggle("axis_control", is_side_axis_controlled(hex*3+s)) ui.sides[hex*3+s].classList.toggle("allied_control", is_side_allied_controlled(hex*3+s)) } } } } function update_cards() { if (view.cards) { for (let i = 0; i < 28; ++i) ui.cards[i].classList.toggle("hide", i >= view.cards[0]) for (let i = 0; i < 14; ++i) ui.cards[i+28].classList.toggle("hide", i >= view.cards[1]) } else { for (let i = 0; i < 42; ++i) ui.cards[i].classList.add("hide") } } function insert_battle_block(root, node, unit) { for (let i = 0; i < root.children.length; ++i) { let prev = root.children[i] if (prev.unit > unit) { root.insertBefore(node, prev) return } } root.appendChild(node) } function update_battle_line(hex, line, test) { for (let u = 0; u < unit_count; ++u) { let e = ui.battle_units[u] if (unit_hex(u) === hex && test(u)) { if (!line.contains(e)) insert_battle_block(line, e, u) update_unit(e, u) e.classList.toggle("selected", is_unit_selected(u)) e.classList.toggle("action", is_unit_action(u) || is_unit_hit_action(u)) e.classList.toggle("hit", is_unit_hit_action(u)) e.classList.toggle("fired", is_unit_fired(u)) } else { if (line.contains(e)) line.removeChild(e) } } } function update_battle() { ui.battle.classList.remove("hide") ui.battle_header.textContent = hex_name[view.battle] ui.battle_message.textContent = view.flash if (player === ALLIED) { update_battle_line(view.battle, ui.battle_line_1, u => is_axis_unit(u) && is_artillery_unit(u) && !is_unit_disrupted(u)) update_battle_line(view.battle, ui.battle_line_2, u => is_axis_unit(u) && !is_artillery_unit(u) && !is_unit_disrupted(u)) update_battle_line(view.battle, ui.battle_line_3, u => is_allied_unit(u) && !is_artillery_unit(u) && !is_unit_disrupted(u)) update_battle_line(view.battle, ui.battle_line_4, u => is_allied_unit(u) && is_artillery_unit(u) && !is_unit_disrupted(u)) } else { update_battle_line(view.battle, ui.battle_line_1, u => is_allied_unit(u) && is_artillery_unit(u) && !is_unit_disrupted(u)) update_battle_line(view.battle, ui.battle_line_2, u => is_allied_unit(u) && !is_artillery_unit(u) && !is_unit_disrupted(u)) update_battle_line(view.battle, ui.battle_line_3, u => is_axis_unit(u) && !is_artillery_unit(u) && !is_unit_disrupted(u)) update_battle_line(view.battle, ui.battle_line_4, u => is_axis_unit(u) && is_artillery_unit(u) && !is_unit_disrupted(u)) } for (let i = 0; i < 4; ++i) ui.battle_hits[i].textContent = view.hits[i] battle_button("battle_armor_button", "armor") battle_button("battle_infantry_button", "infantry") battle_button("battle_antitank_button", "antitank") battle_button("battle_artillery_button", "artillery") battle_button("battle_end_hits_button", "end_hits") battle_button("battle_end_fire_button", "end_fire") } function update_pursuit() { ui.pursuit.classList.remove("hide") ui.pursuit_header.textContent = "Pursuit Fire at " + hex_name[view.pursuit] ui.pursuit_message.textContent = view.flash if (player === ALLIED) { let slowest = update_battle_line(view.pursuit, ui.pursuit_line_1, u => is_axis_unit(u)) update_battle_line(view.pursuit, ui.pursuit_line_2, u => is_allied_unit(u)) } else { update_battle_line(view.pursuit, ui.pursuit_line_1, u => is_allied_unit(u)) update_battle_line(view.pursuit, ui.pursuit_line_2, u => is_axis_unit(u)) } if (view.hits === 1) ui.pursuit_hits.textContent = view.hits + " hit" else ui.pursuit_hits.textContent = view.hits + " hits" battle_button("pursuit_end_hits_button", "end_hits") battle_button("pursuit_end_fire_button", "end_fire") } function battle_button(id, action) { let button = document.getElementById(id) if (view.actions && view.actions[action]) button.classList.remove("hide") else button.classList.add("hide") } function on_update() { if (!ui.loaded) { return setTimeout(on_update, 500) } update_map() update_cards() if (view.battle) update_battle() else ui.battle.classList.add("hide") if (view.pursuit) update_pursuit() else ui.pursuit.classList.add("hide") if (view.phasing === "Axis" && view.commit >= 0) ui.axis_supply.textContent = view.commit + " + " + view.axis_hand else ui.axis_supply.textContent = view.axis_hand if (view.phasing === "Allied" && view.commit >= 0) ui.allied_supply.textContent = view.commit + " + " + view.allied_hand else ui.allied_supply.textContent = view.allied_hand for (let i = 0; i < 28; ++i) ui.cards[i].classList.toggle("action", !!(view.actions && view.actions.real_card)) for (let i = 0; i < 14; ++i) ui.cards[i+28].classList.toggle("action", !!(view.actions && view.actions.dummy_card)) action_button("discard", "Discard") action_button("keep", "Keep") action_button("select_all", "Select all") action_button("overrun", "Overrun") action_button("rommel", "Rommel") action_button("eliminate", "Eliminate") action_button("overrun", "Overrun") action_button("retreat", "Retreat") action_button("probe", "Probe") action_button("replacement", "Replacement") action_button("refit", "Refit") action_button("minefield", "Minefield") action_button("dismantle", "Dismantle") action_button("extra_supply_card", "Card") action_button("group_rommel", "Group (R)") action_button("regroup_rommel", "Regroup (R)") action_button("group", "Group") action_button("regroup", "Regroup") action_button("basic", "Basic") action_button("offensive", "Offensive") action_button("assault", "Assault") action_button("blitz", "Blitz") action_button("pass", "Pass") action_button("next", "Next") action_button("withhold", "Withhold") action_button("end_move", "End movement") action_button("end_fire", "End fire") action_button("end_rout", "End rout") action_button("end_retreat", "End retreat") action_button("end_combat", "End combat") action_button("end_deployment", "End deployment") action_button("end_buildup", "End buildup") action_button("end_turn", "End turn") confirm_action_button("confirm_end_move", "End movement?", "You have unused moves remaining. End movement anyway?") action_button("undo", "Undo") } function on_reply(q, params) { if (q === 'supply') show_supply(params) } function on_focus_hex_tip(x) { ui.hexes[x].classList.add("tip") } function on_click_hex_tip(x) { ui.hexes[x].scrollIntoView({ block:"center", inline:"center", behavior:"smooth" }) } function on_blur_hex_tip(x) { ui.hexes[x].classList.remove("tip") } function sub_hex_name(match, p1, offset, string) { let x = p1 | 0 let n = hex_name[x] return `${n}` } function sub_unit_name(match, p1, offset, string) { let u = p1 | 0 return units[u].name } function on_log_line(text, cn) { let p = document.createElement("div") if (cn) p.className = cn p.innerHTML = text return p } function on_log(text) { let p = document.createElement("div") if (text.match(/^>>/)) { text = text.substring(2) p.className = "ii" } if (text.match(/^>/)) { text = text.substring(1) p.className = "i" } text = text.replace(/&/g, "&") text = text.replace(//g, ">") text = text.replace(/#(\d+)/g, sub_hex_name) text = text.replace(/%(\d+)/g, sub_unit_name) if (text.match(/^\.h1/)) { text = text.substring(4) p.className = "h1" } if (text.match(/^\.h2/)) { text = text.substring(4) if (text.startsWith("Axis")) p.className = "h2 axis" else if (text.startsWith("Allied")) p.className = "h2 allied" else p.className = "h2" } if (text.match(/^\.h3/)) { text = text.substring(4) p.className = "h3" } if (text.match(/^\.h4/)) { text = text.substring(4) p.className = "h4" } if (text.indexOf("\n") < 0) { p.innerHTML = text } else { text = text.split("\n") p.appendChild(on_log_line(text[0])) for (let i = 1; i < text.length; ++i) p.appendChild(on_log_line(text[i], "i")) } return p } drag_element_with_mouse("#battle", "#battle_header") drag_element_with_mouse("#pursuit", "#pursuit_header") scroll_with_middle_mouse("main") fetch("map.svg") .then(r => { if (!r.ok) throw new Error("Could not fetch \"map.svg\": " + r.statusText) return r.text() }) .then(text => { document.getElementById("mapsvg").outerHTML = text build_hexes() }) .catch(error => { window.alert(error.message) })