"use strict" // TODO: intermediate regroup moves // https://www.redblobgames.com/grids/hexagons/ const svgNS = "http://www.w3.org/2000/svg" const round = Math.round const sqrt = Math.sqrt 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 } let ui = { hexes: [], sides: [], hex_x: [], hex_y: [], units: [], battle_units: [], 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"), onmap: document.getElementById("units"), focus: null, } const AXIS = 'Axis' const ALLIED = 'Allied' function unit_hex(u) { return view.units[u] >>> 5 } function unit_lost_steps(u) { return view.units[u] & 3 } function is_unit_supplied(u) { return (view.units[u] & 4) === 4 } function is_unit_disrupted(u) { return (view.units[u] & 8) === 8 } function is_unit_moved(u) { return (view.units[u] & 16) === 16 } function is_unit_fired(u) { return set_has(view.fired, u) } function is_unit_action(unit) { return !!(view.actions && view.actions.unit && view.actions.unit.includes(unit)) } function is_unit_selected(unit) { return !!(view.selected && view.selected.includes(unit)) } function is_allied_unit(u) { return units[u].nationality === 'allied' } function is_axis_unit(u) { return units[u].nationality !== 'allied' } function is_hex_action(hex) { return !!(view.actions && view.actions.hex && view.actions.hex.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 focus_stack(stack) { if (ui.focus !== stack) { console.log("FOCUS STACK", stack) ui.focus = stack update_map() return stack.length <= 1 } return true } function blur_stack() { if (ui.focus !== null) { console.log("BLUR STACK") ui.focus = null update_map() } } function on_blur(evt) { document.getElementById("status").textContent = "" } function on_click_hex(evt) { if (evt.button === 0) { send_action('hex', evt.target.hex) } } function on_click_unit(evt) { if (evt.button === 0) { evt.stopPropagation() if (focus_stack(evt.target.stack)) send_action('unit', evt.target.unit) return true } } function on_click_battle_unit(evt) { if (evt.button === 0) { send_action('unit', evt.target.unit) } } document.getElementById("map").addEventListener("mousedown", function (evt) { if (evt.button === 0) { blur_stack() } }) function on_focus_hex(evt) { let h = evt.target.hex let text = "(" + h + ") " + hex_name[h] for (let r in regions) if (regions[r].includes(h)) text += " - " + r document.getElementById("status").textContent = text } function on_focus_unit(evt) { let u = evt.target.unit let data = units[u] document.getElementById("status").textContent = `(${u}) ${data.nationality} ${data.elite ? "elite " : ""}${data.type} - ${data.steps} - ${data.name}` } function on_focus_battle_unit(evt) { let u = evt.target.unit let data = units[u] document.getElementById("status").textContent = `(${u}) ${data.nationality} ${data.elite ? "elite " : ""}${data.type} - ${data.steps} - ${data.name}` } function toggle_units() { document.getElementById("units").classList.toggle("hide") } 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 let hexnext = [ 1, map_w, map_w-1, -1, -map_w, -(map_w-1) ] 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_hex(x, y) { return [ [ round(x), round(y-a) ], [ round(x+w), round(y-b) ], [ round(x+w), round(y+b) ], [ round(x), round(y+a) ], [ round(x-w), round(y+b) ], [ round(x-w), round(y-b) ] ].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]) { 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 document.getElementById("mapsvg").getElementById("hexes").appendChild(hex) } // Add hex sides // if (hex_exists[hex_id]) { for (let s = 0; s < 3; ++s) { let next_id = hex_id + hexnext[s] // if (hex_exists[next_id]) { let side_id = hex_id * 3 + s add_line(hex_x, hex_y, s, side_id) } } } } } for (let month = 1; month <= 20; ++month) { ui.hex_y[map_w * map_h + month] = 24 + 37 ui.hex_x[map_w * map_h + month] = 1840 + 37 + (month-1) * 81 } document.getElementById("mapsvg").getElementById("grid").setAttribute("d", path.join(" ")) } function build_units() { function build_unit(u, data) { let elt = ui.units[u] = document.createElement("div") elt.className = `unit ${data.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 ${data.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 < units.length; ++u) { build_unit(u, units[u]) } } build_hexes() build_units() let stack = new Array(map_w * map_h + 21) for (let i = 0; i < stack.length; ++i) stack[i] = [] function update_map() { for (let i = 0; i < stack.length; ++i) stack[i].length = 0 for (let u = 0; u < units.length; ++u) { let e = ui.units[u] let hex = unit_hex(u) if (hex) { if (!ui.onmap.contains(e)) ui.onmap.appendChild(e) stack[hex].push(u) e.stack = stack[hex] } else { e.remove() } } for (let hex = 0; hex < stack.length; ++hex) { for (let i = 0; i < stack[hex].length; ++i) { let u = stack[hex][i] let e = ui.units[u] let x, y, z if (stack[hex] === ui.focus) { x = ui.hex_x[hex] - 25 y = ui.hex_y[hex] - 25 + i * 56 z = 100 } else { if (stack[hex].length <= 1) { x = ui.hex_x[hex] - 25 + i * 11 y = ui.hex_y[hex] - 25 + i * 14 } else if (stack[hex].length <= 4) { x = ui.hex_x[hex] - 30 + i * 11 y = ui.hex_y[hex] - 30 + i * 14 } else if (stack[hex].length <= 8) { x = ui.hex_x[hex] - 30 + i * 4 y = ui.hex_y[hex] - 30 + i * 4 } else { x = ui.hex_x[hex] - 35 + i * 3 y = ui.hex_y[hex] - 35 + i * 3 } z = 1 + i } e.style.top = y + "px" e.style.left = x + "px" e.style.zIndex = z 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("action", !view.battle && is_unit_action(u)) e.classList.toggle("selected", !view.battle && is_unit_selected(u)) e.classList.toggle("disrupted", is_unit_disrupted(u)) e.classList.toggle("moved", is_unit_moved(u)) // e.classList.toggle("unsupplied", !is_unit_supplied(u)) } if (ui.hexes[hex]) { ui.hexes[hex].classList.toggle("action", is_hex_action(hex)) ui.hexes[hex].classList.toggle("from", hex === view.from1 || hex === view.from2) ui.hexes[hex].classList.toggle("to", hex === view.to1 || hex === view.to2) ui.hexes[hex].classList.toggle("axis_control", is_hex_axis_controlled(hex)) ui.hexes[hex].classList.toggle("allied_control", is_hex_allied_controlled(hex)) if (view.axis_supply) { ui.hexes[hex].classList.toggle("axis_supply", is_hex_axis_supply(hex)) for (let s = 0; s < 3; ++s) ui.sides[hex*3+s].classList.toggle("axis_supply", is_side_axis_supply_line(hex*3+s)) } if (view.allied_supply) { ui.hexes[hex].classList.toggle("allied_supply", is_hex_allied_supply(hex)) for (let s = 0; s < 3; ++s) ui.sides[hex*3+s].classList.toggle("allied_supply", is_side_allied_supply_line(hex*3+s)) } 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_battle_line(line, test) { for (let u = 0; u < units.length; ++u) { let e = ui.battle_units[u] if (unit_hex(u) === view.battle && test(u)) { if (!line.contains(e)) line.appendChild(e) 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("action", is_unit_action(u)) e.classList.toggle("selected", is_unit_selected(u)) e.classList.toggle("disrupted", is_unit_disrupted(u)) e.classList.toggle("fire", 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(ui.battle_line_1, is_axis_unit) update_battle_line(ui.battle_line_2, is_allied_unit) } else { update_battle_line(ui.battle_line_1, is_allied_unit) update_battle_line(ui.battle_line_2, is_axis_unit) } target_button("armor") target_button("infantry") target_button("antitank") target_button("artillery") for (let i = 0; i < 4; ++i) ui.battle_hits[i].textContent = view.hits[i] } function target_button(action) { let button = document.getElementById("target_" + action + "_button") if (view.actions) { button.classList.remove("hide") if (view.actions[action]) button.disabled = false else button.disabled = true } else { button.classList.add("hide") } } function on_update() { update_map() if (view.battle) update_battle() else ui.battle.classList.add("hide") action_button("overrun", "Overrun") action_button("rommel", "Rommel") action_button("stop", "Stop") action_button("end_move", "End move") action_button("end_combat", "End combat") 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("undo", "Undo") } drag_element_with_mouse("#battle", "#battle_header") scroll_with_middle_mouse("main")