mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-22 15:17:23 +01:00
Merge branch 'master' into relief-webgl-renderer
This commit is contained in:
commit
3c7b6fffef
12 changed files with 170 additions and 643 deletions
131
.github/copilot-instructions.md
vendored
131
.github/copilot-instructions.md
vendored
|
|
@ -1,118 +1,65 @@
|
|||
# Fantasy Map Generator
|
||||
|
||||
Azgaar's Fantasy Map Generator is a web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
|
||||
Azgaar's Fantasy Map Generator is a client-only web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
|
||||
|
||||
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
Always reference these instructions first.
|
||||
|
||||
## Working Effectively
|
||||
# Architecture
|
||||
|
||||
- The project uses NPM, Vite, and TypeScript for development and building.
|
||||
- **Setup**: Run `npm install` to install dependencies (requires Node.js >= 24.0.0)
|
||||
- **Development**: Run `npm run dev` to start the Vite development server
|
||||
- Access at: `http://localhost:5173` (Vite's default port)
|
||||
- Hot module replacement (HMR) enabled - changes are reflected immediately
|
||||
- **Building**: Run `npm run build` to compile TypeScript and build for production
|
||||
- TypeScript compilation runs first (`tsc`)
|
||||
- Vite builds the application to `dist/` directory
|
||||
- **Preview**: Run `npm run preview` to preview the production build locally
|
||||
The codebase is gradually transitioning from **vanilla JavaScript to TypeScript** while maintaining compatibility with the existing generation pipeline and legacy `.map` user files.
|
||||
|
||||
## Validation
|
||||
The expected **future architecture** is based on a separation between **world data**, **procedural generation**, **interactive editing**, and **rendering**.
|
||||
|
||||
- Always manually validate any changes by:
|
||||
1. Run `npm run dev` to start the development server (wait for "ready" message)
|
||||
2. Navigate to the application in browser (typically `http://localhost:5173`)
|
||||
3. Click the "►" button to open the menu and generate a new map
|
||||
4. **CRITICAL VALIDATION**: Verify the map generates with countries, cities, roads, and geographic features
|
||||
5. Test UI interaction: click "Layers" button, verify layer controls work
|
||||
6. Test regeneration: click "New Map!" button, verify new map generates correctly
|
||||
- **Known Issues**: Google Analytics and font loading errors are normal (blocked external resources)
|
||||
- For production build validation: run `npm run build` followed by `npm run preview`
|
||||
The application is conceptually divided into four main layers:
|
||||
|
||||
## Repository Structure
|
||||
- **State** — world data and style configuration, the single source of truth
|
||||
- **Generators** — procedural world simulation (model)
|
||||
- **Editors** — user-driven mutations of the world state (controllers)
|
||||
- **Renderer** — map visualization (view)
|
||||
|
||||
### Core Files
|
||||
Flow:
|
||||
settings → generators → world data → renderer
|
||||
UI → editors → world data → renderer
|
||||
|
||||
- `package.json` - NPM package configuration with scripts and dependencies
|
||||
- `vite.config.ts` - Vite build configuration
|
||||
- `tsconfig.json` - TypeScript compiler configuration
|
||||
### Layer responsibilities
|
||||
|
||||
### Source Directories
|
||||
**State (world data)**
|
||||
Stores all map data and style configuration.
|
||||
The data layer must contain **no logic and no rendering code**.
|
||||
|
||||
- `src/` - Source code directory (build input)
|
||||
- `src/index.html` - Main application entry point
|
||||
- `src/utils/` - TypeScript utility modules (migrated from JS)
|
||||
- `public/` - Static assets (copied to build output)
|
||||
- `public/main.js` - Core application logic
|
||||
- `public/versioning.js` - Version management and update handling
|
||||
- `public/modules/` - Core functionality modules:
|
||||
- `modules/ui/` - UI components (editors, tools, style management)
|
||||
- `modules/dynamic/` - runtime modules (export, installation)
|
||||
- `modules/renderers/` - drawing and rendering logic
|
||||
- `public/styles/` - Visual style presets (JSON files)
|
||||
- `public/libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
|
||||
- `public/images/` - Backgrounds, UI elements
|
||||
- `public/charges/` - Heraldic symbols and coat of arms elements
|
||||
- `public/config/` - Heightmap templates and configurations
|
||||
- `public/heightmaps/` - Terrain generation data
|
||||
- `dist/` - Production build output (generated by `npm run build`)
|
||||
**Generators**
|
||||
Implement the procedural world simulation and populate or update world data based on generation settings.
|
||||
|
||||
## Common Tasks
|
||||
**Editors**
|
||||
Implement interactive editing tools used by the user.
|
||||
Editors perform controlled mutations of the world state and can be viewed as **interactive generators**.
|
||||
|
||||
### Making Code Changes
|
||||
**Renderer**
|
||||
Converts the world state into **SVG or WebGL graphics**.
|
||||
Rendering must be a **pure visualization step** and must **not modify world data**.
|
||||
|
||||
1. **TypeScript files** (`src/utils/*.ts`):
|
||||
- Edit TypeScript files in the `src/utils/` directory
|
||||
- Changes are automatically recompiled and hot-reloaded in dev mode
|
||||
- Run `npm run build` to verify TypeScript compilation succeeds
|
||||
2. **JavaScript files** (`public/*.js`, `public/modules/*.js`):
|
||||
- Edit JavaScript files directly in the `public/` directory
|
||||
- Changes are automatically reflected in dev mode via HMR
|
||||
- **Note**: Core application logic is still in JavaScript and gradually being migrated
|
||||
3. **Always test map generation** after making changes
|
||||
4. Update version in `public/versioning.js` for all changes
|
||||
5. For production builds, update file hashes in `src/index.html` if needed (format: `file.js?v=1.108.1`)
|
||||
# Working Effectively
|
||||
|
||||
### Debugging Map Generation
|
||||
The project uses **NPM**, **Vite**, and **TypeScript** for development and building.
|
||||
|
||||
- Open browser developer tools console
|
||||
- Look for timing logs, e.g. "TOTAL: ~0.76s"
|
||||
- Map generation logs show each step (heightmap, rivers, states, etc.)
|
||||
- Error messages will indicate specific generation failures
|
||||
## Setup
|
||||
|
||||
### Testing Different Map Types
|
||||
Install dependencies: `npm install`
|
||||
|
||||
- Use "New Map!" button for quick regeneration
|
||||
- Access "Layers" menu to change map visualization
|
||||
- Available presets: Political, Cultural, Religions, Biomes, Heightmap, Physical, Military
|
||||
Requirements: Node.js **>= 24.0.0**
|
||||
|
||||
## Troubleshooting
|
||||
## Development
|
||||
|
||||
### Application Won't Load
|
||||
Start the development server: `npm run dev`
|
||||
|
||||
- Run `npm install` to ensure dependencies are installed
|
||||
- Run `npm run dev` to start the development server
|
||||
- Check console for JavaScript/TypeScript errors
|
||||
- Verify all files are present in repository
|
||||
- Ensure Node.js version >= 24.0.0 (`node --version`)
|
||||
Access the application at: http://localhost:5173
|
||||
|
||||
### Build Failures
|
||||
## Build
|
||||
|
||||
- Check TypeScript compilation errors (`tsc` output)
|
||||
- Verify all dependencies are installed (`npm install`)
|
||||
- Check `tsconfig.json` for configuration issues
|
||||
- Look for import/module resolution errors
|
||||
Create a production build: `npm run build`
|
||||
|
||||
### Map Generation Fails
|
||||
Build steps:
|
||||
|
||||
- Check browser console for error messages
|
||||
- Look for specific module failures in generation logs
|
||||
- Try refreshing page and generating new map
|
||||
- Verify build completed successfully if using production build
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- Map generation should complete in ~1 second for standard configurations
|
||||
- If slower, check browser console for errors
|
||||
- Development mode may be slower due to HMR overhead
|
||||
|
||||
Remember: This is a sophisticated application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. The project is gradually migrating from vanilla JavaScript to TypeScript. Always validate that your changes preserve the core map generation functionality.
|
||||
1. TypeScript compilation (`tsc`)
|
||||
2. Vite build
|
||||
3. Output written to `dist/`
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -9,5 +9,5 @@
|
|||
/_bmad
|
||||
/_bmad-output
|
||||
/.DS_Store
|
||||
/.github/agents
|
||||
/.github/prompts
|
||||
.github/agents/bmad-*
|
||||
.github/prompts/bmad-*
|
||||
16
README.md
16
README.md
|
|
@ -16,8 +16,6 @@ Join our [Discord server](https://discordapp.com/invite/X7E84HU) and [Reddit com
|
|||
|
||||
Contact me via [email](mailto:azgaar.fmg@yandex.com) if you have non-public suggestions. For bug reports please use [GitHub issues](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or _#fmg-bugs_ channel on Discord. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips).
|
||||
|
||||
Pull requests are highly welcomed. The codebase is messy and requires re-design. I will appreciate if you start with minor changes. Check out the [data model](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Data-model) before contributing.
|
||||
|
||||
You can support the project on [Patreon](https://www.patreon.com/azgaar).
|
||||
|
||||
_Inspiration:_
|
||||
|
|
@ -27,3 +25,17 @@ _Inspiration:_
|
|||
- Amit Patel's [_Polygonal Map Generation for Games_](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation)
|
||||
|
||||
- Scott Turner's [_Here Dragons Abound_](https://heredragonsabound.blogspot.com)
|
||||
|
||||
## Contribution
|
||||
|
||||
Pull requests are highly welcomed. The codebase is messy and I will appreciate if you start with minor changes. Check out the [data model](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Data-model) before contributing.
|
||||
|
||||
The codebase is gradually transitioning from **vanilla JavaScript to TypeScript** while maintaining compatibility with the existing generation pipeline and old `.map` user files.
|
||||
|
||||
The expected **future** architecture is based on a separation between **world data**, **procedural generation**, **interactive editing**, and **rendering**. The application is conceptually divided into four main layers: world data and styles (state), generators (model), editors (controllers), renderers (view).
|
||||
|
||||
Flow:
|
||||
settings → generators → world data → renderer
|
||||
UI → editors → world data → renderer.
|
||||
|
||||
The data layer must contain no logic and no rendering code. Generators implement the procedural world simulation. Editors implement interactive editing tools used by the user. They perform controlled mutations of the world state. Editors can be viewed as interactive generators. The renderer converts the world state into SVG or WebGl graphics. Renderer must be pure visualization step and not modify world data.
|
||||
|
|
|
|||
|
|
@ -1143,7 +1143,6 @@ function reGraph() {
|
|||
pack.cells = packCells;
|
||||
pack.cells.p = newCells.p;
|
||||
pack.cells.g = createTypedArray({maxValue: grid.points.length, from: newCells.g});
|
||||
pack.cells.q = d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i]));
|
||||
pack.cells.h = createTypedArray({maxValue: 100, from: newCells.h});
|
||||
pack.cells.area = createTypedArray({maxValue: UINT16_MAX, length: packCells.i.length}).map((_, cellId) => {
|
||||
const area = Math.abs(d3.polygonArea(getPackPolygon(cellId)));
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ function getName(id) {
|
|||
}
|
||||
|
||||
function getGraph(currentGraph) {
|
||||
const newGraph = shouldRegenerateGrid(currentGraph, seed) ? generateGrid() : deepCopy(currentGraph);
|
||||
const newGraph = shouldRegenerateGrid(currentGraph, seed) ? generateGrid() : structuredClone(currentGraph);
|
||||
delete newGraph.cells.h;
|
||||
return newGraph;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ window.Resample = (function () {
|
|||
scale: Number
|
||||
*/
|
||||
function process({projection, inverse, scale}) {
|
||||
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
|
||||
const parentMap = {grid: structuredClone(grid), pack: structuredClone(pack), notes: structuredClone(notes)};
|
||||
const riversData = saveRiversData(pack.rivers);
|
||||
|
||||
grid = generateGrid();
|
||||
|
|
@ -28,7 +28,7 @@ window.Resample = (function () {
|
|||
|
||||
reGraph();
|
||||
Features.markupPack();
|
||||
Ice.generate()
|
||||
Ice.generate();
|
||||
createDefaultRuler();
|
||||
|
||||
restoreCellData(parentMap, inverse, scale);
|
||||
|
|
@ -51,9 +51,10 @@ window.Resample = (function () {
|
|||
grid.cells.temp = new Int8Array(grid.points.length);
|
||||
grid.cells.prec = new Uint8Array(grid.points.length);
|
||||
|
||||
const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i]));
|
||||
grid.points.forEach(([x, y], newGridCell) => {
|
||||
const [parentX, parentY] = inverse(x, y);
|
||||
const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||
const parentPackCell = parentPackQ.find(parentX, parentY, Infinity)[2];
|
||||
const parentGridCell = parentMap.pack.cells.g[parentPackCell];
|
||||
|
||||
grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
|
||||
|
|
@ -347,11 +348,12 @@ window.Resample = (function () {
|
|||
}
|
||||
|
||||
function restoreFeatureDetails(parentMap, inverse) {
|
||||
const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i]));
|
||||
pack.features.forEach(feature => {
|
||||
if (!feature) return;
|
||||
const [x, y] = pack.cells.p[feature.firstCell];
|
||||
const [parentX, parentY] = inverse(x, y);
|
||||
const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||
const parentCell = parentPackQ.find(parentX, parentY, Infinity)[2];
|
||||
if (parentCell === undefined) return;
|
||||
const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,408 +0,0 @@
|
|||
"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();
|
||||
Features.defineGroups();
|
||||
// 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
|
||||
});
|
||||
States.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();
|
||||
Lakes.defineNames();
|
||||
|
||||
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};
|
||||
})();
|
||||
|
|
@ -8542,7 +8542,7 @@
|
|||
|
||||
<script defer src="config/heightmap-templates.js"></script>
|
||||
<script defer src="config/precreated-heightmaps.js"></script>
|
||||
<script defer src="modules/resample.js?v=1.112.1"></script>
|
||||
<script defer src="modules/resample.js?v=1.113.6"></script>
|
||||
<script defer src="libs/alea.min.js?v1.105.0"></script>
|
||||
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
|
||||
<script defer src="libs/lineclip.min.js?v1.105.0"></script>
|
||||
|
|
|
|||
|
|
@ -16,46 +16,6 @@ export const unique = <T>(array: T[]): T[] => {
|
|||
return [...new Set(array)];
|
||||
};
|
||||
|
||||
/**
|
||||
* Deep copy an object or array
|
||||
* @param {Object|Array} obj - The object or array to deep copy
|
||||
* @returns A deep copy of the object or array
|
||||
*/
|
||||
export const deepCopy = <T>(obj: T): T => {
|
||||
const id = (x: T): T => x;
|
||||
const dcTArray = (a: T[]): T[] => a.map(id);
|
||||
const dcObject = (x: object): object =>
|
||||
Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
|
||||
const dcAny = (x: any): any =>
|
||||
x instanceof Object ? (cf.get(x.constructor) || id)(x) : x;
|
||||
// don't map keys, probably this is what we would expect
|
||||
const dcMapCore = (m: Map<any, any>): [any, any][] =>
|
||||
[...m.entries()].map(([k, v]) => [k, dcAny(v)]);
|
||||
|
||||
const cf: Map<any, (x: any) => any> = new Map<any, (x: any) => any>([
|
||||
[Int8Array, dcTArray],
|
||||
[Uint8Array, dcTArray],
|
||||
[Uint8ClampedArray, dcTArray],
|
||||
[Int16Array, dcTArray],
|
||||
[Uint16Array, dcTArray],
|
||||
[Int32Array, dcTArray],
|
||||
[Uint32Array, dcTArray],
|
||||
[Float32Array, dcTArray],
|
||||
[Float64Array, dcTArray],
|
||||
[BigInt64Array, dcTArray],
|
||||
[BigUint64Array, dcTArray],
|
||||
[Map, (m) => new Map(dcMapCore(m))],
|
||||
[WeakMap, (m) => new WeakMap(dcMapCore(m))],
|
||||
[Array, (a) => a.map(dcAny)],
|
||||
[Set, (s) => [...s.values()].map(dcAny)],
|
||||
[Date, (d) => new Date(d.getTime())],
|
||||
[Object, dcObject],
|
||||
// ... extend here to implement their custom deep copy
|
||||
]);
|
||||
|
||||
return dcAny(obj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the appropriate typed array constructor based on the maximum value
|
||||
* @param {number} maxValue - The maximum value that will be stored in the array
|
||||
|
|
@ -109,7 +69,6 @@ declare global {
|
|||
interface Window {
|
||||
last: typeof last;
|
||||
unique: typeof unique;
|
||||
deepCopy: typeof deepCopy;
|
||||
getTypedArray: typeof getTypedArray;
|
||||
createTypedArray: typeof createTypedArray;
|
||||
INT8_MAX: number;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const getSegmentId = (
|
|||
};
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking func until after ms milliseconds have elapsed
|
||||
* Creates a debounced function that delays next func call until after ms milliseconds
|
||||
* @param func - The function to debounce
|
||||
* @param ms - The number of milliseconds to delay
|
||||
* @returns The debounced function
|
||||
|
|
@ -212,11 +212,14 @@ export const isCtrlClick = (event: MouseEvent | KeyboardEvent): boolean => {
|
|||
* @returns Formatted date string
|
||||
*/
|
||||
export const generateDate = (from: number = 100, to: number = 1000): string => {
|
||||
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
return new Date(rand(from, to), rand(11), rand(1, 28)).toLocaleDateString(
|
||||
"en",
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Alea from "alea";
|
||||
import { color } from "d3";
|
||||
import { color, quadtree } from "d3";
|
||||
import Delaunator from "delaunator";
|
||||
import {
|
||||
type Cells,
|
||||
|
|
@ -266,6 +266,11 @@ export const findGridAll = (
|
|||
return found;
|
||||
};
|
||||
|
||||
const quadtreeCache = new WeakMap<
|
||||
object,
|
||||
ReturnType<typeof quadtree<[number, number, number]>>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Returns the index of the packed cell containing the given x and y coordinates
|
||||
* @param {number} x - The x coordinate
|
||||
|
|
@ -277,10 +282,16 @@ export const findClosestCell = (
|
|||
x: number,
|
||||
y: number,
|
||||
radius = Infinity,
|
||||
packedGraph: any,
|
||||
pack: { cells: { p: [number, number][] } },
|
||||
): number | undefined => {
|
||||
if (!packedGraph.cells?.q) return;
|
||||
const found = packedGraph.cells.q.find(x, y, radius);
|
||||
if (!pack.cells?.p) throw new Error("Pack cells not found");
|
||||
let qTree = quadtreeCache.get(pack.cells.p);
|
||||
if (!qTree) {
|
||||
qTree = quadtree(pack.cells.p.map(([px, py], i) => [px, py, i]));
|
||||
if (!qTree) throw new Error("Failed to create quadtree");
|
||||
quadtreeCache.set(pack.cells.p, qTree);
|
||||
}
|
||||
const found = qTree.find(x, y, radius);
|
||||
return found ? found[2] : undefined;
|
||||
};
|
||||
|
||||
|
|
@ -414,8 +425,13 @@ export const findAllCellsInRadius = (
|
|||
radius: number,
|
||||
packedGraph: any,
|
||||
): number[] => {
|
||||
// Use findAllInQuadtree directly instead of relying on prototype extension
|
||||
const found = findAllInQuadtree(x, y, radius, packedGraph.cells.q);
|
||||
const q = quadtree<[number, number, number]>(
|
||||
packedGraph.cells.p.map(
|
||||
([px, py]: [number, number], i: number) =>
|
||||
[px, py, i] as [number, number, number],
|
||||
),
|
||||
);
|
||||
const found = findAllInQuadtree(x, y, radius, q);
|
||||
return found.map((r: any) => r[2]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ window.list = list;
|
|||
|
||||
import {
|
||||
createTypedArray,
|
||||
deepCopy,
|
||||
getTypedArray,
|
||||
last,
|
||||
TYPED_ARRAY_MAX_VALUES,
|
||||
|
|
@ -35,7 +34,6 @@ import {
|
|||
|
||||
window.last = last;
|
||||
window.unique = unique;
|
||||
window.deepCopy = deepCopy;
|
||||
window.getTypedArray = getTypedArray;
|
||||
window.createTypedArray = createTypedArray;
|
||||
window.INT8_MAX = TYPED_ARRAY_MAX_VALUES.INT8_MAX;
|
||||
|
|
@ -274,90 +272,89 @@ window.drawPoint = drawPoint;
|
|||
window.drawPath = drawPath;
|
||||
|
||||
export {
|
||||
rn,
|
||||
lim,
|
||||
minmax,
|
||||
normalize,
|
||||
lerp,
|
||||
isVowel,
|
||||
trimVowels,
|
||||
getAdjective,
|
||||
nth,
|
||||
abbreviate,
|
||||
list,
|
||||
last,
|
||||
unique,
|
||||
deepCopy,
|
||||
getTypedArray,
|
||||
createTypedArray,
|
||||
TYPED_ARRAY_MAX_VALUES,
|
||||
rand,
|
||||
P,
|
||||
each,
|
||||
gauss,
|
||||
Pint,
|
||||
biased,
|
||||
generateSeed,
|
||||
getNumberInRange,
|
||||
ra,
|
||||
rw,
|
||||
convertTemperature,
|
||||
si,
|
||||
getIntegerFromSI,
|
||||
toHEX,
|
||||
getColors,
|
||||
getRandomColor,
|
||||
getMixedColor,
|
||||
C_12,
|
||||
getComposedPath,
|
||||
getNextId,
|
||||
rollups,
|
||||
distanceSquared,
|
||||
getIsolines,
|
||||
getPolesOfInaccessibility,
|
||||
connectVertices,
|
||||
findPath,
|
||||
getVertexPath,
|
||||
round,
|
||||
capitalize,
|
||||
splitInTwo,
|
||||
parseTransform,
|
||||
isValidJSON,
|
||||
safeParseJSON,
|
||||
sanitizeId,
|
||||
byId,
|
||||
shouldRegenerateGrid,
|
||||
generateGrid,
|
||||
findGridAll,
|
||||
findGridCell,
|
||||
findClosestCell,
|
||||
C_12,
|
||||
calculateVoronoi,
|
||||
findAllCellsInRadius,
|
||||
getPackPolygon,
|
||||
getGridPolygon,
|
||||
poissonDiscSampler,
|
||||
isLand,
|
||||
isWater,
|
||||
findAllInQuadtree,
|
||||
drawHeights,
|
||||
capitalize,
|
||||
clipPoly,
|
||||
getSegmentId,
|
||||
connectVertices,
|
||||
convertTemperature,
|
||||
createTypedArray,
|
||||
debounce,
|
||||
throttle,
|
||||
parseError,
|
||||
getBase64,
|
||||
openURL,
|
||||
wiki,
|
||||
link,
|
||||
isCtrlClick,
|
||||
generateDate,
|
||||
getLongitude,
|
||||
getLatitude,
|
||||
getCoordinates,
|
||||
initializePrompt,
|
||||
distanceSquared,
|
||||
drawCellsValue,
|
||||
drawHeights,
|
||||
drawPath,
|
||||
drawPoint,
|
||||
drawPolygons,
|
||||
drawRouteConnections,
|
||||
drawPoint,
|
||||
drawPath,
|
||||
each,
|
||||
findAllCellsInRadius,
|
||||
findAllInQuadtree,
|
||||
findClosestCell,
|
||||
findGridAll,
|
||||
findGridCell,
|
||||
findPath,
|
||||
gauss,
|
||||
generateDate,
|
||||
generateGrid,
|
||||
generateSeed,
|
||||
getAdjective,
|
||||
getBase64,
|
||||
getColors,
|
||||
getComposedPath,
|
||||
getCoordinates,
|
||||
getGridPolygon,
|
||||
getIntegerFromSI,
|
||||
getIsolines,
|
||||
getLatitude,
|
||||
getLongitude,
|
||||
getMixedColor,
|
||||
getNextId,
|
||||
getNumberInRange,
|
||||
getPackPolygon,
|
||||
getPolesOfInaccessibility,
|
||||
getRandomColor,
|
||||
getSegmentId,
|
||||
getTypedArray,
|
||||
getVertexPath,
|
||||
initializePrompt,
|
||||
isCtrlClick,
|
||||
isLand,
|
||||
isValidJSON,
|
||||
isVowel,
|
||||
isWater,
|
||||
last,
|
||||
lerp,
|
||||
lim,
|
||||
link,
|
||||
list,
|
||||
minmax,
|
||||
normalize,
|
||||
nth,
|
||||
openURL,
|
||||
P,
|
||||
parseError,
|
||||
parseTransform,
|
||||
Pint,
|
||||
poissonDiscSampler,
|
||||
ra,
|
||||
rand,
|
||||
rn,
|
||||
rollups,
|
||||
round,
|
||||
rw,
|
||||
safeParseJSON,
|
||||
sanitizeId,
|
||||
shouldRegenerateGrid,
|
||||
si,
|
||||
splitInTwo,
|
||||
throttle,
|
||||
toHEX,
|
||||
trimVowels,
|
||||
TYPED_ARRAY_MAX_VALUES,
|
||||
unique,
|
||||
wiki,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue