This commit is contained in:
Azgaar 2020-04-23 19:44:32 +03:00
parent e7b4d0e39a
commit 72d124d95d
26 changed files with 713 additions and 498 deletions

View file

@ -263,4 +263,5 @@
margin-left: 1px;
width: .6em;
font-family: monospace;
}
}
.icon-die:before {content:'🎲';}

View file

@ -60,13 +60,6 @@ textarea {
pointer-events: none;
}
#preview {
position: absolute;
bottom: 1em;
left: 1em;
cursor: pointer;
}
#pickerContainer {
position: absolute;
z-index: 100;
@ -101,7 +94,7 @@ button, select, a, .pointer {
fill-rule: evenodd;
}
#lakes, #coastline, #armies {
#lakes, #coastline, #armies, #ice {
cursor: pointer;
}
@ -1151,6 +1144,7 @@ div.slider .ui-slider-handle {
.table {
max-height: 75vh;
max-width: 75vw;
overflow-x: hidden;
overflow-y: auto;
}
@ -1657,7 +1651,6 @@ rect.fillRect {
#militaryOptionsTable input {
width: 9em;
padding-left: 3px;
border: 1px solid #d4d4d4;
}
@ -1760,24 +1753,6 @@ div.textual span, .textual legend {
vertical-align: top;
}
#markerIconTable {
font-size: 1.6em;
cursor: pointer;
}
#markerIconTable td:hover {
transition: .1s;
color: #3c3ca9;
}
#markerIconTable td:active {
transform: translate(0px, 1px);
}
#markerIconTable td.selected {
outline: 1px solid #9b9b9b;
}
.highlighted {
outline-width: 2px;
outline-style: dashed;
@ -2025,7 +2000,7 @@ svg.button {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 21em;
max-width: 22em;
background-color: #fff;
padding: 1.2em;
border: solid 1px #000;

View file

@ -34,11 +34,11 @@
#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="preload" href="index.css?version=1.3" as="style">
<link rel="preload" href="icons.css?version=1.3" as="style">
<link rel="preload" href="index.css?version=1.4" as="style">
<link rel="preload" href="icons.css?version=1.4" as="style">
<link rel="preload" href="libs/jquery-ui.css" as="style">
<link rel="stylesheet" href="index.css?version=1.3">
<link rel="stylesheet" href="icons.css?version=1.3">
<link rel="stylesheet" href="index.css?version=1.4">
<link rel="stylesheet" href="icons.css?version=1.4">
<link rel="stylesheet" href="libs/jquery-ui.css">
</head>
@ -899,7 +899,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.3</div>
<div id="version"><t data-t="version">v. </t>1.4</div>
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
</div>
@ -960,6 +960,7 @@
<li id="toggleRoutes"data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: U" onclick="toggleRoutes(event)">Ro<u>u</u>tes</li>
<li id="toggleTemp" data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: T" class="buttonoff" onclick="toggleTemp(event)"><u>T</u>emperature</li>
<li id="togglePopulation" data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: N" class="buttonoff" onclick="togglePopulation(event)">Populatio<u>n</u></li>
<li id="toggleIce" data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: J" class="buttonoff" onclick="toggleIce(event)">Ice</li>
<li id="togglePrec" data-tip="Precipitation map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: A" class="buttonoff" onclick="togglePrec(event)">Precipit<u>a</u>tion</li>
<li id="toggleLabels" data-tip="Labels: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: L" onclick="toggleLabels(event)"><u>L</u>abels</li>
<li id="toggleIcons" data-tip="Burg icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: I" onclick="toggleIcons(event)"><u>I</u>cons</li>
@ -1002,6 +1003,7 @@
<option value="fogging">Fogging</option>
<option value="gridOverlay">Grid</option>
<option value="terrs">Heightmap</option>
<option value="ice">Ice</option>
<option value="labels">Labels</option>
<option value="lakes">Lakes</option>
<option value="landmass">Landmass</option>
@ -1022,7 +1024,6 @@
<option value="texture">Texture</option>
<option value="compass">Wind Rose</option>
<option value="zones">Zones</option>
<option value="seaIce">Ice</option>
</select>
<!-- <button id="restoreStyle" data-tip="Click to restore default style for all elements" class="icon-ccw styleButton" onclick="askToRestoreDefaultStyle()"></button> -->
@ -1857,6 +1858,7 @@
<button id="regenerateProvinces" data-tip="Click to regenerate provinces. States will remain as they are">Provinces</button>
<button id="regenerateReligions" data-tip="Click to regenerate religions">Religions</button>
<button id="regenerateMilitary" data-tip="Click to recalculate military forces based on current military options">Military</button>
<button id="regenerateIce" data-tip="Click to icebergs and glaciers">Ice</button>
<button id="regenerateMarkers" data-tip="Click to regenerate markers. Hold Ctrl and click to set markers number multiplier">Markers</button>
<button id="regenerateZones" data-tip="Click to regenerate zones. Hold Ctrl and click to set zones number multiplier">Zones</button>
</div>
@ -2160,6 +2162,14 @@
<button id="lakeLegend" data-tip="Edit free text notes (legend) for the lake" class="icon-edit"></button>
</div>
<div id="iceEditor" class="dialog" style="display: none">
<button id="iceEditStyle" data-tip="Edit style in Style Editor" class="icon-brush"></button>
<button id="iceRandomize" data-tip="Randomize Iceberd shape" class="icon-shuffle"></button>
<input id="iceSize" data-tip="Change Iceberg size" type="range" min=".05" max="1" step=".01">
<button id="iceNew" data-tip="Add an Iceberg (click on map)" class="icon-plus"></button>
<button id="iceRemove" data-tip="Remove the element. Shortcut: Delete" class="icon-trash"></button>
</div>
<div id="coastlineEditor" class="dialog" style="display: none">
<button id="coastlineGroupsShow" data-tip="Show the group selection" class="icon-tags"></button>
<div id="coastlineGroupsSelection" style="display: none">
@ -2373,15 +2383,13 @@
<button id="markerIcon" data-tip="Change marker icon and edit positioning" class="icon-star"></button>
<div id="markerIconSection" style="display: none">
<i data-tip="Change marker icon size" class="icon-resize-full"></i>
<input id="markerIconSize" data-tip="Change marker icon size" type="range" min=10 max=30 step=.5 value=22 style="width:12em">
<input id="markerIconSize" data-tip="Change marker icon size" type="range" min=5 max=30 step=.5 value=22 style="width:12em"><br>
<i data-tip="Marker Icon" class="icon-info"></i>
<button id="markerIconSelect" data-tip="Click to select icon"></button>
<i data-tip="Change marker horizontal shift" class="icon-resize-horizontal"></i>
<input id="markerIconShiftX" data-tip="Change icon horizontal shift" type="number" value=50 style="width:3em">
<i data-tip="Change marker vertical shift" class="icon-resize-vertical"></i>
<input id="markerIconShiftY" data-tip="Change vertical shift" type="number" min=0 max=100 value=50 style="width:3em">
<span data-tip="Paste custom unicode symbol to use as icon">Paste here:</span>
<input id="markerIconCustom" data-tip="Paste custom unicode symbol to use as icon" style="width:3em">
<table id="markerIconTable"></table>
<div style="font-style: italic; color: darkgrey;">Emojis look different in different browsers. Visit <a href="https://emojipedia.org/" rel="noopener" target="_blank">Emojipedia</a> to check and find more</div>
</div>
<button id="markerStyle" data-tip="Change marker size and colors" class="icon-brush"></button>
@ -2421,6 +2429,7 @@
</div>
<div id="regimentBottom">
<button id="regimentAttack" data-tip="Attack other regiment" class="icon-target"></button>
<button id="regimentAdd" data-tip="Create new regiment or fleet" class="icon-user-plus"></button>
<button id="regimentSplit" data-tip="Split regiment into 2 separate ones" class="icon-half"></button>
<button id="regimentAttach" data-tip="Attach regiment to another one (include this regiment to another one)" class="icon-attach"></button>
@ -2430,6 +2439,22 @@
</div>
</div>
<div id="battleScreen" class="dialog" style="display: none">
<div id="battleBody">
<div style="font-size:1.2em; font-weight: bold"><div style="width: 1.2em; display: inline-block"></div><span>Attackers</span></div>
<table id="battleAttackers" style="padding: .2em .6em .2em .6em; border: 1px solid #ccc; margin: .0 0 .4em 0"><thead><tr></tr></thead><tbody></tbody></table>
<div style="font-size:1.2em; font-weight: bold"><div style="width: 1.2em; display: inline-block">🛡</div><span>Defenders</span></div>
<table id="battleDefenders" style="padding: .2em .6em .2em .6em; border: 1px solid #ccc; margin: .0 0 .4em 0"><thead><tr></tr></thead><tbody></tbody></table>
</div>
<div id="battleBottom">
<button id="battleRoll" data-tip="Roll dice to randomize sides power and battle phase" class="icon-die"></button>
<button id="battleNext" data-tip="Calculate and apply phase results" class="icon-play"></button>
<button id="battleApply" data-tip="Apply battle results" class="icon-check"></button>
<button id="battleCancel" data-tip="Cancel battle results and restore initial troop value" class="icon-cancel"></button>
</div>
</div>
<div id="brushesPanel" class="dialog stable" style="display: none">
<div id="brushesButtons" style="display: inline-block">
<button id="brushRaise" data-tip="Raise brush: increase height of cells in radius by Power value">
@ -2839,7 +2864,7 @@
<div id="diplomacyEditor" class="dialog stable" style="display: none">
<div id="diplomacyHeader" class="header">
<div style="left:.2em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State&nbsp;</div>
<div style="left:12.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div>
<div style="left:13.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div>
</div>
<div id="diplomacyBodySection" class="table"></div>
@ -3329,10 +3354,12 @@
<table id="militaryOptionsTable">
<thead>
<tr>
<th data-tip="Unit name. If name is changed for existing unit, old unit will be replaced">Military unit</th>
<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 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">Crew</th>
<th data-tip="Average number of people in crew (used for total personnel calculation)">Crew</th>
<th data-tip="Unit military power (used for battle simulation)">Power</th>
<th data-tip="Unit type to apply special rules on forces recalculation">Type</th>
<th data-tip="Check if unit is separate and can be stacked only with units of the same type">Sep.</th>
</tr>
@ -3382,6 +3409,15 @@
<p><b>Burg:</b> <span id="infoBurg">n/a</span></p>
</div>
<div id="iconSelector" style="display: none" class="dialog">
<table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center"></table>
<div style="font-style: italic; font-size: 1.2em; margin: .4em 0 0 .4em">
<span>Select from the list or paste a Unicode character here: </span>
<input id="iconInput" style="width: 2em">
<span>. See <a href="https://emojipedia.org" target="_blank">Emojipedia</a> for reference</span>
</div>
</div>
<div id="options3d" class="dialog stable" style="display: none">
<div id="options3dMesh" style="display: none">
@ -3523,6 +3559,7 @@
<script src="modules/religions-generator.js"></script>
<script src="modules/military-generator.js"></script>
<script src="libs/polylabel.min.js"></script>
<script src="libs/lineclip.js"></script>
<script src="libs/jquery-ui.min.js"></script>
<script src="libs/seedrandom.min.js"></script>
<script src="modules/ui/layers.js"></script>
@ -3532,7 +3569,7 @@
<script defer src="modules/ui/style.js"></script>
<script defer src="modules/ui/measurers.js"></script>
<script defer src="modules/save-and-load.js"></script>
<script defer src="main.js?version=1.0"></script>
<script defer src="main.js?version=1.4"></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>
@ -3543,6 +3580,7 @@
<script defer src="modules/ui/cultures-editor.js"></script>
<script defer src="modules/ui/namesbase-editor.js"></script>
<script defer src="modules/ui/routes-editor.js"></script>
<script defer src="modules/ui/ice-editor.js"></script>
<script defer src="modules/ui/lakes-editor.js"></script>
<script defer src="modules/ui/coastline-editor.js"></script>
<script defer src="modules/ui/labels-editor.js"></script>
@ -3560,6 +3598,7 @@
<script defer src="modules/ui/military-overview.js"></script>
<script defer src="modules/ui/regiments-overview.js"></script>
<script defer src="modules/ui/regiment-editor.js"></script>
<script defer src="modules/ui/battle-screen.js"></script>
<script defer src="modules/ui/editors.js"></script>
<script defer src="modules/ui/3d.js"></script>
<script defer src="libs/quantize.min.js"></script>

File diff suppressed because one or more lines are too long

102
libs/lineclip.js Normal file
View file

@ -0,0 +1,102 @@
'use strict';
// lineclip by mourner, https://github.com/mapbox/lineclip
// Cohen-Sutherland line clippign algorithm, adapted to efficiently
// handle polylines rather than just segments
function lineclip(points, bbox, result) {
var len = points.length,
codeA = bitCode(points[0], bbox),
part = [],
i, a, b, codeB, lastCode;
if (!result) result = [];
for (i = 1; i < len; i++) {
a = points[i - 1];
b = points[i];
codeB = lastCode = bitCode(b, bbox);
while (true) {
if (!(codeA | codeB)) { // accept
part.push(a);
if (codeB !== lastCode) { // segment went outside
part.push(b);
if (i < len - 1) { // start a new line
result.push(part);
part = [];
}
} else if (i === len - 1) {
part.push(b);
}
break;
} else if (codeA & codeB) { // trivial reject
break;
} else if (codeA) { // a outside, intersect with clip edge
a = intersect(a, b, codeA, bbox);
codeA = bitCode(a, bbox);
} else { // b outside
b = intersect(a, b, codeB, bbox);
codeB = bitCode(b, bbox);
}
}
codeA = lastCode;
}
if (part.length) result.push(part);
return result;
}
// Sutherland-Hodgeman polygon clipping algorithm
function polygonclip(points, bbox, secure = false) {
var result, edge, prev, prevInside, inter, i, p, inside;
// clip against each side of the clip rectangle
for (edge = 1; edge <= 8; edge *= 2) {
result = [];
prev = points[points.length - 1];
prevInside = !(bitCode(prev, bbox) & edge);
for (i = 0; i < points.length; i++) {
p = points[i];
inside = !(bitCode(p, bbox) & edge);
inter = inside !== prevInside; // segment goes through the clip window
if (secure && inter && inside && i) result.push(points[i-1]); // add previous point in secure mode (to get a correct d3 curve)
if (inter) result.push(intersect(prev, p, edge, bbox)); // add an intersection point
if (inside) result.push(p); // add a point if it's inside
else if (secure && prevInside) result.push(p); // // add an outside point if in secure mode (to get a correct d3 curve)
prev = p;
prevInside = inside;
}
points = result;
if (!points.length) break;
}
return result;
}
// intersect a segment against one of the 4 lines that make up the bbox
function intersect(a, b, edge, bbox) {
return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top
edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom
edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right
edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : null; // left
}
// bit code reflects the point position relative to the bbox:
// left mid right
// top 1001 1000 1010
// mid 0001 0000 0010
// bottom 0101 0100 0110
function bitCode(p, bbox) {
var code = 0;
if (p[0] < bbox[0]) code |= 1; // left
else if (p[0] > bbox[2]) code |= 2; // right
if (p[1] < bbox[1]) code |= 4; // bottom
else if (p[1] > bbox[3]) code |= 8; // top
return code;
}

153
main.js
View file

@ -7,7 +7,7 @@
// See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
"use strict";
const version = "1.3"; // generator version
const version = "1.4"; // generator version
document.title += " v" + version;
// if map version is not stored, clear localStorage and show a message
@ -52,6 +52,7 @@ let trails = routes.append("g").attr("id", "trails");
let searoutes = routes.append("g").attr("id", "searoutes");
let temperature = viewbox.append("g").attr("id", "temperature");
let coastline = viewbox.append("g").attr("id", "coastline");
let ice = viewbox.append("g").attr("id", "ice").style("display", "none");
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
let population = viewbox.append("g").attr("id", "population");
let labels = viewbox.append("g").attr("id", "labels");
@ -220,7 +221,7 @@ function focusOn() {
const url = new URL(window.location.href);
const params = url.searchParams;
if (params.get("from") === "MFCG") {
if (params.get("from") === "MFCG" && document.referrer) {
if (params.get("seed").length === 13) {
// show back burg from MFCG
params.set("burg", params.get("seed").slice(-4));
@ -313,12 +314,12 @@ function applyDefaultBiomesSystem() {
const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150];
const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:1},{deciduous:6, swamp:1},{conifer:1},{grass:1},{},{swamp:1}];
const cost = [10,200,150,60,50,70,70,80,90,200,1000,5000,150]; // biome movement cost
const biomesMartix = [ // hot ↔ cold; dry ↕ wet
new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]),
new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,9,10,10]),
const biomesMartix = [ // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,10]),
new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,10,10,10]),
new Uint8Array([5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,9,9,9,9,9,10,10,10]),
new Uint8Array([5,6,6,6,6,6,6,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10]),
new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10])
new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10])
];
// parse icons weighted array into a simple array
@ -527,7 +528,7 @@ function generate() {
elevateLakes();
Rivers.generate();
defineBiomes();
//drawSeaIce();
drawIce();
rankCells();
Cultures.generate();
@ -701,8 +702,9 @@ function openNearSeaLakes() {
// define map size and position based on template and random factor
function defineMapSize() {
const [size, latitude] = getSizeAndLatitude();
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
if (randomize || !locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
function getSizeAndLatitude() {
const template = document.getElementById("templateInput").value; // heightmap template
@ -963,7 +965,7 @@ function drawCoastline() {
let vchain = connectVertices(start, type);
if (features[f].type === "lake") relax(vchain, 1.2);
used[f] = 1;
let points = vchain.map(v => vertices.p[v]);
let points = clipPoly(vchain.map(v => vertices.p[v]), 1);
const area = d3.polygonArea(points); // area with lakes/islands
if (area > 0 && features[f].type === "lake") {
points = points.reverse();
@ -1056,7 +1058,6 @@ function reMarkFeatures() {
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20;
//const frozen = !land && temp[cells.g[start]] < -5; // check if water is frozen
let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature
@ -1075,7 +1076,6 @@ function reMarkFeatures() {
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
}
if (!cells.f[e] && land === eLand) {
//if (!land && frozen !== temp[cells.g[e]] < -5) return;
queue.push(e);
cells.f[e] = i;
cellNumber++;
@ -1137,28 +1137,32 @@ function elevateLakes() {
// assign biome id for each cell
function defineBiomes() {
console.time("defineBiomes");
const cells = pack.cells, f = pack.features;
const cells = pack.cells, f = pack.features, temp = grid.cells.temp, prec = grid.cells.prec;
cells.biome = new Uint8Array(cells.i.length); // biomes array
for (const i of cells.i) {
if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes
const temp = grid.cells.temp[cells.g[i]]; // temperature
if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes; here to save some resources
const t = temp[cells.g[i]]; // cell temperature
const h = cells.h[i]; // cell height
const m = h < 20 ? 0 : calculateMoisture(i); // cell moisture
cells.biome[i] = getBiomeId(m, t, h);
}
if (cells.h[i] < 20 && temp > -6) continue; // liquid water cells have biome 0
let moist = grid.cells.prec[cells.g[i]];
function calculateMoisture(i) {
let moist = prec[cells.g[i]];
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2);
const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]);
moist = rn(4 + d3.mean(n));
cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
const n = cells.c[i].filter(isLand).map(c => prec[cells.g[c]]).concat([moist]);
return rn(4 + d3.mean(n));
}
console.timeEnd("defineBiomes");
}
// assign biome id to a cell
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome, including sea ice
if (height < 20) return 0; // liquid water cells have marine biome
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
if (height < 20) return 0; // marine biome: liquid water cells
if (moisture > 40 && temperature > -2 && (height < 25 || moisture > 24 && height > 24)) return 12; // wetland biome
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t];
@ -1211,20 +1215,15 @@ function addMarkers(number = 1) {
void function addVolcanoes() {
let mounts = Array.from(cells.i).filter(i => cells.h[i] > 70).sort((a, b) => cells.h[b] - cells.h[a]);
let count = mounts.length < 10 ? 0 : Math.ceil(mounts.length / 300 * number);
if (count) addMarker("volcano", "🌋", 52, 52, 17.5);
if (count) addMarker("volcano", "🌋", 52, 50, 13);
while (count && mounts.length) {
const cell = mounts.splice(biased(0, mounts.length-1, 5), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id).attr("data-cell", cell)
.attr("xlink:href", "#marker_volcano").attr("data-id", "#marker_volcano")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const height = getFriendlyHeight([x, y]);
const id = appendMarker(cell, "volcano");
const proper = Names.getCulture(cells.culture[cell]);
const name = P(.3) ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper;
notes.push({id, name, legend:`Active volcano. Height: ${height}`});
notes.push({id, name, legend:`Active volcano. Height: ${getFriendlyHeight([x, y])}`});
count--;
}
}()
@ -1232,17 +1231,11 @@ function addMarkers(number = 1) {
void function addHotSprings() {
let springs = Array.from(cells.i).filter(i => cells.h[i] > 50).sort((a, b) => cells.h[b]-cells.h[a]);
let count = springs.length < 30 ? 0 : Math.ceil(springs.length / 1000 * number);
if (count) addMarker("hot_springs", "♨", 50, 50, 19.5);
if (count) addMarker("hot_springs", "♨", 50, 52, 12.5);
while (count && springs.length) {
const cell = springs.splice(biased(1, springs.length-1, 3), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_hot_springs").attr("data-id", "#marker_hot_springs")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "hot_springs");
const proper = Names.getCulture(cells.culture[cell]);
const temp = convertTemperature(gauss(30,15,20,100));
notes.push({id, name: proper + " Hot Springs", legend:`A hot springs area. Temperature: ${temp}`});
@ -1255,17 +1248,12 @@ function addMarkers(number = 1) {
let count = !hills.length ? 0 : Math.ceil(hills.length / 7 * number);
if (!count) return;
addMarker("mine", "⚒", 50, 50, 20);
addMarker("mine", "⛏️", 48, 50, 13.5);
const resources = {"salt":5, "gold":2, "silver":4, "copper":2, "iron":3, "lead":1, "tin":1};
while (count && hills.length) {
const cell = hills.splice(Math.floor(Math.random() * hills.length), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_mine").attr("data-id", "#marker_mine")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "mine");
const resource = rw(resources);
const burg = pack.burgs[cells.burg[cell]];
const name = `${burg.name}${resource} mining town`;
@ -1285,17 +1273,11 @@ function addMarkers(number = 1) {
.sort((a, b) => (cells.road[b] + cells.fl[b] / 10) - (cells.road[a] + cells.fl[a] / 10));
let count = !bridges.length ? 0 : Math.ceil(bridges.length / 12 * number);
if (count) addMarker("bridge", "🌉", 50, 50, 16.5);
if (count) addMarker("bridge", "🌉", 50, 50, 14);
while (count && bridges.length) {
const cell = bridges.splice(0, 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_bridge").attr("data-id", "#marker_bridge")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "bridge");
const burg = pack.burgs[cells.burg[cell]];
const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
const riverName = river ? `${river.name} ${river.type}` : "river";
@ -1310,7 +1292,7 @@ function addMarkers(number = 1) {
let taverns = Array.from(cells.i).filter(i => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad);
if (!taverns.length) return;
const count = Math.ceil(4 * number);
addMarker("inn", "🍻", 50, 50, 17.5);
addMarker("inn", "🍻", 50, 50, 14.5);
const color = ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"];
const animal = ["Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "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", "Wolf", "Wolverine", "Camel", "Falcon", "Hound", "Ox"];
@ -1318,14 +1300,7 @@ function addMarkers(number = 1) {
for (let i=0; i < taverns.length && i < count; i++) {
const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_inn").attr("data-id", "#marker_inn")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "inn");
const type = P(.3) ? "inn" : "tavern";
const name = P(.5) ? ra(color) + " " + ra(animal) : P(.6) ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type);
notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`});
@ -1340,14 +1315,7 @@ function addMarkers(number = 1) {
for (let i=0; i < lighthouses.length && i < count; i++) {
const cell = lighthouses[i][0], vertex = lighthouses[i][1];
const x = pack.vertices.p[vertex][0], y = pack.vertices.p[vertex][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_lighthouse").attr("data-id", "#marker_lighthouse")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "lighthouse");
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`});
}
@ -1360,14 +1328,7 @@ function addMarkers(number = 1) {
for (let i=0; i < waterfalls.length && i < count; i++) {
const cell = waterfalls[i];
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_waterfall").attr("data-id", "#marker_waterfall")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "waterfall");
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`});
}
@ -1376,17 +1337,11 @@ function addMarkers(number = 1) {
void function addBattlefields() {
let battlefields = Array.from(cells.i).filter(i => cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25);
let count = battlefields.length < 100 ? 0 : Math.ceil(battlefields.length / 500 * number);
if (count) addMarker("battlefield", "⚔", 50, 50, 20);
if (count) addMarker("battlefield", "⚔", 50, 52, 12);
while (count && battlefields.length) {
const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const id = appendMarker(cell, "battlefield");
const campaign = ra(states[cells.state[cell]].campaigns);
const date = generateDate(campaign.start, campaign.end);
const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
@ -1407,6 +1362,19 @@ function addMarkers(number = 1) {
.attr("font-size", size+"px").attr("dominant-baseline", "central").text(icon);
}
function appendMarker(cell, type) {
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
const name = "#marker_" + type;
markers.append("use").attr("id", id)
.attr("xlink:href", name).attr("data-id", name)
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
return id;
}
console.timeEnd("addMarkers");
}
@ -1593,14 +1561,11 @@ function addZones(number = 1) {
}
function addEruption() {
const volcanoes = [];
markers.selectAll("use[data-id='#marker_volcano']").each(function() {
volcanoes.push(this.dataset.cell);
});
if (!volcanoes.length) return;
const volcano = document.getElementById("markers").querySelector("use[data-id='#marker_volcano']");
if (!volcano) return;
const cell = +ra(volcanoes);
const id = markers.select("use[data-cell='"+cell+"']").attr("id");
const x = +volcano.dataset.x, y = +volcano.dataset.y, cell = findCell(x, y);
const id = volcano.id;
const note = notes.filter(n => n.id === id);
if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano");
@ -1613,7 +1578,7 @@ function addZones(number = 1) {
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (used[e] || cells.h[e] < 20) return;
used[e] = 1;
queue.push(e);
});

View file

@ -187,7 +187,7 @@
} else b.port = 0;
// define burg population (keep urbanization at about 10% rate)
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) {
@ -420,7 +420,7 @@
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;}
const nC = cells.c[c].filter(n => cells.state[n] === state);
const intersected = intersect(nQ, nC).length
const intersected = common(nQ, nC).length
if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;}
if (used[c]) return;
used[c] = 1;
@ -810,7 +810,7 @@
});
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
const republic = {Republic:70, Federation:2, Oligarchy:2, Tetrarchy:1, Triumvirate:1, Diarchy:1, "Trade Company":3}; // weighted random
const republic = {Republic:75, Federation:4, Oligarchy: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
for (const s of states) {
@ -870,11 +870,13 @@
if (s.form === "Union") return rw(union);
if (s.form === "Theocracy") {
// default name is "Theocracy", some culture bases have special names
if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
if ([7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
if ([21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
if ([18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
// default name is "Theocracy"
if (P(.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
if (P(.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
if (P(.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
if (P(.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
if (P(.02)) return "Thearchy"; // "Thearchy" in very rare case
if (P(.05)) return "See"; // "See" in rare case
return "Theocracy";
}
}

View file

@ -208,11 +208,11 @@
const getDefaultOptions = function() {
return [
{name:"infantry", rural:.25, urban:.2, crew:1, type:"melee", separate:0},
{name:"archers", rural:.12, urban:.2, crew:1, type:"ranged", separate:0},
{name:"cavalry", rural:.12, urban:.03, crew:3, type:"mounted", separate:0},
{name:"artillery", rural:0, urban:.03, crew:8, type:"machinery", separate:0},
{name:"fleet", rural:0, urban:.015, crew:100, type:"naval", separate:1}
{icon: "⚔️", name:"infantry", rural:.25, urban:.2, crew:1, power:1, type:"melee", separate:0},
{icon: "🏹", name:"archers", rural:.12, urban:.2, crew:1, power:1, type:"ranged", separate:0},
{icon: "🐴", name:"cavalry", rural:.12, urban:.03, crew:3, power:4, type:"mounted", separate:0},
{icon: "💣", name:"artillery", rural:0, urban:.03, crew:8, power:12, type:"machinery", separate:0},
{icon: "🌊", name:"fleet", rural:0, urban:.015, crew:100, power:50, type:"naval", separate:1}
];
}
@ -271,16 +271,11 @@
// get default regiment emblem
const getEmblem = function(r) {
if (r.n) return "🌊";
if (!Object.values(r.u).length) return "🛡️";
const mainUnit = Object.entries(r.u).sort((a,b) => b[1]-a[1])[0][0];
const type = options.military.find(u => u.name === mainUnit).type;
if (type === "ranged") return "🏹";
if (type === "mounted") return "🐴";
if (type === "machinery") return "💣";
if (type === "armored") return "🐢";
if (type === "aviation") return "🦅";
if (type === "magical") return "🔮";
else return "⚔️";
if (!Object.values(r.u).length) return "🔰"; // regiment without troops
if (cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]].capital) return "👑"; // "Royal" regiment based in capital
const mainUnit = Object.entries(r.u).sort((a,b) => b[1]-a[1])[0][0]; // unit with more troops in regiment
const unit = options.military.find(u => u.name === mainUnit);
return unit.icon;
}
const generateNote = function(r, s) {

View file

@ -11,6 +11,7 @@
if (outline === "none") return;
console.time("drawOceanLayers");
lineGen.curve(d3.curveBasisClosed);
cells = grid.cells, pointsN = grid.cells.i.length, vertices = grid.vertices;
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
markupOcean(limits);
@ -29,7 +30,8 @@
const chain = connectVertices(start, t); // vertices chain to form a path
const relaxation = 1 + t * -2; // select only n-th point
const relaxed = chain.filter((v, i) => i % relaxation === 0 || vertices.c[v].some(c => c >= pointsN));
if (relaxed.length >= 3) chains.push([t, relaxed.map(v => vertices.p[v])]);
const points = clipPoly(relaxed.map(v => vertices.p[v]), 1);
if (relaxed.length >= 3) chains.push([t, points]);
}
for (const t of limits) {
@ -57,8 +59,8 @@
return limits;
}
// Define grid ocean cells type based on distance form land
function markupOcean(limits) {
// Define ocean cells type based on distance form land
for (let t = -2; t >= limits[0]-1; t--) {
for (let i = 0; i < pointsN; i++) {
if (cells.t[i] !== t+1) continue;

View file

@ -296,6 +296,33 @@ async function saveMap() {
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
}
// Send .map file to server [test function]
async function sendToURL(URL = "http://localhost:8000/upload.php") {
if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;}
closeDialogs("#alert");
const blob = await getMapData();
const xhr = new XMLHttpRequest();
xhr.open("POST", URL, true);
//xhr.setRequestHeader("Content-Type", "blob"); // set request header
xhr.onload = e => console.log("onload", e.responseText);
xhr.onreadystatechange = () => {if (xhr.readyState === 4 && xhr.status === 200) tip(`File is sent to cloud storage`, true, "success", 7000);}
xhr.send(blob);
}
// Load .map file from server [test function]
async function loadFromCloud() {
ldb.get("lastMap", blob => {
if (blob) {
loadMapPrompt(blob);
} else {
tip("No map stored. Save map to storage first", true, "error", 2000);
console.error("No map stored");
}
});
uploadMap(blob);
}
function saveGeoJSON_Cells() {
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
const cells = pack.cells, v = pack.vertices;
@ -617,6 +644,7 @@ function parseLoadedData(data) {
texture = viewbox.select("#texture");
terrs = viewbox.select("#terrs");
biomes = viewbox.select("#biomes");
ice = viewbox.select("#ice");
cells = viewbox.select("#cells");
gridOverlay = viewbox.select("#gridOverlay");
coordinates = viewbox.select("#coordinates");
@ -953,12 +981,34 @@ function parseLoadedData(data) {
Military.generate();
}
if (version < 1.35) {
if (version < 1.4) {
// v 1.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", .7).attr("filter", null);
}
// v 1.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)");
drawIce();
// v 1.4 added icon and power attributes for units
for (const unit of options.military) {
if (!unit.icon) unit.icon = getUnitIcon(unit.type);
if (!unit.power) unit.power = unit.crew;
}
function getUnitIcon(type) {
if (type === "naval") return "🌊";
if (type === "ranged") return "🏹";
if (type === "mounted") return "🐴";
if (type === "machinery") return "💣";
if (type === "armored") return "🐢";
if (type === "aviation") return "🦅";
if (type === "magical") return "🔮";
else return "⚔️";
}
}
}()

View file

@ -0,0 +1,50 @@
"use strict";
function showBattleScreen(attacker, defender) {
if (customization) return;
closeDialogs(".stable");
const battle = {name:"Battle", attackers:[attacker], defenders:[defender]};
const battleAttackers = document.getElementById("battleAttackers");
const battleDefenders = document.getElementById("battleDefenders");
addHeaders();
addRegiment(battleAttackers, attacker);
addRegiment(battleDefenders, defender);
$("#battleScreen").dialog({
title: battle.name, resizable: false, width: fitContent(), close: closeBattleScreen,
position: {my: "center", at: "center", of: "#map"}
});
if (modules.showBattleScreen) return;
modules.showBattleScreen = true;
// add listeners
//document.getElementById("regimentNameRestore").addEventListener("click", restoreName);
function addHeaders() {
document.getElementById("battleScreen").querySelectorAll("th").forEach(el => el.remove());
const attackers = battleAttackers.querySelector("tr");
const defenders = battleDefenders.querySelector("tr");
let headers = "<th></th>";
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
headers += `<th data-tip="${label}">${u.icon}</th>`;
}
headers += "<th>Total</th>";
attackers.insertAdjacentHTML("beforebegin", headers);
defenders.insertAdjacentHTML("beforebegin", headers);
}
function addRegiment(div, regiment) {
const reg = document.createElement("div");
reg.innerHTML = regiment.name;
div.append(reg);
}
function closeBattleScreen() {
}
}

View file

@ -215,6 +215,8 @@ function editBiomes() {
function addCustomBiome() {
const b = biomesData, i = biomesData.i.length;
if (i > 254) {tip("Maximum number of biomes reached (255), data cleansing is required", false, "error"); return;}
b.i.push(i);
b.color.push(getRandomColor());
b.habitability.push(50);

View file

@ -294,13 +294,14 @@ function editBurg(id) {
function openMFCG(seed) {
if (!seed && burg.MFCGlink) {openURL(burg.MFCGlink); return;}
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 65), 6);
const s = burg.MFCG || defSeed;
const cell = burg.cell;
const hub = +pack.cells.road[cell] > 50;
const river = pack.cells.r[cell] ? 1 : 0;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
@ -309,8 +310,31 @@ function editBurg(id) {
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : "";
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI - 90;
if (deg < 0) deg += 360;
let norm = rn(normalize(deg, 0, 360) * 8) / 4;
if (norm === 2) norm = 0;
return "sea="+norm;
// debug.selectAll("*").remove();
// pack.burgs.filter(b => b.port).forEach(b => {
// var p1 = pack.cells.p[b.cell];
// var p2 = pack.cells.p[pack.cells.haven[b.cell]];
// var deg = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI - 90;
// if (deg < 0) deg += 360;
// var norm = rn(normalize(deg, 0, 360) * 8) / 4;
// if (norm === 2) norm = 0;
// debug.append("line").attr("x1", p1[0]).attr("y1", p1[1]).attr("x2", p2[0]).attr("y2", p2[1]).attr("stroke", "red").attr("stroke-width", .2);
// debug.append("circle").attr("cx", b.x).attr("cy", b.y).attr("r", .4);
// debug.append("text").attr("x", b.x+1).attr("y", b.y).attr("font-size", 2).text(rn(norm, 2));
// });
}
const site = "http://fantasycities.watabou.ru/";
const url = `${site}?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}`;
const url = `${site}?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
}
}

View file

@ -26,6 +26,7 @@ function clicked() {
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "ice") editIce();
else if (parent.id === "terrain") editReliefIcon();
else if (parent.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline();
@ -581,4 +582,43 @@ function highlightElement(element) {
let y = box.y + box.height / 2;
if (tr[1]) y += tr[1];
if (scale >= 2) zoomTo(x, y, scale, 1600);
}
function selectIcon(initial, callback) {
if (!callback) return;
$("#iconSelector").dialog();
const table = document.getElementById("iconTable");
const input = document.getElementById("iconInput");
input.value = initial;
if (!table.innerHTML) {
const icons = ["⚔️","🏹","🐴","💣","🌊","🎯","⚓","🔮","📯","⚒️","🛡️","👑","⚜️",
"☠️","🎆","🗡️","🔪","⛏️","🔥","🩸","💧","🐾","🎪","🏰","🏯","⛓️","❤️","💘","💜","📜","🔔",
"🔱","💎","🌈","🌠","✨","💥","☀️","🌙","⚡","❄️","♨️","🎲","🚨","🌉","🗻","🌋","🧱",
"⚖️","✂️","🎵","👗","🎻","🎨","🎭","⛲","💉","📖","📕","🎁","💍","⏳","🕸️","⚗️","☣️","☢️",
"🔰","🎖️","🚩","🏳️","🏴","💪","✊","👊","🤜","🤝","🙏","🧙","🧙‍♀️","💂","🤴","🧛","🧟","🧞","🧝","👼",
"👻","👺","👹","🦄","🐲","🐉","🐎","🦓","🐺","🦊","🐱","🐈","🦁","🐯","🐅","🐆","🐕","🦌","🐵","🐒","🦍",
"🦅","🕊️","🐓","🦇","🦜","🐦","🦉","🐮","🐄","🐂","🐃","🐷","🐖","🐗","🐏","🐑","🐐","🐫","🦒","🐘","🦏","🐭","🐁","🐀",
"🐹","🐰","🐇","🦔","🐸","🐊","🐢","🦎","🐍","🐳","🐬","🦈","🐠","🐙","🦑","🐌","🦋","🐜","🐝","🐞","🦗","🕷️","🦂","🦀",
"🌳","🌲","🎄","🌴","🍂","🍁","🌵","☘️","🍀","🌿","🌱","🌾","🍄","🌽","🌸","🌹","🌻",
"🍒","🍏","🍇","🍉","🍅","🍓","🥔","🥕","🥩","🍗","🍞","🍻","🍺","🍲","🍷"
];
let row = "";
for (let i=0; i < icons.length; i++) {
if (i%17 === 0) row = table.insertRow(i/17|0);
const cell = row.insertCell(i%17);
cell.innerHTML = icons[i];
}
}
table.onclick = e => {if (e.target.tagName === "TD") {input.value = e.target.innerHTML; callback(input.value)}};
table.onmouseover = e => {if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`)};
$("#iconSelector").dialog({width: fitContent(), title: "Select Icon",
buttons: {
Apply: function() {callback(input.value||""); $(this).dialog("close")},
Close: function() {callback(initial); $(this).dialog("close")}}
});
}

View file

@ -106,6 +106,7 @@ function showMapTooltip(point, e, i, g) {
if (group === "lakes" && !land) {tip(`${capitalize(subgroup)} lake. Click to edit`); return;}
if (group === "coastline") {tip("Click to edit the coastline"); return;}
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
if (group === "ice") {tip("Click to edit the Ice"); return;}
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
@ -403,6 +404,7 @@ document.addEventListener("keyup", event => {
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
else if (key === 78) togglePopulation(); // "N" to toggle Population layer
else if (key === 74) toggleIce(); // "J" to toggle Ice layer
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer

View file

@ -965,7 +965,7 @@ function editHeightmap() {
closeDialogs("#imageConverter");
$("#imageConverter").dialog({
title: "Image Converter", minHeight: "auto", width: "19.5em", resizable: false,
title: "Image Converter", maxHeight: svgHeight*.75, minHeight: "auto", width: "19.5em", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"},
beforeClose: closeImageConverter
});

105
modules/ui/ice-editor.js Normal file
View file

@ -0,0 +1,105 @@
"use strict";
function editIce() {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleIce")) toggleIce();
elSelected = d3.select(d3.event.target);
const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
document.getElementById("iceRandomize").style.display = type === "Glacier" ? "none" : "inline-block";
document.getElementById("iceSize").style.display = type === "Glacier" ? "none" : "inline-block";
if (type === "Iceberg") document.getElementById("iceSize").value = +elSelected.attr("size");
ice.selectAll("*").classed("draggable", true).call(d3.drag().on("drag", dragElement));
$("#iceEditor").dialog({
title: "Edit "+type, resizable: false,
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"},
close: closeEditor
});
if (modules.editIce) return;
modules.editIce = true;
// add listeners
document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice"));
document.getElementById("iceRandomize").addEventListener("click", randomizeShape);
document.getElementById("iceSize").addEventListener("input", changeSize);
document.getElementById("iceNew").addEventListener("click", toggleAdd);
document.getElementById("iceRemove").addEventListener("click", removeIce);
function randomizeShape() {
const c = grid.points[+elSelected.attr("cell")];
const s = +elSelected.attr("size");
const i = ra(grid.cells.i), cn = grid.points[i];
const poly = getGridPolygon(i).map(p => [p[0]-cn[0], p[1]-cn[1]]);
const points = poly.map(p => [rn(c[0] + p[0] * s, 2), rn(c[1] + p[1] * s, 2)]);
elSelected.attr("points", points);
}
function changeSize() {
const c = grid.points[+elSelected.attr("cell")];
const s = +elSelected.attr("size");
const flat = elSelected.attr("points").split(",").map(el => +el);
const pairs = [];
while (flat.length) pairs.push(flat.splice(0,2));
const poly = pairs.map(p => [(p[0]-c[0]) / s, (p[1]-c[1]) / s]);
const size = +this.value;
const points = poly.map(p => [rn(c[0] + p[0] * size, 2), rn(c[1] + p[1] * size, 2)]);
elSelected.attr("points", points).attr("size", size);
}
function toggleAdd() {
document.getElementById("iceNew").classList.toggle("pressed");
if (document.getElementById("iceNew").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addIcebergOnClick);
tip("Click on map to create an iceberg. Hold Shift to add multiple", true);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
}
}
function addIcebergOnClick() {
const point = d3.mouse(this);
const i = findGridCell(point[0], point[1]);
const c = grid.points[i];
const s = +document.getElementById("iceSize").value;
const points = getGridPolygon(i).map(p => [(p[0] + (c[0]-p[0]) / s)|0, (p[1] + (c[1]-p[1]) / s)|0]);
const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", s);
iceberg.call(d3.drag().on("drag", dragElement));
if (d3.event.shiftKey === false) toggleAdd();
}
function removeIce() {
const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
alertMessage.innerHTML = `Are you sure you want to remove the ${type}?`;
$("#alert").dialog({resizable: false, title: "Remove "+type,
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
$("#iceEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function dragElement() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const x = d3.event.x, y = d3.event.y;
this.setAttribute("transform", `translate(${(dx+x)},${(dy+y)})`);
});
}
function closeEditor() {
ice.selectAll("*").classed("draggable", false).call(d3.drag().on("drag", null));
clearMainTip();
iceNew.classList.remove("pressed");
unselect();
}
}

View file

@ -32,10 +32,10 @@ function getDefaultPresets() {
"cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"provinces": ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
"biomes": ["toggleBiomes", "toggleRivers", "toggleScaleBar"],
"biomes": ["toggleBiomes", "toggleIce", "toggleRivers", "toggleScaleBar"],
"heightmap": ["toggleHeight", "toggleRivers"],
"physical": ["toggleCoordinates", "toggleHeight", "toggleRivers", "toggleScaleBar"],
"poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"poi": ["toggleBorders", "toggleHeight", "toggleIce", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"military": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleMilitary", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
"landmass": ["toggleScaleBar"]
}
@ -345,7 +345,7 @@ function drawBiomes() {
const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
const chain = connectVertices(edgeVerticle, b);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v]);
const points = clipPoly(chain.map(v => vertices.p[v]), 1);
paths[b] += "M" + points.join("L") + "Z";
}
@ -456,38 +456,77 @@ function drawCells() {
cells.append("path").attr("d", path);
}
function drawSeaIce() {
const seaIce = viewbox.append("g").attr("id", "seaIce").attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("filter", "url(#dropShadow05)");//.attr("opacity", .8);
for (const i of grid.cells.i) {
const t = grid.cells.temp[i] ;
if (t > 2) continue;
if (t > -5 && grid.cells.h[i] >= 20) continue;
if (t < -5) drawpolygon(i);
if (P(normalize(t, -5.5, 2.5))) continue; // t[-5; 2]
const size = t < -14 ? 0 : t > -6 ? (7 + t) / 10 : (15 + t) / 100; // [0; 1], where 0 = full size, 1 = zero size
resizePolygon(i, rn(size * (.2 + rand() * .9), 2));
function toggleIce() {
if (!layerIsOn("toggleIce")) {
turnButtonOn("toggleIce");
$('#ice').fadeIn();
if (event && isCtrlClick(event)) editStyle("ice");
} else {
if (event && isCtrlClick(event)) {editStyle("ice"); return;}
$('#ice').fadeOut();
turnButtonOff("toggleIce");
}
}
// -9: .06
// -8: .07
// -7: .08
// -6: .09
function drawIce() {
const cells = grid.cells, vertices = grid.vertices, n = cells.i.length, temp = cells.temp, h = cells.h;
const used = new Uint8Array(cells.i.length);
Math.seedrandom(seed);
// -5: .2
// -4: .3
// -3: .4
// -2: .5
// -1: .6
// 0: .7
const shieldMin = -6; // min temp to form ice shield (glacier)
const icebergMax = 2; // max temp to form an iceberg
function drawpolygon(i) {
seaIce.append("polygon").attr("points", getGridPolygon(i));
for (const i of grid.cells.i) {
const t = temp[i];
if (t > icebergMax) continue; // too warm: no ice
if (t > shieldMin && h[i] >= 20) continue; // non-glacier land: no ice
if (t <= shieldMin) {
// very cold: ice shield
if (used[i]) continue; // already rendered
const onborder = cells.c[i].some(n => temp[n] > shieldMin);
if (!onborder) continue; // need to start from onborder cell
const vertex = cells.v[i].find(v => vertices.c[v].some(i => temp[i] > shieldMin));
const chain = connectVertices(vertex);
if (chain.length < 3) continue;
const points = clipPoly(chain.map(v => vertices.p[v]));
ice.append("polygon").attr("points", points).attr("type", "iceShield");
continue;
}
// mildly cold: iceberd
if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells
if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers
let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size
if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers
size = Math.min(size * (.4 + rand() * 1.2), .95); // randomize iceberd size
resizePolygon(i, size);
}
function resizePolygon(i, s) {
const c = grid.points[i];
const points = getGridPolygon(i).map(p => [p[0] + (c[0]-p[0]) * s, p[1] + (c[1]-p[1]) * s]);
seaIce.append("polygon").attr("points", points);
const points = getGridPolygon(i).map(p => [(p[0] + (c[0]-p[0]) * s)|0, (p[1] + (c[1]-p[1]) * s)|0]);
ice.append("polygon").attr("points", points).attr("cell", i).attr("size", rn(1-s, 2));
}
// connect vertices to chain
function connectVertices(start) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = last(chain); // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => temp[c] <= shieldMin).forEach(c => used[c] = 1);
const c0 = c[0] >= n || temp[c[0]] > shieldMin;
const c1 = c[1] >= n || temp[c[1]] > shieldMin;
const c2 = c[2] >= n || temp[c[2]] > shieldMin;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
return chain;
}
}

View file

@ -27,7 +27,7 @@ function editMarker() {
document.getElementById("markerIconSize").addEventListener("input", changeIconSize);
document.getElementById("markerIconShiftX").addEventListener("input", changeIconShiftX);
document.getElementById("markerIconShiftY").addEventListener("input", changeIconShiftY);
document.getElementById("markerIconCustom").addEventListener("input", applyCustomUnicodeIcon);
document.getElementById("markerIconSelect").addEventListener("click", selectMarkerIcon);
document.getElementById("markerStyle").addEventListener("click", toggleStyleSection);
document.getElementById("markerSize").addEventListener("input", changeMarkerSize);
@ -73,13 +73,7 @@ function editMarker() {
markerIconFill.value = icon.attr("fill");
markerToggleBubble.className = symbol.select("circle").attr("opacity") === "0" ? "icon-info" : "icon-info-circled";
const table = document.getElementById("markerIconTable");
let selected = table.getElementsByClassName("selected");
if (selected.length) selected[0].removeAttribute("class");
selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt());
if (selected.length) selected[0].className = "selected";
markerIconCustom.value = selected.length ? "" : icon.text();
markerIconSelect.innerHTML = icon.text();
}
function toggleGroupSection() {
@ -162,242 +156,20 @@ function editMarker() {
if (markerIconSection.style.display === "inline-block") {
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
markerIconSection.style.display = "none";
markerIconSelect.style.display = "none";
} else {
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
markerIconSection.style.display = "inline-block";
if (!markerIconTable.innerHTML) drawIconsList();
markerIconSelect.style.display = "inline-block";
}
}
function drawIconsList() {
const icons = [
// emoticons in FF:
["2693", "⚓", "Anchor"],
["26EA", "⛪", "Church"],
["1F3EF", "🏯", "Japanese Castle"],
["1F3F0", "🏰", "Castle"],
["1F5FC", "🗼", "Tower"],
["1F3E0", "🏠", "House"],
["1F3AA", "🎪", "Tent"],
["1F3E8", "🏨", "Hotel"],
["1F4B0", "💰", "Money bag"],
["1F6A8", "🚨", "Revolving Light"],
["1F309", "🌉", "Bridge at Night"],
["1F5FB", "🗻", "Mountain"],
["1F30B", "🌋", "Volcano"],
["270A", "✊", "Raised Fist"],
["1F44A", "👊", "Oncoming Fist"],
["1F4AA", "💪", "Flexed Biceps"],
["1F47C", "👼", "Baby Angel"],
["1F40E", "🐎", "Horse"],
["1F434", "🐴", "Horse Face"],
["1F42E", "🐮", "Cow"],
["1F43A", "🐺", "Wolf Face"],
["1F435", "🐵", "Monkey face"],
["1F437", "🐷", "Pig face"],
["1F414", "🐔", "Chicken"],
["1F411", "🐑", "Ewe"],
["1F42B", "🐫", "Camel"],
["1F418", "🐘", "Elephant"],
["1F422", "🐢", "Turtle"],
["1F40C", "🐌", "Snail"],
["1F40D", "🐍", "Snake"],
["1F41D", "🐝", "Honeybee"],
["1F41C", "🐜", "Ant"],
["1F41B", "🐛", "Bug"],
["1F426", "🐦", "Bird"],
["1F438", "🐸", "Frog Face"],
["1F433", "🐳", "Whale"],
["1F42C", "🐬", "Dolphin"],
["1F420", "🐟", "Fish"],
["1F480", "💀", "Skull"],
["1F432", "🐲", "Dragon Head"],
["1F479", "👹", "Ogre"],
["1F47A", "👺", "Goblin"],
["1F47B", "👻", "Ghost"],
["1F47E", "👾", "Alien"],
["1F383", "🎃", "Jack-O-Lantern"],
["1F384", "🎄", "Christmas Tree"],
["1F334", "🌴", "Palm"],
["1F335", "🌵", "Cactus"],
["2618", "☘️", "Shamrock"],
["1F340", "🍀", "Four Leaf Clover"],
["1F341", "🍁", "Maple Leaf"],
["1F33F", "🌿", "Herb"],
["1F33E", "🌾", "Sheaf"],
["1F344", "🍄", "Mushroom"],
["1F374", "🍴", "Fork and knife"],
["1F372", "🍲", "Food"],
["1F35E", "🍞", "Bread"],
["1F357", "🍗", "Poultry leg"],
["1F347", "🍇", "Grapes"],
["1F34F", "🍏", "Apple"],
["1F352", "🍒", "Cherries"],
["1F36F", "🍯", "Honey pot"],
["1F37A", "🍺", "Beer"],
["1F37B", "🍻", "Beers"],
["1F377", "🍷", "Wine glass"],
["1F3BB", "🎻", "Violin"],
["1F3B8", "🎸", "Guitar"],
["1F52A", "🔪", "Knife"],
["1F52B", "🔫", "Pistol"],
["1F4A3", "💣", "Bomb"],
["1F4A5", "💥", "Collision"],
["1F4A8", "💨", "Dashing away"],
["1F301", "🌁", "Foggy"],
["2744", "❄️", "Snowflake"],
["26A1", "⚡", "Electricity"],
["1F320", "🌠", "Shooting star"],
["1F319", "🌙", "Crescent moon"],
["1F525", "🔥", "Fire"],
["1F4A7", "💧", "Droplet"],
["1F30A", "🌊", "Wave"],
["23F0", "⏰", "Alarm Clock"],
["231B", "⌛", "Hourglass"],
["1F3C6", "🏆", "Goblet"],
["26F2", "⛲", "Fountain"],
["26F5", "⛵", "Sailboat"],
["26FA", "⛺", "Campfire"],
["2764", "❤", "Red Heart"],
["1F498", "💘", "Heart With Arrow"],
["1F489", "💉", "Syringe"],
["1F4D5", "📕", "Closed Book"],
["1F4D6", "📚", "Books"],
["1F381", "🎁", "Wrapped Gift"],
["1F3AF", "🎯", "Archery"],
["1F52E", "🔮", "Magic ball"],
["1F3AD", "🎭", "Performing arts"],
["1F3A8", "🎨", "Artist palette"],
["1F457", "👗", "Dress"],
["1F392", "🎒", "Backpack"],
["1F451", "👑", "Crown"],
["1F48D", "💍", "Ring"],
["1F48E", "💎", "Gem"],
["1F514", "🔔", "Bell"],
["1F3B2", "🎲", "Die"],
// black and white icons in FF:
["26A0", "⚠", "Alert"],
["2317", "⌗", "Hash"],
["2318", "⌘", "POI"],
["2307", "⌇", "Wavy"],
["27F1", "⟱", "Downwards Quadruple"],
["21E6", "⇦", "Left arrow"],
["21E7", "⇧", "Top arrow"],
["21E8", "⇨", "Right arrow"],
["21E9", "⇩", "Left arrow"],
["21F6", "⇶", "Three arrows"],
["2699", "⚙", "Gear"],
["269B", "⚛", "Atom"],
["2680", "⚀", "Die1"],
["2681", "⚁", "Die2"],
["2682", "⚂", "Die3"],
["2683", "⚃", "Die4"],
["2684", "⚄", "Die5"],
["2685", "⚅", "Die6"],
["26B4", "⚴", "Pallas"],
["26B5", "⚵", "Juno"],
["26B6", "⚶", "Vesta"],
["26B7", "⚷", "Chiron"],
["26B8", "⚸", "Lilith"],
["263F", "☿", "Mercury"],
["2640", "♀", "Venus"],
["2641", "♁", "Earth"],
["2642", "♂", "Mars"],
["2643", "♃", "Jupiter"],
["2644", "♄", "Saturn"],
["2645", "♅", "Uranus"],
["2646", "♆", "Neptune"],
["2647", "♇", "Pluto"],
["26B3", "⚳", "Ceres"],
["2654", "♔", "Chess king"],
["2655", "♕", "Chess queen"],
["2656", "♖", "Chess rook"],
["2657", "♗", "Chess bishop"],
["2658", "♘", "Chess knight"],
["2659", "♙", "Chess pawn"],
["2660", "♠", "Spade"],
["2663", "♣", "Club"],
["2665", "♥", "Heart"],
["2666", "♦", "Diamond"],
["2698", "⚘", "Flower"],
["2625", "☥", "Ankh"],
["2626", "☦", "Orthodox"],
["2627", "☧", "Chi Rho"],
["2628", "☨", "Lorraine"],
["2629", "☩", "Jerusalem"],
["2670", "♰", "Syriac cross"],
["2020", "†", "Dagger"],
["262A", "☪", "Muslim"],
["262D", "☭", "Soviet"],
["262E", "☮", "Peace"],
["262F", "☯", "Yin yang"],
["26A4", "⚤", "Heterosexuality"],
["26A2", "⚢", "Female homosexuality"],
["26A3", "⚣", "Male homosexuality"],
["26A5", "⚥", "Male and female"],
["26AD", "⚭", "Rings"],
["2690", "⚐", "White flag"],
["2691", "⚑", "Black flag"],
["263C", "☼", "Sun"],
["263E", "☾", "Moon"],
["2668", "♨", "Hot springs"],
["2600", "☀", "Black sun"],
["2601", "☁", "Cloud"],
["2602", "☂", "Umbrella"],
["2603", "☃", "Snowman"],
["2604", "☄", "Comet"],
["2605", "★", "Black star"],
["2606", "☆", "White star"],
["269D", "⚝", "Outlined star"],
["2618", "☘", "Shamrock"],
["21AF", "↯", "Lightning"],
["269C", "⚜", "FleurDeLis"],
["2622", "☢", "Radiation"],
["2623", "☣", "Biohazard"],
["2620", "☠", "Skull"],
["2638", "☸", "Dharma"],
["2624", "☤", "Caduceus"],
["2695", "⚕", "Aeculapius staff"],
["269A", "⚚", "Hermes staff"],
["2697", "⚗", "Alembic"],
["266B", "♫", "Music"],
["2702", "✂", "Scissors"],
["2696", "⚖", "Scales"],
["2692", "⚒", "Hammer and pick"],
["2694", "⚔", "Swords"]
];
const table = document.getElementById("markerIconTable");
table.addEventListener("click", selectIcon, false);
table.addEventListener("mouseover", hoverIcon, false);
let row = "";
for (let i=0; i < icons.length; i++) {
if (i%16 === 0) row = table.insertRow(0);
const cell = row.insertCell(0);
const icon = String.fromCodePoint(parseInt(icons[i][0], 16));
cell.innerHTML = icon;
cell.id = "markerIcon" + icon.codePointAt();
cell.dataset.desc = icons[i][2];
}
}
function selectIcon(e) {
if (e.target !== e.currentTarget) {
const table = document.getElementById("markerIconTable");
const selected = table.getElementsByClassName("selected");
if (selected.length) selected[0].removeAttribute("class");
e.target.className = "selected";
function selectMarkerIcon() {
selectIcon(this.innerHTML, v => {
this.innerHTML = v;
const id = elSelected.attr("data-id");
const icon = e.target.innerHTML;
d3.select("#defs-markers").select(id).select("text").text(icon);
}
e.stopPropagation();
}
function hoverIcon(e) {
if (e.target !== e.currentTarget) tip(e.target.innerHTML + " " + e.target.dataset.desc);
e.stopPropagation();
d3.select("#defs-markers").select(id).select("text").text(v);
});
}
function changeIconSize() {

View file

@ -183,7 +183,7 @@ function overviewMilitary() {
position: {my: "center", at: "center", of: "svg"},
buttons: {
Apply: applyMilitaryOptions,
Add: () => addUnitLine({name: "custom"+militaryOptionsTable.rows.length, rural: .2, urban: .5, crew: 1, type: "melee"}),
Add: () => addUnitLine({icon: "🛡️", name: "custom"+militaryOptionsTable.rows.length, rural: .2, urban: .5, crew: 1, power: 1, type: "melee"}),
Restore: restoreDefaultUnits,
Cancel: function() {$(this).dialog("close");}
}, open: function() {
@ -200,19 +200,20 @@ function overviewMilitary() {
}
function addUnitLine(u) {
const row = `<tr>
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" type="number" min=1 step=1 value="${u.crew}"></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>
<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>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>
</tr>`;
table.insertAdjacentHTML("beforeend", row);
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector("button").addEventListener("click", function(e) {selectIcon(this.innerHTML, v => this.innerHTML = v)});
table.appendChild(row);
}
function restoreDefaultUnits() {
@ -230,8 +231,14 @@ function overviewMilitary() {
$("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => {
const [name, rural, urban, crew, type, separate] = Array.from(r.querySelectorAll("input, select")).map(d => d.type === "checkbox" ? d.checked : d.value);
return {name:names[i], rural:+rural||0, urban:+urban||0, crew:+crew||0, type, separate:+separate||0};
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;
});
return {icon, name:names[i], rural, urban, crew, power, type, separate};
});
localStorage.setItem("military", JSON.stringify(options.military));
Military.generate();

View file

@ -14,8 +14,7 @@ function editRegiment(selector) {
$("#regimentEditor").dialog({
title: "Edit Regiment", resizable: false, close: closeEditor,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeEditor
position: {my: "left top", at: "left+10 top+10", of: "#map"}
});
if (modules.editRegiment) return;
@ -27,6 +26,7 @@ function editRegiment(selector) {
document.getElementById("regimentName").addEventListener("change", changeName);
document.getElementById("regimentEmblem").addEventListener("input", changeEmblem);
document.getElementById("regimentEmblemSelect").addEventListener("click", selectEmblem);
document.getElementById("regimentAttack").addEventListener("click", toggleAttack);
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
document.getElementById("regimentLegend").addEventListener("click", editLegend);
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
@ -92,50 +92,15 @@ function editRegiment(selector) {
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
}
function selectEmblem() {
selectIcon(regimentEmblem.value, v => {regimentEmblem.value = v; changeEmblem()});
}
function changeEmblem() {
const emblem = document.getElementById("regimentEmblem").value;
regiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
}
function selectEmblem() {
const emblems = ["⚔️","🏹","🐴","💣","🌊","🎯","⚓","🔮","📯","🛡️","👑",
"☠️","🎆","🗡️","⛏️","🔥","🐾","🎪","🏰","⚜️","⛓️","❤️","📜","🔱","🌈","🌠","💥","☀️","🍀",
"🔰","🕸️","⚗️","☣️","☢️","🎖️","⚕️","☸️","✡️","🚩","🏳️","🏴","🌈","💪","✊","👊","🤜","🤝","🙏","🧙","💂","🤴","🧛","🧟","🧞","🧝",
"🦄","🐲","🐉","🐎","🦓","🐺","🦊","🐱","🐈","🦁","🐯","🐅","🐆","🐕","🦌","🐵","🐒","🦍",
"🦅","🕊️","🐓","🦇","🐦","🦉","🐮","🐄","🐂","🐃","🐷","🐖","🐗","🐏","🐑","🐐","🐫","🦒","🐘","🦏",
"🐭","🐁","🐀","🐹","🐰","🐇","🦔","🐸","🐊","🐢","🦎","🐍","🐳","🐬","🦈","🐙","🦑","🐌","🦋","🐜","🐝","🐞","🦗","🕷️","🦂","🦀"];
alertMessage.innerHTML = "";
const container = document.createElement("div");
container.style.userSelect = "none";
container.style.cursor = "pointer";
container.style.fontSize = "2em";
container.style.width = "47vw";
container.innerHTML = emblems.map(i => `<span>${i}</span>`).join("");
container.addEventListener("mouseover", e => showTip(e), false);
container.addEventListener("click", e => clickEmblem(e), false);
alertMessage.appendChild(container);
$("#alert").dialog({
resizable: false, width: fitContent(), title: "Select emblem",
buttons: {
Emojipedia: function() {openURL("https://emojipedia.org/");},
Close: function() {$(this).dialog("close");}
}
});
function showTip(e) {
if (e.target.tagName !== "SPAN") return;
tip(`Click to select ${e.target.innerHTML} emblem`);
}
function clickEmblem(e) {
if (e.target.tagName !== "SPAN") return;
document.getElementById("regimentEmblem").value = e.target.innerHTML;
changeEmblem();
}
}
function changeUnit() {
const u = this.dataset.u;
const reg = regiment();
@ -200,6 +165,60 @@ function editRegiment(selector) {
toggleAdd();
}
function toggleAttack() {
document.getElementById("regimentAttack").classList.toggle("pressed");
if (document.getElementById("regimentAttack").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attackRegimentOnClick);
tip("Click on another regiment to initiate battle", true);
armies.selectAll(":scope > g").classed("draggable", false);
} else {
clearMainTip();
armies.selectAll(":scope > g").classed("draggable", true);
viewbox.on("click", clicked).style("cursor", "default");
}
}
function attackRegimentOnClick() {
const target = d3.event.target, regSelected = target.parentElement, army = regSelected.parentElement;
const oldState = +elSelected.dataset.state, newState = +regSelected.dataset.state;
if (army.parentElement.id !== "armies") {tip("Please click on a regiment to attack", false, "error"); return;}
if (regSelected === elSelected) {tip("Regiment cannot attack itself", false, "error"); return;}
if (oldState === newState) {tip("Cannot attack fraternal regiment", false, "error"); return;}
const attacker = regiment();
const defender = pack.states[regSelected.dataset.state].military.find(r => r.i == regSelected.dataset.id);
if (!attacker.a || !defender.a) {tip("Regiment has no troops to battle", false, "error"); return;}
// move attacked to defender
const duration = Math.hypot(attacker.x - defender.x, attacker.y - defender.y) * 6;
const x = attacker.x = defender.x;
const y = attacker.y = defender.y + 8;
const size = +armies.attr("box-size");
const w = attacker.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
const attack = d3.transition().delay(duration).duration(800).ease(d3.easeSinInOut).on("end", () => showBattleScreen(attacker, defender));
d3.select(elSelected.querySelector("rect")).transition(move).attr("x", x1(x)).attr("y", y1(y));
d3.select(elSelected.querySelector("text")).transition(move).attr("x", x).attr("y", y);
d3.select(elSelected.querySelectorAll("rect")[1]).transition(move).attr("x", x1(x)-h).attr("y", y1(y));
d3.select(elSelected.querySelector(".regimentIcon")).transition(move).attr("x", x1(x)-size).attr("y", y);
// battle icon
svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2)
.text("⚔️").attr("font-size", 0).attr("opacity", 1)
.style("dominant-baseline", "central").style("text-anchor", "middle")
.transition(attack).attr("font-size", 1000).attr("opacity", 0).remove();
tip("", false);
$("#regimentEditor").dialog("close");
}
function toggleAttach() {
document.getElementById("regimentAttach").classList.toggle("pressed");
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
@ -289,7 +308,7 @@ function editRegiment(selector) {
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const baseRect = this.querySelectorAll("rect")[0];
const baseRect = this.querySelector("rect");
const text = this.querySelector("text");
const iconRect = this.querySelectorAll("rect")[1];
const icon = this.querySelector(".regimentIcon");
@ -330,6 +349,7 @@ function editRegiment(selector) {
armies.selectAll("g>g").call(d3.drag().on("drag", null));
viewbox.selectAll("g#regimentBase").remove();
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttack").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");
restoreDefaultEvents();
elSelected = null;

File diff suppressed because one or more lines are too long

View file

@ -65,6 +65,7 @@ function processFeatureRegeneration(event, button) {
if (button === "regenerateProvinces") regenerateProvinces(); else
if (button === "regenerateReligions") regenerateReligions(); else
if (button === "regenerateMilitary") regenerateMilitary(); else
if (button === "regenerateIce") regenerateIce(); else
if (button === "regenerateMarkers") regenerateMarkers(event); else
if (button === "regenerateZones") regenerateZones(event);
}
@ -86,7 +87,7 @@ function recalculatePopulation() {
if (!b.i || b.removed) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
@ -249,6 +250,12 @@ function regenerateMilitary() {
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
}
function regenerateIce() {
if (!layerIsOn("toggleIce")) toggleIce();
ice.selectAll("*").remove();
drawIce();
}
function regenerateMarkers(event) {
if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, .5, .3, 5, 2));

View file

@ -370,11 +370,16 @@ function biased(min, max, ex) {
}
// return array of values common for both array a and array b
function intersect(a, b) {
function common(a, b) {
const setB = new Set(b);
return [...new Set(a)].filter(a => setB.has(a));
}
// clip polygon by graph bbox
function clipPoly(points, secure = false) {
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}
// check if char is vowel
function vowel(c) {
return "aeiouy".includes(c);
@ -610,5 +615,5 @@ void function() {
cancel.addEventListener("click", () => prompt.style.display = "none");
}()
// localStorageDB
!function(){function e(t,o){return n?void(n.transaction("s").objectStore("s").get(t).onsuccess=function(e){var t=e.target.result&&e.target.result.v||null;o(t)}):void setTimeout(function(){e(t,o)},100)}var t=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;if(!t)return void console.error("indexDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){console.error("indexedDB request error"),console.log(e)},r.onupgradeneeded=function(e){n=null;var t=e.target.result.createObjectStore("s",{keyPath:"k"});t.transaction.oncomplete=function(e){n=e.target.db}},window.ldb={get:e,set:function(e,t){o.k=e,o.v=t,n.transaction("s","readwrite").objectStore("s").put(o)}}}();
// indexedDB; ldb object
!function(){function e(t,o){return n?void(n.transaction("s").objectStore("s").get(t).onsuccess=function(e){var t=e.target.result&&e.target.result.v||null;o(t)}):void setTimeout(function(){e(t,o)},100)}var t=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;if(!t)return void console.error("indexedDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){console.error("indexedDB request error"),console.log(e)},r.onupgradeneeded=function(e){n=null;var t=e.target.result.createObjectStore("s",{keyPath:"k"});t.transaction.oncomplete=function(e){n=e.target.db}},window.ldb={get:e,set:function(e,t){o.k=e,o.v=t,n.transaction("s","readwrite").objectStore("s").put(o)}}}();

3
run_php_server.bat Normal file
View file

@ -0,0 +1,3 @@
start chrome.exe http://localhost:8000/
@echo off
php -S localhost:8000

6
upload.php Normal file
View file

@ -0,0 +1,6 @@
<?php
header("Content-type: text/plain");
echo "received:";
var_dump(file_get_contents('php://input'));
print_r($_POST);
?>