diff options
Diffstat (limited to 'play.js')
-rw-r--r-- | play.js | 729 |
1 files changed, 729 insertions, 0 deletions
@@ -0,0 +1,729 @@ +"use strict" + +const MAP_DPI = 75 + +const round = Math.round +const floor = Math.floor +const ceil = Math.ceil + +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_teutonic_region = 0 +const last_teutonic_region = 23 +const first_russian_region = 24 +const last_russian_region = 52 + +function is_teutonic_region(loc) { + return loc >= first_teutonic_region && loc <= last_teutonic_region +} + +function is_russian_region(loc) { + return loc >= first_russian_region && loc <= last_russian_region +} + +function count_teutonic_vp() { + let vp = 0 + for (let loc of view.conquered) + if (is_russian_region(loc)) + vp += data.locales[loc].vp << 1 + for (let loc of view.ravaged) + if (is_russian_region(loc)) + vp += 1 + return vp +} + +function count_russian_vp() { + let vp = view.veche_vp * 2 + for (let loc of view.conquered) + if (is_teutonic_region(loc)) + vp += data.locales[loc].vp << 1 + for (let loc of view.ravaged) + if (is_teutonic_region(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"), + 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 ], + archbishopric: [ 156, 96 ], +} + +const extra_size = { + town: [ 45, 32 ], + castle: [ 45, 32 ], + fort: [ 54, 32 ], + traderoute: [ 54, 32 ], + bishopric: [ 63, 45 ], + city: [ 100, 54 ], + archbishopric: [ 117, 72 ], +} + +function toggle_pieces() { + document.getElementById("pieces").classList.toggle("hide") +} + +function on_click_locale(evt) { + if (evt.button === 0) { + let id = evt.target.dataset.locale | 0 + send_action('locale', id) + } +} + +function on_focus_locale(evt) { + let id = evt.target.dataset.locale | 0 + 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.dataset.lord | 0 + send_action('lord', id) + } +} + +function on_click_arts_of_war(evt) { +console.log("AOW CLICK", evt.target.dataset.arts_of_war) + if (evt.button === 0) { + let id = evt.target.dataset.arts_of_war | 0 + send_action('arts_of_war', id) + } +} + +function on_focus_cylinder(evt) { + let id = evt.target.dataset.lord | 0 + 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.dataset.lord | 0 + send_action('lord_service', id) + } +} + +function on_focus_lord_service_marker(evt) { + let id = evt.target.dataset.lord | 0 + 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.dataset.vassal | 0 + send_action('vassal', id) + } +} + +function on_focus_vassal_service_marker(evt) { + let id = evt.target.dataset.vassal | 0 + 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 `<span class="card_tip" onmouseenter="on_focus_card_tip(${x})" onmouseleave="on_blur_card_tip(${x})">${n}</span>` +} + +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 `<span class="locale_tip" onmouseenter="on_focus_locale_tip(${x})" onmouseleave="on_blur_locale_tip(${x})" onclick="on_click_locale_tip(${x})">${n}</span>` +} + +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(/>/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 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_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) + } + + 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_teutonic_vp() + let vp2 = count_russian_vp() + 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("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.dataset.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 + // XXX e.classList.add("action") + 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.dataset.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 + // XXX e.classList.add("action") + 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.dataset.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.dataset.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.dataset.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.dataset.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 + // XXX e.classList.add("action") + 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") |