This commit is contained in:
Azgaar 2020-03-15 17:35:07 +03:00
parent 7c74c3d29f
commit f4a84fc6d6
22 changed files with 1678 additions and 325 deletions

View file

@ -17,7 +17,7 @@ function restoreDefaultEvents() {
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement, grand = parent.parentElement;
const parent = el.parentElement, grand = parent.parentElement, great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
@ -27,8 +27,9 @@ function clicked() {
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "terrain") editReliefIcon();
else if (parent.id === "markers") editMarker();
else if (parent.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline();
else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) {
const node = document.getElementById("island_"+pack.cells.f[i]);
editCoastline(node);

View file

@ -39,10 +39,14 @@ function clearMainTip() {
tooltip.innerHTML = "";
}
// show tip at the bottom of the screen, consider possible translation
function showDataTip(e) {
if (!e.target) return;
if (e.target.dataset.tip) {tip(e.target.dataset.tip); return;};
if (e.target.parentNode.dataset.tip) tip(e.target.parentNode.dataset.tip);
let dataTip = e.target.dataset.tip;
if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip;
if (!dataTip) return;
const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip);
tip(tooltip);
}
function moved() {
@ -84,6 +88,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === "armies") {tip(e.target.dataset.name + ". Click to edit"); return;}
if (group === "rivers") {tip(getRiverName(e.target.id) + "Click to edit"); return;}
if (group === "routes") {tip("Click to edit the Route"); return;}
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
@ -132,14 +137,15 @@ function updateCellInfo(point, i, g) {
const cells = pack.cells;
const x = infoX.innerHTML = rn(point[0]);
const y = infoY.innerHTML = rn(point[1]);
const f = cells.f[i];
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, "lat");
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, "lon");
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
const h = pack.cells.h[i] < 20 ? grid.cells.h[pack.cells.g[i]] : pack.cells.h[i];
infoHeight.innerHTML = getFriendlyHeight(point) + " (" + h + ")";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
@ -149,7 +155,6 @@ function updateCellInfo(point, i, g) {
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
const f = cells.f[i];
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
@ -164,6 +169,26 @@ function toDMS(coord, c) {
return degrees + "° " + minutes + " " + seconds + "″ " + cardinal;
}
// get surface elevation
function getElevation(f, h) {
if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height
if (f.border) return "0 " + heightUnit.value; // ocean: 0
// lake: lowest coast height - 1
const lakeCells = Array.from(pack.cells.i.filter(i => pack.cells.f[i] === f.i));
const heights = lakeCells.map(i => pack.cells.c[i].map(c => pack.cells.h[c])).flat().filter(h => h > 19);
const elevation = (d3.min(heights)||20) - 1;
return getHeight(elevation) + " (" + elevation + ")";
}
// get water depth
function getDepth(f, h, p) {
if (f.land) return "0 " + heightUnit.value; // land: 0
if (!f.border) return getHeight(h, "abs"); // lake: pack abs height
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
return getHeight(gridH, "abs"); // ocean: grig height
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(p) {
const packH = pack.cells.h[findCell(p[0], p[1])];
@ -172,7 +197,7 @@ function getFriendlyHeight(p) {
return getHeight(h);
}
function getHeight(h) {
function getHeight(h, abs) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === "m") unitRatio = 1; // if meter
@ -182,6 +207,7 @@ function getHeight(h) {
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
if (abs) height = Math.abs(height);
return rn(height * unitRatio) + " " + unit;
}
@ -216,7 +242,7 @@ document.querySelectorAll("[data-locked]").forEach(function(e) {
else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
});
e.addEventListener("click", function(event) {
const id = (this.id).slice(5);
if (this.className === "icon-lock") unlock(id);
@ -341,7 +367,7 @@ document.addEventListener("keyup", event => {
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers(); // Shift + "V" to open Rivers overview
//else if (shift && key === 77) overviewMilitary(); // Shift + "M" to open Military overview
else if (shift && key === 77) overviewMilitary(); // Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails(); // Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg(); // Shift + "1" to click to add Burg

View file

@ -3,8 +3,8 @@
function editHeightmap() {
void function selectEditMode() {
alertMessage.innerHTML = `<p>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</p>
alertMessage.innerHTML = `<span>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</span>
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
@ -128,8 +128,7 @@ function editHeightmap() {
customization = 0;
customizationMenu.style.display = "none";
if (options.querySelector(".tab > button.active").id === "toolsTab")
toolsContent.style.display = "block";
if (document.getElementById("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block";
layersPreset.disabled = false;
exitCustomization.style.display = "none"; // hide finalize button
restoreDefaultEvents();
@ -195,6 +194,7 @@ function editHeightmap() {
BurgsAndStates.drawStateLabels();
Rivers.specify();
Military.generate();
addMarkers();
addZones();
console.timeEnd("regenerateErasedData");
@ -307,6 +307,7 @@ function editHeightmap() {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.features[pack.cells.f[i]].group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
const land = pack.cells.h[i] >= 20;
// check biome

View file

@ -399,7 +399,7 @@ function drawPrec() {
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
prec.selectAll("circle").data(data).enter().append("circle")
.attr("cx", d => p[d][0]).attr("cy", d => p[d][1]).attr("r", 0)
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
}
function togglePopulation(event) {

View file

@ -6,11 +6,12 @@ function overviewMilitary() {
if (!layerIsOn("toggleBorders")) toggleBorders();
const body = document.getElementById("militaryBody");
militaryOverviewAddLines();
addLines();
$("#militaryOverview").dialog();
if (modules.overviewMilitary) return;
modules.overviewMilitary = true;
updateHeaders();
$("#militaryOverview").dialog({
title: "Military Overview", resizable: false, width: fitContent(),
@ -18,43 +19,58 @@ function overviewMilitary() {
});
// add listeners
document.getElementById("militaryOverviewRefresh").addEventListener("click", militaryOverviewAddLines);
document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines);
document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize);
document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate);
document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData);
// add line for each river
function militaryOverviewAddLines() {
body.innerHTML = "";
let lines = "", militaryTotal = 0;
body.addEventListener("change", function(ev) {
const el = ev.target, line = el.parentNode, state = +line.dataset.id, type = el.dataset.type;
if (type && type !== "alert") changeForces(state, line, type, +el.value); else
if (type === "alert") changeAlert(state, line, +el.value);
});
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById("militaryHeader");
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(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
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);
const popRate = +populationRate.value;
for (const s of states) {
const total = (s.military.infantry + s.military.cavalry + s.military.archers + s.military.fleet / 10);
const rate = total / (s.rural + s.urban * urbanization.value) * 100;
militaryTotal += total;
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
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;
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" data-infantry="${s.military.infantry}"
data-archers="${s.military.archers}" data-cavalry="${s.military.cavalry}" data-reserve="${s.military.reserve}"
data-fleet="${s.military.fleet}" data-rate="${rate}" data-total="${total}">
<svg data-tip="State color" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<input data-tip="State name" class="stateName" value="${s.name}" readonly>
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<input data-type="${u.name}" data-tip="State ${u.name} units number" type="number" min=0 step=1 value="${getForces(u)}">`).join(" ");
<input data-tip="State infantry number" type="number" class="militaryArmy" min=0 step=1 value="${rn(s.military.infantry * popRate)}">
<input data-tip="State archers number" type="number" class="militaryArmy" min=0 step=1 value="${rn(s.military.archers * popRate)}">
<input data-tip="State cavalry number" type="number" class="militaryArmy" min=0 step=1 value="${rn(s.military.cavalry * popRate)}">
<input data-tip="Number of ships in state navy" class="militaryFleet" type="number" min=0 step=1 value="${s.military.fleet}">
<div data-tip="Total military personnel (including ships crew)">${si(total * popRate)}</div>
<div data-tip="Armed forces personnel (% of state population). Depends on diplomatic situation">${rn(rate, 2)}%</div>
<div data-tip="State manpower (reserve)">${si(s.military.reserve * popRate)}</div>
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)"><b>${si(total)}</b></div>
<div data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div>
<input data-type="alert" data-tip="War Alert. Modifier to military forces number, depends of political situation" type="number" min=0 step=.01 value="${rn(s.alert, 2)}">
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
// update footer
militaryFooterStates.innerHTML = states.length;
militaryFooterAverage.innerHTML = si(militaryTotal / states.length * popRate);
updateFooter();
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
@ -62,6 +78,40 @@ function overviewMilitary() {
applySorting(militaryHeader);
}
function changeForces(state, line, type, value) {
const s = pack.states[state];
if (!s.military.alert) {tip("Value won't be applied as War Alert is 0. Change Alert value to positive first", false, "error"); return;}
line.dataset[type] = value;
s.military[type] = value / populationRate.value / s.military.alert;
updateTotal(s.military, line);
updateFooter();
}
function changeAlert(state, line, alert) {
const s = pack.states[state];
s.military.alert = line.dataset.alert = alert;
const getForces = u => rn(s.military[u.name] * alert * populationRate.value)||0;
options.military.forEach(u => line.dataset[u.name] = line.querySelector(`input[data-type='${u.name}']`).value = getForces(u));
updateTotal(s.military, line);
updateFooter();
}
function updateTotal(m, line) {
line.dataset.total = rn(d3.sum(options.military.map(u => (m[u.name]||0) * u.crew)) * m.alert * populationRate.value);
line.dataset.rate = line.dataset.total / line.dataset.population * 100;
line.querySelector("div[data-type='total']>b").innerHTML = si(line.dataset.total);
line.querySelector("div[data-type='rate']").innerHTML = rn(line.dataset.rate, 2) + "%";
}
function updateFooter() {
const lines = Array.from(body.querySelectorAll(":scope > div"));
const statesNumber = militaryFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length;
militaryFooterForces.innerHTML = si(d3.sum(lines.map(el => el.dataset.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) {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
@ -82,7 +132,7 @@ function overviewMilitary() {
const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t);
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
}
@ -93,19 +143,92 @@ function overviewMilitary() {
});
}
function militaryCustomize() {
const types = ["default", "melee", "ranged", "mounted", "machinery", "naval"];
const table = document.getElementById("militaryOptions").querySelector("tbody");
removeUnitLines();
options.military.map(u => addUnitLine(u));
$("#militaryOptions").dialog({
title: "Edit Military Units", resizable: false, width: fitContent(),
position: {my: "center", at: "center", of: "svg"},
buttons: {
Apply: function() {applyMilitaryOptions(); $(this).dialog("close");},
Add: () => addUnitLine({name: "custom", rural: 0.2, urban: 0.5, crew: 1, type: "default"}),
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"));
}
});
function removeUnitLines() {
table.querySelectorAll("tr").forEach(el => el.remove());
}
function addUnitLine(u) {
const row = `<tr>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew" type="number" min=1 step=1 value="${u.crew}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types.map(t => `<option ${u.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ")}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" checked=${u.separate}>
<label for="${u.name}Separate" class="checkbox-label"></label>
</td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>
</tr>`;
table.insertAdjacentHTML("beforeend", row);
}
function restoreDefaultUnits() {
removeUnitLines();
[{name:"infantry", rural:.25, urban:.2, crew:1, type:"melee", separate:0},
{name:"archers", rural:.12, urban:.2, crew:1, type:"ranged", separate:0},
{name:"cavalry", rural:.12, urban:.03, crew:3, type:"mounted", separate:0},
{name:"artillery", rural:0, urban:.03, crew:8, type:"machinery", separate:0},
{name:"fleet", rural:0, urban:.015, crew:100, type:"naval", separate:1}].map(u => addUnitLine(u));
}
function applyMilitaryOptions() {
options.military = Array.from(table.querySelectorAll("tr")).map(r => {
const [name, rural, urban, crew, type, separate] = Array.from(r.querySelectorAll("input, select")).map(d => d.value||d.checked);
return {name:name.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'), rural:+rural||0, urban:+urban||0, crew:+crew||0, type, separate:+separate||0};
});
localStorage.setItem("military", JSON.stringify(options.military));
calculateMilitaryForces();
updateHeaders();
addLines();
}
}
function militaryRecalculate() {
calculateMilitaryForces();
addLines();
}
function downloadMilitaryData() {
let data = "Id,River,Type,Length,Basin\n"; // headers
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.name + ",";
data += el.dataset.type + ",";
data += el.querySelector(".biomeArea").innerHTML + ",";
data += el.dataset.basin + "\n";
data += el.dataset.state + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += el.dataset.total + ",";
data += el.dataset.population + ",";
data += el.dataset.rate + ",";
data += el.dataset.alert + "\n";
});
const name = getFileName("Military") + ".csv";
downloadFile(data, name);
}
}
}

View file

@ -20,7 +20,7 @@ function showOptions(event) {
}
regenerate.style.display = "none";
options.style.display = "block";
document.getElementById("options").style.display = "block";
optionsTrigger.style.display = "none";
if (event) event.stopPropagation();
@ -28,21 +28,21 @@ function showOptions(event) {
// Hide options pane on trigger click
function hideOptions(event) {
options.style.display = "none";
document.getElementById("options").style.display = "none";
optionsTrigger.style.display = "block";
if (event) event.stopPropagation();
}
// To toggle options on hotkey press
function toggleOptions(event) {
if (options.style.display === "none") showOptions(event);
if (document.getElementById("options").style.display === "none") showOptions(event);
else hideOptions(event);
}
// Toggle "New Map!" pane on hover
optionsTrigger.addEventListener("mouseenter", function() {
if (optionsTrigger.classList.contains("glow")) return;
if (options.style.display === "none") regenerate.style.display = "block";
if (document.getElementById("options").style.display === "none") regenerate.style.display = "block";
});
collapsible.addEventListener("mouseleave", function() {
@ -50,15 +50,15 @@ collapsible.addEventListener("mouseleave", function() {
});
// Activate options tab on click
options.querySelector("div.tab").addEventListener("click", function(event) {
document.getElementById("options").querySelector("div.tab").addEventListener("click", function(event) {
if (event.target.tagName !== "BUTTON") return;
const id = event.target.id;
const active = options.querySelector(".tab > button.active");
const active = document.getElementById("options").querySelector(".tab > button.active");
if (active && id === active.id) return; // already active tab is clicked
if (active) active.classList.remove("active");
document.getElementById(id).classList.add("active");
options.querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
document.getElementById("options").querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
if (id === "layersTab") layersContent.style.display = "block"; else
if (id === "styleTab") styleContent.style.display = "block"; else
@ -69,7 +69,7 @@ options.querySelector("div.tab").addEventListener("click", function(event) {
if (id === "aboutTab") aboutContent.style.display = "block";
});
options.querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
document.getElementById("options").querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
function collapse(e) {
const trigger = e.target;
const section = trigger.parentElement.nextElementSibling;
@ -309,7 +309,8 @@ function applyStoredOptions() {
if(stored.slice(0,5) === "style") applyOption(stylePreset, stored, stored.slice(5));
}
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
if (localStorage.getItem("winds")) options.winds = localStorage.getItem("winds").split(",").map(w => +w);
if (localStorage.getItem("military")) options.military = JSON.parse(localStorage.getItem("military"));
changeDialogsTransparency(localStorage.getItem("transparency") || 5);
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));

View file

@ -37,7 +37,7 @@ function editProvinces() {
const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id;
if (cl.contains("fillRect")) changeFill(el); else
if (cl.contains("name")) editProvinceName(p); else
if (cl.contains("icon-fleur")) provinceOpenCOA(ev, p); else
if (cl.contains("icon-coa")) provinceOpenCOA(ev, p); else
if (cl.contains("icon-star-empty")) capitalZoomIn(p); else
if (cl.contains("icon-flag-empty")) declareProvinceIndependence(p); else
if (cl.contains("culturePopulation")) changePopulation(p); else
@ -116,7 +116,7 @@ function editProvinces() {
lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${p.color}" class="fillRect pointer"></svg>
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly>
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-fleur pointer hide"></span>
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-coa pointer hide"></span>
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly>
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg?'':'placeholder'}"></span>
<select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${p.burgs.length?'':'placeholder'}">${p.burgs.length ? getCapitalOptions(p.burgs, p.burg) : ''}</select>

View file

@ -0,0 +1,277 @@
"use strict";
function editRegiment() {
if (customization) return;
closeDialogs(".stable");
// if (!layerIsOn("toggleArmies")) toggleArmies();
armies.selectAll(":scope > g").classed("draggable", true);
armies.selectAll(":scope > g > g").call(d3.drag().on("drag", dragRegiment));
elSelected = d3.event.target;
if (!pack.states[elSelected.dataset.state]) return;
if (!regiment()) return;
updateRegimentData(regiment());
drawBase();
$("#regimentEditor").dialog({
title: "Edit Regiment", resizable: false, close: closeEditor,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeEditor
});
if (modules.editRegiment) return;
modules.editRegiment = true;
// add listeners
document.getElementById("regimentNameRestore").addEventListener("click", restoreName);
document.getElementById("regimentType").addEventListener("click", changeType);
document.getElementById("regimentName").addEventListener("change", changeName);
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
document.getElementById("regimentLegend").addEventListener("click", editLegend);
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
document.getElementById("regimentAdd").addEventListener("click", toggleAdd);
document.getElementById("regimentAttach").addEventListener("click", toggleAttach);
document.getElementById("regimentRemove").addEventListener("click", removeRegiment);
// get regiment data element
function regiment() {
return pack.states[elSelected.dataset.state].military.find(r => r.i == elSelected.dataset.id);
}
function updateRegimentData(regiment) {
document.getElementById("regimentType").className = regiment.n ? "icon-anchor" :"icon-users";
document.getElementById("regimentName").value = regiment.name;
const composition = document.getElementById("regimentComposition");
composition.innerHTML = options.military.map(u => {
return `<div data-tip="${capitalize(u.name)} number. Input to change">
<div class="label">${capitalize(u.name)}:</div>
<input data-u="${u.name}" type="number" min=0 step=1 value="${(regiment.u[u.name]||0)}">
<i>${u.type}</i></div>`
}).join("");
composition.querySelectorAll("input").forEach(el => el.addEventListener("change", changeUnit));
}
function drawBase() {
const reg = regiment();
const tr = parseTransform(elSelected.parentNode.getAttribute("transform"));
const tx = +tr[0], ty = +tr[1];
const x2 = +elSelected.nextSibling.getAttribute("x"), y2 = +elSelected.nextSibling.getAttribute("y");
const clr = pack.states[elSelected.dataset.state].color;
const base = viewbox.insert("g", "g#armies").attr("id", "regimentBase");
base.on("mouseenter", d => {tip("Regiment base. Drag to re-base the regiment", true);}).on("mouseleave", d => {tip('', true);});
base.append("line").attr("x1", reg.x).attr("y1", reg.y).attr("x2", x2+tx).attr("y2", y2+ty).attr("class", "dragLine");
base.append("circle").attr("cx", reg.x).attr("cy", reg.y).attr("r", 2).attr("fill", clr).call(d3.drag().on("drag", dragBase));
}
function changeType() {
const reg = regiment();
reg.n = +!reg.n;
document.getElementById("regimentType").className = reg.n ? "icon-anchor" :"icon-users";
const size = 3;
elSelected.setAttribute("x", reg.n ? reg.x-size*2 : reg.x-size*3);
elSelected.setAttribute("width", reg.n ? size*4 : size*6);
}
function changeName() {
elSelected.dataset.name = regiment().name = this.value;
}
function restoreName() {
const reg = regiment(), regs = pack.states[elSelected.dataset.state].military;
const name = Military.getName(reg, regs);
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
}
function changeUnit() {
const u = this.dataset.u;
const reg = regiment();
reg.u[u] = (+this.value)||0;
reg.a = d3.sum(Object.values(reg.u));
elSelected.nextSibling.innerHTML = reg.a;
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
}
function splitRegiment() {
const reg = regiment(), u1 = reg.u;
const state = elSelected.dataset.state, military = pack.states[state].military;
const i = last(military).i + 1, u2 = Object.assign({}, u1); // u clone
Object.keys(u1).forEach(u => u1[u] = Math.ceil(u1[u]/2)); // halved old reg
Object.keys(u2).forEach(u => u2[u] = Math.floor(u2[u]/2)); // halved new reg
reg.a = d3.sum(Object.values(u1)); // old reg total
const a = d3.sum(Object.values(u2)); // new reg total
const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:reg.y};
newReg.name = Military.getName(newReg, military);
military.push(newReg);
elSelected.parentNode.remove(); // undraw old reg
Military.drawRegiment(reg, state, reg.x, reg.y-6); // draw old reg above
Military.drawRegiment(newReg, state, reg.x, reg.y+6); // draw new reg below
$("#regimentEditor").dialog("close");
}
function toggleAdd() {
document.getElementById("regimentAdd").classList.toggle("pressed");
if (document.getElementById("regimentAdd").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
}
}
function addRegimentOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
const state = elSelected.dataset.state, military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.drawRegiment(reg, state);
toggleAdd();
}
function toggleAttach() {
document.getElementById("regimentAttach").classList.toggle("pressed");
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attachRegimentOnClick);
tip("Click on another regiment to unite both regiments. The current regiment will be removed", true);
armies.selectAll(":scope > g").classed("draggable", false);
} else {
clearMainTip();
armies.selectAll(":scope > g").classed("draggable", true);
viewbox.on("click", clicked).style("cursor", "default");
}
}
function attachRegimentOnClick() {
const target = d3.event.target, army = target.parentElement.parentElement;
if (army.parentElement.id !== "armies") {
tip("Please click on a regiment", false, "error");
return;
}
if (target === elSelected) {
tip("Cannot attach regiment to itself. Please click on another regiment", false, "error");
return;
}
if (army !== elSelected.parentElement.parentElement) {
tip("Cannot attach this regiment to regiment of other state", false, "error");
return;
};
const reg = regiment(); // reg to be attached
const sel = pack.states[target.dataset.state].military.find(r => r.i == target.dataset.id); // reg to attach to
for (const unit of options.military) {
const u = unit.name;
if (reg.u[u]) sel.u[u] ? sel.u[u] += reg.u[u] : sel.u[u] = reg.u[u];
}
sel.a = d3.sum(Object.values(sel.u)); // reg total
target.nextSibling.innerHTML = sel.a; // update selected reg total text
// remove attached regiment
const military = pack.states[elSelected.dataset.state].military;
military.splice(military.indexOf(reg), 1);
const index = notes.findIndex(n => n.id === elSelected.parentNode.id);
if (index != -1) notes.splice(index, 1);
elSelected.parentNode.remove();
$("#regimentEditor").dialog("close");
}
function regenerateLegend() {
const index = notes.findIndex(n => n.id === elSelected.parentNode.id);
if (index != -1) notes.splice(index, 1);
const s = pack.states[elSelected.dataset.state];
Military.generateNote(regiment(), s);
}
function editLegend() {
editNotes(elSelected.parentNode.id, regiment().name);
}
function removeRegiment() {
alertMessage.innerHTML = "Are you sure you want to remove the regiment?";
$("#alert").dialog({resizable: false, title: "Remove regiment",
buttons: {
Remove: function() {
$(this).dialog("close");
const military = pack.states[elSelected.dataset.state].military;
const regIndex = military.indexOf(regiment());
if (regIndex === -1) return;
military.splice(regIndex, 1);
const index = notes.findIndex(n => n.id === elSelected.parentNode.id);
if (index != -1) notes.splice(index, 1);
elSelected.parentNode.remove();
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
$("#regimentEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function dragRegiment() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.select(this).raise();
d3.select(this.parentNode).raise();
const self = elSelected.parentNode === this;
const baseLine = viewbox.select("g#regimentBase > line");
const x2 = +elSelected.nextSibling.getAttribute("x");
const y2 = +elSelected.nextSibling.getAttribute("y");
d3.event.on("drag", function() {
const x = dx + d3.event.x, y = dy + d3.event.y;
this.setAttribute("transform", `translate(${(x)},${(y)})`);
if (self) baseLine.attr("x2", x2+x).attr("y2", y2+y);
});
}
function dragBase() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
const baseLine = viewbox.select("g#regimentBase > line");
d3.event.on("drag", function() {
const x = dx + d3.event.x, y = dy + d3.event.y;
this.setAttribute("transform", `translate(${(x)},${(y)})`);
baseLine.attr("x1", d3.event.x).attr("y1", d3.event.y);
});
d3.event.on("end", function() {
const reg = regiment();
const x = d3.event.x, y = d3.event.y, cell = findCell(x, y);
reg.cell = cell, reg.x = x, reg.y = y;
});
}
function closeEditor() {
armies.selectAll(":scope > g").classed("draggable", false);
armies.selectAll("g>g").call(d3.drag().on("drag", null));
viewbox.select("g#regimentBase").remove();
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");
restoreDefaultEvents();
elSelected = null;
}
}

View file

@ -42,7 +42,7 @@ function editStates() {
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
if (cl.contains("fillRect")) stateChangeFill(el); else
if (cl.contains("name")) editStateName(state); else
if (cl.contains("icon-fleur")) stateOpenCOA(ev, state); else
if (cl.contains("icon-coa")) stateOpenCOA(ev, state); else
if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else
if (cl.contains("culturePopulation")) changePopulation(state); else
if (cl.contains("icon-pin")) focusOnState(state, cl); else
@ -90,7 +90,7 @@ function editStates() {
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Neutral lands name. Click to change" class="stateName name pointer italic" value="${s.name}" readonly>
<span class="icon-fleur placeholder hide"></span>
<span class="icon-coa placeholder hide"></span>
<input class="stateForm placeholder" value="none">
<span class="icon-star-empty placeholder hide"></span>
<input class="stateCapital placeholder hide">
@ -114,7 +114,7 @@ function editStates() {
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
<svg data-tip="State fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect pointer"></svg>
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly>
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-fleur pointer hide"></span>
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-coa pointer hide"></span>
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${s.formName}" readonly>
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>

View file

@ -8,13 +8,13 @@ function editWorld() {
"Southern": () => applyPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button")
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
},
}
});
const globe = d3.select("#globe");
@ -56,6 +56,7 @@ function editWorld() {
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
}
function updateGlobePosition() {
@ -100,26 +101,26 @@ function editWorld() {
function updateWindDirections() {
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${winds[i]} ${tr[1]} ${tr[2]})`);
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
}
function changeWind() {
const arrow = d3.event.target.nextElementSibling;
const tier = +arrow.dataset.tier;
winds[tier] = (winds[tier] + 45) % 360;
options.winds[tier] = (options.winds[tier] + 45) % 360;
const tr = parseTransform(arrow.getAttribute("transform"));
arrow.setAttribute("transform", `rotate(${winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", winds);
arrow.setAttribute("transform", `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", options.winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
if (mapTiers.includes(tier)) updateWorld();
}
function restoreDefaultWinds() {
const defaultWinds = [225, 45, 225, 315, 135, 315];
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const update = mapTiers.some(t => winds[t] != defaultWinds[t]);
winds = defaultWinds;
const update = mapTiers.some(t => options.winds[t] != defaultWinds[t]);
options.winds = defaultWinds;
updateWindDirections();
if (update) updateWorld();
}