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 */ `
-
- -
`; - - // 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 */ `
-
- -
`; + 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 */ `
+ + + + + + + + + + + + + +
+
+ +
+
+
`; + + 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*/ ` +
+ ${selectableElementsHtml.join("")} +
+ `; + + $("#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;