From f4ef859af81631362592fb491fab5b36cd6aa2de Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 5 Jun 2022 01:27:58 +0300 Subject: [PATCH 1/8] feat: multi-parental tree --- modules/cultures-generator.js | 145 +++++++++++++-- modules/dynamic/auto-update.js | 131 +++++++++++-- modules/dynamic/editors/cultures-editor.js | 205 +++++++++++++-------- versioning.js | 3 +- 4 files changed, 370 insertions(+), 114 deletions(-) diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js index 37f5bc93..b1072c22 100644 --- a/modules/cultures-generator.js +++ b/modules/cultures-generator.js @@ -60,7 +60,7 @@ window.Cultures = (function () { c.color = colors[i]; c.type = defineCultureType(cell); c.expansionism = defineCultureExpansionism(c.type); - c.origin = 0; + c.origins = [0]; c.code = abbreviate(c.name, codes); codes.push(c.code); cells.culture[cell] = i + 1; @@ -80,7 +80,7 @@ window.Cultures = (function () { } // the first culture with id 0 is for wildlands - cultures.unshift({name: "Wildlands", i: 0, base: 1, origin: null, shield: "round"}); + cultures.unshift({name: "Wildlands", i: 0, base: 1, origins: [null], shield: "round"}); // make sure all bases exist in nameBases if (!nameBases.length) { @@ -115,7 +115,11 @@ window.Cultures = (function () { if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations const f = pack.features[cells.f[cells.haven[i]]]; // opposite feature if (f.type === "lake" && f.cells > 5) return "Lake"; // low water cross penalty and high for growth not along coastline - if ((cells.harbor[i] && f.type !== "lake" && P(0.1)) || (cells.harbor[i] === 1 && P(0.6)) || (pack.features[cells.f[i]].group === "isle" && P(0.4))) + if ( + (cells.harbor[i] && f.type !== "lake" && P(0.1)) || + (cells.harbor[i] === 1 && P(0.6)) || + (pack.features[cells.f[i]].group === "isle" && P(0.4)) + ) return "Naval"; // low water cross penalty and high for non-along-coastline growth if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth if (cells.t[i] > 2 && [3, 7, 8, 9, 10, 12].includes(cells.biome[i])) return "Hunting"; // high penalty in non-native biomes @@ -163,7 +167,22 @@ window.Cultures = (function () { const emblemShape = document.getElementById("emblemShape").value; if (emblemShape === "random") shield = getRandomShield(); - pack.cultures.push({name, color, base, center, i, expansionism: 1, type: "Generic", cells: 0, area: 0, rural: 0, urban: 0, origin: 0, code, shield}); + pack.cultures.push({ + name, + color, + base, + center, + i, + expansionism: 1, + type: "Generic", + cells: 0, + area: 0, + rural: 0, + urban: 0, + origins: [0], + code, + shield + }); }; const getDefault = function (count) { @@ -180,7 +199,8 @@ window.Cultures = (function () { return d ? d + 1 : 1; }; // temperature difference fee const bd = (cell, biomes, fee = 4) => (biomes.includes(cells.biome[cell]) ? 1 : fee); // biome difference fee - const sf = (cell, fee = 4) => (cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee); // not on sea coast fee + const sf = (cell, fee = 4) => + cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee; // not on sea coast fee if (culturesSet.value === "european") { return [ @@ -208,7 +228,13 @@ window.Cultures = (function () { {name: "Hantzu", base: 11, odd: 1, sort: i => n(i) / td(i, 13), shield: "banner"}, {name: "Yamoto", base: 12, odd: 1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"}, {name: "Turchian", base: 16, odd: 1, sort: i => n(i) / td(i, 12), shield: "round"}, - {name: "Berberan", base: 17, odd: 0.2, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "oval"}, + { + name: "Berberan", + base: 17, + odd: 0.2, + sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], + shield: "oval" + }, {name: "Eurabic", base: 18, odd: 1, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "oval"}, {name: "Efratic", base: 23, odd: 0.1, sort: i => (n(i) / td(i, 22)) * t[i], shield: "round"}, {name: "Tehrani", base: 24, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"}, @@ -259,15 +285,45 @@ window.Cultures = (function () { if (culturesSet.value === "highFantasy") { return [ // fantasy races - {name: "Quenian (Elfish)", base: 33, odd: 1, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "gondor"}, // Elves - {name: "Eldar (Elfish)", base: 33, odd: 1, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "noldor"}, // Elves - {name: "Trow (Dark Elfish)", base: 34, odd: 0.9, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "hessen"}, // Dark Elves - {name: "Lothian (Dark Elfish)", base: 34, odd: 0.3, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "wedged"}, // Dark Elves + { + name: "Quenian (Elfish)", + base: 33, + odd: 1, + sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], + shield: "gondor" + }, // Elves + { + name: "Eldar (Elfish)", + base: 33, + odd: 1, + sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], + shield: "noldor" + }, // Elves + { + name: "Trow (Dark Elfish)", + base: 34, + odd: 0.9, + sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], + shield: "hessen" + }, // Dark Elves + { + name: "Lothian (Dark Elfish)", + base: 34, + odd: 0.3, + sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], + shield: "wedged" + }, // Dark Elves {name: "Dunirr (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "ironHills"}, // Dwarfs {name: "Khazadur (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarfs {name: "Kobold (Goblin)", base: 36, odd: 1, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin {name: "Uruk (Orkish)", base: 37, odd: 1, sort: i => h[i] * t[i], shield: "urukHai"}, // Orc - {name: "Ugluk (Orkish)", base: 37, odd: 0.5, sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: "moriaOrc"}, // Orc + { + name: "Ugluk (Orkish)", + base: 37, + odd: 0.5, + sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), + shield: "moriaOrc" + }, // Orc {name: "Yotunn (Giants)", base: 38, odd: 0.7, sort: i => td(i, -10), shield: "pavise"}, // Giant {name: "Rake (Drakonic)", base: 39, odd: 0.7, sort: i => -s[i], shield: "fantasy2"}, // Draconic {name: "Arago (Arachnid)", base: 40, odd: 0.7, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid @@ -276,7 +332,13 @@ window.Cultures = (function () { {name: "Anor (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 10), shield: "fantasy5"}, {name: "Dail (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 13), shield: "roman"}, {name: "Rohand (Human)", base: 16, odd: 1, sort: i => n(i) / td(i, 16), shield: "round"}, - {name: "Dulandir (Human)", base: 31, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "easterling"} + { + name: "Dulandir (Human)", + base: 31, + odd: 1, + sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], + shield: "easterling" + } ]; } @@ -296,18 +358,48 @@ window.Cultures = (function () { {name: "Hetallian", base: 3, odd: 0.3, sort: i => n(i) / td(i, 15), shield: "oval"}, {name: "Astellian", base: 4, odd: 0.3, sort: i => n(i) / td(i, 16), shield: "spanish"}, // rare real-world exotic - {name: "Kiswaili", base: 28, odd: 0.05, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: "vesicaPiscis"}, + { + name: "Kiswaili", + base: 28, + odd: 0.05, + sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), + shield: "vesicaPiscis" + }, {name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"}, {name: "Koryo", base: 10, odd: 0.05, sort: i => n(i) / td(i, 12) / t[i], shield: "round"}, {name: "Hantzu", base: 11, odd: 0.05, sort: i => n(i) / td(i, 13), shield: "banner"}, {name: "Yamoto", base: 12, odd: 0.05, sort: i => n(i) / td(i, 15) / t[i], shield: "round"}, {name: "Guantzu", base: 30, odd: 0.05, sort: i => n(i) / td(i, 17), shield: "banner"}, - {name: "Ulus", base: 31, odd: 0.05, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"}, + { + name: "Ulus", + base: 31, + odd: 0.05, + sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], + shield: "banner" + }, {name: "Turan", base: 16, odd: 0.05, sort: i => n(i) / td(i, 12), shield: "round"}, - {name: "Berberan", base: 17, odd: 0.05, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "round"}, - {name: "Eurabic", base: 18, odd: 0.05, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"}, + { + name: "Berberan", + base: 17, + odd: 0.05, + sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], + shield: "round" + }, + { + name: "Eurabic", + base: 18, + odd: 0.05, + sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], + shield: "round" + }, {name: "Slovan", base: 5, odd: 0.05, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"}, - {name: "Keltan", base: 22, odd: 0.1, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: "vesicaPiscis"}, + { + name: "Keltan", + base: 22, + odd: 0.1, + sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), + shield: "vesicaPiscis" + }, {name: "Elladan", base: 7, odd: 0.2, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"}, {name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"}, // fantasy races @@ -350,12 +442,24 @@ window.Cultures = (function () { {name: "Nawatli", base: 14, odd: 0.1, sort: i => h[i] / td(i, 18) / bd(i, [7]), shield: "square"}, {name: "Vengrian", base: 15, odd: 0.2, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "wedged"}, {name: "Turchian", base: 16, odd: 0.2, sort: i => n(i) / td(i, 13), shield: "round"}, - {name: "Berberan", base: 17, odd: 0.1, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "round"}, + { + name: "Berberan", + base: 17, + odd: 0.1, + sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], + shield: "round" + }, {name: "Eurabic", base: 18, odd: 0.2, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"}, {name: "Inuk", base: 19, odd: 0.05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield: "square"}, {name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "spanish"}, {name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"}, - {name: "Keltan", base: 22, odd: 0.05, sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: "vesicaPiscis"}, + { + name: "Keltan", + base: 22, + odd: 0.05, + sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], + shield: "vesicaPiscis" + }, {name: "Efratic", base: 23, odd: 0.05, sort: i => (n(i) / td(i, 22)) * t[i], shield: "diamond"}, {name: "Tehrani", base: 24, odd: 0.1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"}, {name: "Maui", base: 25, odd: 0.05, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "round"}, @@ -394,7 +498,8 @@ window.Cultures = (function () { const heightCost = getHeightCost(e, cells.h[e], type); const riverCost = getRiverCost(cells.r[e], e, type); const typeCost = getTypeCost(cells.t[e], type); - const totalCost = p + (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / pack.cultures[c].expansionism; + const totalCost = + p + (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / pack.cultures[c].expansionism; if (totalCost > neutral) return; diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index 6df4b634..da5f0250 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -30,8 +30,18 @@ export function resolveVersionConflicts(version) { .attr("stroke-dasharray", null) .attr("stroke-linecap", null) .attr("filter", null); - stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt"); - provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt"); + stateBorders + .attr("opacity", 0.8) + .attr("stroke", "#56566d") + .attr("stroke-width", 1) + .attr("stroke-dasharray", "2") + .attr("stroke-linecap", "butt"); + provinceBorders + .attr("opacity", 0.8) + .attr("stroke", "#56566d") + .attr("stroke-width", 0.5) + .attr("stroke-dasharray", "1") + .attr("stroke-linecap", "butt"); // v1.0 added state relations, provinces, forms and full names provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6); @@ -47,7 +57,12 @@ export function resolveVersionConflicts(version) { // v1.0 added zones layer zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none"); - zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt"); + zones + .attr("opacity", 0.6) + .attr("stroke", null) + .attr("stroke-width", 0) + .attr("stroke-dasharray", null) + .attr("stroke-linecap", "butt"); addZones(); if (!markers.selectAll("*").size()) { Markers.generate(); @@ -55,9 +70,23 @@ export function resolveVersionConflicts(version) { } // v1.0 add fogging layer (state focus) - fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none"); + fogging = viewbox + .insert("g", "#ruler") + .attr("id", "fogging-cont") + .attr("mask", "url(#fog)") + .append("g") + .attr("id", "fogging") + .style("display", "none"); fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); - defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white"); + defs + .append("mask") + .attr("id", "fog") + .append("rect") + .attr("x", 0) + .attr("y", 0) + .attr("width", "100%") + .attr("height", "100%") + .attr("fill", "white"); // v1.0 changes states opacity bask to regions level if (statesBody.attr("opacity")) { @@ -103,12 +132,24 @@ export function resolveVersionConflicts(version) { if (!document.getElementById("freshwater")) { lakes.append("g").attr("id", "freshwater"); - lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null); + lakes + .select("#freshwater") + .attr("opacity", 0.5) + .attr("fill", "#a6c1fd") + .attr("stroke", "#5f799d") + .attr("stroke-width", 0.7) + .attr("filter", null); } if (!document.getElementById("salt")) { lakes.append("g").attr("id", "salt"); - lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null); + lakes + .select("#salt") + .attr("opacity", 0.5) + .attr("fill", "#409b8a") + .attr("stroke", "#388985") + .attr("stroke-width", 0.7) + .attr("filter", null); } // v1.1 added new lake and coast groups @@ -116,14 +157,42 @@ export function resolveVersionConflicts(version) { lakes.append("g").attr("id", "sinkhole"); lakes.append("g").attr("id", "frozen"); lakes.append("g").attr("id", "lava"); - lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null); - lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null); - lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)"); + lakes + .select("#sinkhole") + .attr("opacity", 1) + .attr("fill", "#5bc9fd") + .attr("stroke", "#53a3b0") + .attr("stroke-width", 0.7) + .attr("filter", null); + lakes + .select("#frozen") + .attr("opacity", 0.95) + .attr("fill", "#cdd4e7") + .attr("stroke", "#cfe0eb") + .attr("stroke-width", 0) + .attr("filter", null); + lakes + .select("#lava") + .attr("opacity", 0.7) + .attr("fill", "#90270d") + .attr("stroke", "#f93e0c") + .attr("stroke-width", 2) + .attr("filter", "url(#crumpled)"); coastline.append("g").attr("id", "sea_island"); coastline.append("g").attr("id", "lake_island"); - coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)"); - coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null); + coastline + .select("#sea_island") + .attr("opacity", 0.5) + .attr("stroke", "#1f3846") + .attr("stroke-width", 0.7) + .attr("filter", "url(#dropShadow)"); + coastline + .select("#lake_island") + .attr("opacity", 1) + .attr("stroke", "#7c8eaf") + .attr("stroke-width", 0.35) + .attr("filter", null); } // v1.1 features stores more data @@ -203,7 +272,13 @@ export function resolveVersionConflicts(version) { // v1.3 added militry layer armies = viewbox.insert("g", "#icons").attr("id", "armies"); - armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3); + armies + .attr("opacity", 1) + .attr("fill-opacity", 1) + .attr("font-size", 6) + .attr("box-size", 3) + .attr("stroke", "#000") + .attr("stroke-width", 0.3); turnButtonOn("toggleMilitary"); Military.generate(); } @@ -212,12 +287,23 @@ export function resolveVersionConflicts(version) { // v1.35 added dry lakes if (!lakes.select("#dry").size()) { lakes.append("g").attr("id", "dry"); - lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null); + lakes + .select("#dry") + .attr("opacity", 1) + .attr("fill", "#c9bfa7") + .attr("stroke", "#8e816f") + .attr("stroke-width", 0.7) + .attr("filter", null); } // v1.4 added ice layer ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none"); - ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)"); + ice + .attr("opacity", null) + .attr("fill", "#e8f0f6") + .attr("stroke", "#e8f0f6") + .attr("stroke-width", 1) + .attr("filter", "url(#dropShadow05)"); drawIce(); // v1.4 added icon and power attributes for units @@ -530,4 +616,19 @@ export function resolveVersionConflicts(version) { // v1.84.0 moved intial screen out of maon svg svg.select("#initial").remove(); } + + if (version < 1.86) { + // v1.86.0 added support of multi-origin culture and religion hierarchy trees + for (const culture of pack.cultures) { + const origin = culture.origin; + delete culture.origin; + culture.origins = [origin]; + } + + for (const religion of pack.religions) { + const origin = religion.origin; + delete religion.origin; + religion.origins = [origin]; + } + } } diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js index b3003584..b72702a7 100644 --- a/modules/dynamic/editors/cultures-editor.js +++ b/modules/dynamic/editors/cultures-editor.js @@ -143,7 +143,9 @@ function culturesEditorAddLines() { const rural = c.rural * populationRate; const urban = c.urban * populationRate * urbanization; const population = rn(rural + urban); - const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to edit`; + const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si( + urban + )}. Click to edit`; totalArea += area; totalPopulation += population; @@ -167,7 +169,9 @@ function culturesEditorAddLines() { value="${c.name}" autocorrect="off" spellcheck="false" /> - +
${c.cells}
@@ -175,7 +179,9 @@ function culturesEditorAddLines() {
${si(area)} ${unit}
-
${si(population)}
+
${si( + population + )}
${getShapeOptions(selectShape, c.shield)} `; @@ -199,8 +205,12 @@ function culturesEditorAddLines() { - - + +
${c.cells}
@@ -216,7 +226,9 @@ function culturesEditorAddLines() {
${si(area)} ${unit}
-
${si(population)}
+
${si( + population + )}
${getShapeOptions(selectShape, c.shield)} @@ -276,12 +288,14 @@ function getShapeOptions(selectShape, selected) { const shapes = Object.keys(COA.shields.types) .map(type => Object.keys(COA.shields[type])) .flat(); - const options = shapes.map(shape => ``); + const options = shapes.map( + shape => `` + ); return ``; } function cultureHighlightOn(event) { - const culture = +event.target.dataset.id; + const culture = Number(event.id || event.target.dataset.id); const $info = byId("cultureInfo"); if ($info) { d3.select("#hierarchy") @@ -314,7 +328,7 @@ function cultureHighlightOn(event) { } function cultureHighlightOff(event) { - const culture = +event.target.dataset.id; + const culture = Number(event.id || event.target.dataset.id); const $info = byId("cultureInfo"); if ($info) { d3.select("#hierarchy") @@ -412,7 +426,14 @@ function cultureChangeEmblemsShape() { }); pack.provinces.forEach(province => { - if (pack.cells.culture[province.center] !== culture || !province.i || province.removed || !province.coa || province.coa === "custom") return; + if ( + pack.cells.culture[province.center] !== culture || + !province.i || + province.removed || + !province.coa || + province.coa === "custom" + ) + return; if (shape === province.coa.shield) return; province.coa.shield = shape; rerenderCOA("provinceCOA" + province.i, province.coa); @@ -438,8 +459,12 @@ function changePopulation() { const burgs = pack.burgs.filter(b => !b.removed && b.culture === cultureId); alertMessage.innerHTML = /* html */ `Rural: Urban: - -

Total population: ${l(total)} ⇒ ${l(total)} (100%)

`; + +

Total population: ${l(total)} ⇒ ${l( + total + )} (100%)

`; const update = function () { const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber; @@ -522,10 +547,12 @@ function removeCulture(cultureId) { }); cultures[cultureId].removed = true; - const origin = cultures[cultureId].origin; - cultures.forEach(c => { - if (c.origin === cultureId) c.origin = origin; - }); + cultures + .filter(c => c.i && !c.removed) + .forEach(c => { + c.origins = c.origins.filter(origin => origin !== cultureId); + if (!c.origins.length) c.origins = [0]; + }); refreshCulturesEditor(); } @@ -552,7 +579,12 @@ function cultureRemove() { function drawCultureCenters() { const tooltip = "Drag to move the culture center (ancestral home)"; debug.select("#cultureCenters").remove(); - const cultureCenters = debug.append("g").attr("id", "cultureCenters").attr("stroke-width", 2).attr("stroke", "#444444").style("cursor", "move"); + const cultureCenters = debug + .append("g") + .attr("id", "cultureCenters") + .attr("stroke-width", 2) + .attr("stroke", "#444444") + .style("cursor", "move"); const data = pack.cultures.filter(c => c.i && !c.removed); cultureCenters @@ -623,17 +655,17 @@ function togglePercentageMode() { function showHierarchy() { // build hierarchy tree - pack.cultures[0].origin = null; + 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"); const root = d3 .stratify() .id(d => d.i) - .parentId(d => d.origin)(validCultures); + .parentId(d => d.origins[0])(validCultures); const treeWidth = root.leaves().length; const treeHeight = root.height; - const width = treeWidth * 40; + const width = Math.max(treeWidth * 40, 300); const height = treeHeight * 60; const margin = {top: 10, right: 10, bottom: -5, left: 10}; @@ -649,39 +681,59 @@ function showHierarchy() { .attr("id", "hierarchy") .attr("width", width) .attr("height", height) - .style("text-anchor", "middle"); + .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 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 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); - links - .selectAll("path") - .data(root.links()) - .enter() - .append("path") - .attr("d", d => { - return ( - "M" + - d.source.x + - "," + - d.source.y + - "C" + - d.source.x + - "," + - (d.source.y * 3 + d.target.y) / 4 + - " " + - d.target.x + - "," + - (d.source.y * 2 + d.target.y) / 3 + - " " + - d.target.x + - "," + - d.target.y - ); - }); + + 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") @@ -691,42 +743,29 @@ function showHierarchy() { .attr("data-id", d => d.data.i) .attr("stroke", "#333333") .attr("transform", d => `translate(${d.x}, ${d.y})`) - .on("mouseenter", () => cultureHighlightOn(event)) - .on("mouseleave", () => cultureHighlightOff(event)) - .call(d3.drag().on("start", d => dragToReorigin(d))); + .on("mouseenter", cultureHighlightOn) + .on("mouseleave", cultureHighlightOff) + .call(d3.drag().on("start", dragToReorigin)); node .append("path") - .attr("d", d => { - if (!d.data.i) return "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0"; - // small circle - else if (d.data.type === "Generic") return "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0"; - // circle - else if (d.data.type === "River") return "M0,-14L14,0L0,14L-14,0Z"; - // diamond - else if (d.data.type === "Lake") return "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z"; - // hexagon - else if (d.data.type === "Naval") return "M-11,-11h22v22h-22Z"; // square - if (d.data.type === "Highland") return "M-11,-11l11,2l11,-2l-2,11l2,11l-11,-2l-11,2l2,-11Z"; // concave square - if (d.data.type === "Nomadic") return "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 - if (d.data.type === "Hunting") return "M0,-14l14,11l-6,14h-16l-6,-14Z"; // pentagon - return "M-11,-11h22v22h-22Z"; // square - }) - .attr("fill", d => (d.data.i ? d.data.color : "#ffffff")) + .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.i ? d.data.code : "")); + .text(d => d.data.code || ""); } $("#alert").dialog({ title: "Cultures tree", width: fitContent(), + minWidth: "20vw", resizable: false, position: {my: "left center", at: "left+10 center", of: "svg"}, - buttons: {}, + buttons: null, close: () => { alertMessage.innerHTML = ""; } @@ -745,14 +784,17 @@ function showHierarchy() { originLine.remove(); const selected = graph.select("path.selected"); if (!selected.size()) return; - const culture = d.data.i; - const oldOrigin = d.data.origin; - let newOrigin = selected.datum().data.i; - if (newOrigin == oldOrigin) return; // already a child of the selected node - if (newOrigin == culture) newOrigin = 0; // move to top - if (newOrigin && d.descendants().some(node => node.id == newOrigin)) return; // cannot be a child of its own child - pack.cultures[culture].origin = d.data.origin = newOrigin; // change data - showHierarchy(); // update hierarchy + const cultureId = d.data.i; + let newOrigin = Number(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 (newOrigin && d.descendants().some(node => node.id === 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(); }); } @@ -853,7 +895,13 @@ function changeCultureForSelection(selection) { // change of append new element if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color); else - temp.append("polygon").attr("data-cell", i).attr("data-culture", cultureNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color); + temp + .append("polygon") + .attr("data-cell", i) + .attr("data-culture", cultureNew) + .attr("points", getPackPolygon(i)) + .attr("fill", color) + .attr("stroke", color); }); } @@ -921,7 +969,8 @@ function addCulture() { const point = d3.mouse(this); const center = findCell(point[0], point[1]); - if (pack.cells.h[center] < 20) return tip("You cannot place culture center into the water. Please click on a land cell", false, "error"); + if (pack.cells.h[center] < 20) + return tip("You cannot place culture center into the water. Please click on a land cell", false, "error"); const occupied = pack.cultures.some(c => !c.removed && c.center === center); if (occupied) return tip("This cell is already a culture center. Please select a different cell", false, "error"); @@ -989,7 +1038,7 @@ async function uploadCulturesData() { current.color = c.color; current.expansionism = +c.expansionism; - current.origin = +c.origin; + current.origins = JSON.parse(c.origins); if (cultureTypes.includes(c.type)) current.type = c.type; else current.type = "Generic"; diff --git a/versioning.js b/versioning.js index 24ba6288..e9e89f2d 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.85.02"; // generator version, update each time +const version = "1.86.00"; // generator version, update each time { document.title += " v" + version; @@ -28,6 +28,7 @@ const version = "1.85.02"; // generator version, update each time