"use strict" const MAP_DPI = 75 const round = Math.round const floor = Math.floor const ceil = Math.ceil // unit types const KNIGHTS = 0 const SERGEANTS = 1 const LIGHT_HORSE = 2 const ASIATIC_HORSE = 3 const MEN_AT_ARMS = 4 const MILITIA = 5 const SERFS = 6 // asset types const PROV = 0 const COIN = 1 const LOOT = 2 const CART = 3 const SLED = 4 const BOAT = 5 const SHIP = 6 function pack1_get(word, n) { return (word >>> n) & 1 } function pack4_get(word, n) { n = n << 2 return (word >>> n) & 15 } function is_lord_action(lord) { return !!(view.actions && view.actions.lord && view.actions.lord.includes(lord)) } function is_service_action(lord) { return !!(view.actions && view.actions.service && view.actions.service.includes(lord)) } function is_vassal_action(vassal) { return !!(view.actions && view.actions.vassal && view.actions.vassal.includes(vassal)) } function is_locale_action(locale) { return !!(view.actions && view.actions.locale && view.actions.locale.includes(locale)) } const force_type_count = 7 const force_type_name = [ "knights", "sergeants", "light_horse", "asiatic_horse", "men_at_arms", "militia", "serfs" ] const asset_type_count = 7 const asset_type_name = [ "prov", "coin", "loot", "cart", "sled", "boat", "ship" ] const asset_type_x3 = [ 1, 1, 1, 0, 0, 0, 0 ] const first_p1_locale = 0 const last_p1_locale = 23 const first_p2_locale = 24 const last_p2_locale = 52 function is_p1_locale(loc) { return loc >= first_p1_locale && loc <= last_p1_locale } function is_p2_locale(loc) { return loc >= first_p2_locale && loc <= last_p2_locale } function count_vp1() { let vp = 0 for (let loc of view.castles) if (is_p2_locale(loc)) vp += 2 for (let loc of view.conquered) if (is_p2_locale(loc)) vp += data.locales[loc].vp << 1 for (let loc of view.ravaged) if (is_p2_locale(loc)) vp += 1 return vp } function count_vp2() { let vp = view.veche_vp * 2 for (let loc of view.castles) if (is_p1_locale(loc)) vp += 2 for (let loc of view.conquered) if (is_p1_locale(loc)) vp += data.locales[loc].vp << 1 for (let loc of view.ravaged) if (is_p1_locale(loc)) vp += 1 return vp } function is_card_in_use(c) { if (view.global_cards.includes(c)) return true if (view.lords.cards.includes(c)) return true if (c === 18 || c === 19 || c === 20) return true if (c === 39 || c === 40 || c === 41) return true return false } function for_each_teutonic_arts_of_war(fn) { for (let i = 0; i < 21; ++i) fn(i) } function for_each_russian_arts_of_war(fn) { for (let i = 21; i < 42; ++i) fn(i) } function for_each_friendly_arts_of_war(fn) { if (player === "Teutons") for_each_teutonic_arts_of_war(fn) else for_each_russian_arts_of_war(fn) } function for_each_enemy_arts_of_war(fn) { if (player !== "Teutons") for_each_teutonic_arts_of_war(fn) else for_each_russian_arts_of_war(fn) } const original_boxes = { "way crossroads": [1500,4717,462,149], "way wirz": [1295,4526,175,350], "way peipus-east": [2232,4197,220,480], "way peipus-north": [2053,3830,361,228], // "way peipus-west": [1988,4141,218,520], "calendar summer box1": [40,168,590,916], "calendar summer box2": [650,168,590,916], "calendar winter box3": [1313,168,590,916], "calendar winter box4": [1922,168,590,916], "calendar winter box5": [2587,168,590,916], "calendar winter box6": [3196,168,590,916], "calendar rasputitsa box7": [3860,168,590,916], "calendar rasputitsa box8": [4470,168,590,916], "calendar summer box9": [40,1120,590,916], "calendar summer box10": [650,1120,590,916], "calendar winter box11": [1313,1120,590,916], "calendar winter box12": [1922,1120,590,916], "calendar winter box13": [2587,1120,590,916], "calendar winter box14": [3196,1120,590,916], "calendar rasputitsa box15": [3860,1120,590,916], "calendar rasputitsa box16": [4470,1120,590,916], // "victory": [176,185,210,210], // "turn": [402,185,210,210], } const calendar_xy = [ [0, 0], [40,168], [650,168], [1313,168], [1922,168], [2587,168], [3196,168], [3860,168], [4470,168], [40,1120], [650,1120], [1313,1120], [1922,1120], [2587,1120], [3196,1120], [3860,1120], [4470,1120], [4470, 2052], ].map(([x,y])=>[x/4|0,y/4|0]) const locale_xy = [] const ui = { locale: [], locale_extra: [], lord_cylinder: [], lord_service: [], lord_mat: [], vassal_service: [], forces: [], routed: [], assets: [], c1: [], c2: [], arts_of_war: [], boxes: {}, veche: document.getElementById("veche"), arts_of_war_dialog: document.getElementById("arts_of_war"), arts_of_war_list: document.getElementById("arts_of_war_list"), p1_global: document.getElementById("p1_global"), p2_global: document.getElementById("p2_global"), command: document.getElementById("command"), turn: document.getElementById("turn"), vp1: document.getElementById("vp1"), vp2: document.getElementById("vp2"), } let locale_layout = new Array(data.locales.length).fill(0) let calendar_layout = new Array(18).fill(0) function clean_name(name) { return name.toLowerCase().replaceAll("&", "and").replaceAll(" ", "_") } const extra_size_100 = { town: [ 60, 42 ], castle: [ 60, 42 ], fort: [ 72, 42 ], traderoute: [ 72, 42 ], bishopric: [ 84, 60 ], city: [ 132, 72 ], novgorod: [ 156, 96 ], } const extra_size = { town: [ 45, 32 ], castle: [ 45, 32 ], fort: [ 54, 32 ], traderoute: [ 54, 32 ], bishopric: [ 63, 45 ], city: [ 100, 54 ], novgorod: [ 117, 72 ], } function toggle_pieces() { document.getElementById("pieces").classList.toggle("hide") } function on_click_locale(evt) { if (evt.button === 0) { let id = evt.target.my_locale send_action('locale', id) } } function on_focus_locale(evt) { let id = evt.target.my_locale document.getElementById("status").textContent = `(${id}) ${data.locales[id].name} - ${data.locales[id].type}` } function on_click_cylinder(evt) { if (evt.button === 0) { let id = evt.target.my_lord send_action('lord', id) } } function on_click_arts_of_war(evt) { if (evt.button === 0) { let id = evt.target.my_arts_of_war send_action('arts_of_war', id) } } function on_focus_cylinder(evt) { let id = evt.target.my_lord document.getElementById("status").textContent = `(${id}) ${data.lords[id].full_name} [${data.lords[id].command}] - ${data.lords[id].title}` } function on_click_lord_service_marker(evt) { if (evt.button === 0) { let id = evt.target.my_lord send_action('lord_service', id) } } function on_focus_lord_service_marker(evt) { let id = evt.target.my_lord document.getElementById("status").textContent = `(${id}) ${data.lords[id].full_name} - ${data.lords[id].title}` } function on_click_vassal_service_marker(evt) { if (evt.button === 0) { let id = evt.target.my_vassal send_action('vassal', id) } } function on_focus_vassal_service_marker(evt) { let id = evt.target.my_vassal let vassal = data.vassals[id] let lord = data.lords[vassal.lord] document.getElementById("status").textContent = `(${id}) ${lord.name} / ${vassal.name}` } function on_blur(evt) { document.getElementById("status").textContent = "" } function on_focus_card_tip(c) { } function on_blur_card_tip(c) { } function sub_card_name(match, p1) { let x = p1 | 0 let n = data.cards[x].name return `${n}` } function on_focus_locale_tip(loc) { ui.locale[loc].classList.add("tip") ui.locale_extra[loc].classList.add("tip") } function on_blur_locale_tip(loc) { ui.locale[loc].classList.remove("tip") ui.locale_extra[loc].classList.remove("tip") } function on_click_locale_tip(loc) { ui.locale[loc].scrollIntoView({ block:"center", inline:"center", behavior:"smooth" }) } function sub_locale_name(match, p1) { let x = p1 | 0 let n = data.locales[x].name return `${n}` } 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_card_name) text = text.replace(/%(\d+)/g, sub_locale_name) if (text.match(/^\.h1/)) { text = text.substring(4) p.className = "h1" } if (text.match(/^\.h2/)) { text = text.substring(4) if (text.startsWith("Teuton")) p.className = "h2 teutonic" else if (text.startsWith("Russian")) p.className = "h2 russian" 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" } p.innerHTML = text return p } function layout_locale_item(loc, e) { let [x, y] = locale_xy[loc] x += locale_layout[loc] * (46 + 6) e.style.top = (y - 23) + "px" e.style.left = (x - 23) + "px" locale_layout[loc] ++ } function layout_calendar_item(loc, e) { let [x, y] = calendar_xy[loc] y += 66 + calendar_layout[loc] * 42 x += 24 + calendar_layout[loc] * 6 e.style.top = (y + 4) + "px" e.style.left = (x + 4) + "px" calendar_layout[loc] ++ } function add_force(parent, type) { // TODO: reuse pool of elements? build_div(parent, "unit " + force_type_name[type], "force", type) } function add_asset(parent, type, n) { // TODO: reuse pool of elements? build_div(parent, "asset " + asset_type_name[type] + " x"+n, "asset", type) } function add_veche_vp(parent) { // TODO: reuse pool of elements? build_div(parent, "marker square conquered russian") } function update_forces(parent, forces) { parent.replaceChildren() for (let i = 0; i < force_type_count; ++i) { let n = pack4_get(forces, i) for (let k = 0; k < n; ++k) { add_force(parent, i) } } } function update_assets(parent, assets) { parent.replaceChildren() for (let i = 0; i < asset_type_count; ++i) { let n = pack4_get(assets, i) while (n >= 4) { add_asset(parent, i, 4) n -= 4 } if (asset_type_x3[i]) { while (n >= 3) { add_asset(parent, i, 3) n -= 3 } } while (n >= 2) { add_asset(parent, i, 2) n -= 2 } while (n >= 1) { add_asset(parent, i, 1) n -= 1 } } } function update_vassals(parent, lord_ix) { for (let v of data.lords[lord_ix].vassals) { let e = ui.vassal_service[v] if (view.vassals[v] === 0) { e.classList.remove("hide") parent.appendChild(e) } else { e.classList.add("hide") } e.classList.toggle("action", is_vassal_action(v)) } } function update_lord_mat(ix) { update_assets(ui.assets[ix], view.lords.assets[ix]) update_vassals(ui.assets[ix], ix) update_forces(ui.forces[ix], view.lords.forces[ix]) update_forces(ui.routed[ix], view.lords.routed_forces[ix]) } function update_lord(ix) { let locale = view.lords.locale[ix] let service = view.lords.service[ix] if (locale < 0) { ui.lord_cylinder[ix].classList.add("hide") ui.lord_service[ix].classList.add("hide") ui.lord_mat[ix].classList.add("hide") ui.lord_mat[ix].classList.remove("action") return } if (locale < 100) { layout_locale_item(locale, ui.lord_cylinder[ix]) layout_calendar_item(service, ui.lord_service[ix]) ui.lord_cylinder[ix].classList.remove("hide") ui.lord_service[ix].classList.remove("hide") ui.lord_mat[ix].classList.remove("hide") update_lord_mat(ix) } else { layout_calendar_item(locale - 100, ui.lord_cylinder[ix]) ui.lord_cylinder[ix].classList.remove("hide") ui.lord_service[ix].classList.add("hide") ui.lord_mat[ix].classList.add("hide") } ui.lord_cylinder[ix].classList.toggle("action", is_lord_action(ix)) ui.lord_service[ix].classList.toggle("action", is_service_action(ix)) ui.lord_cylinder[ix].classList.toggle("selected", ix === view.who) ui.lord_mat[ix].classList.toggle("selected", ix === view.who) } function update_veche() { ui.veche.replaceChildren() let n = view.veche_coin while (n >= 3) { add_asset(ui.veche, COIN, 3) n -= 3 } while (n >= 2) { add_asset(ui.veche, COIN, 2) n -= 2 } while (n >= 1) { add_asset(ui.veche, COIN, 1) n -= 1 } for (let i = 0; i < view.veche_vp; ++i) add_veche_vp(ui.veche) } function update_locale(loc) { ui.locale[loc].classList.toggle("action", is_locale_action(loc)) if (ui.locale_extra[loc]) ui.locale_extra[loc].classList.toggle("action", is_locale_action(loc)) } function update_arts_of_war() { if (view.actions && view.actions.arts_of_war) { ui.arts_of_war_dialog.classList.remove("hide") ui.arts_of_war_list.replaceChildren() for_each_friendly_arts_of_war(c => { if (!is_card_in_use(c)) { let elt = ui.arts_of_war[c] ui.arts_of_war_list.appendChild(elt) elt.classList.toggle("action", view.actions.arts_of_war.includes(c)) elt.classList.toggle("disabled", !view.actions.arts_of_war.includes(c)) } }) } else { ui.arts_of_war_dialog.classList.add("hide") for (let c = 0; c < 42; ++c) { let elt = ui.arts_of_war[c] elt.classList.remove("action") elt.classList.remove("disabled") } } ui.p1_global.replaceChildren() for_each_teutonic_arts_of_war(c => { if (view.global_cards.includes(c)) ui.p1_global.appendChild(ui.arts_of_war[c]) }) ui.p2_global.replaceChildren() for_each_russian_arts_of_war(c => { if (view.global_cards.includes(c)) ui.p2_global.appendChild(ui.arts_of_war[c]) }) for (let ix = 0; ix < data.lords.length; ++ix) { let side = ix < 6 ? "teutonic" : "russian" let c = view.lords.cards[(ix << 1) + 0] if (c < 0) ui.c1[ix].classList = `c1 card ${side} hide` else ui.c1[ix].classList = `c1 card ${side} aow_${c}` c = view.lords.cards[(ix << 1) + 1] if (c < 0) ui.c2[ix].classList = `c2 card ${side} hide` else ui.c2[ix].classList = `c2 card ${side} aow_${c}` } } function on_update() { locale_layout.fill(0) calendar_layout.fill(0) for (let ix = 0; ix < data.lords.length; ++ix) { if (view.lords[ix] === null) { ui.lord_cylinder[ix].classList.add("hide") ui.lord_service[ix].classList.add("hide") ui.lord_mat[ix].classList.add("hide") } else { ui.lord_cylinder[ix].classList.remove("hide") update_lord(ix) } } for (let loc = 0; loc < data.locales.length; ++loc) update_locale(loc) update_veche() if ((view.turn & 1) === 0) { if (player === "Russians") ui.command.className = `card russian aow_back` else ui.command.className = `card teutonic aow_back` } else if (view.command < 0) { if (player === "Russians") ui.command.className = `card russian cc_back` else ui.command.className = `card teutonic cc_back` } else { if (view.command < 6) ui.command.className = `card russian cc_lord_${view.command}` else ui.command.className = `card teutonic cc_lord_${view.command}` } if (view.turn & 1) ui.turn.className = `marker circle turn campaign t${view.turn>>1}` else ui.turn.className = `marker circle turn levy t${view.turn>>1}` let vp1 = count_vp1() let vp2 = count_vp2() if ((vp1 >> 1) === (vp2 >> 1)) { if (vp1 & 1) ui.vp1.className = `marker circle victory teutonic stack v${vp1>>1} half` else ui.vp1.className = `marker circle victory teutonic stack v${vp1>>1}` if (vp2 & 1) ui.vp2.className = `marker circle victory russian stack v${vp2>>1} half` else ui.vp2.className = `marker circle victory russian stack v${vp2>>1}` } else { if (vp1 & 1) ui.vp1.className = `marker circle victory teutonic v${vp1>>1} half` else ui.vp1.className = `marker circle victory teutonic v${vp1>>1}` if (vp2 & 1) ui.vp2.className = `marker circle victory russian v${vp2>>1} half` else ui.vp2.className = `marker circle victory russian v${vp2>>1}` } update_arts_of_war() action_button("ship", "Ship") action_button("boat", "Boat") action_button("cart", "Cart") action_button("sled", "Sled") action_button("capability", "Capability") action_button("done", "Done") action_button("unfed", "Unfed") action_button("end_feed", "End feed") action_button("end_pay", "End pay") action_button("end_disband", "End disband") action_button("end_actions", "End actions") action_button("end_levy", "End levy") action_button("end_muster", "End muster") action_button("end_setup", "End setup") action_button("undo", "Undo") } function build_div(parent, className, dataname, datavalue, onclick) { let e = document.createElement("div") e.className = className if (dataname) e.dataset[dataname] = datavalue if (onclick) e.addEventListener("mousedown", onclick) parent.appendChild(e) return e } function build_lord_mat(lord, ix, side, name) { let parent = document.getElementById(side === 'teutonic' ? "p1_court" : "p2_court") let mat = build_div(parent, `mat ${side} ${name} hide`) let bg = build_div(mat, "background") ui.forces[ix] = build_div(bg, "forces", "lord", ix) ui.routed[ix] = build_div(bg, "routed", "lord", ix) ui.assets[ix] = build_div(bg, "assets", "lord", ix) ui.c1[ix] = build_div(mat, `c1 card ${side} hide`, "lord", ix) ui.c2[ix] = build_div(mat, `c2 card ${side} hide`, "lord", ix) ui.lord_mat[ix] = mat } function build_arts_of_war(side, c) { let card = ui.arts_of_war[c] = document.createElement("div") card.className = `card ${side} aow_${c}` card.my_arts_of_war = c card.addEventListener("mousedown", on_click_arts_of_war) } function build_map() { data.locales.forEach((locale, ix) => { let e = ui.locale[ix] = document.createElement("div") let region = clean_name(locale.region) e.className = "locale " + locale.type + " " + region let x = round(locale.box.x * MAP_DPI / 300) let y = round(locale.box.y * MAP_DPI / 300) let w = floor((locale.box.x+locale.box.w) * MAP_DPI / 300) - x let h = floor((locale.box.y+locale.box.h) * MAP_DPI / 300) - y if (locale.type === 'town') { locale_xy[ix] = [ round(x + w / 2), y - 24 ] x -= 11 y -= 5 w += 16 h += 5 } else if (locale.type === 'region') { locale_xy[ix] = [ round(x + w / 2), round(y + h / 2) ] x -= 3 y -= 4 } else { locale_xy[ix] = [ round(x + w / 2), y - 36 ] x -= 2 y -= 2 w -= 2 h -= 2 } e.style.left = x + "px" e.style.top = y + "px" e.style.width = w + "px" e.style.height = h + "px" e.my_locale = ix e.addEventListener("mousedown", on_click_locale) e.addEventListener("mouseenter", on_focus_locale) e.addEventListener("mouseleave", on_blur) document.getElementById("locales").appendChild(e) if (locale.type !== 'region') { e = ui.locale_extra[ix] = document.createElement("div") e.className = "locale_extra " + locale.type + " " + region let cx = x + (w >> 1) + 4 let ew = extra_size[locale.type][0] let eh = extra_size[locale.type][1] e.style.top = (y - eh) + "px" e.style.left = (cx - ew/2) + "px" e.style.width = ew + "px" e.style.height = eh + "px" e.my_locale = ix e.addEventListener("mousedown", on_click_locale) e.addEventListener("mouseenter", on_focus_locale) e.addEventListener("mouseleave", on_blur) document.getElementById("locales").appendChild(e) } }) let x = 160 let y = 2740 data.lords.forEach((lord, ix) => { let e = ui.lord_cylinder[ix] = document.createElement("div") e.className = "cylinder lord " + clean_name(lord.side) + " " + clean_name(lord.name) + " hide" e.my_lord = ix e.addEventListener("mousedown", on_click_cylinder) e.addEventListener("mouseenter", on_focus_cylinder) e.addEventListener("mouseleave", on_blur) document.getElementById("pieces").appendChild(e) e = ui.lord_service[ix] = document.createElement("div") e.className = "service_marker lord image" + lord.image + " " + clean_name(lord.side) + " " + clean_name(lord.name) + " hide" e.my_lord = ix e.addEventListener("mousedown", on_click_lord_service_marker) e.addEventListener("mouseenter", on_focus_lord_service_marker) e.addEventListener("mouseleave", on_blur) document.getElementById("pieces").appendChild(e) build_lord_mat(lord, ix, clean_name(lord.side), clean_name(lord.name)) x += 70 }) data.vassals.forEach((vassal, ix) => { let lord = data.lords[vassal.lord] let e = ui.vassal_service[ix] = document.createElement("div") e.className = "service_marker vassal image" + vassal.image + " " + clean_name(lord.side) + " " + clean_name(vassal.name) + " hide" e.my_vassal = ix e.addEventListener("mousedown", on_click_vassal_service_marker) e.addEventListener("mouseenter", on_focus_vassal_service_marker) e.addEventListener("mouseleave", on_blur) document.getElementById("pieces").appendChild(e) }) for (let name in original_boxes) { let x = round(original_boxes[name][0] * MAP_DPI / 300) let y = round(original_boxes[name][1] * MAP_DPI / 300) let w = round(original_boxes[name][2] * MAP_DPI / 300) - 8 let h = round(original_boxes[name][3] * MAP_DPI / 300) - 8 let e = ui.boxes[name] = document.createElement("div") e.className = "box " + name e.style.left = x + "px" e.style.top = y + "px" e.style.width = w + "px" e.style.height = h + "px" document.getElementById("boxes").appendChild(e) } for (let c = 0; c < 21; ++c) build_arts_of_war("teutonic", c) for (let c = 21; c < 42; ++c) build_arts_of_war("russian", c) } build_map() // drag_element_with_mouse("#battle", "#battle_header") drag_element_with_mouse("#arts_of_war", "#arts_of_war_header") scroll_with_middle_mouse("main")