import {tip} from "/src/scripts/tooltips"; import {wiki} from "@/utils/linkUtils"; import {rn} from "/src/utils/numberUtils"; import {capitalize} from "@/utils/stringUtils"; import {si} from "@/utils/unitUtils"; export function overviewMilitary() { if (customization) return; closeDialogs("#militaryOverview, .stable"); if (!layerIsOn("toggleStates")) toggleStates(); if (!layerIsOn("toggleBorders")) toggleBorders(); if (!layerIsOn("toggleMilitary")) toggleMilitary(); const body = document.getElementById("militaryBody"); addLines(); $("#militaryOverview").dialog(); if (fmg.modules.overviewMilitary) return; fmg.modules.overviewMilitary = true; updateHeaders(); $("#militaryOverview").dialog({ title: "Military Overview", resizable: false, width: "fit-content", position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} }); // add listeners document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines); document.getElementById("militaryPercentage").addEventListener("click", togglePercentageMode); document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize); document.getElementById("militaryRegimentsList").addEventListener("click", () => overviewRegiments(-1)); document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate); document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData); document.getElementById("militaryWiki").addEventListener("click", () => wiki("Military-Forces")); body.addEventListener("change", function (ev) { const el = ev.target, line = el.parentNode, state = +line.dataset.id; changeAlert(state, line, +el.value); }); body.addEventListener("click", function (ev) { const el = ev.target, line = el.parentNode, state = +line.dataset.id; if (el.tagName === "SPAN") overviewRegiments(state); }); // update military types in header and tooltips function updateHeaders() { const header = document.getElementById("militaryHeader"); const units = options.military.length; header.style.gridTemplateColumns = `8em repeat(${units}, 5.2em) 4em 7em 5em 6em`; header.querySelectorAll(".removable").forEach(el => el.remove()); const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html); for (const u of options.military) { const label = capitalize(u.name.replace(/_/g, " ")); insert( `
${label} 
` ); } header.querySelectorAll(".removable").forEach(function (e) { e.addEventListener("click", function () { sortLines(this); }); }); } // add line for each state function addLines() { body.innerHTML = ""; let lines = ""; const states = pack.states.filter(s => s.i && !s.removed); for (const s of states) { const population = rn((s.rural + s.urban * urbanization) * populationRate); const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0); const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0); const rate = (total / population) * 100; const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" "); const lineData = options.military .map(u => `
${getForces(u)}
`) .join(" "); lines += /* html */ `
${lineData}
${si( total )}
${si(population)}
${rn( rate, 2 )}%
`; } body.insertAdjacentHTML("beforeend", lines); updateFooter(); // add listeners body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev))); if (body.dataset.type === "percentage") { body.dataset.type = "absolute"; togglePercentageMode(); } applySorting(militaryHeader); } function changeAlert(state, line, alert) { const s = pack.states[state]; const dif = s.alert || alert ? alert / s.alert : 0; // modifier s.alert = line.dataset.alert = alert; s.military.forEach(r => { Object.keys(r.u).forEach(u => (r.u[u] = rn(r.u[u] * dif))); // change units value r.a = d3.sum(Object.values(r.u)); // change total armies.select(`g>g#regiment${s.i}-${r.i}>text`).text(Military.getTotal(r)); // change icon text }); const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0); options.military.forEach( u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)) ); const population = rn((s.rural + s.urban * urbanization) * populationRate); const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0)); const rate = (line.dataset.rate = (total / population) * 100); line.querySelector("div[data-type='total']").innerHTML = si(total); line.querySelector("div[data-type='rate']").innerHTML = rn(rate, 2) + "%"; updateFooter(); } function updateFooter() { const lines = Array.from(body.querySelectorAll(":scope > div")); const statesNumber = (militaryFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length); const total = d3.sum(lines.map(el => el.dataset.total)); militaryFooterForcesTotal.innerHTML = si(total); militaryFooterForces.innerHTML = si(total / statesNumber); militaryFooterRate.innerHTML = rn(d3.sum(lines.map(el => el.dataset.rate)) / statesNumber, 2) + "%"; militaryFooterAlert.innerHTML = rn(d3.sum(lines.map(el => el.dataset.alert)) / statesNumber, 2); } function stateHighlightOn(event) { const state = +event.target.dataset.id; if (customization || !state) return; armies .select("#army" + state) .transition() .duration(2000) .style("fill", "#ff0000"); if (!layerIsOn("toggleStates")) return; const d = regions.select("#state" + state).attr("d"); const path = debug .append("path") .attr("class", "highlight") .attr("d", d) .attr("fill", "none") .attr("stroke", "red") .attr("stroke-width", 1) .attr("opacity", 1) .attr("filter", "url(#blur1)"); const l = path.node().getTotalLength(), dur = (l + 5000) / 2; const i = d3.interpolateString("0," + l, l + "," + l); path .transition() .duration(dur) .attrTween("stroke-dasharray", function () { return t => i(t); }); } function stateHighlightOff(event) { debug.selectAll(".highlight").each(function () { d3.select(this).transition().duration(1000).attr("opacity", 0).remove(); }); const state = +event.target.dataset.id; armies .select("#army" + state) .transition() .duration(1000) .style("fill", null); } function togglePercentageMode() { if (body.dataset.type === "absolute") { body.dataset.type = "percentage"; const lines = body.querySelectorAll(":scope > div"); const array = Array.from(lines), cache = []; const total = function (type) { if (cache[type]) cache[type]; cache[type] = d3.sum(array.map(el => +el.dataset[type])); return cache[type]; }; lines.forEach(function (el) { el.querySelectorAll("div").forEach(function (div) { const type = div.dataset.type; if (type === "rate") return; div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + "%" : "0%"; }); }); } else { body.dataset.type = "absolute"; addLines(); } } function militaryCustomize() { const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"]; const tableBody = document.getElementById("militaryOptions").querySelector("tbody"); removeUnitLines(); options.military.map(unit => addUnitLine(unit)); $("#militaryOptions").dialog({ title: "Edit Military Units", resizable: false, width: "fit-content", position: {my: "center", at: "center", of: "svg"}, buttons: { Apply: applyMilitaryOptions, Add: () => addUnitLine({ icon: "🛡️", name: "custom" + militaryOptionsTable.rows.length, rural: 0.2, urban: 0.5, crew: 1, power: 1, type: "melee" }), Restore: restoreDefaultUnits, Cancel: function () { $(this).dialog("close"); } }, open: function () { const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button"); buttons[0].addEventListener("mousemove", () => tip("Apply military units settings. All forces will be recalculated!") ); buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table")); buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings")); buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes")); } }); if (fmg.modules.overviewMilitaryCustomize) return; fmg.modules.overviewMilitaryCustomize = true; tableBody.addEventListener("click", event => { const el = event.target; if (el.tagName !== "BUTTON") return; const type = el.dataset.type; if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v)); if (type === "biomes") { const {i, name, color} = biomesData; const biomesArray = Array(i.length).fill(null); const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]})); return selectLimitation(el, biomes); } if (type === "states") return selectLimitation(el, pack.states); if (type === "cultures") return selectLimitation(el, pack.cultures); if (type === "religions") return selectLimitation(el, pack.religions); }); function removeUnitLines() { tableBody.querySelectorAll("tr").forEach(el => el.remove()); } function getLimitValue(attr) { return attr?.join(",") || ""; } function getLimitText(attr) { return attr?.length ? "some" : "all"; } function getLimitTip(attr, data) { if (!attr || !attr.length) return ""; return attr.map(i => data?.[i]?.name || "").join(", "); } function addUnitLine(unit) { const {type, icon, name, rural, urban, power, crew, separate} = unit; const row = document.createElement("tr"); const typeOptions = types .map(t => ``) .join(" "); const getLimitButton = attr => ``; row.innerHTML = /* html */ ` ${getLimitButton("biomes")} ${getLimitButton("states")} ${getLimitButton("cultures")} ${getLimitButton("religions")} `; tableBody.appendChild(row); } function restoreDefaultUnits() { removeUnitLines(); Military.getDefaultOptions().map(unit => addUnitLine(unit)); } function selectLimitation(el, data) { const type = el.dataset.type; const value = el.dataset.value; const initial = value ? value.split(",").map(v => +v) : []; const filtered = data.filter(datum => datum.i && !datum.removed); const lines = filtered.map( ({i, name, fullName, color}) => ` ` ); alertMessage.innerHTML = /* html */ `Limit unit by ${type}: ${lines.join("")}
`; $("#alert").dialog({ width: "fit-content", title: `Limit unit`, buttons: { Invert: function () { alertMessage.querySelectorAll("input").forEach(el => (el.checked = !el.checked)); }, Apply: function () { const inputs = Array.from(alertMessage.querySelectorAll("input")); const selected = inputs.reduce((acc, input) => { if (input.checked) acc.push(input.dataset.i); return acc; }, []); if (!selected.length) return tip("Select at least one element", false, "error"); const allAreSelected = selected.length === inputs.length; el.dataset.value = allAreSelected ? "" : selected.join(","); el.innerHTML = allAreSelected ? "all" : "some"; el.setAttribute("title", getLimitTip(selected, data)); $(this).dialog("close"); }, Cancel: function () { $(this).dialog("close"); } } }); } function applyMilitaryOptions() { const unitLines = Array.from(tableBody.querySelectorAll("tr")); const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_")); if (new Set(names).size !== names.length) { tip("All units should have unique names", false, "error"); return; } $("#militaryOptions").dialog("close"); options.military = unitLines.map((r, i) => { const elements = Array.from(r.querySelectorAll("input, button, select")); const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => { const {type, value} = el.dataset || {}; if (type === "icon") return el.innerHTML || "⠀"; if (type) return value ? value.split(",").map(v => parseInt(v)) : null; if (el.type === "number") return +el.value || 0; if (el.type === "checkbox") return +el.checked || 0; return el.value; }); const unit = {icon, name: names[i], rural, urban, crew, power, type, separate}; if (biomes) unit.biomes = biomes; if (states) unit.states = states; if (cultures) unit.cultures = cultures; if (religions) unit.religions = religions; return unit; }); localStorage.setItem("military", JSON.stringify(options.military)); Military.generate(); updateHeaders(); addLines(); } } function militaryRecalculate() { alertMessage.innerHTML = "Are you sure you want to recalculate military forces for all states?
Regiments for all states will be regenerated"; $("#alert").dialog({ resizable: false, title: "Remove regiment", buttons: { Recalculate: function () { $(this).dialog("close"); Military.generate(); addLines(); }, Cancel: function () { $(this).dialog("close"); } } }); } function downloadMilitaryData() { const units = options.military.map(u => u.name); let data = "Id,State," + units.map(u => capitalize(u)).join(",") + ",Total,Population,Rate,War Alert\n"; // headers body.querySelectorAll(":scope > div").forEach(function (el) { data += el.dataset.id + ","; data += el.dataset.state + ","; data += units.map(u => el.dataset[u]).join(",") + ","; data += el.dataset.total + ","; data += el.dataset.population + ","; data += rn(el.dataset.rate, 2) + "%,"; data += el.dataset.alert + "\n"; }); const name = getFileName("Military") + ".csv"; downloadFile(data, name); } }