"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 const class_name = [ "armor", "infantry", "anti-tank", "artillery" ] const ARMOR = 0 const INFANTRY = 1 const ANTITANK = 2 const ARTILLERY = 3 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: [], hex_x: [], hex_y: [], units: [], battle_units: [], cards: [], axis_supply: document.getElementById("axis_supply"), allied_supply: document.getElementById("allied_supply"), turn_info: document.getElementById("turn_info"), 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"), onmap: document.getElementById("units"), focus: null, } const AXIS = 'Axis' const ALLIED = 'Allied' // === 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 set_unit_disrupted(u) { view.units[u] |= UNIT_DISRUPTED_MASK } function clear_unit_disrupted(u) { view.units[u] &= ~UNIT_DISRUPTED_MASK } function unit_hex(u) { return (view.units[u] & UNIT_HEX_MASK) >> UNIT_HEX_SHIFT } function set_unit_hex(u, x) { view.units[u] = (view.units[u] & ~UNIT_HEX_MASK) | (x << UNIT_HEX_SHIFT) } function is_unit_supplied(u) { return ((view.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT) !== 0 } function unit_supply(u) { let src = (view.units[u] & UNIT_SUPPLY_MASK) >> UNIT_SUPPLY_SHIFT return hex_from_supply_source[src] } function set_unit_supply(u, hex) { let src = supply_source_from_hex(hex) view.units[u] = (view.units[u] & ~UNIT_SUPPLY_MASK) | (src << UNIT_SUPPLY_SHIFT) } function unit_lost_steps(u) { return (view.units[u] & UNIT_STEPS_MASK) >> UNIT_STEPS_SHIFT } function set_unit_lost_steps(u, n) { view.units[u] = (view.units[u] & ~UNIT_STEPS_MASK) | (n << UNIT_STEPS_SHIFT) } function unit_steps(u) { return units[u].steps - unit_lost_steps(u) } function set_unit_steps(u, n) { set_unit_lost_steps(u, units[u].steps - n) } function is_unit_moved(u) { return set_has(view.moved, u) } 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) { if (Array.isArray(view.selected)) return view.selected.includes(unit) return view.selected === unit } function is_artillery_unit(u) { return units[u].class === ARTILLERY } 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_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 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_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)) send_action('unit', evt.target.unit) } } 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) { hide_supply() 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") } let showing_supply = false 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)) for (let s = 0; s < 3; ++s) ui.sides[x*3+s].classList.toggle("axis_supply", is_side_axis_supply_line(x*3+s)) ui.hexes[x].classList.toggle("allied_supply", is_hex_allied_supply(x)) for (let s = 0; s < 3; ++s) ui.sides[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) { ui.sides[x*3+s].classList.toggle("axis_supply", false) ui.sides[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 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) { 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]) { 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]) } } 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_hexes() build_units() build_cards() 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) { let start_x = ui.hex_x[hex] let start_y = ui.hex_y[hex] if (stack[hex] === ui.focus) { let height = stack[hex].length * 56 if (start_y + height + 25 > 960) start_y = 960 - height + 25 } 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 = start_x - 25 y = start_y - 25 + i * 56 z = 100 } else { if (stack[hex].length <= 1) { x = start_x - 25 + i * 11 y = start_y - 25 + i * 14 } else if (stack[hex].length <= 4) { x = start_x - 30 + i * 11 y = start_y - 30 + i * 14 } else if (stack[hex].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 } 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) || is_hex_forced_march_action(hex)) ui.hexes[hex].classList.toggle("forced_march", is_hex_forced_march_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)) 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+28].classList.add("hide") } } function update_battle_line(hex, line, test) { for (let u = 0; u < units.length; ++u) { let e = ui.battle_units[u] if (unit_hex(u) === hex && 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 // TODO: don't show disrupted units? if (player === ALLIED) { update_battle_line(view.battle, ui.battle_line_1, u => is_axis_unit(u) && is_artillery_unit(u)) update_battle_line(view.battle, ui.battle_line_2, u => is_axis_unit(u) && !is_artillery_unit(u)) update_battle_line(view.battle, ui.battle_line_3, u => is_allied_unit(u) && !is_artillery_unit(u)) update_battle_line(view.battle, ui.battle_line_4, u => is_allied_unit(u) && is_artillery_unit(u)) } else { update_battle_line(view.battle, ui.battle_line_1, u => is_allied_unit(u) && is_artillery_unit(u)) update_battle_line(view.battle, ui.battle_line_2, u => is_allied_unit(u) && !is_artillery_unit(u)) update_battle_line(view.battle, ui.battle_line_3, u => is_axis_unit(u) && !is_artillery_unit(u)) update_battle_line(view.battle, ui.battle_line_4, u => is_axis_unit(u) && is_artillery_unit(u)) } 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 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" } 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() update_cards() if (view.battle) update_battle() else ui.battle.classList.add("hide") if (view.pursuit) update_pursuit() else ui.pursuit.classList.add("hide") ui.axis_supply.textContent = view.axis_hand ui.allied_supply.textContent = view.allied_hand ui.turn_info.textContent = `Month: ${view.month}\nSupply Commitment: ${view.commit}` if (view.actions && (view.actions.real_card || view.actions.dummy_card)) ui.cards.forEach(elt => elt.classList.add("action")) else ui.cards.forEach(elt => elt.classList.remove("action")) 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("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("end_move", "End move") action_button("end_rout", "End rout") action_button("end_retreat", "End retreat") action_button("end_combat", "End combat") action_button("end_turn", "End turn") 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) { console.log(ui.hexes[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(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.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")