Merge branch 'master' into master

This commit is contained in:
dranorter 2021-10-11 05:19:24 -04:00 committed by GitHub
commit d0d8015c96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 980 additions and 351 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

View file

@ -31,7 +31,6 @@
#loading-text span:nth-child(2), #mapOverlay > span:nth-child(2) {animation-delay: 1s;}
#loading-text span:nth-child(3), #mapOverlay > span:nth-child(3) {animation-delay: 2s;}
@keyframes blink {0% {opacity: 0;} 20% {opacity: 1;} 100% {opacity: .1;}}
</style>
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="icons.css">
@ -227,7 +226,7 @@
<div id="loading">
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
<div id="version"><t data-t="version">v. </t>1.7</div>
<div id="version"><t data-t="version">v. </t>1.71</div>
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
</div>
@ -431,18 +430,24 @@
<td>
<select id="styleTextureInput">
<option value="none">None</option>
<option value="default" selected>Default</option>
<option value="https://i.imgur.com/EWvXSou.jpg">Folded paper</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/marble-big.jpg">Marble big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/folded-paper-big.jpg">Folded paper big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/folded-paper-small.jpg">Folded paper small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/gray-paper.jpg">Gray paper</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/soiled-paper.jpg">Soiled paper horizontal</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/soiled-paper-e1633784189147.jpg">Soided paper vertical</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/plaster.jpg">Plaster</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/ocean.jpg">Ocean</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/antique-small.jpg">Antique small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/antique-big.jpg">Antique big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/pergamena-small.jpg">Pergamena small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2021/10/marble-big.jpg" selected>Marble big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/marble-small.jpg">Marble small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/marble-blue-small.jpg">Marble Blue</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/marble-blue-big.jpg">Marble Blue big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/stone-small.jpg">Stone small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/stone-big.jpg">Stone big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/timbercut-small.jpg">Timber Cut small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/timbercut-big.jpg">Timber Cut big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/antique-small.jpg">Antique small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/antique-big.jpg">Antique big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/pergamena-small.jpg">Pergamena small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/mars-small.jpg">Mars small</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/mars-big.jpg">Mars big</option>
<option value="https://i2.wp.com/azgaar.files.wordpress.com/2019/07/mercury-small.jpg">Mercury small</option>
@ -1430,7 +1435,7 @@
<p>Join our <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a> and <a href="https://www.reddit.com/r/FantasyMapGenerator/" target="_blank">Reddit community</a> to ask questions, get help and share maps.</p>
<p>The project is under active development. Creator and main maintainer: Azgaar. To track the development progress see the <a href="https://trello.com/b/7x832DG4/fantasy-map-generator" target="_blank">devboard</a>. For older versions see the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">changelog</a>. Please report bugs <a href="https://github.com/Azgaar/Fantasy-Map-Generator/issues" target="_blank">here</a>. You can also contact me directly via <a href="mailto:azgaar.fmg@yandex.by" target="_blank">email</a>.</p>
<div style="background-color: #e85b46; padding: .4em; width: max-content; margin: .6em auto 0 auto; border: 1px solid #943838">
<a href="https://www.patreon.com/azgaar" target="_blank" style="color: white; text-decoration: none; font-family: sans-serif">
<a href="https://www.patreon.com/azgaar" onclick="track('click', 'patreon from about')" target="_blank" style="color: white; text-decoration: none; font-family: sans-serif">
<div>
<div style="width: .8em; display: inline-block; padding: 0 .2em; fill: white">
<svg viewBox="0 0 569 546">
@ -3353,6 +3358,10 @@
<tr>
<th data-tip="Unit icon">Icon</th>
<th data-tip="Unit name. If name is changed for existing unit, old unit will be replaced">Unit name</th>
<th style="width:5em" data-tip="Select allowed biomes">Biomes</th>
<th style="width:5em" data-tip="Select allowed states">States</th>
<th style="width:5em" data-tip="Select allowed cultures">Cultures</th>
<th style="width:5em" data-tip="Select allowed religions">Religions</th>
<th data-tip="Conscription percentage for rural population">Rural</th>
<th data-tip="Conscription percentage for urban population">Urban</th>
<th data-tip="Average number of people in crew (used for total personnel calculation)">Crew</th>
@ -4332,18 +4341,18 @@
<script src="modules/ui/measurers.js"></script>
<script src="libs/umami.js"></script>
<script async defer src="https://unpkg.com/dropbox@10.8.0/dist/Dropbox-sdk.min.js"></script>
<script async defer src="modules/ui/general.js"></script>
<script async defer src="modules/ui/options.js"></script>
<script async defer src="modules/ui/style.js"></script>
<script async defer src="modules/load.js"></script>
<script async defer src="modules/cloud.js"></script>
<script async defer src="main.js"></script>
<script async defer src="modules/save.js"></script>
<script async defer src="modules/export.js"></script>
<script async defer src="modules/relief-icons.js"></script>
<script async defer src="modules/ui/tools.js"></script>
<script async defer src="modules/ui/world-configurator.js"></script>
<script defer src="https://unpkg.com/dropbox@10.8.0/dist/Dropbox-sdk.min.js"></script>
<script defer src="modules/ui/general.js"></script>
<script defer src="modules/ui/options.js"></script>
<script defer src="modules/ui/style.js"></script>
<script defer src="modules/load.js"></script>
<script defer src="modules/cloud.js"></script>
<script defer src="main.js"></script>
<script defer src="modules/save.js"></script>
<script defer src="modules/export.js"></script>
<script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/tools.js"></script>
<script defer src="modules/ui/world-configurator.js"></script>
<script async defer src="modules/ui/heightmap-editor.js"></script>
<script async defer src="modules/ui/states-editor.js"></script>
<script async defer src="modules/ui/provinces-editor.js"></script>

View file

@ -1,5 +1,5 @@
(window => {
const noTrack = window.localStorage.getItem("noTrack");
const noTrack = !location.hostname || window.localStorage.getItem("noTrack");
const {
screen: {width, height},

42
main.js
View file

@ -2,7 +2,7 @@
// https://github.com/Azgaar/Fantasy-Map-Generator
"use strict";
const version = "1.7"; // generator version
const version = "1.71"; // generator version
document.title += " v" + version;
// Switches to disable/enable logging features
@ -111,14 +111,14 @@ legend.on("mousemove", () => tip("Drag to change the position. Click to hide the
// main data variables
let grid = {}; // initial grapg based on jittered square grid and data
let pack = {}; // packed graph and data
let seed,
mapId,
mapHistory = [],
elSelected,
modules = {},
notes = [];
let seed;
let mapId;
let mapHistory = [];
let elSelected;
let modules = {};
let notes = [];
let rulers = new Rulers();
let customization = 0; // 0 - no; 1 = heightmap draw; 2 - states draw; 3 - add state/burg; 4 - cultures draw
let customization = 0;
let biomesData = applyDefaultBiomesSystem();
let nameBases = Names.getNameBases(); // cultures-related data
@ -156,20 +156,24 @@ let urbanization = +document.getElementById("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value;
applyStoredOptions();
let graphWidth = +mapWidthInput.value,
graphHeight = +mapHeightInput.value; // voronoi graph extention, cannot be changed arter generation
let svgWidth = graphWidth,
svgHeight = graphHeight; // svg canvas resolution, can be changed
// voronoi graph extention, cannot be changed arter generation
let graphWidth = +mapWidthInput.value;
let graphHeight = +mapHeightInput.value;
// svg canvas resolution, can be changed
let svgWidth = graphWidth;
let svgHeight = graphHeight;
landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
oceanLayers.append("rect").attr("id", "oceanBase").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
void (function removeLoading() {
// remove loading screen
d3.select("#loading").transition().duration(4000).style("opacity", 0).remove();
d3.select("#initial").transition().duration(4000).attr("opacity", 0).remove();
d3.select("#optionsContainer").transition().duration(3000).style("opacity", 1);
d3.select("#tooltip").transition().duration(4000).style("opacity", 1);
})();
// decide which map should be loaded or generated on page load
void (function checkLoadParameters() {
@ -405,6 +409,7 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>Main changes:
<li>Ability to limit military units by biome, state, culture and religion</li>
<li>New marker types</li>
<li>New markers editor</li>
<li>Markers overview screen</li>
@ -416,7 +421,7 @@ function showWelcomeMessage() {
</ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
<span>Thanks for all supporters on ${patreon}!</i></span>`;
<span>Thanks for all supporters on <a href="https://www.patreon.com/azgaar" onclick="track('click', 'patreon from update')" target="_blank">Patreon</a>!</i></span>`;
$("#alert").dialog({
resizable: false,
@ -951,7 +956,7 @@ function calculateTemperatures() {
const lat = Math.abs(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT); // [0; 90]
const initTemp = tEq - int(lat / 90) * tDelta;
for (let i = r; i < r + grid.cellsX; i++) {
cells.temp[i] = Math.max(Math.min(initTemp - convertToFriendly(cells.h[i]), 127), -128);
cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127);
}
});
@ -1066,7 +1071,7 @@ function generatePrecipitation() {
const precipitation = isPassable ? getPrecipitation(humidity, current, next) : humidity;
cells.prec[current] += precipitation;
const evaporation = precipitation > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
humidity = isPassable ? Math.min(Math.max(humidity - precipitation + evaporation, 0), maxPrec) : 0;
humidity = isPassable ? minmax(humidity - precipitation + evaporation, 0, maxPrec) : 0;
}
}
}
@ -1075,7 +1080,7 @@ function generatePrecipitation() {
const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions
const diff = Math.max(cells.h[i + n] - cells.h[i], 0); // difference in height
const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
return Math.min(Math.max(normalLoss + diff * mod, 1), humidity);
return minmax(normalLoss + diff * mod, 1, humidity);
}
void (function drawWindDirection() {
@ -1854,7 +1859,6 @@ function showStatistics() {
mapId = Date.now(); // unique map id is it's creation date number
mapHistory.push({seed, width: graphWidth, height: graphHeight, template, created: mapId});
INFO && console.log(stats);
track("generate", `Template: ${template} ${templateRandom}. Points: ${pointsInput.dataset.cells}`);
}
const regenerateMap = debounce(function (source) {

View file

@ -416,7 +416,7 @@ window.BurgsAndStates = (function () {
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100); // river penalty from 20 to 100 based on flux
return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {
@ -606,15 +606,15 @@ window.BurgsAndStates = (function () {
if (pathLength < s.name.length) {
// only short name will fit
lines = splitInTwo(s.name);
ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 60), 150), 50);
ratio = minmax(rn((pathLength / lines[0].length) * 60), 50, 150);
} else if (pathLength > s.fullName.length * 2.5) {
// full name will fit in one line
lines = [s.fullName];
ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 70), 170), 70);
ratio = minmax(rn((pathLength / lines[0].length) * 70), 70, 170);
} else {
// try miltilined label
lines = splitInTwo(s.fullName);
ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 60), 150), 70);
ratio = minmax(rn((pathLength / lines[0].length) * 60), 70, 150);
}
// prolongate path if it's too short
@ -665,7 +665,7 @@ window.BurgsAndStates = (function () {
example.text(name);
const left = example.node().getBBox().width / -2; // x offset
el.innerHTML = `<tspan x="${left}px">${name}</tspan>`;
ratio = Math.max(Math.min(rn((pathLength / name.length) * 60), 130), 40);
ratio = minmax(rn((pathLength / name.length) * 60), 40, 130);
el.setAttribute("font-size", ratio + "%");
});
@ -738,21 +738,24 @@ window.BurgsAndStates = (function () {
TIME && console.timeEnd("assignColors");
};
// generate historical conflicts of each state
const generateCampaigns = function () {
const wars = {War: 6, Conflict: 2, Campaign: 4, Invasion: 2, Rebellion: 2, Conquest: 2, Intervention: 1, Expedition: 1, Crusade: 1};
pack.states.forEach(s => {
if (!s.i || s.removed) return;
const n = s.neighbors.length ? s.neighbors : [0];
s.campaigns = n
const generateCampaign = state => {
const neighbors = state.neighbors.length ? state.neighbors : [0];
return neighbors
.map(i => {
const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(s.culture);
const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(state.culture);
const start = gauss(options.year - 100, 150, 1, options.year - 6);
const end = start + gauss(4, 5, 1, options.year - start - 1);
return {name: getAdjective(name) + " " + rw(wars), start, end};
})
.sort((a, b) => a.start - b.start);
};
// generate historical conflicts of each state
const generateCampaigns = function () {
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.campaigns = generateCampaign(s);
});
};
@ -947,7 +950,17 @@ window.BurgsAndStates = (function () {
});
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
const republic = {Republic: 75, Federation: 4, Oligarchy: 2, "Most Serene Republic": 2, Tetrarchy: 1, Triumvirate: 1, Diarchy: 1, "Trade Company": 4, Junta: 1}; // weighted random
const republic = {
Republic: 75,
Federation: 4,
Oligarchy: 2,
"Most Serene Republic": 2,
Tetrarchy: 1,
Triumvirate: 1,
Diarchy: 1,
"Trade Company": 4,
Junta: 1
}; // weighted random
const union = {Union: 3, League: 4, Confederation: 1, "United Kingdom": 1, "United Republic": 1, "United Provinces": 2, Commonwealth: 1, Heptarchy: 1}; // weighted random
const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, "Holy State": 1};
const anarchy = {"Free Territory": 2, Council: 3, Commune: 1, Community: 1};
@ -957,7 +970,8 @@ window.BurgsAndStates = (function () {
const tier = expTiers[s.i];
const religion = pack.cells.religion[s.center];
const isTheocracy = (religion && pack.religions[religion].expansion === "state") || (P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type));
const isTheocracy =
(religion && pack.religions[religion].expansion === "state") || (P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type));
const isAnarchy = P(0.01 - tier / 500);
if (isTheocracy) s.form = "Theocracy";
@ -1025,7 +1039,25 @@ window.BurgsAndStates = (function () {
};
// state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name
const adjForms = ["Empire", "Sultanate", "Khaganate", "Shogunate", "Caliphate", "Despotate", "Theocracy", "Oligarchy", "Union", "Confederation", "Trade Company", "League", "Tetrarchy", "Triumvirate", "Diarchy", "Horde", "Marches"];
const adjForms = [
"Empire",
"Sultanate",
"Khaganate",
"Shogunate",
"Caliphate",
"Despotate",
"Theocracy",
"Oligarchy",
"Union",
"Confederation",
"Trade Company",
"League",
"Tetrarchy",
"Triumvirate",
"Diarchy",
"Horde",
"Marches"
];
const getFullName = function (s) {
if (!s.formName) return s.name;
@ -1223,5 +1255,23 @@ window.BurgsAndStates = (function () {
TIME && console.timeEnd("generateProvinces");
};
return {generate, expandStates, normalizeStates, assignColors, drawBurgs, specifyBurgs, defineBurgFeatures, getType, drawStateLabels, collectStatistics, generateCampaigns, generateDiplomacy, defineStateForms, getFullName, generateProvinces, updateCultures};
return {
generate,
expandStates,
normalizeStates,
assignColors,
drawBurgs,
specifyBurgs,
defineBurgFeatures,
getType,
drawStateLabels,
collectStatistics,
generateCampaign,
generateCampaigns,
generateDiplomacy,
defineStateForms,
getFullName,
generateProvinces,
updateCultures
};
})();

View file

@ -117,7 +117,8 @@ 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))) return "Naval"; // low water cross penalty and high for non-along-coastline growth
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
return "Generic";
@ -435,7 +436,7 @@ window.Cultures = (function () {
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100); // river penalty from 20 to 100 based on flux
return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {

View file

@ -225,8 +225,8 @@ function parseLoadedData(data) {
if (settings[11]) barPosY.value = settings[11];
if (settings[12]) populationRate = populationRateInput.value = populationRateOutput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(settings[14], 100), 1);
if (settings[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(settings[15], 100), 0);
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
if (settings[18]) precInput.value = precOutput.value = settings[18];
@ -234,7 +234,7 @@ function parseLoadedData(data) {
if (settings[20]) mapName.value = settings[20];
if (settings[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22];
if (settings[23]) rescaleLabels.checked = settings[23];
if (settings[23]) rescaleLabels.checked = +settings[23];
})();
void (function parseConfiguration() {
@ -875,16 +875,23 @@ function parseLoadedData(data) {
const defs = document.getElementById("defs-markers");
const markersGroup = document.getElementById("markers");
const markerElements = markersGroup.querySelectorAll("use");
const rescale = +markersGroup.getAttribute("rescale");
pack.markers = Array.from(markerElements).map((el, i) => {
const id = el.getAttribute("id");
const note = notes.find(note => note.id === id);
if (note) note.id = `marker${i}`;
const x = rn(+el.dataset.x, 1);
const y = rn(+el.dataset.y, 1);
let x = +el.dataset.x;
let y = +el.dataset.y;
const transform = el.getAttribute("transform");
if (transform) {
const [dx, dy] = parseTransform(transform);
if (dx) x += +dx;
if (dy) y += +dy;
}
const cell = findCell(x, y);
const size = rn(el.dataset.size * 30, 1);
const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1);
const href = el.href.baseVal;
const type = href.replace("#marker_", "");
@ -906,6 +913,7 @@ function parseLoadedData(data) {
if (!isNaN(dy) && dy !== 50) marker.dy = dy;
if (fill && fill !== "#ffffff") marker.fill = fill;
if (stroke && stroke !== "#000000") marker.stroke = stroke;
if (circle.getAttribute("opacity") === "0") marker.pin = "no";
return marker;
});

View file

@ -46,9 +46,11 @@ window.Markers = (function () {
};
const regenerate = () => {
pack.markers = pack.markers.filter(({i, lock}) => {
if (lock) return true;
pack.markers = pack.markers.filter(({i, lock, cell}) => {
if (lock) {
occupied[cell] = true;
return true;
}
const id = `marker${i}`;
document.getElementById(id)?.remove();
const index = notes.findIndex(note => note.id === id);
@ -110,12 +112,11 @@ window.Markers = (function () {
const {cells} = pack;
let mountains = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] >= 70).sort((a, b) => cells.h[b] - cells.h[a]));
let quantity = getQuantity(mountains, 10, 300, multiplier);
let quantity = getQuantity(mountains, 10, 500, multiplier);
if (!quantity) return;
const highestMountains = mountains.slice(0, 20);
while (quantity) {
const [cell] = extractAnyElement(highestMountains);
const [cell] = extractAnyElement(mountains);
const id = addMarker({cell, icon, type, dx: 52, px: 13});
const proper = Names.getCulture(cells.culture[cell]);
const name = P(0.3) ? "Mount " + proper : Math.random() > 0.3 ? proper + " Volcano" : proper;
@ -128,12 +129,11 @@ window.Markers = (function () {
const {cells} = pack;
let springs = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] > 50).sort((a, b) => cells.h[b] - cells.h[a]));
let quantity = getQuantity(springs, 30, 800, multiplier);
let quantity = getQuantity(springs, 30, 1200, multiplier);
if (!quantity) return;
const highestSprings = springs.slice(0, 40);
while (quantity) {
const [cell] = extractAnyElement(highestSprings);
const [cell] = extractAnyElement(springs);
const id = addMarker({cell, icon, type, dy: 52});
const proper = Names.getCulture(cells.culture[cell]);
const temp = convertTemperature(gauss(35, 15, 20, 100));
@ -181,7 +181,16 @@ window.Markers = (function () {
const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
const riverName = river ? `${river.name} ${river.type}` : "river";
const name = river && P(0.2) ? river.name : burg.name;
notes.push({id, name: `${name} Bridge`, legend: `A stone bridge over the ${riverName} near ${burg.name}`});
const weightedAdjectives = {
stone: 10,
wooden: 1,
lengthy: 2,
formidable: 2,
rickety: 1,
beaten: 1,
weathered: 1
};
notes.push({id, name: `${name} Bridge`, legend: `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}`});
quantity--;
}
}
@ -448,7 +457,7 @@ window.Markers = (function () {
const [cell] = extractAnyElement(lighthouses);
const id = addMarker({cell, icon, type, px: 14});
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend: `A lighthouse to keep the navigation safe`});
notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend: `A lighthouse to serve as a beacon for ships in the open sea`});
quantity--;
}
}
@ -460,11 +469,19 @@ window.Markers = (function () {
const quantity = getQuantity(waterfalls, 1, 5, multiplier);
if (!quantity) return;
const descriptions = [
"A gorgeous waterfall flows here",
"The rapids of an exceptionally beautiful waterfall",
"An impressive waterfall has cut through the land",
"The cascades of a stunning waterfall",
"A river drops down from a great height forming a wonderous waterfall",
"A breathtaking waterfall cuts through the landscape"
];
for (let i = 0; i < waterfalls.length && i < quantity; i++) {
const cell = waterfalls[i];
const id = addMarker({cell, icon, type, dy: 54, px: 16});
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `An extremely beautiful waterfall`});
notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `${ra(descriptions)}`});
}
}
@ -478,7 +495,9 @@ window.Markers = (function () {
while (quantity && battlefields.length) {
const [cell] = extractAnyElement(battlefields);
const id = addMarker({cell, icon, type, dy: 52});
const campaign = ra(states[cells.state[cell]].campaigns);
const state = states[cells.state[cell]];
if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
const campaign = ra(state.campaigns);
const date = generateDate(campaign.start, campaign.end);
const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`;
@ -519,7 +538,7 @@ window.Markers = (function () {
const id = addMarker({cell, icon, type, dy: 48});
const name = `${lake.name} Monster`;
const length = gauss(10, 5, 5, 100);
const legend = `Rumors said a relic monster of ${length} ${heightUnit.value} long inhabits ${lake.name} Lake. Truth or lie, but folks are afraid to fish in the lake`;
const legend = `Rumors say a relic monster of ${length} ${heightUnit.value} long inhabits ${lake.name} Lake. Truth or lie, folks are afraid to fish in the lake`;
notes.push({id, name, legend});
quantity--;
}
@ -550,15 +569,54 @@ window.Markers = (function () {
let quantity = getQuantity(hills, 30, 600, multiplier);
if (!quantity) return;
const subjects = ["Locals", "Old folks", "Old books", "Tipplers"];
const species = ["Ogre", "Troll", "Cyclops", "Giant", "Monster", "Beast", "Dragon", "Undead", "Ghoul", "Vampire"];
const adjectives = [
"great",
"big",
"huge",
"prime",
"golden",
"proud",
"lucky",
"fat",
"giant",
"hungry",
"magical",
"superior",
"terrifying",
"horrifying",
"feared"
];
const subjects = ["Locals", "Elders", "Inscriptions", "Tipplers", "Legends", "Whispers", "Rumors", "Journeying folk", "Tales"];
const species = [
"Ogre",
"Troll",
"Cyclops",
"Giant",
"Monster",
"Beast",
"Dragon",
"Undead",
"Ghoul",
"Vampire",
"Hag",
"Banshee",
"Bearded Devil",
"Roc",
"Hydra",
"Warg"
];
const modusOperandi = [
"steals their cattle",
"doesn't mind eating children",
"steals cattle at night",
"prefers eating children",
"doesn't mind of human flesh",
"keeps the region at bay",
"eats their kids",
"abducts young women"
"eats kids whole",
"abducts young women",
"terrorizes the region",
"harasses travelers in the area",
"snatches people from homes",
"attacks anyone who dares to approach its lair",
"attacks unsuspecting victims"
];
while (quantity) {
@ -567,7 +625,7 @@ window.Markers = (function () {
const monster = ra(species);
const toponym = Names.getCulture(cells.culture[cell]);
const name = `${toponym} ${monster}`;
const legend = `${ra(subjects)} tell tales of an old ${monster} who inhabits ${toponym} hills and ${ra(modusOperandi)}`;
const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(modusOperandi)}`;
notes.push({id, name, legend});
quantity--;
}
@ -737,7 +795,7 @@ window.Markers = (function () {
let quantity = getQuantity(statues, 80, 1200, multiplier);
if (!quantity) return;
const variants = ["Statue", "Obelisk", "Monument", "Column", "Monolith", "Pillar", "Megalith", "Stele", "Runestone"];
const variants = ["Statue", "Obelisk", "Monument", "Column", "Monolith", "Pillar", "Megalith", "Stele", "Runestone", "Sculpture", "Effigy", "Idol"];
const scripts = {
cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
@ -771,7 +829,21 @@ window.Markers = (function () {
let quantity = getQuantity(ruins, 80, 1200, multiplier);
if (!quantity) return;
const types = ["City", "Town", "Pyramid", "Fort"];
const types = [
"City",
"Town",
"Settlement",
"Pyramid",
"Fort",
"Stronghold",
"Temple",
"Sacred site",
"Mausoleum",
"Outpost",
"Fortification",
"Fortress",
"Castle"
];
while (quantity) {
const [cell] = extractAnyElement(ruins);
@ -779,7 +851,7 @@ window.Markers = (function () {
const ruinType = ra(types);
const name = `Ruined ${ruinType}`;
const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. A good place for a treasures hunt`;
const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within.`;
notes.push({id, name, legend});
quantity--;
}

View file

@ -3,9 +3,8 @@
window.Military = (function () {
const generate = function () {
TIME && console.time("generateMilitaryForces");
const cells = pack.cells,
p = cells.p,
states = pack.states;
const {cells, states} = pack;
const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states
if (!options.military) options.military = getDefaultOptions();
@ -19,7 +18,6 @@ window.Military = (function () {
mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
// non-default generic:
armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
@ -38,27 +36,24 @@ window.Military = (function () {
};
valid.forEach(s => {
const temp = (s.temp = {}),
d = s.diplomacy;
const expansionRate = Math.min(Math.max(s.expansionism / expn / (s.area / area), 0.25), 4); // how much state expansionism is realized
s.temp = {};
const d = s.diplomacy;
const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? 0.8 : d.some(d => d === "Suspicion") ? 0.5 : 0.1; // peacefulness
const neighborsRate = Math.min(
Math.max(
s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5),
0.3
),
3
); // neighbors rate
s.alert = Math.min(Math.max(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1), 5); // war alert rate (army modifier)
temp.platoons = [];
const neighborsRateRaw = s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5);
const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
s.temp.platoons = [];
// apply overall state modifiers for unit types based on state features
for (const unit of options.military) {
if (!stateModifier[unit.type]) continue;
let modifier = stateModifier[unit.type][s.type] || 1;
if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
temp[unit.name] = modifier * s.alert;
s.temp[unit.name] = modifier * s.alert;
}
});
@ -69,66 +64,91 @@ window.Military = (function () {
return "generic";
};
function passUnitLimits(unit, biome, state, culture, religion) {
if (unit.biomes && !unit.biomes.includes(biome)) return false;
if (unit.states && !unit.states.includes(state)) return false;
if (unit.cultures && !unit.cultures.includes(culture)) return false;
if (unit.religions && !unit.religions.includes(religion)) return false;
return true;
}
for (const i of cells.i) {
if (!cells.pop[i]) continue;
const s = states[cells.state[i]]; // cell state
if (!s.i || s.removed) continue;
let m = cells.pop[i] / 100; // basic rural army in percentages
if (cells.culture[i] !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (cells.religion[i] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[i] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
const biome = cells.biome[i];
const state = cells.state[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const stateObj = states[state];
if (!state || stateObj.removed) continue;
let modifier = cells.pop[i] / 100; // basic rural army in percentages
if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
if (religion !== cells.religion[stateObj.center]) modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
if (cells.f[i] !== cells.f[stateObj.center]) modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
const type = getType(i);
for (const u of options.military) {
const perc = +u.rural;
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
for (const unit of options.military) {
const perc = +unit.rural;
if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
const mod = type === "generic" ? 1 : cellTypeModifier[type][u.type]; // cell specific modifier
const army = m * perc * mod; // rural cell army
const t = rn(army * s.temp[u.name] * populationRate); // total troops
if (!t) continue;
let x = p[i][0],
y = p[i][1],
n = 0;
if (u.type === "naval") {
const cellTypeMod = type === "generic" ? 1 : cellTypeModifier[type][unit.type]; // cell specific modifier
const army = modifier * perc * cellTypeMod; // rural cell army
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
if (!total) continue;
let [x, y] = p[i];
let n = 0;
if (unit.type === "naval") {
let haven = cells.haven[i];
(x = p[haven][0]), (y = p[haven][1]);
[x, y] = p[haven];
n = 1;
} // place naval to sea
s.temp.platoons.push({cell: i, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
stateObj.temp.platoons.push({cell: i, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type});
}
}
for (const b of pack.burgs) {
if (!b.i || b.removed || !b.state || !b.population) continue;
const s = states[b.state]; // burg state
const biome = cells.biome[b.cell];
const state = b.state;
const culture = b.culture;
const religion = cells.religion[b.cell];
const stateObj = states[state];
let m = (b.population * urbanization) / 100; // basic urban army in percentages
if (b.capital) m *= 1.2; // capital has household troops
if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[b.cell] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
if (culture !== stateObj.culture) m = stateObj.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (religion !== cells.religion[stateObj.center]) m = stateObj.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[b.cell] !== cells.f[stateObj.center]) m = stateObj.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
const type = getType(b.cell);
for (const u of options.military) {
if (u.type === "naval" && !b.port) continue; // only ports produce naval units
const perc = +u.urban;
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
for (const unit of options.military) {
if (unit.type === "naval" && !b.port) continue; // only ports produce naval units
const perc = +unit.urban;
if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
const mod = type === "generic" ? 1 : burgTypeModifier[type][u.type]; // cell specific modifier
const mod = type === "generic" ? 1 : burgTypeModifier[type][unit.type]; // cell specific modifier
const army = m * perc * mod; // urban cell army
const t = rn(army * s.temp[u.name] * populationRate); // total troops
if (!t) continue;
let x = p[b.cell][0],
y = p[b.cell][1],
n = 0;
if (u.type === "naval") {
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
if (!total) continue;
let [x, y] = p[b.cell];
let n = 0;
if (unit.type === "naval") {
let haven = cells.haven[b.cell];
(x = p[haven][0]), (y = p[haven][1]);
[x, y] = p[haven];
n = 1;
} // place naval in sea cell
s.temp.platoons.push({cell: b.cell, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
} // place naval to sea
stateObj.temp.platoons.push({cell: b.cell, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type});
}
}
@ -334,7 +354,13 @@ window.Military = (function () {
const getName = function (r, regiments) {
const cells = pack.cells;
const proper = r.n ? null : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].name : cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : null;
const proper = r.n
? null
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
? pack.provinces[cells.province[r.cell]].name
: cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
? pack.burgs[cells.burg[r.cell]].name
: null;
const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
const form = r.n ? "Fleet" : "Regiment";
return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
@ -351,7 +377,12 @@ window.Military = (function () {
const generateNote = function (r, s) {
const cells = pack.cells;
const base = cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].fullName : null;
const base =
cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
? pack.burgs[cells.burg[r.cell]].name
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
? pack.provinces[cells.province[r.cell]].fullName
: null;
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
const composition = r.a

View file

@ -52,7 +52,7 @@ window.ReliefIcons = (function () {
function getReliefIcon(i, h) {
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
const size = h > 70 ? (h - 45) * mod : Math.min(Math.max((h - 40) * mod, 3), 6);
const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
return [getIcon(type), size];
}
}

View file

@ -2,7 +2,22 @@
window.Religions = (function () {
// name generation approach and relative chance to be selected
const approach = {Number: 1, Being: 3, Adjective: 5, "Color + Animal": 5, "Adjective + Animal": 5, "Adjective + Being": 5, "Adjective + Genitive": 1, "Color + Being": 3, "Color + Genitive": 3, "Being + of + Genitive": 2, "Being + of the + Genitive": 1, "Animal + of + Genitive": 1, "Adjective + Being + of + Genitive": 2, "Adjective + Animal + of + Genitive": 2};
const approach = {
Number: 1,
Being: 3,
Adjective: 5,
"Color + Animal": 5,
"Adjective + Animal": 5,
"Adjective + Being": 5,
"Adjective + Genitive": 1,
"Color + Being": 3,
"Color + Genitive": 3,
"Being + of + Genitive": 2,
"Being + of the + Genitive": 1,
"Animal + of + Genitive": 1,
"Adjective + Being + of + Genitive": 2,
"Adjective + Animal + of + Genitive": 2
};
// turn weighted array into simple array
const approaches = [];
@ -14,11 +29,254 @@ window.Religions = (function () {
const base = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: ["God", "Goddess", "Lord", "Lady", "Deity", "Creator", "Maker", "Overlord", "Ruler", "Chief", "Master", "Spirit", "Ancestor", "Father", "Forebear", "Forefather", "Mother", "Brother", "Sister", "Elder", "Numen", "Ancient", "Virgin", "Giver", "Council", "Guardian", "Reaper"],
animal: ["Dragon", "Wyvern", "Phoenix", "Unicorn", "Sphinx", "Centaur", "Pegasus", "Kraken", "Basilisk", "Chimera", "Cyclope", "Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Cobra", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Viper", "Vulture", "Walrus", "Wolf", "Wolverine", "Worm", "Camel", "Falcon", "Hound", "Ox", "Serpent"],
adjective: ["New", "Good", "High", "Old", "Great", "Big", "Young", "Major", "Strong", "Happy", "Last", "Main", "Huge", "Far", "Beautiful", "Wild", "Fair", "Prime", "Crazy", "Ancient", "Proud", "Secret", "Lucky", "Sad", "Silent", "Latter", "Severe", "Fat", "Holy", "Pure", "Aggressive", "Honest", "Giant", "Mad", "Pregnant", "Distant", "Lost", "Broken", "Blind", "Friendly", "Unknown", "Sleeping", "Slumbering", "Loud", "Hungry", "Wise", "Worried", "Sacred", "Magical", "Superior", "Patient", "Dead", "Deadly", "Peaceful", "Grateful", "Frozen", "Evil", "Scary", "Burning", "Divine", "Bloody", "Dying", "Waking", "Brutal", "Unhappy", "Calm", "Cruel", "Favorable", "Blond", "Explicit", "Disturbing", "Devastating", "Brave", "Sunny", "Troubled", "Flying", "Sustainable", "Marine", "Fatal", "Inherent", "Selected", "Naval", "Cheerful", "Almighty", "Benevolent", "Eternal", "Immutable", "Infallible"],
genitive: ["Day", "Life", "Death", "Night", "Home", "Fog", "Snow", "Winter", "Summer", "Cold", "Springs", "Gates", "Nature", "Thunder", "Lightning", "War", "Ice", "Frost", "Fire", "Doom", "Fate", "Pain", "Heaven", "Justice", "Light", "Love", "Time", "Victory"],
theGenitive: ["World", "Word", "South", "West", "North", "East", "Sun", "Moon", "Peak", "Fall", "Dawn", "Eclipse", "Abyss", "Blood", "Tree", "Earth", "Harvest", "Rainbow", "Sea", "Sky", "Stars", "Storm", "Underworld", "Wild"],
being: [
"God",
"Goddess",
"Lord",
"Lady",
"Deity",
"Creator",
"Maker",
"Overlord",
"Ruler",
"Chief",
"Master",
"Spirit",
"Ancestor",
"Father",
"Forebear",
"Forefather",
"Mother",
"Brother",
"Sister",
"Elder",
"Numen",
"Ancient",
"Virgin",
"Giver",
"Council",
"Guardian",
"Reaper"
],
animal: [
"Dragon",
"Wyvern",
"Phoenix",
"Unicorn",
"Sphinx",
"Centaur",
"Pegasus",
"Kraken",
"Basilisk",
"Chimera",
"Cyclope",
"Antelope",
"Ape",
"Badger",
"Bear",
"Beaver",
"Bison",
"Boar",
"Buffalo",
"Cat",
"Cobra",
"Crane",
"Crocodile",
"Crow",
"Deer",
"Dog",
"Eagle",
"Elk",
"Fox",
"Goat",
"Goose",
"Hare",
"Hawk",
"Heron",
"Horse",
"Hyena",
"Ibis",
"Jackal",
"Jaguar",
"Lark",
"Leopard",
"Lion",
"Mantis",
"Marten",
"Moose",
"Mule",
"Narwhal",
"Owl",
"Panther",
"Rat",
"Raven",
"Rook",
"Scorpion",
"Shark",
"Sheep",
"Snake",
"Spider",
"Swan",
"Tiger",
"Turtle",
"Viper",
"Vulture",
"Walrus",
"Wolf",
"Wolverine",
"Worm",
"Camel",
"Falcon",
"Hound",
"Ox",
"Serpent"
],
adjective: [
"New",
"Good",
"High",
"Old",
"Great",
"Big",
"Young",
"Major",
"Strong",
"Happy",
"Last",
"Main",
"Huge",
"Far",
"Beautiful",
"Wild",
"Fair",
"Prime",
"Crazy",
"Ancient",
"Proud",
"Secret",
"Lucky",
"Sad",
"Silent",
"Latter",
"Severe",
"Fat",
"Holy",
"Pure",
"Aggressive",
"Honest",
"Giant",
"Mad",
"Pregnant",
"Distant",
"Lost",
"Broken",
"Blind",
"Friendly",
"Unknown",
"Sleeping",
"Slumbering",
"Loud",
"Hungry",
"Wise",
"Worried",
"Sacred",
"Magical",
"Superior",
"Patient",
"Dead",
"Deadly",
"Peaceful",
"Grateful",
"Frozen",
"Evil",
"Scary",
"Burning",
"Divine",
"Bloody",
"Dying",
"Waking",
"Brutal",
"Unhappy",
"Calm",
"Cruel",
"Favorable",
"Blond",
"Explicit",
"Disturbing",
"Devastating",
"Brave",
"Sunny",
"Troubled",
"Flying",
"Sustainable",
"Marine",
"Fatal",
"Inherent",
"Selected",
"Naval",
"Cheerful",
"Almighty",
"Benevolent",
"Eternal",
"Immutable",
"Infallible"
],
genitive: [
"Day",
"Life",
"Death",
"Night",
"Home",
"Fog",
"Snow",
"Winter",
"Summer",
"Cold",
"Springs",
"Gates",
"Nature",
"Thunder",
"Lightning",
"War",
"Ice",
"Frost",
"Fire",
"Doom",
"Fate",
"Pain",
"Heaven",
"Justice",
"Light",
"Love",
"Time",
"Victory"
],
theGenitive: [
"World",
"Word",
"South",
"West",
"North",
"East",
"Sun",
"Moon",
"Peak",
"Fall",
"Dawn",
"Eclipse",
"Abyss",
"Blood",
"Tree",
"Earth",
"Harvest",
"Rainbow",
"Sea",
"Sky",
"Stars",
"Storm",
"Underworld",
"Wild"
],
color: ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]
};
@ -29,7 +287,16 @@ window.Religions = (function () {
Heresy: {Heresy: 1}
};
const methods = {"Random + type": 3, "Random + ism": 1, "Supreme + ism": 5, "Faith of + Supreme": 5, "Place + ism": 1, "Culture + ism": 2, "Place + ian + type": 6, "Culture + type": 4};
const methods = {
"Random + type": 3,
"Random + ism": 1,
"Supreme + ism": 5,
"Faith of + Supreme": 5,
"Place + ism": 1,
"Culture + ism": 2,
"Place + ian + type": 6,
"Culture + type": 4
};
const types = {
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
@ -78,7 +345,10 @@ window.Religions = (function () {
}
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const sorted = burgs.length > +religionsInput.value ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell) : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const sorted =
burgs.length > +religionsInput.value
? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
: cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
@ -160,9 +430,20 @@ window.Religions = (function () {
const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
religions.push({i: religions.length, name, color, culture, type: "Heresy", form: r.form, deity: r.deity, expansion: "global", expansionism, center, origin: r.i});
religions.push({
i: religions.length,
name,
color,
culture,
type: "Heresy",
form: r.form,
deity: r.deity,
expansion: "global",
expansionism,
center,
origin: r.i
});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
@ -195,7 +476,24 @@ window.Religions = (function () {
name,
religions.map(r => r.code)
);
religions.push({i, name, color, culture, type, form: formName, deity, expansion, expansionism: 0, center, cells: 0, area: 0, rural: 0, urban: 0, origin: r, code});
religions.push({
i,
name,
color,
culture,
type,
form: formName,
deity,
expansion,
expansionism: 0,
center,
cells: 0,
area: 0,
rural: 0,
urban: 0,
origin: r,
code
});
cells.religion[center] = i;
};
@ -292,20 +590,18 @@ window.Religions = (function () {
};
function checkCenters() {
const cells = pack.cells,
religions = pack.religions;
const {cells, religions} = pack;
const codes = religions.map(r => r.code);
religions
.filter(r => r.i)
.forEach(r => {
religions.forEach(r => {
if (!r.i) return;
r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter(i => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => b.pop - a.pop)[0];
r.center = religCells.sort((a, b) => cells.pop[b] - cells.pop[a])[0];
});
}

View file

@ -1,20 +1,43 @@
"use strict";
window.ThreeD = (function () {
// set default options
const options = {scale: 50, lightness: 0.7, shadow: 0.5, sun: {x: 100, y: 600, z: 1000}, rotateMesh: 0, rotateGlobe: 0.5, skyColor: "#9ecef5", waterColor: "#466eab", extendedWater: 0, labels3d: 0, resolution: 2};
const options = {
scale: 50,
lightness: 0.7,
shadow: 0.5,
sun: {x: 100, y: 600, z: 1000},
rotateMesh: 0,
rotateGlobe: 0.5,
skyColor: "#9ecef5",
waterColor: "#466eab",
extendedWater: 0,
labels3d: 0,
resolution: 2
};
// set variables
let Renderer, scene, camera, controls, animationFrame, material, texture, geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh, raycaster;
const drawCtx = document.createElement("canvas").getContext("2d");
const drawSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
document.body.appendChild(drawSVG);
let Renderer,
scene,
camera,
controls,
animationFrame,
material,
texture,
geometry,
mesh,
ambientLight,
spotLight,
waterPlane,
waterMaterial,
waterMesh,
raycaster;
let labels = [];
let icons = [];
let lines = [];
const context2d = document.createElement("canvas").getContext("2d");
// initiate 3d scene
const create = async function (canvas, type = "viewMesh") {
options.isOn = true;
@ -210,16 +233,16 @@ window.ThreeD = (function () {
}
async function createTextLabel({text, font, size, color, quality}) {
drawCtx.font = `${size * quality}px ${font}`;
drawCtx.canvas.width = drawCtx.measureText(text).width;
drawCtx.canvas.height = size * quality * 1.25; // 25% margin as text can overflow the font size
drawCtx.clearRect(0, 0, drawCtx.canvas.width, drawCtx.canvas.height);
context2d.font = `${size * quality}px ${font}`;
context2d.canvas.width = context2d.measureText(text).width;
context2d.canvas.height = size * quality * 1.25; // 25% margin as text can overflow the font size
context2d.clearRect(0, 0, context2d.canvas.width, context2d.canvas.height);
drawCtx.font = `${size * quality}px ${font}`;
drawCtx.fillStyle = color;
drawCtx.fillText(text, 0, size * quality);
context2d.font = `${size * quality}px ${font}`;
context2d.fillStyle = color;
context2d.fillText(text, 0, size * quality);
return textureToSprite(drawCtx.canvas.toDataURL(), drawCtx.canvas.width / quality, drawCtx.canvas.height / quality);
return textureToSprite(context2d.canvas.toDataURL(), context2d.canvas.width / quality, context2d.canvas.height / quality);
}
function get3dCoords(baseX, baseY) {
@ -578,5 +601,21 @@ window.ThreeD = (function () {
});
}
return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleLabels, toggleSky, setResolution, setColors, saveScreenshot, saveOBJ};
return {
create,
redraw,
update,
stop,
options,
setScale,
setLightness,
setSun,
setRotation,
toggleLabels,
toggleSky,
setResolution,
setColors,
saveScreenshot,
saveOBJ
};
})();

View file

@ -330,7 +330,7 @@ class Battle {
}
getInitialMorale() {
const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
const powerFee = diff => minmax(100 - diff ** 1.5 * 10 + 10, 50, 100);
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
const powerDiff = this.defenders.power / this.attackers.power;
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);

View file

@ -621,7 +621,7 @@ function editHeightmap() {
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {
return Math.max(Math.min(v, 100), land ? 20 : 0);
return minmax(v, land ? 20 : 0, 100);
}
const h = grid.cells.h;

View file

@ -4,10 +4,10 @@ document.addEventListener("keydown", handleKeydown);
document.addEventListener("keyup", handleKeyup);
function handleKeydown(event) {
const {key, code, ctrlKey, altKey} = event;
const {code, ctrlKey, altKey} = event;
if (altKey && !ctrlKey) event.preventDefault(); // disallow alt key combinations
if (ctrlKey && ["KeyS", "KeyC"].includes(code)) event.preventDefault(); // disallow CTRL + S and CTRL + C
if (["F1", "F2", "F6", "F9", "Tab"].includes(key)) event.preventDefault(); // disallow default Fn and Tab
if (["F1", "F2", "F6", "F9", "Tab"].includes(code)) event.preventDefault(); // disallow default Fn and Tab
}
function handleKeyup(event) {
@ -19,83 +19,82 @@ function handleKeyup(event) {
if (document.getSelection().toString()) return; // don't trigger if user selects text
event.stopPropagation();
const {ctrlKey, metaKey, shiftKey, altKey} = event;
const key = event.key.toUpperCase();
const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
const ctrl = ctrlKey || metaKey || key === "Control";
const shift = shiftKey || key === "Shift";
const alt = altKey || key === "Alt";
if (key === "F1") showInfo();
else if (key === "F2") regeneratePrompt("hotkey");
else if (key === "F6") quickSave();
else if (key === "F9") quickLoad();
else if (key === "TAB") toggleOptions(event);
else if (key === "ESCAPE") closeAllDialogs();
else if (key === "DELETE") removeElementOnKey();
else if (key === "O" && document.getElementById("canvas3d")) toggle3dOptions();
else if (ctrl && key === "Q") toggleSaveReminder();
else if (ctrl && key === "S") dowloadMap();
else if (ctrl && key === "C") saveToDropbox();
else if (ctrl && key === "Z" && undo.offsetParent) undo.click();
else if (ctrl && key === "Y" && redo.offsetParent) redo.click();
else if (shift && key === "H") editHeightmap();
else if (shift && key === "B") editBiomes();
else if (shift && key === "S") editStates();
else if (shift && key === "P") editProvinces();
else if (shift && key === "D") editDiplomacy();
else if (shift && key === "C") editCultures();
else if (shift && key === "N") editNamesbase();
else if (shift && key === "Z") editZones();
else if (shift && key === "R") editReligions();
else if (shift && key === "Y") openEmblemEditor();
else if (shift && key === "Q") editUnits();
else if (shift && key === "O") editNotes();
else if (shift && key === "T") overviewBurgs();
else if (shift && key === "V") overviewRivers();
else if (shift && key === "M") overviewMilitary();
else if (shift && key === "K") overviewMarkers();
else if (shift && key === "E") viewCellDetails();
else if (shift && key === "1") toggleAddBurg();
else if (shift && key === "2") toggleAddLabel();
else if (shift && key === "3") toggleAddRiver();
else if (shift && key === "4") toggleAddRoute();
else if (shift && key === "5") toggleAddMarker();
else if (alt && key === "B") console.table(pack.burgs);
else if (alt && key === "S") console.table(pack.states);
else if (alt && key === "C") console.table(pack.cultures);
else if (alt && key === "R") console.table(pack.religions);
else if (alt && key === "F") console.table(pack.features);
else if (key === "X") toggleTexture();
else if (key === "H") toggleHeight();
else if (key === "B") toggleBiomes();
else if (key === "E") toggleCells();
else if (key === "G") toggleGrid();
else if (key === "O") toggleCoordinates();
else if (key === "W") toggleCompass();
else if (key === "V") toggleRivers();
else if (key === "F") toggleRelief();
else if (key === "C") toggleCultures();
else if (key === "S") toggleStates();
else if (key === "P") toggleProvinces();
else if (key === "Z") toggleZones();
else if (key === "D") toggleBorders();
else if (key === "R") toggleReligions();
else if (key === "U") toggleRoutes();
else if (key === "T") toggleTemp();
else if (key === "N") togglePopulation();
else if (key === "J") toggleIce();
else if (key === "A") togglePrec();
else if (key === "Y") toggleEmblems();
else if (key === "L") toggleLabels();
else if (key === "I") toggleIcons();
else if (key === "M") toggleMilitary();
else if (key === "K") toggleMarkers();
else if (key === "=") toggleRulers();
else if (key === "/") toggleScaleBar();
else if (key === "ARROWLEFT") zoom.translateBy(svg, 10, 0);
else if (key === "ARROWRIGHT") zoom.translateBy(svg, -10, 0);
else if (key === "ARROWUP") zoom.translateBy(svg, 0, 10);
else if (key === "ARROWDOWN") zoom.translateBy(svg, 0, -10);
if (code === "F1") showInfo();
else if (code === "F2") regeneratePrompt("hotkey");
else if (code === "F6") quickSave();
else if (code === "F9") quickLoad();
else if (code === "Tab") toggleOptions(event);
else if (code === "Escape") closeAllDialogs();
else if (code === "Delete") removeElementOnKey();
else if (code === "KeyO" && document.getElementById("canvas3d")) toggle3dOptions();
else if (ctrl && code === "KeyQ") toggleSaveReminder();
else if (ctrl && code === "KeyS") dowloadMap();
else if (ctrl && code === "KeyC") saveToDropbox();
else if (ctrl && code === "KeyZ" && undo.offsetParent) undo.click();
else if (ctrl && code === "KeyY" && redo.offsetParent) redo.click();
else if (shift && code === "KeyH") editHeightmap();
else if (shift && code === "KeyB") editBiomes();
else if (shift && code === "KeyS") editStates();
else if (shift && code === "KeyP") editProvinces();
else if (shift && code === "KeyD") editDiplomacy();
else if (shift && code === "KeyC") editCultures();
else if (shift && code === "KeyN") editNamesbase();
else if (shift && code === "KeyZ") editZones();
else if (shift && code === "KeyR") editReligions();
else if (shift && code === "KeyY") openEmblemEditor();
else if (shift && code === "KeyQ") editUnits();
else if (shift && code === "KeyO") editNotes();
else if (shift && code === "KeyT") overviewBurgs();
else if (shift && code === "KeyV") overviewRivers();
else if (shift && code === "KeyM") overviewMilitary();
else if (shift && code === "KeyK") overviewMarkers();
else if (shift && code === "KeyE") viewCellDetails();
else if (key === "!") toggleAddBurg();
else if (key === "@") toggleAddLabel();
else if (key === "#") toggleAddRiver();
else if (key === "$") toggleAddRoute();
else if (key === "%") toggleAddMarker();
else if (alt && code === "KeyB") console.table(pack.burgs);
else if (alt && code === "KeyS") console.table(pack.states);
else if (alt && code === "KeyC") console.table(pack.cultures);
else if (alt && code === "KeyR") console.table(pack.religions);
else if (alt && code === "KeyF") console.table(pack.features);
else if (code === "KeyX") toggleTexture();
else if (code === "KeyH") toggleHeight();
else if (code === "KeyB") toggleBiomes();
else if (code === "KeyE") toggleCells();
else if (code === "KeyG") toggleGrid();
else if (code === "KeyO") toggleCoordinates();
else if (code === "KeyW") toggleCompass();
else if (code === "KeyV") toggleRivers();
else if (code === "KeyF") toggleRelief();
else if (code === "KeyC") toggleCultures();
else if (code === "KeyS") toggleStates();
else if (code === "KeyP") toggleProvinces();
else if (code === "KeyZ") toggleZones();
else if (code === "KeyD") toggleBorders();
else if (code === "KeyR") toggleReligions();
else if (code === "KeyU") toggleRoutes();
else if (code === "KeyT") toggleTemp();
else if (code === "KeyN") togglePopulation();
else if (code === "KeyJ") toggleIce();
else if (code === "KeyA") togglePrec();
else if (code === "KeyY") toggleEmblems();
else if (code === "KeyL") toggleLabels();
else if (code === "KeyI") toggleIcons();
else if (code === "KeyM") toggleMilitary();
else if (code === "KeyK") toggleMarkers();
else if (code === "Equal") toggleRulers();
else if (code === "Slash") toggleScaleBar();
else if (code === "ArrowLeft") zoom.translateBy(svg, 10, 0);
else if (code === "ArrowRight") zoom.translateBy(svg, -10, 0);
else if (code === "ArrowUp") zoom.translateBy(svg, 0, 10);
else if (code === "ArrowDown") zoom.translateBy(svg, 0, -10);
else if (key === "+" || key === "-") pressNumpadSign(key);
else if (key === "0") resetZoom(1000);
else if (key === "1") zoom.scaleTo(svg, 1);
@ -123,7 +122,7 @@ function pressNumpadSign(key) {
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
if (brush) {
const value = Math.max(Math.min(+brush.value + change, +brush.max), +brush.min);
const value = minmax(+brush.value + change, +brush.min, +brush.max);
brush.value = document.getElementById(brush.id + "Number").value = value;
return;
}

View file

@ -47,8 +47,7 @@ function changePreset(preset) {
.querySelectorAll("li")
.forEach(function (e) {
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click();
// turn on
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click();
});
layersPreset.value = preset;
localStorage.setItem("preset", preset);
@ -121,6 +120,7 @@ function restoreLayers() {
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleIce")) drawIce();
if (layerIsOn("toggleEmblems")) drawEmblems();
if (layerIsOn("toggleMarkers")) drawMarkers();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
@ -1435,8 +1435,8 @@ function toggleTexture(event) {
turnButtonOn("toggleTexture");
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll("*").size()) {
const x = +styleTextureShiftX.value,
y = +styleTextureShiftY.value;
const x = +styleTextureShiftX.value;
const y = +styleTextureShiftY.value;
const image = texture
.append("image")
.attr("id", "textureImage")
@ -1444,18 +1444,14 @@ function toggleTexture(event) {
.attr("y", y)
.attr("width", graphWidth - x)
.attr("height", graphHeight - y)
.attr("xlink:href", getDefaultTexture())
.attr("preserveAspectRatio", "xMidYMid slice");
if (styleTextureInput.value !== "default") getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
}
$("#texture").fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
if (event && isCtrlClick(event)) editStyle("texture");
} else {
if (event && isCtrlClick(event)) {
editStyle("texture");
return;
}
if (event && isCtrlClick(event)) return editStyle("texture");
$("#texture").fadeOut();
turnButtonOff("toggleTexture");
}
@ -1563,7 +1559,7 @@ const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
function drawMarker(marker, rescale = 1) {
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
const id = `marker${i}`;
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : 1;
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
const viewX = rn(x - zoomSize / 2, 1);
const viewY = rn(y - zoomSize, 1);
const pinHTML = getPin(pin, fill, stroke);
@ -1674,21 +1670,21 @@ function drawEmblems() {
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coaSize != 0);
const getStateEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +document.getElementById("emblemsStateSizeInput").value || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +document.getElementById("emblemsProvinceSizeInput").value || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +document.getElementById("emblemsBurgSizeInput").value || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs

View file

@ -8,6 +8,8 @@ function editMarker(markerI) {
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");

View file

@ -75,14 +75,21 @@ function overviewMilitary() {
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" ");
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
lines += `<div class="states" data-id=${s.i} data-state="${
s.name
}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" class="fillRect"></svg>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>
<div data-type="population" data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div>
<input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(s.alert, 2)}">
<input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(
s.alert,
2
)}">
<span data-tip="Show regiments list" class="icon-list-bullet pointer"></span>
</div>`;
}
@ -145,7 +152,15 @@ function overviewMilitary() {
if (!layerIsOn("toggleStates")) return;
const d = regions.select("#state" + state).attr("d");
const path = debug.append("path").attr("class", "highlight").attr("d", d).attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1).attr("filter", "url(#blur1)");
const path = debug
.append("path")
.attr("class", "highlight")
.attr("d", d)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("opacity", 1)
.attr("filter", "url(#blur1)");
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
@ -199,9 +214,9 @@ function overviewMilitary() {
function militaryCustomize() {
const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"];
const table = document.getElementById("militaryOptions").querySelector("tbody");
const tableBody = document.getElementById("militaryOptions").querySelector("tbody");
removeUnitLines();
options.military.map(u => addUnitLine(u));
options.military.map(unit => addUnitLine(unit));
$("#militaryOptions").dialog({
title: "Edit Military Units",
@ -218,43 +233,132 @@ function overviewMilitary() {
},
open: function () {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[0].addEventListener("mousemove", () =>
tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>")
);
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
}
});
if (modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true;
tableBody.addEventListener("click", event => {
const el = event.target;
if (el.tagName !== "BUTTON") return;
const type = el.dataset.type;
if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v));
if (type === "biomes") {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
return selectLimitation(el, biomes);
}
if (type === "states") return selectLimitation(el, pack.states);
if (type === "cultures") return selectLimitation(el, pack.cultures);
if (type === "religions") return selectLimitation(el, pack.religions);
});
function removeUnitLines() {
table.querySelectorAll("tr").forEach(el => el.remove());
tableBody.querySelectorAll("tr").forEach(el => el.remove());
}
function addUnitLine(u) {
function getLimitValue(attr) {
return attr?.join(",") || "";
}
function getLimitText(attr) {
return attr?.length ? "some" : "all";
}
function getLimitTip(attr, data) {
if (!attr || !attr.length) return "";
return attr.map(i => data?.[i]?.name || "").join(", ");
}
function addUnitLine(unit) {
const row = document.createElement("tr");
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || " "}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types.map(t => `<option ${u.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ")}</select></td>
const typeOptions = types.map(t => `<option ${unit.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ");
const getLimitButton = attr =>
`<button
data-tip="Select allowed ${attr}"
data-type="${attr}"
title="${getLimitTip(unit[attr], pack[attr])}"
data-value="${getLimitValue(unit[attr])}">
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = `<td><button data-type="icon" data-tip="Click to select unit icon">${unit.icon || " "}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${unit.name}"></td>
<td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
<td>${getLimitButton("cultures")}</td>
<td>${getLimitButton("religions")}</td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${unit.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${unit.urban}"></td>
<td><input data-tip="Enter average number of people in crew (for total personnel calculation)" type="number" min=1 step=1 value="${unit.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${unit.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${typeOptions}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? "checked" : ""}>
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<input id="${unit.name}Separate" type="checkbox" class="checkbox" ${unit.separate ? "checked" : ""}>
<label for="${unit.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector("button").addEventListener("click", function (e) {
selectIcon(this.innerHTML, v => (this.innerHTML = v));
});
table.appendChild(row);
tableBody.appendChild(row);
}
function restoreDefaultUnits() {
removeUnitLines();
Military.getDefaultOptions().map(u => addUnitLine(u));
Military.getDefaultOptions().map(unit => addUnitLine(unit));
}
function selectLimitation(el, data) {
const type = el.dataset.type;
const value = el.dataset.value;
const initial = value ? value.split(",").map(v => +v) : [];
const lines = data.slice(1).map(
({i, name, fullName, color}) =>
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
<td><input id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? "checked" : ""} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>`
);
alertMessage.innerHTML = `<b>Limit unit by ${type}:</b><div style="margin-top:.3em" class="table"><table><tbody>${lines.join("")}</tbody></table></div>`;
$("#alert").dialog({
width: fitContent(),
title: `Limit unit`,
buttons: {
Invert: function () {
alertMessage.querySelectorAll("input").forEach(el => (el.checked = !el.checked));
},
Apply: function () {
const inputs = Array.from(alertMessage.querySelectorAll("input"));
const selected = inputs.reduce((acc, input, index) => {
if (input.checked) acc.push(index + 1);
return acc;
}, []);
if (!selected.length) return tip("Select at least one element", false, "error");
const allAreSelected = selected.length === inputs.length;
el.dataset.value = allAreSelected ? "" : selected.join(",");
el.innerHTML = allAreSelected ? "all" : "some";
el.setAttribute("title", getLimitTip(selected, data));
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function applyMilitaryOptions() {
const unitLines = Array.from(table.querySelectorAll("tr"));
const unitLines = Array.from(tableBody.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
if (new Set(names).size !== names.length) {
tip("All units should have unique names", false, "error");
@ -263,14 +367,22 @@ function overviewMilitary() {
$("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => {
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll("input, select, button")).map(d => {
let value = d.value;
if (d.type === "number") value = +d.value || 0;
if (d.type === "checkbox") value = +d.checked || 0;
if (d.type === "button") value = d.innerHTML || "";
return value;
const elements = Array.from(r.querySelectorAll("input, button, select"));
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => {
const {type, value} = el.dataset || {};
if (type === "icon") return el.innerHTML || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0;
return el.value;
});
return {icon, name: names[i], rural, urban, crew, power, type, separate};
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
if (biomes) unit.biomes = biomes;
if (states) unit.states = states;
if (cultures) unit.cultures = cultures;
if (religions) unit.religions = religions;
return unit;
});
localStorage.setItem("military", JSON.stringify(options.military));
Military.generate();

View file

@ -1,4 +1,5 @@
"use strict";
function editNotes(id, name) {
// update list of objects
const select = document.getElementById("notesSelect");
@ -8,11 +9,12 @@ function editNotes(id, name) {
}
// initiate pell (html editor)
const notesText = document.getElementById("notesText");
notesText.innerHTML = "";
const editor = Pell.init({
element: document.getElementById("notesText"),
element: notesText,
onChange: html => {
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
const note = notes.find(note => note.id === select.value);
if (!note) return;
note.legend = html;
showNote(note);
@ -43,8 +45,7 @@ function editNotes(id, name) {
title: "Notes Editor",
minWidth: "40em",
width: "50vw",
position: {my: "center", at: "center", of: "svg"},
close: () => (notesText.innerHTML = "")
position: {my: "center", at: "center", of: "svg"}
});
if (modules.editNotes) return;

View file

@ -13,7 +13,6 @@ if (localStorage.getItem("disable_click_arrow_tooltip")) {
// Show options pane on trigger click
function showOptions(event) {
track("click", "show options");
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
clearMainTip();
localStorage.setItem("disable_click_arrow_tooltip", true);
@ -76,7 +75,6 @@ document
// show popup with a list of Patreon supportes (updated manually, to be replaced with API call)
function showSupporters() {
track("click", "show supporters");
const supporters = `Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip,
E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey,
Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott,
@ -91,19 +89,18 @@ function showSupporters() {
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR,
ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill,
Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,Alex Debus,Joshua Vaught,
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,
Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,
George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,Mike Conley,Xavier privé,Hope You're Well,
Mark Sprietsma,Robert Landry,Nick Mowry"`;
Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,
Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,
PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,
Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,Dario Spadavecchia`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@ -476,10 +473,10 @@ function changeDialogsTheme(themeColor, transparency) {
}
function changeZoomExtent(value) {
const min = Math.max(+zoomExtentMin.value, 0.01),
max = Math.min(+zoomExtentMax.value, 200);
const min = Math.max(+zoomExtentMin.value, 0.01);
const max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
const scale = Math.max(Math.min(+value, 200), 0.01);
const scale = minmax(+value, 0.01, 200);
zoom.scaleTo(svg, scale);
}
@ -520,7 +517,7 @@ function applyStoredOptions() {
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
else changeUIsize(Math.max(Math.min(rn(mapWidthInput.value / 1280, 1), 2.5), 1));
else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options
const params = new URL(window.location.href).searchParams;

View file

@ -335,7 +335,6 @@ styleFilterInput.addEventListener("change", function () {
styleTextureInput.addEventListener("change", function () {
if (this.value === "none") texture.select("image").attr("xlink:href", "");
if (this.value === "default") texture.select("image").attr("xlink:href", getDefaultTexture());
else getBase64(this.value, base64 => texture.select("image").attr("xlink:href", base64));
});

View file

@ -469,7 +469,10 @@ function addLabelOnClick() {
const name = Names.getCulture(culture);
const id = getNextId("label");
let group = labels.select("#addedLabels");
// use most recently selected label group
let selected = labelGroupSelect.value;
const symbol = selected ? "#" + selected : "#addedLabels";
let group = labels.select(symbol);
if (!group.size())
group = labels
.append("g")

File diff suppressed because one or more lines are too long