feat: routes - change data format

This commit is contained in:
Azgaar 2024-04-28 00:52:49 +02:00
parent 597f6ddd75
commit b47fa6b92d
14 changed files with 155 additions and 139 deletions

View file

@ -1,24 +1,8 @@
"use strict";
const MIN_LAND_HEIGHT = 20;
const names = [
"Marine",
"Hot desert",
"Cold desert",
"Savanna",
"Grassland",
"Tropical seasonal forest",
"Temperate deciduous forest",
"Tropical rainforest",
"Temperate rainforest",
"Taiga",
"Tundra",
"Glacier",
"Wetland"
];
window.Biomes = (function () {
const MIN_LAND_HEIGHT = 20;
const getDefault = () => {
const name = [
"Marine",
@ -52,7 +36,7 @@ window.Biomes = (function () {
"#0b9131"
];
const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150];
const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250];
const icons = [
{},
{dune: 3, cactus: 6, deadTree: 1},

View file

@ -846,9 +846,9 @@ export function resolveVersionConflicts(version) {
if (version < 1.98) {
// v1.98.00 changed routes generation algorithm and data format
// 1. cells.road => cells.route; 1 = MAIN; 2 = TRAIL; 3 = SEA;
// 1. cells.road => cells.routes and now it an object of objects {i1: {i2: routeId, i3: routeId}}
// 2. cells.crossroad is removed
// 3. pack.routes is added
// 3. pack.routes is added as an array of objects
// 4. rendering is changed
}
}

View file

@ -103,7 +103,7 @@ function getSettings() {
}
function getPackCellsData() {
const dataArrays = {
const data = {
v: pack.cells.v,
c: pack.cells.c,
p: pack.cells.p,
@ -122,7 +122,7 @@ function getPackCellsData() {
pop: Array.from(pack.cells.pop),
culture: Array.from(pack.cells.culture),
burg: Array.from(pack.cells.burg),
route: Array.from(pack.cells.route),
routes: pack.cells.routes,
state: Array.from(pack.cells.state),
religion: Array.from(pack.cells.religion),
province: Array.from(pack.cells.province)
@ -131,28 +131,28 @@ function getPackCellsData() {
return {
cells: Array.from(pack.cells.i).map(cellId => ({
i: cellId,
v: dataArrays.v[cellId],
c: dataArrays.c[cellId],
p: dataArrays.p[cellId],
g: dataArrays.g[cellId],
h: dataArrays.h[cellId],
area: dataArrays.area[cellId],
f: dataArrays.f[cellId],
t: dataArrays.t[cellId],
haven: dataArrays.haven[cellId],
harbor: dataArrays.harbor[cellId],
fl: dataArrays.fl[cellId],
r: dataArrays.r[cellId],
conf: dataArrays.conf[cellId],
biome: dataArrays.biome[cellId],
s: dataArrays.s[cellId],
pop: dataArrays.pop[cellId],
culture: dataArrays.culture[cellId],
burg: dataArrays.burg[cellId],
route: dataArrays.route[cellId],
state: dataArrays.state[cellId],
religion: dataArrays.religion[cellId],
province: dataArrays.province[cellId]
v: data.v[cellId],
c: data.c[cellId],
p: data.p[cellId],
g: data.g[cellId],
h: data.h[cellId],
area: data.area[cellId],
f: data.f[cellId],
t: data.t[cellId],
haven: data.haven[cellId],
harbor: data.harbor[cellId],
fl: data.fl[cellId],
r: data.r[cellId],
conf: data.conf[cellId],
biome: data.biome[cellId],
s: data.s[cellId],
pop: data.pop[cellId],
culture: data.culture[cellId],
burg: data.burg[cellId],
routes: data.routes[cellId],
state: data.state[cellId],
religion: data.religion[cellId],
province: data.province[cellId]
})),
vertices: Array.from(pack.vertices.p).map((_, vertexId) => ({
i: vertexId,

View file

@ -374,6 +374,7 @@ async function parseLoadedData(data, mapVersion) {
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
pack.markers = data[35] ? JSON.parse(data[35]) : [];
pack.routes = data[37] ? JSON.parse(data[37]) : [];
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(","));
@ -383,12 +384,13 @@ async function parseLoadedData(data, mapVersion) {
cells.fl = Uint16Array.from(data[20].split(","));
cells.pop = Float32Array.from(data[21].split(","));
cells.r = Uint16Array.from(data[22].split(","));
cells.route = Uint8Array.from(data[23].split(","));
// data[23] for deprecated cells.road
cells.s = Uint16Array.from(data[24].split(","));
cells.state = Uint16Array.from(data[25].split(","));
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
// data[28] for deprecated cells.crossroad
cells.routes = data[36] ? JSON.parse(data[36]) : {};
if (data[31]) {
const namesDL = data[31].split("/");

View file

@ -97,6 +97,8 @@ function prepareMapData() {
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const markers = JSON.stringify(pack.markers);
const cellRoutes = JSON.stringify(pack.cells.routes);
const routes = JSON.stringify(pack.routes);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
@ -135,7 +137,7 @@ function prepareMapData() {
pack.cells.fl,
pop,
pack.cells.r,
pack.cells.route,
[], // deprecated pack.cells.road
pack.cells.s,
pack.cells.state,
pack.cells.religion,
@ -147,7 +149,9 @@ function prepareMapData() {
rivers,
rulersString,
fonts,
markers
markers,
cellRoutes,
routes
].join("\r\n");
return mapData;
}

View file

@ -27,7 +27,7 @@ window.Markers = (function () {
{type: "water-sources", icon: "💧", min: 1, each: 1000, multiplier: 1, list: listWaterSources, add: addWaterSource},
{type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine},
{type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge},
{type: "inns", icon: "🍻", px: 14, min: 1, each: 100, multiplier: 1, list: listInns, add: addInn},
{type: "inns", icon: "🍻", px: 14, min: 1, each: 10, multiplier: 1, list: listInns, add: addInn},
{type: "lighthouses", icon: "🚨", px: 14, min: 1, each: 2, multiplier: 1, list: listLighthouses, add: addLighthouse},
{type: "waterfalls", icon: "⟱", dy: 54, px: 16, min: 1, each: 5, multiplier: 1, list: listWaterfalls, add: addWaterfall},
{type: "battlefields", icon: "⚔️", dy: 52, min: 50, each: 700, multiplier: 1, list: listBattlefields, add: addBattlefield},
@ -279,7 +279,8 @@ window.Markers = (function () {
}
function listInns({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.route[i] === 1 && cells.pop[i] > 10);
const crossRoads = cells.i.filter(i => !occupied[i] && cells.pop[i] > 5 && Routes.isCrossroad(i));
return crossRoads;
}
function addInn(id, cell) {
@ -542,7 +543,7 @@ window.Markers = (function () {
function listLighthouses({cells}) {
return cells.i.filter(
i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.route[c])
i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && Routes.isConnected(c))
);
}
@ -642,7 +643,7 @@ window.Markers = (function () {
function listSeaMonsters({cells, features}) {
return cells.i.filter(
i => !occupied[i] && cells.h[i] < 20 && cells.route[i] && features[cells.f[i]].type === "ocean"
i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i) && features[cells.f[i]].type === "ocean"
);
}
@ -792,7 +793,7 @@ window.Markers = (function () {
cells.religion[i] &&
cells.biome[i] === 1 &&
cells.pop[i] > 1 &&
cells.route[i]
Routes.isConnected(i)
);
}
@ -807,7 +808,7 @@ window.Markers = (function () {
}
function listBrigands({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.route[i] === 1);
return cells.i.filter(i => !occupied[i] && cells.culture[i] && Routes.hasRoad(i));
}
function addBrigands(id, cell) {
@ -867,7 +868,7 @@ window.Markers = (function () {
// Pirates spawn on sea routes
function listPirates({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.route[i]);
return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i));
}
function addPirates(id, cell) {
@ -961,7 +962,7 @@ window.Markers = (function () {
}
function listCircuses({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.route[i]);
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && Routes.isConnected(i));
}
function addCircuse(id, cell) {
@ -1254,16 +1255,16 @@ window.Markers = (function () {
const name = `${toponym} ${type}`;
const legend = ra([
"A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls",
"A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics",
"This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed",
"Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets",
"An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers",
"A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths",
"This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls",
"A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air",
"A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners",
"A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses"
"A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls.",
"A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics.",
"This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed.",
"Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets.",
"An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers.",
"A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths.",
"This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls.",
"A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air.",
"A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners.",
"A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses."
]);
notes.push({id, name, legend});

View file

@ -692,7 +692,7 @@ window.Religions = (function () {
// growth algorithm to assign cells to religions
function expandReligions(religions) {
const cells = pack.cells;
const {cells, routes} = pack;
const religionIds = spreadFolkReligions(religions);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
@ -700,8 +700,6 @@ window.Religions = (function () {
const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth
const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]];
religions
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
.forEach(r => {
@ -712,11 +710,6 @@ window.Religions = (function () {
const religionsMap = new Map(religions.map(r => [r.i, r]));
const isMainRoad = cellId => cells.route[cellId] === 1;
const isTrail = cellId => cells.route[cellId] === 2;
const isSeaRoute = cellId => cells.route[cellId] === 3;
const isWater = cellId => cells.h[cellId] < 20;
while (queue.length) {
const {e: cellId, p, r, s: state} = queue.dequeue();
const {culture, expansion, expansionism} = religionsMap.get(r);
@ -728,7 +721,7 @@ window.Religions = (function () {
const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0;
const stateCost = state !== cells.state[nextCell] ? 10 : 0;
const passageCost = getPassageCost(nextCell);
const passageCost = getPassageCost(cellId, nextCell);
const cellCost = cultureCost + stateCost + passageCost;
const totalCost = p + 10 + cellCost / expansionism;
@ -745,11 +738,18 @@ window.Religions = (function () {
return religionIds;
function getPassageCost(cellId) {
if (isWater(cellId)) return isSeaRoute ? 50 : 500;
if (isMainRoad(cellId)) return 1;
const biomeCost = biomePassageCost(cellId);
return isTrail(cellId) ? biomeCost / 1.5 : biomeCost;
function getPassageCost(cellId, nextCellId) {
const route = Routes.getRoute(cellId, nextCellId);
if (isWater(cellId)) return route ? 50 : 500;
const biomePassageCost = biomesData.cost[cells.biome[nextCellId]];
if (route) {
if (route.group === "roads") return 1;
return biomePassageCost / 3; // trails and other routes
}
return biomePassageCost;
}
}

View file

@ -1,40 +1,14 @@
// suggested data format
// pack.cells.connectivity = {
// cellId1: {
// toCellId2: routeId2,
// toCellId3: routeId2,
// },
// cellId2: {
// toCellId1: routeId2,
// toCellId3: routeId1,
// }
// }
// pack.routes = [
// {i, group: "roads", feature: featureId, cells: [cellId], points?: [[x, y], [x, y]]}
// ];
window.Routes = (function () {
const ROUTES = {
MAIN_ROAD: 1,
TRAIL: 2,
SEA_ROUTE: 3
};
function generate() {
const {cells, burgs} = pack;
const cellRoutes = new Uint8Array(cells.h.length);
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(burgs);
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
const connections = new Map();
const mainRoads = generateMainRoads();
const trails = generateTrails();
const seaRoutes = generateSeaRoutes();
cells.route = cellRoutes;
pack.routes = combineRoutes();
pack.cells.routes = buildLinks(pack.routes);
function sortBurgsByFeature(burgs) {
const burgsByFeature = {};
@ -71,7 +45,7 @@ window.Routes = (function () {
const segments = findPathSegments({isWater: false, connections, start, exit});
for (const segment of segments) {
addConnections(segment, ROUTES.MAIN_ROAD);
addConnections(segment);
mainRoads.push({feature: Number(key), cells: segment});
}
});
@ -94,7 +68,7 @@ window.Routes = (function () {
const segments = findPathSegments({isWater: false, connections, start, exit});
for (const segment of segments) {
addConnections(segment, ROUTES.TRAIL);
addConnections(segment);
trails.push({feature: Number(key), cells: segment});
}
});
@ -117,7 +91,7 @@ window.Routes = (function () {
const exit = featurePorts[toId].cell;
const segments = findPathSegments({isWater: true, connections, start, exit});
for (const segment of segments) {
addConnections(segment, ROUTES.SEA_ROUTE);
addConnections(segment);
seaRoutes.push({feature: Number(featureId), cells: segment});
}
});
@ -127,7 +101,7 @@ window.Routes = (function () {
return seaRoutes;
}
function addConnections(segment, routeTypeId) {
function addConnections(segment) {
for (let i = 0; i < segment.length; i++) {
const cellId = segment[i];
const nextCellId = segment[i + 1];
@ -135,7 +109,6 @@ window.Routes = (function () {
connections.set(`${cellId}-${nextCellId}`, true);
connections.set(`${nextCellId}-${cellId}`, true);
}
if (!cellRoutes[cellId]) cellRoutes[cellId] = routeTypeId;
}
}
@ -165,10 +138,29 @@ window.Routes = (function () {
return routes;
}
function buildLinks(routes) {
const links = {};
for (const {cells, i: routeId} of routes) {
for (let i = 0; i < cells.length; i++) {
const cellId = cells[i];
const nextCellId = cells[i + 1];
if (nextCellId) {
if (!links[cellId]) links[cellId] = {};
links[cellId][nextCellId] = routeId;
if (!links[nextCellId]) links[nextCellId] = {};
links[nextCellId][cellId] = routeId;
}
}
}
return links;
}
}
const MIN_PASSABLE_SEA_TEMP = -4;
const TYPE_MODIFIERS = {
"-1": 1, // coastline
"-2": 1.8, // sea
@ -339,5 +331,36 @@ window.Routes = (function () {
return edges;
}
return {generate};
// utility functions
function isConnected(cellId) {
const {routes} = pack.cells;
return routes[cellId] && Object.keys(routes[cellId]).length > 0;
}
function areConnected(from, to) {
const routeId = pack.cells.routes[from]?.[to];
return routeId !== undefined;
}
function getRoute(from, to) {
const routeId = pack.cells.routes[from]?.[to];
return routeId === undefined ? null : pack.routes[routeId];
}
function hasRoad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return Object.values(connections).some(routeId => pack.routes[routeId].group === "roads");
}
function isCrossroad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return (
Object.keys(connections).length > 3 ||
Object.values(connections).filter(routeId => pack.routes[routeId].group === "roads").length > 2
);
}
return {generate, isConnected, areConnected, getRoute, hasRoad, isCrossroad};
})();

View file

@ -145,7 +145,6 @@ window.Submap = (function () {
cells.state = new Uint16Array(pn);
cells.burg = new Uint16Array(pn);
cells.religion = new Uint16Array(pn);
cells.route = new Uint8Array(pn);
cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map.");

View file

@ -326,8 +326,7 @@ function createMfcgLink(burg) {
const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i));
const hub = +cells.route[cell] === 1;
const hub = Routes.isCrossroad(cell);
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
@ -371,10 +370,12 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
const roadsAround = cells.c[cell].filter(c => cells.h[c] >= 20 && cells.route[c]).length;
if (roadsAround > 1) tags.push("highway");
else if (roadsAround === 1) tags.push("dead end");
else tags.push("isolated");
const connections = pack.cells.routes[cell] || {};
const roads = Object.values(connections).filter(routeId => {
const route = pack.routes[routeId];
return route.group === "roads" || route.group === "trails";
}).length;
tags.push(roads > 1 ? "highway" : roads === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];

View file

@ -282,7 +282,7 @@ function editHeightmap(options) {
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const pop = new Uint16Array(l);
const route = new Uint8Array(l);
const routes = {};
const s = new Uint16Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
@ -300,7 +300,7 @@ function editHeightmap(options) {
biome[g] = pack.cells.biome[i];
culture[g] = pack.cells.culture[i];
pop[g] = pack.cells.pop[i];
route[g] = pack.cells.route[i];
routes[g] = pack.cells.routes[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
@ -352,7 +352,7 @@ function editHeightmap(options) {
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.pop = new Float32Array(n);
pack.cells.route = new Uint8Array(n);
pack.cells.routes = {};
pack.cells.s = new Uint16Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
@ -387,7 +387,7 @@ function editHeightmap(options) {
if (!isLand) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.route[i] = route[g];
pack.cells.routes[i] = routes[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];

View file

@ -486,7 +486,7 @@ class RouteOpisometer extends Measurer {
const cells = pack.cells;
const c = findCell(mousePoint[0], mousePoint[1]);
if (!cells.route[c] && !d3.event.sourceEvent.shiftKey) return;
if (!Routes.isConnected(c) && !d3.event.sourceEvent.shiftKey) return;
context.trackCell(c, rigth);
});

View file

@ -179,13 +179,15 @@ function editUnits() {
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(
d3.drag().on("start", function () {
const cells = pack.cells;
const burgs = pack.burgs;
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.route[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1];
@ -194,7 +196,7 @@ function editUnits() {
d3.event.on("drag", function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.route[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true);
}
});