diff --git a/index.css b/index.css
index 9c884fa2..9653262e 100644
--- a/index.css
+++ b/index.css
@@ -254,13 +254,6 @@ i.icon-lock {
font-size: 12px;
}
-#hierarchy .selected {
- stroke: #c13119;
- stroke-width: 1;
- cursor: move;
-}
-
-#hierarchy text,
#statesTree text,
#provincesTree text {
pointer-events: none;
@@ -281,7 +274,7 @@ i.icon-lock {
stroke-width: 2;
}
-.dragLine {
+.regimentDragLine {
marker-end: url(#end-arrow);
stroke: #333333;
stroke-dasharray: 5;
diff --git a/index.html b/index.html
index 52b3e7cd..3c9dcacf 100644
--- a/index.html
+++ b/index.html
@@ -108,7 +108,7 @@
}
-
+
-
+
@@ -7820,7 +7820,7 @@
-
+
@@ -7847,7 +7847,7 @@
-
+
diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js
index 9b30fb0e..07b58cae 100644
--- a/modules/dynamic/editors/cultures-editor.js
+++ b/modules/dynamic/editors/cultures-editor.js
@@ -293,17 +293,8 @@ function getShapeOptions(selectShape, selected) {
return ``;
}
-function cultureHighlightOn(event) {
+const cultureHighlightOn = debounce(event => {
const cultureId = Number(event.id || event.target.dataset.id);
- const $info = byId("cultureInfo");
- if ($info) {
- d3.select("#hierarchy").select(`g[data-id='${cultureId}']`).classed("selected", 1);
- const {name, type, rural, urban} = pack.cultures[cultureId];
- const population = rural * populationRate + urban * populationRate * urbanization;
- const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct";
- $info.innerHTML = `${name} culture. ${type}. ${populationText}`;
- tip("Drag to other node to add parent, click to edit");
- }
if (!layerIsOn("toggleCultures")) return;
if (customization) return;
@@ -321,18 +312,11 @@ function cultureHighlightOn(event) {
.transition(animate)
.attr("r", 8)
.attr("stroke", "#d0240f");
-}
+}, 200);
function cultureHighlightOff(event) {
const cultureId = Number(event.id || event.target.dataset.id);
- const $info = byId("cultureInfo");
- if ($info) {
- d3.select("#hierarchy").select(`g[data-id='${cultureId}']`).classed("selected", 0);
- $info.innerHTML = "";
- tip("");
- }
-
if (!layerIsOn("toggleCultures")) return;
cults
.select("#culture" + cultureId)
@@ -644,189 +628,36 @@ function togglePercentageMode() {
}
}
-function showHierarchy() {
- // build hierarchy tree
- pack.cultures[0].origins = [null];
- const validCultures = pack.cultures.filter(c => !c.removed);
- if (validCultures.length < 3) return tip("Not enough cultures to show hierarchy", false, "error");
+async function showHierarchy() {
+ if (customization) return;
+ const HeirarchyTree = await import("../hierarchy-tree.js");
- const root = d3
- .stratify()
- .id(d => d.i)
- .parentId(d => d.origins[0])(validCultures);
- const treeWidth = root.leaves().length;
- const treeHeight = root.height;
- const width = Math.max(treeWidth * 40, 300);
- const height = treeHeight * 60;
+ const getDescription = culture => {
+ const {name, type, rural, urban} = culture;
- const margin = {top: 10, right: 10, bottom: -5, left: 10};
- const w = width - margin.left - margin.right;
- const h = height + 30 - margin.top - margin.bottom;
- const treeLayout = d3.tree().size([w, h]);
-
- alertMessage.innerHTML = /* html */ `
-
-
- culture.
- Abbreviation:
-
-
-
-
`;
-
- // prepare svg
- const svg = d3
- .select("#alertMessage")
- .insert("svg", "#cultureChartDetails")
- .attr("id", "hierarchy")
- .attr("width", width)
- .attr("height", height)
- .style("text-anchor", "middle")
- .style("min-width", "300px");
- const graph = svg.append("g").attr("transform", `translate(10, -45)`);
- const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
- const primaryLinks = links.append("g");
- const secondaryLinks = links.append("g").attr("stroke-dasharray", 1);
- const nodes = graph.append("g");
-
- // render helper functions
- const getLinkPath = d => {
- const {
- source: {x: sx, y: sy},
- target: {x: tx, y: ty}
- } = d;
- return `M${sx},${sy} C${sx},${(sy * 3 + ty) / 4} ${tx},${(sy * 2 + ty) / 3} ${tx},${ty}`;
+ const population = rural * populationRate + urban * populationRate * urbanization;
+ const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct";
+ return `${name} culture. ${type}. ${populationText}`;
};
- const getSecondaryLinks = root => {
- const nodes = root.descendants();
- const links = [];
-
- for (const node of nodes) {
- const origins = node.data.origins;
- if (node.depth < 2) continue;
-
- for (let i = 1; i < origins.length; i++) {
- const source = nodes.find(n => n.data.i === origins[i]);
- if (source) links.push({source, target: node});
- }
- }
-
- return links;
+ const getShape = ({type}) => {
+ if (type === "Generic") return "circle";
+ if (type === "River") return "diamond";
+ if (type === "Lake") return "hexagon";
+ if (type === "Naval") return "square";
+ if (type === "Highland") return "concave";
+ if (type === "Nomadic") return "octagon";
+ if (type === "Hunting") return "pentagon";
};
- const nodePathMap = {
- undefined: "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0", // small circle
- Generic: "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0", // circle
- River: "M0,-14L14,0L0,14L-14,0Z", // diamond
- Lake: "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z", // hexagon
- Naval: "M-11,-11h22v22h-22Z", // square
- Highland: "M-11,-11l11,2l11,-2l-2,11l2,11l-11,-2l-11,2l2,-11Z", // concave square
- Nomadic: "M-4.97,-12.01 l9.95,0 l7.04,7.04 l0,9.95 l-7.04,7.04 l-9.95,0 l-7.04,-7.04 l0,-9.95Z", // octagon
- Hunting: "M0,-14l14,11l-6,14h-16l-6,-14Z" // pentagon
- };
-
- const getNodePath = d => nodePathMap[d.data.type];
-
- renderTree();
- function renderTree() {
- treeLayout(root);
-
- primaryLinks.selectAll("path").data(root.links()).enter().append("path").attr("d", getLinkPath);
- secondaryLinks.selectAll("path").data(getSecondaryLinks(root)).enter().append("path").attr("d", getLinkPath);
-
- const node = nodes
- .selectAll("g")
- .data(root.descendants())
- .enter()
- .append("g")
- .attr("data-id", d => d.data.i)
- .attr("stroke", "#333333")
- .attr("transform", d => `translate(${d.x}, ${d.y})`)
- .on("mouseenter", cultureHighlightOn)
- .on("mouseleave", cultureHighlightOff)
- .on("click", cultureSelect)
- .call(d3.drag().on("start", dragToReorigin));
-
- node
- .append("path")
- .attr("d", getNodePath)
- .attr("fill", d => d.data.color || "#ffffff")
- .attr("stroke-dasharray", d => (d.data.cells ? "null" : "1"));
-
- node
- .append("text")
- .attr("dy", ".35em")
- .text(d => d.data.code || "");
- }
-
- $("#alert").dialog({
- title: "Cultures tree",
- width: fitContent(),
- resizable: false,
- position: {my: "left center", at: "left+10 center", of: "svg"},
- buttons: null,
- close: () => {
- alertMessage.innerHTML = "";
- }
+ HeirarchyTree.open({
+ type: "cultures",
+ data: pack.cultures,
+ onNodeEnter: cultureHighlightOn,
+ onNodeLeave: cultureHighlightOff,
+ getDescription,
+ getShape
});
-
- function cultureSelect(d) {
- d3.event.stopPropagation();
-
- nodes.selectAll("g").style("outline", "none");
- this.style.outline = "1px solid #c13119";
- byId("cultureSelected").style.display = "block";
- byId("cultureInfo").style.display = "none";
-
- const culture = d.data;
- byId("cultureSelectedName").innerText = culture.name;
- byId("cultureSelectedCode").value = culture.code;
-
- byId("cultureSelectedCode").onchange = function () {
- if (this.value.length > 3) return tip("Abbreviation must be 3 characters or less", false, "error", 3000);
- if (!this.value.length) return tip("Abbreviation cannot be empty", false, "error", 3000);
- nodes.select(`g[data-id="${d.id}"] > text`).text(this.value);
- culture.code = this.value;
- };
-
- byId("cultureSelectedClear").onclick = () => {
- culture.origins = [0];
- showHierarchy();
- };
-
- byId("cultureSelectedClose").onclick = () => {
- this.style.outline = "none";
- byId("cultureSelected").style.display = "none";
- byId("cultureInfo").style.display = "block";
- };
- }
-
- function dragToReorigin(d) {
- const originLine = graph.append("path").attr("class", "dragLine").attr("d", `M${d.x},${d.y}L${d.x},${d.y}`);
-
- d3.event.on("drag", () => {
- originLine.attr("d", `M${d.x},${d.y}L${d3.event.x},${d3.event.y}`);
- });
-
- d3.event.on("end", () => {
- originLine.remove();
- const selected = graph.select("g.selected");
- if (!selected.size()) return;
-
- const cultureId = d.data.i;
- const newOrigin = selected.datum().data.i;
- if (cultureId === newOrigin) return; // dragged to itself
- if (d.data.origins.includes(newOrigin)) return; // already a child of the selected node
- if (d.descendants().some(node => node.data.i === newOrigin)) return; // cannot be a child of its own child
-
- const culture = pack.cultures[cultureId];
- if (culture.origins[0] === 0) culture.origins = [];
- culture.origins.push(newOrigin);
-
- showHierarchy();
- });
- }
}
function recalculateCultures(must) {
diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js
index 745d1457..381212d6 100644
--- a/modules/dynamic/editors/religions-editor.js
+++ b/modules/dynamic/editors/religions-editor.js
@@ -258,27 +258,8 @@ function getTypeOptions(type) {
return options;
}
-function religionHighlightOn(event) {
+const religionHighlightOn = debounce(event => {
const religionId = Number(event.id || event.target.dataset.id);
- const $info = byId("religionInfo");
- if ($info) {
- d3.select("#hierarchy").select(`g[data-id='${religionId}']`).classed("selected", 1);
- const {name, type, form, rural, urban} = pack.religions[religionId];
-
- const getTypeText = () => {
- if (name.includes(type)) return "";
- if (form.includes(type)) return "";
- if (type === "Folk" || type === "Organized") return `. ${type} religion`;
- return `. ${type}`;
- };
- const formText = form === type ? "" : ". " + form;
- const population = rural * populationRate + urban * populationRate * urbanization;
- const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct";
-
- $info.innerHTML = `${name}${getTypeText()}${formText}. ${populationText}`;
- tip("Drag to other node to add parent, click to edit");
- }
-
const $el = $body.querySelector(`div[data-id='${religionId}']`);
if ($el) $el.classList.add("active");
@@ -299,17 +280,10 @@ function religionHighlightOn(event) {
.attr("r", 8)
.attr("stroke-width", 2)
.attr("stroke", "#c13119");
-}
+}, 200);
function religionHighlightOff(event) {
const religionId = Number(event.id || event.target.dataset.id);
- const $info = byId("religionInfo");
- if ($info) {
- d3.select("#hierarchy").select(`g[data-id='${religionId}']`).classed("selected", 0);
- $info.innerHTML = "";
- tip("");
- }
-
const $el = $body.querySelector(`div[data-id='${religionId}']`);
if ($el) $el.classList.remove("active");
@@ -557,185 +531,42 @@ function togglePercentageMode() {
}
}
-function showHierarchy() {
- // build hierarchy tree
- pack.religions[0].origins = [null];
- const validReligions = pack.religions.filter(r => !r.removed);
- if (validReligions.length < 3) return tip("Not enough religions to show hierarchy", false, "error");
+async function showHierarchy() {
+ if (customization) return;
+ const HeirarchyTree = await import("../hierarchy-tree.js");
- const root = d3
- .stratify()
- .id(d => d.i)
- .parentId(d => d.origins[0])(validReligions);
- const treeWidth = root.leaves().length;
- const treeHeight = root.height;
- const width = Math.max(treeWidth * 40, 300);
- const height = treeHeight * 60;
+ const getDescription = religion => {
+ const {name, type, form, rural, urban} = religion;
- const margin = {top: 10, right: 10, bottom: -5, left: 10};
- const w = width - margin.left - margin.right;
- const h = height + 30 - margin.top - margin.bottom;
- const treeLayout = d3.tree().size([w, h]);
+ const getTypeText = () => {
+ if (name.includes(type)) return "";
+ if (form.includes(type)) return "";
+ if (type === "Folk" || type === "Organized") return `. ${type} religion`;
+ return `. ${type}`;
+ };
- alertMessage.innerHTML = /* html */ `
-
-
- .
- Abbreviation:
-
-
-
-
`;
+ const formText = form === type ? "" : ". " + form;
+ const population = rural * populationRate + urban * populationRate * urbanization;
+ const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct";
- // prepare svg
- const svg = d3
- .select("#alertMessage")
- .insert("svg", "#religionChartDetails")
- .attr("id", "hierarchy")
- .attr("width", width)
- .attr("height", height)
- .style("text-anchor", "middle");
- const graph = svg.append("g").attr("transform", `translate(10, -45)`);
- const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
- const primaryLinks = links.append("g");
- const secondaryLinks = links.append("g").attr("stroke-dasharray", 1);
- const nodes = graph.append("g");
-
- // render helper functions
- const getLinkPath = d => {
- const {
- source: {x: sx, y: sy},
- target: {x: tx, y: ty}
- } = d;
- return `M${sx},${sy} C${sx},${(sy * 3 + ty) / 4} ${tx},${(sy * 2 + ty) / 3} ${tx},${ty}`;
+ return `${name}${getTypeText()}${formText}. ${populationText}`;
};
- const getSecondaryLinks = root => {
- const nodes = root.descendants();
- const links = [];
-
- for (const node of nodes) {
- const origins = node.data.origins;
- if (node.depth < 2) continue;
-
- for (let i = 1; i < origins.length; i++) {
- const source = nodes.find(n => n.data.i === origins[i]);
- if (source) links.push({source, target: node});
- }
- }
-
- return links;
+ const getShape = ({type}) => {
+ if (type === "Folk") return "circle";
+ if (type === "Organized") return "square";
+ if (type === "Cult") return "hexagon";
+ if (type === "Heresy") return "diamond";
};
- const nodePathMap = {
- undefined: "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0", // small circle
- Folk: "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0", // circle
- Organized: "M-11,-11h22v22h-22Z", // square
- Cult: "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z", // hexagon
- Heresy: "M0,-14L14,0L0,14L-14,0Z" // diamond
- };
-
- const getNodePath = d => nodePathMap[d.data.type];
-
- renderTree();
- function renderTree() {
- treeLayout(root);
-
- primaryLinks.selectAll("path").data(root.links()).enter().append("path").attr("d", getLinkPath);
- secondaryLinks.selectAll("path").data(getSecondaryLinks(root)).enter().append("path").attr("d", getLinkPath);
-
- const node = nodes
- .selectAll("g")
- .data(root.descendants())
- .enter()
- .append("g")
- .attr("data-id", d => d.data.i)
- .attr("stroke", "#333333")
- .attr("transform", d => `translate(${d.x}, ${d.y})`)
- .on("mouseenter", religionHighlightOn)
- .on("mouseleave", religionHighlightOff)
- .on("click", religionSelect)
- .call(d3.drag().on("start", dragToReorigin));
-
- node
- .append("path")
- .attr("d", getNodePath)
- .attr("fill", d => d.data.color || "#ffffff")
- .attr("stroke-dasharray", d => (d.data.cells ? "null" : "1"));
-
- node
- .append("text")
- .attr("dy", ".35em")
- .text(d => d.data.code || "");
- }
-
- $("#alert").dialog({
- title: "Religions tree",
- width: fitContent(),
- resizable: false,
- position: {my: "left center", at: "left+10 center", of: "svg"},
- buttons: {},
- close: () => {
- alertMessage.innerHTML = "";
- }
+ HeirarchyTree.open({
+ type: "religions",
+ data: pack.religions,
+ onNodeEnter: religionHighlightOn,
+ onNodeLeave: religionHighlightOff,
+ getDescription,
+ getShape
});
-
- function religionSelect(d) {
- d3.event.stopPropagation();
-
- nodes.selectAll("g").style("outline", "none");
- this.style.outline = "1px solid #c13119";
- byId("religionSelected").style.display = "block";
- byId("religionInfo").style.display = "none";
-
- const religion = d.data;
- byId("religionSelectedName").innerText = religion.name;
- byId("religionSelectedCode").value = religion.code;
-
- byId("religionSelectedCode").onchange = function () {
- if (this.value.length > 3) return tip("Abbreviation must be 3 characters or less", false, "error", 3000);
- if (!this.value.length) return tip("Abbreviation cannot be empty", false, "error", 3000);
- nodes.select(`g[data-id="${d.id}"] > text`).text(this.value);
- religion.code = this.value;
- };
-
- byId("religionSelectedClear").onclick = () => {
- religion.origins = [0];
- showHierarchy();
- };
-
- byId("religionSelectedClose").onclick = () => {
- this.style.outline = "none";
- byId("religionSelected").style.display = "none";
- byId("religionInfo").style.display = "block";
- };
- }
-
- function dragToReorigin(d) {
- const originLine = graph.append("path").attr("class", "dragLine").attr("d", `M${d.x},${d.y}L${d.x},${d.y}`);
-
- d3.event.on("drag", () => {
- originLine.attr("d", `M${d.x},${d.y}L${d3.event.x},${d3.event.y}`);
- });
-
- d3.event.on("end", () => {
- originLine.remove();
- const selected = graph.select("g.selected");
- if (!selected.size()) return;
-
- const religionId = d.data.i;
- const newOrigin = selected.datum().data.i;
- if (religionId === newOrigin) return; // dragged to itself
- if (d.data.origins.includes(newOrigin)) return; // already a child of the selected node
- if (d.descendants().some(node => node.data.i === newOrigin)) return; // cannot be a child of its own child
-
- const religion = pack.religions[religionId];
- if (religion.origins[0] === 0) religion.origins = [];
- religion.origins.push(newOrigin);
-
- showHierarchy();
- });
- }
}
function toggleExtinct() {
diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js
index e203fe59..b16246fd 100644
--- a/modules/dynamic/editors/states-editor.js
+++ b/modules/dynamic/editors/states-editor.js
@@ -707,7 +707,6 @@ function togglePercentageMode() {
}
function showStatesChart() {
- // build hierarchy tree
const statesData = pack.states.filter(s => !s.removed);
if (statesData.length < 2) return tip("There are no states to show", false, "error");
diff --git a/modules/dynamic/heightmap-selection.js b/modules/dynamic/heightmap-selection.js
index 9318987d..5ca94e61 100644
--- a/modules/dynamic/heightmap-selection.js
+++ b/modules/dynamic/heightmap-selection.js
@@ -2,7 +2,7 @@ const initialSeed = generateSeed();
let graph = getGraph(grid);
appendStyleSheet();
-insertEditorHtml();
+insertHtml();
addListeners();
export function open() {
@@ -150,7 +150,7 @@ function appendStyleSheet() {
document.head.appendChild(style);
}
-function insertEditorHtml() {
+function insertHtml() {
const heightmapSelectionHtml = /* html */ `
diff --git a/modules/dynamic/hierarchy-tree.js b/modules/dynamic/hierarchy-tree.js
new file mode 100644
index 00000000..5a420eb5
--- /dev/null
+++ b/modules/dynamic/hierarchy-tree.js
@@ -0,0 +1,486 @@
+appendStyleSheet();
+insertHtml();
+addListeners();
+
+const MARGINS = {top: 10, right: 10, bottom: -5, left: 10};
+
+const handleZoom = () => viewbox.attr("transform", d3.event.transform);
+const zoom = d3.zoom().scaleExtent([0.2, 1.5]).on("zoom", handleZoom);
+
+// store old root for transitions
+let oldRoot;
+
+// define svg elements
+const svg = d3.select("#hierarchyTree > svg").call(zoom);
+const viewbox = svg.select("g#hierarchyTree_viewbox");
+const primaryLinks = viewbox.select("g#hierarchyTree_linksPrimary");
+const secondaryLinks = viewbox.select("g#hierarchyTree_linksSecondary");
+const nodes = viewbox.select("g#hierarchyTree_nodes");
+const dragLine = viewbox.select("path#hierarchyTree_dragLine");
+
+// properties
+let dataElements; // {i, name, type, origins}[], e.g. path.religions
+let validElements; // not-removed dataElements
+let onNodeEnter; // d3Data => void
+let onNodeLeave; // d3Data => void
+let getDescription; // dataElement => string
+let getShape; // dataElement => string;
+
+export function open(props) {
+ closeDialogs(".stable");
+
+ dataElements = props.data;
+ dataElements[0].origins = [null];
+ validElements = dataElements.filter(r => !r.removed);
+ if (validElements.length < 3) return tip(`Not enough ${props.type} to show hierarchy`, false, "error");
+
+ onNodeEnter = props.onNodeEnter;
+ onNodeLeave = props.onNodeLeave;
+ getDescription = props.getDescription;
+ getShape = props.getShape;
+
+ const root = getRoot();
+ const treeWidth = root.leaves().length * 50;
+ const treeHeight = root.height * 50;
+
+ const w = treeWidth - MARGINS.left - MARGINS.right;
+ const h = treeHeight + 30 - MARGINS.top - MARGINS.bottom;
+ const treeLayout = d3.tree().size([w, h]);
+
+ const width = minmax(treeWidth, 300, innerWidth * 0.75);
+ const height = minmax(treeHeight, 200, innerHeight * 0.75);
+
+ zoom.extent([Array(2).fill(0), [width, height]]);
+ svg.attr("viewBox", `0, 0, ${width}, ${height}`);
+
+ $("#hierarchyTree").dialog({
+ title: `${capitalize(props.type)} tree`,
+ position: {my: "left center", at: "left+10 center", of: "svg"},
+ width
+ });
+
+ renderTree(root, treeLayout);
+}
+
+function appendStyleSheet() {
+ const styles = /* css */ `
+ #hierarchyTree_selectedOrigins > button {
+ margin: 0 2px;
+ }
+
+ .hierarchyTree_selectedButton {
+ border: 1px solid #aaa;
+ background: none;
+ padding: 1px 4px;
+ }
+
+ .hierarchyTree_selectedButton:hover {
+ border: 1px solid #333;
+ }
+
+ .hierarchyTree_selectedOrigin::after {
+ content: "✕";
+ margin-left: 8px;
+ color: #999;
+ }
+
+ .hierarchyTree_selectedOrigin:hover:after {
+ color: #333;
+ }
+
+ #hierarchyTree_originSelector > form > div {
+ padding: 0.3em;
+ margin: 1px 0;
+ border-radius: 1em;
+ }
+
+ #hierarchyTree_originSelector > form > div:hover {
+ background-color: #ddd;
+ }
+
+ #hierarchyTree_originSelector > form > div[checked] {
+ background-color: #c6d6d6;
+ }
+
+ #hierarchyTree_nodes > g > text {
+ pointer-events: none;
+ stroke: none;
+ font-size: 11px;
+ }
+
+ #hierarchyTree_nodes > g.selected {
+ stroke: #c13119;
+ stroke-width: 1;
+ cursor: move;
+ }
+
+ #hierarchyTree_dragLine {
+ marker-end: url(#end-arrow);
+ stroke: #333333;
+ stroke-dasharray: 5;
+ stroke-dashoffset: 1000;
+ animation: dash 80s linear backwards;
+ }
+ `;
+
+ const style = document.createElement("style");
+ style.appendChild(document.createTextNode(styles));
+ document.head.appendChild(style);
+}
+
+function insertHtml() {
+ const html = /* html */ `
+
+
+
+
+
+ .
+ Abbreviation:
+ Origins:
+
+
+
+
+
+
`;
+
+ byId("dialogs").insertAdjacentHTML("beforeend", html);
+}
+
+function addListeners() {}
+
+function getRoot() {
+ const root = d3
+ .stratify()
+ .id(d => d.i)
+ .parentId(d => d.origins[0])(validElements);
+
+ oldRoot = root;
+ return root;
+}
+
+function getLinkKey(d) {
+ return `${d.source.id}-${d.target.id}`;
+}
+
+function getNodeKey(d) {
+ return d.id;
+}
+
+function getLinkPath(d) {
+ const {
+ source: {x: sx, y: sy},
+ target: {x: tx, y: ty}
+ } = d;
+ return `M${sx},${sy} C${sx},${(sy * 3 + ty) / 4} ${tx},${(sy * 2 + ty) / 3} ${tx},${ty}`;
+}
+
+function getSecondaryLinks(root) {
+ const nodes = root.descendants();
+ const links = [];
+
+ for (const node of nodes) {
+ const origins = node.data.origins;
+
+ for (let i = 1; i < origins.length; i++) {
+ const source = nodes.find(n => n.data.i === origins[i]);
+ if (source) links.push({source, target: node});
+ }
+ }
+
+ return links;
+}
+
+const shapesMap = {
+ undefined: "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0", // small circle
+ circle: "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0",
+ square: "M-11,-11h22v22h-22Z",
+ hexagon: "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z",
+ diamond: "M0,-14L14,0L0,14L-14,0Z",
+ concave: "M-11,-11l11,2l11,-2l-2,11l2,11l-11,-2l-11,2l2,-11Z",
+ octagon: "M-4.97,-12.01 l9.95,0 l7.04,7.04 l0,9.95 l-7.04,7.04 l-9.95,0 l-7.04,-7.04 l0,-9.95Z",
+ pentagon: "M0,-14l14,11l-6,14h-16l-6,-14Z"
+};
+
+const getSortIndex = node => {
+ const descendants = node.descendants();
+ const secondaryOrigins = descendants.map(({data}) => data.origins.slice(1)).flat();
+
+ if (secondaryOrigins.length === 0) return node.data.i;
+ return d3.mean(secondaryOrigins);
+};
+
+function renderTree(root, treeLayout) {
+ treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b)));
+
+ primaryLinks.selectAll("path").data(root.links(), getLinkKey).join("path").attr("d", getLinkPath);
+ secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join("path").attr("d", getLinkPath);
+
+ const node = nodes
+ .selectAll("g")
+ .data(root.descendants(), getNodeKey)
+ .join("g")
+ .attr("data-id", d => d.data.i)
+ .attr("stroke", "#333")
+ .attr("transform", d => `translate(${d.x}, ${d.y})`)
+ .on("mouseenter", handleNoteEnter)
+ .on("mouseleave", handleNodeExit)
+ .on("click", selectElement)
+ .call(d3.drag().on("start", dragToReorigin));
+
+ node
+ .append("path")
+ .attr("d", ({data}) => shapesMap[getShape(data)])
+ .attr("fill", d => d.data.color || "#ffffff")
+ .attr("stroke-dasharray", d => (d.data.cells ? "none" : "1"));
+
+ node.append("text").text(d => d.data.code || "");
+}
+
+function mapCoords(newRoot, prevRoot) {
+ newRoot.x = prevRoot.x;
+ newRoot.y = prevRoot.y;
+
+ for (const node of newRoot.descendants()) {
+ const prevNode = prevRoot.descendants().find(n => n.data.i === node.data.i);
+ if (prevNode) {
+ node.x = prevNode.x;
+ node.y = prevNode.y;
+ }
+ }
+}
+
+function updateTree() {
+ const prevRoot = oldRoot;
+ const root = getRoot();
+ mapCoords(root, prevRoot);
+
+ const linksUpdateDuration = 50;
+ const moveDuration = 1000;
+
+ // old layout: update links at old nodes positions
+ const linkEnter = enter =>
+ enter
+ .append("path")
+ .attr("d", getLinkPath)
+ .attr("opacity", 0)
+ .call(enter => enter.transition().duration(linksUpdateDuration).attr("opacity", 1));
+
+ const linkUpdate = update =>
+ update.call(update => update.transition().duration(linksUpdateDuration).attr("d", getLinkPath));
+
+ const linkExit = exit =>
+ exit.call(exit => exit.transition().duration(linksUpdateDuration).attr("opacity", 0).remove());
+
+ primaryLinks.selectAll("path").data(root.links(), getLinkKey).join(linkEnter, linkUpdate, linkExit);
+ secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join(linkEnter, linkUpdate, linkExit);
+
+ // new layout: move nodes with links to new positions
+ const treeWidth = root.leaves().length * 50;
+ const treeHeight = root.height * 50;
+
+ const w = treeWidth - MARGINS.left - MARGINS.right;
+ const h = treeHeight + 30 - MARGINS.top - MARGINS.bottom;
+
+ const treeLayout = d3.tree().size([w, h]);
+ treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b)));
+
+ primaryLinks
+ .selectAll("path")
+ .data(root.links(), getLinkKey)
+ .transition()
+ .duration(moveDuration)
+ .delay(linksUpdateDuration)
+ .attr("d", getLinkPath);
+
+ secondaryLinks
+ .selectAll("path")
+ .data(getSecondaryLinks(root), getLinkKey)
+ .transition()
+ .duration(moveDuration)
+ .delay(linksUpdateDuration)
+ .attr("d", getLinkPath);
+
+ nodes
+ .selectAll("g")
+ .data(root.descendants(), getNodeKey)
+ .transition()
+ .delay(linksUpdateDuration)
+ .duration(moveDuration)
+ .attr("transform", d => `translate(${d.x},${d.y})`);
+}
+
+function selectElement(d) {
+ const dataElement = d.data;
+
+ const node = nodes.select(`g[data-id="${d.id}"]`);
+ nodes.selectAll("g").style("outline", "none");
+ node.style("outline", "1px solid #c13119");
+
+ byId("hierarchyTree_selected").style.display = "block";
+ byId("hierarchyTree_infoLine").style.display = "none";
+
+ byId("hierarchyTree_selectedName").innerText = dataElement.name;
+ byId("hierarchyTree_selectedCode").value = dataElement.code;
+
+ byId("hierarchyTree_selectedCode").onchange = function () {
+ if (this.value.length > 3) return tip("Abbreviation must be 3 characters or less", false, "error", 3000);
+ if (!this.value.length) return tip("Abbreviation cannot be empty", false, "error", 3000);
+
+ node.select("text").text(this.value);
+ dataElement.code = this.value;
+ };
+
+ const createOriginButtons = () => {
+ byId("hierarchyTree_selectedOrigins").innerHTML = dataElement.origins
+ .filter(origin => origin)
+ .map((origin, index) => {
+ const {name, code} = validElements.find(r => r.i === origin) || {};
+ const type = index ? "Secondary" : "Primary";
+ const tip = `${type} origin: ${name}. Click to remove link to that origin`;
+ return ``;
+ })
+ .join("");
+
+ byId("hierarchyTree_selectedOrigins").onclick = event => {
+ const target = event.target;
+ if (target.tagName !== "BUTTON") return;
+ const origin = Number(target.dataset.id);
+ const filtered = dataElement.origins.filter(elementOrigin => elementOrigin !== origin);
+ dataElement.origins = filtered.length ? filtered : [0];
+ target.remove();
+ updateTree();
+ };
+ };
+
+ createOriginButtons();
+
+ byId("hierarchyTree_selectedSelectButton").onclick = () => {
+ const origins = dataElement.origins;
+
+ const descendants = d.descendants().map(d => d.data.i);
+ const selectableElements = validElements.filter(({i}) => !descendants.includes(i));
+
+ const selectableElementsHtml = selectableElements.map(({i, name, code, color}) => {
+ const isPrimary = origins[0] === i ? "checked" : "";
+ const isChecked = origins.includes(i) ? "checked" : "";
+
+ if (i === 0) {
+ return /*html*/ `
+
+
+ Top level
+
+ `;
+ }
+
+ return /*html*/ `
+
+
+
+
+
+ `;
+ });
+
+ byId("hierarchyTree_originSelector").innerHTML = /*html*/ `
+
+ `;
+
+ $("#hierarchyTree_originSelector").dialog({
+ title: "Select origins",
+ position: {my: "center", at: "center", of: "svg"},
+ buttons: {
+ Select: () => {
+ $("#hierarchyTree_originSelector").dialog("close");
+ const $selector = byId("hierarchyTree_originSelector");
+ const selectedRadio = $selector.querySelector("input[type='radio']:checked");
+ const selectedCheckboxes = $selector.querySelectorAll("input[type='checkbox']:checked");
+
+ const primary = selectedRadio ? Number(selectedRadio.value) : 0;
+ const secondary = Array.from(selectedCheckboxes)
+ .map(input => Number(input.dataset.id))
+ .filter(origin => origin !== primary);
+
+ dataElement.origins = [primary, ...secondary];
+
+ updateTree();
+ createOriginButtons();
+ },
+ Cancel: () => {
+ $("#hierarchyTree_originSelector").dialog("close");
+ }
+ }
+ });
+ };
+
+ byId("hierarchyTree_selectedCloseButton").onclick = () => {
+ this.style.outline = "none";
+ byId("hierarchyTree_selected").style.display = "none";
+ byId("hierarchyTree_infoLine").style.display = "block";
+ };
+}
+
+function handleNoteEnter(d) {
+ if (d.depth === 0) return;
+
+ this.classList.add("selected");
+ onNodeEnter(d);
+
+ byId("hierarchyTree_infoLine").innerText = getDescription(d.data);
+ tip("Drag to other node to add parent, click to edit");
+}
+
+function handleNodeExit(d) {
+ this.classList.remove("selected");
+ onNodeLeave(d);
+
+ byId("hierarchyTree_infoLine").innerHTML = "";
+ tip("");
+}
+
+function dragToReorigin(from) {
+ dragLine.attr("d", `M${from.x},${from.y}L${from.x},${from.y}`);
+
+ d3.event.on("drag", () => {
+ dragLine.attr("d", `M${from.x},${from.y}L${d3.event.x},${d3.event.y}`);
+ });
+
+ d3.event.on("end", function () {
+ dragLine.attr("d", "");
+ const selected = nodes.select("g.selected");
+ if (!selected.size()) return;
+
+ const elementId = from.data.i;
+ const newOrigin = selected.datum().data.i;
+ if (elementId === newOrigin) return; // dragged to itself
+ if (from.data.origins.includes(newOrigin)) return; // already a child of the selected node
+ if (from.descendants().some(node => node.data.i === newOrigin)) return; // cannot be a child of its own child
+
+ const element = dataElements.find(({i}) => i === elementId);
+ if (!element) return;
+
+ if (element.origins[0] === 0) element.origins = [];
+ element.origins.push(newOrigin);
+
+ selectElement(from);
+ updateTree();
+ });
+}
diff --git a/modules/religions-generator.js b/modules/religions-generator.js
index a213f728..b7b3464c 100644
--- a/modules/religions-generator.js
+++ b/modules/religions-generator.js
@@ -418,7 +418,7 @@ window.Religions = (function () {
const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk");
if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name;
- const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 30, max: 2});
+ const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 150 / count, max: 2});
const expansionism = rand(3, 8);
const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor();
const color = getMixedColor(baseColor, 0.3, 0);
@@ -451,7 +451,7 @@ window.Religions = (function () {
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center];
- const origins = getReligionsInRadius({x, y, r: 75, max: rand(0, 4)});
+ const origins = getReligionsInRadius({x, y, r: 300 / count, max: rand(0, 4)});
const deity = getDeityName(culture);
const name = getCultName(form, center);
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 079d95ee..3cf31e5c 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -32,7 +32,7 @@ function clicked() {
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]);
+ const node = byId("island_" + pack.cells.f[i]);
editCoastline(node);
} else if (grand.id === "lakes") editLake();
}
@@ -58,10 +58,10 @@ function closeDialogs(except = "#except") {
// move brush radius circle
function moveCircle(x, y, r = 20) {
- let circle = document.getElementById("brushCircle");
+ let circle = byId("brushCircle");
if (!circle) {
const html = /* html */ ``;
- document.getElementById("debug").insertAdjacentHTML("afterBegin", html);
+ byId("debug").insertAdjacentHTML("afterBegin", html);
} else {
circle.setAttribute("cx", x);
circle.setAttribute("cy", y);
@@ -70,7 +70,7 @@ function moveCircle(x, y, r = 20) {
}
function removeCircle() {
- if (document.getElementById("brushCircle")) document.getElementById("brushCircle").remove();
+ if (byId("brushCircle")) byId("brushCircle").remove();
}
// get browser-defined fit-content
@@ -79,8 +79,8 @@ function fitContent() {
}
// apply sorting behaviour for lines on Editor header click
-document.querySelectorAll(".sortable").forEach(function (e) {
- e.addEventListener("click", function () {
+document.querySelectorAll(".sortable").forEach(function (event) {
+ event.on("click", function () {
sortLines(this);
});
});
@@ -90,7 +90,7 @@ function applySortingByHeader(headerContainer) {
.getElementById(headerContainer)
.querySelectorAll(".sortable")
.forEach(function (element) {
- element.addEventListener("click", function () {
+ element.on("click", function () {
sortLines(this);
});
});
@@ -235,7 +235,7 @@ function removeBurg(id) {
if (burg.coa) {
const coaId = "burgCOA" + id;
- if (document.getElementById(coaId)) document.getElementById(coaId).remove();
+ if (byId(coaId)) byId(coaId).remove();
emblems.select(`#burgEmblems > use[data-i='${id}']`).remove();
delete burg.coa; // remove to save data
}
@@ -629,7 +629,7 @@ function createPicker() {
}
function updateSelectedRect(fill) {
- document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
+ byId("picker").querySelector("rect.selected").classList.remove("selected");
document
.getElementById("picker")
.querySelector("rect[fill='" + fill.toLowerCase() + "']")
@@ -687,7 +687,7 @@ function openPicker(fill, callback) {
updateSelectedRect(fill);
openPicker.updateFill = function () {
- const selected = document.getElementById("picker").querySelector("rect.selected");
+ const selected = byId("picker").querySelector("rect.selected");
if (!selected) return;
callback(selected.getAttribute("fill"));
};
@@ -888,8 +888,8 @@ function selectIcon(initial, callback) {
if (!callback) return;
$("#iconSelector").dialog();
- const table = document.getElementById("iconTable");
- const input = document.getElementById("iconInput");
+ const table = byId("iconTable");
+ const input = byId("iconInput");
input.value = initial;
if (!table.innerHTML) {
@@ -1119,13 +1119,11 @@ function selectIcon(initial, callback) {
}
function getAreaUnit(squareMark = "²") {
- return document.getElementById("areaUnit").value === "square"
- ? document.getElementById("distanceUnitInput").value + squareMark
- : document.getElementById("areaUnit").value;
+ return byId("areaUnit").value === "square" ? byId("distanceUnitInput").value + squareMark : byId("areaUnit").value;
}
function getArea(rawArea) {
- const distanceScale = document.getElementById("distanceScaleInput")?.value;
+ const distanceScale = byId("distanceScaleInput")?.value;
return rawArea * distanceScale ** 2;
}
@@ -1150,44 +1148,44 @@ function confirmationDialog(options) {
}
};
- document.getElementById("alertMessage").innerHTML = message;
+ byId("alertMessage").innerHTML = message;
$("#alert").dialog({resizable: false, title, buttons});
}
// add and register event listeners to clean up on editor closure
function listen(element, event, handler) {
- element.addEventListener(event, handler);
+ element.on(event, handler);
return () => element.removeEventListener(event, handler);
}
// Calls the refresh functionality on all editors currently open.
function refreshAllEditors() {
TIME && console.time("refreshAllEditors");
- if (document.getElementById("culturesEditorRefresh")?.offsetParent) culturesEditorRefresh.click();
- if (document.getElementById("biomesEditorRefresh")?.offsetParent) biomesEditorRefresh.click();
- if (document.getElementById("diplomacyEditorRefresh")?.offsetParent) diplomacyEditorRefresh.click();
- if (document.getElementById("provincesEditorRefresh")?.offsetParent) provincesEditorRefresh.click();
- if (document.getElementById("religionsEditorRefresh")?.offsetParent) religionsEditorRefresh.click();
- if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
- if (document.getElementById("zonesEditorRefresh")?.offsetParent) zonesEditorRefresh.click();
+ if (byId("culturesEditorRefresh")?.offsetParent) culturesEditorRefresh.click();
+ if (byId("biomesEditorRefresh")?.offsetParent) biomesEditorRefresh.click();
+ if (byId("diplomacyEditorRefresh")?.offsetParent) diplomacyEditorRefresh.click();
+ if (byId("provincesEditorRefresh")?.offsetParent) provincesEditorRefresh.click();
+ if (byId("religionsEditorRefresh")?.offsetParent) religionsEditorRefresh.click();
+ if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
+ if (byId("zonesEditorRefresh")?.offsetParent) zonesEditorRefresh.click();
TIME && console.timeEnd("refreshAllEditors");
}
// dynamically loaded editors
async function editStates() {
if (customization) return;
- const Editor = await import("../dynamic/editors/states-editor.js?v=08062022");
+ const Editor = await import("../dynamic/editors/states-editor.js?v=12062022");
Editor.open();
}
async function editCultures() {
if (customization) return;
- const Editor = await import("../dynamic/editors/cultures-editor.js?v=08062022");
+ const Editor = await import("../dynamic/editors/cultures-editor.js?v=12062022");
Editor.open();
}
async function editReligions() {
if (customization) return;
- const Editor = await import("../dynamic/editors/religions-editor.js?v=080620222");
+ const Editor = await import("../dynamic/editors/religions-editor.js?v=12062022");
Editor.open();
}
diff --git a/modules/ui/regiment-editor.js b/modules/ui/regiment-editor.js
index 881ce3bf..359cca91 100644
--- a/modules/ui/regiment-editor.js
+++ b/modules/ui/regiment-editor.js
@@ -13,7 +13,9 @@ function editRegiment(selector) {
drawBase();
$("#regimentEditor").dialog({
- title: "Edit Regiment", resizable: false, close: closeEditor,
+ title: "Edit Regiment",
+ resizable: false,
+ close: closeEditor,
position: {my: "left top", at: "left+10 top+10", of: "#map"}
});
@@ -40,17 +42,19 @@ function editRegiment(selector) {
}
function updateRegimentData(regiment) {
- document.getElementById("regimentType").className = regiment.n ? "icon-anchor" :"icon-users";
+ document.getElementById("regimentType").className = regiment.n ? "icon-anchor" : "icon-users";
document.getElementById("regimentName").value = regiment.name;
document.getElementById("regimentEmblem").value = regiment.icon;
const composition = document.getElementById("regimentComposition");
- composition.innerHTML = options.military.map(u => {
- return `
+ composition.innerHTML = options.military
+ .map(u => {
+ return `
${capitalize(u.name)}:
-
-
${u.type} `
- }).join("");
+
+
${u.type} `;
+ })
+ .join("");
composition.querySelectorAll("input").forEach(el => el.addEventListener("change", changeUnit));
}
@@ -58,26 +62,49 @@ function editRegiment(selector) {
function drawBase() {
const reg = regiment();
const clr = pack.states[elSelected.dataset.state].color;
- const base = viewbox.insert("g", "g#armies").attr("id", "regimentBase").attr("stroke-width", .3).attr("stroke", "#000").attr("cursor", "move");
- base.on("mouseenter", () => {tip("Regiment base. Drag to re-base the regiment", true);}).on("mouseleave", () => {tip('', true);});
+ const base = viewbox
+ .insert("g", "g#armies")
+ .attr("id", "regimentBase")
+ .attr("stroke-width", 0.3)
+ .attr("stroke", "#000")
+ .attr("cursor", "move");
+ base
+ .on("mouseenter", () => {
+ tip("Regiment base. Drag to re-base the regiment", true);
+ })
+ .on("mouseleave", () => {
+ tip("", true);
+ });
- base.append("line").attr("x1", reg.bx).attr("y1", reg.by).attr("x2", reg.x).attr("y2", reg.y).attr("class", "dragLine");
- base.append("circle").attr("cx", reg.bx).attr("cy", reg.by).attr("r", 2).attr("fill", clr).call(d3.drag().on("drag", dragBase));
+ base
+ .append("line")
+ .attr("x1", reg.bx)
+ .attr("y1", reg.by)
+ .attr("x2", reg.x)
+ .attr("y2", reg.y)
+ .attr("class", "regimentDragLine");
+ base
+ .append("circle")
+ .attr("cx", reg.bx)
+ .attr("cy", reg.by)
+ .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";
+ document.getElementById("regimentType").className = reg.n ? "icon-anchor" : "icon-users";
const size = +armies.attr("box-size");
const baseRect = elSelected.querySelectorAll("rect")[0];
const iconRect = elSelected.querySelectorAll("rect")[1];
const icon = elSelected.querySelector(".regimentIcon");
- const x = reg.n ? reg.x-size*2 : reg.x-size*3;
+ const x = reg.n ? reg.x - size * 2 : reg.x - size * 3;
baseRect.setAttribute("x", x);
- baseRect.setAttribute("width", reg.n ? size*4 : size*6);
- iconRect.setAttribute("x", x - size*2);
+ baseRect.setAttribute("width", reg.n ? size * 4 : size * 6);
+ iconRect.setAttribute("x", x - size * 2);
icon.setAttribute("x", x - size);
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
}
@@ -87,13 +114,17 @@ function editRegiment(selector) {
}
function restoreName() {
- const reg = regiment(), regs = pack.states[elSelected.dataset.state].military;
+ 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 selectEmblem() {
- selectIcon(regimentEmblem.value, v => {regimentEmblem.value = v; changeEmblem()});
+ selectIcon(regimentEmblem.value, v => {
+ regimentEmblem.value = v;
+ changeEmblem();
+ });
}
function changeEmblem() {
@@ -104,7 +135,7 @@ function editRegiment(selector) {
function changeUnit() {
const u = this.dataset.u;
const reg = regiment();
- reg.u[u] = (+this.value)||0;
+ reg.u[u] = +this.value || 0;
reg.a = d3.sum(Object.values(reg.u));
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
@@ -112,24 +143,47 @@ function editRegiment(selector) {
}
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
+ 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(u2).forEach(u => u2[u] = Math.floor(u2[u]/2)); // halved new reg
+ Object.keys(u2).forEach(u => (u2[u] = Math.floor(u2[u] / 2))); // halved new reg
const a = d3.sum(Object.values(u2)); // new reg total
- if (!a) {tip("Not enough forces to split", false, "error"); return}; // nothing to add
+ if (!a) {
+ tip("Not enough forces to split", false, "error");
+ return;
+ } // nothing to add
// update old regiment
- Object.keys(u1).forEach(u => u1[u] = Math.ceil(u1[u]/2)); // halved old reg
+ Object.keys(u1).forEach(u => (u1[u] = Math.ceil(u1[u] / 2))); // halved old reg
reg.a = d3.sum(Object.values(u1)); // old reg total
- regimentComposition.querySelectorAll("input").forEach(el => el.value = reg.u[el.dataset.u]||0);
+ regimentComposition.querySelectorAll("input").forEach(el => (el.value = reg.u[el.dataset.u] || 0));
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
// create new regiment
const shift = +armies.attr("box-size") * 2;
- const y = function(x, y) {do {y+=shift} while (military.find(r => r.x === x && r.y === y)); return y;}
- const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:y(reg.x, reg.y), bx:reg.bx, by:reg.by, state, icon: reg.icon};
+ const y = function (x, y) {
+ do {
+ y += shift;
+ } while (military.find(r => r.x === x && r.y === y));
+ return y;
+ };
+ const newReg = {
+ a,
+ cell: reg.cell,
+ i,
+ n: reg.n,
+ u: u2,
+ x: reg.x,
+ y: y(reg.x, reg.y),
+ bx: reg.bx,
+ by: reg.by,
+ state,
+ icon: reg.icon
+ };
newReg.name = Military.getName(newReg, military);
military.push(newReg);
Military.generateNote(newReg, pack.states[state]); // add legend
@@ -152,11 +206,13 @@ function editRegiment(selector) {
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 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, bx:x, by:y, state, icon:"🛡️"};
+ const reg = {a: 0, cell, i, n, u: {}, x, y, bx: x, by: y, state, icon: "🛡️"};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
@@ -179,30 +235,59 @@ function editRegiment(selector) {
}
function attackRegimentOnClick() {
- const target = d3.event.target, regSelected = target.parentElement, army = regSelected.parentElement;
- const oldState = +elSelected.dataset.state, newState = +regSelected.dataset.state;
+ const target = d3.event.target,
+ regSelected = target.parentElement,
+ army = regSelected.parentElement;
+ const oldState = +elSelected.dataset.state,
+ newState = +regSelected.dataset.state;
- if (army.parentElement.id !== "armies") {tip("Please click on a regiment to attack", false, "error"); return;}
- if (regSelected === elSelected) {tip("Regiment cannot attack itself", false, "error"); return;}
- if (oldState === newState) {tip("Cannot attack fraternal regiment", false, "error"); return;}
+ if (army.parentElement.id !== "armies") {
+ tip("Please click on a regiment to attack", false, "error");
+ return;
+ }
+ if (regSelected === elSelected) {
+ tip("Regiment cannot attack itself", false, "error");
+ return;
+ }
+ if (oldState === newState) {
+ tip("Cannot attack fraternal regiment", false, "error");
+ return;
+ }
const attacker = regiment();
const defender = pack.states[regSelected.dataset.state].military.find(r => r.i == regSelected.dataset.id);
- if (!attacker.a || !defender.a) {tip("Regiment has no troops to battle", false, "error"); return;}
+ if (!attacker.a || !defender.a) {
+ tip("Regiment has no troops to battle", false, "error");
+ return;
+ }
// save initial position to temp attribute
- attacker.px = attacker.x, attacker.py = attacker.y;
- defender.px = defender.x, defender.py = defender.y;
+ (attacker.px = attacker.x), (attacker.py = attacker.y);
+ (defender.px = defender.x), (defender.py = defender.y);
// move attacker to defender
- Military.moveRegiment(attacker, defender.x, defender.y-8);
+ Military.moveRegiment(attacker, defender.x, defender.y - 8);
// draw battle icon
- const attack = d3.transition().delay(300).duration(700).ease(d3.easeSinInOut).on("end", () => new Battle(attacker, defender));
- svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2)
- .text("⚔️").attr("font-size", 0).attr("opacity", 1)
- .style("dominant-baseline", "central").style("text-anchor", "middle")
- .transition(attack).attr("font-size", 1000).attr("opacity", .2).remove();
+ const attack = d3
+ .transition()
+ .delay(300)
+ .duration(700)
+ .ease(d3.easeSinInOut)
+ .on("end", () => new Battle(attacker, defender));
+ svg
+ .append("text")
+ .attr("x", window.innerWidth / 2)
+ .attr("y", window.innerHeight / 2)
+ .text("⚔️")
+ .attr("font-size", 0)
+ .attr("opacity", 1)
+ .style("dominant-baseline", "central")
+ .style("text-anchor", "middle")
+ .transition(attack)
+ .attr("font-size", 1000)
+ .attr("opacity", 0.2)
+ .remove();
clearMainTip();
$("#regimentEditor").dialog("close");
@@ -222,18 +307,27 @@ function editRegiment(selector) {
}
function attachRegimentOnClick() {
- const target = d3.event.target, regSelected = target.parentElement, army = regSelected.parentElement;
- const oldState = +elSelected.dataset.state, newState = +regSelected.dataset.state;
+ const target = d3.event.target,
+ regSelected = target.parentElement,
+ army = regSelected.parentElement;
+ const oldState = +elSelected.dataset.state,
+ newState = +regSelected.dataset.state;
- if (army.parentElement.id !== "armies") {tip("Please click on a regiment", false, "error"); return;}
- if (regSelected === elSelected) {tip("Cannot attach regiment to itself. Please click on another regiment", false, "error"); return;}
+ if (army.parentElement.id !== "armies") {
+ tip("Please click on a regiment", false, "error");
+ return;
+ }
+ if (regSelected === elSelected) {
+ tip("Cannot attach regiment to itself. Please click on another regiment", false, "error");
+ return;
+ }
const reg = regiment(); // reg to be attached
const sel = pack.states[newState].military.find(r => r.i == regSelected.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];
+ 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
regSelected.querySelector("text").innerHTML = Military.getTotal(sel); // update selected reg total text
@@ -247,7 +341,7 @@ function editRegiment(selector) {
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
$("#regimentEditor").dialog("close");
- editRegiment("#"+regSelected.id);
+ editRegiment("#" + regSelected.id);
}
function regenerateLegend() {
@@ -264,9 +358,11 @@ function editRegiment(selector) {
function removeRegiment() {
alertMessage.innerHTML = "Are you sure you want to remove the regiment?";
- $("#alert").dialog({resizable: false, title: "Remove regiment",
+ $("#alert").dialog({
+ resizable: false,
+ title: "Remove regiment",
buttons: {
- Remove: function() {
+ Remove: function () {
$(this).dialog("close");
const military = pack.states[elSelected.dataset.state].military;
const regIndex = military.indexOf(regiment());
@@ -281,7 +377,9 @@ function editRegiment(selector) {
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
$("#regimentEditor").dialog("close");
},
- Cancel: function() {$(this).dialog("close");}
+ Cancel: function () {
+ $(this).dialog("close");
+ }
}
});
}
@@ -305,16 +403,17 @@ function editRegiment(selector) {
const self = elSelected === this;
const baseLine = viewbox.select("g#regimentBase > line");
- d3.event.on("drag", function() {
- const x = reg.x = d3.event.x, y = reg.y = d3.event.y;
+ d3.event.on("drag", function () {
+ const x = (reg.x = d3.event.x),
+ y = (reg.y = d3.event.y);
baseRect.setAttribute("x", x1(x));
baseRect.setAttribute("y", y1(y));
text.setAttribute("x", x);
text.setAttribute("y", y);
- iconRect.setAttribute("x", x1(x)-h);
+ iconRect.setAttribute("x", x1(x) - h);
iconRect.setAttribute("y", y1(y));
- icon.setAttribute("x", x1(x)-size);
+ icon.setAttribute("x", x1(x) - size);
icon.setAttribute("y", y);
if (self) baseLine.attr("x2", x).attr("y2", y);
});
@@ -324,13 +423,16 @@ function editRegiment(selector) {
const baseLine = viewbox.select("g#regimentBase > line");
const reg = regiment();
- d3.event.on("drag", function() {
+ d3.event.on("drag", function () {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
baseLine.attr("x1", d3.event.x).attr("y1", d3.event.y);
});
- d3.event.on("end", function() {reg.bx = d3.event.x; reg.by = d3.event.y;});
+ d3.event.on("end", function () {
+ reg.bx = d3.event.x;
+ reg.by = d3.event.y;
+ });
}
function closeEditor() {
@@ -343,5 +445,4 @@ function editRegiment(selector) {
restoreDefaultEvents();
elSelected = null;
}
-
-}
\ No newline at end of file
+}
diff --git a/versioning.js b/versioning.js
index 7bb4172f..21d29bf7 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,7 +1,7 @@
"use strict";
// version and caching control
-const version = "1.86.05"; // generator version, update each time
+const version = "1.86.06"; // generator version, update each time
{
document.title += " v" + version;