mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 18:11:24 +01:00
Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into submap-refactoring
This commit is contained in:
commit
fb48f5d57a
18 changed files with 511 additions and 73 deletions
36
index.html
36
index.html
|
|
@ -4964,7 +4964,18 @@
|
|||
>Model:
|
||||
<select id="aiGeneratorModel"></select>
|
||||
</label>
|
||||
|
||||
<label for="aiGeneratorTemperature"
|
||||
>Temperature:
|
||||
<input id="aiGeneratorTemperature" type="number" min="0" max="2" placeholder="1.2" class="icon-key" />
|
||||
<a
|
||||
href="https://platform.openai.com/docs/api-reference/chat/create#chat-create-temperature"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="icon-help-circled"
|
||||
style="text-decoration: none"
|
||||
data-tip="Between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic."
|
||||
></a>
|
||||
</label>
|
||||
<label for="aiGeneratorKey"
|
||||
>Key:
|
||||
<input id="aiGeneratorKey" placeholder="Enter OpenAI API key" class="icon-key" />
|
||||
|
|
@ -8033,7 +8044,6 @@
|
|||
<script src="libs/jquery-ui.min.js"></script>
|
||||
<script src="versioning.js"></script>
|
||||
<script src="libs/d3.min.js"></script>
|
||||
<script src="libs/priority-queue.min.js"></script>
|
||||
<script src="libs/flatqueue.js"></script>
|
||||
<script src="libs/delaunator.min.js"></script>
|
||||
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
||||
|
|
@ -8048,7 +8058,7 @@
|
|||
<script src="utils/numberUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/polyfills.js?v=1.99.00"></script>
|
||||
<script src="utils/probabilityUtils.js?v=1.99.05"></script>
|
||||
<script src="utils/stringUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/stringUtils.js?v=1.105.19"></script>
|
||||
<script src="utils/languageUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/unitUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/pathUtils.js?v=1.105.8"></script>
|
||||
|
|
@ -8064,14 +8074,14 @@
|
|||
<script src="modules/lakes.js?v=1.99.00"></script>
|
||||
<script src="modules/biomes.js?v=1.99.00"></script>
|
||||
<script src="modules/names-generator.js?v=1.105.11"></script>
|
||||
<script src="modules/cultures-generator.js?v=1.105.19"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.105.7"></script>
|
||||
<script src="modules/provinces-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/cultures-generator.js?v=1.105.21"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.105.21"></script>
|
||||
<script src="modules/provinces-generator.js?v=1.105.21"></script>
|
||||
<script src="modules/routes-generator.js?v=1.104.10"></script>
|
||||
<script src="modules/religions-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/religions-generator.js?v=1.105.21"></script>
|
||||
<script src="modules/military-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/markers-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/zones-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/zones-generator.js?v=1.105.21"></script>
|
||||
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/resample.js?v=1.105.13"></script>
|
||||
<script src="libs/alea.min.js?v1.105.0"></script>
|
||||
|
|
@ -8110,10 +8120,10 @@
|
|||
<script defer src="modules/ui/burg-editor.js?v=1.102.00"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.99.09"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.105.22"></script>
|
||||
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.105.18"></script>
|
||||
<script defer src="modules/ui/burgs-overview.js?v=1.105.19"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
|
||||
<script defer src="modules/ui/burgs-overview.js?v=1.105.15"></script>
|
||||
<script defer src="modules/ui/routes-overview.js?v=1.104.3"></script>
|
||||
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
|
||||
|
|
@ -8131,8 +8141,8 @@
|
|||
<script defer src="libs/rgbquant.min.js"></script>
|
||||
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
|
||||
<script defer src="modules/io/save.js?v=1.100.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.105.19"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.99.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.105.17"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.105.19"></script>
|
||||
<script defer src="modules/io/export.js?v=1.100.00"></script>
|
||||
|
||||
<script defer src="modules/renderers/draw-features.js?v=1.105.19"></script>
|
||||
|
|
|
|||
1
libs/priority-queue.min.js
vendored
1
libs/priority-queue.min.js
vendored
File diff suppressed because one or more lines are too long
4
main.js
4
main.js
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
// set debug options
|
||||
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
|
||||
const DEBUG = localStorage.getItem("debug");
|
||||
const DEBUG = JSON.safeParse(localStorage.getItem("debug")) || {};
|
||||
const INFO = true;
|
||||
const TIME = true;
|
||||
const WARN = true;
|
||||
|
|
@ -915,7 +915,7 @@ function calculateTemperatures() {
|
|||
const [, y] = grid.points[rowCellId];
|
||||
const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // [90; -90]
|
||||
const tempSeaLevel = calculateSeaLevelTemp(rowLatitude);
|
||||
DEBUG && console.info(`${rn(rowLatitude)}° sea temperature: ${rn(tempSeaLevel)}°C`);
|
||||
DEBUG.temperature && console.info(`${rn(rowLatitude)}° sea temperature: ${rn(tempSeaLevel)}°C`);
|
||||
|
||||
for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) {
|
||||
const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]);
|
||||
|
|
|
|||
|
|
@ -286,7 +286,8 @@ window.BurgsAndStates = (() => {
|
|||
const {cells, states, cultures, burgs} = pack;
|
||||
|
||||
cells.state = cells.state || new Uint16Array(cells.i.length);
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
|
||||
|
|
@ -307,12 +308,13 @@ window.BurgsAndStates = (() => {
|
|||
cells.state[capitalCell] = state.i;
|
||||
const cultureCenter = cultures[state.culture].center;
|
||||
const b = cells.biome[cultureCenter]; // state native biome
|
||||
queue.queue({e: state.center, p: 0, s: state.i, b});
|
||||
queue.push({e: state.center, p: 0, s: state.i, b}, 0);
|
||||
cost[state.center] = 1;
|
||||
}
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
|
||||
const {e, p, s, b} = next;
|
||||
const {type, culture} = states[s];
|
||||
|
||||
|
|
@ -335,7 +337,7 @@ window.BurgsAndStates = (() => {
|
|||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, s, b});
|
||||
queue.push({e, p: totalCost, s, b}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ window.Cultures = (function () {
|
|||
TIME && console.time("expandCultures");
|
||||
const {cells, cultures} = pack;
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
|
||||
|
|
@ -538,11 +538,11 @@ window.Cultures = (function () {
|
|||
|
||||
for (const culture of cultures) {
|
||||
if (!culture.i || culture.removed || culture.lock) continue;
|
||||
queue.queue({cellId: culture.center, cultureId: culture.i, priority: 0});
|
||||
queue.push({cellId: culture.center, cultureId: culture.i, priority: 0}, 0);
|
||||
}
|
||||
|
||||
while (queue.length) {
|
||||
const {cellId, priority, cultureId} = queue.dequeue();
|
||||
const {cellId, priority, cultureId} = queue.pop();
|
||||
const {type, expansionism} = cultures[cultureId];
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
|
|
@ -566,7 +566,7 @@ window.Cultures = (function () {
|
|||
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
|
||||
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.queue({cellId: neibCellId, cultureId, priority: totalCost});
|
||||
queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ window.Cloud = (function () {
|
|||
|
||||
async save(fileName, contents) {
|
||||
const resp = await this.call("filesUpload", {path: "/" + fileName, contents});
|
||||
DEBUG && console.info("Dropbox response:", resp);
|
||||
DEBUG.cloud && console.info("Dropbox response:", resp);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ window.Cloud = (function () {
|
|||
|
||||
// Callback function for auth window
|
||||
async setDropBoxToken(token) {
|
||||
DEBUG && console.info("Access token:", token);
|
||||
DEBUG.cloud && console.info("Access token:", token);
|
||||
setToken(this.name, token);
|
||||
await this.connect(token);
|
||||
this.authWindow.close();
|
||||
|
|
@ -131,7 +131,7 @@ window.Cloud = (function () {
|
|||
allow_download: true
|
||||
};
|
||||
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings});
|
||||
DEBUG && console.info("Dropbox link object:", resp.result);
|
||||
DEBUG.cloud && console.info("Dropbox link object:", resp.result);
|
||||
return resp.result.url;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async function quickLoad() {
|
|||
async function loadFromDropbox() {
|
||||
const mapPath = byId("loadFromDropboxSelect")?.value;
|
||||
|
||||
DEBUG && console.info("Loading map from Dropbox:", mapPath);
|
||||
console.info("Loading map from Dropbox:", mapPath);
|
||||
const blob = await Cloud.providers.dropbox.load(mapPath);
|
||||
uploadMap(blob);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,18 +77,18 @@ window.Provinces = (function () {
|
|||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
const {e, p, province, state} = queue.pop();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
|
@ -103,7 +103,7 @@ window.Provinces = (function () {
|
|||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land) provinceIds[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, province, state});
|
||||
queue.push({e, province, state, p: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -158,9 +158,9 @@ window.Provinces = (function () {
|
|||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
queue.push({e: center, p: 0}, 0);
|
||||
while (queue.length) {
|
||||
const {e, p} = queue.dequeue();
|
||||
const {e, p} = queue.pop();
|
||||
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
|
|
@ -173,7 +173,7 @@ window.Provinces = (function () {
|
|||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
queue.push({e: nextCellId, p: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -216,15 +216,15 @@ window.Provinces = (function () {
|
|||
// check if there is a land way within the same state between two cells
|
||||
function isPassable(from, to) {
|
||||
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
||||
const queue = [from],
|
||||
const passableQueue = [from],
|
||||
used = new Uint8Array(cells.i.length),
|
||||
state = cells.state[from];
|
||||
while (queue.length) {
|
||||
const current = queue.pop();
|
||||
while (passableQueue.length) {
|
||||
const current = passableQueue.pop();
|
||||
if (current === to) return true; // way is found
|
||||
cells.c[current].forEach(c => {
|
||||
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
|
||||
queue.push(c);
|
||||
passableQueue.push(c);
|
||||
used[c] = 1;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -695,7 +695,7 @@ window.Religions = (function () {
|
|||
const {cells, routes} = pack;
|
||||
const religionIds = spreadFolkReligions(religions);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
// limit cost for organized religions growth
|
||||
|
|
@ -705,14 +705,14 @@ window.Religions = (function () {
|
|||
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
|
||||
.forEach(r => {
|
||||
religionIds[r.center] = r.i;
|
||||
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]});
|
||||
queue.push({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}, 0);
|
||||
cost[r.center] = 1;
|
||||
});
|
||||
|
||||
const religionsMap = new Map(religions.map(r => [r.i, r]));
|
||||
|
||||
while (queue.length) {
|
||||
const {e: cellId, p, r, s: state} = queue.dequeue();
|
||||
const {e: cellId, p, r, s: state} = queue.pop();
|
||||
const {culture, expansion, expansionism} = religionsMap.get(r);
|
||||
|
||||
cells.c[cellId].forEach(nextCell => {
|
||||
|
|
@ -732,7 +732,7 @@ window.Religions = (function () {
|
|||
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
|
||||
cost[nextCell] = totalCost;
|
||||
|
||||
queue.queue({e: nextCell, p: totalCost, r, s: state});
|
||||
queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,10 @@ function drawStateLabels(list) {
|
|||
const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
|
||||
if (ray1.x > ray2.x) pathPoints.reverse();
|
||||
|
||||
DEBUG && drawPoint(state.pole, {color: "black", radius: 1});
|
||||
DEBUG && drawPath(pathPoints, {color: "black", width: 0.2});
|
||||
if (DEBUG.stateLabels) {
|
||||
drawPoint(state.pole, {color: "black", radius: 1});
|
||||
drawPath(pathPoints, {color: "black", width: 0.2});
|
||||
}
|
||||
|
||||
labelPaths.push([state.i, pathPoints]);
|
||||
}
|
||||
|
|
@ -163,9 +165,11 @@ function drawStateLabels(list) {
|
|||
const offset1 = [x + -dy * offset, y + dx * offset];
|
||||
const offset2 = [x + dy * offset, y + -dx * offset];
|
||||
|
||||
DEBUG && drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
|
||||
DEBUG && drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
|
||||
DEBUG && drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
|
||||
if (DEBUG.stateLabels) {
|
||||
drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
|
||||
drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
|
||||
drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
|
||||
}
|
||||
|
||||
const inState = isInsideState(x, y) && isInsideState(...offset1) && isInsideState(...offset2);
|
||||
if (!inState) break;
|
||||
|
|
|
|||
407
modules/submap.js
Normal file
407
modules/submap.js
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
"use strict";
|
||||
|
||||
window.Submap = (function () {
|
||||
const isWater = (pack, id) => pack.cells.h[id] < 20;
|
||||
const inMap = (x, y) => x > 0 && x < graphWidth && y > 0 && y < graphHeight;
|
||||
|
||||
/*
|
||||
generate new map based on an existing one (resampling parentMap)
|
||||
parentMap: {seed, grid, pack} from original map
|
||||
options = {
|
||||
projection: f(Number,Number)->[Number, Number]
|
||||
function to calculate new coordinates
|
||||
inverse: g(Number,Number)->[Number, Number]
|
||||
inverse of f
|
||||
depressRivers: Bool carve out riverbeds?
|
||||
smoothHeightMap: Bool run smooth filter on heights
|
||||
addLakesInDepressions: call FMG original funtion on heightmap
|
||||
|
||||
lockMarkers: Bool Auto lock all copied markers
|
||||
lockBurgs: Bool Auto lock all copied burgs
|
||||
}
|
||||
*/
|
||||
function resample(parentMap, options) {
|
||||
const projection = options.projection;
|
||||
const inverse = options.inverse;
|
||||
const stage = s => INFO && console.info("SUBMAP:", s);
|
||||
const timeStart = performance.now();
|
||||
invokeActiveZooming();
|
||||
|
||||
// copy seed
|
||||
seed = parentMap.seed;
|
||||
Math.random = aleaPRNG(seed);
|
||||
INFO && console.group("SubMap with seed: " + seed);
|
||||
|
||||
applyGraphSize();
|
||||
grid = generateGrid();
|
||||
|
||||
drawScaleBar(scaleBar, scale);
|
||||
fitScaleBar(scaleBar, svgWidth, svgHeight);
|
||||
|
||||
const resampler = (points, qtree, f) => {
|
||||
for (const [i, [x, y]] of points.entries()) {
|
||||
const [tx, ty] = inverse(x, y);
|
||||
const oldid = qtree.find(tx, ty, Infinity)[2];
|
||||
f(i, oldid);
|
||||
}
|
||||
};
|
||||
|
||||
stage("Resampling heightmap, temperature and precipitation");
|
||||
// resample heightmap from old WorldState
|
||||
const n = grid.points.length;
|
||||
grid.cells.h = new Uint8Array(n); // heightmap
|
||||
grid.cells.temp = new Int8Array(n); // temperature
|
||||
grid.cells.prec = new Uint8Array(n); // precipitation
|
||||
const reverseGridMap = new Uint32Array(n); // cellmap from new -> oldcell
|
||||
|
||||
const oldGrid = parentMap.grid;
|
||||
// build cache old -> [newcelllist]
|
||||
const forwardGridMap = parentMap.grid.points.map(_ => []);
|
||||
resampler(grid.points, parentMap.pack.cells.q, (id, oldid) => {
|
||||
const cid = parentMap.pack.cells.g[oldid];
|
||||
grid.cells.h[id] = oldGrid.cells.h[cid];
|
||||
grid.cells.temp[id] = oldGrid.cells.temp[cid];
|
||||
grid.cells.prec[id] = oldGrid.cells.prec[cid];
|
||||
if (options.depressRivers) forwardGridMap[cid].push(id);
|
||||
reverseGridMap[id] = cid;
|
||||
});
|
||||
// TODO: add smooth/noise function for h, temp, prec n times
|
||||
|
||||
// smooth heightmap
|
||||
// smoothing should never change cell type (land->water or water->land)
|
||||
|
||||
if (options.smoothHeightMap) {
|
||||
const gcells = grid.cells;
|
||||
gcells.h.forEach((h, i) => {
|
||||
const hs = gcells.c[i].map(c => gcells.h[c]);
|
||||
hs.push(h);
|
||||
gcells.h[i] = h >= 20 ? Math.max(d3.mean(hs), 20) : Math.min(d3.mean(hs), 19);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.depressRivers) {
|
||||
stage("Generating riverbeds");
|
||||
const rbeds = new Uint16Array(grid.cells.i.length);
|
||||
|
||||
// and erode riverbeds
|
||||
parentMap.pack.rivers.forEach(r =>
|
||||
r.cells.forEach(oldpc => {
|
||||
if (oldpc < 0) return; // ignore out-of-map marker (-1)
|
||||
const oldc = parentMap.pack.cells.g[oldpc];
|
||||
const targetCells = forwardGridMap[oldc];
|
||||
if (!targetCells) throw "TargetCell shouldn't be empty";
|
||||
targetCells.forEach(c => {
|
||||
if (grid.cells.h[c] < 20) return;
|
||||
rbeds[c] = 1;
|
||||
});
|
||||
})
|
||||
);
|
||||
// raise every land cell a bit except riverbeds
|
||||
grid.cells.h.forEach((h, i) => {
|
||||
if (rbeds[i] || h < 20) return;
|
||||
grid.cells.h[i] = Math.min(h + 2, 100);
|
||||
});
|
||||
}
|
||||
|
||||
stage("Detect features, ocean and generating lakes");
|
||||
Features.markupGrid();
|
||||
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
||||
OceanLayers();
|
||||
|
||||
calculateMapCoordinates();
|
||||
calculateTemperatures();
|
||||
generatePrecipitation();
|
||||
stage("Cell cleanup");
|
||||
reGraph();
|
||||
|
||||
// remove misclassified cells
|
||||
stage("Define coastline");
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
|
||||
// Packed Graph
|
||||
const oldCells = parentMap.pack.cells;
|
||||
const forwardMap = parentMap.pack.cells.p.map(_ => []); // old -> [newcelllist]
|
||||
|
||||
const pn = pack.cells.i.length;
|
||||
const cells = pack.cells;
|
||||
cells.culture = new Uint16Array(pn);
|
||||
cells.state = new Uint16Array(pn);
|
||||
cells.burg = new Uint16Array(pn);
|
||||
cells.religion = new Uint16Array(pn);
|
||||
cells.province = new Uint16Array(pn);
|
||||
|
||||
stage("Resampling culture, state and religion map");
|
||||
for (const [id, gridCellId] of cells.g.entries()) {
|
||||
const oldGridId = reverseGridMap[gridCellId];
|
||||
if (oldGridId === undefined) {
|
||||
console.error("Can not find old cell id", reverseGridMap, "in", gridCellId);
|
||||
continue;
|
||||
}
|
||||
// find old parent's children
|
||||
const oldChildren = oldCells.i.filter(oid => oldCells.g[oid] == oldGridId);
|
||||
let oldid; // matching cell on the original map
|
||||
|
||||
if (!oldChildren.length) {
|
||||
// it *must* be a (deleted) deep ocean cell
|
||||
if (!oldGrid.cells.h[oldGridId] < 20) {
|
||||
console.error(`Warning, ${gridCellId} should be water cell, not ${oldGrid.cells.h[oldGridId]}`);
|
||||
continue;
|
||||
}
|
||||
// find replacement: closest water cell
|
||||
const [ox, oy] = cells.p[id];
|
||||
const [tx, ty] = inverse(x, y);
|
||||
oldid = oldCells.q.find(tx, ty, Infinity)[2];
|
||||
if (!oldid) {
|
||||
console.warn("Warning, no id found in quad", id, "parent", gridCellId);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// find closest children (packcell) on the parent map
|
||||
const distance = x => (x[0] - cells.p[id][0]) ** 2 + (x[1] - cells.p[id][1]) ** 2;
|
||||
let d = Infinity;
|
||||
oldChildren.forEach(oid => {
|
||||
// this should be always true, unless some algo modded the height!
|
||||
if (isWater(parentMap.pack, oid) !== isWater(pack, id)) {
|
||||
console.warn(`cell sank because of addLakesInDepressions: ${oid}`);
|
||||
}
|
||||
const [oldpx, oldpy] = oldCells.p[oid];
|
||||
const nd = distance(projection(oldpx, oldpy));
|
||||
if (isNaN(nd)) {
|
||||
console.error("Distance is not a number!", "Old point:", oldpx, oldpy);
|
||||
}
|
||||
if (nd < d) [d, oldid] = [nd, oid];
|
||||
});
|
||||
if (oldid === undefined) {
|
||||
console.warn("Warning, no match for", id, "(parent:", gridCellId, ")");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isWater(pack, id) !== isWater(parentMap.pack, oldid)) {
|
||||
WARN && console.warn("Type discrepancy detected:", id, oldid, `${pack.cells.t[id]} != ${oldCells.t[oldid]}`);
|
||||
}
|
||||
|
||||
cells.culture[id] = oldCells.culture[oldid];
|
||||
cells.state[id] = oldCells.state[oldid];
|
||||
cells.religion[id] = oldCells.religion[oldid];
|
||||
cells.province[id] = oldCells.province[oldid];
|
||||
// reverseMap.set(id, oldid)
|
||||
forwardMap[oldid].push(id);
|
||||
}
|
||||
|
||||
stage("Regenerating river network");
|
||||
Rivers.generate();
|
||||
|
||||
// biome calculation based on (resampled) grid.cells.temp and prec
|
||||
// it's safe to recalculate.
|
||||
stage("Regenerating Biome");
|
||||
Biomes.define();
|
||||
// recalculate suitability and population
|
||||
// TODO: normalize according to the base-map
|
||||
rankCells();
|
||||
|
||||
stage("Porting Cultures");
|
||||
pack.cultures = parentMap.pack.cultures;
|
||||
// fix culture centers
|
||||
const validCultures = new Set(pack.cells.culture);
|
||||
pack.cultures.forEach((c, i) => {
|
||||
if (!i) return; // ignore wildlands
|
||||
if (!validCultures.has(i)) {
|
||||
c.removed = true;
|
||||
c.center = null;
|
||||
return;
|
||||
}
|
||||
const newCenters = forwardMap[c.center];
|
||||
c.center = newCenters.length ? newCenters[0] : pack.cells.culture.findIndex(x => x === i);
|
||||
});
|
||||
|
||||
stage("Porting and locking burgs");
|
||||
copyBurgs(parentMap, projection, options);
|
||||
|
||||
// transfer states, mark states without land as removed.
|
||||
stage("Porting states");
|
||||
const validStates = new Set(pack.cells.state);
|
||||
pack.states = parentMap.pack.states;
|
||||
// keep valid states and neighbors only
|
||||
pack.states.forEach((s, i) => {
|
||||
if (!s.i || s.removed) return; // ignore removed and neutrals
|
||||
if (!validStates.has(i)) s.removed = true;
|
||||
s.neighbors = s.neighbors.filter(n => validStates.has(n));
|
||||
|
||||
// find center
|
||||
s.center = pack.burgs[s.capital].cell
|
||||
? pack.burgs[s.capital].cell // capital is the best bet
|
||||
: pack.cells.state.findIndex(x => x === i); // otherwise use the first valid cell
|
||||
});
|
||||
BurgsAndStates.getPoles();
|
||||
|
||||
// transfer provinces, mark provinces without land as removed.
|
||||
stage("Porting provinces");
|
||||
const validProvinces = new Set(pack.cells.province);
|
||||
pack.provinces = parentMap.pack.provinces;
|
||||
// mark uneccesary provinces
|
||||
pack.provinces.forEach((p, i) => {
|
||||
if (!p || p.removed) return;
|
||||
if (!validProvinces.has(i)) {
|
||||
p.removed = true;
|
||||
return;
|
||||
}
|
||||
const newCenters = forwardMap[p.center];
|
||||
p.center = newCenters.length ? newCenters[0] : pack.cells.province.findIndex(x => x === i);
|
||||
});
|
||||
Provinces.getPoles();
|
||||
|
||||
stage("Regenerating routes network");
|
||||
regenerateRoutes();
|
||||
|
||||
Rivers.specify();
|
||||
Features.specify();
|
||||
|
||||
stage("Porting military");
|
||||
for (const s of pack.states) {
|
||||
if (!s.military) continue;
|
||||
for (const m of s.military) {
|
||||
[m.x, m.y] = projection(m.x, m.y);
|
||||
[m.bx, m.by] = projection(m.bx, m.by);
|
||||
const cc = forwardMap[m.cell];
|
||||
m.cell = cc && cc.length ? cc[0] : null;
|
||||
}
|
||||
s.military = s.military.filter(m => m.cell).map((m, i) => ({...m, i}));
|
||||
}
|
||||
|
||||
stage("Copying markers");
|
||||
for (const m of pack.markers) {
|
||||
const [x, y] = projection(m.x, m.y);
|
||||
if (!inMap(x, y)) {
|
||||
Markers.deleteMarker(m.i);
|
||||
} else {
|
||||
m.x = x;
|
||||
m.y = y;
|
||||
m.cell = findCell(x, y);
|
||||
if (options.lockMarkers) m.lock = true;
|
||||
}
|
||||
}
|
||||
if (layerIsOn("toggleMarkers")) drawMarkers();
|
||||
|
||||
stage("Regenerating Zones");
|
||||
Zones.generate();
|
||||
Names.getMapName();
|
||||
stage("Restoring Notes");
|
||||
notes = parentMap.notes;
|
||||
stage("Submap done");
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||
showStatistics();
|
||||
INFO && console.groupEnd("Generated Map " + seed);
|
||||
}
|
||||
|
||||
/* find the nearest cell accepted by filter f *and* having at
|
||||
* least one *neighbor* fulfilling filter g, up to cell-distance `max`
|
||||
* returns [cellid, neighbor] tuple or undefined if no such cell.
|
||||
* accepts coordinates (x, y)
|
||||
*/
|
||||
const findNearest =
|
||||
(f, g, max = 3) =>
|
||||
(px, py) => {
|
||||
const d2 = c => (px - pack.cells.p[c][0]) ** 2 + (py - pack.cells.p[c][0]) ** 2;
|
||||
const startCell = findCell(px, py);
|
||||
const tested = new Set([startCell]); // ignore analyzed cells
|
||||
const kernel = (cs, level) => {
|
||||
const [bestf, bestg] = cs.filter(f).reduce(
|
||||
([cf, cg], c) => {
|
||||
const neighbors = pack.cells.c[c];
|
||||
const betterg = neighbors.filter(g).reduce((u, x) => (d2(x) < d2(u) ? x : u));
|
||||
if (cf === undefined) return [c, betterg];
|
||||
return betterg && d2(cf) < d2(c) ? [c, betterg] : [cf, cg];
|
||||
},
|
||||
[undefined, undefined]
|
||||
);
|
||||
if (bestf && bestg) return [bestf, bestg];
|
||||
|
||||
// no suitable pair found, retry with next ring
|
||||
const targets = new Set(cs.map(c => pack.cells.c[c]).flat());
|
||||
const ring = Array.from(targets).filter(nc => !tested.has(nc));
|
||||
if (level >= max || !ring.length) return [undefined, undefined];
|
||||
ring.forEach(c => tested.add(c));
|
||||
return kernel(ring, level + 1);
|
||||
};
|
||||
const pair = kernel([startCell], 1);
|
||||
return pair;
|
||||
};
|
||||
|
||||
function copyBurgs(parentMap, projection, options) {
|
||||
const cells = pack.cells;
|
||||
pack.burgs = parentMap.pack.burgs;
|
||||
|
||||
// remap burgs to the best new cell
|
||||
pack.burgs.forEach((b, id) => {
|
||||
if (id == 0) return; // skip empty city of neturals
|
||||
[b.x, b.y] = projection(b.x, b.y);
|
||||
b.population = b.population * options.scale; // adjust for populationRate change
|
||||
|
||||
// disable out-of-map (removed) burgs
|
||||
if (!inMap(b.x, b.y)) {
|
||||
b.removed = true;
|
||||
b.cell = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const cityCell = findCell(b.x, b.y);
|
||||
let searchFunc;
|
||||
const isFreeLand = c => cells.t[c] === 1 && !cells.burg[c];
|
||||
const nearCoast = c => cells.t[c] === -1;
|
||||
|
||||
// check if we need to relocate the burg
|
||||
if (cells.burg[cityCell])
|
||||
// already occupied
|
||||
searchFunc = findNearest(isFreeLand, _ => true, 3);
|
||||
|
||||
if (isWater(pack, cityCell) || b.port)
|
||||
// burg is in water or port
|
||||
searchFunc = findNearest(isFreeLand, nearCoast, 6);
|
||||
|
||||
if (searchFunc) {
|
||||
const [newCell, neighbor] = searchFunc(b.x, b.y);
|
||||
if (!newCell) {
|
||||
WARN && console.warn(`Can not relocate Burg: ${b.name} sunk and destroyed. :-(`);
|
||||
b.cell = null;
|
||||
b.removed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
[b.x, b.y] = b.port ? getCloseToEdgePoint(newCell, neighbor) : cells.p[newCell];
|
||||
if (b.port) b.port = cells.f[neighbor]; // copy feature number
|
||||
b.cell = newCell;
|
||||
if (b.port && !isWater(pack, neighbor)) console.error("betrayal! negihbor must be water!", b);
|
||||
} else {
|
||||
b.cell = cityCell;
|
||||
}
|
||||
if (b.i && !b.lock) b.lock = options.lockBurgs;
|
||||
cells.burg[b.cell] = id;
|
||||
});
|
||||
}
|
||||
|
||||
function getCloseToEdgePoint(cell1, cell2) {
|
||||
const {cells, vertices} = pack;
|
||||
|
||||
const [x0, y0] = cells.p[cell1];
|
||||
|
||||
const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
|
||||
const [x1, y1] = vertices.p[commonVertices[0]];
|
||||
const [x2, y2] = vertices.p[commonVertices[1]];
|
||||
const xEdge = (x1 + x2) / 2;
|
||||
const yEdge = (y1 + y2) / 2;
|
||||
|
||||
const x = rn(x0 + 0.95 * (xEdge - x0), 2);
|
||||
const y = rn(y0 + 0.95 * (yEdge - y0), 2);
|
||||
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
// export
|
||||
return {resample, findNearest};
|
||||
})();
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"];
|
||||
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
|
||||
|
||||
function geneateWithAi(defaultPrompt, onApply) {
|
||||
function generateWithAi(defaultPrompt, onApply) {
|
||||
updateValues();
|
||||
|
||||
$("#aiGenerator").dialog({
|
||||
|
|
@ -26,13 +26,14 @@ function geneateWithAi(defaultPrompt, onApply) {
|
|||
}
|
||||
});
|
||||
|
||||
if (modules.geneateWithAi) return;
|
||||
modules.geneateWithAi = true;
|
||||
if (modules.generateWithAi) return;
|
||||
modules.generateWithAi = true;
|
||||
|
||||
function updateValues() {
|
||||
byId("aiGeneratorResult").value = "";
|
||||
byId("aiGeneratorPrompt").value = defaultPrompt;
|
||||
byId("aiGeneratorKey").value = localStorage.getItem("fmg-ai-kl") || "";
|
||||
byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1.2";
|
||||
|
||||
const select = byId("aiGeneratorModel");
|
||||
select.options.length = 0;
|
||||
|
|
@ -52,6 +53,12 @@ function geneateWithAi(defaultPrompt, onApply) {
|
|||
const prompt = byId("aiGeneratorPrompt").value;
|
||||
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
|
||||
|
||||
const temperature = parseFloat(byId("aiGeneratorTemperature").value);
|
||||
if (isNaN(temperature) || temperature < 0 || temperature > 2) {
|
||||
return tip("Temperature must be a number between 0 and 2", true, "error", 4000);
|
||||
}
|
||||
localStorage.setItem("fmg-ai-temperature", temperature.toString());
|
||||
|
||||
try {
|
||||
button.disabled = true;
|
||||
const resultArea = byId("aiGeneratorResult");
|
||||
|
|
@ -70,7 +77,7 @@ function geneateWithAi(defaultPrompt, onApply) {
|
|||
{role: "system", content: SYSTEM_MESSAGE},
|
||||
{role: "user", content: prompt}
|
||||
],
|
||||
temperature: 1.2,
|
||||
temperature: temperature,
|
||||
stream: true // Enable streaming
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ function editNotes(id, name) {
|
|||
}
|
||||
};
|
||||
|
||||
geneateWithAi(prompt, onApply);
|
||||
generateWithAi(prompt, onApply);
|
||||
}
|
||||
|
||||
function downloadLegends() {
|
||||
|
|
|
|||
|
|
@ -116,20 +116,20 @@ function selectStyleElement() {
|
|||
if (
|
||||
[
|
||||
"armies",
|
||||
"routes",
|
||||
"lakes",
|
||||
"biomes",
|
||||
"borders",
|
||||
"cults",
|
||||
"relig",
|
||||
"cells",
|
||||
"coastline",
|
||||
"prec",
|
||||
"coordinates",
|
||||
"cults",
|
||||
"gridOverlay",
|
||||
"ice",
|
||||
"icons",
|
||||
"coordinates",
|
||||
"zones",
|
||||
"gridOverlay"
|
||||
"lakes",
|
||||
"prec",
|
||||
"relig",
|
||||
"routes",
|
||||
"zones"
|
||||
].includes(styleElement)
|
||||
) {
|
||||
styleStroke.style.display = "block";
|
||||
|
|
@ -140,7 +140,7 @@ function selectStyleElement() {
|
|||
|
||||
// stroke dash
|
||||
if (
|
||||
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(
|
||||
["borders", "cells", "coordinates", "gridOverlay", "legend", "population", "routes", "temperature", "zones"].includes(
|
||||
styleElement
|
||||
)
|
||||
) {
|
||||
|
|
@ -788,7 +788,7 @@ styleShadowInput.on("input", function () {
|
|||
styleFontAdd.on("click", function () {
|
||||
addFontNameInput.value = "";
|
||||
addFontURLInput.value = "";
|
||||
|
||||
|
||||
$("#addFontDialog").dialog({
|
||||
title: "Add custom font",
|
||||
width: "26em",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function editZones() {
|
||||
closeDialogs();
|
||||
closeDialogs("#zonesEditor, .stable");
|
||||
if (!layerIsOn("toggleZones")) toggleZones();
|
||||
const body = byId("zonesBodySection");
|
||||
|
||||
|
|
|
|||
|
|
@ -209,11 +209,11 @@ window.Zones = (function () {
|
|||
const cost = [];
|
||||
const maxCells = rand(20, 40);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
queue.queue({e: burg.cell, p: 0});
|
||||
const queue = new FlatQueue();
|
||||
queue.push({e: burg.cell, p: 0}, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
|
|
@ -224,7 +224,7 @@ window.Zones = (function () {
|
|||
|
||||
if (!cost[nextCellId] || p < cost[nextCellId]) {
|
||||
cost[nextCellId] = p;
|
||||
queue.queue({e: nextCellId, p});
|
||||
queue.push({e: nextCellId, p}, p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -251,11 +251,11 @@ window.Zones = (function () {
|
|||
const cost = [];
|
||||
const maxCells = rand(5, 25);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
queue.queue({e: burg.cell, p: 0});
|
||||
const queue = new FlatQueue();
|
||||
queue.push({e: burg.cell, p: 0}, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ window.Zones = (function () {
|
|||
|
||||
if (!cost[e] || p < cost[e]) {
|
||||
cost[e] = p;
|
||||
queue.queue({e, p});
|
||||
queue.push({e, p}, p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,3 +56,11 @@ JSON.isValid = str => {
|
|||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
JSON.safeParse = str => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
*
|
||||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||
*/
|
||||
const VERSION = "1.105.19";
|
||||
|
||||
const VERSION = "1.105.22";
|
||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue