diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 698d23ae..c524a957 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,58 +1,76 @@ # Fantasy Map Generator -Azgaar's Fantasy Map Generator is a client-side JavaScript 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 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. ## Working Effectively -- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling. -- Run the application using HTTP server (required - cannot run with file:// protocol): - - `python3 -m http.server 8000` - takes 2-3 seconds to start -- Access at: `http://localhost:8000` +- 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 ## Validation - Always manually validate any changes by: - 1. Starting the HTTP server (NEVER CANCEL - wait for full startup) - 2. Navigate to the application in browser + 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` ## Repository Structure ### Core Files -- `index.html` - Main application entry point -- `main.js` - Core application logic -- `versioning.js` - Version management and update handling +- `package.json` - NPM package configuration with scripts and dependencies +- `vite.config.ts` - Vite build configuration +- `tsconfig.json` - TypeScript compiler configuration -### Key Directories +### Source Directories -- `modules/` - core functionality modules: - - `modules/ui/` - UI components (editors, tools, style management) - - `modules/dynamic/` - runtime modules (export, installation) - - `modules/renderers/` - drawing and rendering logic -- `utils/` - utility libraries (math, arrays, strings, etc.) -- `styles/` - visual style presets (JSON files) -- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.) -- `images/` - backgrounds, UI elements -- `charges/` - heraldic symbols and coat of arms elements -- `config/` - Heightmap templates and configurations -- `heightmaps/` - Terrain generation data +- `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`) ## Common Tasks ### Making Code Changes -1. Edit JavaScript files directly (no compilation needed) -2. Refresh browser to see changes immediately -3. **ALWAYS test map generation** after making changes -4. Update version in `versioning.js` for all changes -5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`) +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`) ### Debugging Map Generation @@ -71,19 +89,30 @@ Always reference these instructions first and fallback to search or bash command ### Application Won't Load -- Ensure using HTTP server (not file://) -- Check console for JavaScript errors +- 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`) + +### Build Failures + +- 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 ### Map Generation Fails - 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 client-side application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. Always validate that your changes preserve the core map generation functionality. +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. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index acedeb18..641ad1fa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ # Type of change - + - [ ] Bug fix - [ ] New feature diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..55bc8109 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,51 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ['master'] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Build + run: npm run build + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + # Upload dist folder + path: './dist' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..dc7ad769 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Code quality + +on: + push: + pull_request: + +jobs: + quality: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + - name: Run Biome + run: biome ci . \ No newline at end of file diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..b41b4ac2 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,25 @@ +name: Playwright Tests +on: + pull_request: + branches: [ master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: '24' + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npm run test:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..7d02cf0f --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,17 @@ +name: Unit Tests +on: + pull_request: + branches: [ master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: '24' + - name: Install dependencies + run: npm ci + - name: Run Unit tests + run: npm run test \ No newline at end of file diff --git a/.gitignore b/.gitignore index b0a273f0..c730ec13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .vscode .idea /node_modules +*/node_modules /dist /coverage +/playwright-report +/test-results \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 6afdcc7d..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.1.0", - "configurations": [ - { - "name": "Debug", - "type": "chrome", - "request": "launch", - "file": "${workspaceFolder}/index.html" - } - ] -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fbf5a425..58f9f058 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,27 @@ +# Build stage +FROM node:24-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY ./src ./src +COPY ./public ./public +COPY vite.config.js . + +# Build the application +RUN npm run build + +# Production stage FROM nginx:stable-alpine -# Copy the contents of the repo to the container -COPY . /usr/share/nginx/html +# Copy built files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html -# Move the customized nginx config file to the nginx folder -RUN mv /usr/share/nginx/html/.docker/default.conf /etc/nginx/conf.d/default.conf +# Copy the customized nginx config file to the nginx folder +COPY .docker/default.conf /etc/nginx/conf.d/default.conf diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..fc1bbe74 --- /dev/null +++ b/biome.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["src/**/*.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useTemplate": { + "level": "warn", + "fix": "safe" + }, + "noNonNullAssertion": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noGlobalIsNan": { + "level": "error", + "fix": "safe" + } + }, + "correctness": { + "noUnusedVariables": { + "level": "error", + "fix": "safe" + }, + "useParseIntRadix": { + "fix": "safe", + "level": "error" + } + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/modules/biomes.js b/modules/biomes.js deleted file mode 100644 index 06280fad..00000000 --- a/modules/biomes.js +++ /dev/null @@ -1,128 +0,0 @@ -"use strict"; - -window.Biomes = (function () { - const MIN_LAND_HEIGHT = 20; - - const getDefault = () => { - const name = [ - "Marine", - "Hot desert", - "Cold desert", - "Savanna", - "Grassland", - "Tropical seasonal forest", - "Temperate deciduous forest", - "Tropical rainforest", - "Temperate rainforest", - "Taiga", - "Tundra", - "Glacier", - "Wetland" - ]; - - const color = [ - "#466eab", - "#fbe79f", - "#b5b887", - "#d2d082", - "#c8d68f", - "#b6d95d", - "#29bc56", - "#7dcb35", - "#409c43", - "#4b6b32", - "#96784b", - "#d5e7eb", - "#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, 250]; - const icons = [ - {}, - {dune: 3, cactus: 6, deadTree: 1}, - {dune: 9, deadTree: 1}, - {acacia: 1, grass: 9}, - {grass: 1}, - {acacia: 8, palm: 1}, - {deciduous: 1}, - {acacia: 5, palm: 3, deciduous: 1, swamp: 1}, - {deciduous: 6, swamp: 1}, - {conifer: 1}, - {grass: 1}, - {}, - {swamp: 1} - ]; - const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost - const biomesMartix = [ - // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet - new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]), - new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]), - new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]), - new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]), - new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10]) - ]; - - // parse icons weighted array into a simple array - for (let i = 0; i < icons.length; i++) { - const parsed = []; - for (const icon in icons[i]) { - for (let j = 0; j < icons[i][icon]; j++) { - parsed.push(icon); - } - } - icons[i] = parsed; - } - - return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost}; - }; - - // assign biome id for each cell - function define() { - TIME && console.time("defineBiomes"); - - const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells; - const {temp, prec} = grid.cells; - pack.cells.biome = new Uint8Array(pack.cells.i.length); // biomes array - - for (let cellId = 0; cellId < heights.length; cellId++) { - const height = heights[cellId]; - const moisture = height < MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId); - const temperature = temp[gridReference[cellId]]; - pack.cells.biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId])); - } - - function calculateMoisture(cellId) { - let moisture = prec[gridReference[cellId]]; - if (riverIds[cellId]) moisture += Math.max(flux[cellId] / 10, 2); - - const moistAround = neighbors[cellId] - .filter(neibCellId => heights[neibCellId] >= MIN_LAND_HEIGHT) - .map(c => prec[gridReference[c]]) - .concat([moisture]); - return rn(4 + d3.mean(moistAround)); - } - - TIME && console.timeEnd("defineBiomes"); - } - - function getId(moisture, temperature, height, hasRiver) { - if (height < 20) return 0; // all water cells: marine biome - if (temperature < -5) return 11; // too cold: permafrost biome - if (temperature >= 25 && !hasRiver && moisture < 8) return 1; // too hot and dry: hot desert biome - if (isWetland(moisture, temperature, height)) return 12; // too wet: wetland biome - - // in other cases use biome matrix - const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4] - const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25] - return biomesData.biomesMartix[moistureBand][temperatureBand]; - } - - function isWetland(moisture, temperature, height) { - if (temperature <= -2) return false; // too cold - if (moisture > 40 && height < 25) return true; // near coast - if (moisture > 24 && height > 24 && height < 60) return true; // off coast - return false; - } - - return {getDefault, define, getId}; -})(); diff --git a/modules/burgs-generator.js b/modules/burgs-generator.js deleted file mode 100644 index 1e530fe2..00000000 --- a/modules/burgs-generator.js +++ /dev/null @@ -1,597 +0,0 @@ -"use strict"; - -window.Burgs = (() => { - const generate = () => { - TIME && console.time("generateBurgs"); - const {cells} = pack; - - let burgs = [0]; // burgs array - cells.burg = new Uint16Array(cells.i.length); - - const populatedCells = cells.i.filter(i => cells.s[i] > 0 && cells.culture[i]); - if (!populatedCells.length) { - ERROR && console.error("There is no populated cells with culture assigned. Cannot generate states"); - return burgs; - } - - let quadtree = d3.quadtree(); - generateCapitals(); - generateTowns(); - - pack.burgs = burgs; - shift(); - - TIME && console.timeEnd("generateBurgs"); - - function generateCapitals() { - const randomize = score => score * (0.5 + Math.random() * 0.5); - const score = new Int16Array(cells.s.map(randomize)); - const sorted = populatedCells.sort((a, b) => score[b] - score[a]); - - const capitalsNumber = getCapitalsNumber(); - let spacing = (graphWidth + graphHeight) / 2 / capitalsNumber; // min distance between capitals - - for (let i = 0; burgs.length <= capitalsNumber; i++) { - const cell = sorted[i]; - const [x, y] = cells.p[cell]; - - if (quadtree.find(x, y, spacing) === undefined) { - burgs.push({cell, x, y}); - quadtree.add([x, y]); - } - - // reset if all cells were checked - if (i === sorted.length - 1) { - WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing"); - quadtree = d3.quadtree(); - i = -1; - burgs = [0]; - spacing /= 1.2; - } - } - - burgs.forEach((burg, burgId) => { - if (!burgId) return; - burg.i = burgId; - burg.state = burgId; - burg.culture = cells.culture[burg.cell]; - burg.name = Names.getCultureShort(burg.culture); - burg.feature = cells.f[burg.cell]; - burg.capital = 1; - cells.burg[burg.cell] = burgId; - }); - } - - function generateTowns() { - const randomize = score => score * gauss(1, 3, 0, 20, 3); - const score = new Int16Array(cells.s.map(randomize)); - const sorted = populatedCells.sort((a, b) => score[b] - score[a]); - - const burgsNumber = getTownsNumber(); - let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between town - - for (let added = 0; added < burgsNumber && spacing > 1; ) { - for (let i = 0; added < burgsNumber && i < sorted.length; i++) { - if (cells.burg[sorted[i]]) continue; - const cell = sorted[i]; - const [x, y] = cells.p[cell]; - - const minSpacing = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform - if (quadtree.find(x, y, minSpacing) !== undefined) continue; // to close to existing burg - - const burgId = burgs.length; - const culture = cells.culture[cell]; - const name = Names.getCulture(culture); - const feature = cells.f[cell]; - burgs.push({cell, x, y, i: burgId, state: 0, culture, name, feature, capital: 0}); - added++; - cells.burg[cell] = burgId; - } - - spacing *= 0.5; - } - } - - function getCapitalsNumber() { - let number = +byId("statesNumber").value; - - if (populatedCells.length < number * 10) { - number = Math.floor(populatedCells.length / 10); - WARN && console.warn(`Not enough populated cells. Generating only ${number} capitals/states`); - } - - return number; - } - - function getTownsNumber() { - const manorsInput = byId("manorsInput"); - const isAuto = manorsInput.value === "1000"; // '1000' is considered as auto - if (isAuto) return rn(populatedCells.length / 5 / (grid.points.length / 10000) ** 0.8); - - return Math.min(manorsInput.valueAsNumber, populatedCells.length); - } - }; - - // define port status and shift ports and burgs on rivers close to the edge of the water body - function shift() { - const {cells, features, burgs} = pack; - const temp = grid.cells.temp; - - // port is a capital with any harbor OR any burg with a safe harbor - // safe harbor is a cell having just one adjacent water cell - const featurePortCandidates = {}; - for (const burg of burgs) { - if (!burg.i || burg.lock) continue; - delete burg.port; // reset port status - const cellId = burg.cell; - - const haven = cells.haven[cellId]; - const harbor = cells.harbor[cellId]; - const featureId = cells.f[haven]; - if (!featureId) continue; // no adjacent water body - - const isMulticell = features[featureId].cells > 1; - const isHarbor = (harbor && burg.capital) || harbor === 1; - const isFrozen = temp[cells.g[cellId]] <= 0; - - if (isMulticell && isHarbor && !isFrozen) { - if (!featurePortCandidates[featureId]) featurePortCandidates[featureId] = []; - featurePortCandidates[featureId].push(burg); - } - } - - // shift ports to the edge of the water body - Object.entries(featurePortCandidates).forEach(([featureId, burgs]) => { - if (burgs.length < 2) return; // only one port on water body - skip - burgs.forEach(burg => { - burg.port = featureId; - const haven = cells.haven[burg.cell]; - const [x, y] = getCloseToEdgePoint(burg.cell, haven); - burg.x = x; - burg.y = y; - }); - }); - - // shift non-port river burgs a bit - for (const burg of burgs) { - if (!burg.i || burg.lock || burg.port || !cells.r[burg.cell]) continue; - const cellId = burg.cell; - const shift = Math.min(cells.fl[cellId] / 150, 1); - burg.x = cellId % 2 ? rn(burg.x + shift, 2) : rn(burg.x - shift, 2); - burg.y = cells.r[cellId] % 2 ? rn(burg.y + shift, 2) : rn(burg.y - shift, 2); - } - - 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]; - } - } - - const specify = () => { - TIME && console.time("specifyBurgs"); - - pack.burgs.forEach(burg => { - if (!burg.i || burg.removed || burg.lock) return; - definePopulation(burg); - defineEmblem(burg); - defineFeatures(burg); - }); - - const populations = pack.burgs - .filter(b => b.i && !b.removed) - .map(b => b.population) - .sort((a, b) => a - b); // ascending - - pack.burgs.forEach(burg => { - if (!burg.i || burg.removed) return; - defineGroup(burg, populations); - }); - - TIME && console.timeEnd("specifyBurgs"); - }; - - const getType = (cellId, port) => { - const {cells, features} = pack; - - if (port) return "Naval"; - - const haven = cells.haven[cellId]; - if (haven !== undefined && features[cells.f[haven]].type === "lake") return "Lake"; - - if (cells.h[cellId] > 60) return "Highland"; - - if (cells.r[cellId] && cells.fl[cellId] >= 100) return "River"; - - const biome = cells.biome[cellId]; - const population = cells.pop[cellId]; - if (!cells.burg[cellId] || population <= 5) { - if (population < 5 && [1, 2, 3, 4].includes(biome)) return "Nomadic"; - if (biome > 4 && biome < 10) return "Hunting"; - } - - return "Generic"; - }; - - function definePopulation(burg) { - const cellId = burg.cell; - let population = pack.cells.s[cellId] / 5; - if (burg.capital) population *= 1.5; - const connectivityRate = Routes.getConnectivityRate(cellId); - if (connectivityRate) population *= connectivityRate; - population *= gauss(1, 1, 0.25, 4, 5); // randomize - population += ((burg.i % 100) - (cellId % 100)) / 1000; // unround - burg.population = rn(Math.max(population, 0.01), 3); - } - - function defineEmblem(burg) { - burg.type = getType(burg.cell, burg.port); - - const state = pack.states[burg.state]; - const stateCOA = state.coa; - - let kinship = 0.25; - if (burg.capital) kinship += 0.1; - else if (burg.port) kinship -= 0.1; - if (burg.culture !== state.culture) kinship -= 0.25; - - const type = burg.capital && P(0.2) ? "Capital" : burg.type === "Generic" ? "City" : burg.type; - burg.coa = COA.generate(stateCOA, kinship, null, type); - burg.coa.shield = COA.getShield(burg.culture, burg.state); - } - - function defineFeatures(burg) { - const pop = burg.population; - burg.citadel = Number(burg.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1)); - burg.plaza = Number( - Routes.isCrossroad(burg.cell) || (Routes.hasRoad(burg.cell) && P(0.7)) || pop > 20 || (pop > 10 && P(0.8)) - ); - burg.walls = Number(burg.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1)); - burg.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && burg.walls && P(0.4))); - const religion = pack.cells.religion[burg.cell]; - const theocracy = pack.states[burg.state].form === "Theocracy"; - burg.temple = Number( - (religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) - ); - } - - const getDefaultGroups = () => [ - {name: "capital", active: true, order: 9, features: {capital: true}, preview: "watabou-city"}, - {name: "city", active: true, order: 8, percentile: 90, min: 5, preview: "watabou-city"}, - { - name: "fort", - active: true, - features: {citadel: true, walls: false, plaza: false, port: false}, - order: 6, - max: 1 - }, - { - name: "monastery", - active: true, - features: {temple: true, walls: false, plaza: false, port: false}, - order: 5, - max: 0.8 - }, - { - name: "caravanserai", - active: true, - features: {port: false, plaza: true}, - order: 4, - max: 0.8, - biomes: [1, 2, 3] - }, - { - name: "trading_post", - active: true, - order: 3, - features: {plaza: true}, - max: 0.8, - biomes: [5, 6, 7, 8, 9, 10, 11, 12] - }, - { - name: "village", - active: true, - order: 2, - min: 0.1, - max: 2, - preview: "watabou-village" - }, - { - name: "hamlet", - active: true, - order: 1, - features: {plaza: false}, - max: 0.1, - preview: "watabou-village" - }, - {name: "town", active: true, order: 7, isDefault: true, preview: "watabou-city"} - ]; - - function defineGroup(burg, populations) { - if (burg.lock && burg.group) { - // locked burgs: don't change group if it still exists - const group = options.burgs.groups.find(g => g.name === burg.group); - if (group) return; - } - - const defaultGroup = options.burgs.groups.find(g => g.isDefault); - if (!defaultGroup) { - ERROR && console.error("No default group defined"); - return; - } - burg.group = defaultGroup.name; - - for (const group of options.burgs.groups) { - if (!group.active) continue; - - if (group.min) { - const isFit = burg.population >= group.min; - if (!isFit) continue; - } - - if (group.max) { - const isFit = burg.population <= group.max; - if (!isFit) continue; - } - - if (group.features) { - const isFit = Object.entries(group.features).every(([feature, value]) => Boolean(burg[feature]) === value); - if (!isFit) continue; - } - - if (group.biomes) { - const isFit = group.biomes.includes(pack.cells.biome[burg.cell]); - if (!isFit) continue; - } - - if (group.percentile) { - const index = populations.indexOf(burg.population); - const isFit = index >= Math.floor((populations.length * group.percentile) / 100); - if (!isFit) continue; - } - - burg.group = group.name; // apply fitting group - return; - } - } - - const previewGeneratorsMap = { - "watabou-city": createWatabouCityLinks, - "watabou-village": createWatabouVillageLinks, - "watabou-dwelling": createWatabouDwellingLinks - }; - - function getPreview(burg) { - if (burg.link) return {link: burg.link, preview: burg.link}; - - const group = options.burgs.groups.find(g => g.name === burg.group); - if (!group?.preview || !previewGeneratorsMap[group.preview]) return {link: null, preview: null}; - - return previewGeneratorsMap[group.preview](burg); - } - - function createWatabouCityLinks(burg) { - const cells = pack.cells; - const {i, name, population: burgPopulation, cell} = burg; - const burgSeed = burg.MFCG || seed + String(burg.i).padStart(4, 0); - - const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385); - const size = minmax(Math.ceil(sizeRaw), 6, 100); - const population = rn(burgPopulation * populationRate * urbanization); - - const river = cells.r[cell] ? 1 : 0; - const coast = Number(burg.port > 0); - const sea = (() => { - if (!coast || !cells.haven[cell]) return null; - - // calculate see direction: 0 = east, 0.5 = north, 1 = west, 1.5 = south - const [x1, y1] = cells.p[cell]; - const [x2, y2] = cells.p[cells.haven[cell]]; - const deg = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI; - - if (deg <= 0) return normalize(Math.abs(deg), 0, 180); - return 2 - normalize(deg, 0, 180); - })(); - - const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8]; - const farms = +arableBiomes.includes(cells.biome[cell]); - - const citadel = +burg.citadel; - const urban_castle = +(citadel && each(2)(i)); - - const hub = Routes.isCrossroad(cell); - const walls = +burg.walls; - const plaza = +burg.plaza; - const temple = +burg.temple; - const shantytown = +burg.shanty; - - const style = "natural"; - - const url = new URL("https://watabou.github.io/city-generator/"); - url.search = new URLSearchParams({ - name, - population, - size, - seed: burgSeed, - river, - coast, - farms, - citadel, - urban_castle, - hub, - plaza, - temple, - walls, - shantytown, - gates: -1, - style - }); - if (sea) url.searchParams.append("sea", sea); - - const link = url.toString(); - return {link, preview: link + "&preview=1"}; - } - - function createWatabouVillageLinks(burg) { - const {cells, features} = pack; - const {i, population, cell} = burg; - - const burgSeed = seed + String(i).padStart(4, 0); - const pop = rn(population * populationRate * urbanization); - const tags = []; - - if (cells.r[cell] && cells.haven[cell]) tags.push("estuary"); - else if (cells.haven[cell] && features[cells.f[cell]].cells === 1) tags.push("island,district"); - else if (burg.port) tags.push("coast"); - else if (cells.conf[cell]) tags.push("confluence"); - else if (cells.r[cell]) tags.push("river"); - else if (pop < 200 && each(4)(cell)) tags.push("pond"); - - const connectivityRate = Routes.getConnectivityRate(cell); - tags.push(connectivityRate > 1 ? "highway" : connectivityRate === 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]; - if (!arableBiomes.includes(biome)) tags.push("uncultivated"); - else if (each(6)(cell)) tags.push("farmland"); - - const temp = grid.cells.temp[cells.g[cell]]; - if (temp <= 0 || temp > 28 || (temp > 25 && each(3)(cell))) tags.push("no orchards"); - - if (!burg.plaza) tags.push("no square"); - if (burg.walls) tags.push("palisade"); - - if (pop < 100) tags.push("sparse"); - else if (pop > 300) tags.push("dense"); - - const width = (() => { - if (pop > 1500) return 1600; - if (pop > 1000) return 1400; - if (pop > 500) return 1000; - if (pop > 200) return 800; - if (pop > 100) return 600; - return 400; - })(); - const height = rn(width / 2.05); - - const style = (() => { - if ([1, 2].includes(biome)) return "sand"; - if (temp <= 5 || [9, 10, 11].includes(biome)) return "snow"; - return "default"; - })(); - - const url = new URL("https://watabou.github.io/village-generator/"); - url.search = new URLSearchParams({pop, name: burg.name, seed: burgSeed, width, height, style, tags}); - - const link = url.toString(); - return {link, preview: link + "&preview=1"}; - } - - function createWatabouDwellingLinks(burg) { - const burgSeed = seed + String(burg.i).padStart(4, 0); - const pop = rn(burg.population * populationRate * urbanization); - - const tags = (() => { - if (pop > 200) return ["large", "tall"]; - if (pop > 100) return ["large"]; - if (pop > 50) return ["tall"]; - if (pop > 20) return ["low"]; - return ["small"]; - })(); - - const url = new URL("https://watabou.github.io/dwellings/"); - url.search = new URLSearchParams({pop, name: "", seed: burgSeed, tags}); - - const link = url.toString(); - return {link, preview: link + "&preview=1"}; - } - - function add([x, y]) { - const {cells} = pack; - - const burgId = pack.burgs.length; - const cellId = findCell(x, y); - const culture = cells.culture[cellId]; - const name = Names.getCulture(culture); - const state = cells.state[cellId]; - const feature = cells.f[cellId]; - - const burg = { - cell: cellId, - x, - y, - i: burgId, - state, - culture, - name, - feature, - capital: 0, - port: 0 - }; - definePopulation(burg); - defineEmblem(burg); - defineFeatures(burg); - - const populations = pack.burgs - .filter(b => b.i && !b.removed) - .map(b => b.population) - .sort((a, b) => a - b); // ascending - defineGroup(burg, populations); - - pack.burgs.push(burg); - cells.burg[cellId] = burgId; - - const newRoute = Routes.connect(cellId); - if (newRoute && layerIsOn("toggleRoutes")) drawRoute(newRoute); - - drawBurgIcon(burg); - drawBurgLabel(burg); - - return burgId; - } - - function changeGroup(burg, group) { - if (group) { - burg.group = group; - } else { - const validBurgs = pack.burgs.filter(b => b.i && !b.removed); - const populations = validBurgs.map(b => b.population).sort((a, b) => a - b); - defineGroup(burg, populations); - } - - drawBurgIcon(burg); - drawBurgLabel(burg); - } - - function remove(burgId) { - const burg = pack.burgs[burgId]; - if (!burg) return tip(`Burg ${burgId} not found`, false, "error"); - - pack.cells.burg[burg.cell] = 0; - burg.removed = true; - - const noteId = notes.findIndex(note => note.id === `burg${burgId}`); - if (noteId !== -1) notes.splice(noteId, 1); - - if (burg.coa) { - byId("burgCOA" + burgId)?.remove(); - emblems.select(`#burgEmblems > use[data-i='${burgId}']`).remove(); - delete burg.coa; - } - - removeBurgIcon(burg.i); - removeBurgLabel(burg.i); - } - - return {generate, getDefaultGroups, shift, specify, defineGroup, getPreview, getType, add, changeGroup, remove}; -})(); diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js deleted file mode 100644 index 34dc5edd..00000000 --- a/modules/cultures-generator.js +++ /dev/null @@ -1,618 +0,0 @@ -"use strict"; - -window.Cultures = (function () { - let cells; - - const generate = function () { - TIME && console.time("generateCultures"); - cells = pack.cells; - - const cultureIds = new Uint16Array(cells.i.length); // cell cultures - - const culturesInputNumber = +byId("culturesInput").value; - const culturesInSetNumber = +byId("culturesSet").selectedOptions[0].dataset.max; - let count = Math.min(culturesInputNumber, culturesInSetNumber); - - const populated = cells.i.filter(i => cells.s[i]); // populated cells - if (populated.length < count * 25) { - count = Math.floor(populated.length / 50); - if (!count) { - WARN && console.warn(`There are no populated cells. Cannot generate cultures`); - pack.cultures = [{name: "Wildlands", i: 0, base: 1, shield: "round"}]; - cells.culture = cultureIds; - - alertMessage.innerHTML = /* html */ `The climate is harsh and people cannot live in this world.
- No cultures, states and burgs will be created.
- Please consider changing climate settings in the World Configurator`; - - $("#alert").dialog({ - resizable: false, - title: "Extreme climate warning", - buttons: { - Ok: function () { - $(this).dialog("close"); - } - } - }); - return; - } else { - WARN && console.warn(`Not enough populated cells (${populated.length}). Will generate only ${count} cultures`); - alertMessage.innerHTML = /* html */ ` There are only ${populated.length} populated cells and it's insufficient livable area.
- Only ${count} out of ${culturesInput.value} requested cultures will be generated.
- Please consider changing climate settings in the World Configurator`; - $("#alert").dialog({ - resizable: false, - title: "Extreme climate warning", - buttons: { - Ok: function () { - $(this).dialog("close"); - } - } - }); - } - } - - const cultures = (pack.cultures = selectCultures(count)); - const centers = d3.quadtree(); - const colors = getColors(count); - const emblemShape = document.getElementById("emblemShape").value; - - const codes = []; - - cultures.forEach(function (c, i) { - const newId = i + 1; - - if (c.lock) { - codes.push(c.code); - centers.add(c.center); - - for (const i of cells.i) { - if (cells.culture[i] === c.i) cultureIds[i] = newId; - } - - c.i = newId; - return; - } - - const sortingFn = c.sort ? c.sort : i => cells.s[i]; - const center = placeCenter(sortingFn); - - centers.add(cells.p[center]); - c.center = center; - c.i = newId; - delete c.odd; - delete c.sort; - c.color = colors[i]; - c.type = defineCultureType(center); - c.expansionism = defineCultureExpansionism(c.type); - c.origins = [0]; - c.code = abbreviate(c.name, codes); - codes.push(c.code); - cultureIds[center] = newId; - if (emblemShape === "random") c.shield = getRandomShield(); - }); - - cells.culture = cultureIds; - - function placeCenter(sortingFn) { - let spacing = (graphWidth + graphHeight) / 2 / count; - const MAX_ATTEMPTS = 100; - - const sorted = [...populated].sort((a, b) => sortingFn(b) - sortingFn(a)); - const max = Math.floor(sorted.length / 2); - - let cellId = 0; - for (let i = 0; i < MAX_ATTEMPTS; i++) { - cellId = sorted[biased(0, max, 5)]; - spacing *= 0.9; - if (!cultureIds[cellId] && !centers.find(cells.p[cellId][0], cells.p[cellId][1], spacing)) break; - } - - return cellId; - } - - // the first culture with id 0 is for wildlands - cultures.unshift({name: "Wildlands", i: 0, base: 1, origins: [null], shield: "round"}); - - // make sure all bases exist in nameBases - if (!nameBases.length) { - ERROR && console.error("Name base is empty, default nameBases will be applied"); - nameBases = Names.getNameBases(); - } - - cultures.forEach(c => (c.base = c.base % nameBases.length)); - - function selectCultures(culturesNumber) { - let defaultCultures = getDefault(culturesNumber); - const cultures = []; - - pack.cultures?.forEach(function (culture) { - if (culture.lock && !culture.removed) cultures.push(culture); - }); - - if (!cultures.length) { - if (culturesNumber === defaultCultures.length) return defaultCultures; - if (defaultCultures.every(d => d.odd === 1)) return defaultCultures.splice(0, culturesNumber); - } - - for (let culture, rnd, i = 0; cultures.length < culturesNumber && defaultCultures.length > 0; ) { - do { - rnd = rand(defaultCultures.length - 1); - culture = defaultCultures[rnd]; - i++; - } while (i < 200 && !P(culture.odd)); - cultures.push(culture); - defaultCultures.splice(rnd, 1); - } - return cultures; - } - - // set culture type based on culture center position - function defineCultureType(i) { - if (cells.h[i] < 70 && [1, 2, 4].includes(cells.biome[i])) return "Nomadic"; // high penalty in forest biomes and near coastline - if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations - const f = pack.features[cells.f[cells.haven[i]]]; // opposite feature - if (f.type === "lake" && f.cells > 5) return "Lake"; // low water cross penalty and high for growth not along coastline - if ( - (cells.harbor[i] && f.type !== "lake" && P(0.1)) || - (cells.harbor[i] === 1 && P(0.6)) || - (pack.features[cells.f[i]].group === "isle" && P(0.4)) - ) - return "Naval"; // low water cross penalty and high for non-along-coastline growth - if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth - if (cells.t[i] > 2 && [3, 7, 8, 9, 10, 12].includes(cells.biome[i])) return "Hunting"; // high penalty in non-native biomes - return "Generic"; - } - - function defineCultureExpansionism(type) { - let base = 1; // Generic - if (type === "Lake") base = 0.8; - else if (type === "Naval") base = 1.5; - else if (type === "River") base = 0.9; - else if (type === "Nomadic") base = 1.5; - else if (type === "Hunting") base = 0.7; - else if (type === "Highland") base = 1.2; - return rn(((Math.random() * byId("sizeVariety").value) / 2 + 1) * base, 1); - } - - TIME && console.timeEnd("generateCultures"); - }; - - const add = function (center) { - const defaultCultures = getDefault(); - let culture, base, name; - - if (pack.cultures.length < defaultCultures.length) { - // add one of the default cultures - culture = pack.cultures.length; - base = defaultCultures[culture].base; - name = defaultCultures[culture].name; - } else { - // add random culture besed on one of the current ones - culture = rand(pack.cultures.length - 1); - name = Names.getCulture(culture, 5, 8, ""); - base = pack.cultures[culture].base; - } - - const code = abbreviate( - name, - pack.cultures.map(c => c.code) - ); - const i = pack.cultures.length; - const color = getRandomColor(); - - // define emblem shape - let shield = culture.shield; - const emblemShape = document.getElementById("emblemShape").value; - if (emblemShape === "random") shield = getRandomShield(); - - pack.cultures.push({ - name, - color, - base, - center, - i, - expansionism: 1, - type: "Generic", - cells: 0, - area: 0, - rural: 0, - urban: 0, - origins: [pack.cells.culture[center]], - code, - shield - }); - }; - - const getDefault = function (count) { - // generic sorting functions - const cells = pack.cells, - s = cells.s, - sMax = d3.max(s), - t = cells.t, - h = cells.h, - temp = grid.cells.temp; - const n = cell => Math.ceil((s[cell] / sMax) * 3); // normalized cell score - const td = (cell, goal) => { - const d = Math.abs(temp[cells.g[cell]] - goal); - return d ? d + 1 : 1; - }; // temperature difference fee - const bd = (cell, biomes, fee = 4) => (biomes.includes(cells.biome[cell]) ? 1 : fee); // biome difference fee - const sf = (cell, fee = 4) => - cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee; // not on sea coast fee - - if (culturesSet.value === "european") { - return [ - {name: "Shwazen", base: 0, odd: 1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "swiss"}, - {name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "wedged"}, - {name: "Luari", base: 2, odd: 1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "french"}, - {name: "Tallian", base: 3, odd: 1, sort: i => n(i) / td(i, 15), shield: "horsehead"}, - {name: "Astellian", base: 4, odd: 1, sort: i => n(i) / td(i, 16), shield: "spanish"}, - {name: "Slovan", base: 5, odd: 1, sort: i => (n(i) / td(i, 6)) * t[i], shield: "polish"}, - {name: "Norse", base: 6, odd: 1, sort: i => n(i) / td(i, 5), shield: "heater"}, - {name: "Elladan", base: 7, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "boeotian"}, - {name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 15) / t[i], shield: "roman"}, - {name: "Soumi", base: 9, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: "pavise"}, - {name: "Portuzian", base: 13, odd: 1, sort: i => n(i) / td(i, 17) / sf(i), shield: "renaissance"}, - {name: "Vengrian", base: 15, odd: 1, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "horsehead2"}, - {name: "Turchian", base: 16, odd: 0.05, sort: i => n(i) / td(i, 14), shield: "round"}, - {name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "oldFrench"}, - {name: "Keltan", base: 22, odd: 0.05, sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: "oval"} - ]; - } - - if (culturesSet.value === "oriental") { - return [ - {name: "Koryo", base: 10, odd: 1, sort: i => n(i) / td(i, 12) / t[i], shield: "round"}, - {name: "Hantzu", base: 11, odd: 1, sort: i => n(i) / td(i, 13), shield: "banner"}, - {name: "Yamoto", base: 12, odd: 1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"}, - {name: "Turchian", base: 16, odd: 1, sort: i => n(i) / td(i, 12), shield: "round"}, - { - name: "Berberan", - base: 17, - odd: 0.2, - sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], - shield: "oval" - }, - {name: "Eurabic", base: 18, odd: 1, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "oval"}, - {name: "Efratic", base: 23, odd: 0.1, sort: i => (n(i) / td(i, 22)) * t[i], shield: "round"}, - {name: "Tehrani", base: 24, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"}, - {name: "Maui", base: 25, odd: 0.2, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "vesicaPiscis"}, - {name: "Carnatic", base: 26, odd: 0.5, sort: i => n(i) / td(i, 26), shield: "round"}, - {name: "Vietic", base: 29, odd: 0.8, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: "banner"}, - {name: "Guantzu", base: 30, odd: 0.5, sort: i => n(i) / td(i, 17), shield: "banner"}, - {name: "Ulus", base: 31, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"} - ]; - } - - if (culturesSet.value === "english") { - const getName = () => Names.getBase(1, 5, 9, "", 0); - return [ - {name: getName(), base: 1, odd: 1, shield: "heater"}, - {name: getName(), base: 1, odd: 1, shield: "wedged"}, - {name: getName(), base: 1, odd: 1, shield: "swiss"}, - {name: getName(), base: 1, odd: 1, shield: "oldFrench"}, - {name: getName(), base: 1, odd: 1, shield: "swiss"}, - {name: getName(), base: 1, odd: 1, shield: "spanish"}, - {name: getName(), base: 1, odd: 1, shield: "hessen"}, - {name: getName(), base: 1, odd: 1, shield: "fantasy5"}, - {name: getName(), base: 1, odd: 1, shield: "fantasy4"}, - {name: getName(), base: 1, odd: 1, shield: "fantasy1"} - ]; - } - - if (culturesSet.value === "antique") { - return [ - {name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"}, // Roman - {name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 15) / sf(i), shield: "roman"}, // Roman - {name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 16) / sf(i), shield: "roman"}, // Roman - {name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 17) / t[i], shield: "roman"}, // Roman - {name: "Hellenic", base: 7, odd: 1, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"}, // Greek - {name: "Hellenic", base: 7, odd: 1, sort: i => (n(i) / td(i, 19) / sf(i)) * h[i], shield: "boeotian"}, // Greek - {name: "Macedonian", base: 7, odd: 0.5, sort: i => (n(i) / td(i, 12)) * h[i], shield: "round"}, // Greek - {name: "Celtic", base: 22, odd: 1, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: "round"}, - {name: "Germanic", base: 0, odd: 1, sort: i => n(i) / td(i, 10) ** 0.5 / bd(i, [6, 8]), shield: "round"}, - {name: "Persian", base: 24, odd: 0.8, sort: i => (n(i) / td(i, 18)) * h[i], shield: "oval"}, // Iranian - {name: "Scythian", base: 24, odd: 0.5, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [4]), shield: "round"}, // Iranian - {name: "Cantabrian", base: 20, odd: 0.5, sort: i => (n(i) / td(i, 16)) * h[i], shield: "oval"}, // Basque - {name: "Estian", base: 9, odd: 0.2, sort: i => (n(i) / td(i, 5)) * t[i], shield: "pavise"}, // Finnic - {name: "Carthaginian", base: 42, odd: 0.3, sort: i => n(i) / td(i, 20) / sf(i), shield: "oval"}, // Levantine - {name: "Hebrew", base: 42, odd: 0.2, sort: i => (n(i) / td(i, 19)) * sf(i), shield: "oval"}, // Levantine - {name: "Mesopotamian", base: 23, odd: 0.2, sort: i => n(i) / td(i, 22) / bd(i, [1, 2, 3]), shield: "oval"} // Mesopotamian - ]; - } - - if (culturesSet.value === "highFantasy") { - return [ - // fantasy races - { - name: "Quenian (Elfish)", - base: 33, - odd: 1, - sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], - shield: "gondor" - }, // Elves - { - name: "Eldar (Elfish)", - base: 33, - odd: 1, - sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], - shield: "noldor" - }, // Elves - { - name: "Trow (Dark Elfish)", - base: 34, - odd: 0.9, - sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], - shield: "hessen" - }, // Dark Elves - { - name: "Lothian (Dark Elfish)", - base: 34, - odd: 0.3, - sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], - shield: "wedged" - }, // Dark Elves - {name: "Dunirr (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "ironHills"}, // Dwarfs - {name: "Khazadur (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarfs - {name: "Kobold (Goblin)", base: 36, odd: 1, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin - {name: "Uruk (Orkish)", base: 37, odd: 1, sort: i => h[i] * t[i], shield: "urukHai"}, // Orc - { - name: "Ugluk (Orkish)", - base: 37, - odd: 0.5, - sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), - shield: "moriaOrc" - }, // Orc - {name: "Yotunn (Giants)", base: 38, odd: 0.7, sort: i => td(i, -10), shield: "pavise"}, // Giant - {name: "Rake (Drakonic)", base: 39, odd: 0.7, sort: i => -s[i], shield: "fantasy2"}, // Draconic - {name: "Arago (Arachnid)", base: 40, odd: 0.7, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid - {name: "Aj'Snaga (Serpents)", base: 41, odd: 0.7, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"}, // Serpents - // fantasy human - {name: "Anor (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 10), shield: "fantasy5"}, - {name: "Dail (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 13), shield: "roman"}, - {name: "Rohand (Human)", base: 16, odd: 1, sort: i => n(i) / td(i, 16), shield: "round"}, - { - name: "Dulandir (Human)", - base: 31, - odd: 1, - sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], - shield: "easterling" - } - ]; - } - - if (culturesSet.value === "darkFantasy") { - return [ - // common real-world English - {name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "heater"}, - {name: "Enlandic", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"}, - {name: "Westen", base: 1, odd: 1, sort: i => n(i) / td(i, 10), shield: "heater"}, - {name: "Nortumbic", base: 1, odd: 1, sort: i => n(i) / td(i, 7), shield: "heater"}, - {name: "Mercian", base: 1, odd: 1, sort: i => n(i) / td(i, 9), shield: "heater"}, - {name: "Kentian", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"}, - // rare real-world western - {name: "Norse", base: 6, odd: 0.7, sort: i => n(i) / td(i, 5) / sf(i), shield: "oldFrench"}, - {name: "Schwarzen", base: 0, odd: 0.3, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "gonfalon"}, - {name: "Luarian", base: 2, odd: 0.3, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "oldFrench"}, - {name: "Hetallian", base: 3, odd: 0.3, sort: i => n(i) / td(i, 15), shield: "oval"}, - {name: "Astellian", base: 4, odd: 0.3, sort: i => n(i) / td(i, 16), shield: "spanish"}, - // rare real-world exotic - { - name: "Kiswaili", - base: 28, - odd: 0.05, - sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), - shield: "vesicaPiscis" - }, - {name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"}, - {name: "Koryo", base: 10, odd: 0.05, sort: i => n(i) / td(i, 12) / t[i], shield: "round"}, - {name: "Hantzu", base: 11, odd: 0.05, sort: i => n(i) / td(i, 13), shield: "banner"}, - {name: "Yamoto", base: 12, odd: 0.05, sort: i => n(i) / td(i, 15) / t[i], shield: "round"}, - {name: "Guantzu", base: 30, odd: 0.05, sort: i => n(i) / td(i, 17), shield: "banner"}, - { - name: "Ulus", - base: 31, - odd: 0.05, - sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], - shield: "banner" - }, - {name: "Turan", base: 16, odd: 0.05, sort: i => n(i) / td(i, 12), shield: "round"}, - { - name: "Berberan", - base: 17, - odd: 0.05, - sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], - shield: "round" - }, - { - name: "Eurabic", - base: 18, - odd: 0.05, - sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], - shield: "round" - }, - {name: "Slovan", base: 5, odd: 0.05, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"}, - { - name: "Keltan", - base: 22, - odd: 0.1, - sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), - shield: "vesicaPiscis" - }, - {name: "Elladan", base: 7, odd: 0.2, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"}, - {name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"}, - // fantasy races - {name: "Eldar", base: 33, odd: 0.5, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "fantasy5"}, // Elves - {name: "Trow", base: 34, odd: 0.8, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "hessen"}, // Dark Elves - {name: "Durinn", base: 35, odd: 0.8, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarven - {name: "Kobblin", base: 36, odd: 0.8, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin - {name: "Uruk", base: 37, odd: 0.8, sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: "urukHai"}, // Orc - {name: "Yotunn", base: 38, odd: 0.8, sort: i => td(i, -10), shield: "pavise"}, // Giant - {name: "Drake", base: 39, odd: 0.9, sort: i => -s[i], shield: "fantasy2"}, // Draconic - {name: "Rakhnid", base: 40, odd: 0.9, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid - {name: "Aj'Snaga", base: 41, odd: 0.9, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"} // Serpents - ]; - } - - if (culturesSet.value === "random") { - return d3.range(count).map(function () { - const rnd = rand(nameBases.length - 1); - const name = Names.getBaseShort(rnd); - return {name, base: rnd, odd: 1, shield: getRandomShield()}; - }); - } - - // all-world - return [ - {name: "Shwazen", base: 0, odd: 0.7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "hessen"}, - {name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "heater"}, - {name: "Luari", base: 2, odd: 0.6, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "oldFrench"}, - {name: "Tallian", base: 3, odd: 0.6, sort: i => n(i) / td(i, 15), shield: "horsehead2"}, - {name: "Astellian", base: 4, odd: 0.6, sort: i => n(i) / td(i, 16), shield: "spanish"}, - {name: "Slovan", base: 5, odd: 0.7, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"}, - {name: "Norse", base: 6, odd: 0.7, sort: i => n(i) / td(i, 5), shield: "heater"}, - {name: "Elladan", base: 7, odd: 0.7, sort: i => (n(i) / td(i, 18)) * h[i], shield: "boeotian"}, - {name: "Romian", base: 8, odd: 0.7, sort: i => n(i) / td(i, 15), shield: "roman"}, - {name: "Soumi", base: 9, odd: 0.3, sort: i => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: "pavise"}, - {name: "Koryo", base: 10, odd: 0.1, sort: i => n(i) / td(i, 12) / t[i], shield: "round"}, - {name: "Hantzu", base: 11, odd: 0.1, sort: i => n(i) / td(i, 13), shield: "banner"}, - {name: "Yamoto", base: 12, odd: 0.1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"}, - {name: "Portuzian", base: 13, odd: 0.4, sort: i => n(i) / td(i, 17) / sf(i), shield: "spanish"}, - {name: "Nawatli", base: 14, odd: 0.1, sort: i => h[i] / td(i, 18) / bd(i, [7]), shield: "square"}, - {name: "Vengrian", base: 15, odd: 0.2, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "wedged"}, - {name: "Turchian", base: 16, odd: 0.2, sort: i => n(i) / td(i, 13), shield: "round"}, - { - name: "Berberan", - base: 17, - odd: 0.1, - sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], - shield: "round" - }, - {name: "Eurabic", base: 18, odd: 0.2, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"}, - {name: "Inuk", base: 19, odd: 0.05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield: "square"}, - {name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "spanish"}, - {name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"}, - { - name: "Keltan", - base: 22, - odd: 0.05, - sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], - shield: "vesicaPiscis" - }, - {name: "Efratic", base: 23, odd: 0.05, sort: i => (n(i) / td(i, 22)) * t[i], shield: "diamond"}, - {name: "Tehrani", base: 24, odd: 0.1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"}, - {name: "Maui", base: 25, odd: 0.05, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "round"}, - {name: "Carnatic", base: 26, odd: 0.05, sort: i => n(i) / td(i, 26), shield: "round"}, - {name: "Inqan", base: 27, odd: 0.05, sort: i => h[i] / td(i, 13), shield: "square"}, - {name: "Kiswaili", base: 28, odd: 0.1, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: "vesicaPiscis"}, - {name: "Vietic", base: 29, odd: 0.1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: "banner"}, - {name: "Guantzu", base: 30, odd: 0.1, sort: i => n(i) / td(i, 17), shield: "banner"}, - {name: "Ulus", base: 31, odd: 0.1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"}, - {name: "Hebrew", base: 42, odd: 0.2, sort: i => (n(i) / td(i, 18)) * sf(i), shield: "oval"} // Levantine - ]; - }; - - // expand cultures across the map (Dijkstra-like algorithm) - const expand = function () { - TIME && console.time("expandCultures"); - const {cells, cultures} = pack; - - const queue = new FlatQueue(); - const cost = []; - - const neutralRate = byId("neutralRate")?.valueAsNumber || 1; - const maxExpansionCost = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth - - // remove culture from all cells except of locked - const hasLocked = cultures.some(c => !c.removed && c.lock); - if (hasLocked) { - for (const cellId of cells.i) { - const culture = cultures[cells.culture[cellId]]; - if (culture.lock) continue; - cells.culture[cellId] = 0; - } - } else { - cells.culture = new Uint16Array(cells.i.length); - } - - for (const culture of cultures) { - if (!culture.i || culture.removed || culture.lock) continue; - queue.push({cellId: culture.center, cultureId: culture.i, priority: 0}, 0); - } - - while (queue.length) { - const {cellId, priority, cultureId} = queue.pop(); - const {type, expansionism} = cultures[cultureId]; - - cells.c[cellId].forEach(neibCellId => { - if (hasLocked) { - const neibCultureId = cells.culture[neibCellId]; - if (neibCultureId && cultures[neibCultureId].lock) return; // do not overwrite cell of locked culture - } - - const biome = cells.biome[neibCellId]; - const biomeCost = getBiomeCost(cultureId, biome, type); - const biomeChangeCost = biome === cells.biome[neibCellId] ? 0 : 20; // penalty on biome change - const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type); - const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type); - const typeCost = getTypeCost(cells.t[neibCellId], type); - - const cellCost = (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / expansionism; - const totalCost = priority + cellCost; - - if (totalCost > maxExpansionCost) return; - - if (!cost[neibCellId] || totalCost < cost[neibCellId]) { - if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell - cost[neibCellId] = totalCost; - queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost); - } - }); - } - - function getBiomeCost(c, biome, type) { - if (cells.biome[cultures[c].center] === biome) return 10; // tiny penalty for native biome - if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters - if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads - return biomesData.cost[biome] * 2; // general non-native biome penalty - } - - function getHeightCost(i, h, type) { - const f = pack.features[cells.f[i]], - a = cells.area[i]; - if (type === "Lake" && f.type === "lake") return 10; // no lake crossing penalty for Lake cultures - if (type === "Naval" && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures - if (type === "Nomadic" && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads - if (h < 20) return a * 6; // general sea/lake crossing penalty - if (type === "Highland" && h < 44) return 3000; // giant penalty for highlanders on lowlands - if (type === "Highland" && h < 62) return 200; // giant penalty for highlanders on lowhills - if (type === "Highland") return 0; // no penalty for highlanders on highlands - if (h >= 67) return 200; // general mountains crossing penalty - if (h >= 44) return 30; // general hills crossing penalty - return 0; - } - - function getRiverCost(riverId, cellId, type) { - if (type === "River") return riverId ? 0 : 100; // penalty for river cultures - if (!riverId) return 0; // no penalty for others if there is no river - return minmax(cells.fl[cellId] / 10, 20, 100); // river penalty from 20 to 100 based on flux - } - - function getTypeCost(t, type) { - if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline - if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads - if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals - return 0; - } - - TIME && console.timeEnd("expandCultures"); - }; - - const getRandomShield = function () { - const type = rw(COA.shields.types); - return rw(COA.shields[type]); - }; - - return {generate, add, expand, getDefault, getRandomShield}; -})(); diff --git a/modules/features.js b/modules/features.js deleted file mode 100644 index 714d4f38..00000000 --- a/modules/features.js +++ /dev/null @@ -1,267 +0,0 @@ -"use strict"; - -window.Features = (function () { - const DEEPER_LAND = 3; - const LANDLOCKED = 2; - const LAND_COAST = 1; - const UNMARKED = 0; - const WATER_COAST = -1; - const DEEP_WATER = -2; - - // calculate distance to coast for every cell - function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) { - for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) { - marked = 0; - const prevDistance = distance - increment; - for (let cellId = 0; cellId < neighbors.length; cellId++) { - if (distanceField[cellId] !== prevDistance) continue; - - for (const neighborId of neighbors[cellId]) { - if (distanceField[neighborId] !== UNMARKED) continue; - distanceField[neighborId] = distance; - marked++; - } - } - } - } - - // mark Grid features (ocean, lakes, islands) and calculate distance field - function markupGrid() { - TIME && console.time("markupGrid"); - Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode - - const {h: heights, c: neighbors, b: borderCells, i} = grid.cells; - const cellsNumber = i.length; - const distanceField = new Int8Array(cellsNumber); // gird.cells.t - const featureIds = new Uint16Array(cellsNumber); // gird.cells.f - const features = [0]; - - const queue = [0]; - for (let featureId = 1; queue[0] !== -1; featureId++) { - const firstCell = queue[0]; - featureIds[firstCell] = featureId; - - const land = heights[firstCell] >= 20; - let border = false; // set true if feature touches map edge - - while (queue.length) { - const cellId = queue.pop(); - if (!border && borderCells[cellId]) border = true; - - for (const neighborId of neighbors[cellId]) { - const isNeibLand = heights[neighborId] >= 20; - - if (land === isNeibLand && featureIds[neighborId] === UNMARKED) { - featureIds[neighborId] = featureId; - queue.push(neighborId); - } else if (land && !isNeibLand) { - distanceField[cellId] = LAND_COAST; - distanceField[neighborId] = WATER_COAST; - } - } - } - - const type = land ? "island" : border ? "ocean" : "lake"; - features.push({i: featureId, land, border, type}); - - queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell - } - - // markup deep ocean cells - markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); - - grid.cells.t = distanceField; - grid.cells.f = featureIds; - grid.features = features; - - TIME && console.timeEnd("markupGrid"); - } - - // mark Pack features (ocean, lakes, islands), calculate distance field and add properties - function markupPack() { - TIME && console.time("markupPack"); - - const {cells, vertices} = pack; - const {c: neighbors, b: borderCells, i} = cells; - const packCellsNumber = i.length; - if (!packCellsNumber) return; // no cells -> there is nothing to do - - const distanceField = new Int8Array(packCellsNumber); // pack.cells.t - const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f - const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell - const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells - const features = [0]; - - const queue = [0]; - for (let featureId = 1; queue[0] !== -1; featureId++) { - const firstCell = queue[0]; - featureIds[firstCell] = featureId; - - const land = isLand(firstCell); - let border = Boolean(borderCells[firstCell]); // true if feature touches map border - let totalCells = 1; // count cells in a feature - - while (queue.length) { - const cellId = queue.pop(); - if (borderCells[cellId]) border = true; - if (!border && borderCells[cellId]) border = true; - - for (const neighborId of neighbors[cellId]) { - const isNeibLand = isLand(neighborId); - - if (land && !isNeibLand) { - distanceField[cellId] = LAND_COAST; - distanceField[neighborId] = WATER_COAST; - if (!haven[cellId]) defineHaven(cellId); - } else if (land && isNeibLand) { - if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST) - distanceField[neighborId] = LANDLOCKED; - else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST) - distanceField[cellId] = LANDLOCKED; - } - - if (!featureIds[neighborId] && land === isNeibLand) { - queue.push(neighborId); - featureIds[neighborId] = featureId; - totalCells++; - } - } - } - - features.push(addFeature({firstCell, land, border, featureId, totalCells})); - queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell - } - - markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land - markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water - - pack.cells.t = distanceField; - pack.cells.f = featureIds; - pack.cells.haven = haven; - pack.cells.harbor = harbor; - pack.features = features; - - TIME && console.timeEnd("markupPack"); - - function defineHaven(cellId) { - const waterCells = neighbors[cellId].filter(isWater); - const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId])); - const closest = distances.indexOf(Math.min.apply(Math, distances)); - - haven[cellId] = waterCells[closest]; - harbor[cellId] = waterCells.length; - } - - function addFeature({firstCell, land, border, featureId, totalCells}) { - const type = land ? "island" : border ? "ocean" : "lake"; - const [startCell, featureVertices] = getCellsData(type, firstCell); - const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex])); - const area = d3.polygonArea(points); // feature perimiter area - const absArea = Math.abs(rn(area)); - - const feature = { - i: featureId, - type, - land, - border, - cells: totalCells, - firstCell: startCell, - vertices: featureVertices, - area: absArea - }; - - if (type === "lake") { - if (area > 0) feature.vertices = feature.vertices.reverse(); - feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat()); - feature.height = Lakes.getHeight(feature); - } - - return feature; - - function getCellsData(featureType, firstCell) { - if (featureType === "ocean") return [firstCell, []]; - - const getType = cellId => featureIds[cellId]; - const type = getType(firstCell); - const ofSameType = cellId => getType(cellId) === type; - const ofDifferentType = cellId => getType(cellId) !== type; - - const startCell = findOnBorderCell(firstCell); - const featureVertices = getFeatureVertices(startCell); - return [startCell, featureVertices]; - - function findOnBorderCell(firstCell) { - const isOnBorder = cellId => borderCells[cellId] || neighbors[cellId].some(ofDifferentType); - if (isOnBorder(firstCell)) return firstCell; - - const startCell = cells.i.filter(ofSameType).find(isOnBorder); - if (startCell === undefined) - throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`); - - return startCell; - } - - function getFeatureVertices(startCell) { - const startingVertex = cells.v[startCell].find(v => vertices.c[v].some(ofDifferentType)); - if (startingVertex === undefined) - throw new Error(`Markup: startingVertex for cell ${startCell} is not found`); - - return connectVertices({vertices, startingVertex, ofSameType, closeRing: false}); - } - } - } - } - - // add properties to pack features - function defineGroups() { - const gridCellsNumber = grid.cells.i.length; - const OCEAN_MIN_SIZE = gridCellsNumber / 25; - const SEA_MIN_SIZE = gridCellsNumber / 1000; - const CONTINENT_MIN_SIZE = gridCellsNumber / 10; - const ISLAND_MIN_SIZE = gridCellsNumber / 1000; - - for (const feature of pack.features) { - if (!feature || feature.type === "ocean") continue; - - if (feature.type === "lake") feature.height = Lakes.getHeight(feature); - feature.group = defineGroup(feature); - } - - function defineGroup(feature) { - if (feature.type === "island") return defineIslandGroup(feature); - if (feature.type === "ocean") return defineOceanGroup(); - if (feature.type === "lake") return defineLakeGroup(feature); - throw new Error(`Markup: unknown feature type ${feature.type}`); - } - - function defineOceanGroup(feature) { - if (feature.cells > OCEAN_MIN_SIZE) return "ocean"; - if (feature.cells > SEA_MIN_SIZE) return "sea"; - return "gulf"; - } - - function defineIslandGroup(feature) { - const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]]; - if (prevFeature && prevFeature.type === "lake") return "lake_island"; - if (feature.cells > CONTINENT_MIN_SIZE) return "continent"; - if (feature.cells > ISLAND_MIN_SIZE) return "island"; - return "isle"; - } - - function defineLakeGroup(feature) { - if (feature.temp < -3) return "frozen"; - if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava"; - - if (!feature.inlets && !feature.outlet) { - if (feature.evaporation > feature.flux * 4) return "dry"; - if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole"; - } - - if (!feature.outlet && feature.evaporation > feature.flux) return "salt"; - - return "freshwater"; - } - } - - return {markupGrid, markupPack, defineGroups}; -})(); diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js deleted file mode 100644 index 87bc02d5..00000000 --- a/modules/heightmap-generator.js +++ /dev/null @@ -1,543 +0,0 @@ -"use strict"; - -window.HeightmapGenerator = (function () { - let grid = null; - let heights = null; - let blobPower; - let linePower; - - const setGraph = graph => { - const {cellsDesired, cells, points} = graph; - heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length}); - blobPower = getBlobPower(cellsDesired); - linePower = getLinePower(cellsDesired); - grid = graph; - }; - - const getHeights = () => heights; - - const clearData = () => { - heights = null; - grid = null; - }; - - const fromTemplate = (graph, id) => { - const templateString = heightmapTemplates[id]?.template || ""; - const steps = templateString.split("\n"); - - if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`); - setGraph(graph); - - for (const step of steps) { - const elements = step.trim().split(" "); - if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`); - addStep(...elements); - } - - return heights; - }; - - const fromPrecreated = (graph, id) => { - return new Promise(resolve => { - // create canvas where 1px corresponts to a cell - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - const {cellsX, cellsY} = graph; - canvas.width = cellsX; - canvas.height = cellsY; - - // load heightmap into image and render to canvas - const img = new Image(); - img.src = `./heightmaps/${id}.png`; - img.onload = () => { - ctx.drawImage(img, 0, 0, cellsX, cellsY); - const imageData = ctx.getImageData(0, 0, cellsX, cellsY); - setGraph(graph); - getHeightsFromImageData(imageData.data); - canvas.remove(); - img.remove(); - resolve(heights); - }; - }); - }; - - const generate = async function (graph) { - TIME && console.time("defineHeightmap"); - const id = byId("templateInput").value; - - Math.random = aleaPRNG(seed); - const isTemplate = id in heightmapTemplates; - const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id); - TIME && console.timeEnd("defineHeightmap"); - - clearData(); - return heights; - }; - - function addStep(tool, a2, a3, a4, a5) { - if (tool === "Hill") return addHill(a2, a3, a4, a5); - if (tool === "Pit") return addPit(a2, a3, a4, a5); - if (tool === "Range") return addRange(a2, a3, a4, a5); - if (tool === "Trough") return addTrough(a2, a3, a4, a5); - if (tool === "Strait") return addStrait(a2, a3); - if (tool === "Mask") return mask(a2); - if (tool === "Invert") return invert(a2, a3); - if (tool === "Add") return modify(a3, +a2, 1); - if (tool === "Multiply") return modify(a3, 0, +a2); - if (tool === "Smooth") return smooth(a2); - } - - function getBlobPower(cells) { - const blobPowerMap = { - 1000: 0.93, - 2000: 0.95, - 5000: 0.97, - 10000: 0.98, - 20000: 0.99, - 30000: 0.991, - 40000: 0.993, - 50000: 0.994, - 60000: 0.995, - 70000: 0.9955, - 80000: 0.996, - 90000: 0.9964, - 100000: 0.9973 - }; - return blobPowerMap[cells] || 0.98; - } - - function getLinePower(cells) { - const linePowerMap = { - 1000: 0.75, - 2000: 0.77, - 5000: 0.79, - 10000: 0.81, - 20000: 0.82, - 30000: 0.83, - 40000: 0.84, - 50000: 0.86, - 60000: 0.87, - 70000: 0.88, - 80000: 0.91, - 90000: 0.92, - 100000: 0.93 - }; - - return linePowerMap[cells] || 0.81; - } - - const addHill = (count, height, rangeX, rangeY) => { - count = getNumberInRange(count); - while (count > 0) { - addOneHill(); - count--; - } - - function addOneHill() { - const change = new Uint8Array(heights.length); - let limit = 0; - let start; - let h = lim(getNumberInRange(height)); - - do { - const x = getPointInRange(rangeX, graphWidth); - const y = getPointInRange(rangeY, graphHeight); - start = findGridCell(x, y, grid); - limit++; - } while (heights[start] + h > 90 && limit < 50); - - change[start] = h; - const queue = [start]; - while (queue.length) { - const q = queue.shift(); - - for (const c of grid.cells.c[q]) { - if (change[c]) continue; - change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9); - if (change[c] > 1) queue.push(c); - } - } - - heights = heights.map((h, i) => lim(h + change[i])); - } - }; - - const addPit = (count, height, rangeX, rangeY) => { - count = getNumberInRange(count); - while (count > 0) { - addOnePit(); - count--; - } - - function addOnePit() { - const used = new Uint8Array(heights.length); - let limit = 0, - start; - let h = lim(getNumberInRange(height)); - - do { - const x = getPointInRange(rangeX, graphWidth); - const y = getPointInRange(rangeY, graphHeight); - start = findGridCell(x, y, grid); - limit++; - } while (heights[start] < 20 && limit < 50); - - const queue = [start]; - while (queue.length) { - const q = queue.shift(); - h = h ** blobPower * (Math.random() * 0.2 + 0.9); - if (h < 1) return; - - grid.cells.c[q].forEach(function (c, i) { - if (used[c]) return; - heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9)); - used[c] = 1; - queue.push(c); - }); - } - } - }; - - // fromCell, toCell are options cell ids - const addRange = (count, height, rangeX, rangeY, startCell, endCell) => { - count = getNumberInRange(count); - while (count > 0) { - addOneRange(); - count--; - } - - function addOneRange() { - const used = new Uint8Array(heights.length); - let h = lim(getNumberInRange(height)); - - if (rangeX && rangeY) { - // find start and end points - const startX = getPointInRange(rangeX, graphWidth); - const startY = getPointInRange(rangeY, graphHeight); - - let dist = 0, - limit = 0, - endX, - endY; - - do { - endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1; - endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15; - dist = Math.abs(endY - startY) + Math.abs(endX - startX); - limit++; - } while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50); - - startCell = findGridCell(startX, startY, grid); - endCell = findGridCell(endX, endY, grid); - } - - let range = getRange(startCell, endCell); - - // get main ridge - function getRange(cur, end) { - const range = [cur]; - const p = grid.points; - used[cur] = 1; - - while (cur !== end) { - let min = Infinity; - grid.cells.c[cur].forEach(function (e) { - if (used[e]) return; - let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2; - if (Math.random() > 0.85) diff = diff / 2; - if (diff < min) { - min = diff; - cur = e; - } - }); - if (min === Infinity) return range; - range.push(cur); - used[cur] = 1; - } - - return range; - } - - // add height to ridge and cells around - let queue = range.slice(), - i = 0; - while (queue.length) { - const frontier = queue.slice(); - (queue = []), i++; - frontier.forEach(i => { - heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85)); - }); - h = h ** linePower - 1; - if (h < 2) break; - frontier.forEach(f => { - grid.cells.c[f].forEach(i => { - if (!used[i]) { - queue.push(i); - used[i] = 1; - } - }); - }); - } - - // generate prominences - range.forEach((cur, d) => { - if (d % 6 !== 0) return; - for (const l of d3.range(i)) { - const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell - heights[min] = (heights[cur] * 2 + heights[min]) / 3; - cur = min; - } - }); - } - }; - - const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => { - count = getNumberInRange(count); - while (count > 0) { - addOneTrough(); - count--; - } - - function addOneTrough() { - const used = new Uint8Array(heights.length); - let h = lim(getNumberInRange(height)); - - if (rangeX && rangeY) { - // find start and end points - let limit = 0, - startX, - startY, - dist = 0, - endX, - endY; - do { - startX = getPointInRange(rangeX, graphWidth); - startY = getPointInRange(rangeY, graphHeight); - startCell = findGridCell(startX, startY, grid); - limit++; - } while (heights[startCell] < 20 && limit < 50); - - limit = 0; - do { - endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1; - endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15; - dist = Math.abs(endY - startY) + Math.abs(endX - startX); - limit++; - } while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50); - - endCell = findGridCell(endX, endY, grid); - } - - let range = getRange(startCell, endCell); - - // get main ridge - function getRange(cur, end) { - const range = [cur]; - const p = grid.points; - used[cur] = 1; - - while (cur !== end) { - let min = Infinity; - grid.cells.c[cur].forEach(function (e) { - if (used[e]) return; - let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2; - if (Math.random() > 0.8) diff = diff / 2; - if (diff < min) { - min = diff; - cur = e; - } - }); - if (min === Infinity) return range; - range.push(cur); - used[cur] = 1; - } - - return range; - } - - // add height to ridge and cells around - let queue = range.slice(), - i = 0; - while (queue.length) { - const frontier = queue.slice(); - (queue = []), i++; - frontier.forEach(i => { - heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85)); - }); - h = h ** linePower - 1; - if (h < 2) break; - frontier.forEach(f => { - grid.cells.c[f].forEach(i => { - if (!used[i]) { - queue.push(i); - used[i] = 1; - } - }); - }); - } - - // generate prominences - range.forEach((cur, d) => { - if (d % 6 !== 0) return; - for (const l of d3.range(i)) { - const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell - //debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1); - heights[min] = (heights[cur] * 2 + heights[min]) / 3; - cur = min; - } - }); - } - }; - - const addStrait = (width, direction = "vertical") => { - width = Math.min(getNumberInRange(width), grid.cellsX / 3); - if (width < 1 && P(width)) return; - const used = new Uint8Array(heights.length); - const vert = direction === "vertical"; - const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5; - const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3); - const endX = vert - ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) - : graphWidth - 5; - const endY = vert - ? graphHeight - 5 - : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2); - - const start = findGridCell(startX, startY, grid); - const end = findGridCell(endX, endY, grid); - let range = getRange(start, end); - const query = []; - - function getRange(cur, end) { - const range = []; - const p = grid.points; - - while (cur !== end) { - let min = Infinity; - grid.cells.c[cur].forEach(function (e) { - let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2; - if (Math.random() > 0.8) diff = diff / 2; - if (diff < min) { - min = diff; - cur = e; - } - }); - range.push(cur); - } - - return range; - } - - const step = 0.1 / width; - - while (width > 0) { - const exp = 0.9 - step * width; - range.forEach(function (r) { - grid.cells.c[r].forEach(function (e) { - if (used[e]) return; - used[e] = 1; - query.push(e); - heights[e] **= exp; - if (heights[e] > 100) heights[e] = 5; - }); - }); - range = query.slice(); - - width--; - } - }; - - const modify = (range, add, mult, power) => { - const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0]; - const max = range === "land" || range === "all" ? 100 : +range.split("-")[1]; - const isLand = min === 20; - - heights = heights.map(h => { - if (h < min || h > max) return h; - - if (add) h = isLand ? Math.max(h + add, 20) : h + add; - if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult; - if (power) h = isLand ? (h - 20) ** power + 20 : h ** power; - return lim(h); - }); - }; - - const smooth = (fr = 2, add = 0) => { - heights = heights.map((h, i) => { - const a = [h]; - grid.cells.c[i].forEach(c => a.push(heights[c])); - if (fr === 1) return d3.mean(a) + add; - return lim((h * (fr - 1) + d3.mean(a) + add) / fr); - }); - }; - - const mask = (power = 1) => { - const fr = power ? Math.abs(power) : 1; - - heights = heights.map((h, i) => { - const [x, y] = grid.points[i]; - const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center - const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center - let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge - if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge - const masked = h * distance; - return lim((h * (fr - 1) + masked) / fr); - }); - }; - - const invert = (count, axes) => { - if (!P(count)) return; - - const invertX = axes !== "y"; - const invertY = axes !== "x"; - const {cellsX, cellsY} = grid; - - const inverted = heights.map((h, i) => { - const x = i % cellsX; - const y = Math.floor(i / cellsX); - - const nx = invertX ? cellsX - x - 1 : x; - const ny = invertY ? cellsY - y - 1 : y; - const invertedI = nx + ny * cellsX; - return heights[invertedI]; - }); - - heights = inverted; - }; - - function getPointInRange(range, length) { - if (typeof range !== "string") { - ERROR && console.error("Range should be a string"); - return; - } - - const min = range.split("-")[0] / 100 || 0; - const max = range.split("-")[1] / 100 || min; - return rand(min * length, max * length); - } - - function getHeightsFromImageData(imageData) { - for (let i = 0; i < heights.length; i++) { - const lightness = imageData[i * 4] / 255; - const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8; - heights[i] = minmax(Math.floor(powered * 100), 0, 100); - } - } - - return { - setGraph, - getHeights, - generate, - fromTemplate, - fromPrecreated, - addHill, - addRange, - addTrough, - addStrait, - addPit, - smooth, - modify, - mask, - invert - }; -})(); diff --git a/modules/lakes.js b/modules/lakes.js deleted file mode 100644 index 8ce18793..00000000 --- a/modules/lakes.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; - -window.Lakes = (function () { - const LAKE_ELEVATION_DELTA = 0.1; - - // check if lake can be potentially open (not in deep depression) - const detectCloseLakes = h => { - const {cells} = pack; - const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value; - - pack.features.forEach(feature => { - if (feature.type !== "lake") return; - delete feature.closed; - - const MAX_ELEVATION = feature.height + ELEVATION_LIMIT; - if (MAX_ELEVATION > 99) { - feature.closed = false; - return; - } - - let isDeep = true; - const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0]; - const queue = [lowestShorelineCell]; - const checked = []; - checked[lowestShorelineCell] = true; - - while (queue.length && isDeep) { - const cellId = queue.pop(); - - for (const neibCellId of cells.c[cellId]) { - if (checked[neibCellId]) continue; - if (h[neibCellId] >= MAX_ELEVATION) continue; - - if (h[neibCellId] < 20) { - const nFeature = pack.features[cells.f[neibCellId]]; - if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false; - } - - checked[neibCellId] = true; - queue.push(neibCellId); - } - } - - feature.closed = isDeep; - }); - }; - - const defineClimateData = function (heights) { - const {cells, features} = pack; - const lakeOutCells = new Uint16Array(cells.i.length); - - features.forEach(feature => { - if (feature.type !== "lake") return; - feature.flux = getFlux(feature); - feature.temp = getLakeTemp(feature); - feature.evaporation = getLakeEvaporation(feature); - if (feature.closed) return; // no outlet for lakes in depressed areas - - feature.outCell = getLowestShoreCell(feature); - lakeOutCells[feature.outCell] = feature.i; - }); - - return lakeOutCells; - - function getFlux(lake) { - return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0); - } - - function getLakeTemp(lake) { - if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]]; - return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); - } - - function getLakeEvaporation(lake) { - const height = (lake.height - 18) ** heightExponentInput.value; // height in meters - const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11] - return rn(evaporation * lake.cells); - } - - function getLowestShoreCell(lake) { - return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0]; - } - }; - - const cleanupLakeData = function () { - for (const feature of pack.features) { - if (feature.type !== "lake") continue; - delete feature.river; - delete feature.enteringFlux; - delete feature.outCell; - delete feature.closed; - feature.height = rn(feature.height, 3); - - const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r)); - if (!inlets || !inlets.length) delete feature.inlets; - else feature.inlets = inlets; - - const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet); - if (!outlet) delete feature.outlet; - } - }; - - const getHeight = function (feature) { - const heights = pack.cells.h; - const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20; - return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2); - }; - - const defineNames = function () { - pack.features.forEach(feature => { - if (feature.type !== "lake") return; - feature.name = getName(feature); - }); - }; - - const getName = function (feature) { - const landCell = feature.shoreline[0]; - const culture = pack.cells.culture[landCell]; - return Names.getCulture(culture); - }; - - return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, defineNames, getName}; -})(); diff --git a/modules/names-generator.js b/modules/names-generator.js deleted file mode 100644 index c35afedc..00000000 --- a/modules/names-generator.js +++ /dev/null @@ -1,328 +0,0 @@ -"use strict"; - -window.Names = (function () { - let chains = []; - - // calculate Markov chain for a namesbase - const calculateChain = function (string) { - const chain = []; - const array = string.split(","); - - for (const n of array) { - let name = n.trim().toLowerCase(); - const basic = !/[^\u0000-\u007f]/.test(name); // basic chars and English rules can be applied - - // split word into pseudo-syllables - for (let i = -1, syllable = ""; i < name.length; i += syllable.length || 1, syllable = "") { - let prev = name[i] || ""; // pre-onset letter - let v = 0; // 0 if no vowels in syllable - - for (let c = i + 1; name[c] && syllable.length < 5; c++) { - const that = name[c], - next = name[c + 1]; // next char - syllable += that; - if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen - if (!next || next === " " || next === "-") break; // no need to check - - if (vowel(that)) v = 1; // check if letter is vowel - - // do not split some diphthongs - if (that === "y" && next === "e") continue; // 'ye' - if (basic) { - // English-like - if (that === "o" && next === "o") continue; // 'oo' - if (that === "e" && next === "e") continue; // 'ee' - if (that === "a" && next === "e") continue; // 'ae' - if (that === "c" && next === "h") continue; // 'ch' - } - - if (vowel(that) === next) break; // two same vowels in a row - if (v && vowel(name[c + 2])) break; // syllable has vowel and additional vowel is expected soon - } - - if (chain[prev] === undefined) chain[prev] = []; - chain[prev].push(syllable); - } - } - - return chain; - }; - - const updateChain = i => { - chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null; - }; - - const clearChains = () => { - chains = []; - }; - - // generate name using Markov's chain - const getBase = function (base, min, max, dupl) { - if (base === undefined) return ERROR && console.error("Please define a base"); - - if (nameBases[base] === undefined) { - if (nameBases[0]) { - WARN && console.warn("Namebase " + base + " is not found. First available namebase will be used"); - base = 0; - } else { - ERROR && console.error("Namebase " + base + " is not found"); - return "ERROR"; - } - } - - if (!chains[base]) updateChain(base); - - const data = chains[base]; - if (!data || data[""] === undefined) { - tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error"); - ERROR && console.error("Namebase " + base + " is incorrect!"); - return "ERROR"; - } - - if (!min) min = nameBases[base].min; - if (!max) max = nameBases[base].max; - if (dupl !== "") dupl = nameBases[base].d; - - let v = data[""], - cur = ra(v), - w = ""; - for (let i = 0; i < 20; i++) { - if (cur === "") { - // end of word - if (w.length < min) { - cur = ""; - w = ""; - v = data[""]; - } else break; - } else { - if (w.length + cur.length > max) { - // word too long - if (w.length < min) w += cur; - break; - } else v = data[last(cur)] || data[""]; - } - - w += cur; - cur = ra(v); - } - - // parse word to get a final name - const l = last(w); // last letter - if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end - - let name = [...w].reduce(function (r, c, i, d) { - if (c === d[i + 1] && !dupl.includes(c)) return r; // duplication is not allowed - if (!r.length) return c.toUpperCase(); - if (r.slice(-1) === "-" && c === " ") return r; // remove space after hyphen - if (r.slice(-1) === " ") return r + c.toUpperCase(); // capitalize letter after space - if (r.slice(-1) === "-") return r + c.toUpperCase(); // capitalize letter after hyphen - if (c === "a" && d[i + 1] === "e") return r; // "ae" => "e" - if (i + 2 < d.length && c === d[i + 1] && c === d[i + 2]) return r; // remove three same letters in a row - return r + c; - }, ""); - - // join the word if any part has only 1 letter - if (name.split(" ").some(part => part.length < 2)) - name = name - .split(" ") - .map((p, i) => (i ? p.toLowerCase() : p)) - .join(""); - - if (name.length < 2) { - ERROR && console.error("Name is too short! Random name will be selected"); - name = ra(nameBases[base].b.split(",")); - } - - return name; - }; - - // generate name for culture - const getCulture = function (culture, min, max, dupl) { - if (culture === undefined) return ERROR && console.error("Please define a culture"); - const base = pack.cultures[culture].base; - return getBase(base, min, max, dupl); - }; - - // generate short name for culture - const getCultureShort = function (culture) { - if (culture === undefined) return ERROR && console.error("Please define a culture"); - return getBaseShort(pack.cultures[culture].base); - }; - - // generate short name for base - const getBaseShort = function (base) { - const min = nameBases[base] ? nameBases[base].min - 1 : null; - const max = min ? Math.max(nameBases[base].max - 2, min) : null; - return getBase(base, min, max, "", 0); - }; - - // generate state name based on capital or random name and culture-specific suffix - const getState = function (name, culture, base) { - if (name === undefined) return ERROR && console.error("Please define a base name"); - if (culture === undefined && base === undefined) return ERROR && console.error("Please define a culture"); - if (base === undefined) base = pack.cultures[culture].base; - - // exclude endings inappropriate for states name - if (name.includes(" ")) name = capitalize(name.replace(/ /g, "").toLowerCase()); // don't allow multiword state names - if (name.length > 6 && name.slice(-4) === "berg") name = name.slice(0, -4); // remove -berg for any - if (name.length > 5 && name.slice(-3) === "ton") name = name.slice(0, -3); // remove -ton for any - - if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0, -2); - // remove -sk/-ev/-ov for Ruthenian - else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; - // Japanese ends on any vowel or -u - else if (base === 18 && P(0.4)) - name = vowel(name.slice(0, 1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al - - // no suffix for fantasy bases - if (base > 32 && base < 42) return name; - - // define if suffix should be used - if (name.length > 3 && vowel(name.slice(-1))) { - if (vowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2); - // 85% for vv - else if (P(0.7)) name = name.slice(0, -1); - // ~60% for cv - else return name; - } else if (P(0.4)) return name; // 60% for cc and vc - - // define suffix - let suffix = "ia"; // standard suffix - - const rnd = Math.random(), - l = name.length; - if (base === 3 && rnd < 0.03 && l < 7) suffix = "terra"; - // Italian - else if (base === 4 && rnd < 0.03 && l < 7) suffix = "terra"; - // Spanish - else if (base === 13 && rnd < 0.03 && l < 7) suffix = "terra"; - // Portuguese - else if (base === 2 && rnd < 0.03 && l < 7) suffix = "terre"; - // French - else if (base === 0 && rnd < 0.5 && l < 7) suffix = "land"; - // German - else if (base === 1 && rnd < 0.4 && l < 7) suffix = "land"; - // English - else if (base === 6 && rnd < 0.3 && l < 7) suffix = "land"; - // Nordic - else if (base === 32 && rnd < 0.1 && l < 7) suffix = "land"; - // generic Human - else if (base === 7 && rnd < 0.1) suffix = "eia"; - // Greek - else if (base === 9 && rnd < 0.35) suffix = "maa"; - // Finnic - else if (base === 15 && rnd < 0.4 && l < 6) suffix = "orszag"; - // Hungarian - else if (base === 16) suffix = rnd < 0.6 ? "yurt" : "eli"; - // Turkish - else if (base === 10) suffix = "guk"; - // Korean - else if (base === 11) suffix = " Guo"; - // Chinese - else if (base === 14) suffix = rnd < 0.5 && l < 6 ? "tlan" : "co"; - // Nahuatl - else if (base === 17 && rnd < 0.8) suffix = "a"; - // Berber - else if (base === 18 && rnd < 0.8) suffix = "a"; // Arabic - - return validateSuffix(name, suffix); - }; - - function validateSuffix(name, suffix) { - if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it - const s1 = suffix.charAt(0); - if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter - if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2, -1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st - if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter - return name + suffix; - } - - // generato name for the map - const getMapName = function (force) { - if (!force && locked("mapName")) return; - if (force && locked("mapName")) unlock("mapName"); - const base = P(0.7) ? 2 : P(0.5) ? rand(0, 6) : rand(0, 31); - if (!nameBases[base]) { - tip("Namebase is not found", false, "error"); - return ""; - } - const min = nameBases[base].min - 1; - const max = Math.max(nameBases[base].max - 3, min); - const baseName = getBase(base, min, max, "", 0); - const name = P(0.7) ? addSuffix(baseName) : baseName; - mapName.value = name; - }; - - function addSuffix(name) { - const suffix = P(0.8) ? "ia" : "land"; - if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length - 3)); - else if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length - 5)); - return validateSuffix(name, suffix); - } - - const getNameBases = function () { - // name, min length, max length, letters to allow duplication, multi-word name rate [deprecated] - // prettier-ignore - return [ - // real-world bases by Azgaar: - {name: "German", i: 0, min: 5, max: 12, d: "lt", m: 0, b: "Achern,Aichhalden,Aitern,Albbruck,Alpirsbach,Altensteig,Althengstett,Appenweier,Auggen,Badenen,Badenweiler,Baiersbronn,Ballrechten,Bellingen,Berghaupten,Bernau,Biberach,Biederbach,Binzen,Birkendorf,Birkenfeld,Bischweier,Blumberg,Bollen,Bollschweil,Bonndorf,Bosingen,Braunlingen,Breisach,Breisgau,Breitnau,Brigachtal,Buchenbach,Buggingen,Buhl,Buhlertal,Calw,Dachsberg,Dobel,Donaueschingen,Dornhan,Dornstetten,Dottingen,Dunningen,Durbach,Durrheim,Ebhausen,Ebringen,Efringen,Egenhausen,Ehrenkirchen,Ehrsberg,Eimeldingen,Eisenbach,Elzach,Elztal,Emmendingen,Endingen,Engelsbrand,Enz,Enzklosterle,Eschbronn,Ettenheim,Ettlingen,Feldberg,Fischerbach,Fischingen,Fluorn,Forbach,Freiamt,Freiburg,Freudenstadt,Friedenweiler,Friesenheim,Frohnd,Furtwangen,Gaggenau,Geisingen,Gengenbach,Gernsbach,Glatt,Glatten,Glottertal,Gorwihl,Gottenheim,Grafenhausen,Grenzach,Griesbach,Gutach,Gutenbach,Hag,Haiterbach,Hardt,Harmersbach,Hasel,Haslach,Hausach,Hausen,Hausern,Heitersheim,Herbolzheim,Herrenalb,Herrischried,Hinterzarten,Hochenschwand,Hofen,Hofstetten,Hohberg,Horb,Horben,Hornberg,Hufingen,Ibach,Ihringen,Inzlingen,Kandern,Kappel,Kappelrodeck,Karlsbad,Karlsruhe,Kehl,Keltern,Kippenheim,Kirchzarten,Konigsfeld,Krozingen,Kuppenheim,Kussaberg,Lahr,Lauchringen,Lauf,Laufenburg,Lautenbach,Lauterbach,Lenzkirch,Liebenzell,Loffenau,Loffingen,Lorrach,Lossburg,Mahlberg,Malsburg,Malsch,March,Marxzell,Marzell,Maulburg,Monchweiler,Muhlenbach,Mullheim,Munstertal,Murg,Nagold,Neubulach,Neuenburg,Neuhausen,Neuried,Neuweiler,Niedereschach,Nordrach,Oberharmersbach,Oberkirch,Oberndorf,Oberbach,Oberried,Oberwolfach,Offenburg,Ohlsbach,Oppenau,Ortenberg,otigheim,Ottenhofen,Ottersweier,Peterstal,Pfaffenweiler,Pfalzgrafenweiler,Pforzheim,Rastatt,Renchen,Rheinau,Rheinfelden,Rheinmunster,Rickenbach,Rippoldsau,Rohrdorf,Rottweil,Rummingen,Rust,Sackingen,Sasbach,Sasbachwalden,Schallbach,Schallstadt,Schapbach,Schenkenzell,Schiltach,Schliengen,Schluchsee,Schomberg,Schonach,Schonau,Schonenberg,Schonwald,Schopfheim,Schopfloch,Schramberg,Schuttertal,Schwenningen,Schworstadt,Seebach,Seelbach,Seewald,Sexau,Simmersfeld,Simonswald,Sinzheim,Solden,Staufen,Stegen,Steinach,Steinen,Steinmauern,Straubenhardt,Stuhlingen,Sulz,Sulzburg,Teinach,Tiefenbronn,Tiengen,Titisee,Todtmoos,Todtnau,Todtnauberg,Triberg,Tunau,Tuningen,uhlingen,Unterkirnach,Reichenbach,Utzenfeld,Villingen,Villingendorf,Vogtsburg,Vohrenbach,Waldachtal,Waldbronn,Waldkirch,Waldshut,Wehr,Weil,Weilheim,Weisenbach,Wembach,Wieden,Wiesental,Wildbad,Wildberg,Winzeln,Wittlingen,Wittnau,Wolfach,Wutach,Wutoschingen,Wyhlen,Zavelstein"}, - {name: "English", i: 1, min: 6, max: 11, d: "", m: .1, b: "Abingdon,Albrighton,Alcester,Almondbury,Altrincham,Amersham,Andover,Appleby,Ashboume,Atherstone,Aveton,Axbridge,Aylesbury,Baldock,Bamburgh,Barton,Basingstoke,Berden,Bere,Berkeley,Berwick,Betley,Bideford,Bingley,Birmingham,Blandford,Blechingley,Bodmin,Bolton,Bootham,Boroughbridge,Boscastle,Bossinney,Bramber,Brampton,Brasted,Bretford,Bridgetown,Bridlington,Bromyard,Bruton,Buckingham,Bungay,Burton,Calne,Cambridge,Canterbury,Carlisle,Castleton,Caus,Charmouth,Chawleigh,Chichester,Chillington,Chinnor,Chipping,Chisbury,Cleobury,Clifford,Clifton,Clitheroe,Cockermouth,Coleshill,Combe,Congleton,Crafthole,Crediton,Cuddenbeck,Dalton,Darlington,Dodbrooke,Drax,Dudley,Dunstable,Dunster,Dunwich,Durham,Dymock,Exeter,Exning,Faringdon,Felton,Fenny,Finedon,Flookburgh,Fowey,Frampton,Gateshead,Gatton,Godmanchester,Grampound,Grantham,Guildford,Halesowen,Halton,Harbottle,Harlow,Hatfield,Hatherleigh,Haydon,Helston,Henley,Hertford,Heytesbury,Hinckley,Hitchin,Holme,Hornby,Horsham,Kendal,Kenilworth,Kilkhampton,Kineton,Kington,Kinver,Kirby,Knaresborough,Knutsford,Launceston,Leighton,Lewes,Linton,Louth,Luton,Lyme,Lympstone,Macclesfield,Madeley,Malborough,Maldon,Manchester,Manningtree,Marazion,Marlborough,Marshfield,Mere,Merryfield,Middlewich,Midhurst,Milborne,Mitford,Modbury,Montacute,Mousehole,Newbiggin,Newborough,Newbury,Newenden,Newent,Norham,Northleach,Noss,Oakham,Olney,Orford,Ormskirk,Oswestry,Padstow,Paignton,Penkneth,Penrith,Penzance,Pershore,Petersfield,Pevensey,Pickering,Pilton,Pontefract,Portsmouth,Preston,Quatford,Reading,Redcliff,Retford,Rockingham,Romney,Rothbury,Rothwell,Salisbury,Saltash,Seaford,Seasalter,Sherston,Shifnal,Shoreham,Sidmouth,Skipsea,Skipton,Solihull,Somerton,Southam,Southwark,Standon,Stansted,Stapleton,Stottesdon,Sudbury,Swavesey,Tamerton,Tarporley,Tetbury,Thatcham,Thaxted,Thetford,Thornbury,Tintagel,Tiverton,Torksey,Totnes,Towcester,Tregoney,Trematon,Tutbury,Uxbridge,Wallingford,Wareham,Warenmouth,Wargrave,Warton,Watchet,Watford,Wendover,Westbury,Westcheap,Weymouth,Whitford,Wickwar,Wigan,Wigmore,Winchelsea,Winkleigh,Wiscombe,Witham,Witheridge,Wiveliscombe,Woodbury,Yeovil"}, - {name: "French", i: 2, min: 5, max: 13, d: "nlrs", m: .1, b: "Adon,Aillant,Amilly,Andonville,Ardon,Artenay,Ascheres,Ascoux,Attray,Aubin,Audeville,Aulnay,Autruy,Auvilliers,Auxy,Aveyron,Baccon,Bardon,Barville,Batilly,Baule,Bazoches,Beauchamps,Beaugency,Beaulieu,Beaune,Bellegarde,Boesses,Boigny,Boiscommun,Boismorand,Boisseaux,Bondaroy,Bonnee,Bonny,Bordes,Bou,Bougy,Bouilly,Boulay,Bouzonville,Bouzy,Boynes,Bray,Breteau,Briare,Briarres,Bricy,Bromeilles,Bucy,Cepoy,Cercottes,Cerdon,Cernoy,Cesarville,Chailly,Chaingy,Chalette,Chambon,Champoulet,Chanteau,Chantecoq,Chapell,Charme,Charmont,Charsonville,Chateau,Chateauneuf,Chatel,Chatenoy,Chatillon,Chaussy,Checy,Chevannes,Chevillon,Chevilly,Chevry,Chilleurs,Choux,Chuelles,Clery,Coinces,Coligny,Combleux,Combreux,Conflans,Corbeilles,Corquilleroy,Cortrat,Coudroy,Coullons,Coulmiers,Courcelles,Courcy,Courtemaux,Courtempierre,Courtenay,Cravant,Crottes,Dadonville,Dammarie,Dampierre,Darvoy,Desmonts,Dimancheville,Donnery,Dordives,Dossainville,Douchy,Dry,Echilleuses,Egry,Engenville,Epieds,Erceville,Ervauville,Escrennes,Escrignelles,Estouy,Faverelles,Fay,Feins,Ferolles,Ferrieres,Fleury,Fontenay,Foret,Foucherolles,Freville,Gatinais,Gaubertin,Gemigny,Germigny,Gidy,Gien,Girolles,Givraines,Gondreville,Grangermont,Greneville,Griselles,Guigneville,Guilly,Gyleslonains,Huetre,Huisseau,Ingrannes,Ingre,Intville,Isdes,Ivre,Jargeau,Jouy,Juranville,Bussiere,Laas,Ladon,Lailly,Langesse,Leouville,Ligny,Lombreuil,Lorcy,Lorris,Loury,Louzouer,Malesherbois,Marcilly,Mardie,Mareau,Marigny,Marsainvilliers,Melleroy,Menestreau,Merinville,Messas,Meung,Mezieres,Migneres,Mignerette,Mirabeau,Montargis,Montbarrois,Montbouy,Montcresson,Montereau,Montigny,Montliard,Mormant,Morville,Moulinet,Moulon,Nancray,Nargis,Nesploy,Neuville,Neuvy,Nevoy,Nibelle,Nogent,Noyers,Ocre,Oison,Olivet,Ondreville,Onzerain,Orleans,Ormes,Orville,Oussoy,Outarville,Ouzouer,Pannecieres,Pannes,Patay,Paucourt,Pers,Pierrefitte,Pithiverais,Pithiviers,Poilly,Potier,Prefontaines,Presnoy,Pressigny,Puiseaux,Quiers,Ramoulu,Rebrechien,Rouvray,Rozieres,Rozoy,Ruan,Sandillon,Santeau,Saran,Sceaux,Seichebrieres,Semoy,Sennely,Sermaises,Sigloy,Solterre,Sougy,Sully,Sury,Tavers,Thignonville,Thimory,Thorailles,Thou,Tigy,Tivernon,Tournoisis,Trainou,Treilles,Trigueres,Trinay,Vannes,Varennes,Vennecy,Vieilles,Vienne,Viglain,Vignes,Villamblain,Villemandeur,Villemoutiers,Villemurlin,Villeneuve,Villereau,Villevoques,Villorceau,Vimory,Vitry,Vrigny"}, - {name: "Italian", i: 3, min: 5, max: 12, d: "cltr", m: .1, b: "Accumoli,Acquafondata,Acquapendente,Acuto,Affile,Agosta,Alatri,Albano,Allumiere,Alvito,Amaseno,Amatrice,Anagni,Anguillara,Anticoli,Antrodoco,Anzio,Aprilia,Aquino,Arcinazzo,Ariccia,Arpino,Arsoli,Ausonia,Bagnoregio,Bassiano,Bellegra,Belmonte,Bolsena,Bomarzo,Borgorose,Boville,Bracciano,Broccostella,Calcata,Camerata,Campagnano,Campoli,Canale,Canino,Cantalice,Cantalupo,Capranica,Caprarola,Carbognano,Casalattico,Casalvieri,Castelforte,Castelnuovo,Castiglione,Castro,Castrocielo,Ceccano,Celleno,Cellere,Cerreto,Cervara,Cerveteri,Ciampino,Ciciliano,Cittaducale,Cittareale,Civita,Civitella,Colfelice,Colleferro,Collepardo,Colonna,Concerviano,Configni,Contigliano,Cori,Cottanello,Esperia,Faleria,Farnese,Ferentino,Fiamignano,Filacciano,Fiuggi,Fiumicino,Fondi,Fontana,Fonte,Fontechiari,Formia,Frascati,Frasso,Frosinone,Fumone,Gaeta,Gallese,Gavignano,Genazzano,Giuliano,Gorga,Gradoli,Grottaferrata,Grotte,Guarcino,Guidonia,Ischia,Isola,Labico,Labro,Ladispoli,Latera,Lenola,Leonessa,Licenza,Longone,Lubriano,Maenza,Magliano,Marano,Marcellina,Marcetelli,Marino,Mazzano,Mentana,Micigliano,Minturno,Montalto,Montasola,Montebuono,Monteflavio,Montelanico,Monteleone,Montenero,Monterosi,Moricone,Morlupo,Nazzano,Nemi,Nerola,Nespolo,Nettuno,Norma,Olevano,Onano,Oriolo,Orte,Orvinio,Paganico,Paliano,Palombara,Patrica,Pescorocchiano,Petrella,Piansano,Picinisco,Pico,Piedimonte,Piglio,Pignataro,Poggio,Poli,Pomezia,Pontecorvo,Pontinia,Ponzano,Posta,Pozzaglia,Priverno,Proceno,Rignano,Riofreddo,Ripi,Rivodutri,Rocca,Roccagorga,Roccantica,Roccasecca,Roiate,Ronciglione,Roviano,Salisano,Sambuci,Santa,Santini,Scandriglia,Segni,Selci,Sermoneta,Serrone,Settefrati,Sezze,Sgurgola,Sonnino,Sora,Soriano,Sperlonga,Spigno,Subiaco,Supino,Sutri,Tarano,Tarquinia,Terelle,Terracina,Tivoli,Toffia,Tolfa,Torrice,Torricella,Trevi,Trevignano,Trivigliano,Turania,Tuscania,Valentano,Vallecorsa,Vallemaio,Vallepietra,Vallerano,Vasanello,Vejano,Velletri,Ventotene,Veroli,Vetralla,Vicalvi,Vico,Vicovaro,Vignanello,Viterbo,Viticuso,Vitorchiano,Vivaro,Zagarolo"}, - {name: "Castillian", i: 4, min: 5, max: 11, d: "lr", m: 0, b: "Ajofrin,Alameda,Alaminos,Albares,Albarreal,Albendiego,Alcanizo,Alcaudete,Alcolea,Aldea,Aldeanueva,Algar,Algora,Alhondiga,Almadrones,Almendral,Alovera,Anguita,Arbancon,Argecilla,Arges,Arroyo,Atanzon,Atienza,Azuqueca,Baides,Banos,Bargas,Barriopedro,Belvis,Berninches,Brihuega,Buenaventura,Burgos,Burguillos,Bustares,Cabanillas,Calzada,Camarena,Campillo,Cantalojas,Cardiel,Carmena,Casas,Castejon,Castellar,Castilforte,Castillo,Castilnuevo,Cazalegas,Centenera,Cervera,Checa,Chozas,Chueca,Cifuentes,Cincovillas,Ciruelas,Cogollor,Cogolludo,Consuegra,Copernal,Corral,Cuerva,Domingo,Dosbarrios,Driebes,Duron,Escalona,Escalonilla,Escamilla,Escopete,Espinosa,Esplegares,Esquivias,Estables,Estriegana,Fontanar,Fuembellida,Fuensalida,Fuentelsaz,Gajanejos,Galvez,Gascuena,Gerindote,Guadamur,Heras,Herreria,Herreruela,Hinojosa,Hita,Hombrados,Hontanar,Hormigos,Huecas,Huerta,Humanes,Illana,Illescas,Iniestola,Irueste,Jadraque,Jirueque,Lagartera,Ledanca,Lillo,Lominchar,Loranca,Lucillos,Luzaga,Luzon,Madrid,Magan,Malaga,Malpica,Manzanar,Maqueda,Masegoso,Matillas,Medranda,Megina,Mejorada,Millana,Milmarcos,Mirabueno,Miralrio,Mocejon,Mochales,Molina,Mondejar,Montarron,Mora,Moratilla,Morenilla,Navas,Negredo,Noblejas,Numancia,Nuno,Ocana,Ocentejo,Olias,Olmeda,Ontigola,Orea,Orgaz,Oropesa,Otero,Palma,Pardos,Paredes,Penalver,Pepino,Peralejos,Pinilla,Pioz,Piqueras,Portillo,Poveda,Pozo,Pradena,Prados,Puebla,Puerto,Quero,Quintanar,Rebollosa,Retamoso,Riba,Riofrio,Robledo,Romanillos,Romanones,Rueda,Salmeron,Santiuste,Santo,Sauca,Segura,Selas,Semillas,Sesena,Setiles,Sevilla,Siguenza,Solanillos,Somolinos,Sonseca,Sotillo,Talavera,Taravilla,Tembleque,Tendilla,Tierzo,Torralba,Torre,Torrejon,Torrijos,Tortola,Tortuera,Totanes,Trillo,Uceda,Ugena,Urda,Utande,Valdesotos,Valhermoso,Valtablado,Valverde,Velada,Viana,Yebra,Yuncos,Yunquera,Zaorejas,Zarzuela,Zorita"}, - {name: "Ruthenian", i: 5, min: 5, max: 10, d: "", m: 0, b: "Belgorod,Beloberezhye,Belyi,Belz,Berestiy,Berezhets,Berezovets,Berezutsk,Bobruisk,Bolonets,Borisov,Borovsk,Bozhesk,Bratslav,Bryansk,Brynsk,Buryn,Byhov,Chechersk,Chemesov,Cheremosh,Cherlen,Chern,Chernigov,Chernitsa,Chernobyl,Chernogorod,Chertoryesk,Chetvertnia,Demyansk,Derevesk,Devyagoresk,Dichin,Dmitrov,Dorogobuch,Dorogobuzh,Drestvin,Drokov,Drutsk,Dubechin,Dubichi,Dubki,Dubkov,Dveren,Galich,Glebovo,Glinsk,Goloty,Gomiy,Gorodets,Gorodische,Gorodno,Gorohovets,Goroshin,Gorval,Goryshon,Holm,Horobor,Hoten,Hotin,Hotmyzhsk,Ilovech,Ivan,Izborsk,Izheslavl,Kamenets,Kanev,Karachev,Karna,Kavarna,Klechesk,Klyapech,Kolomyya,Kolyvan,Kopyl,Korec,Kornik,Korochunov,Korshev,Korsun,Koshkin,Kotelno,Kovyla,Kozelsk,Kozelsk,Kremenets,Krichev,Krylatsk,Ksniatin,Kulatsk,Kursk,Kursk,Lebedev,Lida,Logosko,Lomihvost,Loshesk,Loshichi,Lubech,Lubno,Lubutsk,Lutsk,Luchin,Luki,Lukoml,Luzha,Lvov,Mtsensk,Mdin,Medniki,Melecha,Merech,Meretsk,Mescherskoe,Meshkovsk,Metlitsk,Mezetsk,Mglin,Mihailov,Mikitin,Mikulino,Miloslavichi,Mogilev,Mologa,Moreva,Mosalsk,Moschiny,Mozyr,Mstislav,Mstislavets,Muravin,Nemech,Nemiza,Nerinsk,Nichan,Novgorod,Novogorodok,Obolichi,Obolensk,Obolensk,Oleshsk,Olgov,Omelnik,Opoka,Opoki,Oreshek,Orlets,Osechen,Oster,Ostrog,Ostrov,Perelai,Peremil,Peremyshl,Pererov,Peresechen,Perevitsk,Pereyaslav,Pinsk,Ples,Polotsk,Pronsk,Proposhesk,Punia,Putivl,Rechitsa,Rodno,Rogachev,Romanov,Romny,Roslavl,Rostislavl,Rostovets,Rsha,Ruza,Rybchesk,Rylsk,Rzhavesk,Rzhev,Rzhischev,Sambor,Serensk,Serensk,Serpeysk,Shilov,Shuya,Sinech,Sizhka,Skala,Slovensk,Slutsk,Smedin,Sneporod,Snitin,Snovsk,Sochevo,Sokolec,Starica,Starodub,Stepan,Sterzh,Streshin,Sutesk,Svinetsk,Svisloch,Terebovl,Ternov,Teshilov,Teterin,Tiversk,Torchevsk,Toropets,Torzhok,Tripolye,Trubchevsk,Tur,Turov,Usvyaty,Uteshkov,Vasilkov,Velil,Velye,Venev,Venicha,Verderev,Vereya,Veveresk,Viazma,Vidbesk,Vidychev,Voino,Volodimer,Volok,Volyn,Vorobesk,Voronich,Voronok,Vorotynsk,Vrev,Vruchiy,Vselug,Vyatichsk,Vyatka,Vyshegorod,Vyshgorod,Vysokoe,Yagniatin,Yaropolch,Yasenets,Yuryev,Yuryevets,Zaraysk,Zhitomel,Zholvazh,Zizhech,Zubkov,Zudechev,Zvenigorod"}, - {name: "Nordic", i: 6, min: 6, max: 10, d: "kln", m: .1, b: "Akureyri,Aldra,Alftanes,Andenes,Austbo,Auvog,Bakkafjordur,Ballangen,Bardal,Beisfjord,Bifrost,Bildudalur,Bjerka,Bjerkvik,Bjorkosen,Bliksvaer,Blokken,Blonduos,Bolga,Bolungarvik,Borg,Borgarnes,Bosmoen,Bostad,Bostrand,Botsvika,Brautarholt,Breiddalsvik,Bringsli,Brunahlid,Budardalur,Byggdakjarni,Dalvik,Djupivogur,Donnes,Drageid,Drangsnes,Egilsstadir,Eiteroga,Elvenes,Engavogen,Ertenvog,Eskifjordur,Evenes,Eyrarbakki,Fagernes,Fallmoen,Fellabaer,Fenes,Finnoya,Fjaer,Fjelldal,Flakstad,Flateyri,Flostrand,Fludir,Gardaber,Gardur,Gimstad,Givaer,Gjeroy,Gladstad,Godoya,Godoynes,Granmoen,Gravdal,Grenivik,Grimsey,Grindavik,Grytting,Hafnir,Halsa,Hauganes,Haugland,Hauknes,Hella,Helland,Hellissandur,Hestad,Higrav,Hnifsdalur,Hofn,Hofsos,Holand,Holar,Holen,Holkestad,Holmavik,Hopen,Hovden,Hrafnagil,Hrisey,Husavik,Husvik,Hvammstangi,Hvanneyri,Hveragerdi,Hvolsvollur,Igeroy,Indre,Inndyr,Innhavet,Innes,Isafjordur,Jarklaustur,Jarnsreykir,Junkerdal,Kaldvog,Kanstad,Karlsoy,Kavosen,Keflavik,Kjelde,Kjerstad,Klakk,Kopasker,Kopavogur,Korgen,Kristnes,Krutoga,Krystad,Kvina,Lande,Laugar,Laugaras,Laugarbakki,Laugarvatn,Laupstad,Leines,Leira,Leiren,Leland,Lenvika,Loding,Lodingen,Lonsbakki,Lopsmarka,Lovund,Luroy,Maela,Melahverfi,Meloy,Mevik,Misvaer,Mornes,Mosfellsber,Moskenes,Myken,Naurstad,Nesberg,Nesjahverfi,Nesset,Nevernes,Obygda,Ofoten,Ogskardet,Okervika,Oknes,Olafsfjordur,Oldervika,Olstad,Onstad,Oppeid,Oresvika,Orsnes,Orsvog,Osmyra,Overdal,Prestoya,Raudalaekur,Raufarhofn,Reipo,Reykholar,Reykholt,Reykjahlid,Rif,Rinoya,Rodoy,Rognan,Rosvika,Rovika,Salhus,Sanden,Sandgerdi,Sandoker,Sandset,Sandvika,Saudarkrokur,Selfoss,Selsoya,Sennesvik,Setso,Siglufjordur,Silvalen,Skagastrond,Skjerstad,Skonland,Skorvogen,Skrova,Sleneset,Snubba,Softing,Solheim,Solheimar,Sorarnoy,Sorfugloy,Sorland,Sormela,Sorvaer,Sovika,Stamsund,Stamsvika,Stave,Stokka,Stokkseyri,Storjord,Storo,Storvika,Strand,Straumen,Strendene,Sudavik,Sudureyri,Sundoya,Sydalen,Thingeyri,Thorlakshofn,Thorshofn,Tjarnabyggd,Tjotta,Tosbotn,Traelnes,Trofors,Trones,Tverro,Ulvsvog,Unnstad,Utskor,Valla,Vandved,Varmahlid,Vassos,Vevelstad,Vidrek,Vik,Vikholmen,Vogar,Vogehamn,Vopnafjordur"}, - {name: "Greek", i: 7, min: 5, max: 11, d: "s", m: .1, b: "Abdera,Acharnae,Aegae,Aegina,Agrinion,Aigosthena,Akragas,Akroinon,Akrotiri,Alalia,Alexandria,Amarynthos,Amaseia,Amphicaea,Amphigeneia,Amphipolis,Antipatrea,Antiochia,Apamea,Aphidna,Apollonia,Argos,Artemita,Argyropolis,Asklepios,Athenai,Athmonia,Bhrytos,Borysthenes,Brauron,Byblos,Byzantion,Bythinion,Calydon,Chamaizi,Chalcis,Chios,Cleona,Corcyra,Croton,Cyrene,Cythera,Decelea,Delos,Delphi,Dicaearchia,Didyma,Dion,Dioscurias,Dodona,Dorylaion,Elateia,Eleusis,Eleutherna,Emporion,Ephesos,Epidamnos,Epidauros,Epizephyrian,Erythrae,Eubea,Golgi,Gonnos,Gorgippia,Gournia,Gortyn,Gytion,Hagios,Halicarnassos,Heliopolis,Hellespontos,Heloros,Heraclea,Hierapolis,Himera,Histria,Hubla,Hyele,Ialysos,Iasos,Idalion,Imbros,Iolcos,Itanos,Ithaca,Juktas,Kallipolis,Kameiros,Karistos,Kasmenai,Kepoi,Kimmerikon,Knossos,Korinthos,Kos,Kourion,Kydonia,Kyrenia,Lamia,Lampsacos,Laodicea,Lapithos,Larissa,Lebena,Lefkada,Lekhaion,Leibethra,Leontinoi,Lilaea,Lindos,Lissos,Magnesia,Mantineia,Marathon,Marmara,Massalia,Megalopolis,Megara,Metapontion,Methumna,Miletos,Morgantina,Mulai,Mukenai,Myonia,Myra,Myrmekion,Myos,Nauplios,Naucratis,Naupaktos,Naxos,Neapolis,Nemea,Nicaea,Nicopolis,Nymphaion,Nysa,Odessos,Olbia,Olympia,Olynthos,Opos,Orchomenos,Oricos,Orestias,Oreos,Onchesmos,Pagasae,Palaikastro,Pandosia,Panticapaion,Paphos,Pargamon,Paros,Pegai,Pelion,Peiraies,Phaistos,Phaleron,Pharos,Pithekussa,Philippopolis,Phocaea,Pinara,Pisa,Pitane,Plataea,Poseidonia,Potidaea,Pseira,Psychro,Pteleos,Pydna,Pylos,Pyrgos,Rhamnos,Rhithymna,Rhypae,Rizinia,Rodos,Salamis,Samos,Skyllaion,Seleucia,Semasos,Sestos,Scidros,Sicyon,,Sinope,Siris,Smyrna,Sozopolis,Sparta,Stagiros,Stratos,Stymphalos,Sybaris,Surakousai,Taras,Tanagra,Tanais,Tauromenion,Tegea,Temnos,Teos,Thapsos,Thassos,Thebai,Theodosia,Therma,Thespian,Thronion,Thoricos,Thurii,Thyreum,Thyria,Tithoraea,Tomis,Tragurion,Tripolis,Troliton,Troy,Tylissos,Tyros,Vathypetros,Zakynthos,Zakros"}, - {name: "Roman", i: 8, min: 6, max: 11, d: "ln", m: .1, b: "Abila,Adflexum,Adnicrem,Aelia,Aelius,Aeminium,Aequum,Agrippina,Agrippinae,Ala,Albanianis,Aleria,Ambianum,Andautonia,Apulum,Aquae,Aquaegranni,Aquensis,Aquileia,Aquincum,Arae,Argentoratum,Ariminum,Ascrivium,Asturica,Atrebatum,Atuatuca,Augusta,Aurelia,Aurelianorum,Batavar,Batavorum,Belum,Biriciana,Blestium,Bonames,Bonna,Bononia,Borbetomagus,Bovium,Bracara,Brigantium,Burgodunum,Caesaraugusta,Caesarea,Caesaromagus,Calleva,Camulodunum,Cannstatt,Cantiacorum,Capitolina,Caralis,Castellum,Castra,Castrum,Cibalae,Clausentum,Colonia,Concangis,Condate,Confluentes,Conimbriga,Corduba,Coria,Corieltauvorum,Corinium,Coriovallum,Cornoviorum,Danum,Deva,Dianium,Divodurum,Dobunnorum,Drusi,Dubris,Dumnoniorum,Durnovaria,Durocobrivis,Durocornovium,Duroliponte,Durovernum,Durovigutum,Eboracum,Ebusus,Edetanorum,Emerita,Emona,Emporiae,Euracini,Faventia,Flaviae,Florentia,Forum,Gerulata,Gerunda,Gesoscribate,Glevensium,Hadriani,Herculanea,Isca,Italica,Iulia,Iuliobrigensium,Iuvavum,Lactodurum,Lagentium,Lapurdum,Lauri,Legionis,Lemanis,Lentia,Lepidi,Letocetum,Lindinis,Lindum,Lixus,Londinium,Lopodunum,Lousonna,Lucus,Lugdunum,Luguvalium,Lutetia,Mancunium,Marsonia,Martius,Massa,Massilia,Matilo,Mattiacorum,Mediolanum,Mod,Mogontiacum,Moridunum,Mursa,Naissus,Nervia,Nida,Nigrum,Novaesium,Noviomagus,Olicana,Olisippo,Ovilava,Parisiorum,Partiscum,Paterna,Pistoria,Placentia,Pollentia,Pomaria,Pompeii,Pons,Portus,Praetoria,Praetorium,Pullum,Ragusium,Ratae,Raurica,Ravenna,Regina,Regium,Regulbium,Rigomagus,Roma,Romula,Rutupiae,Salassorum,Salernum,Salona,Scalabis,Segovia,Silurum,Sirmium,Siscia,Sorviodurum,Sumelocenna,Tarraco,Taurinorum,Theranda,Traiectum,Treverorum,Tungrorum,Turicum,Ulpia,Valentia,Venetiae,Venta,Verulamium,Vesontio,Vetera,Victoriae,Victrix,Villa,Viminacium,Vindelicorum,Vindobona,Vinovia,Viroconium"}, - {name: "Finnic", i: 9, min: 5, max: 11, d: "akiut", m: 0, b: "Aanekoski,Ahlainen,Aholanvaara,Ahtari,Aijala,Akaa,Alajarvi,Antsla,Aspo,Bennas,Bjorkoby,Elva,Emasalo,Espoo,Esse,Evitskog,Forssa,Haapamaki,Haapavesi,Haapsalu,Hameenlinna,Hanko,Harjavalta,Hattuvaara,Hautajarvi,Havumaki,Heinola,Hetta,Hinkabole,Hirmula,Hossa,Huittinen,Husula,Hyryla,Hyvinkaa,Ikaalinen,Iskmo,Itakoski,Jamsa,Jarvenpaa,Jeppo,Jioesuu,Jiogeva,Joensuu,Jokikyla,Jungsund,Jyvaskyla,Kaamasmukka,Kajaani,Kalajoki,Kallaste,Kankaanpaa,Karkku,Karpankyla,Kaskinen,Kasnas,Kauhajoki,Kauhava,Kauniainen,Kauvatsa,Kehra,Kellokoski,Kelottijarvi,Kemi,Kemijarvi,Kerava,Keuruu,Kiljava,Kiuruvesi,Kivesjarvi,Kiviioli,Kivisuo,Klaukkala,Klovskog,Kohtlajarve,Kokemaki,Kokkola,Kolho,Koskue,Kotka,Kouva,Kaupunki,Kuhmo,Kunda,Kuopio,Kuressaare,Kurikka,Kuusamo,Kylmalankyla,Lahti,Laitila,Lankipohja,Lansikyla,Lapua,Laurila,Lautiosaari,Lempaala,Lepsama,Liedakkala,Lieksa,Littoinen,Lohja,Loimaa,Loksa,Loviisa,Malmi,Mantta,Matasvaara,Maula,Miiluranta,Mioisakula,Munapirtti,Mustvee,Muurahainen,Naantali,Nappa,Narpio,Niinimaa,Niinisalo,Nikkila,Nilsia,Nivala,Nokia,Nummela,Nuorgam,Nuvvus,Obbnas,Oitti,Ojakkala,Onninen,Orimattila,Orivesi,Otanmaki,Otava,Otepaa,Oulainen,Oulu,Paavola,Paide,Paimio,Pakankyla,Paldiski,Parainen,Parkumaki,Parola,Perttula,Pieksamaki,Pioltsamaa,Piolva,Pohjavaara,Porhola,Porrasa,Porvoo,Pudasjarvi,Purmo,Pyhajarvi,Raahe,Raasepori,Raisio,Rajamaki,Rakvere,Rapina,Rapla,Rauma,Rautio,Reposaari,Riihimaki,Rovaniemi,Roykka,Ruonala,Ruottala,Rutalahti,Saarijarvi,Salo,Sastamala,Saue,Savonlinna,Seinajoki,Sillamae,Siuntio,Sompujarvi,Suonenjoki,Suurejaani,Syrjantaka,Tamsalu,Tapa,Temmes,Tiorva,Tormasenvaara,Tornio,Tottijarvi,Tulppio,Turenki,Turi,Tuukkala,Tuurala,Tuuri,Tuuski,Tuusniemi,Ulvila,Unari,Upinniemi,Utti,Uusikaupunki,Vaaksy,Vaalimaa,Vaarinmaja,Vaasa,Vainikkala,Valga,Valkeakoski,Vantaa,Varkaus,Vehkapera,Vehmasmaki,Vieki,Vierumaki,Viitasaari,Viljandi,Vilppula,Viohma,Vioru,Virrat,Ylike,Ylivieska,Ylojarvi"}, - {name: "Korean", i: 10, min: 5, max: 11, d: "", m: 0, b: "Anjung,Ansan,Anseong,Anyang,Aphae,Apo,Baekseok,Baeksu,Beolgyo,Boeun,Boseong,Busan,Buyeo,Changnyeong,Changwon,Cheonan,Cheongdo,Cheongjin,Cheongsong,Cheongyang,Cheorwon,Chirwon,Chuncheon,Chungju,Daedeok,Daegaya,Daejeon,Damyang,Dangjin,Dasa,Donghae,Dongsong,Doyang,Eonyang,Gaeseong,Ganggyeong,Ganghwa,Gangneung,Ganseong,Gaun,Geochang,Geoje,Geoncheon,Geumho,Geumil,Geumwang,Gijang,Gimcheon,Gimhwa,Gimje,Goa,Gochang,Gohan,Gongdo,Gongju,Goseong,Goyang,Gumi,Gunpo,Gunsan,Guri,Gurye,Gwangju,Gwangyang,Gwansan,Gyeongseong,Hadong,Hamchang,Hampyeong,Hamyeol,Hanam,Hapcheon,Hayang,Heungnam,Hongnong,Hongseong,Hwacheon,Hwando,Hwaseong,Hwasun,Hwawon,Hyangnam,Incheon,Inje,Iri,Janghang,Jangheung,Jangseong,Jangseungpo,Jangsu,Jecheon,Jeju,Jeomchon,Jeongeup,Jeonggwan,Jeongju,Jeongok,Jeongseon,Jeonju,Jido,Jiksan,Jinan,Jincheon,Jindo,Jingeon,Jinjeop,Jinnampo,Jinyeong,Jocheon,Jochiwon,Jori,Maepo,Mangyeong,Mokpo,Muju,Munsan,Naesu,Naju,Namhae,Namwon,Namyang,Namyangju,Nongong,Nonsan,Ocheon,Okcheon,Okgu,Onam,Onsan,Onyang,Opo,Paengseong,Pogok,Poseung,Pungsan,Pyeongchang,Pyeonghae,Pyeongyang,Sabi,Sacheon,Samcheok,Samho,Samrye,Sancheong,Sangdong,Sangju,Sapgyo,Sariwon,Sejong,Seocheon,Seogwipo,Seonghwan,Seongjin,Seongju,Seongnam,Seongsan,Seosan,Seungju,Siheung,Sindong,Sintaein,Soheul,Sokcho,Songak,Songjeong,Songnim,Songtan,Suncheon,Taean,Taebaek,Tongjin,Uijeongbu,Uiryeong,Uiwang,Uljin,Ulleung,Unbong,Ungcheon,Ungjin,Waegwan,Wando,Wayang,Wiryeseong,Wondeok,Yangju,Yangsan,Yangyang,Yecheon,Yeomchi,Yeoncheon,Yeongam,Yeongcheon,Yeongdeok,Yeongdong,Yeonggwang,Yeongju,Yeongwol,Yeongyang,Yeonil,Yongin,Yongjin,Yugu"}, - {name: "Chinese", i: 11, min: 5, max: 10, d: "", m: 0, b: "Anding,Anlu,Anqing,Anshun,Baixing,Banyang,Baoqing,Binzhou,Caozhou,Changbai,Changchun,Changde,Changling,Changsha,Changzhou,Chengdu,Chenzhou,Chizhou,Chongqing,Chuxiong,Chuzhou,Dading,Daming,Datong,Daxing,Dengzhou,Deqing,Dihua,Dingli,Dongan,Dongchang,Dongchuan,Dongping,Duyun,Fengtian,Fengxiang,Fengyang,Fenzhou,Funing,Fuzhou,Ganzhou,Gaoyao,Gaozhou,Gongchang,Guangnan,Guangning,Guangping,Guangxin,Guangzhou,Guiyang,Hailong,Hangzhou,Hanyang,Hanzhong,Heihe,Hejian,Henan,Hengzhou,Hezhong,Huaian,Huaiqing,Huanglong,Huangzhou,Huining,Hulan,Huzhou,Jiading,Jian,Jianchang,Jiangning,Jiankang,Jiaxing,Jiayang,Jilin,Jinan,Jingjiang,Jingzhao,Jinhua,Jinzhou,Jiujiang,Kaifeng,Kaihua,Kangding,Kuizhou,Laizhou,Lianzhou,Liaoyang,Lijiang,Linan,Linhuang,Lintao,Liping,Liuzhou,Longan,Longjiang,Longxing,Luan,Lubin,Luzhou,Mishan,Nanan,Nanchang,Nandian,Nankang,Nanyang,Nenjiang,Ningbo,Ningguo,Ningwu,Ningxia,Ningyuan,Pingjiang,Pingliang,Pingyang,Puer,Puzhou,Qianzhou,Qingyang,Qingyuan,Qingzhou,Qujing,Quzhou,Raozhou,Rende,Ruian,Ruizhou,Shafeng,Shajing,Shaoqing,Shaowu,Shaoxing,Shaozhou,Shinan,Shiqian,Shouchun,Shuangcheng,Shulei,Shunde,Shuntian,Shuoping,Sicheng,Sinan,Sizhou,Songjiang,Suiding,Suihua,Suining,Suzhou,Taian,Taibei,Taiping,Taiwan,Taiyuan,Taizhou,Taonan,Tengchong,Tingzhou,Tongchuan,Tongqing,Tongzhou,Weihui,Wensu,Wenzhou,Wuchang,Wuding,Wuzhou,Xian,Xianchun,Xianping,Xijin,Xiliang,Xincheng,Xingan,Xingde,Xinghua,Xingjing,Xingyi,Xingyuan,Xingzhong,Xining,Xinmen,Xiping,Xuanhua,Xunzhou,Xuzhou,Yanan,Yangzhou,Yanji,Yanping,Yanzhou,Yazhou,Yichang,Yidu,Yilan,Yili,Yingchang,Yingde,Yingtian,Yingzhou,Yongchang,Yongping,Yongshun,Yuanzhou,Yuezhou,Yulin,Yunnan,Yunyang,Zezhou,Zhang,Zhangzhou,Zhaoqing,Zhaotong,Zhenan,Zhending,Zhenhai,Zhenjiang,Zhenxi,Zhenyun,Zhongshan,Zunyi"}, - {name: "Japanese", i: 12, min: 4, max: 10, d: "", m: 0, b: "Abira,Aga,Aikawa,Aizumisato,Ajigasawa,Akkeshi,Amagi,Ami,Ando,Asakawa,Ashikita,Bandai,Biratori,Chonan,Esashi,Fuchu,Fujimi,Funagata,Genkai,Godo,Goka,Gonohe,Gyokuto,Haboro,Hamatonbetsu,Harima,Hashikami,Hayashima,Heguri,Hidaka,Higashiura,Hiranai,Hirogawa,Hiroo,Hodatsushimizu,Hoki,Hokuei,Hokuryu,Horokanai,Ibigawa,Ichikai,Ichikawa,Ichinohe,Iijima,Iizuna,Ikawa,Inagawa,Itakura,Iwaizumi,Iwate,Kaisei,Kamifurano,Kamiita,Kamijima,Kamikawa,Kamishihoro,Kamiyama,Kanda,Kanna,Kasagi,Kasuya,Katsuura,Kawabe,Kawamoto,Kawanehon,Kawanishi,Kawara,Kawasaki,Kawatana,Kawazu,Kihoku,Kikonai,Kin,Kiso,Kitagata,Kitajima,Kiyama,Kiyosato,Kofu,Koge,Kohoku,Kokonoe,Kora,Kosa,Kotohira,Kudoyama,Kumejima,Kumenan,Kumiyama,Kunitomi,Kurate,Kushimoto,Kutchan,Kyonan,Kyotamba,Mashike,Matsumae,Mifune,Mihama,Minabe,Minami,Minamiechizen,Minamitane,Misaki,Misasa,Misato,Miyashiro,Miyoshi,Mori,Moseushi,Mutsuzawa,Nagaizumi,Nagatoro,Nagayo,Nagomi,Nakadomari,Nakanojo,Nakashibetsu,Namegawa,Nanbu,Nanporo,Naoshima,Nasu,Niseko,Nishihara,Nishiizu,Nishikatsura,Nishikawa,Nishinoshima,Nishiwaga,Nogi,Noto,Nyuzen,Oarai,Obuse,Odai,Ogawara,Oharu,Oirase,Oishida,Oiso,Oizumi,Oji,Okagaki,Okutama,Omu,Ono,Osaka,Otobe,Otsuki,Owani,Reihoku,Rifu,Rikubetsu,Rishiri,Rokunohe,Ryuo,Saka,Sakuho,Samani,Satsuma,Sayo,Saza,Setana,Shakotan,Shibayama,Shikama,Shimamoto,Shimizu,Shintomi,Shirakawa,Shisui,Shitara,Sobetsu,Sue,Sumita,Suooshima,Suttsu,Tabuse,Tachiarai,Tadami,Tadaoka,Taiji,Taiki,Takachiho,Takahama,Taketoyo,Taragi,Tateshina,Tatsugo,Tawaramoto,Teshikaga,Tobe,Tokigawa,Toma,Tomioka,Tonosho,Tosa,Toyokoro,Toyotomi,Toyoyama,Tsubata,Tsubetsu,Tsukigata,Tsuno,Tsuwano,Umi,Wakasa,Yamamoto,Yamanobe,Yamatsuri,Yanaizu,Yasuda,Yoichi,Yonaguni,Yoro,Yoshino,Yubetsu,Yugawara,Yuni,Yusuhara,Yuza"}, - {name: "Portuguese", i: 13, min: 5, max: 11, d: "", m: .1, b: "Abrigada,Afonsoeiro,Agueda,Aguilada,Alagoas,Alagoinhas,Albufeira,Alcanhoes,Alcobaca,Alcoutim,Aldoar,Alenquer,Alfeizerao,Algarve,Almada,Almagreira,Almeirim,Alpalhao,Alpedrinha,Alvorada,Amieira,Anapolis,Apelacao,Aranhas,Arganil,Armacao,Assenceira,Aveiro,Avelar,Balsas,Barcarena,Barreiras,Barretos,Batalha,Beira,Benavente,Betim,Braga,Braganca,Brasilia,Brejo,Cabeceiras,Cabedelo,Cachoeiras,Cadafais,Calhandriz,Calheta,Caminha,Campinas,Canidelo,Canoas,Capinha,Carmoes,Cartaxo,Carvalhal,Carvoeiro,Cascavel,Castanhal,Caxias,Chapadinha,Chaves,Cocais,Coentral,Coimbra,Comporta,Conde,Coqueirinho,Coruche,Damaia,Dourados,Enxames,Ericeira,Ervidel,Escalhao,Esmoriz,Espinhal,Estela,Estoril,Eunapolis,Evora,Famalicao,Fanhoes,Faro,Fatima,Felgueiras,Ferreira,Figueira,Flecheiras,Florianopolis,Fornalhas,Fortaleza,Freiria,Freixeira,Fronteira,Fundao,Gracas,Gradil,Grainho,Gralheira,Guimaraes,Horta,Ilhavo,Ilheus,Lages,Lagos,Laranjeiras,Lavacolhos,Leiria,Limoeiro,Linhares,Lisboa,Lomba,Lorvao,Lourical,Lourinha,Luziania,Macedo,Machava,Malveira,Marinhais,Maxial,Mealhada,Milharado,Mira,Mirandela,Mogadouro,Montalegre,Mourao,Nespereira,Nilopolis,Obidos,Odemira,Odivelas,Oeiras,Oleiros,Olhalvo,Olinda,Olival,Oliveira,Oliveirinha,Palheiros,Palmeira,Palmital,Pampilhosa,Pantanal,Paradinha,Parelheiros,Pedrosinho,Pegoes,Penafiel,Peniche,Pinhao,Pinheiro,Pombal,Pontal,Pontinha,Portel,Portimao,Quarteira,Queluz,Ramalhal,Reboleira,Recife,Redinha,Ribadouro,Ribeira,Ribeirao,Rosais,Sabugal,Sacavem,Sagres,Sandim,Sangalhos,Santarem,Santos,Sarilhos,Seixas,Seixezelo,Seixo,Silvares,Silveira,Sinhaem,Sintra,Sobral,Sobralinho,Tabuaco,Tabuleiro,Taveiro,Teixoso,Telhado,Telheiro,Tomar,Torreira,Trancoso,Troviscal,Vagos,Varzea,Velas,Viamao,Viana,Vidigal,Vidigueira,Vidual,Vilamar,Vimeiro,Vinhais,Vitoria"}, - {name: "Nahuatl", i: 14, min: 6, max: 13, d: "l", m: 0, b: "Acapulco,Acatepec,Acatlan,Acaxochitlan,Acolman,Actopan,Acuamanala,Ahuacatlan,Almoloya,Amacuzac,Amanalco,Amaxac,Apaxco,Apetatitlan,Apizaco,Atenco,Atizapan,Atlacomulco,Atlapexco,Atotonilco,Axapusco,Axochiapan,Axocomanitla,Axutla,Azcapotzalco,Aztahuacan,Calimaya,Calnali,Calpulalpan,Camotlan,Capulhuac,Chalco,Chapulhuacan,Chapultepec,Chiapan,Chiautempan,Chiconautla,Chihuahua,Chilcuautla,Chimalhuacan,Cholollan,Cihuatlan,Coahuila,Coatepec,Coatetelco,Coatlan,Coatlinchan,Coatzacoalcos,Cocotitlan,Cohetzala,Colima,Colotlan,Coyoacan,Coyohuacan,Cuapiaxtla,Cuauhnahuac,Cuauhtemoc,Cuauhtitlan,Cuautepec,Cuautla,Cuaxomulco,Culhuacan,Ecatepec,Eloxochitlan,Epatlan,Epazoyucan,Huamantla,Huascazaloya,Huatlatlauca,Huautla,Huehuetlan,Huehuetoca,Huexotla,Hueyapan,Hueyotlipan,Hueypoxtla,Huichapan,Huimilpan,Huitzilac,Ixtapallocan,Iztacalco,Iztaccihuatl,Iztapalapa,Lolotla,Malinalco,Mapachtlan,Mazatepec,Mazatlan,Metepec,Metztitlan,Mexico,Miacatlan,Michoacan,Minatitlan,Mixcoac,Mixtla,Molcaxac,Nanacamilpa,Naucalpan,Naupan,Nextlalpan,Nezahualcoyotl,Nopalucan,Oaxaca,Ocotepec,Ocotitlan,Ocotlan,Ocoyoacac,Ocuilan,Ocuituco,Omitlan,Otompan,Otzoloapan,Pacula,Pahuatlan,Panotla,Papalotla,Patlachican,Piaztla,Popocatepetl,Sultepec,Tecamac,Tecolotlan,Tecozautla,Temamatla,Temascalapa,Temixco,Temoac,Temoaya,Tenayuca,Tenochtitlan,Teocuitlatlan,Teotihuacan,Teotlalco,Tepeacac,Tepeapulco,Tepehuacan,Tepetitlan,Tepeyanco,Tepotzotlan,Tepoztlan,Tetecala,Tetlatlahuca,Texcalyacac,Texcoco,Tezontepec,Tezoyuca,Timilpan,Tizapan,Tizayuca,Tlacopan,Tlacotenco,Tlahuac,Tlahuelilpan,Tlahuiltepa,Tlalmanalco,Tlalnepantla,Tlalpan,Tlanchinol,Tlatelolco,Tlaxcala,Tlaxcoapan,Tlayacapan,Tocatlan,Tolcayuca,Toluca,Tonanitla,Tonantzintla,Tonatico,Totolac,Totolapan,Tototlan,Tuchtlan,Tulantepec,Tultepec,Tzompantepec,Xalatlaco,Xaloztoc,Xaltocan,Xiloxoxtla,Xochiatipan,Xochicoatlan,Xochimilco,Xochitepec,Xolotlan,Xonacatlan,Yahualica,Yautepec,Yecapixtla,Yehaultepec,Zacatecas,Zacazonapan,Zacoalco,Zacualpan,Zacualtipan,Zapotlan,Zimapan,Zinacantepec,Zoyaltepec,Zumpahuacan"}, - {name: "Hungarian", i: 15, min: 6, max: 13, d: "", m: 0.1, b: "Aba,Abadszalok,Adony,Ajak,Albertirsa,Alsozsolca,Aszod,Babolna,Bacsalmas,Baktaloranthaza,Balassagyarmat,Balatonalmadi,Balatonboglar,Balkany,Balmazujvaros,Barcs,Bataszek,Batonyterenye,Battonya,Bekes,Berettyoujfalu,Berhida,Biatorbagy,Bicske,Biharkeresztes,Bodajk,Boly,Bonyhad,Budakalasz,Budakeszi,Celldomolk,Csakvar,Csenger,Csongrad,Csorna,Csorvas,Csurgo,Dabas,Demecser,Derecske,Devavanya,Devecser,Dombovar,Dombrad,Dunafoldvar,Dunaharaszti,Dunavarsany,Dunavecse,Edeleny,Elek,Emod,Encs,Enying,Ercsi,Fegyvernek,Fehergyarmat,Felsozsolca,Fertoszentmiklos,Fonyod,Fot,Fuzesabony,Fuzesgyarmat,Gardony,God,Gyal,Gyomaendrod,Gyomro,Hajdudorog,Hajduhadhaz,Hajdusamson,Hajduszoboszlo,Halasztelek,Harkany,Hatvan,Heves,Heviz,Ibrany,Isaszeg,Izsak,Janoshalma,Janossomorja,Jaszapati,Jaszarokszallas,Jaszfenyszaru,Jaszkiser,Kaba,Kalocsa,Kapuvar,Karcag,Kecel,Kemecse,Kenderes,Kerekegyhaza,Keszthely,Kisber,Kiskunmajsa,Kistarcsa,Kistelek,Kisujszallas,Kisvarda,Komadi,Komarom,Komlo,Kormend,Korosladany,Koszeg,Kozarmisleny,Kunhegyes,Kunszentmarton,Kunszentmiklos,Labatlan,Lajosmizse,Lenti,Letavertes,Letenye,Lorinci,Maglod,Mako,Mandok,Marcali,Martonvasar,Mateszalka,Melykut,Mezobereny,Mezocsat,Mezohegyes,Mezokeresztes,Mezokovesd,Mezotur,Mindszent,Mohacs,Monor,Mor,Morahalom,Nadudvar,Nagyatad,Nagyecsed,Nagyhalasz,Nagykallo,Nagykoros,Nagymaros,Nyekladhaza,Nyergesujfalu,Nyirbator,Nyirmada,Nyirtelek,Ocsa,Orkeny,Oroszlany,Paks,Pannonhalma,Paszto,Pecel,Pecsvarad,Pilisvorosvar,Polgar,Polgardi,Pomaz,Puspokladany,Pusztaszabolcs,Putnok,Racalmas,Rackeve,Rakamaz,Rakoczifalva,Sajoszent,Sandorfalva,Sarbogard,Sarkad,Sarospatak,Sarvar,Satoraljaujhely,Siklos,Simontornya,Soltvadkert,Sumeg,Szabadszallas,Szarvas,Szazhalombatta,Szecseny,Szeghalom,Szentgotthard,Szentlorinc,Szerencs,Szigethalom,Szigetvar,Szikszo,Tab,Tamasi,Tapioszele,Tapolca,Teglas,Tet,Tiszafoldvar,Tiszafured,Tiszakecske,Tiszalok,Tiszaujvaros,Tiszavasvari,Tokaj,Tokol,Tompa,Torokbalint,Torokszentmiklos,Totkomlos,Tura,Turkeve,Ujkigyos,ujszasz,Vamospercs,Varpalota,Vasarosnameny,Vasvar,Vecses,Veresegyhaz,Verpelet,Veszto,Zahony,Zalaszentgrot,Zirc,Zsambek"}, - {name: "Turkish", i: 16, min: 4, max: 10, d: "", m: 0, b: "Yelkaya,Buyrukkaya,Erdemtepe,Alakesen,Baharbeyli,Bozbay,Karaoklu,Altunbey,Yalkale,Yalkut,Akardere,Altayburnu,Esentepe,Okbelen,Derinsu,Alaoba,Yamanbeyli,Aykor,Ekinova,Saztepe,Baharkale,Devrekdibi,Alpseki,Ormanseki,Erkale,Yalbelen,Aytay,Yamanyaka,Altaydelen,Esen,Yedieli,Alpkor,Demirkor,Yediyol,Erdemkaya,Yayburnu,Ganiler,Bayatyurt,Kopuzteke,Aytepe,Deniz,Ayan,Ayazdere,Tepe,Kayra,Ayyaka,Deren,Adatepe,Kalkaneli,Bozkale,Yedidelen,Kocayolu,Sazdere,Bozkesen,Oguzeli,Yayladibi,Uluyol,Altay,Ayvar,Alazyaka,Yaloba,Suyaka,Baltaberi,Poyrazdelen,Eymir,Yediyuva,Kurt,Yeltepe,Oktar,Kara Ok,Ekinberi,Er Yurdu,Eren,Erenler,Ser,Oguz,Asay,Bozokeli,Aykut,Ormanyol,Yazkaya,Kalkanova,Yazbeyli,Dokuz Teke,Bilge,Ertensuyu,Kopuzyuva,Buyrukkut,Akardiken,Aybaray,Aslanbeyli,Altun Kaynak,Atikobasi,Yayla Eli,Kor Tepe,Salureli,Kor Kaya,Aybarberi,Kemerev,Yanaray,Beydileli,Buyrukoba,Yolduman,Tengri Tepe,Dokuzsu,Uzunkor,Erdem Yurdu,Kemer,Korteke,Bozokev,Bozoba,Ormankale,Askale,Oguztoprak,Yolberi,Kumseki,Esenobasi,Turkbelen,Ayazseki,Cereneli,Taykut,Bayramdelen,Beydilyaka,Boztepe,Uluoba,Yelyaka,Ulgardiken,Esensu,Baykale,Cerenkor,Bozyol,Duranoba,Aladuman,Denizli,Bahar,Yarkesen,Dokuzer,Yamankaya,Kocatarla,Alayaka,Toprakeli,Sarptarla,Sarpkoy,Serkaynak,Adayaka,Ayazkaynak,Kopuz,Turk,Kart,Kum,Erten,Buyruk,Yel,Ada,Alazova,Ayvarduman,Buyrukok,Ayvartoprak,Uzuntepe,Binseki,Yedibey,Durankale,Alaztoprak,Sarp Ok,Yaparobasi,Yaytepe,Asberi,Kalkankor,Beydiltepe,Adaberi,Bilgeyolu,Ganiyurt,Alkanteke,Esenerler,Asbey,Erdemkale,Erenkaynak,Oguzkoyu,Ayazoba,Boynuztoprak,Okova,Yaloklu,Sivriberi,Yuladiken,Sazbey,Karakaynak,Kopuzkoyu,Buyrukay,Kocakaya,Tepeduman,Yanarseki,Atikyurt,Esenev,Akarbeyli,Yayteke,Devreksungur,Akseki,Baykut,Kalkandere,Ulgarova,Devrekev,Yulabey,Bayatev,Yazsu,Vuraleli,Sivribeyli,Alaova,Alpobasi,Yalyurt,Elmatoprak,Alazkaynak,Esenay,Ertenev,Salurkor,Ekinok,Yalbey,Yeldere,Ganibay,Altaykut,Baltaboy,Ereli,Ayvarsu,Uzunsaz,Bayeli,Erenyol,Kocabay,Derintay,Ayazyol,Aslanoba,Esenkaynak,Ekinlik,Alpyolu,Alayunt,Bozeski,Erkil,Duransuyu,Yulak,Kut,Dodurga,Kutlubey,Kutluyurt,Boynuz,Alayol,Aybar,Aslaneli,Kemerseki,Baltasuyu,Akarer,Ayvarburnu,Boynuzbeyli,Adasungur,Esenkor,Yamanoba,Toprakkor,Uzunyurt,Sungur,Bozok,Kemerli,Alaz,Demirci,Kartepe"}, - {name: "Berber", i: 17, min: 4, max: 10, d: "s", m: .2, b: "Abkhouch,Adrar,Aeraysh,Afrag,Agadir,Agelmam,Aghmat,Agrakal,Agulmam,Ahaggar,Ait Baha,Ajdir,Akka,Almou,Amegdul,Amizmiz,Amknas,Amlil,Amurakush,Anfa,Annaba,Aousja,Arbat,Arfud,Argoub,Arif,Asfi,Asfru,Ashawen,Assamer,Assif,Awlluz,Ayt Melel,Azaghar,Azila,Azilal,Azmour,Azro,Azrou,Beccar,Beja,Bennour,Benslimane,Berkane,Berrechid,Bizerte,Bjaed,Bouayach,Boudenib,Boufrah,Bouskoura,Boutferda,Darallouch,Dar Bouazza,Darchaabane,Dcheira,Demnat,Denden,Djebel,Djedeida,Drargua,Elhusima,Essaouira,Ezzahra,Fas,Fnideq,Ghezeze,Goubellat,Grisaffen,Guelmim,Guercif,Hammamet,Harrouda,Hdifa,Hoceima,Houara,Idhan,Idurar,Ifendassen,Ifoghas,Ifrane,Ighoud,Ikbir,Imilchil,Imzuren,Inezgane,Irherm,Izoughar,Jendouba,Kacem,Kelibia,Kenitra,Kerrando,Khalidia,Khemisset,Khenifra,Khouribga,Khourigba,Kidal,Korba,Korbous,Lahraouyine,Larache,Leyun,Lqliaa,Manouba,Martil,Mazagan,Mcherga,Mdiq,Megrine,Mellal,Melloul,Midelt,Misur,Mohammedia,Mornag,Mrirt,Nabeul,Nadhour,Nador,Nawaksut,Nefza,Ouarzazate,Ouazzane,Oued Zem,Oujda,Ouladteima,Qsentina,Rades,Rafraf,Safi,Sefrou,Sejnane,Settat,Sijilmassa,Skhirat,Slimane,Somaa,Sraghna,Susa,Tabarka,Tadrart,Taferka,Tafilalt,Tafrawt,Tafza,Tagbalut,Tagerdayt,Taghzut,Takelsa,Taliouine,Tanja,Tantan,Taourirt,Targuist,Taroudant,Tarudant,Tasfelalayt,Tassort,Tata,Tattiwin,Tawnat,Taza,Tazagurt,Tazerka,Tazizawt,Taznakht,Tebourba,Teboursouk,Temara,Testour,Tetouan,Tibeskert,Tifelt,Tijdit,Tinariwen,Tinduf,Tinja,Tittawan,Tiznit,Toubkal,Trables,Tubqal,Tunes,Ultasila,Urup,Wagguten,Wararni,Warzazat,Watlas,Wehran,Wejda,Xamida,Yedder,Youssoufia,Zaghouan,Zahret,Zemmour,Zriba"}, - {name: "Arabic", i: 18, min: 4, max: 9, d: "ae", m: .2, b: "Abha,Ajman,Alabar,Alarjam,Alashraf,Alawali,Albawadi,Albirk,Aldhabiyah,Alduwaid,Alfareeq,Algayed,Alhazim,Alhrateem,Alhudaydah,Alhuwaya,Aljahra,Aljubail,Alkhafah,Alkhalas,Alkhawaneej,Alkhen,Alkhobar,Alkhuznah,Allisafah,Almshaykh,Almurjan,Almuwayh,Almuzaylif,Alnaheem,Alnashifah,Alqah,Alqouz,Alqurayyat,Alradha,Alraqmiah,Alsadyah,Alsafa,Alshagab,Alshuqaiq,Alsilaa,Althafeer,Alwasqah,Amaq,Amran,Annaseem,Aqbiyah,Arafat,Arar,Ardah,Asfan,Ashayrah,Askar,Ayaar,Aziziyah,Baesh,Bahrah,Balhaf,Banizayd,Bidiyah,Bisha,Biyatah,Buqhayq,Burayda,Dafiyat,Damad,Dammam,Dariyah,Dhafar,Dhahran,Dhalkut,Dhurma,Dibab,Doha,Dukhan,Duwaibah,Enaker,Fadhla,Fahaheel,Fanateer,Farasan,Fardah,Fujairah,Ghalilah,Ghar,Ghizlan,Ghomgyah,Ghran,Hadiyah,Haffah,Hajanbah,Hajrah,Haqqaq,Haradh,Hasar,Hawiyah,Hebaa,Hefar,Hijal,Husnah,Huwailat,Huwaitah,Irqah,Isharah,Ithrah,Jamalah,Jarab,Jareef,Jazan,Jeddah,Jiblah,Jihanah,Jilah,Jizan,Joraibah,Juban,Jumeirah,Kamaran,Keyad,Khab,Khaiybar,Khasab,Khathirah,Khawarah,Khulais,Kumzar,Limah,Linah,Madrak,Mahab,Mahalah,Makhtar,Mashwar,Masirah,Masliyah,Mastabah,Mazhar,Medina,Meeqat,Mirbah,Mokhtara,Muharraq,Muladdah,Musaykah,Mushayrif,Musrah,Mussafah,Nafhan,Najran,Nakhab,Nizwa,Oman,Qadah,Qalhat,Qamrah,Qasam,Qosmah,Qurain,Quriyat,Qurwa,Radaa,Rafha,Rahlah,Rakamah,Rasheedah,Rasmadrakah,Risabah,Rustaq,Ryadh,Sabtaljarah,Sadah,Safinah,Saham,Saihat,Salalah,Salmiya,Shabwah,Shalim,Shaqra,Sharjah,Sharurah,Shatifiyah,Shidah,Shihar,Shoqra,Shuwaq,Sibah,Sihmah,Sinaw,Sirwah,Sohar,Suhailah,Sulaibiya,Sunbah,Tabuk,Taif,Taqah,Tarif,Tharban,Thuqbah,Thuwal,Tubarjal,Turaif,Turbah,Tuwaiq,Ubar,Umaljerem,Urayarah,Urwah,Wabrah,Warbah,Yabreen,Yadamah,Yafur,Yarim,Yemen,Yiyallah,Zabid,Zahwah,Zallaq,Zinjibar,Zulumah"}, - {name: "Inuit", i: 19, min: 5, max: 15, d: "alutsn", m: 0, b: "Aaluik,Aappilattoq,Aasiaat,Agissat,Agssaussat,Akuliarutsip,Akunnaaq,Alluitsup,Alluttoq,Amitsorsuaq,Ammassalik,Anarusuk,Anguniartarfik,Annertussoq,Annikitsoq,Apparsuit,Apusiaajik,Arsivik,Arsuk,Atammik,Ateqanaq,Atilissuaq,Attu,Augpalugtoq,Aukarnersuaq,Aumat,Auvilkikavsaup,Avadtlek,Avallersuaq,Bjornesk,Blabaerdalen,Blomsterdalen,Brattalhid,Bredebrae,Brededal,Claushavn,Edderfulegoer,Egger,Eqalugalinnguit,Eqalugarssuit,Eqaluit,Eqqua,Etah,Graah,Hakluyt,Haredalen,Hareoen,Hundeo,Igaliku,Igdlorssuit,Igdluluarssuk,Iginniafik,Ikamiut,Ikarissat,Ikateq,Ikermiut,Ikermoissuaq,Ikorfarssuit,Ilimanaq,Illorsuit,Illunnguit,Iluileq,Ilulissat,Imaarsivik,Imartunarssuk,Immikkoortukajik,Innaarsuit,Inneruulalik,Inussullissuaq,Iperaq,Ippik,Iqek,Isortok,Isungartussoq,Itileq,Itissaalik,Itivdleq,Ittit,Ittoqqortoormiit,Ivingmiut,Ivittuut,Kanajoorartuut,Kangaamiut,Kangeq,Kangerluk,Kangerlussuaq,Kanglinnguit,Kapisillit,Kekertamiut,Kiatak,Kiataussaq,Kigatak,Kinaussak,Kingittorsuaq,Kitak,Kitsissuarsuit,Kitsissut,Klenczner,Kook,Kraulshavn,Kujalleq,Kullorsuaq,Kulusuk,Kuurmiit,Kuusuaq,Laksedalen,Maniitsoq,Marrakajik,Mattaangassut,Mernoq,Mittivakkat,Moriusaq,Myggbukta,Naajaat,Nangissat,Nanuuseq,Nappassoq,Narsarmijt,Narsarsuaq,Narssaq,Nasiffik,Natsiarsiorfik,Naujanguit,Niaqornaarsuk,Niaqornat,Nordfjordspasset,Nugatsiaq,Nunarssit,Nunarsuaq,Nunataaq,Nunatakavsaup,Nutaarmiut,Nuugaatsiaq,Nuuk,Nuukullak,Olonkinbyen,Oodaaq,Oqaatsut,Oqaitsunguit,Oqonermiut,Paagussat,Paamiut,Paatuut,Palungataq,Pamialluk,Perserajoq,Pituffik,Puugutaa,Puulkuip,Qaanaq,Qaasuitsup,Qaersut,Qajartalik,Qallunaat,Qaneq,Qaqortok,Qasigiannguit,Qassimiut,Qeertartivaq,Qeqertaq,Qeqertasussuk,Qeqqata,Qernertoq,Qernertunnguit,Qianarreq,Qingagssat,Qoornuup,Qorlortorsuaq,Qullikorsuit,Qunnerit,Qutdleq,Ravnedalen,Ritenbenk,Rypedalen,Saarloq,Saatorsuaq,Saattut,Salliaruseq,Sammeqqat,Sammisoq,Sanningassoq,Saqqaq,Saqqarlersuaq,Saqqarliit,Sarfannguit,Sattiaatteq,Savissivik,Serfanguaq,Sermersooq,Sermiligaaq,Sermilik,Sermitsiaq,Simitakaja,Simiutaq,Singamaq,Siorapaluk,Sisimiut,Sisuarsuit,Sullorsuaq,Suunikajik,Sverdrup,Taartoq,Takiseeq,Tasirliaq,Tasiusak,Tiilerilaaq,Timilersua,Timmiarmiut,Tukingassoq,Tussaaq,Tuttulissuup,Tuujuk,Uiivaq,Uilortussoq,Ujuaakajiip,Ukkusissat,Upernavik,Uttorsiutit,Uumannaq,Uunartoq,Uvkusigssat,Ymer"}, - {name: "Basque", i: 20, min: 4, max: 11, d: "r", m: .1, b: "Agurain,Aia,Aiara,Albiztur,Alkiza,Altzaga,Amorebieta,Amurrio,Andoain,Anoeta,Antzuola,Arakaldo,Arantzazu,Arbatzegi,Areatza,Arratzua,Arrieta,Artea,Artziniega,Asteasu,Astigarraga,Ataun,Atxondo,Aulesti,Azkoitia,Azpeitia,Bakio,Baliarrain,Barakaldo,Barrika,Barrundia,Basauri,Beasain,Bedia,Beizama,Belauntza,Berastegi,Bergara,Bermeo,Bernedo,Berriatua,Berriz,Bidania,Bilar,Bilbao,Busturia,Deba,Derio,Donostia,Dulantzi,Durango,Ea,Eibar,Elantxobe,Elduain,Elgeta,Elgoibar,Elorrio,Erandio,Ergoitia,Ermua,Errenteria,Errezil,Eskoriatza,Eskuernaga,Etxebarri,Etxebarria,Ezkio,Forua,Gabiria,Gaintza,Galdakao,Gamiz,Garai,Gasteiz,Gatzaga,Gaubea,Gautegiz,Gaztelu,Gernika,Gerrikaitz,Getaria,Getxo,Gizaburuaga,Goiatz,Gorliz,Gorriaga,Harana,Hernani,Hondarribia,Ibarra,Ibarrangelu,Idiazabal,Iekora,Igorre,Ikaztegieta,Irun,Irura,Iruraiz,Itsaso,Itsasondo,Iurreta,Izurtza,Jatabe,Kanpezu,Karrantza,Kortezubi,Kripan,Kuartango,Lanestosa,Lantziego,Larrabetzu,Lasarte,Laukiz,Lazkao,Leaburu,Legazpi,Legorreta,Legutio,Leintz,Leioa,Lekeitio,Lemoa,Lemoiz,Leza,Lezama,Lezo,Lizartza,Maeztu,Mallabia,Manaria,Markina,Maruri,Menaka,Mendaro,Mendata,Mendexa,Morga,Mundaka,Mungia,Munitibar,Murueta,Muskiz,Mutiloa,Mutriku,Nabarniz,Oiartzun,Oion,Okondo,Olaberria,Onati,Ondarroa,Ordizia,Orendain,Orexa,Oria,Orio,Ormaiztegi,Orozko,Ortuella,Otegi,Otxandio,Pasaia,Plentzia,Santurtzi,Sestao,Sondika,Soraluze,Sukarrieta,Tolosa,Trapagaran,Turtzioz,Ubarrundia,Ubide,Ugao,Urdua,Urduliz,Urizaharra,Urkabustaiz,Urnieta,Urretxu,Usurbil,Xemein,Zabaleta,Zaia,Zaldibar,Zambrana,Zamudio,Zaratamo,Zarautz,Zeberio,Zegama,Zerain,Zestoa,Zierbena,Zigoitia,Ziortza,Zuia,Zumaia,Zumarraga"}, - {name: "Nigerian", i: 21, min: 4, max: 10, d: "", m: .3, b: "Abadogo,Abafon,Adealesu,Adeto,Adyongo,Afaga,Afamju,Agigbigi,Agogoke,Ahute,Aiyelaboro,Ajebe,Ajola,Akarekwu,Akunuba,Alawode,Alkaijji,Amangam,Amgbaye,Amtasa,Amunigun,Animahun,Anyoko,Arapagi,Asande,Awgbagba,Awhum,Awodu,Babateduwa,Bandakwai,Bangdi,Bilikani,Birnindodo,Braidu,Bulakawa,Buriburi,Cainnan,Chakum,Chondugh,Dagwarga,Darpi,Dokatofa,Dozere,Ebelibri,Efem,Ekoku,Ekpe,Ewhoeviri,Galea,Gamen,Ganjin,Gantetudu,Gargar,Garinbode,Gbure,Gerti,Gidan,Gitabaremu,Giyagiri,Giyawa,Gmawa,Golakochi,Golumba,Gunji,Gwambula,Gwodoti,Hayinlere,Hayinmaialewa,Hirishi,Hombo,Ibefum,Iberekodo,Icharge,Idofin,Idofinoka,Igbogo,Ijoko,Ijuwa,Ikawga,Ikhin,Ikpakidout,Ikpeoniong,Imuogo,Ipawo,Ipinlerere,Isicha,Itakpa,Jangi,Jare,Jataudakum,Jaurogomki,Jepel,Kafinmalama,Katab,Katanga,Katinda,Katirije,Kaurakimba,Keffinshanu,Kellumiri,Kiagbodor,Kirbutu,Kita,Kogogo,Kopje,Korokorosei,Kotoku,Kuata,Kujum,Kukau,Kunboon,Kuonubogbene,Kurawe,Kushinahu,Kwaramakeri,Ladimeji,Lafiaro,Lahaga,Laindebajanle,Laindegoro,Lakati,Litenswa,Maba,Madarzai,Maianita,Malikansaa,Mata,Megoyo,Meku,Miama,Modi,Mshi,Msugh,Muduvu,Murnachehu,Namnai,Ndamanma,Ndiwulunbe,Ndonutim,Ngbande,Nguengu,Ntoekpe,Nyajo,Nyior,Odajie,Ogbaga,Ogultu,Ogunbunmi,Ojopode,Okehin,Olugunna,Omotunde,Onipede,Onma,Orhere,Orya,Otukwang,Otunade,Rampa,Rimi,Rugan,Rumbukawa,Sabiu,Sangabama,Sarabe,Seboregetore,Shafar,Shagwa,Shata,Shengu,Sokoron,Sunnayu,Tafoki,Takula,Talontan,Tarhemba,Tayu,Ter,Timtim,Timyam,Tindirke,Tokunbo,Torlwam,Tseakaadza,Tseanongo,Tsebeeve,Tsepaegh,Tuba,Tumbo,Tungalombo,Tunganyakwe,Uhkirhi,Umoru,Umuabai,Umuajuju,Unchida,Ungua,Unguwar,Unongo,Usha,Utongbo,Vembera,Wuro,Yanbashi,Yanmedi,Yoku,Zarunkwari,Zilumo,Zulika"}, - {name: "Celtic", i: 22, min: 4, max: 12, d: "nld", m: 0, b: "Aberaman,Aberangell,Aberarth,Aberavon,Aberbanc,Aberbargoed,Aberbeeg,Abercanaid,Abercarn,Abercastle,Abercegir,Abercraf,Abercregan,Abercych,Abercynon,Aberdare,Aberdaron,Aberdaugleddau,Aberdeen,Aberdulais,Aberdyfi,Aberedw,Abereiddy,Abererch,Abereron,Aberfan,Aberffraw,Aberffrwd,Abergavenny,Abergele,Aberglasslyn,Abergorlech,Abergwaun,Abergwesyn,Abergwili,Abergwynfi,Abergwyngregyn,Abergynolwyn,Aberhafesp,Aberhonddu,Aberkenfig,Aberllefenni,Abermain,Abermaw,Abermorddu,Abermule,Abernant,Aberpennar,Aberporth,Aberriw,Abersoch,Abersychan,Abertawe,Aberteifi,Aberthin,Abertillery,Abertridwr,Aberystwyth,Achininver,Afonhafren,Alisaha,Anfosadh,Antinbhearmor,Ardenna,Attacon,Banwen,Beira,Bhrura,Bleddfa,Boioduro,Bona,Boskyny,Boslowenpolbrogh,Boudobriga,Bravon,Brigant,Briganta,Briva,Brosnach,Caersws,Cambodunum,Cambra,Caracta,Catumagos,Centobriga,Ceredigion,Chalain,Chearbhallain,Chlasaigh,Chormaic,Cuileannach,Dinn,Diwa,Dubingen,Duibhidighe,Duro,Ebora,Ebruac,Eburodunum,Eccles,Egloskuri,Eighe,Eireann,Elerghi,Ferkunos,Fhlaithnin,Gallbhuaile,Genua,Ghrainnse,Gwyles,Heartsease,Hebron,Hordh,Inbhear,Inbhir,Inbhirair,Innerleithen,Innerleven,Innerwick,Inver,Inveraldie,Inverallan,Inveralmond,Inveramsay,Inveran,Inveraray,Inverarnan,Inverbervie,Inverclyde,Inverell,Inveresk,Inverfarigaig,Invergarry,Invergordon,Invergowrie,Inverhaddon,Inverkeilor,Inverkeithing,Inverkeithney,Inverkip,Inverleigh,Inverleith,Inverloch,Inverlochlarig,Inverlochy,Invermay,Invermoriston,Inverness,Inveroran,Invershin,Inversnaid,Invertrossachs,Inverugie,Inveruglas,Inverurie,Iubhrach,Karardhek,Kilninver,Kirkcaldy,Kirkintilloch,Krake,Lanngorrow,Latense,Leming,Lindomagos,Llanaber,Llandidiwg,Llandyrnog,Llanfarthyn,Llangadwaldr,Llansanwyr,Lochinver,Lugduno,Magoduro,Mheara,Monmouthshire,Nanshiryarth,Narann,Novioduno,Nowijonago,Octoduron,Penning,Pheofharain,Ponsmeur,Raithin,Ricomago,Rossinver,Salodurum,Seguia,Sentica,Theorsa,Tobargeal,Trealaw,Trefesgob,Trewedhenek,Trewythelan,Tuaisceart,Uige,Vitodurum,Windobona"}, - {name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Adamndun,Adma,Admatum,Agrab,Akkad,Akshak,Amnanum,Andarig,Anshan,Apiru,Apum,Arantu,Arbid,Arpachiyah,Arpad,Arrapha,Ashlakka,Assur,Awan,Babilim,Bad-Tibira,Balawat,Barsip,Birtu,Bit-Bunakki,Borsippa,Chuera,Dashrah,Der,Dilbat,Diniktum,Doura,Dur-Kurigalzu,Dur-Sharrukin,Dur-Untash,Dûr-gurgurri,Ebla,Ekallatum,Ekalte,Emar,Erbil,Eresh,Eridu,Eshnunn,Eshnunna,Gargamish,Gasur,Gawra,Gibil,Girsu,Gizza,Habirun,Habur,Hadatu,Hakkulan,Halab,Halabit,Hamazi,Hamoukar,Haradum,Harbidum,Harran,Harranu,Hassuna,Hatarikka,Hatra,Hissar,Hiyawa,Hormirzad,Ida-Maras,Idamaraz,Idu,Imerishu,Imgur-Enlil,Irisagrig,Irnina,Irridu,Isin,Issinnitum,Iturungal,Izubitum,Jarmo,Jemdet,Kabnak,Kadesh,Kahat,Kalhu,Kar-Shulmanu-Asharedu,Kar-Tukulti-Ninurta,Kar-shulmanu-asharedu,Karana,Karatepe,Kartukulti,Kazallu,Kesh,Kidsha,Kinza,Kish,Kisiga,Kisurra,Kuara,Kurda,Kurruhanni,Kutha,Lagaba,Lagash,Larak,Larsa,Leilan,Malgium,Marad,Mardaman,Mari,Marlik,Mashkan,Mashkan-shapir,Matutem,Me-Turan,Meliddu,Mumbaqat,Nabada,Nagar,Nanagugal,Nerebtum,Nigin,Nimrud,Nina,Nineveh,Ninua,Nippur,Niru,Niya,Nuhashe,Nuhasse,Nuzi,Puzrish-Dagan,Qalatjarmo,Qatara,Qatna,Qattunan,Qidshu,Rapiqum,Rawda,Sagaz,Shaduppum,Shaggaratum,Shalbatu,Shanidar,Sharrukin,Shawwan,Shehna,Shekhna,Shemshara,Shibaniba,Shubat-Enlil,Shurkutir,Shuruppak,Shusharra,Shushin,Sikan,Sippar,Sippar-Amnanum,Sippar-sha-Annunitum,Subatum,Susuka,Tadmor,Tarbisu,Telul,Terqa,Tirazish,Tisbon,Tuba,Tushhan,Tuttul,Tutub,Ubaid,Umma,Ur,Urah,Urbilum,Urkesh,Ursa'um,Uruk,Urum,Uzarlulu,Warka,Washukanni,Zabalam,Zarri-Amnan"}, - {name: "Iranian", i: 24, min: 5, max: 11, d: "", m: .1, b: "Abali,Abrisham,Absard,Abuzeydabad,Afus,Alavicheh,Alikosh,Amol,Anarak,Anbar,Andisheh,Anshan,Aran,Ardabil,Arderica,Ardestan,Arjomand,Asgaran,Asgharabad,Ashian,Awan,Babajan,Badrud,Bafran,Baghestan,Baghshad,Bahadoran,Baharan Shahr,Baharestan,Bakun,Bam,Baqershahr,Barzok,Bastam,Behistun,Bitistar,Bumahen,Bushehr,Chadegan,Chahardangeh,Chamgardan,Chermahin,Choghabonut,Chugan,Damaneh,Damavand,Darabgard,Daran,Dastgerd,Dehaq,Dehaqan,Dezful,Dizicheh,Dorcheh,Dowlatabad,Duruntash,Ecbatana,Eslamshahr,Estakhr,Ezhiyeh,Falavarjan,Farrokhi,Fasham,Ferdowsieh,Fereydunshahr,Ferunabad,Firuzkuh,Fuladshahr,Ganjdareh,Ganzak,Gaz,Geoy,Godin,Goldasht,Golestan,Golpayegan,Golshahr,Golshan,Gorgab,Guged,Habibabad,Hafshejan,Hajjifiruz,Hana,Harand,Hasanabad,Hasanlu,Hashtgerd,Hecatompylos,Hormirzad,Imanshahr,Isfahan,Jandaq,Javadabad,Jiroft,Jowsheqan ,Jowzdan,Kabnak,Kahrizak,Kahriz Sang,Kangavar,Karaj,Karkevand,Kashan,Kelishad,Kermanshah,Khaledabad,Khansar,Khorramabad,Khur,Khvorzuq,Kilan,Komeh,Komeshcheh,Konar,Kuhpayeh,Kul,Kushk,Lavasan,Laybid,Liyan,Lyan,Mahabad,Mahallat,Majlesi,Malard,Manzariyeh,Marlik,Meshkat,Meymeh,Miandasht,Mish,Mobarakeh,Nahavand,Nain,Najafabad,Naqshe,Narezzash,Nasimshahr,Nasirshahr,Nasrabad,Natanz,Neyasar,Nikabad,Nimvar,Nushabad,Pakdasht,Parand,Pardis,Parsa,Pasargadai,Patigrabana,Pir Bakran,Pishva,Qahderijan,Qahjaverestan,Qamsar,Qarchak,Qods,Rabat,Ray-shahr,Rezvanshahr,Rhages,Robat Karim,Rozveh,Rudehen,Sabashahr,Safadasht,Sagzi,Salehieh,Sandal,Sarvestan,Sedeh,Sefidshahr,Semirom,Semnan,Shadpurabad,Shah,Shahdad,Shahedshahr,Shahin,Shahpour,Shahr,Shahreza,Shahriar,Sharifabad,Shemshak,Shiraz,Shushan,Shushtar,Sialk,Sin,Sukhteh,Tabas,Tabriz,Takhte,Talkhuncheh,Talli,Tarq,Temukan,Tepe,Tiran,Tudeshk,Tureng,Urmia,Vahidieh,Vahrkana,Vanak,Varamin,Varnamkhast,Varzaneh,Vazvan,Yahya,Yarim,Yasuj,Zarrin Shahr,Zavareh,Zayandeh,Zazeran,Ziar,Zibashahr,Zranka"}, - {name: "Hawaiian", i: 25, min: 5, max: 10, d: "auo", m: 1, b: "Aapueo,Ahoa,Ahuakaio,Ahupau,Alaakua,Alae,Alaeloa,Alamihi,Aleamai,Alena,Alio,Aupokopoko,Halakaa,Haleu,Haliimaile,Hamoa,Hanakaoo,Hanaulu,Hanawana,Hanehoi,Haou,Hikiaupea,Hokuula,Honohina,Honokahua,Honokeana,Honokohau,Honolulu,Honomaele,Hononana,Honopou,Hoolawa,Huelo,Kaalaea,Kaapahu,Kaeo,Kahalehili,Kahana,Kahuai,Kailua,Kainehe,Kakalahale,Kakanoni,Kalenanui,Kaleoaihe,Kalialinui,Kalihi,Kalimaohe,Kaloi,Kamani,Kamehame,Kanahena,Kaniaula,Kaonoulu,Kapaloa,Kapohue,Kapuaikini,Kapunakea,Kauau,Kaulalo,Kaulanamoa,Kauluohana,Kaumakani,Kaumanu,Kaunauhane,Kaupakulua,Kawaloa,Keaa,Keaaula,Keahua,Keahuapono,Kealahou,Keanae,Keauhou,Kelawea,Keokea,Keopuka,Kikoo,Kipapa,Koakupuna,Koali,Kolokolo,Kopili,Kou,Kualapa,Kuhiwa,Kuholilea,Kuhua,Kuia,Kuikui,Kukoae,Kukohia,Kukuiaeo,Kukuipuka,Kukuiula,Kulahuhu,Lapakea,Lapueo,Launiupoko,Lole,Maalo,Mahinahina,Mailepai,Makaakini,Makaalae,Makaehu,Makaiwa,Makaliua,Makapipi,Makapuu,Maluaka,Manawainui,Mehamenui,Moalii,Moanui,Mohopili,Mokae,Mokuia,Mokupapa,Mooiki,Mooloa,Moomuku,Muolea,Nakaaha,Nakalepo,Nakaohu,Nakapehu,Nakula,Napili,Niniau,Nuu,Oloewa,Olowalu,Omaopio,Onau,Onouli,Opaeula,Opana,Opikoula,Paakea,Paeahu,Paehala,Paeohi,Pahoa,Paia,Pakakia,Palauea,Palemo,Paniau,Papaaea,Papaanui,Papaauhau,Papaka,Papauluana,Pauku,Paunau,Pauwalu,Pauwela,Pohakanele,Polaiki,Polanui,Polapola,Poopoo,Poponui,Poupouwela,Puahoowali,Puakea,Puako,Pualaea,Puehuehu,Pueokauiki,Pukaauhuhu,Pukuilua,Pulehu,Puolua,Puou,Puuhaehae,Puuiki,Puuki,Puulani,Puunau,Puuomaile,Uaoa,Uhao,Ukumehame,Ulaino,Ulumalu,Wahikuli,Waianae,Waianu,Waiawa,Waiehu,Waieli,Waikapu,Wailamoa,Wailaulau,Wainee,Waiohole,Waiohonu,Waiohuli,Waiokama,Waiokila,Waiopai,Waiopua,Waipao,Waipionui,Waipouli"}, - {name: "Karnataka", i: 26, min: 5, max: 11, d: "tnl", m: 0, b: "Adityapatna,Adyar,Afzalpur,Aland,Alnavar,Alur,Ambikanagara,Anekal,Ankola,Annigeri,Arkalgud,Arsikere,Athni,Aurad,Badami,Bagalkot,Bagepalli,Bail,Bajpe,Bangalore,Bangarapet,Bankapura,Bannur,Bantval,Basavakalyan,Basavana,Belgaum,Beltangadi,Belur,Bhadravati,Bhalki,Bhatkal,Bhimarayanagudi,Bidar,Bijapur,Bilgi,Birur,Bommasandra,Byadgi,Challakere,Chamarajanagar,Channagiri,Channapatna,Channarayapatna,Chik,Chikmagalur,Chiknayakanhalli,Chikodi,Chincholi,Chintamani,Chitapur,Chitgoppa,Chitradurga,Dandeli,Dargajogihalli,Devadurga,Devanahalli,Dod,Donimalai,Gadag,Gajendragarh,Gangawati,Gauribidanur,Gokak,Gonikoppal,Gubbi,Gudibanda,Gulbarga,Guledgudda,Gundlupet,Gurmatkal,Haliyal,Hangal,Harapanahalli,Harihar,Hassan,Hatti,Haveri,Hebbagodi,Heggadadevankote,Hirekerur,Holalkere,Hole,Homnabad,Honavar,Honnali,Hoovina,Hosakote,Hosanagara,Hosdurga,Hospet,Hubli,Hukeri,Hungund,Hunsur,Ilkal,Indi,Jagalur,Jamkhandi,Jevargi,Jog,Kadigenahalli,Kadur,Kalghatgi,Kamalapuram,Kampli,Kanakapura,Karkal,Karwar,Khanapur,Kodiyal,Kolar,Kollegal,Konnur,Koppa,Koppal,Koratagere,Kotturu,Krishnarajanagara,Krishnarajasagara,Krishnarajpet,Kudchi,Kudligi,Kudremukh,Kumta,Kundapura,Kundgol,Kunigal,Kurgunta,Kushalnagar,Kushtagi,Lakshmeshwar,Lingsugur,Londa,Maddur,Madhugiri,Madikeri,Mahalingpur,Malavalli,Mallar,Malur,Mandya,Mangalore,Manvi,Molakalmuru,Mudalgi,Mudbidri,Muddebihal,Mudgal,Mudhol,Mudigere,Mulbagal,Mulgund,Mulki,Mulur,Mundargi,Mundgod,Munirabad,Mysore,Nagamangala,Nanjangud,Narasimharajapura,Naregal,Nargund,Navalgund,Nipani,Pandavapura,Pavagada,Piriyapatna,Pudu,Puttur,Rabkavi,Raichur,Ramanagaram,Ramdurg,Ranibennur,Raybag,Robertson,Ron,Sadalgi,Sagar,Sakleshpur,Saligram,Sandur,Sankeshwar,Saundatti,Savanur,Sedam,Shahabad,Shahpur,Shaktinagar,Shiggaon,Shikarpur,Shirhatti,Shorapur,Shrirangapattana,Siddapur,Sidlaghatta,Sindgi,Sindhnur,Sira,Siralkoppa,Sirsi,Siruguppa,Somvarpet,Sorab,Sringeri,Srinivaspur,Sulya,Talikota,Tarikere,Tekkalakote,Terdal,Thumbe,Tiptur,Tirthahalli,Tirumakudal,Tumkur,Turuvekere,Udupi,Vijayapura,Wadi,Yadgir,Yelandur,Yelbarga,Yellapur,Yenagudde"}, - {name: "Quechua", i: 27, min: 6, max: 12, d: "l", m: 0, b: "Alpahuaycco,Anchihuay,Anqea,Apurimac,Arequipa,Atahuallpa,Atawalpa,Atico,Ayacucho,Ayahuanco,Ayllu,Cajamarca,Canayre,Canchacancha,Carapo,Carhuac,Carhuacatac,Cashan,Caullaraju,Caxamalca,Cayesh,Ccahuasno,Ccarhuacc,Ccopayoc,Chacchapunta,Chacraraju,Challhuamayo,Champara,Chanchan,Chekiacraju,Chillihua,Chinchey,Chontah,Chopicalqui,Chucuito,Chuito,Chullo,Chumpi,Chuncho,Chupahuacho,Chuquiapo,Chuquisaca,Churup,Cocapata,Cochabamba,Cojup,Collota,Conococha,Corihuayrachina,Cuchoquesera,Cusichaca,Haika,Hanpiq,Hatun,Haywarisqa,Huaca,Huachinga,Hualcan,Hualchancca,Huamanga,Huamashraju,Huancarhuas,Huandoy,Huantsan,Huanupampa,Huarmihuanusca,Huascaran,Huaylas,Huayllabamba,Huayrana,Huaytara,Huichajanca,Huinayhuayna,Huinche,Huinioch,Illiasca,Intipunku,Iquicha,Ishinca,Jahuacocha,Jirishanca,Juli,Jurau,Kakananpunta,Kamasqa,Karpay,Kausay,Khuya,Kuelap,Lanccochayocc,Llaca,Llactapata,Llanganuco,Llaqta,Lloqllasca,Llupachayoc,Luricocha,Machu,Mallku,Matarraju,Mechecc,Mikhuy,Milluacocha,Morochuco,Munay,Ocshapalca,Ollantaytambo,Oroccahua,Oronccoy,Oyolo,Pacamayo,Pacaycasa,Paccharaju,Pachacamac,Pachakamaq,Pachakuteq,Pachakuti,Pachamama,Paititi,Pajaten,Palcaraju,Pallccas,Pampa,Panaka,Paqarina,Paqo,Parap,Paria,Patahuasi,Patallacta,Patibamba,Pisac,Pisco,Pongos,Pucacolpa,Pucahirca,Pucaranra,Pumatambo,Puscanturpa,Putaca,Puyupatamarca,Qawaq,Qayqa,Qochamoqo,Qollana,Qorihuayrachina,Qorimoqo,Qotupuquio,Quenuaracra,Queshque,Quillcayhuanca,Quillya,Quitaracsa,Quitaraju,Qusqu,Rajucolta,Rajutakanan,Rajutuna,Ranrahirca,Ranrapalca,Raria,Rasac,Rimarima,Riobamba,Runkuracay,Rurec,Sacsa,Sacsamarca,Saiwa,Sarapo,Sayacmarca,Sayripata,Sinakara,Sonccopa,Taripaypacha,Taulliraju,Tawantinsuyu,Taytanchis,Tiwanaku,Tocllaraju,Tsacra,Tuco,Tucubamba,Tullparaju,Tumbes,Uchuraccay,Uchuraqay,Ulta,Urihuana,Uruashraju,Vallunaraju,Vilcabamba,Wacho,Wankawillka,Wayra,Yachay,Yahuarraju,Yanamarey,Yanaqucha,Yanesha,Yerupaja"}, - {name: "Swahili", i: 28, min: 4, max: 9, d: "", m: 0, b: "Abim,Adjumani,Alebtong,Amolatar,Amuru,Apac,Arua,Arusha,Babati,Baragoi,Bombo,Budaka,Bugembe,Bugiri,Buikwe,Bukedea,Bukoba,Bukomansimbi,Bukungu,Buliisa,Bundibugyo,Bungoma,Busembatya,Bushenyi,Busia,Busolwe,Butaleja,Butambala,Butere,Buwenge,Buyende,Dadaab,Dodoma,Dokolo,Eldoret,Elegu,Emali,Embu,Entebbe,Garissa,Gede,Gulu,Handeni,Hima,Hoima,Hola,Ibanda,Iganga,Iringa,Isingiro,Isiolo,Jinja,Kaabong,Kabuyanda,Kabwohe,Kagadi,Kajiado,Kakinga,Kakiri,Kakuma,Kalangala,Kaliro,Kalongo,Kalungu,Kampala,Kamwenge,Kanungu,Kapchorwa,Kasese,Kasulu,Katakwi,Kayunga,Keroka,Kiambu,Kibaale,Kibaha,Kibingo,Kibwezi,Kigoma,Kihiihi,Kilifi,Kiruhura,Kiryandongo,Kisii,Kisoro,Kisumu,Kitale,Kitgum,Kitui,Koboko,Korogwe,Kotido,Kumi,Kyazanga,Kyegegwa,Kyenjojo,Kyotera,Lamu,Langata,Lindi,Lodwar,Lokichoggio,Londiani,Loyangalani,Lugazi,Lukaya,Luweero,Lwakhakha,Lwengo,Lyantonde,Machakos,Mafinga,Makambako,Makindu,Malaba,Malindi,Manafwa,Mandera,Marsabit,Masaka,Masindi,Masulita,Matugga,Mayuge,Mbale,Mbarara,Mbeya,Meru,Mitooma,Mityana,Mombasa,Morogoro,Moroto,Moyale,Moyo,Mpanda,Mpigi,Mpondwe,Mtwara,Mubende,Mukono,Muranga,Musoma,Mutomo,Mutukula,Mwanza,Nagongera,Nairobi,Naivasha,Nakapiripirit,Nakaseke,Nakasongola,Nakuru,Namanga,Namayingo,Namutumba,Nansana,Nanyuki,Narok,Naromoru,Nebbi,Ngora,Njeru,Njombe,Nkokonjeru,Ntungamo,Nyahururu,Nyeri,Oyam,Pader,Paidha,Pakwach,Pallisa,Rakai,Ruiru,Rukungiri,Rwimi,Sanga,Sembabule,Shimoni,Shinyanga,Singida,Sironko,Songea,Soroti,Ssabagabo,Sumbawanga,Tabora,Takaungu,Tanga,Thika,Tororo,Tunduma,Vihiga,Voi,Wajir,Wakiso,Watamu,Webuye,Wobulenzi,Wote,Wundanyi,Yumbe,Zanzibar"}, - {name: "Vietnamese", i: 29, min: 3, max: 12, d: "", m: 1, b: "An Giang,Anh Son,An Khe,An Nhon,Ayun Pa,Bac Giang,Bac Kan,Bac Lieu,Bac Ninh,Ba Don,Bao Loc,Ba Ria,Ba Ria-Vung Tau,Ba Thuoc,Ben Cat,Ben Tre,Bien Hoa,Bim Son,Binh Dinh,Binh Duong,Binh Long,Binh Minh,Binh Phuoc,Binh Thuan,Buon Ho,Buon Ma Thuot,Cai Lay,Ca Mau,Cam Khe,Cam Pha,Cam Ranh,Cam Thuy,Can Tho,Cao Bang,Cao Lanh,Cao Phong,Chau Doc,Chi Linh,Con Cuong,Cua Lo,Da Bac,Dak Lak,Da Lat,Da Nang,Di An,Dien Ban,Dien Bien,Dien Bien Phu,Dien Chau,Do Luong,Dong Ha,Dong Hoi,Dong Trieu,Duc Pho,Duyen Hai,Duy Tien,Gia Lai,Gia Nghia,Gia Rai,Go Cong,Ha Giang,Ha Hoa,Hai Duong,Hai Phong,Ha Long,Ha Nam,Ha Noi,Ha Tinh,Ha Trung,Hau Giang,Hoa Binh,Hoang Mai,Hoa Thanh,Ho Chi Minh,Hoi An,Hong Linh,Hong Ngu,Hue,Hung Nguyen,Hung Yen,Huong Thuy,Huong Tra,Khanh Hoa,Kien Tuong,Kim Boi,Kinh Mon,Kon Tum,Ky Anh,Ky Son,Lac Son,Lac Thuy,La Gi,Lai Chau,Lam Thao,Lang Chanh,Lang Son,Lao Cai,Long An,Long Khanh,Long My,Long Xuyen,Luong Son,Mai Chau,Mong Cai,Muong Lat,Muong Lay,My Hao,My Tho,Nam Dan,Nam Dinh,Nga Bay,Nga Nam,Nga Son,Nghe An,Nghia Dan,Nghia Lo,Nghi Loc,Nghi Son,Ngoc Lac,Nha Trang,Nhu Thanh,Nhu Xuan,Ninh Binh,Ninh Hoa,Nong Cong,Phan Rang Thap Cham,Phan Thiet,Pho Yen,Phu Ly,Phu My,Phu Ninh,Phuoc Long,Phu Tho,Phu Yen,Pleiku,Quang Binh,Quang Nam,Quang Ngai,Quang Ninh,Quang Tri,Quang Xuong,Quang Yen,Quan Hoa,Quan Son,Que Phong,Quy Chau,Quy Hop,Quynh Luu,Quy Nhon,Rach Gia,Sa Dec,Sai Gon,Sam Son,Sa Pa,Soc Trang,Song Cau,Song Cong,Son La,Son Tay,Tam Diep,Tam Ky,Tan An,Tan Chau,Tan Ky,Tan Lac,Tan Son,Tan Uyen,Tay Ninh,Thach Thanh,Thai Binh,Thai Hoa,Thai Nguyen,Thanh Chuong,Thanh Hoa,Thieu Hoa,Thuan An,Thua Thien-Hue,Thu Dau Mot,Thu Duc,Thuong Xuan,Tien Giang,Trang Bang,Tra Vinh,Trieu Son,Tu Son,Tuyen Quang,Tuy Hoa,Uong Bi,Viet Tri,Vinh,Vinh Chau,Vinh Loc,Vinh Long,Vinh Yen,Vi Thanh,Vung Tau,Yen Bai,Yen Dinh,Yen Thanh,Yen Thuy"}, - {name: "Cantonese", i: 30, min: 5, max: 11, d: "", m: 0, b: "Chaiwan,Chingchung,Chinghoi,Chingsen,Chingshing,Chiunam,Chiuon,Chiuyeung,Chiyuen,Choihung,Chuehoi,Chuiman,Chungfu,Chungsan,Chunguktsuen,Dakhing,Daopo,Daumun,Dingwu,Dinpak,Donggun,Dongyuen,Duenchau,Fachau,Fanling,Fatgong,Fatshan,Fotan,Fuktien,Fumun,Funggong,Funghoi,Fungshun,Fungtei,Gamtin,Gochau,Goming,Gonghoi,Gongshing,Goyiu,Hanghau,Hangmei,Hengon,Heungchau,Heunggong,Heungkiu,Hingning,Hohfuktong,Hoichue,Hoifung,Hoiping,Hokong,Hokshan,Hoyuen,Hunghom,Hungshuikiu,Jiuling,Kamsheung,Kamwan,Kaulongtong,Keilun,Kinon,Kinsang,Kityeung,Kongmun,Kukgong,Kwaifong,Kwaihing,Kwongchau,Kwongling,Kwongming,Kwuntong,Laichikok,Laiking,Laiwan,Lamtei,Lamtin,Leitung,Leungking,Limkong,Linping,Linshan,Loding,Lokcheong,Lokfu,Longchuen,Longgong,Longmun,Longping,Longwa,Longwu,Lowu,Luichau,Lukfung,Lukho,Lungmun,Macheung,Maliushui,Maonshan,Mauming,Maunam,Meifoo,Mingkum,Mogong,Mongkok,Muichau,Muigong,Muiyuen,Naiwai,Namcheong,Namhoi,Namhong,Namsha,Nganwai,Ngautaukok,Ngchuen,Ngwa,Onting,Pakwun,Paotoishan,Pingshan,Pingyuen,Poklo,Pongon,Poning,Potau,Puito,Punyue,Saiwanho,Saiyingpun,Samshing,Samshui,Samtsen,Samyuenlei,Sanfung,Sanhing,Sanhui,Sanwai,Seiwui,Shamshuipo,Shanmei,Shantau,Shauking,Shekmun,Shekpai,Sheungshui,Shingkui,Shiuhing,Shundak,Shunyi,Shupinwai,Simshing,Siuhei,Siuhong,Siukwan,Siulun,Suikai,Taihing,Taikoo,Taipo,Taishuihang,Taiwai,Taiwohau,Tinhau,Tinshuiwai,Tiukengleng,Toishan,Tongfong,Tonglowan,Tsakyoochung,Tsamgong,Tsangshing,Tseungkwano,Tsimshatsui,Tsinggong,Tsingshantsuen,Tsingwun,Tsingyi,Tsingyuen,Tsiuchau,Tsuenshekshan,Tsuenwan,Tuenmun,Tungchung,Waichap,Waichau,Waidong,Wailoi,Waishing,Waiyeung,Wanchai,Wanfau,Wanshing,Wingon,Wongpo,Wongtaisin,Woping,Wukaisha,Yano,Yaumatei,Yautong,Yenfa,Yeungchun,Yeungdong,Yeungsai,Yeungshan,Yimtin,Yingdak,Yiuping,Yongshing,Yongyuen,Yuenlong,Yuenshing,Yuetsau,Yuknam,Yunping"}, - {name: "Mongolian", i: 31, min: 5, max: 12, d: "aou", m: .3, b: "Adaatsag,Airag,Alag Erdene,Altai,Altanshiree,Altantsogts,Arbulag,Baatsagaan,Batnorov,Batshireet,Battsengel,Bayan Adarga,Bayan Agt,Bayanbulag,Bayandalai,Bayandun,Bayangovi,Bayanjargalan,Bayankhongor,Bayankhutag,Bayanlig,Bayanmonkh,Bayannur,Bayannuur,Bayan Ondor,Bayan Ovoo,Bayantal,Bayantsagaan,Bayantumen,Bayan Uul,Bayanzurkh,Berkh,Biger,Binder,Bogd,Bombogor,Bor Ondor,Bugat,Bugt,Bulgan,Buregkhangai,Burentogtokh,Buutsagaan,Buyant,Chandmani,Chandmani Ondor,Choibalsan,Chuluunkhoroot,Chuluut,Dadal,Dalanjargalan,Dalanzadgad,Darhan Muminggan,Darkhan,Darvi,Dashbalbar,Dashinchilen,Delger,Delgerekh,Delgerkhaan,Delgerkhangai,Delgertsogt,Deluun,Deren,Dorgon,Duut,Erdene,Erdenebulgan,Erdeneburen,Erdenedalai,Erdenemandal,Erdenetsogt,Galshar,Galt,Galuut,Govi Ugtaal,Gurvan,Gurvanbulag,Gurvansaikhan,Gurvanzagal,Hinggan,Hodong,Holingol,Hondlon,Horin Ger,Horqin,Hulunbuir,Hure,Ikhkhet,Ikh Tamir,Ikh Uul,Jargalan,Jargalant,Jargaltkhaan,Jarud,Jinst,Khairkhan,Khalhgol,Khaliun,Khanbogd,Khangai,Khangal,Khankh,Khankhongor,Khashaat,Khatanbulag,Khatgal,Kherlen,Khishig Ondor,Khokh,Kholonbuir,Khongor,Khotont,Khovd,Khovsgol,Khuld,Khureemaral,Khurmen,Khutag Ondor,Luus,Mandakh,Mandal Ovoo,Mankhan,Manlai,Matad,Mogod,Monkhkhairkhan,Moron,Most,Myangad,Nogoonnuur,Nomgon,Norovlin,Noyon,Ogii,Olgii,Olziit,Omnodelger,Ondorkhaan,Ondorshil,Ondor Ulaan,Ongniud,Ordos,Orgon,Orkhon,Rashaant,Renchinlkhumbe,Sagsai,Saikhan,Saikhandulaan,Saikhan Ovoo,Sainshand,Saintsagaan,Selenge,Sergelen,Sevrei,Sharga,Sharyngol,Shine Ider,Shinejinst,Shiveegovi,Sumber,Taishir,Tarialan,Tariat,Teshig,Togrog,Togtoh,Tolbo,Tomorbulag,Tonkhil,Tosontsengel,Tsagaandelger,Tsagaannuur,Tsagaan Ovoo,Tsagaan Uur,Tsakhir,Tseel,Tsengel,Tsenkher,Tsenkhermandal,Tsetseg,Tsetserleg,Tsogt,Tsogt Ovoo,Tsogttsetsii,Tumed,Tunel,Tuvshruulekh,Ulaanbadrakh,Ulaankhus,Ulaan Uul,Ulanhad,Ulanqab,Uyench,Yesonbulag,Zag,Zalainur,Zamyn Uud,Zereg"}, - // fantasy bases by Dopu: - {name: "Human Generic", i: 32, min: 6, max: 11, d: "peolst", m: 0, b: "Amberglen,Angelhand,Arrowden,Autumnband,Autumnkeep,Basinfrost,Basinmore,Bayfrost,Beargarde,Bearmire,Bellcairn,Bellport,Bellreach,Blackwatch,Bleakward,Bonemouth,Boulder,Bridgefalls,Bridgeforest,Brinepeak,Brittlehelm,Bronzegrasp,Castlecross,Castlefair,Cavemire,Claymond,Claymouth,Clearguard,Cliffgate,Cliffshear,Cliffshield,Cloudbay,Cloudcrest,Cloudwood,Coldholde,Cragbury,Crowgrove,Crowvault,Crystalrock,Crystalspire,Cursefield,Curseguard,Cursespell,Dawnforest,Dawnwater,Deadford,Deadkeep,Deepcairn,Deerchill,Demonfall,Dewglen,Dewmere,Diredale,Direden,Dirtshield,Dogcoast,Dogmeadow,Dragonbreak,Dragonhold,Dragonward,Dryhost,Dustcross,Dustwatch,Eaglevein,Earthfield,Earthgate,Earthpass,Ebonfront,Edgehaven,Eldergate,Eldermere,Embervault,Everchill,Evercoast,Falsevale,Faypond,Fayvale,Fayyard,Fearpeak,Flameguard,Flamewell,Freyshell,Ghostdale,Ghostpeak,Gloomburn,Goldbreach,Goldyard,Grassplains,Graypost,Greeneld,Grimegrove,Grimeshire,Heartfall,Heartford,Heartvault,Highbourne,Hillpass,Hollowstorm,Honeywater,Houndcall,Houndholde,Iceholde,Icelight,Irongrave,Ironhollow,Knightlight,Knighttide,Lagoonpass,Lakecross,Lastmere,Laststar,Lightvale,Limeband,Littlehall,Littlehold,Littlemire,Lostcairn,Lostshield,Loststar,Madfair,Madham,Midholde,Mightglen,Millstrand,Mistvault,Mondpass,Moonacre,Moongulf,Moonwell,Mosshand,Mosstide,Mosswind,Mudford,Mudwich,Mythgulch,Mythshear,Nevercrest,Neverfront,Newfalls,Nighthall,Oakenbell,Oakenrun,Oceanstar,Oldreach,Oldwall,Oldwatch,Oxbrook,Oxlight,Pearlhaven,Pinepond,Pondfalls,Pondtown,Pureshell,Quickbell,Quickpass,Ravenside,Roguehaven,Roseborn,Rosedale,Rosereach,Rustmore,Saltmouth,Sandhill,Scorchpost,Scorchstall,Shadeforest,Shademeadow,Shadeville,Shimmerrun,Shimmerwood,Shroudrock,Silentkeep,Silvercairn,Silvergulch,Smallmire,Smoothcliff,Smoothgrove,Smoothtown,Snakemere,Snowbay,Snowshield,Snowtown,Southbreak,Springmire,Springview,Stagport,Steammouth,Steamwall,Steepmoor,Stillhall,Stoneguard,Stonespell,Stormhand,Stormhorn,Sungulf,Sunhall,Swampmaw,Swangarde,Swanwall,Swiftwell,Thorncairn,Thornhelm,Thornyard,Timberside,Tradewick,Westmeadow,Westpoint,Whiteshore,Whitvalley,Wildeden,Wildwell,Wildyard,Winterhaven,Wolfpass"}, - {name: "Elven", i: 33, min: 6, max: 12, d: "lenmsrg", m: 0, b: "Adrindest,Aethel,Afranthemar,Aiqua,Alari,Allanar,Almalian,Alora,Alyanasari,Alyelona,Alyran,Ammar,Anyndell,Arasari,Aren,Ashmebel,Aymlume,Bel-Didhel,Brinorion,Caelora,Chaulssad,Chaundra,Cyhmel,Cyrang,Dolarith,Dolonde,Draethe,Dranzan,Draugaust,E'ana,Eahil,Edhil,Eebel,Efranluma,Eld-Sinnocrin,Elelthyr,Ellanalin,Ellena,Ellorthond,Eltaesi,Elunore,Emyranserine,Entheas,Eriargond,Esari,Esath,Eserius,Eshsalin,Eshthalas,Evraland,Faellenor,Famelenora,Filranlean,Filsaqua,Gafetheas,Gaf Serine,Geliene,Gondorwin,Guallu,Haeth,Hanluna,Haulssad,Heloriath,Himlarien,Himliene,Hinnead,Hlinas,Hloireenil,Hluihei,Hlurthei,Hlynead,Iaenarion,Iaron,Illanathaes,Illfanora,Imlarlon,Imyse,Imyvelian,Inferius,Inlurth,innsshe,Iralserin,Irethtalos,Irholona,Ishal,Ishlashara,Ithelion,Ithlin,Iulil,Jaal,Jamkadi,Kaalume,Kaansera,Karanthanil,Karnosea,Kasethyr,Keatheas,Kelsya,Keth Aiqua,Kmlon,Kyathlenor,Kyhasera,Lahetheas,Lefdorei,Lelhamelle,Lilean,Lindeenil,Lindoress,Litys,Llaughei,Lya,Lyfa,Lylharion,Lynathalas,Machei,Masenoris,Mathethil,Mathentheas,Meethalas,Menyamar,Mithlonde,Mytha,Mythsemelle,Mythsthas,Naahona,Nalore,Nandeedil,Nasad Ilaurth,Nasin,Nathemar,Neadar,Neilon,Nelalon,Nellean,Nelnetaesi,Nilenathyr,Nionande,Nylm,Nytenanas,Nythanlenor,O'anlenora,Obeth,Ofaenathyr,Ollmnaes,Ollsmel,Olwen,Olyaneas,Omanalon,Onelion,Onelond,Orlormel,Ormrion,Oshana,Oshvamel,Raethei,Rauguall,Reisera,Reslenora,Ryanasera,Rymaserin,Sahnor,Saselune,Sel-Zedraazin,Selananor,Sellerion,Selmaluma,Shaeras,Shemnas,Shemserin,Sheosari,Sileltalos,Siriande,Siriathil,Srannor,Sshanntyr,Sshaulu,Syholume,Sylharius,Sylranbel,Taesi,Thalor,Tharenlon,Thelethlune,Thelhohil,Themar,Thene,Thilfalean,Thilnaenor,Thvethalas,Thylathlond,Tiregul,Tlauven,Tlindhe,Ulal,Ullve,Ulmetheas,Ulssin,Umnalin,Umye,Umyheserine,Unanneas,Unarith,Undraeth,Unysarion,Vel-Shonidor,Venas,Vin Argor,Wasrion,Wlalean,Yaeluma,Yeelume,Yethrion,Ymserine,Yueghed,Yuerran,Yuethin"}, - {name: "Dark Elven", i: 34, min: 6, max: 14, d: "nrslamg", m: .2, b: "Abaethaggar,Abburth,Afranthemar,Aharasplit,Aidanat,Ald'ruhn,Ashamanu,Ashesari,Ashletheas,Baerario,Baereghel,Baethei,Bahashae,Balmora,Bel-Didhel,Borethanil,Buiyrandyn,Caellagith,Caellathala,Caergroth,Caldras,Chaggar,Chaggaust,Channtar,Charrvhel'raugaust,Chaulssin,Chaundra,ChedNasad,ChetarIthlin,ChethRrhinn,Chymaer,Clarkarond,Cloibbra,Commoragh,Cyrangroth,Cilben,D'eldarc,Daedhrog,Dalkyn,Do'Urden,Doladress,Dolarith,Dolonde,Draethe,Dranzan,Dranzithl,Draugaust,Dreghei,Drelhei,Dryndlu,Dusklyngh,DyonG'ennivalz,Edraithion,Eld-Sinnocrin,Ellorthond,Enhethyr,Entheas,ErrarIthinn,Eryndlyn,Faladhell,Faneadar,Fethalas,Filranlean,Formarion,Ferdor,Gafetheas,Ghrond,Gilranel,Glamordis,Gnaarmok,Gnisis,Golothaer,Gondorwin,Guallidurth,Guallu,Gulshin,Haeth,Haggraef,Harganeth,Harkaldra,Haulssad,Haundrauth,Heloriath,Hlammachar,Hlaughei,Hloireenil,Hluitar,Inferius,Innsshe,Ithilaughym,Iz'aiogith,Jaal,Jhachalkhyn,Kaerabrae,Karanthanil,Karondkar,Karsoluthiyl,Kellyth,Khuul,Lahetheas,Lidurth,Lindeenil,Lirillaquen,LithMy'athar,LlurthDreier,Lolth,Lothuial,Luihaulen'tar,Maeralyn,Maerimydra,Mathathlona,Mathethil,Mellodona,Menagith,Menegwen,Menerrendil,Menzithl,Menzoberranzan,Mila-Nipal,Mithryn,Molagmar,Mundor,Myvanas,Naggarond,Nandeedil,NasadIlaurth,Nauthor,Navethas,Neadar,Nurtaleewe,Nidiel,Noruiben,Olwen,O'lalona,Obeth,Ofaenathyr,Orlormel,Orlytlar,Pelagiad,Raethei,Raugaust,Rauguall,Rilauven,Rrharrvhei,Sadrith,Sel-Zedraazin,Seydaneen,Shaz'rir,Skaal,Sschindylryn,Shamath,Shamenz,Shanntur,Sshanntynlan,Sshanntyr,Shaulssin,SzithMorcane,Szithlin,Szobaeth,Sirdhemben,T'lindhet,Tebh'zhor,Telmere,Telnarquel,Tharlarast,Thylathlond,Tlaughe,Trizex,Tyrybblyn,Ugauth,Ughym,Uhaelben,Ullmatalos,Ulmetheas,Ulrenserine,Uluitur,Undraeth,Undraurth,Undrek'Thoz,Ungethal,UstNatha,Uthaessien,V'elddrinnsshar,Vaajha,Vel-Shonidor,Velddra,Velothi,Venead,Vhalth'vha,Vinargothr,Vojha,Waethe,Waethei,Xaalkis,Yakaridan,Yeelume,Yridhremben,Yuethin,Yuethindrynn,Zirnakaynin"}, - {name: "Dwarven", i: 35, min: 4, max: 11, d: "dk", m: 0, b: "Addundad,Ahagzad,Ahazil,Akil,Akzizad,Anumush,Araddush,Arar,Arbhur,Badushund,Baragzig,Baragzund,Barakinb,Barakzig,Barakzinb,Barakzir,Baramunz,Barazinb,Barazir,Bilgabar,Bilgatharb,Bilgathaz,Bilgila,Bilnaragz,Bilnulbar,Bilnulbun,Bizaddum,Bizaddush,Bizanarg,Bizaram,Bizinbiz,Biziram,Bunaram,Bundinar,Bundushol,Bundushund,Bundushur,Buzaram,Buzundab,Buzundush,Gabaragz,Gabaram,Gabilgab,Gabilgath,Gabizir,Gabunal,Gabunul,Gabuzan,Gatharam,Gatharbhur,Gathizdum,Gathuragz,Gathuraz,Gila,Giledzir,Gilukkhath,Gilukkhel,Gunala,Gunargath,Gunargil,Gundumunz,Gundusharb,Gundushizd,Kharbharbiln,Kharbhatharb,Kharbhela,Kharbilgab,Kharbuzadd,Khatharbar,Khathizdin,Khathundush,Khazanar,Khazinbund,Khaziragz,Khaziraz,Khizdabun,Khizdusharbh,Khizdushath,Khizdushel,Khizdushur,Kholedzar,Khundabiln,Khundabuz,Khundinarg,Khundushel,Khuragzig,Khuramunz,Kibarak,Kibilnal,Kibizar,Kibunarg,Kibundin,Kibuzan,Kinbadab,Kinbaragz,Kinbarakz,Kinbaram,Kinbizah,Kinbuzar,Nala,Naledzar,Naledzig,Naledzinb,Naragzah,Naragzar,Naragzig,Narakzah,Narakzar,Naramunz,Narazar,Nargabad,Nargabar,Nargatharb,Nargila,Nargundum,Nargundush,Nargunul,Narukthar,Narukthel,Nula,Nulbadush,Nulbaram,Nulbilnarg,Nulbunal,Nulbundab,Nulbundin,Nulbundum,Nulbuzah,Nuledzah,Nuledzig,Nulukkhaz,Nulukkhund,Nulukkhur,Sharakinb,Sharakzar,Sharamunz,Sharbarukth,Shatharbhizd,Shatharbiz,Shathazah,Shathizdush,Shathola,Shaziragz,Shizdinar,Shizdushund,Sholukkharb,Shundinulb,Shundushund,Shurakzund,Shuramunz,Tumunzadd,Tumunzan,Tumunzar,Tumunzinb,Tumunzir,Ukthad,Ulbirad,Ulbirar,Ulunzar,Ulur,Umunzad,Undalar,Undukkhil,Undun,Undur,Unduzur,Unzar,Unzathun,Usharar,Zaddinarg,Zaddushur,Zaharbad,Zaharbhizd,Zarakib,Zarakzar,Zaramunz,Zarukthel,Zinbarukth,Zirakinb,Zirakzir,Ziramunz,Ziruktharbh,Zirukthur,Zundumunz"}, - {name: "Goblin", i: 36, min: 4, max: 9, d: "eag", m: 0, b: "Asinx,Bhiagielt,Biokvish,Blix,Blus,Bratliaq,Breshass,Bridvelb,Brybsil,Bugbig,Buyagh,Cel,Chalk,Chiafzia,Chox,Cielb,Cosvil,Crekork,Crild,Croibieq,Diervaq,Dobruing,Driord,Eebligz,Een,Enissee,Esz,Far,Felhob,Froihiofz,Fruict,Fygsee,Gagablin,Gigganqi,Givzieqee,Glamzofs,Glernaahx,Gneabs,Gnoklig,Gobbledak,gobbok,Gobbrin,Heszai,Hiszils,Hobgar,Honk,Iahzaarm,Ialsirt,Ilm,Ish,Jasheafta,Joimtoilm,Kass,Katmelt,Kleabtong,Kleardeek,Klilm,Kluirm,Kuipuinx,Moft,Mogg,Nilbog,Oimzoishai,Onq,Ozbiard,Paas,Phax,Phigheldai,Preang,Prolkeh,Pyreazzi,Qeerags,Qosx,Rekx,Shaxi,Sios,Slehzit,Slofboif,Slukex,Srefs,Srurd,Stiaggaltia,Stiolx,Stioskurt,Stroir,Strytzakt,Stuikvact,Styrzangai,Suirx,Swaxi,Taxai,Thelt,Thresxea,Thult,Traglila,Treaq,Ulb,Ulm,Utha,Utiarm,Veekz,Vohniots,Vreagaald,Watvielx,Wrogdilk,Wruilt,Xurx,Ziggek,Zriokots"}, - {name: "Orc", i: 37, min: 4, max: 8, d: "gzrcu", m: 0, b: "Adgoz,Adgril-Gha,Adog,Adzurd,Agkadh,Agzil-Ghal,Akh,Ariz-Dru,Arkugzo,Arrordri,Ashnedh,Azrurdrekh,Bagzildre,Bashnud,Bedgez-Graz,Bhakh,Bhegh,Bhiccozdur,Bhicrur,Bhirgoshbel,Bhog,Bhurkrukh,Bod-Rugniz,Bogzel,Bozdra,Bozgrun,Bozziz,Bral-Lazogh,Brazadh,Brogved,Brogzozir,Brolzug,Brordegeg,Brorkril-Zrog,Brugroz,Brukh-Zrabrul,Brur-Korre,Bulbredh,Bulgragh,Chaz-Charard,Chegan-Khed,Chugga,Chuzar,Dhalgron-Mog,Dhazon-Ner,Dhezza,Dhoddud,Dhodh-Brerdrodh,Dhodh-Ghigin,Dhoggun-Bhogh,Dhulbazzol,Digzagkigh,Dirdrurd,Dodkakh,Dorgri,Drizdedh,Drobagh,Drodh-Ashnugh,Drogvukh-Drodh,Drukh-Qodgoz,Drurkuz,Dududh,Dur-Khaddol,Egmod,Ekh-Beccon,Ekh-Krerdrugh,Ekh-Mezred,Gagh-Druzred,Gazdrakh-Vrard,Gegnod,Gerkradh,Ghagrocroz,Ghared-Krin,Ghedgrolbrol,Gheggor,Ghizgil,Gho-Ugnud,Gholgard,Gidh-Ucceg,Goccogmurd,Golkon,Graz-Khulgag,Gribrabrokh,Gridkog,Grigh-Kaggaz,Grirkrun-Qur,Grughokh,Grurro,Gugh-Zozgrod,Gur-Ghogkagh,Ibagh-Chol,Ibruzzed,Ibul-Brad,Iggulzaz,Ikh-Ugnan,Irdrelzug,Irmekh-Bhor,Kacruz,Kalbrugh,Karkor-Zrid,Kazzuz-Zrar,Kezul-Bruz,Kharkiz,Khebun,Khorbric,Khuldrerra,Khuzdraz,Kirgol,Koggodh,Korkrir-Grar,Kraghird,Krar-Zurmurd,Krigh-Bhurdin,Kroddadh,Krudh-Khogzokh,Kudgroccukh,Kudrukh,Kudzal,Kuzgrurd-Dedh,Larud,Legvicrodh,Lorgran,Lugekh,Lulkore,Mazgar,Merkraz,Mocculdrer,Modh-Odod,Morbraz,Mubror,Muccug-Ghuz,Mughakh-Chil,Murmad,Nazad-Ludh,Negvidh,Nelzor-Zroz,Nirdrukh,Nogvolkar,Nubud,Nuccag,Nudh-Kuldra,Nuzecro,Oddigh-Krodh,Okh-Uggekh,Ordol,Orkudh-Bhur,Orrad,Qashnagh,Qiccad-Chal,Qiddolzog,Qidzodkakh,Qirzodh,Rarurd,Reradgri,Rezegh,Rezgrugh,Rodrekh,Rogh-Chirzaz,Rordrushnokh,Rozzez,Ruddirgrad,Rurguz-Vig,Ruzgrin,Ugh-Vruron,Ughudadh,Uldrukh-Bhudh,Ulgor,Ulkin,Ummugh-Ekh,Uzaggor,Uzdriboz,Uzdroz,Uzord,Uzron,Vaddog,Vagord-Khod,Velgrudh,Verrugh,Vrazin,Vrobrun,Vrugh-Nardrer,Vrurgu,Vuccidh,Vun-Gaghukh,Zacrad,Zalbrez,Zigmorbredh,Zordrordud,Zorrudh,Zradgukh,Zragmukh,Zragrizgrakh,Zraldrozzuz,Zrard-Krodog,Zrazzuz-Vaz,Zrigud,Zrulbukh-Dekh,Zubod-Ur,Zulbriz,Zun-Bergrord"}, - {name: "Giant", i: 38, min: 5, max: 10, d: "kdtng", m: 0, b: "Addund,Aerora,Agane,Anumush,Arangrim,Bahourg,Baragzund,Barakinb,Barakzig,Barakzinb,Baramunz,Barazinb,Beornelde,Beratira,Borgbert,Botharic,Bremrol,Brerstin,Brildung,Brozu,Bundushund,Burthug,Chazruc,Chergun,Churtec,Dagdhor,Dankuc,Darnaric,Debuch,Dina,Dinez,Diru,Drard,Druguk,Dugfast,Duhal,Dulkun,Eldond,Enuz,Eraddam,Eradhelm,Froththorn,Fynwyn,Gabaragz,Gabaram,Gabizir,Gabuzan,Gagkake,Galfald,Galgrim,Gatal,Gazin,Geru,Gila,Giledzir,Girkun,Glumvat,Gluthmark,Gomruch,Gorkege,Gortho,Gostuz,Grimor,Grimtira,Guddud,Gudgiz,Gulwo,Gunargath,Gundusharb,Guril,Gurkale,Guruge,Guzi,Hargarth,Hartreo,Heimfara,Hildlaug,Idgurth,Inez,Inginy,Iora,Irkin,Jaldhor,Jarwar,Jornangar,Jornmoth,Kakkek,Kaltoch,Kegkez,Kengord,Kharbharbiln,Khatharbar,Khathizdin,Khazanar,Khaziragz,Khizdabun,Khizdushel,Khundinarg,Kibarak,Kibizar,Kigine,Kilfond,Kilkan,Kinbadab,Kinbuzar,Koril,Kostand,Kuzake,Lindira,Lingarth,Maerdis,Magald,Marbold,Marbrand,Memron,Minu,Mistoch,Morluch,Mornkin,Morntaric,Nagu,Naragzah,Naramunz,Narazar,Nargabar,Nargatharb,Nargundush,Nargunul,Natan,Natil,Neliz,Nelkun,Noluch,Norginny,Nulbaram,Nulbilnarg,Nuledzah,Nuledzig,Nulukkhaz,Nulukkhur,Nurkel,Oci,Olane,Oldstin,Orga,Ranava,Ranhera,Rannerg,Rirkan,Rizen,Rurki,Rurkoc,Sadgach,Sgandrol,Sharakzar,Shatharbiz,Shathizdush,Shathola,Shizdinar,Sholukkharb,Shundushund,Shurakzund,Sidga,Sigbeorn,Sigbi,Solfod,Somrud,Srokvan,Stighere,Sulduch,Talkale,Theoddan,Theodgrim,Throtrek,Tigkiz,Tolkeg,Toren,Tozage,Tulkug,Tumunzar,Umunzad,Undukkhil,Usharar,Valdhere,Varkud,Velfirth,Velhera,Vigkan,Vorkige,Vozig,Vylwed,Widhyrde,Wylaeya,Yili,Yotane,Yudgor,Yulkake,Zigez,Zugkan,Zugke"}, - {name: "Draconic", i: 39, min: 6, max: 14, d: "aliuszrox", m: 0, b: "Aaronarra,Adalon,Adamarondor,Aeglyl,Aerosclughpalar,Aghazstamn,Aglaraerose,Agoshyrvor,Alduin,Alhazmabad,Altagos,Ammaratha,Amrennathed,Anaglathos,Andrathanach,Araemra,Araugauthos,Arauthator,Arharzel,Arngalor,Arveiaturace,Athauglas,Augaurath,Auntyrlothtor,Azarvilandral,Azhaq,Balagos,Baratathlaer,Bleucorundum,BrazzPolis,Canthraxis,Capnolithyl,Charvekkanathor,Chellewis,Chelnadatilar,Cirrothamalan,Claugiyliamatar,Cragnortherma,Dargentum,Dendeirmerdammarar,Dheubpurcwenpyl,Domborcojh,Draconobalen,Dragansalor,Dupretiskava,Durnehviir,Eacoathildarandus,Eldrisithain,Enixtryx,Eormennoth,Esmerandanna,Evenaelorathos,Faenphaele,Felgolos,Felrivenser,Firkraag,Fll'Yissetat,Furlinastis,Galadaeros,Galglentor,Garnetallisar,Garthammus,Gaulauntyr,Ghaulantatra,Glouroth,Greshrukk,Guyanothaz,Haerinvureem,Haklashara,Halagaster,Halaglathgar,Havarlan,Heltipyre,Hethcypressarvil,Hoondarrh,Icehauptannarthanyx,Iiurrendeem,Ileuthra,Iltharagh,Ingeloakastimizilian,Irdrithkryn,Ishenalyr,Iymrith,Jaerlethket,Jalanvaloss,Jharakkan,Kasidikal,Kastrandrethilian,Khavalanoth,Khuralosothantar,Kisonraathiisar,Kissethkashaan,Kistarianth,Klauth,Klithalrundrar,Krashos,Kreston,Kriionfanthicus,Krosulhah,Krustalanos,Kruziikrel,Kuldrak,Lareth,Latovenomer,Lhammaruntosz,Llimark,Ma'fel'no'sei'kedeh'naar,MaelestorRex,Magarovallanthanz,Mahatnartorian,Mahrlee,Malaeragoth,Malagarthaul,Malazan,Maldraedior,Maldrithor,MalekSalerno,Maughrysear,Mejas,Meliordianix,Merah,Mikkaalgensis,Mirmulnir,Mistinarperadnacles,Miteach,Mithbarazak,Morueme,Moruharzel,Naaslaarum,Nahagliiv,Nalavarauthatoryl,Naxorlytaalsxar,Nevalarich,Nolalothcaragascint,Nurvureem,Nymmurh,Odahviing,Olothontor,Ormalagos,Otaaryliakkarnos,Paarthurnax,Pelath,Pelendralaar,Praelorisstan,Praxasalandos,Protanther,Qiminstiir,Quelindritar,Ralionate,Rathalylaug,Rathguul,Rauglothgor,Raumorthadar,Relonikiv,Ringreemeralxoth,Roraurim,Rynnarvyx,Sablaxaahl,Sahloknir,Sahrotaar,Samdralyrion,Saryndalaghlothtor,Sawaka,Shalamalauth,Shammagar,Sharndrel,Shianax,Skarlthoon,Skurge,Smergadas,Ssalangan,Sssurist,Sussethilasis,Sylvallitham,Tamarand,Tantlevgithus,Tarlacoal,Tenaarlaktor,Thalagyrt,Tharas'kalagram,Thauglorimorgorus,Thoklastees,Thyka,Tsenshivah,Ueurwen,Uinnessivar,Urnalithorgathla,Velcuthimmorhar,Velora,Vendrathdammarar,Venomindhar,Viinturuth,Voaraghamanthar,Voslaarum,Vr'tark,Vrondahorevos,Vuljotnaak,Vulthuryol,Wastirek,Worlathaugh,Xargithorvar,Xavarathimius,Yemere,Ylithargathril,Ylveraasahlisar,Za-Jikku,Zarlandris,Zellenesterex,Zilanthar,Zormapalearath,Zundaerazylym,Zz'Pzora"}, - {name: "Arachnid", i: 40, min: 4, max: 10, d: "erlsk", m: 0, b: "Aaqok'ser,Aiced,Aizachis,Allinqel,As'taq,Ashrash,Caaqtos,Ceek'sax,Ceezuq,Cek'sier,Cen'qi,Ceqzocer,Cezeed,Chachocaq,Charis,Chashilieth,Checib,Chernul,Chezi,Chiazu,Chishros,Chixhi,Chizhi,Chollash,Choq'sha,Cinchichail,Collul,Ecush'taid,Ekiqe,Eqas,Er'uria,Erikas,Es'tase,Esrub,Exha,Haqsho,Hiavheesh,Hitha,Hok'thi,Hossa,Iacid,Iciever,Illuq,Isnir,Keezut,Kheellavas,Kheizoh,Khiachod,Khika,Khirzur,Khonrud,Khrakku,Khraqshis,Khrethish'ti,Khriashus,Khrika,Khrirni,Klashirel,Kleil'sha,Klishuth,Krarnit,Kras'tex,Krotieqas,Lais'tid,Laizuh,Lasnoth,Len'qeer,Leqanches,Lezad,Lhilir,Lhivhath,Lhok'thu,Lialliesed,Liaraq,Liceva,Lichorro,Lilla,Lokieqib,Nakur,Neerhaca,Neet'er,Neezoh,Nenchiled,Nerhalneth,Nir'ih,Nizus,Noreeqo,On'qix,Qalitho,Qas'tor,Qasol,Qavrud,Qavud,Qazar,Qazru,Qekno,Qeqravee,Qes'tor,Qhaik'sal,Qhak'sish,Qhazsakais,Qheliva,Qhenchaqes,Qherazal,Qhon'qos,Qhosh,Qish'tur,Qisih,Qorhoci,Qranchiq,Racith,Rak'zes,Ranchis,Rarhie,Rarzi,Rarzisiaq,Ras'tih,Ravosho,Recad,Rekid,Rernee,Rertachis,Rezhokketh,Reziel,Rhacish,Rhail'shel,Rhairhizse,Rhakivex,Rhaqeer,Rhartix,Rheciezsei,Rheevid,Rhel'shir,Rhevhie,Rhiavekot,Rhikkos,Rhiqese,Rhiqi,Rhiqracar,Rhisned,Rhousnateb,Riakeesnex,Rintachal,Rir'ul,Rourk'u,Rouzakri,Sailiqei,Sanchiqed,Saqshu,Sat'ier,Sazi,Seiqas,Shieth'i,Shiqsheh,Shizha,Shrachuvo,Shranqo,Shravhos,Shravuth,Shreerhod,Shrethuh,Shriantieth,Shronqash,Shrovarhir,Shrozih,Siacaqoh,Siezosh,Siq'sha,Sirro,Sornosi,Srachussi,Szaca,Szacih,Szaqova,Szasu,Szazhilos,Szeerrud,Szeezsad,Szeknur,Szesir,Szezhirros,Szilshith,Szon'qol,Szornuq,Xeekke,Yeek'su,Yeeq'zox,Yeqil,Yeqroq,Yeveed,Yevied,Yicaveeh,Yirresh,Yisie,Yithik'thaih,Yorhaqshes,Zacheek'sa,Zakkasa,Zelraq,Zeqo,Zharuncho,Zhath'arhish,Zhavirrit,Zhazilraq,Zhazsachiel,Zhek'tha,Zhequ,Zhias'ted,Zhicat,Zhicur,Zhirhacil,Zhizri,Zhochizses,Ziarih,Zirnib"}, - {name: "Serpents", i: 41, min: 5, max: 11, d: "slrk", m: 0, b: "Aj'ha,Aj'i,Aj'tiss,Ajakess,Aksas,Aksiss,Al'en,An'jeshe,Apjige,Arkkess,Athaz,Atus,Azras,Caji,Cakrasar,Cal'arrun,Capji,Cathras,Cej'han,Ces,Cez'jenta,Cij'te,Cinash,Cizran,Coth'jus,Cothrash,Culzanek,Cunaless,Ej'tesh,Elzazash,Ergek,Eshjuk,Ethris,Gan'jas,Gapja,Gar'thituph,Gopjeguss,Gor'thesh,Gragishaph,Grar'theness,Grath'ji,Gressinas,Grolzesh,Grorjar,Grozrash,Guj'ika,Harji,Hej'hez,Herkush,Horgarrez,Illuph,Ipjar,Ithashin,Kaj'ess,Kar'kash,Kepjusha,Ki'kintus,Kissere,Koph,Kopjess,Kra'kasher,Krak,Krapjez,Krashjuless,Kraz'ji,Krirrigis,Krussin,Ma'lush,Mage,Maj'tak,Mal'a,Mapja,Mar'kash,Mar'kis,Marjin,Mas,Mathan,Men'jas,Meth'jaresh,Mij'hegak,Min'jash,Mith'jas,Monassu,Moss,Naj'hass,Najugash,Nak,Napjiph,Nar'ka,Nar'thuss,Narrusha,Nash,Nashjekez,Nataph,Nij'ass,Nij'tessiph,Nishjiss,Norkkuss,Nus,Olluruss,Or'thi,Or'thuss,Paj'a,Parkka,Pas,Pathujen,Paz'jaz,Pepjerras,Pirkkanar,Pituk,Porjunek,Pu'ke,Ragen,Ran'jess,Rargush,Razjuph,Rilzan,Riss,Rithruz,Rorgiss,Rossez,Rraj'asesh,Rraj'tass,Rrar'kess,Rrar'thuph,Rras,Rrazresh,Rrej'hish,Rrigelash,Rris,Rris,Rroksurrush,Rukrussush,Rurri,Russa,Ruth'jes,Sa'kitesh,Sar'thass,Sarjas,Sazjuzush,Ser'thez,Sezrass,Shajas,Shas,Shashja,Shass,Shetesh,Shijek,Shun'jaler,Shurjarri,Skaler,Skalla,Skallentas,Skaph,Skar'kerriz,Skath'jeruk,Sker'kalas,Skor,Skoz'ji,Sku'lu,Skuph,Skur'thur,Slalli,Slalt'har,Slelziress,Slil'ar,Sloz'jisa,Sojesh,Solle,Sorge,Sral'e,Sran'ji,Srapjess,Srar'thazur,Srash,Srath'jess,Srathrarre,Srerkkash,Srus,Sruss'tugeph,Sun,Suss'tir,Uzrash,Vargush,Vek,Vess'tu,Viph,Vult'ha,Vupjer,Vushjesash,Xagez,Xassa,Xulzessu,Zaj'tiss,Zan'jer,Zarriss,Zassegus,Zirres,Zsor,Zurjass"}, - // additional by Avengium: - {name: "Levantine", i: 42, min: 4, max: 12, d: "ankprs", m: 0, b: "Adme,Adramet,Agadir,Akko,Akzib,Alimas,Alis-Ubbo,Alqosh,Amid,Ammon,Ampi,Amurru,Andarig,Anpa,Araden,Aram,Arwad,Ashkelon,Athar,Atiq,Aza,Azeka,Baalbek,Babel,Batrun,Beerot,Beersheba,Beit Shemesh,Berytus,Bet Agus,Bet Anya,Beth-Horon,Bethel,Bethlehem,Bethuel,Bet Nahrin,Bet Nohadra,Bet Zalin,Birmula,Biruta,Bit Agushi,Bitan,Bit Zamani,Cerne,Dammeseq,Darmsuq,Dor,Eddial,Eden Ekron,Elah,Emek,Emun,Ephratah,Eyn Ganim,Finike,Gades,Galatia,Gaza,Gebal,Gedera,Gerizzim,Gethsemane,Gibeon,Gilead,Gilgal,Golgotha,Goshen,Gytte,Hagalil,Haifa,Halab,Haqel Dma,Har Habayit,Har Nevo,Har Pisga,Havilah,Hazor,Hebron,Hormah,Iboshim,Iriho,Irinem,Irridu,Israel,Kadesh,Kanaan,Kapara,Karaly,Kart-Hadasht,Keret Chadeshet,Kernah,Kesed,Keysariya,Kfar,Kfar Nahum,Khalibon,Khalpe,Khamat,Kiryat,Kittim,Kurda,Lapethos,Larna,Lepqis,Lepriptza,Liksos,Lod,Luv,Malaka,Malet,Marat,Megido,Melitta,Merdin,Metsada,Mishmarot,Mitzrayim,Moab,Mopsos,Motye,Mukish,Nampigi,Nampigu,Natzrat,Nimrud,Nineveh,Nob,Nuhadra,Oea,Ofir,Oyat,Phineka,Phoenicus,Pleshet,Qart-Tubah Sarepta,Qatna,Rabat Amon,Rakkath,Ramat Aviv,Ramitha,Ramta,Rehovot,Reshef,Rushadir,Rushakad,Samrin,Sefarad,Sehyon,Sepat,Sexi,Sharon,Shechem,Shefelat,Shfanim,Shiloh,Shmaya,Shomron,Sidon,Sinay,Sis,Solki,Sur,Suria,Tabetu,Tadmur,Tarshish,Tartus,Teberya,Tefessedt,Tekoa,Teyman,Tinga,Tipasa,Tsabratan,Tur Abdin,Tzarfat,Tziyon,Tzor,Ugarit,Unubaal,Ureshlem,Urhay,Urushalim,Vaga,Yaffa,Yamhad,Yam hamelach,Yam Kineret,Yamutbal,Yathrib,Yaudi,Yavne,Yehuda,Yerushalayim,Yev,Yevus,Yizreel,Yurdnan,Zarefat,Zeboim,Zeurta,Zeytim,Zikhron,Zmurna"} - ]; - }; - - return { - getBase, - getCulture, - getCultureShort, - getBaseShort, - getState, - updateChain, - clearChains, - getNameBases, - getMapName, - calculateChain - }; -})(); diff --git a/modules/ocean-layers.js b/modules/ocean-layers.js deleted file mode 100644 index 281fad0a..00000000 --- a/modules/ocean-layers.js +++ /dev/null @@ -1,92 +0,0 @@ -"use strict"; - -window.OceanLayers = (function () { - let cells, vertices, pointsN, used; - - const OceanLayers = function OceanLayers() { - const outline = oceanLayers.attr("layers"); - if (outline === "none") return; - TIME && console.time("drawOceanLayers"); - - lineGen.curve(d3.curveBasisClosed); - (cells = grid.cells), (pointsN = grid.cells.i.length), (vertices = grid.vertices); - const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s); - - const chains = []; - const opacity = rn(0.4 / limits.length, 2); - used = new Uint8Array(pointsN); // to detect already passed cells - - for (const i of cells.i) { - const t = cells.t[i]; - if (t > 0) continue; - if (used[i] || !limits.includes(t)) continue; - const start = findStart(i, t); - if (!start) continue; - used[i] = 1; - const chain = connectVertices(start, t); // vertices chain to form a path - if (chain.length < 4) continue; - const relax = 1 + t * -2; // select only n-th point - const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some(c => c >= pointsN)); - if (relaxed.length < 4) continue; - const points = clipPoly( - relaxed.map(v => vertices.p[v]), - 1 - ); - chains.push([t, points]); - } - - for (const t of limits) { - const layer = chains.filter(c => c[0] === t); - let path = layer.map(c => round(lineGen(c[1]))).join(""); - if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").attr("fill-opacity", opacity); - } - - // find eligible cell vertex to start path detection - function findStart(i, t) { - if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= pointsN)); // map border cell - return cells.v[i][cells.c[i].findIndex(c => cells.t[c] < t || !cells.t[c])]; - } - - TIME && console.timeEnd("drawOceanLayers"); - }; - - function randomizeOutline() { - const limits = []; - let odd = 0.2; - for (let l = -9; l < 0; l++) { - if (P(odd)) { - odd = 0.2; - limits.push(l); - } else { - odd *= 2; - } - } - return limits; - } - - // connect vertices to chain - function connectVertices(start, t) { - const chain = []; // vertices chain to form a path - for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) { - const prev = chain[chain.length - 1]; // previous vertex in chain - chain.push(current); // add current vertex to sequence - const c = vertices.c[current]; // cells adjacent to vertex - c.filter(c => cells.t[c] === t).forEach(c => (used[c] = 1)); - const v = vertices.v[current]; // neighboring vertices - const c0 = !cells.t[c[0]] || cells.t[c[0]] === t - 1; - const c1 = !cells.t[c[1]] || cells.t[c[1]] === t - 1; - const c2 = !cells.t[c[2]] || cells.t[c[2]] === t - 1; - if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0]; - else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1]; - else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2]; - if (current === chain[chain.length - 1]) { - ERROR && console.error("Next vertex is not found"); - break; - } - } - chain.push(chain[0]); // push first vertex as the last one - return chain; - } - - return OceanLayers; -})(); diff --git a/modules/provinces-generator.js b/modules/provinces-generator.js deleted file mode 100644 index 3276fdf0..00000000 --- a/modules/provinces-generator.js +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; - -window.Provinces = (function () { - const forms = { - Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1}, - Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1}, - Theocracy: {Parish: 3, Deanery: 1}, - Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1}, - Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1}, - Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1} - }; - - const generate = (regenerate = false, regenerateLockedStates = false) => { - TIME && console.time("generateProvinces"); - const localSeed = regenerate ? generateSeed() : seed; - Math.random = aleaPRNG(localSeed); - - const {cells, states, burgs} = pack; - const provinces = [0]; // 0 index is reserved for "no province" - const provinceIds = new Uint16Array(cells.i.length); - - const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock); - const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]); - - if (regenerate) { - pack.provinces.forEach(province => { - if (!province.i || province.removed || !isProvinceLocked(province)) return; - - const newId = provinces.length; - for (const i of cells.i) { - if (cells.province[i] === province.i) provinceIds[i] = newId; - } - - province.i = newId; - provinces.push(province); - }); - } - - const provincesRatio = +byId("provincesRatio").value; - const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth - - // generate provinces for selected burgs - states.forEach(s => { - s.provinces = []; - if (!s.i || s.removed) return; - if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids - if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state - - const stateBurgs = burgs - .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell]) - .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population) - .sort((a, b) => b.capital - a.capital); - if (stateBurgs.length < 2) return; // at least 2 provinces are required - - const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2); - const form = Object.assign({}, forms[s.form]); - - for (let i = 0; i < provincesNumber; i++) { - const provinceId = provinces.length; - const center = stateBurgs[i].cell; - const burg = stateBurgs[i]; - const c = stateBurgs[i].culture; - const nameByBurg = P(0.5); - const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c); - const formName = rw(form); - form[formName] += 10; - const fullName = name + " " + formName; - const color = getMixedColor(s.color); - const kinship = nameByBurg ? 0.8 : 0.4; - const type = Burgs.getType(center, burg.port); - const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); - coa.shield = COA.getShield(c, s.i); - - s.provinces.push(provinceId); - provinces.push({i: provinceId, state: s.i, center, burg: burg.i, name, formName, fullName, color, coa}); - } - }); - - // expand generated provinces - const queue = new FlatQueue(); - const cost = []; - - provinces.forEach(p => { - if (!p.i || p.removed || isProvinceLocked(p)) return; - provinceIds[p.center] = p.i; - 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.pop(); - - cells.c[e].forEach(e => { - if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces - - const land = cells.h[e] >= 20; - if (!land && !cells.t[e]) return; // cannot pass deep ocean - if (land && cells.state[e] !== state) return; - const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100; - const totalCost = p + evevation; - - if (totalCost > max) return; - if (!cost[e] || totalCost < cost[e]) { - if (land) provinceIds[e] = province; // assign province to a cell - cost[e] = totalCost; - queue.push({e, province, state, p: totalCost}, totalCost); - } - }); - } - - // justify provinces shapes a bit - for (const i of cells.i) { - if (cells.burg[i]) continue; // do not overwrite burgs - if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces - - const neibs = cells.c[i] - .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c)) - .map(c => provinceIds[c]); - const adversaries = neibs.filter(c => c !== provinceIds[i]); - if (adversaries.length < 2) continue; - - const buddies = neibs.filter(c => c === provinceIds[i]).length; - if (buddies.length > 2) continue; - - const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0)); - const max = d3.max(competitors); - if (buddies >= max) continue; - - provinceIds[i] = adversaries[competitors.indexOf(max)]; - } - - // add "wild" provinces if some cells don't have a province assigned - const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned - states.forEach(s => { - if (!s.i || s.removed) return; - if (s.lock && !regenerateLockedStates) return; - if (!s.provinces.length) return; - - const coreProvinceNames = s.provinces.map(p => provinces[p]?.name); - const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)); - const getColonyName = () => { - if (colonyNamePool.length < 1) return null; - - const index = rand(colonyNamePool.length - 1); - const spliced = colonyNamePool.splice(index, 1); - return spliced[0] ? `New ${spliced[0]}` : null; - }; - - let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); - while (stateNoProvince.length) { - // add new province - const provinceId = provinces.length; - const burgCell = stateNoProvince.find(i => cells.burg[i]); - const center = burgCell ? burgCell : stateNoProvince[0]; - const burg = burgCell ? cells.burg[burgCell] : 0; - provinceIds[center] = provinceId; - - // expand province - const cost = []; - cost[center] = 1; - queue.push({e: center, p: 0}, 0); - while (queue.length) { - const {e, p} = queue.pop(); - - cells.c[e].forEach(nextCellId => { - if (provinceIds[nextCellId]) return; - const land = cells.h[nextCellId] >= 20; - if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return; - const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30; - const totalCost = p + ter; - - if (totalCost > max) return; - 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.push({e: nextCellId, p: totalCost}, totalCost); - } - }); - } - - // generate "wild" province name - const c = cells.culture[center]; - const f = pack.features[cells.f[center]]; - const color = getMixedColor(s.color); - - const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId); - const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); - const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle"); - const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center); - - const name = (() => { - const colonyName = colony && P(0.8) && getColonyName(); - if (colonyName) return colonyName; - if (burgCell && P(0.5)) return burgs[burg].name; - return Names.getState(Names.getCultureShort(c), c); - })(); - - const formName = (() => { - if (singleIsle) return "Island"; - if (isleGroup) return "Islands"; - if (colony) return "Colony"; - return rw(forms["Wild"]); - })(); - - const fullName = name + " " + formName; - - const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3); - const kinship = dominion ? 0 : 0.4; - const type = Burgs.getType(center, burgs[burg]?.port); - const coa = COA.generate(s.coa, kinship, dominion, type); - coa.shield = COA.getShield(c, s.i); - - provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa}); - s.provinces.push(provinceId); - - // 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 passableQueue = [from], - used = new Uint8Array(cells.i.length), - state = cells.state[from]; - 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; - passableQueue.push(c); - used[c] = 1; - }); - } - return false; // way is not found - } - - // re-check - stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); - } - }); - - cells.province = provinceIds; - pack.provinces = provinces; - - TIME && console.timeEnd("generateProvinces"); - }; - - // calculate pole of inaccessibility for each province - const getPoles = () => { - const getType = cellId => pack.cells.province[cellId]; - const poles = getPolesOfInaccessibility(pack, getType); - - pack.provinces.forEach(province => { - if (!province.i || province.removed) return; - province.pole = poles[province.i] || [0, 0]; - }); - }; - - return {generate, getPoles}; -})(); diff --git a/modules/religions-generator.js b/modules/religions-generator.js deleted file mode 100644 index 527a187c..00000000 --- a/modules/religions-generator.js +++ /dev/null @@ -1,921 +0,0 @@ -"use strict"; - -window.Religions = (function () { - // name generation approach and relative chance to be selected - const approach = { - Number: 1, - Being: 3, - Adjective: 5, - "Color + Animal": 5, - "Adjective + Animal": 5, - "Adjective + Being": 5, - "Adjective + Genitive": 1, - "Color + Being": 3, - "Color + Genitive": 3, - "Being + of + Genitive": 2, - "Being + of the + Genitive": 1, - "Animal + of + Genitive": 1, - "Adjective + Being + of + Genitive": 2, - "Adjective + Animal + of + Genitive": 2 - }; - - // turn weighted array into simple array - const approaches = []; - for (const a in approach) { - for (let j = 0; j < approach[a]; j++) { - approaches.push(a); - } - } - - const base = { - number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"], - being: [ - "Ancestor", - "Ancient", - "Avatar", - "Brother", - "Champion", - "Chief", - "Council", - "Creator", - "Deity", - "Divine One", - "Elder", - "Enlightened Being", - "Father", - "Forebear", - "Forefather", - "Giver", - "God", - "Goddess", - "Guardian", - "Guide", - "Hierach", - "Lady", - "Lord", - "Maker", - "Master", - "Mother", - "Numen", - "Oracle", - "Overlord", - "Protector", - "Reaper", - "Ruler", - "Sage", - "Seer", - "Sister", - "Spirit", - "Supreme Being", - "Transcendent", - "Virgin" - ], - animal: [ - "Antelope", - "Ape", - "Badger", - "Basilisk", - "Bear", - "Beaver", - "Bison", - "Boar", - "Buffalo", - "Camel", - "Cat", - "Centaur", - "Cerberus", - "Chimera", - "Cobra", - "Cockatrice", - "Crane", - "Crocodile", - "Crow", - "Cyclope", - "Deer", - "Dog", - "Direwolf", - "Drake", - "Dragon", - "Eagle", - "Elephant", - "Elk", - "Falcon", - "Fox", - "Goat", - "Goose", - "Gorgon", - "Gryphon", - "Hare", - "Hawk", - "Heron", - "Hippogriff", - "Horse", - "Hound", - "Hyena", - "Ibis", - "Jackal", - "Jaguar", - "Kitsune", - "Kraken", - "Lark", - "Leopard", - "Lion", - "Manticore", - "Mantis", - "Marten", - "Minotaur", - "Moose", - "Mule", - "Narwhal", - "Owl", - "Ox", - "Panther", - "Pegasus", - "Phoenix", - "Python", - "Rat", - "Raven", - "Roc", - "Rook", - "Scorpion", - "Serpent", - "Shark", - "Sheep", - "Snake", - "Sphinx", - "Spider", - "Swan", - "Tiger", - "Turtle", - "Unicorn", - "Viper", - "Vulture", - "Walrus", - "Wolf", - "Wolverine", - "Worm", - "Wyvern", - "Yeti" - ], - adjective: [ - "Aggressive", - "Almighty", - "Ancient", - "Beautiful", - "Benevolent", - "Big", - "Blind", - "Blond", - "Bloody", - "Brave", - "Broken", - "Brutal", - "Burning", - "Calm", - "Celestial", - "Cheerful", - "Crazy", - "Cruel", - "Dead", - "Deadly", - "Devastating", - "Distant", - "Disturbing", - "Divine", - "Dying", - "Eternal", - "Ethernal", - "Empyreal", - "Enigmatic", - "Enlightened", - "Evil", - "Explicit", - "Fair", - "Far", - "Fat", - "Fatal", - "Favorable", - "Flying", - "Friendly", - "Frozen", - "Giant", - "Good", - "Grateful", - "Great", - "Happy", - "High", - "Holy", - "Honest", - "Huge", - "Hungry", - "Illustrious", - "Immutable", - "Ineffable", - "Infallible", - "Inherent", - "Last", - "Latter", - "Lost", - "Loud", - "Lucky", - "Mad", - "Magical", - "Main", - "Major", - "Marine", - "Mythical", - "Mystical", - "Naval", - "New", - "Noble", - "Old", - "Otherworldly", - "Patient", - "Peaceful", - "Pregnant", - "Prime", - "Proud", - "Pure", - "Radiant", - "Resplendent", - "Sacred", - "Sacrosanct", - "Sad", - "Scary", - "Secret", - "Selected", - "Serene", - "Severe", - "Silent", - "Sleeping", - "Slumbering", - "Sovereign", - "Strong", - "Sunny", - "Superior", - "Supernatural", - "Sustainable", - "Transcendent", - "Transcendental", - "Troubled", - "Unearthly", - "Unfathomable", - "Unhappy", - "Unknown", - "Unseen", - "Waking", - "Wild", - "Wise", - "Worried", - "Young" - ], - genitive: [ - "Cold", - "Day", - "Death", - "Doom", - "Fate", - "Fire", - "Fog", - "Frost", - "Gates", - "Heaven", - "Home", - "Ice", - "Justice", - "Life", - "Light", - "Lightning", - "Love", - "Nature", - "Night", - "Pain", - "Snow", - "Springs", - "Summer", - "Thunder", - "Time", - "Victory", - "War", - "Winter" - ], - theGenitive: [ - "Abyss", - "Blood", - "Dawn", - "Earth", - "East", - "Eclipse", - "Fall", - "Harvest", - "Moon", - "North", - "Peak", - "Rainbow", - "Sea", - "Sky", - "South", - "Stars", - "Storm", - "Sun", - "Tree", - "Underworld", - "West", - "Wild", - "Word", - "World" - ], - color: [ - "Amber", - "Black", - "Blue", - "Bright", - "Bronze", - "Brown", - "Coral", - "Crimson", - "Dark", - "Emerald", - "Golden", - "Green", - "Grey", - "Indigo", - "Lavender", - "Light", - "Magenta", - "Maroon", - "Orange", - "Pink", - "Plum", - "Purple", - "Red", - "Ruby", - "Sapphire", - "Teal", - "Turquoise", - "White", - "Yellow" - ] - }; - - const forms = { - Folk: { - Shamanism: 4, - Animism: 4, - Polytheism: 4, - "Ancestor Worship": 2, - "Nature Worship": 1, - Totemism: 1 - }, - Organized: { - Polytheism: 7, - Monotheism: 7, - Dualism: 3, - Pantheism: 2, - "Non-theism": 2 - }, - Cult: { - Cult: 5, - "Dark Cult": 5, - Sect: 1 - }, - Heresy: { - Heresy: 1 - } - }; - - const namingMethods = { - Folk: { - "Culture + type": 1 - }, - - Organized: { - "Random + type": 3, - "Random + ism": 1, - "Supreme + ism": 5, - "Faith of + Supreme": 5, - "Place + ism": 1, - "Culture + ism": 2, - "Place + ian + type": 6, - "Culture + type": 4 - }, - - Cult: { - "Burg + ian + type": 2, - "Random + ian + type": 1, - "Type + of the + meaning": 2 - }, - - Heresy: { - "Burg + ian + type": 3, - "Random + ism": 3, - "Random + ian + type": 2, - "Type + of the + meaning": 1 - } - }; - - const types = { - Shamanism: {Beliefs: 3, Shamanism: 2, Druidism: 1, Spirits: 1}, - Animism: {Spirits: 3, Beliefs: 1}, - Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1}, - "Ancestor Worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2}, - "Nature Worship": {Beliefs: 3, Druids: 1}, - Totemism: {Beliefs: 2, Totems: 2, Idols: 1}, - - Monotheism: {Religion: 2, Church: 3, Faith: 1}, - Dualism: {Religion: 3, Faith: 1, Cult: 1}, - Pantheism: {Religion: 1, Faith: 1}, - "Non-theism": {Beliefs: 3, Spirits: 1}, - - Cult: {Cult: 4, Sect: 2, Arcanum: 1, Order: 1, Worship: 1}, - "Dark Cult": {Cult: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1}, - Sect: {Sect: 3, Society: 1}, - - Heresy: { - Heresy: 3, - Sect: 2, - Apostates: 1, - Brotherhood: 1, - Circle: 1, - Dissent: 1, - Dissenters: 1, - Iconoclasm: 1, - Schism: 1, - Society: 1 - } - }; - - const expansionismMap = { - Folk: () => 0, - Organized: () => gauss(5, 3, 0, 10, 1), - Cult: () => gauss(0.5, 0.5, 0, 5, 1), - Heresy: () => gauss(1, 0.5, 0, 5, 1) - }; - - function generate() { - TIME && console.time("generateReligions"); - const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || []; - - const folkReligions = generateFolkReligions(); - const organizedReligions = generateOrganizedReligions(+religionsNumber.value, lockedReligions); - - const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]); - const indexedReligions = combineReligions(namedReligions, lockedReligions); - const religionIds = expandReligions(indexedReligions); - const religions = defineOrigins(religionIds, indexedReligions); - - pack.religions = religions; - pack.cells.religion = religionIds; - - checkCenters(); - - TIME && console.timeEnd("generateReligions"); - } - - function generateFolkReligions() { - return pack.cultures - .filter(c => c.i && !c.removed) - .map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center})); - } - - function generateOrganizedReligions(desiredReligionNumber, lockedReligions) { - const cells = pack.cells; - const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0; - const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount; - if (requiredReligionsNumber < 1) return []; - - const candidateCells = getCandidateCells(); - const religionCores = placeReligions(); - - const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40% - const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30% - const organizedCount = religionCores.length - cultsCount - heresiesCount; - - const getType = index => { - if (index < organizedCount) return "Organized"; - if (index < organizedCount + cultsCount) return "Cult"; - return "Heresy"; - }; - - return religionCores.map((cellId, index) => { - const type = getType(index); - const form = rw(forms[type]); - const cultureId = cells.culture[cellId]; - - return {type, form, culture: cultureId, center: cellId}; - }); - - function placeReligions() { - const religionCells = []; - const religionsTree = d3.quadtree(); - - // pre-populate with locked centers - lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center])); - - // min distance between religion inceptions - const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber; - - for (const cellId of candidateCells) { - const [x, y] = cells.p[cellId]; - - if (religionsTree.find(x, y, spacing) === undefined) { - religionCells.push(cellId); - religionsTree.add([x, y]); - - if (religionCells.length === requiredReligionsNumber) return religionCells; - } - } - - WARN && console.warn(`Placed only ${religionCells.length} of ${requiredReligionsNumber} religions`); - return religionCells; - } - - function getCandidateCells() { - const validBurgs = pack.burgs.filter(b => b.i && !b.removed); - - if (validBurgs.length >= requiredReligionsNumber) - return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell); - return cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); - } - } - - function specifyReligions(newReligions) { - const {cells, cultures} = pack; - - const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => { - const supreme = getDeityName(cultureId); - const deity = form === "Non-theism" || form === "Animism" ? null : supreme; - - const stateId = cells.state[center]; - - let [name, expansion] = generateReligionName(type, form, supreme, center); - if (expansion === "state" && !stateId) expansion = "global"; - - const expansionism = expansionismMap[type](); - const color = getReligionColor(cultures[cultureId], type); - - return {name, type, form, culture: cultureId, center, deity, expansion, expansionism, color}; - }); - - return rawReligions; - - function getReligionColor(culture, type) { - if (!culture.i) return getRandomColor(); - - if (type === "Folk") return culture.color; - if (type === "Heresy") return getMixedColor(culture.color, 0.35, 0.2); - if (type === "Cult") return getMixedColor(culture.color, 0.5, 0); - return getMixedColor(culture.color, 0.25, 0.4); - } - } - - // indexes, conditionally renames, and abbreviates religions - function combineReligions(namedReligions, lockedReligions) { - const indexedReligions = [{name: "No religion", i: 0}]; - - const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions(); - const maxIndex = Math.max( - highestLockedIndex, - namedReligions.length + lockedReligions.length + 1 - numberLockedFolk - ); - - for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) { - // place locked religion back at its old index - if (index === lockedReligionQueue[0]?.i) { - const nextReligion = lockedReligionQueue.shift(); - indexedReligions.push(nextReligion); - continue; - } - - // slot the new religions - if (progress < namedReligions.length) { - const nextReligion = namedReligions[progress]; - progress++; - - if ( - nextReligion.type === "Folk" && - lockedReligions.some(({type, culture}) => type === "Folk" && culture === nextReligion.culture) - ) - continue; // when there is a locked Folk religion for this culture discard duplicate - - const newName = renameOld(nextReligion); - const code = abbreviate(newName, codes); - codes.push(code); - indexedReligions.push({...nextReligion, i: index, name: newName, code}); - continue; - } - - indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Removed religion", removed: true}); - } - return indexedReligions; - - function parseLockedReligions() { - // copy and sort the locked religions list - const lockedReligionQueue = lockedReligions - .map(religion => { - // and filter their origins to locked religions - let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n)); - if (newOrigin === []) newOrigin = [0]; - return {...religion, origins: newOrigin}; - }) - .sort((a, b) => a.i - b.i); - - const highestLockedIndex = Math.max(...lockedReligions.map(r => r.i)); - const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : []; - const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length; - - return {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk}; - } - - // prepend 'Old' to names of folk religions which have organized competitors - function renameOld({name, type, culture: cultureId}) { - if (type !== "Folk") return name; - - const haveOrganized = - namedReligions.some( - ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture" - ) || - lockedReligions.some( - ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture" - ); - if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`; - return name; - } - } - - // finally generate and stores origins trees - function defineOrigins(religionIds, indexedReligions) { - const religionOriginsParamsMap = { - Organized: {clusterSize: 100, maxReligions: 2}, - Cult: {clusterSize: 50, maxReligions: 3}, - Heresy: {clusterSize: 50, maxReligions: 4} - }; - - const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => { - if (i === 0) return null; // no religion - if (type === "Folk") return [0]; // folk religions originate from its parent culture only - - const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId); - const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center); - if (isFolkBased) return [folkReligion.i]; - - const {clusterSize, maxReligions} = religionOriginsParamsMap[type]; - const fallbackOrigin = folkReligion?.i || 0; - return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin); - }); - - return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]})); - } - - function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) { - const foundReligions = new Set(); - const queue = [center]; - const checked = {}; - - for (let size = 0; queue.length && size < clusterSize; size++) { - const cellId = queue.shift(); - checked[cellId] = true; - - for (const neibId of neighbors[cellId]) { - if (checked[neibId]) continue; - checked[neibId] = true; - - const neibReligion = religionIds[neibId]; - if (neibReligion && neibReligion < religionId) foundReligions.add(neibReligion); - if (foundReligions.size >= maxReligions) return [...foundReligions]; - queue.push(neibId); - } - } - - return foundReligions.size ? [...foundReligions] : [fallbackOrigin]; - } - - // growth algorithm to assign cells to religions - function expandReligions(religions) { - const {cells, routes} = pack; - const religionIds = spreadFolkReligions(religions); - - const queue = new FlatQueue(); - const cost = []; - - // limit cost for organized religions growth - const maxExpansionCost = (cells.i.length / 20) * byId("growthRate").valueAsNumber; - - religions - .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) - .forEach(r => { - religionIds[r.center] = r.i; - 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.pop(); - const {culture, expansion, expansionism} = religionsMap.get(r); - - cells.c[cellId].forEach(nextCell => { - if (expansion === "culture" && culture !== cells.culture[nextCell]) return; - if (expansion === "state" && state !== cells.state[nextCell]) return; - if (religionsMap.get(religionIds[nextCell])?.lock) return; - - const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0; - const stateCost = state !== cells.state[nextCell] ? 10 : 0; - const passageCost = getPassageCost(cellId, nextCell); - - const cellCost = cultureCost + stateCost + passageCost; - const totalCost = p + 10 + cellCost / expansionism; - if (totalCost > maxExpansionCost) return; - - if (!cost[nextCell] || totalCost < cost[nextCell]) { - if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell - cost[nextCell] = totalCost; - - queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost); - } - }); - } - - return religionIds; - - 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; - } - } - - // folk religions initially get all cells of their culture, and locked religions are retained - function spreadFolkReligions(religions) { - const cells = pack.cells; - const hasPrior = cells.religion && true; - const religionIds = new Uint16Array(cells.i.length); - - const folkReligions = religions.filter(religion => religion.type === "Folk" && !religion.removed); - const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i])); - - for (const cellId of cells.i) { - const oldId = (hasPrior && cells.religion[cellId]) || 0; - if (oldId && religions[oldId]?.lock && !religions[oldId]?.removed) { - religionIds[cellId] = oldId; - continue; - } - const cultureId = cells.culture[cellId]; - religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0; - } - - return religionIds; - } - - function checkCenters() { - const cells = pack.cells; - pack.religions.forEach(r => { - if (!r.i) return; - // move religion center if it's not within religion area after expansion - if (cells.religion[r.center] === r.i) return; // in area - const firstCell = cells.i.find(i => cells.religion[i] === r.i); - const cultureHome = pack.cultures[r.culture]?.center; - if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion - else if (r.type === "Folk" && cultureHome) r.center = cultureHome; // reset extinct culture centers - }); - } - - function recalculate() { - const newReligionIds = expandReligions(pack.religions); - pack.cells.religion = newReligionIds; - - checkCenters(); - } - - const add = function (center) { - const {cells, cultures, religions} = pack; - const religionId = cells.religion[center]; - const i = religions.length; - - const cultureId = cells.culture[center]; - const missingFolk = - cultureId !== 0 && - !religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed); - const color = missingFolk ? cultures[cultureId].color : getMixedColor(religions[religionId].color, 0.3, 0); - - const type = missingFolk - ? "Folk" - : religions[religionId].type === "Organized" - ? rw({Organized: 4, Cult: 1, Heresy: 2}) - : rw({Organized: 5, Cult: 2}); - const form = rw(forms[type]); - const deity = - type === "Heresy" - ? religions[religionId].deity - : form === "Non-theism" || form === "Animism" - ? null - : getDeityName(cultureId); - - const [name, expansion] = generateReligionName(type, form, deity, center); - - const formName = type === "Heresy" ? religions[religionId].form : form; - const code = abbreviate( - name, - religions.map(r => r.code) - ); - const influences = getReligionsInRadius(cells.c, center, cells.religion, i, 25, 3, 0); - const origins = type === "Folk" ? [0] : influences; - - religions.push({ - i, - name, - color, - culture: cultureId, - type, - form: formName, - deity, - expansion, - expansionism: expansionismMap[type](), - center, - cells: 0, - area: 0, - rural: 0, - urban: 0, - origins, - code - }); - cells.religion[center] = i; - }; - - // get supreme deity name - const getDeityName = function (culture) { - if (culture === undefined) { - ERROR && console.error("Please define a culture"); - return; - } - const meaning = generateMeaning(); - const cultureName = Names.getCulture(culture, null, null, "", 0.8); - return cultureName + ", The " + meaning; - }; - - function generateMeaning() { - const a = ra(approaches); // select generation approach - if (a === "Number") return ra(base.number); - if (a === "Being") return ra(base.being); - if (a === "Adjective") return ra(base.adjective); - if (a === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`; - if (a === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`; - if (a === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`; - if (a === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`; - if (a === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`; - if (a === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`; - if (a === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`; - if (a === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`; - if (a === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`; - if (a === "Adjective + Being + of + Genitive") - return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`; - if (a === "Adjective + Animal + of + Genitive") - return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`; - - ERROR && console.error("Unkown generation approach"); - } - - function generateReligionName(variety, form, deity, center) { - const {cells, cultures, burgs, states} = pack; - - const random = () => Names.getCulture(cells.culture[center], null, null, "", 0); - const type = rw(types[form]); - const supreme = deity.split(/[ ,]+/)[0]; - const culture = cultures[cells.culture[center]].name; - - const place = adj => { - const burgId = cells.burg[center]; - const stateId = cells.state[center]; - - const base = burgId ? burgs[burgId].name : states[stateId].name; - let name = trimVowels(base.split(/[ ,]+/)[0]); - return adj ? getAdjective(name) : name; - }; - - const m = rw(namingMethods[variety]); - if (m === "Random + type") return [random() + " " + type, "global"]; - if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"]; - if (m === "Supreme + ism" && deity) return [trimVowels(supreme) + "ism", "global"]; - if (m === "Faith of + Supreme" && deity) - return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme, "global"]; - if (m === "Place + ism") return [place() + "ism", "state"]; - if (m === "Culture + ism") return [trimVowels(culture) + "ism", "culture"]; - if (m === "Place + ian + type") return [place("adj") + " " + type, "state"]; - if (m === "Culture + type") return [culture + " " + type, "culture"]; - if (m === "Burg + ian + type") return [`${place("adj")} ${type}`, "global"]; - if (m === "Random + ian + type") return [`${getAdjective(random())} ${type}`, "global"]; - if (m === "Type + of the + meaning") return [`${type} of the ${generateMeaning()}`, "global"]; - return [trimVowels(random()) + "ism", "global"]; // else - } - - return {generate, add, getDeityName, recalculate}; -})(); diff --git a/modules/renderers/draw-borders.js b/modules/renderers/draw-borders.js deleted file mode 100644 index f0f3006e..00000000 --- a/modules/renderers/draw-borders.js +++ /dev/null @@ -1,120 +0,0 @@ -"use strict"; - -function drawBorders() { - TIME && console.time("drawBorders"); - const {cells, vertices} = pack; - - const statePath = []; - const provincePath = []; - const checked = {}; - - const isLand = cellId => cells.h[cellId] >= 20; - - for (let cellId = 0; cellId < cells.i.length; cellId++) { - if (!cells.state[cellId]) continue; - const provinceId = cells.province[cellId]; - const stateId = cells.state[cellId]; - - // bordering cell of another province - if (provinceId) { - const provToCell = cells.c[cellId].find(neibId => { - const neibProvinceId = cells.province[neibId]; - return ( - neibProvinceId && - provinceId > neibProvinceId && - !checked[`prov-${provinceId}-${neibProvinceId}-${cellId}`] && - cells.state[neibId] === stateId - ); - }); - - if (provToCell !== undefined) { - const addToChecked = cellId => (checked[`prov-${provinceId}-${cells.province[provToCell]}-${cellId}`] = true); - const border = getBorder({type: "province", fromCell: cellId, toCell: provToCell, addToChecked}); - - if (border) { - provincePath.push(border); - cellId--; // check the same cell again - continue; - } - } - } - - // if cell is on state border - const stateToCell = cells.c[cellId].find(neibId => { - const neibStateId = cells.state[neibId]; - return isLand(neibId) && stateId > neibStateId && !checked[`state-${stateId}-${neibStateId}-${cellId}`]; - }); - - if (stateToCell !== undefined) { - const addToChecked = cellId => (checked[`state-${stateId}-${cells.state[stateToCell]}-${cellId}`] = true); - const border = getBorder({type: "state", fromCell: cellId, toCell: stateToCell, addToChecked}); - - if (border) { - statePath.push(border); - cellId--; // check the same cell again - continue; - } - } - } - - svg.select("#borders").selectAll("path").remove(); - svg.select("#stateBorders").append("path").attr("d", statePath.join(" ")); - svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" ")); - - function getBorder({type, fromCell, toCell, addToChecked}) { - const getType = cellId => cells[type][cellId]; - const isTypeFrom = cellId => cellId < cells.i.length && getType(cellId) === getType(fromCell); - const isTypeTo = cellId => cellId < cells.i.length && getType(cellId) === getType(toCell); - - addToChecked(fromCell); - const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && isTypeTo(i))); - if (startingVertex === undefined) return null; - - const checkVertex = vertex => - vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && isTypeTo(c)); - const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked}); - if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" "); - - return null; - } - - // connect vertices to chain to form a border - function getVerticesLine({vertices, startingVertex, checkCell, checkVertex, addToChecked}) { - let chain = []; // vertices chain to form a path - let next = startingVertex; - const MAX_ITERATIONS = vertices.c.length; - - for (let run = 0; run < 2; run++) { - // first run: from any vertex to a border edge - // second run: from found border edge to another edge - chain = []; - - for (let i = 0; i < MAX_ITERATIONS; i++) { - const previous = chain.at(-1); - const current = next; - chain.push(current); - - const neibCells = vertices.c[current]; - neibCells.map(addToChecked); - - const [c1, c2, c3] = neibCells.map(checkCell); - const [v1, v2, v3] = vertices.v[current].map(checkVertex); - const [vertex1, vertex2, vertex3] = vertices.v[current]; - - if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1; - else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2; - else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3; - - if (next === current || next === startingVertex) { - if (next === startingVertex) chain.push(startingVertex); - startingVertex = next; - break; - } - } - } - - return chain; - } - - TIME && console.timeEnd("drawBorders"); -} diff --git a/modules/renderers/draw-burg-icons.js b/modules/renderers/draw-burg-icons.js deleted file mode 100644 index 5952ec81..00000000 --- a/modules/renderers/draw-burg-icons.js +++ /dev/null @@ -1,106 +0,0 @@ -"use strict"; - -function drawBurgIcons() { - TIME && console.time("drawBurgIcons"); - createIconGroups(); - - for (const {name} of options.burgs.groups) { - const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed); - if (!burgsInGroup.length) continue; - - const iconsGroup = document.querySelector("#burgIcons > g#" + name); - if (!iconsGroup) continue; - - const icon = iconsGroup.dataset.icon || "#icon-circle"; - iconsGroup.innerHTML = burgsInGroup - .map(b => ``) - .join(""); - - const portsInGroup = burgsInGroup.filter(b => b.port); - if (!portsInGroup.length) continue; - - const portGroup = document.querySelector("#anchors > g#" + name); - if (!portGroup) continue; - - portGroup.innerHTML = portsInGroup - .map(b => ``) - .join(""); - } - - TIME && console.timeEnd("drawBurgIcons"); -} - -function drawBurgIcon(burg) { - removeBurgIcon(burg.i); - - const iconGroup = burgIcons.select("#" + burg.group); - if (iconGroup.empty()) return; - - const icon = iconGroup.attr("data-icon") || "#icon-circle"; - burgIcons - .select("#" + burg.group) - .append("use") - .attr("href", icon) - .attr("id", "burg" + burg.i) - .attr("data-id", burg.i) - .attr("x", burg.x) - .attr("y", burg.y); - - if (burg.port) { - anchors - .select("#" + burg.group) - .append("use") - .attr("href", "#icon-anchor") - .attr("id", "anchor" + burg.i) - .attr("data-id", burg.i) - .attr("x", burg.x) - .attr("y", burg.y); - } -} - -function removeBurgIcon(burgId) { - const existingIcon = document.getElementById("burg" + burgId); - if (existingIcon) existingIcon.remove(); - - const existingAnchor = document.getElementById("anchor" + burgId); - if (existingAnchor) existingAnchor.remove(); -} - -function createIconGroups() { - // save existing styles and remove all groups - document.querySelectorAll("g#burgIcons > g").forEach(group => { - style.burgIcons[group.id] = Array.from(group.attributes).reduce((acc, attribute) => { - acc[attribute.name] = attribute.value; - return acc; - }, {}); - group.remove(); - }); - - document.querySelectorAll("g#anchors > g").forEach(group => { - style.anchors[group.id] = Array.from(group.attributes).reduce((acc, attribute) => { - acc[attribute.name] = attribute.value; - return acc; - }, {}); - group.remove(); - }); - - // create groups for each burg group and apply stored or default style - const defaultIconStyle = style.burgIcons.town || Object.values(style.burgIcons)[0] || {}; - const defaultAnchorStyle = style.anchors.town || Object.values(style.anchors)[0] || {}; - const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order); - for (const {name} of sortedGroups) { - const burgGroup = burgIcons.append("g"); - const iconStyles = style.burgIcons[name] || defaultIconStyle; - Object.entries(iconStyles).forEach(([key, value]) => { - burgGroup.attr(key, value); - }); - burgGroup.attr("id", name); - - const anchorGroup = anchors.append("g"); - const anchorStyles = style.anchors[name] || defaultAnchorStyle; - Object.entries(anchorStyles).forEach(([key, value]) => { - anchorGroup.attr(key, value); - }); - anchorGroup.attr("id", name); - } -} diff --git a/modules/renderers/draw-burg-labels.js b/modules/renderers/draw-burg-labels.js deleted file mode 100644 index 721025a2..00000000 --- a/modules/renderers/draw-burg-labels.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; - -function drawBurgLabels() { - TIME && console.time("drawBurgLabels"); - createLabelGroups(); - - for (const {name} of options.burgs.groups) { - const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed); - if (!burgsInGroup.length) continue; - - const labelGroup = burgLabels.select("#" + name); - if (labelGroup.empty()) continue; - - const dx = labelGroup.attr("data-dx") || 0; - const dy = labelGroup.attr("data-dy") || 0; - - labelGroup - .selectAll("text") - .data(burgsInGroup) - .enter() - .append("text") - .attr("text-rendering", "optimizeSpeed") - .attr("id", d => "burgLabel" + d.i) - .attr("data-id", d => d.i) - .attr("x", d => d.x) - .attr("y", d => d.y) - .attr("dx", dx + "em") - .attr("dy", dy + "em") - .text(d => d.name); - } - - TIME && console.timeEnd("drawBurgLabels"); -} - -function drawBurgLabel(burg) { - removeBurgLabel(burg.i); - - const labelGroup = burgLabels.select("#" + burg.group); - if (labelGroup.empty()) return; - const dx = labelGroup.attr("data-dx") || 0; - const dy = labelGroup.attr("data-dy") || 0; - - labelGroup - .append("text") - .attr("text-rendering", "optimizeSpeed") - .attr("id", "burgLabel" + burg.i) - .attr("data-id", burg.i) - .attr("x", burg.x) - .attr("y", burg.y) - .attr("dx", dx + "em") - .attr("dy", dy + "em") - .text(burg.name); -} - -function removeBurgLabel(burgId) { - const existingLabel = document.getElementById("burgLabel" + burgId); - if (existingLabel) existingLabel.remove(); -} - -function createLabelGroups() { - // save existing styles and remove all groups - document.querySelectorAll("g#burgLabels > g").forEach(group => { - style.burgLabels[group.id] = Array.from(group.attributes).reduce((acc, attribute) => { - acc[attribute.name] = attribute.value; - return acc; - }, {}); - group.remove(); - }); - - // create groups for each burg group and apply stored or default style - const defaultStyle = style.burgLabels.town || Object.values(style.burgLabels)[0] || {}; - const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order); - for (const {name} of sortedGroups) { - const group = burgLabels.append("g"); - const styles = style.burgLabels[name] || defaultStyle; - Object.entries(styles).forEach(([key, value]) => { - group.attr(key, value); - }); - group.attr("id", name); - } -} diff --git a/modules/renderers/draw-emblems.js b/modules/renderers/draw-emblems.js deleted file mode 100644 index 13781239..00000000 --- a/modules/renderers/draw-emblems.js +++ /dev/null @@ -1,129 +0,0 @@ -"use strict"; - -function drawEmblems() { - TIME && console.time("drawEmblems"); - const {states, provinces, burgs} = pack; - - const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0); - const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0); - const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0); - - const getStateEmblemsSize = () => { - const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100); - const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier - const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1; - return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states - }; - - const getProvinceEmblemsSize = () => { - const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70); - const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier - const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1; - return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces - }; - - const getBurgEmblemSize = () => { - const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50); - const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier - const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1; - return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs - }; - - const sizeBurgs = getBurgEmblemSize(); - const burgCOAs = validBurgs.map(burg => { - const {x, y} = burg; - const size = burg.coa.size || 1; - const shift = (sizeBurgs * size) / 2; - return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift}; - }); - - const sizeProvinces = getProvinceEmblemsSize(); - const provinceCOAs = validProvinces.map(province => { - const [x, y] = province.pole || pack.cells.p[province.center]; - const size = province.coa.size || 1; - const shift = (sizeProvinces * size) / 2; - return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift}; - }); - - const sizeStates = getStateEmblemsSize(); - const stateCOAs = validStates.map(state => { - const [x, y] = state.pole || pack.cells.p[state.center]; - const size = state.coa.size || 1; - const shift = (sizeStates * size) / 2; - return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift}; - }); - - const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs); - const simulation = d3 - .forceSimulation(nodes) - .alphaMin(0.6) - .alphaDecay(0.2) - .velocityDecay(0.6) - .force( - "collision", - d3.forceCollide().radius(d => d.shift) - ) - .stop(); - - d3.timeout(function () { - const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); - for (let i = 0; i < n; ++i) { - simulation.tick(); - } - - const burgNodes = nodes.filter(node => node.type === "burg"); - const burgString = burgNodes - .map( - d => - `` - ) - .join(""); - emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString); - - const provinceNodes = nodes.filter(node => node.type === "province"); - const provinceString = provinceNodes - .map( - d => - `` - ) - .join(""); - emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString); - - const stateNodes = nodes.filter(node => node.type === "state"); - const stateString = stateNodes - .map( - d => - `` - ) - .join(""); - emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString); - - invokeActiveZooming(); - }); - - TIME && console.timeEnd("drawEmblems"); -} - -const getDataAndType = id => { - if (id === "burgEmblems") return [pack.burgs, "burg"]; - if (id === "provinceEmblems") return [pack.provinces, "province"]; - if (id === "stateEmblems") return [pack.states, "state"]; - throw new Error(`Unknown emblem type: ${id}`); -}; - -async function renderGroupCOAs(g) { - const [data, type] = getDataAndType(g.id); - - for (let use of g.children) { - const i = +use.dataset.i; - const id = type + "COA" + i; - COArenderer.trigger(id, data[i].coa); - use.setAttribute("href", "#" + id); - } -} diff --git a/modules/renderers/draw-features.js b/modules/renderers/draw-features.js deleted file mode 100644 index 0112a0ae..00000000 --- a/modules/renderers/draw-features.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; - -function drawFeatures() { - TIME && console.time("drawFeatures"); - - const html = { - paths: [], - landMask: [], - waterMask: [''], - coastline: {}, - lakes: {} - }; - - for (const feature of pack.features) { - if (!feature || feature.type === "ocean") continue; - - html.paths.push(``); - - if (feature.type === "lake") { - html.landMask.push(``); - - const lakeGroup = feature.group || "freshwater"; - if (!html.lakes[lakeGroup]) html.lakes[lakeGroup] = []; - html.lakes[lakeGroup].push(``); - } else { - html.landMask.push(``); - html.waterMask.push(``); - - const coastlineGroup = feature.group === "lake_island" ? "lake_island" : "sea_island"; - if (!html.coastline[coastlineGroup]) html.coastline[coastlineGroup] = []; - html.coastline[coastlineGroup].push(``); - } - } - - defs.select("#featurePaths").html(html.paths.join("")); - defs.select("#land").html(html.landMask.join("")); - defs.select("#water").html(html.waterMask.join("")); - - coastline.selectAll("g").each(function () { - const paths = html.coastline[this.id] || []; - d3.select(this).html(paths.join("")); - }); - - lakes.selectAll("g").each(function () { - const paths = html.lakes[this.id] || []; - d3.select(this).html(paths.join("")); - }); - - TIME && console.timeEnd("drawFeatures"); -} - -function getFeaturePath(feature) { - const points = feature.vertices.map(vertex => pack.vertices.p[vertex]); - if (points.some(point => point === undefined)) { - ERROR && console.error("Undefined point in getFeaturePath"); - return ""; - } - - const simplifiedPoints = simplify(points, 0.3); - const clippedPoints = clipPoly(simplifiedPoints, 1); - - const lineGen = d3.line().curve(d3.curveBasisClosed); - const path = round(lineGen(clippedPoints)) + "Z"; - - return path; -} diff --git a/modules/renderers/draw-markers.js b/modules/renderers/draw-markers.js deleted file mode 100644 index f7466a55..00000000 --- a/modules/renderers/draw-markers.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; - -function drawMarkers() { - TIME && console.time("drawMarkers"); - - const rescale = +markers.attr("rescale"); - const pinned = +markers.attr("pinned"); - - const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers; - const html = markersData.map(marker => drawMarker(marker, rescale)); - markers.html(html.join("")); - - TIME && console.timeEnd("drawMarkers"); -} - -// prettier-ignore -const pinShapes = { - bubble: (fill, stroke) => ``, - pin: (fill, stroke) => ``, - square: (fill, stroke) => ``, - squarish: (fill, stroke) => ``, - diamond: (fill, stroke) => ``, - hex: (fill, stroke) => ``, - hexy: (fill, stroke) => ``, - shieldy: (fill, stroke) => ``, - shield: (fill, stroke) => ``, - pentagon: (fill, stroke) => ``, - heptagon: (fill, stroke) => ``, - circle: (fill, stroke) => ``, - no: () => "" -}; - -const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => { - const shapeFunction = pinShapes[shape] || pinShapes.bubble; - return shapeFunction(fill, stroke); -}; - -function drawMarker(marker, rescale = 1) { - const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker; - const id = `marker${i}`; - const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size; - const viewX = rn(x - zoomSize / 2, 1); - const viewY = rn(y - zoomSize, 1); - - const isExternal = icon.startsWith("http") || icon.startsWith("data:image"); - - return /* html */ ` - - ${getPin(pin, fill, stroke)} - ${isExternal ? "" : icon} - - `; -} diff --git a/modules/renderers/draw-military.js b/modules/renderers/draw-military.js deleted file mode 100644 index a332130f..00000000 --- a/modules/renderers/draw-military.js +++ /dev/null @@ -1,155 +0,0 @@ -"use strict"; - -function drawMilitary() { - TIME && console.time("drawMilitary"); - - armies.selectAll("g").remove(); - pack.states.filter(s => s.i && !s.removed).forEach(s => drawRegiments(s.military, s.i)); - - TIME && console.timeEnd("drawMilitary"); -} - -const drawRegiments = function (regiments, s) { - const size = +armies.attr("box-size"); - const w = d => (d.n ? size * 4 : size * 6); - const h = size * 2; - const x = d => rn(d.x - w(d) / 2, 2); - const y = d => rn(d.y - size, 2); - - const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999"; - const darkerColor = d3.color(baseColor).darker().hex(); - const army = armies - .append("g") - .attr("id", "army" + s) - .attr("fill", baseColor) - .attr("color", darkerColor); - - const g = army - .selectAll("g") - .data(regiments) - .enter() - .append("g") - .attr("id", d => "regiment" + s + "-" + d.i) - .attr("data-name", d => d.name) - .attr("data-state", s) - .attr("data-id", d => d.i) - .attr("transform", d => (d.angle ? `rotate(${d.angle})` : null)) - .attr("transform-origin", d => `${d.x}px ${d.y}px`); - g.append("rect") - .attr("x", d => x(d)) - .attr("y", d => y(d)) - .attr("width", d => w(d)) - .attr("height", h); - g.append("text") - .attr("x", d => d.x) - .attr("y", d => d.y) - .attr("text-rendering", "optimizeSpeed") - .text(d => Military.getTotal(d)); - g.append("rect") - .attr("fill", "currentColor") - .attr("x", d => x(d) - h) - .attr("y", d => y(d)) - .attr("width", h) - .attr("height", h); - g.append("text") - .attr("class", "regimentIcon") - .attr("text-rendering", "optimizeSpeed") - .attr("x", d => x(d) - size) - .attr("y", d => d.y) - .text(d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? "" : d.icon)); - g.append("image") - .attr("class", "regimentImage") - .attr("x", d => x(d) - h) - .attr("y", d => y(d)) - .attr("height", h) - .attr("width", h) - .attr("href", d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? d.icon : "")); -}; - -const drawRegiment = function (reg, stateId) { - const size = +armies.attr("box-size"); - const w = reg.n ? size * 4 : size * 6; - const h = size * 2; - const x1 = rn(reg.x - w / 2, 2); - const y1 = rn(reg.y - size, 2); - - let army = armies.select("g#army" + stateId); - if (!army.size()) { - const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999"; - const darkerColor = d3.color(baseColor).darker().hex(); - army = armies - .append("g") - .attr("id", "army" + stateId) - .attr("fill", baseColor) - .attr("color", darkerColor); - } - - const g = army - .append("g") - .attr("id", "regiment" + stateId + "-" + reg.i) - .attr("data-name", reg.name) - .attr("data-state", stateId) - .attr("data-id", reg.i) - .attr("transform", `rotate(${reg.angle || 0})`) - .attr("transform-origin", `${reg.x}px ${reg.y}px`); - g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h); - g.append("text") - .attr("x", reg.x) - .attr("y", reg.y) - .attr("text-rendering", "optimizeSpeed") - .text(Military.getTotal(reg)); - g.append("rect") - .attr("fill", "currentColor") - .attr("x", x1 - h) - .attr("y", y1) - .attr("width", h) - .attr("height", h); - g.append("text") - .attr("class", "regimentIcon") - .attr("text-rendering", "optimizeSpeed") - .attr("x", x1 - size) - .attr("y", reg.y) - .text(reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? "" : reg.icon); - g.append("image") - .attr("class", "regimentImage") - .attr("x", x1 - h) - .attr("y", y1) - .attr("height", h) - .attr("width", h) - .attr("href", reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? reg.icon : ""); -}; - -// move one regiment to another -const moveRegiment = function (reg, x, y) { - const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i); - if (!el.size()) return; - - const duration = Math.hypot(reg.x - x, reg.y - y) * 8; - reg.x = x; - reg.y = y; - const size = +armies.attr("box-size"); - const w = reg.n ? size * 4 : size * 6; - const h = size * 2; - const x1 = x => rn(x - w / 2, 2); - const y1 = y => rn(y - size, 2); - - const move = d3.transition().duration(duration).ease(d3.easeSinInOut); - el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y)); - el.select("text").transition(move).attr("x", x).attr("y", y); - el.selectAll("rect:nth-of-type(2)") - .transition(move) - .attr("x", x1(x) - h) - .attr("y", y1(y)); - el.select(".regimentIcon") - .transition(move) - .attr("x", x1(x) - size) - .attr("y", y) - .attr("height", "6") - .attr("width", "6"); - el.select(".regimentImage") - .transition(move) - .attr("x", x1(x) - h) - .attr("y", y1(y)) - .attr("height", "6") - .attr("width", "6"); -}; diff --git a/modules/renderers/draw-relief-icons.js b/modules/renderers/draw-relief-icons.js deleted file mode 100644 index ffa0b69c..00000000 --- a/modules/renderers/draw-relief-icons.js +++ /dev/null @@ -1,124 +0,0 @@ -"use strict"; - -function drawReliefIcons() { - TIME && console.time("drawRelief"); - terrain.selectAll("*").remove(); - - const cells = pack.cells; - const density = terrain.attr("density") || 0.4; - const size = 2 * (terrain.attr("size") || 1); - const mod = 0.2 * size; // size modifier - const relief = []; - - for (const i of cells.i) { - const height = cells.h[i]; - if (height < 20) continue; // no icons on water - if (cells.r[i]) continue; // no icons on rivers - const biome = cells.biome[i]; - if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome - - const polygon = getPackPolygon(i); - const [minX, maxX] = d3.extent(polygon, p => p[0]); - const [minY, maxY] = d3.extent(polygon, p => p[1]); - - if (height < 50) placeBiomeIcons(i, biome); - else placeReliefIcons(i); - - function placeBiomeIcons() { - const iconsDensity = biomesData.iconsDensity[biome] / 100; - const radius = 2 / iconsDensity / density; - if (Math.random() > iconsDensity * 10) return; - - for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) { - if (!d3.polygonContains(polygon, [cx, cy])) continue; - let h = (4 + Math.random()) * size; - const icon = getBiomeIcon(i, biomesData.icons[biome]); - if (icon === "#relief-grass-1") h *= 1.2; - relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)}); - } - } - - function placeReliefIcons(i) { - const radius = 2 / density; - const [icon, h] = getReliefIcon(i, height); - - for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) { - if (!d3.polygonContains(polygon, [cx, cy])) continue; - relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)}); - } - } - - function getReliefIcon(i, h) { - const temp = grid.cells.temp[pack.cells.g[i]]; - const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill"; - const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6); - return [getIcon(type), size]; - } - } - - // sort relief icons by y+size - relief.sort((a, b) => a.y + a.s - (b.y + b.s)); - - const reliefHTML = new Array(relief.length); - for (const r of relief) { - reliefHTML.push(``); - } - terrain.html(reliefHTML.join("")); - - TIME && console.timeEnd("drawRelief"); - - function getBiomeIcon(i, b) { - let type = b[Math.floor(Math.random() * b.length)]; - const temp = grid.cells.temp[pack.cells.g[i]]; - if (type === "conifer" && temp < 0) type = "coniferSnow"; - return getIcon(type); - } - - function getVariant(type) { - switch (type) { - case "mount": - return rand(2, 7); - case "mountSnow": - return rand(1, 6); - case "hill": - return rand(2, 5); - case "conifer": - return 2; - case "coniferSnow": - return 1; - case "swamp": - return rand(2, 3); - case "cactus": - return rand(1, 3); - case "deadTree": - return rand(1, 2); - default: - return 2; - } - } - - function getOldIcon(type) { - switch (type) { - case "mountSnow": - return "mount"; - case "vulcan": - return "mount"; - case "coniferSnow": - return "conifer"; - case "cactus": - return "dune"; - case "deadTree": - return "dune"; - default: - return type; - } - } - - function getIcon(type) { - const set = terrain.attr("set") || "simple"; - if (set === "simple") return "#relief-" + getOldIcon(type) + "-1"; - if (set === "colored") return "#relief-" + type + "-" + getVariant(type); - if (set === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw"; - return "#relief-" + getOldIcon(type) + "-1"; // simple - } -} diff --git a/modules/renderers/draw-state-labels.js b/modules/renderers/draw-state-labels.js deleted file mode 100644 index 9586a9c1..00000000 --- a/modules/renderers/draw-state-labels.js +++ /dev/null @@ -1,312 +0,0 @@ -"use strict"; - -// list - an optional array of stateIds to regenerate -function drawStateLabels(list) { - TIME && console.time("drawStateLabels"); - - // temporary make the labels visible - const layerDisplay = labels.style("display"); - labels.style("display", null); - - const {cells, states, features} = pack; - const stateIds = cells.state; - - // increase step to 15 or 30 to make it faster and more horyzontal - // decrease step to 5 to improve accuracy - const ANGLE_STEP = 9; - const angles = precalculateAngles(ANGLE_STEP); - - const LENGTH_START = 5; - const LENGTH_STEP = 5; - const LENGTH_MAX = 300; - - const labelPaths = getLabelPaths(); - const letterLength = checkExampleLetterLength(); - drawLabelPath(letterLength); - - // restore labels visibility - labels.style("display", layerDisplay); - - function getLabelPaths() { - const labelPaths = []; - - for (const state of states) { - if (!state.i || state.removed || state.lock) continue; - if (list && !list.includes(state.i)) continue; - - const offset = getOffsetWidth(state.cells); - const maxLakeSize = state.cells / 20; - const [x0, y0] = state.pole; - - const rays = angles.map(({angle, dx, dy}) => { - const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset}); - return {angle, length, x, y}; - }); - const [ray1, ray2] = findBestRayPair(rays); - - const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]]; - if (ray1.x > ray2.x) pathPoints.reverse(); - - if (DEBUG.stateLabels) { - drawPoint(state.pole, {color: "black", radius: 1}); - drawPath(pathPoints, {color: "black", width: 0.2}); - } - - labelPaths.push([state.i, pathPoints]); - } - - return labelPaths; - } - - function checkExampleLetterLength() { - const textGroup = d3.select("g#labels > g#states"); - const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example"); - const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter - testLabel.remove(); - - return letterLength; - } - - function drawLabelPath(letterLength) { - const mode = options.stateLabelsMode || "auto"; - const lineGen = d3.line().curve(d3.curveNatural); - - const textGroup = d3.select("g#labels > g#states"); - const pathGroup = d3.select("defs > g#deftemp > g#textPaths"); - - for (const [stateId, pathPoints] of labelPaths) { - const state = states[stateId]; - if (!state.i || state.removed) throw new Error("State must not be neutral or removed"); - if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points"); - - textGroup.select("#stateLabel" + stateId).remove(); - pathGroup.select("#textPath_stateLabel" + stateId).remove(); - - const textPath = pathGroup - .append("path") - .attr("d", round(lineGen(pathPoints))) - .attr("id", "textPath_stateLabel" + stateId); - - const pathLength = textPath.node().getTotalLength() / letterLength; // path length in letters - const [lines, ratio] = getLinesAndRatio(mode, state.name, state.fullName, pathLength); - - // prolongate path if it's too short - const longestLineLength = d3.max(lines.map(({length}) => length)); - if (pathLength && pathLength < longestLineLength) { - const [x1, y1] = pathPoints.at(0); - const [x2, y2] = pathPoints.at(-1); - const [dx, dy] = [(x2 - x1) / 2, (y2 - y1) / 2]; - - const mod = longestLineLength / pathLength; - pathPoints[0] = [x1 + dx - dx * mod, y1 + dy - dy * mod]; - pathPoints[pathPoints.length - 1] = [x2 - dx + dx * mod, y2 - dy + dy * mod]; - - textPath.attr("d", round(lineGen(pathPoints))); - } - - const textElement = textGroup - .append("text") - .attr("text-rendering", "optimizeSpeed") - .attr("id", "stateLabel" + stateId) - .append("textPath") - .attr("startOffset", "50%") - .attr("font-size", ratio + "%") - .node(); - - const top = (lines.length - 1) / -2; // y offset - const spans = lines.map((line, index) => `${line}`); - textElement.insertAdjacentHTML("afterbegin", spans.join("")); - - const {width, height} = textElement.getBBox(); - textElement.setAttribute("href", "#textPath_stateLabel" + stateId); - - if (mode === "full" || lines.length === 1) continue; - - // check if label fits state boundaries. If no, replace it with short name - const [[x1, y1], [x2, y2]] = [pathPoints.at(0), pathPoints.at(-1)]; - const angleRad = Math.atan2(y2 - y1, x2 - x1); - - const isInsideState = checkIfInsideState(textElement, angleRad, width / 2, height / 2, stateIds, stateId); - if (isInsideState) continue; - - // replace name to one-liner - const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name; - textElement.innerHTML = `${text}`; - - const correctedRatio = minmax(rn((pathLength / text.length) * 50), 50, 130); - textElement.setAttribute("font-size", correctedRatio + "%"); - } - } - - function getOffsetWidth(cellsNumber) { - if (cellsNumber < 40) return 0; - if (cellsNumber < 200) return 5; - return 10; - } - - function precalculateAngles(step) { - const angles = []; - const RAD = Math.PI / 180; - - for (let angle = 0; angle < 360; angle += step) { - const dx = Math.cos(angle * RAD); - const dy = Math.sin(angle * RAD); - angles.push({angle, dx, dy}); - } - - return angles; - } - - function raycast({stateId, x0, y0, dx, dy, maxLakeSize, offset}) { - let ray = {length: 0, x: x0, y: y0}; - - for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) { - const [x, y] = [x0 + length * dx, y0 + length * dy]; - // offset points are perpendicular to the ray - const offset1 = [x + -dy * offset, y + dx * offset]; - const offset2 = [x + dy * offset, y + -dx * offset]; - - 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; - ray = {length, x, y}; - } - - return ray; - - function isInsideState(x, y) { - if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false; - const cellId = findCell(x, y); - - const feature = features[cells.f[cellId]]; - if (feature.type === "lake") return isInnerLake(feature) || isSmallLake(feature); - - return stateIds[cellId] === stateId; - } - - function isInnerLake(feature) { - return feature.shoreline.every(cellId => stateIds[cellId] === stateId); - } - - function isSmallLake(feature) { - return feature.cells <= maxLakeSize; - } - } - - function findBestRayPair(rays) { - let bestPair = null; - let bestScore = -Infinity; - - for (let i = 0; i < rays.length; i++) { - const score1 = rays[i].length * scoreRayAngle(rays[i].angle); - - for (let j = i + 1; j < rays.length; j++) { - const score2 = rays[j].length * scoreRayAngle(rays[j].angle); - const pairScore = (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle); - - if (pairScore > bestScore) { - bestScore = pairScore; - bestPair = [rays[i], rays[j]]; - } - } - } - - return bestPair; - } - - function scoreRayAngle(angle) { - const normalizedAngle = Math.abs(angle % 180); // [0, 180] - const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1] - - if (horizontality === 1) return 1; // Best: horizontal - if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted - if (horizontality >= 0.5) return 0.6; // Good: moderate slant - if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted - if (horizontality >= 0.15) return 0.2; // Poor: almost vertical - return 0.1; // Very poor: almost vertical - } - - function scoreCurvature(angle1, angle2) { - const delta = getAngleDelta(angle1, angle2); - const similarity = evaluateArc(angle1, angle2); - - if (delta === 180) return 1; // straight line: best - if (delta < 90) return 0; // acute: not allowed - if (delta < 120) return 0.6 * similarity; - if (delta < 140) return 0.7 * similarity; - if (delta < 160) return 0.8 * similarity; - - return similarity; - } - - function getAngleDelta(angle1, angle2) { - let delta = Math.abs(angle1 - angle2) % 360; - if (delta > 180) delta = 360 - delta; // [0, 180] - return delta; - } - - // compute arc similarity towards x-axis - function evaluateArc(angle1, angle2) { - const proximity1 = Math.abs((angle1 % 180) - 90); - const proximity2 = Math.abs((angle2 % 180) - 90); - return 1 - Math.abs(proximity1 - proximity2) / 90; - } - - function getLinesAndRatio(mode, name, fullName, pathLength) { - if (mode === "short") return getShortOneLine(); - if (pathLength > fullName.length * 2) return getFullOneLine(); - return getFullTwoLines(); - - function getShortOneLine() { - const ratio = pathLength / name.length; - return [[name], minmax(rn(ratio * 60), 50, 150)]; - } - - function getFullOneLine() { - const ratio = pathLength / fullName.length; - return [[fullName], minmax(rn(ratio * 70), 70, 170)]; - } - - function getFullTwoLines() { - const lines = splitInTwo(fullName); - const longestLineLength = d3.max(lines.map(({length}) => length)); - const ratio = pathLength / longestLineLength; - return [lines, minmax(rn(ratio * 60), 70, 150)]; - } - } - - // check whether multi-lined label is mostly inside the state. If no, replace it with short name label - function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) { - const bbox = textElement.getBBox(); - const [cx, cy] = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2]; - - const points = [ - [-halfwidth, -halfheight], - [+halfwidth, -halfheight], - [+halfwidth, halfheight], - [-halfwidth, halfheight], - [0, halfheight], - [0, -halfheight] - ]; - - const sin = Math.sin(angleRad); - const cos = Math.cos(angleRad); - const rotatedPoints = points.map(([x, y]) => [cx + x * cos - y * sin, cy + x * sin + y * cos]); - - let pointsInside = 0; - for (const [x, y] of rotatedPoints) { - const isInside = stateIds[findCell(x, y)] === stateId; - if (isInside) pointsInside++; - if (pointsInside > 4) return true; - } - - return false; - } - - TIME && console.timeEnd("drawStateLabels"); -} diff --git a/modules/renderers/draw-temperature.js b/modules/renderers/draw-temperature.js deleted file mode 100644 index 51dc32f5..00000000 --- a/modules/renderers/draw-temperature.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; - -function drawTemperature() { - TIME && console.time("drawTemperature"); - - temperature.selectAll("*").remove(); - lineGen.curve(d3.curveBasisClosed); - const scheme = d3.scaleSequential(d3.interpolateSpectral); - - const tMax = +byId("temperatureEquatorOutput").max; - const tMin = +byId("temperatureEquatorOutput").min; - const delta = tMax - tMin; - - const {cells, vertices} = grid; - const n = cells.i.length; - - const checkedCells = new Uint8Array(n); - const addToChecked = cellId => (checkedCells[cellId] = 1); - - const min = d3.min(cells.temp); - const max = d3.max(cells.temp); - const step = Math.max(Math.round(Math.abs(min - max) / 5), 1); - - const isolines = d3.range(min + step, max, step); - const chains = []; - const labels = []; // store label coordinates - - for (const cellId of cells.i) { - const t = cells.temp[cellId]; - if (checkedCells[cellId] || !isolines.includes(t)) continue; - - const startingVertex = findStart(cellId, t); - if (!startingVertex) continue; - checkedCells[cellId] = 1; - - const ofSameType = cellId => cells.temp[cellId] >= t; - const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked}); - const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n)); - if (relaxed.length < 6) continue; - - const points = relaxed.map(v => vertices.p[v]); - chains.push([t, points]); - addLabel(points, t); - } - - // min temp isoline covers all graph - temperature - .append("path") - .attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`) - .attr("fill", scheme(1 - (min - tMin) / delta)) - .attr("stroke", "none"); - - for (const t of isolines) { - const path = chains - .filter(c => c[0] === t) - .map(c => round(lineGen(c[1]))) - .join(""); - if (!path) continue; - const fill = scheme(1 - (t - tMin) / delta), - stroke = d3.color(fill).darker(0.2); - temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke); - } - - const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1); - tempLabels - .selectAll("text") - .data(labels) - .enter() - .append("text") - .attr("x", d => d[0]) - .attr("y", d => d[1]) - .text(d => convertTemperature(d[2])); - - // find cell with temp < isotherm and find vertex to start path detection - function findStart(i, t) { - if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell - return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])]; - } - - function addLabel(points, t) { - const xCenter = svgWidth / 2; - - // add label on isoline top center - const tc = - points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)]; - pushLabel(tc[0], tc[1], t); - - // add label on isoline bottom center - if (points.length > 20) { - const bc = - points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)]; - const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point - if (dist2 > 100) pushLabel(bc[0], bc[1], t); - } - } - - function pushLabel(x, y, t) { - if (x < 20 || x > svgWidth - 20) return; - if (y < 20 || y > svgHeight - 20) return; - labels.push([x, y, t]); - } - - TIME && console.timeEnd("drawTemperature"); -} diff --git a/modules/routes-generator.js b/modules/routes-generator.js deleted file mode 100644 index 460625ed..00000000 --- a/modules/routes-generator.js +++ /dev/null @@ -1,677 +0,0 @@ -const ROUTES_SHARP_ANGLE = 135; -const ROUTES_VERY_SHARP_ANGLE = 115; - -const MIN_PASSABLE_SEA_TEMP = -4; -const ROUTE_TYPE_MODIFIERS = { - "-1": 1, // coastline - "-2": 1.8, // sea - "-3": 4, // open sea - "-4": 6, // ocean - default: 8 // far ocean -}; - -window.Routes = (function () { - function generate(lockedRoutes = []) { - const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs); - - const connections = new Map(); - lockedRoutes.forEach(route => addConnections(route.points.map(p => p[2]))); - - const mainRoads = generateMainRoads(); - const trails = generateTrails(); - const seaRoutes = generateSeaRoutes(); - - pack.routes = createRoutesData(lockedRoutes); - pack.cells.routes = buildLinks(pack.routes); - - function sortBurgsByFeature(burgs) { - const burgsByFeature = {}; - const capitalsByFeature = {}; - const portsByFeature = {}; - - const addBurg = (collection, feature, burg) => { - if (!collection[feature]) collection[feature] = []; - collection[feature].push(burg); - }; - - for (const burg of burgs) { - if (burg.i && !burg.removed) { - const {feature, capital, port} = burg; - addBurg(burgsByFeature, feature, burg); - if (capital) addBurg(capitalsByFeature, feature, burg); - if (port) addBurg(portsByFeature, port, burg); - } - } - - return {burgsByFeature, capitalsByFeature, portsByFeature}; - } - - function generateMainRoads() { - TIME && console.time("generateMainRoads"); - const mainRoads = []; - - for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) { - const points = featureCapitals.map(burg => [burg.x, burg.y]); - const urquhartEdges = calculateUrquhartEdges(points); - urquhartEdges.forEach(([fromId, toId]) => { - const start = featureCapitals[fromId].cell; - const exit = featureCapitals[toId].cell; - - const segments = findPathSegments({isWater: false, connections, start, exit}); - for (const segment of segments) { - addConnections(segment); - mainRoads.push({feature: Number(key), cells: segment}); - } - }); - } - - TIME && console.timeEnd("generateMainRoads"); - return mainRoads; - } - - function generateTrails() { - TIME && console.time("generateTrails"); - const trails = []; - - for (const [key, featureBurgs] of Object.entries(burgsByFeature)) { - const points = featureBurgs.map(burg => [burg.x, burg.y]); - const urquhartEdges = calculateUrquhartEdges(points); - urquhartEdges.forEach(([fromId, toId]) => { - const start = featureBurgs[fromId].cell; - const exit = featureBurgs[toId].cell; - - const segments = findPathSegments({isWater: false, connections, start, exit}); - for (const segment of segments) { - addConnections(segment); - trails.push({feature: Number(key), cells: segment}); - } - }); - } - - TIME && console.timeEnd("generateTrails"); - return trails; - } - - function generateSeaRoutes() { - TIME && console.time("generateSeaRoutes"); - const seaRoutes = []; - - for (const [featureId, featurePorts] of Object.entries(portsByFeature)) { - const points = featurePorts.map(burg => [burg.x, burg.y]); - const urquhartEdges = calculateUrquhartEdges(points); - - urquhartEdges.forEach(([fromId, toId]) => { - const start = featurePorts[fromId].cell; - const exit = featurePorts[toId].cell; - const segments = findPathSegments({isWater: true, connections, start, exit}); - for (const segment of segments) { - addConnections(segment); - seaRoutes.push({feature: Number(featureId), cells: segment}); - } - }); - } - - TIME && console.timeEnd("generateSeaRoutes"); - return seaRoutes; - } - - function addConnections(segment) { - for (let i = 0; i < segment.length; i++) { - const cellId = segment[i]; - const nextCellId = segment[i + 1]; - if (nextCellId) { - connections.set(`${cellId}-${nextCellId}`, true); - connections.set(`${nextCellId}-${cellId}`, true); - } - } - } - - function findPathSegments({isWater, connections, start, exit}) { - const getCost = createCostEvaluator({isWater, connections}); - const pathCells = findPath(start, current => current === exit, getCost); - if (!pathCells) return []; - const segments = getRouteSegments(pathCells, connections); - return segments; - } - - function createRoutesData(routes) { - const pointsArray = preparePointsArray(); - - for (const {feature, cells, merged} of mergeRoutes(mainRoads)) { - if (merged) continue; - const points = getPoints("roads", cells, pointsArray); - routes.push({i: routes.length, group: "roads", feature, points}); - } - - for (const {feature, cells, merged} of mergeRoutes(trails)) { - if (merged) continue; - const points = getPoints("trails", cells, pointsArray); - routes.push({i: routes.length, group: "trails", feature, points}); - } - - for (const {feature, cells, merged} of mergeRoutes(seaRoutes)) { - if (merged) continue; - const points = getPoints("searoutes", cells, pointsArray); - routes.push({i: routes.length, group: "searoutes", feature, points}); - } - - return routes; - } - - // merge routes so that the last cell of one route is the first cell of the next route - function mergeRoutes(routes) { - let routesMerged = 0; - - for (let i = 0; i < routes.length; i++) { - const thisRoute = routes[i]; - if (thisRoute.merged) continue; - - for (let j = i + 1; j < routes.length; j++) { - const nextRoute = routes[j]; - if (nextRoute.merged) continue; - - if (nextRoute.cells.at(0) === thisRoute.cells.at(-1)) { - routesMerged++; - thisRoute.cells = thisRoute.cells.concat(nextRoute.cells.slice(1)); - nextRoute.merged = true; - } - } - } - - return routesMerged > 1 ? mergeRoutes(routes) : routes; - } - } - - function createCostEvaluator({isWater, connections}) { - return isWater ? getWaterPathCost : getLandPathCost; - - function getLandPathCost(current, next) { - if (pack.cells.h[next] < 20) return Infinity; // ignore water cells - - const habitability = biomesData.habitability[pack.cells.biome[next]]; - if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier) - - const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]); - const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1]; - const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3]; - const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1; - const burgModifier = pack.cells.burg[next] ? 1 : 3; - - const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier; - return pathCost; - } - - function getWaterPathCost(current, next) { - if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells - if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells - - const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]); - const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default; - const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1; - - const pathCost = distanceCost * typeModifier * connectionModifier; - return pathCost; - } - } - - function buildLinks(routes) { - const links = {}; - - for (const {points, i: routeId} of routes) { - const cells = points.map(p => p[2]); - - for (let i = 0; i < cells.length - 1; i++) { - const cellId = cells[i]; - const nextCellId = cells[i + 1]; - - if (cellId !== nextCellId) { - if (!links[cellId]) links[cellId] = {}; - links[cellId][nextCellId] = routeId; - - if (!links[nextCellId]) links[nextCellId] = {}; - links[nextCellId][cellId] = routeId; - } - } - } - - return links; - } - - function preparePointsArray() { - const {cells, burgs} = pack; - return cells.p.map(([x, y], cellId) => { - const burgId = cells.burg[cellId]; - if (burgId) return [burgs[burgId].x, burgs[burgId].y]; - return [x, y]; - }); - } - - function getPoints(group, cells, points) { - const data = cells.map(cellId => [...points[cellId], cellId]); - - // resolve sharp angles - if (group !== "searoutes") { - for (let i = 1; i < cells.length - 1; i++) { - const cellId = cells[i]; - if (pack.cells.burg[cellId]) continue; - - const [prevX, prevY] = data[i - 1]; - const [currX, currY] = data[i]; - const [nextX, nextY] = data[i + 1]; - - const dAx = prevX - currX; - const dAy = prevY - currY; - const dBx = nextX - currX; - const dBy = nextY - currY; - const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI); - - if (angle < ROUTES_SHARP_ANGLE) { - const middleX = (prevX + nextX) / 2; - const middleY = (prevY + nextY) / 2; - let newX, newY; - - if (angle < ROUTES_VERY_SHARP_ANGLE) { - newX = rn((currX + middleX * 2) / 3, 2); - newY = rn((currY + middleY * 2) / 3, 2); - } else { - newX = rn((currX + middleX) / 2, 2); - newY = rn((currY + middleY) / 2, 2); - } - - if (findCell(newX, newY) === cellId) { - data[i] = [newX, newY, cellId]; - points[cellId] = [data[i][0], data[i][1]]; // change cell coordinate for all routes - } - } - } - } - - return data; // [[x, y, cell], [x, y, cell]]; - } - - function getRouteSegments(pathCells, connections) { - const segments = []; - let segment = []; - - for (let i = 0; i < pathCells.length; i++) { - const cellId = pathCells[i]; - const nextCellId = pathCells[i + 1]; - const isConnected = connections.has(`${cellId}-${nextCellId}`) || connections.has(`${nextCellId}-${cellId}`); - - if (isConnected) { - if (segment.length) { - // segment stepped into existing segment - segment.push(pathCells[i]); - segments.push(segment); - segment = []; - } - continue; - } - - segment.push(pathCells[i]); - } - - if (segment.length > 1) segments.push(segment); - - return segments; - } - - // Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation - // this gives us an aproximation of a desired road network, i.e. connections between burgs - // code from https://observablehq.com/@mbostock/urquhart-graph - function calculateUrquhartEdges(points) { - const score = (p0, p1) => dist2(points[p0], points[p1]); - - const {halfedges, triangles} = Delaunator.from(points); - const n = triangles.length; - - const removed = new Uint8Array(n); - const edges = []; - - for (let e = 0; e < n; e += 3) { - const p0 = triangles[e], - p1 = triangles[e + 1], - p2 = triangles[e + 2]; - - const p01 = score(p0, p1), - p12 = score(p1, p2), - p20 = score(p2, p0); - - removed[ - p20 > p01 && p20 > p12 - ? Math.max(e + 2, halfedges[e + 2]) - : p12 > p01 && p12 > p20 - ? Math.max(e + 1, halfedges[e + 1]) - : Math.max(e, halfedges[e]) - ] = 1; - } - - for (let e = 0; e < n; ++e) { - if (e > halfedges[e] && !removed[e]) { - const t0 = triangles[e]; - const t1 = triangles[e % 3 === 2 ? e - 2 : e + 1]; - edges.push([t0, t1]); - } - } - - return edges; - } - - // connect cell with routes system by land - function connect(cellId) { - const getCost = createCostEvaluator({isWater: false, connections: new Map()}); - const isExit = c => isLand(c) && isConnected(c); - const pathCells = findPath(cellId, isExit, getCost); - if (!pathCells) return; - - const pointsArray = preparePointsArray(); - const points = getPoints("trails", pathCells, pointsArray); - const feature = pack.cells.f[cellId]; - const routeId = getNextId(); - const newRoute = {i: routeId, group: "trails", feature, points}; - pack.routes.push(newRoute); - - for (let i = 0; i < pathCells.length; i++) { - const currentCell = pathCells[i]; - const nextCellId = pathCells[i + 1]; - if (nextCellId) addConnection(currentCell, nextCellId, routeId); - } - - return newRoute; - - function addConnection(from, to, routeId) { - const routes = pack.cells.routes; - - if (!routes[from]) routes[from] = {}; - routes[from][to] = routeId; - - if (!routes[to]) routes[to] = {}; - routes[to][from] = routeId; - } - } - - // utility functions - function isConnected(cellId) { - const routes = pack.cells.routes; - 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]; - if (routeId === undefined) return null; - - const route = pack.routes.find(route => route.i === routeId); - if (!route) return null; - - return route; - } - - function hasRoad(cellId) { - const connections = pack.cells.routes[cellId]; - if (!connections) return false; - - return Object.values(connections).some(routeId => { - const route = pack.routes.find(route => route.i === routeId); - if (!route) return false; - return route.group === "roads"; - }); - } - - function isCrossroad(cellId) { - const connections = pack.cells.routes[cellId]; - if (!connections) return false; - if (Object.keys(connections).length > 3) return true; - const roadConnections = Object.values(connections).filter(routeId => { - const route = pack.routes.find(route => route.i === routeId); - return route?.group === "roads"; - }); - return roadConnections.length > 2; - } - - const connectivityRateMap = { - roads: 0.2, - trails: 0.1, - searoutes: 0.2, - default: 0.1 - }; - - function getConnectivityRate(cellId) { - const connections = pack.cells.routes[cellId]; - if (!connections) return 0; - - const connectivity = Object.values(connections).reduce((acc, routeId) => { - const route = pack.routes.find(route => route.i === routeId); - if (!route) return acc; - const rate = connectivityRateMap[route.group] || connectivityRateMap.default; - return acc + rate; - }, 0.8); - - return connectivity; - } - - // name generator data - const models = { - roads: {burg_suffix: 3, prefix_suffix: 6, the_descriptor_prefix_suffix: 2, the_descriptor_burg_suffix: 1}, - trails: {burg_suffix: 8, prefix_suffix: 1, the_descriptor_burg_suffix: 1}, - searoutes: {burg_suffix: 4, prefix_suffix: 2, the_descriptor_prefix_suffix: 1} - }; - - const prefixes = [ - "King", - "Queen", - "Military", - "Old", - "New", - "Ancient", - "Royal", - "Imperial", - "Great", - "Grand", - "High", - "Silver", - "Dragon", - "Shadow", - "Star", - "Mystic", - "Whisper", - "Eagle", - "Golden", - "Crystal", - "Enchanted", - "Frost", - "Moon", - "Sun", - "Thunder", - "Phoenix", - "Sapphire", - "Celestial", - "Wandering", - "Echo", - "Twilight", - "Crimson", - "Serpent", - "Iron", - "Forest", - "Flower", - "Whispering", - "Eternal", - "Frozen", - "Rain", - "Luminous", - "Stardust", - "Arcane", - "Glimmering", - "Jade", - "Ember", - "Azure", - "Gilded", - "Divine", - "Shadowed", - "Cursed", - "Moonlit", - "Sable", - "Everlasting", - "Amber", - "Nightshade", - "Wraith", - "Scarlet", - "Platinum", - "Whirlwind", - "Obsidian", - "Ethereal", - "Ghost", - "Spike", - "Dusk", - "Raven", - "Spectral", - "Burning", - "Verdant", - "Copper", - "Velvet", - "Falcon", - "Enigma", - "Glowing", - "Silvered", - "Molten", - "Radiant", - "Astral", - "Wild", - "Flame", - "Amethyst", - "Aurora", - "Shadowy", - "Solar", - "Lunar", - "Whisperwind", - "Fading", - "Titan", - "Dawn", - "Crystalline", - "Jeweled", - "Sylvan", - "Twisted", - "Ebon", - "Thorn", - "Cerulean", - "Halcyon", - "Infernal", - "Storm", - "Eldritch", - "Sapphire", - "Crimson", - "Tranquil", - "Paved" - ]; - - const descriptors = [ - "Great", - "Shrouded", - "Sacred", - "Fabled", - "Frosty", - "Winding", - "Echoing", - "Serpentine", - "Breezy", - "Misty", - "Rustic", - "Silent", - "Cobbled", - "Cracked", - "Shaky", - "Obscure" - ]; - - const suffixes = { - roads: {road: 7, route: 3, way: 2, highway: 1}, - trails: {trail: 4, path: 1, track: 1, pass: 1}, - searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1} - }; - - function generateName({group, points}) { - if (points.length < 4) return "Unnamed route segment"; - - const model = rw(models[group]); - const suffix = rw(suffixes[group]); - - const burgName = getBurgName(); - if (model === "burg_suffix" && burgName) return `${burgName} ${suffix}`; - if (model === "prefix_suffix") return `${ra(prefixes)} ${suffix}`; - if (model === "the_descriptor_prefix_suffix") return `The ${ra(descriptors)} ${ra(prefixes)} ${suffix}`; - if (model === "the_descriptor_burg_suffix" && burgName) return `The ${ra(descriptors)} ${burgName} ${suffix}`; - return "Unnamed route"; - - function getBurgName() { - const priority = [points.at(-1), points.at(0), points.slice(1, -1).reverse()]; - for (const [_x, _y, cellId] of priority) { - const burgId = pack.cells.burg[cellId]; - if (burgId) return getAdjective(pack.burgs[burgId].name); - } - return null; - } - } - - const ROUTE_CURVES = { - roads: d3.curveCatmullRom.alpha(0.1), - trails: d3.curveCatmullRom.alpha(0.1), - searoutes: d3.curveCatmullRom.alpha(0.5), - default: d3.curveCatmullRom.alpha(0.1) - }; - - function getPath({group, points}) { - const lineGen = d3.line(); - lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default); - const path = round(lineGen(points.map(p => [p[0], p[1]])), 1); - return path; - } - - function getLength(routeId) { - const path = routes.select("#route" + routeId).node(); - return path.getTotalLength(); - } - - function getNextId() { - return pack.routes.length ? Math.max(...pack.routes.map(r => r.i)) + 1 : 0; - } - - function remove(route) { - const routes = pack.cells.routes; - - for (const point of route.points) { - const from = point[2]; - if (!routes[from]) continue; - - for (const [to, routeId] of Object.entries(routes[from])) { - if (routeId === route.i) { - delete routes[from][to]; - delete routes[to][from]; - } - } - } - - pack.routes = pack.routes.filter(r => r.i !== route.i); - viewbox.select("#route" + route.i).remove(); - } - - return { - generate, - buildLinks, - connect, - isConnected, - areConnected, - getRoute, - hasRoad, - isCrossroad, - getConnectivityRate, - generateName, - getPath, - getLength, - getNextId, - remove - }; -})(); diff --git a/modules/states-generator.js b/modules/states-generator.js deleted file mode 100644 index 9662e648..00000000 --- a/modules/states-generator.js +++ /dev/null @@ -1,640 +0,0 @@ -"use strict"; - -window.States = (() => { - const generate = () => { - TIME && console.time("generateStates"); - pack.states = createStates(); - expandStates(); - normalize(); - getPoles(); - findNeighbors(); - assignColors(); - generateCampaigns(); - generateDiplomacy(); - - TIME && console.timeEnd("generateStates"); - - // for each capital create a state - function createStates() { - const states = [{i: 0, name: "Neutrals"}]; - const each5th = each(5); - const sizeVariety = byId("sizeVariety").valueAsNumber; - - pack.burgs.forEach(burg => { - if (!burg.i || !burg.capital) return; - - const expansionism = rn(Math.random() * sizeVariety + 1, 1); - const basename = burg.name.length < 9 && each5th(burg.cell) ? burg.name : Names.getCultureShort(burg.culture); - const name = Names.getState(basename, burg.culture); - const type = pack.cultures[burg.culture].type; - const coa = COA.generate(null, null, null, type); - coa.shield = COA.getShield(burg.culture, null); - states.push({ - i: burg.i, - name, - expansionism, - capital: burg.i, - type, - center: burg.cell, - culture: burg.culture, - coa - }); - }); - - return states; - } - }; - - // expand cultures across the map (Dijkstra-like algorithm) - const expandStates = () => { - TIME && console.time("expandStates"); - const {cells, states, cultures, burgs} = pack; - - cells.state = cells.state || new Uint16Array(cells.i.length); - - const queue = new FlatQueue(); - const cost = []; - - const globalGrowthRate = byId("growthRate").valueAsNumber || 1; - const statesGrowthRate = byId("statesGrowthRate")?.valueAsNumber || 1; - const growthRate = (cells.i.length / 2) * globalGrowthRate * statesGrowthRate; // limit cost for state growth - - // remove state from all cells except of locked - for (const cellId of cells.i) { - const state = states[cells.state[cellId]]; - if (state.lock) continue; - cells.state[cellId] = 0; - } - - for (const state of states) { - if (!state.i || state.removed) continue; - - const capitalCell = burgs[state.capital].cell; - cells.state[capitalCell] = state.i; - const cultureCenter = cultures[state.culture].center; - const b = cells.biome[cultureCenter]; // state native biome - queue.push({e: state.center, p: 0, s: state.i, b}, 0); - cost[state.center] = 1; - } - - while (queue.length) { - const next = queue.pop(); - - const {e, p, s, b} = next; - const {type, culture} = states[s]; - - cells.c[e].forEach(e => { - const state = states[cells.state[e]]; - if (state.lock) return; // do not overwrite cell of locked states - if (cells.state[e] && e === state.center) return; // do not overwrite capital cells - - const cultureCost = culture === cells.culture[e] ? -9 : 100; - const populationCost = cells.h[e] < 20 ? 0 : cells.s[e] ? Math.max(20 - cells.s[e], 0) : 5000; - const biomeCost = getBiomeCost(b, cells.biome[e], type); - const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type); - const riverCost = getRiverCost(cells.r[e], e, type); - const typeCost = getTypeCost(cells.t[e], type); - const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0); - const totalCost = p + 10 + cellCost / states[s].expansionism; - - if (totalCost > growthRate) return; - - if (!cost[e] || totalCost < cost[e]) { - if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell - cost[e] = totalCost; - queue.push({e, p: totalCost, s, b}, totalCost); - } - }); - } - - burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = cells.state[b.cell])); // assign state to burgs - - function getBiomeCost(b, biome, type) { - if (b === biome) return 10; // tiny penalty for native biome - if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters - if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads - return biomesData.cost[biome]; // general non-native biome penalty - } - - function getHeightCost(f, h, type) { - if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures - if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals - if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads - if (h < 20) return 1000; // general sea crossing penalty - if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands - if (type === "Highland") return 0; // no penalty for highlanders on highlands - if (h >= 67) return 2200; // general mountains crossing penalty - if (h >= 44) return 300; // general hills crossing penalty - return 0; - } - - function getRiverCost(r, i, type) { - if (type === "River") return r ? 0 : 100; // penalty for river cultures - if (!r) return 0; // no penalty for others if there is no river - return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux - } - - function getTypeCost(t, type) { - if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline - if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads - if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals - return 0; - } - - TIME && console.timeEnd("expandStates"); - }; - - const normalize = () => { - TIME && console.time("normalizeStates"); - const {cells, burgs} = pack; - - for (const i of cells.i) { - if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs - if (pack.states[cells.state[i]]?.lock) continue; // do not overwrite cells of locks states - if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital - const neibs = cells.c[i].filter(c => cells.h[c] >= 20); - const adversaries = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] !== cells.state[i]); - if (adversaries.length < 2) continue; - const buddies = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] === cells.state[i]); - if (buddies.length > 2) continue; - if (adversaries.length <= buddies.length) continue; - cells.state[i] = cells.state[adversaries[0]]; - } - TIME && console.timeEnd("normalizeStates"); - }; - - // calculate pole of inaccessibility for each state - const getPoles = () => { - const getType = cellId => pack.cells.state[cellId]; - const poles = getPolesOfInaccessibility(pack, getType); - - pack.states.forEach(s => { - if (!s.i || s.removed) return; - s.pole = poles[s.i] || [0, 0]; - }); - }; - - const findNeighbors = () => { - const {cells, states} = pack; - - states.forEach(s => { - if (s.removed) return; - s.neighbors = new Set(); - }); - - for (const i of cells.i) { - if (cells.h[i] < 20) continue; - const s = cells.state[i]; - - cells.c[i] - .filter(c => cells.h[c] >= 20 && cells.state[c] !== s) - .forEach(c => states[s].neighbors.add(cells.state[c])); - } - - // convert neighbors Set object into array - states.forEach(s => { - if (!s.neighbors || s.removed) return; - s.neighbors = Array.from(s.neighbors); - }); - }; - - const assignColors = () => { - TIME && console.time("assignColors"); - const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2; - const states = pack.states; - - // assign basic color using greedy coloring algorithm - states.forEach(state => { - if (!state.i || state.removed || state.lock) return; - state.color = colors.find(color => state.neighbors.every(neibStateId => states[neibStateId].color !== color)); - if (!state.color) state.color = getRandomColor(); - colors.push(colors.shift()); - }); - - // randomize each already used color a bit - colors.forEach(c => { - const sameColored = states.filter(state => state.color === c && state.i && !state.lock); - sameColored.forEach((state, index) => { - if (!index) return; - state.color = getMixedColor(state.color); - }); - }); - - TIME && console.timeEnd("assignColors"); - }; - - // calculate states data like area, population etc. - const collectStatistics = () => { - TIME && console.time("collectStatistics"); - const {cells, states} = pack; - - states.forEach(s => { - if (s.removed) return; - s.cells = s.area = s.burgs = s.rural = s.urban = 0; - }); - - for (const i of cells.i) { - if (cells.h[i] < 20) continue; - const s = cells.state[i]; - - // collect stats - states[s].cells += 1; - states[s].area += cells.area[i]; - states[s].rural += cells.pop[i]; - if (cells.burg[i]) { - states[s].urban += pack.burgs[cells.burg[i]].population; - states[s].burgs++; - } - } - - TIME && console.timeEnd("collectStatistics"); - }; - - const wars = { - War: 6, - Conflict: 2, - Campaign: 4, - Invasion: 2, - Rebellion: 2, - Conquest: 2, - Intervention: 1, - Expedition: 1, - Crusade: 1 - }; - - const generateCampaign = state => { - const neighbors = state.neighbors.length ? state.neighbors : [0]; - return neighbors - .map(i => { - const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(state.culture); - const start = gauss(options.year - 100, 150, 1, options.year - 6); - const end = start + gauss(4, 5, 1, options.year - start - 1); - return {name: getAdjective(name) + " " + rw(wars), start, end}; - }) - .sort((a, b) => a.start - b.start); - }; - - // generate historical conflicts of each state - const generateCampaigns = () => { - pack.states.forEach(s => { - if (!s.i || s.removed) return; - s.campaigns = generateCampaign(s); - }); - }; - - // generate Diplomatic Relationships - const generateDiplomacy = () => { - TIME && console.time("generateDiplomacy"); - const {cells, states} = pack; - const chronicle = (states[0].diplomacy = []); - const valid = states.filter(s => s.i && !states.removed); - - const neibs = {Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9}; // relations to neighbors - const neibsOfNeibs = {Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1}; // relations to neighbors of neighbors - const far = {Friendly: 1, Neutral: 12, Suspicion: 2, Unknown: 6}; // relations to other - const navals = {Neutral: 1, Suspicion: 2, Rival: 1, Unknown: 1}; // relations of naval powers - - valid.forEach(s => (s.diplomacy = new Array(states.length).fill("x"))); // clear all relationships - if (valid.length < 2) return; // no states to renerate relations with - const areaMean = d3.mean(valid.map(s => s.area)); // average state area - - // generic relations - for (let f = 1; f < states.length; f++) { - if (states[f].removed) continue; - - if (states[f].diplomacy.includes("Vassal")) { - // Vassals copy relations from their Suzerains - const suzerain = states[f].diplomacy.indexOf("Vassal"); - - for (let i = 1; i < states.length; i++) { - if (i === f || i === suzerain) continue; - states[f].diplomacy[i] = states[suzerain].diplomacy[i]; - if (states[suzerain].diplomacy[i] === "Suzerain") states[f].diplomacy[i] = "Ally"; - for (let e = 1; e < states.length; e++) { - if (e === f || e === suzerain) continue; - if (states[e].diplomacy[suzerain] === "Suzerain" || states[e].diplomacy[suzerain] === "Vassal") continue; - states[e].diplomacy[f] = states[e].diplomacy[suzerain]; - } - } - continue; - } - - for (let t = f + 1; t < states.length; t++) { - if (states[t].removed) continue; - - if (states[t].diplomacy.includes("Vassal")) { - const suzerain = states[t].diplomacy.indexOf("Vassal"); - states[f].diplomacy[t] = states[f].diplomacy[suzerain]; - continue; - } - - const naval = - states[f].type === "Naval" && - states[t].type === "Naval" && - cells.f[states[f].center] !== cells.f[states[t].center]; - const neib = naval ? false : states[f].neighbors.includes(t); - const neibOfNeib = - naval || neib - ? false - : states[f].neighbors - .map(n => states[n].neighbors) - .join("") - .includes(t); - - let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far); - - // add Vassal - if ( - neib && - P(0.8) && - states[f].area > areaMean && - states[t].area < areaMean && - states[f].area / states[t].area > 2 - ) - status = "Vassal"; - states[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status; - states[t].diplomacy[f] = status; - } - } - - // declare wars - for (let attacker = 1; attacker < states.length; attacker++) { - const ad = states[attacker].diplomacy; // attacker relations; - if (states[attacker].removed) continue; - if (!ad.includes("Rival")) continue; // no rivals to attack - if (ad.includes("Vassal")) continue; // not independent - if (ad.includes("Enemy")) continue; // already at war - - // random independent rival - const defender = ra( - ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d) - ); - let ap = states[attacker].area * states[attacker].expansionism; - let dp = states[defender].area * states[defender].expansionism; - if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong - - const an = states[attacker].name; - const dn = states[defender].name; // names - const attackers = [attacker]; - const defenders = [defender]; // attackers and defenders array - const dd = states[defender].diplomacy; // defender relations; - - // start an ongoing war - const name = `${an}-${trimVowels(dn)}ian War`; - const start = options.year - gauss(2, 3, 0, 10); - const war = [name, `${an} declared a war on its rival ${dn}`]; - const campaign = {name, start, attacker, defender}; - states[attacker].campaigns.push(campaign); - states[defender].campaigns.push(campaign); - - // attacker vassals join the war - ad.forEach((r, d) => { - if (r === "Suzerain") { - attackers.push(d); - war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`); - } - }); - - // defender vassals join the war - dd.forEach((r, d) => { - if (r === "Suzerain") { - defenders.push(d); - war.push(`${dn}'s vassal ${states[d].name} joined the war on defenders side`); - } - }); - - ap = d3.sum(attackers.map(a => states[a].area * states[a].expansionism)); // attackers joined power - dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power - - // defender allies join - dd.forEach((r, d) => { - if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return; - if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > 2 * gauss(1.6, 0.8, 0, 10, 2)) { - const reason = states[d].diplomacy.includes("Enemy") ? "Being already at war," : `Frightened by ${an},`; - war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`); - dd[d] = states[d].diplomacy[defender] = "Suspicion"; - return; - } - defenders.push(d); - dp += states[d].area * states[d].expansionism; - war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`); - - // ally vassals join - states[d].diplomacy - .map((r, d) => (r === "Suzerain" ? d : 0)) - .filter(d => d) - .forEach(v => { - defenders.push(v); - dp += states[v].area * states[v].expansionism; - war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`); - }); - }); - - // attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally - ad.forEach((r, d) => { - if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return; - const name = states[d].name; - if (states[d].diplomacy[defender] !== "Rival" && (P(0.2) || ap <= dp * 1.2)) { - war.push(`${an}'s ally ${name} avoided entering the war`); - return; - } - const allies = states[d].diplomacy.map((r, d) => (r === "Ally" ? d : 0)).filter(d => d); - if (allies.some(ally => defenders.includes(ally))) { - war.push(`${an}'s ally ${name} did not join the war as its allies are in war on both sides`); - return; - } - - attackers.push(d); - ap += states[d].area * states[d].expansionism; - war.push(`${an}'s ally ${name} joined the war on attackers side`); - - // ally vassals join - states[d].diplomacy - .map((r, d) => (r === "Suzerain" ? d : 0)) - .filter(d => d) - .forEach(v => { - attackers.push(v); - dp += states[v].area * states[v].expansionism; - war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`); - }); - }); - - // change relations to Enemy for all participants - attackers.forEach(a => defenders.forEach(d => (states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy"))); - chronicle.push(war); // add a record to diplomatical history - } - - TIME && console.timeEnd("generateDiplomacy"); - }; - - // select a forms for listed or all valid states - const defineStateForms = list => { - TIME && console.time("defineStateForms"); - const states = pack.states.filter(s => s.i && !s.removed && !s.lock); - if (states.length < 1) return; - - const generic = {Monarchy: 25, Republic: 2, Union: 1}; - const naval = {Monarchy: 25, Republic: 8, Union: 3}; - - const median = d3.median(pack.states.map(s => s.area)); - const empireMin = states.map(s => s.area).sort((a, b) => b - a)[Math.max(Math.ceil(states.length ** 0.4) - 2, 0)]; - const expTiers = pack.states.map(s => { - let tier = Math.min(Math.floor((s.area / median) * 2.6), 4); - if (tier === 4 && s.area < empireMin) tier = 3; - return tier; - }); - - const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier - const republic = { - Republic: 75, - Federation: 4, - "Trade Company": 4, - "Most Serene Republic": 2, - Oligarchy: 2, - Tetrarchy: 1, - Triumvirate: 1, - Diarchy: 1, - Junta: 1 - }; // weighted random - const union = { - Union: 3, - League: 4, - Confederation: 1, - "United Kingdom": 1, - "United Republic": 1, - "United Provinces": 2, - Commonwealth: 1, - Heptarchy: 1 - }; // weighted random - const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, "Holy State": 1}; - const anarchy = {"Free Territory": 2, Council: 3, Commune: 1, Community: 1}; - - for (const s of states) { - if (list && !list.includes(s.i)) continue; - const tier = expTiers[s.i]; - - const religion = pack.cells.religion[s.center]; - const isTheocracy = - (religion && pack.religions[religion].expansion === "state") || - (P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type)); - const isAnarchy = P(0.01 - tier / 500); - - if (isTheocracy) s.form = "Theocracy"; - else if (isAnarchy) s.form = "Anarchy"; - else s.form = s.type === "Naval" ? rw(naval) : rw(generic); - s.formName = selectForm(s, tier); - s.fullName = getFullName(s); - } - - function selectForm(s, tier) { - const base = pack.cultures[s.culture].base; - - if (s.form === "Monarchy") { - const form = monarchy[tier]; - // Default name depends on exponent tier, some culture bases have special names for tiers - if (s.diplomacy) { - if ( - form === "Duchy" && - s.neighbors.length > 1 && - rand(6) < s.neighbors.length && - s.diplomacy.includes("Vassal") - ) - return "Marches"; // some vassal duchies on borderland - if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals - if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals - } - - if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khanate"; // Mongolian - if (base === 16 && form === "Principality") return "Beylik"; // Turkic - if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian - if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic - if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese - if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber - if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic - if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek - if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian - if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Horde"; // Turkic - if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian - return form; - } - - if (s.form === "Republic") { - // Default name is from weighted array, special case for small states with only 1 burg - if (tier < 2 && s.burgs === 1) { - if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) { - s.name = pack.burgs[s.capital].name; - return "Free City"; - } - if (P(0.3)) return "City-state"; - } - return rw(republic); - } - - if (s.form === "Union") return rw(union); - if (s.form === "Anarchy") return rw(anarchy); - - if (s.form === "Theocracy") { - // European - if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) { - if (P(0.1)) return "Divine " + monarchy[tier]; - if (tier < 2 && P(0.5)) return "Diocese"; - if (tier < 2 && P(0.5)) return "Bishopric"; - } - if (P(0.9) && [7, 5].includes(base)) { - // Greek, Ruthenian - if (tier < 2) return "Eparchy"; - if (tier === 2) return "Exarchate"; - if (tier > 2) return "Patriarchate"; - } - if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish - if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili - return rw(theocracy); - } - } - - TIME && console.timeEnd("defineStateForms"); - }; - - // state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name - const adjForms = [ - "Empire", - "Sultanate", - "Khaganate", - "Shogunate", - "Caliphate", - "Despotate", - "Theocracy", - "Oligarchy", - "Union", - "Confederation", - "Trade Company", - "League", - "Tetrarchy", - "Triumvirate", - "Diarchy", - "Horde", - "Marches" - ]; - - const getFullName = state => { - if (!state.formName) return state.name; - if (!state.name && state.formName) return "The " + state.formName; - const adjName = adjForms.includes(state.formName) && !/-| /.test(state.name); - return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`; - }; - - return { - generate, - expandStates, - normalize, - getPoles, - findNeighbors, - assignColors, - collectStatistics, - generateCampaign, - generateCampaigns, - generateDiplomacy, - defineStateForms, - getFullName - }; -})(); diff --git a/modules/zones-generator.js b/modules/zones-generator.js deleted file mode 100644 index 641a0784..00000000 --- a/modules/zones-generator.js +++ /dev/null @@ -1,454 +0,0 @@ -"use strict"; - -window.Zones = (function () { - const config = { - invasion: {quantity: 2, generate: addInvasion}, // invasion of enemy lands - rebels: {quantity: 1.5, generate: addRebels}, // rebels along a state border - proselytism: {quantity: 1.6, generate: addProselytism}, // proselitism of organized religion - crusade: {quantity: 1.6, generate: addCrusade}, // crusade on heresy lands - disease: {quantity: 1.4, generate: addDisease}, // disease starting in a random city - disaster: {quantity: 1, generate: addDisaster}, // disaster starting in a random city - eruption: {quantity: 1, generate: addEruption}, // eruption aroung volcano - avalanche: {quantity: 0.8, generate: addAvalanche}, // avalanche impacting highland road - fault: {quantity: 1, generate: addFault}, // fault line in elevated areas - flood: {quantity: 1, generate: addFlood}, // flood on river banks - tsunami: {quantity: 1, generate: addTsunami} // tsunami starting near coast - }; - - const generate = function (globalModifier = 1) { - TIME && console.time("generateZones"); - - const usedCells = new Uint8Array(pack.cells.i.length); - pack.zones = []; - - Object.values(config).forEach(type => { - const expectedNumber = type.quantity * globalModifier; - let number = gauss(expectedNumber, expectedNumber / 2, 0, 100); - while (number--) type.generate(usedCells); - }); - - TIME && console.timeEnd("generateZones"); - }; - - function addInvasion(usedCells) { - const {cells, states} = pack; - - const ongoingConflicts = states - .filter(s => s.i && !s.removed && s.campaigns) - .map(s => s.campaigns) - .flat() - .filter(c => !c.end); - if (!ongoingConflicts.length) return; - const {defender, attacker} = ra(ongoingConflicts); - - const borderCells = cells.i.filter(cellId => { - if (usedCells[cellId]) return false; - if (cells.state[cellId] !== defender) return false; - return cells.c[cellId].some(c => cells.state[c] === attacker); - }); - - const startCell = ra(borderCells); - if (startCell === undefined) return; - - const invasionCells = []; - const queue = [startCell]; - const maxCells = rand(5, 30); - - while (queue.length) { - const cellId = P(0.4) ? queue.shift() : queue.pop(); - invasionCells.push(cellId); - if (invasionCells.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId]) return; - if (cells.state[neibCellId] !== defender) return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - const subtype = rw({ - Invasion: 5, - Occupation: 4, - Conquest: 3, - Incursion: 2, - Intervention: 2, - Assault: 1, - Foray: 1, - Intrusion: 1, - Irruption: 1, - Offensive: 1, - Pillaging: 1, - Plunder: 1, - Raid: 1, - Skirmishes: 1 - }); - const name = getAdjective(states[attacker].name) + " " + subtype; - - pack.zones.push({i: pack.zones.length, name, type: "Invasion", cells: invasionCells, color: "url(#hatch1)"}); - } - - function addRebels(usedCells) { - const {cells, states} = pack; - - const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(Boolean))); - if (!state) return; - - const neibStateId = ra(state.neighbors.filter(n => n && !states[n].removed)); - if (!neibStateId) return; - - const cellsArray = []; - const queue = []; - const borderCellId = cells.i.find( - i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neibStateId) - ); - if (borderCellId) queue.push(borderCellId); - const maxCells = rand(10, 30); - - while (queue.length) { - const cellId = queue.shift(); - cellsArray.push(cellId); - if (cellsArray.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId]) return; - if (cells.state[neibCellId] !== state.i) return; - usedCells[neibCellId] = 1; - if (neibCellId % 4 !== 0 && !cells.c[neibCellId].some(c => cells.state[c] === neibStateId)) return; - queue.push(neibCellId); - }); - } - - const rebels = rw({ - Rebels: 5, - Insurrection: 2, - Mutineers: 1, - Insurgents: 1, - Rebellion: 1, - Renegades: 1, - Revolters: 1, - Revolutionaries: 1, - Rioters: 1, - Separatists: 1, - Secessionists: 1, - Conspiracy: 1 - }); - - const name = getAdjective(states[neibStateId].name) + " " + rebels; - pack.zones.push({i: pack.zones.length, name, type: "Rebels", cells: cellsArray, color: "url(#hatch3)"}); - } - - function addProselytism(usedCells) { - const {cells, religions} = pack; - - const organizedReligions = religions.filter(r => r.i && !r.removed && r.type === "Organized"); - const religion = ra(organizedReligions); - if (!religion) return; - - const targetBorderCells = cells.i.filter( - i => - cells.h[i] < 20 && - cells.pop[i] && - cells.religion[i] !== religion.i && - cells.c[i].some(c => cells.religion[c] === religion.i) - ); - const startCell = ra(targetBorderCells); - if (!startCell) return; - - const targetReligionId = cells.religion[startCell]; - const proselytismCells = []; - const queue = [startCell]; - const maxCells = rand(10, 30); - - while (queue.length) { - const cellId = queue.shift(); - proselytismCells.push(cellId); - if (proselytismCells.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId]) return; - if (cells.religion[neibCellId] !== targetReligionId) return; - if (cells.h[neibCellId] < 20 || !cells.pop[i]) return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - const name = `${getAdjective(religion.name.split(" ")[0])} Proselytism`; - pack.zones.push({i: pack.zones.length, name, type: "Proselytism", cells: proselytismCells, color: "url(#hatch6)"}); - } - - function addCrusade(usedCells) { - const {cells, religions} = pack; - - const heresies = religions.filter(r => !r.removed && r.type === "Heresy"); - if (!heresies.length) return; - - const heresy = ra(heresies); - const crusadeCells = cells.i.filter(i => !usedCells[i] && cells.religion[i] === heresy.i); - if (!crusadeCells.length) return; - crusadeCells.forEach(i => (usedCells[i] = 1)); - - const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade"; - pack.zones.push({ - i: pack.zones.length, - name, - type: "Crusade", - cells: Array.from(crusadeCells), - color: "url(#hatch6)" - }); - } - - function addDisease(usedCells) { - const {cells, burgs} = pack; - - const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); // random burg - if (!burg) return; - - const cellsArray = []; - const cost = []; - const maxCells = rand(20, 40); - - const queue = new FlatQueue(); - queue.push({e: burg.cell, p: 0}, 0); - - while (queue.length) { - const next = queue.pop(); - if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); - usedCells[next.e] = 1; - - cells.c[next.e].forEach(nextCellId => { - const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100; - const p = next.p + c; - if (p > maxCells) return; - - if (!cost[nextCellId] || p < cost[nextCellId]) { - cost[nextCellId] = p; - queue.push({e: nextCellId, p}, p); - } - }); - } - - // prettier-ignore - const name = `${(() => { - const model = rw({color: 2, animal: 1, adjective: 1}); - if (model === "color") return ra(["Amber", "Azure", "Black", "Blue", "Brown", "Crimson", "Emerald", "Golden", "Green", "Grey", "Orange", "Pink", "Purple", "Red", "Ruby", "Scarlet", "Silver", "Violet", "White", "Yellow"]); - if (model === "animal") return ra(["Ape", "Bear", "Bird", "Boar", "Cat", "Cow", "Deer", "Dog", "Fox", "Goat", "Horse", "Lion", "Pig", "Rat", "Raven", "Sheep", "Spider", "Tiger", "Viper", "Wolf", "Worm", "Wyrm"]); - if (model === "adjective") return ra(["Blind", "Bloody", "Brutal", "Burning", "Deadly", "Fatal", "Furious", "Great", "Grim", "Horrible", "Invisible", "Lethal", "Loud", "Mortal", "Savage", "Severe", "Silent", "Unknown", "Venomous", "Vicious"]); - })()} ${rw({Fever: 5, Plague: 3, Cough: 3, Flu: 2, Pox: 2, Cholera: 2, Typhoid: 2, Leprosy: 1, Smallpox: 1, Pestilence: 1, Consumption: 1, Malaria: 1, Dropsy: 1})}`; - - pack.zones.push({i: pack.zones.length, name, type: "Disease", cells: cellsArray, color: "url(#hatch12)"}); - } - - function addDisaster(usedCells) { - const {cells, burgs} = pack; - - const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); - if (!burg) return; - usedCells[burg.cell] = 1; - - const cellsArray = []; - const cost = []; - const maxCells = rand(5, 25); - - const queue = new FlatQueue(); - queue.push({e: burg.cell, p: 0}, 0); - - while (queue.length) { - const next = queue.pop(); - if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); - usedCells[next.e] = 1; - - cells.c[next.e].forEach(function (e) { - const c = rand(1, 10); - const p = next.p + c; - if (p > maxCells) return; - - if (!cost[e] || p < cost[e]) { - cost[e] = p; - queue.push({e, p}, p); - } - }); - } - - const type = rw({ - Famine: 5, - Drought: 3, - Earthquake: 3, - Dearth: 1, - Tornadoes: 1, - Wildfires: 1, - Storms: 1, - Blight: 1 - }); - const name = getAdjective(burg.name) + " " + type; - pack.zones.push({i: pack.zones.length, name, type: "Disaster", cells: cellsArray, color: "url(#hatch5)"}); - } - - function addEruption(usedCells) { - const {cells, markers} = pack; - - const volcanoe = markers.find(m => m.type === "volcanoes" && !usedCells[m.cell]); - if (!volcanoe) return; - usedCells[volcanoe.cell] = 1; - - const note = notes.find(n => n.id === "marker" + volcanoe.i); - if (note) note.legend = note.legend.replace("Active volcano", "Erupting volcano"); - const name = note ? note.name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption"; - - const cellsArray = []; - const queue = [volcanoe.cell]; - const maxCells = rand(10, 30); - - while (queue.length) { - const cellId = P(0.5) ? queue.shift() : queue.pop(); - cellsArray.push(cellId); - if (cellsArray.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId] || cells.h[neibCellId] < 20) return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - pack.zones.push({i: pack.zones.length, name, type: "Eruption", cells: cellsArray, color: "url(#hatch7)"}); - } - - function addAvalanche(usedCells) { - const {cells} = pack; - - const routeCells = cells.i.filter(i => !usedCells[i] && Routes.isConnected(i) && cells.h[i] >= 70); - if (!routeCells.length) return; - - const startCell = ra(routeCells); - usedCells[startCell] = 1; - - const cellsArray = []; - const queue = [startCell]; - const maxCells = rand(3, 15); - - while (queue.length) { - const cellId = P(0.3) ? queue.shift() : queue.pop(); - cellsArray.push(cellId); - if (cellsArray.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId] || cells.h[neibCellId] < 65) return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Avalanche"; - pack.zones.push({i: pack.zones.length, name, type: "Avalanche", cells: cellsArray, color: "url(#hatch5)"}); - } - - function addFault(usedCells) { - const cells = pack.cells; - - const elevatedCells = cells.i.filter(i => !usedCells[i] && cells.h[i] > 50 && cells.h[i] < 70); - if (!elevatedCells.length) return; - - const startCell = ra(elevatedCells); - usedCells[startCell] = 1; - - const cellsArray = []; - const queue = [startCell]; - const maxCells = rand(3, 15); - - while (queue.length) { - const cellId = queue.pop(); - if (cells.h[cellId] >= 20) cellsArray.push(cellId); - if (cellsArray.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId] || cells.r[neibCellId]) return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Fault"; - pack.zones.push({i: pack.zones.length, name, type: "Fault", cells: cellsArray, color: "url(#hatch2)"}); - } - - function addFlood(usedCells) { - const cells = pack.cells; - - const fl = cells.fl.filter(Boolean); - const meanFlux = d3.mean(fl); - const maxFlux = d3.max(fl); - const fluxThreshold = (maxFlux - meanFlux) / 2 + meanFlux; - - const bigRiverCells = cells.i.filter( - i => !usedCells[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > fluxThreshold && cells.burg[i] - ); - if (!bigRiverCells.length) return; - - const startCell = ra(bigRiverCells); - usedCells[startCell] = 1; - - const riverId = cells.r[startCell]; - const cellsArray = []; - const queue = [startCell]; - const maxCells = rand(5, 30); - - while (queue.length) { - const cellId = queue.pop(); - cellsArray.push(cellId); - if (cellsArray.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if ( - usedCells[neibCellId] || - cells.h[neibCellId] < 20 || - cells.r[neibCellId] !== riverId || - cells.h[neibCellId] > 50 || - cells.fl[neibCellId] < meanFlux - ) - return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - const name = getAdjective(pack.burgs[cells.burg[startCell]].name) + " Flood"; - pack.zones.push({i: pack.zones.length, name, type: "Flood", cells: cellsArray, color: "url(#hatch13)"}); - } - - function addTsunami(usedCells) { - const {cells, features} = pack; - - const coastalCells = cells.i.filter( - i => !usedCells[i] && cells.t[i] === -1 && features[cells.f[i]].type !== "lake" - ); - if (!coastalCells.length) return; - - const startCell = ra(coastalCells); - usedCells[startCell] = 1; - - const cellsArray = []; - const queue = [startCell]; - const maxCells = rand(10, 30); - - while (queue.length) { - const cellId = queue.shift(); - if (cells.t[cellId] === 1) cellsArray.push(cellId); - if (cellsArray.length >= maxCells) break; - - cells.c[cellId].forEach(neibCellId => { - if (usedCells[neibCellId]) return; - if (cells.t[neibCellId] > 2) return; - if (pack.features[cells.f[neibCellId]].type === "lake") return; - usedCells[neibCellId] = 1; - queue.push(neibCellId); - }); - } - - const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Tsunami"; - pack.zones.push({i: pack.zones.length, name, type: "Tsunami", cells: cellsArray, color: "url(#hatch13)"}); - } - - return {generate}; -})(); diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..2cf2371b --- /dev/null +++ b/netlify.toml @@ -0,0 +1,9 @@ +[build] + command = "npm run build" + publish = "dist" + environment = { NODE_VERSION = "24" } + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..53616e24 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2667 @@ +{ + "name": "fantasy-map-generator", + "version": "1.110.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fantasy-map-generator", + "version": "1.110.0", + "license": "MIT", + "dependencies": { + "alea": "^1.0.1", + "d3": "^7.9.0", + "delaunator": "^5.0.1", + "polylabel": "^2.0.1" + }, + "devDependencies": { + "@biomejs/biome": "2.3.13", + "@playwright/test": "^1.57.0", + "@types/d3": "^7.4.3", + "@types/delaunator": "^5.0.3", + "@types/node": "^25.0.10", + "@types/polylabel": "^1.1.3", + "@vitest/browser": "^4.0.18", + "@vitest/browser-playwright": "^4.0.18", + "playwright": "^1.57.0", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz", + "integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.13", + "@biomejs/cli-darwin-x64": "2.3.13", + "@biomejs/cli-linux-arm64": "2.3.13", + "@biomejs/cli-linux-arm64-musl": "2.3.13", + "@biomejs/cli-linux-x64": "2.3.13", + "@biomejs/cli-linux-x64-musl": "2.3.13", + "@biomejs/cli-win32-arm64": "2.3.13", + "@biomejs/cli-win32-x64": "2.3.13" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz", + "integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz", + "integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz", + "integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz", + "integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz", + "integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz", + "integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz", + "integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz", + "integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/delaunator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/delaunator/-/delaunator-5.0.3.tgz", + "integrity": "sha512-6tTLP8NX0OwtB/fmW9bXp4EWPptawTSsrSGjboWRuzqkxNEEJGyzRPHbr8wnV2DBWfAZ+EPTOvW3B/KysJrl2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/polylabel": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz", + "integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/browser": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/alea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz", + "integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/polylabel": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-2.0.1.tgz", + "integrity": "sha512-B6Yu+Bdl/8SGtjVhyUfZzD3DwciCS9SPVtHiNdt8idHHatvTHp5Ss8XGDRmQFtfF1ZQnfK+Cj5dXdpkUXBbXgA==", + "license": "ISC", + "dependencies": { + "tinyqueue": "^3.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..37c2ba5a --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "fantasy-map-generator", + "version": "1.110.0", + "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", + "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", + "bugs": { + "url": "https://github.com/Azgaar/Fantasy-Map-Generator/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Azgaar/Fantasy-Map-Generator.git" + }, + "license": "MIT", + "author": "Azgaar", + "main": "main.js", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest", + "test:browser": "vitest --config=vitest.browser.config.ts", + "test:e2e": "playwright test", + "lint": "biome check --write", + "format": "biome format --write" + }, + "devDependencies": { + "@biomejs/biome": "2.3.13", + "@playwright/test": "^1.57.0", + "@types/d3": "^7.4.3", + "@types/delaunator": "^5.0.3", + "@types/node": "^25.0.10", + "@types/polylabel": "^1.1.3", + "@vitest/browser": "^4.0.18", + "@vitest/browser-playwright": "^4.0.18", + "playwright": "^1.57.0", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18" + }, + "dependencies": { + "alea": "^1.0.1", + "d3": "^7.9.0", + "delaunator": "^5.0.1", + "polylabel": "^2.0.1" + }, + "engines": { + "node": ">=24.0.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..86348c64 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' + +const isCI = !!process.env.CI + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: 'html', + // Use OS-independent snapshot names (HTML content is the same across platforms) + snapshotPathTemplate: '{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}', + use: { + baseURL: isCI ? 'http://localhost:4173' : 'http://localhost:5173', + trace: 'on-first-retry', + // Fixed viewport to ensure consistent map rendering + viewport: { width: 1280, height: 720 }, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + // In CI: build and preview for production-like testing + // In dev: use vite dev server (faster, no rebuild needed) + command: isCI ? 'npm run build && npm run preview' : 'npm run dev', + url: isCI ? 'http://localhost:4173' : 'http://localhost:5173', + reuseExistingServer: !isCI, + timeout: 120000, + }, +}) diff --git a/charges/agnusDei.svg b/public/charges/agnusDei.svg similarity index 100% rename from charges/agnusDei.svg rename to public/charges/agnusDei.svg diff --git a/charges/anchor.svg b/public/charges/anchor.svg similarity index 100% rename from charges/anchor.svg rename to public/charges/anchor.svg diff --git a/charges/angel.svg b/public/charges/angel.svg similarity index 100% rename from charges/angel.svg rename to public/charges/angel.svg diff --git a/charges/annulet.svg b/public/charges/annulet.svg similarity index 100% rename from charges/annulet.svg rename to public/charges/annulet.svg diff --git a/charges/anvil.svg b/public/charges/anvil.svg similarity index 100% rename from charges/anvil.svg rename to public/charges/anvil.svg diff --git a/charges/apple.svg b/public/charges/apple.svg similarity index 100% rename from charges/apple.svg rename to public/charges/apple.svg diff --git a/charges/arbalest.svg b/public/charges/arbalest.svg similarity index 100% rename from charges/arbalest.svg rename to public/charges/arbalest.svg diff --git a/charges/arbalest2.svg b/public/charges/arbalest2.svg similarity index 100% rename from charges/arbalest2.svg rename to public/charges/arbalest2.svg diff --git a/charges/archer.svg b/public/charges/archer.svg similarity index 100% rename from charges/archer.svg rename to public/charges/archer.svg diff --git a/charges/armEmbowedHoldingSabre.svg b/public/charges/armEmbowedHoldingSabre.svg similarity index 100% rename from charges/armEmbowedHoldingSabre.svg rename to public/charges/armEmbowedHoldingSabre.svg diff --git a/charges/armEmbowedVambraced.svg b/public/charges/armEmbowedVambraced.svg similarity index 100% rename from charges/armEmbowedVambraced.svg rename to public/charges/armEmbowedVambraced.svg diff --git a/charges/armEmbowedVambracedHoldingSword.svg b/public/charges/armEmbowedVambracedHoldingSword.svg similarity index 100% rename from charges/armEmbowedVambracedHoldingSword.svg rename to public/charges/armEmbowedVambracedHoldingSword.svg diff --git a/charges/armillarySphere.svg b/public/charges/armillarySphere.svg similarity index 100% rename from charges/armillarySphere.svg rename to public/charges/armillarySphere.svg diff --git a/charges/arrow.svg b/public/charges/arrow.svg similarity index 100% rename from charges/arrow.svg rename to public/charges/arrow.svg diff --git a/charges/arrowsSheaf.svg b/public/charges/arrowsSheaf.svg similarity index 100% rename from charges/arrowsSheaf.svg rename to public/charges/arrowsSheaf.svg diff --git a/charges/attire.svg b/public/charges/attire.svg similarity index 100% rename from charges/attire.svg rename to public/charges/attire.svg diff --git a/charges/axe.svg b/public/charges/axe.svg similarity index 100% rename from charges/axe.svg rename to public/charges/axe.svg diff --git a/charges/badgerStatant.svg b/public/charges/badgerStatant.svg similarity index 100% rename from charges/badgerStatant.svg rename to public/charges/badgerStatant.svg diff --git a/charges/banner.svg b/public/charges/banner.svg similarity index 100% rename from charges/banner.svg rename to public/charges/banner.svg diff --git a/charges/basilisk.svg b/public/charges/basilisk.svg similarity index 100% rename from charges/basilisk.svg rename to public/charges/basilisk.svg diff --git a/charges/bearPassant.svg b/public/charges/bearPassant.svg similarity index 100% rename from charges/bearPassant.svg rename to public/charges/bearPassant.svg diff --git a/charges/bearRampant.svg b/public/charges/bearRampant.svg similarity index 100% rename from charges/bearRampant.svg rename to public/charges/bearRampant.svg diff --git a/charges/bee.svg b/public/charges/bee.svg similarity index 100% rename from charges/bee.svg rename to public/charges/bee.svg diff --git a/charges/bell.svg b/public/charges/bell.svg similarity index 100% rename from charges/bell.svg rename to public/charges/bell.svg diff --git a/charges/billet.svg b/public/charges/billet.svg similarity index 100% rename from charges/billet.svg rename to public/charges/billet.svg diff --git a/charges/boarHeadErased.svg b/public/charges/boarHeadErased.svg similarity index 100% rename from charges/boarHeadErased.svg rename to public/charges/boarHeadErased.svg diff --git a/charges/boarRampant.svg b/public/charges/boarRampant.svg similarity index 100% rename from charges/boarRampant.svg rename to public/charges/boarRampant.svg diff --git a/charges/boat.svg b/public/charges/boat.svg similarity index 100% rename from charges/boat.svg rename to public/charges/boat.svg diff --git a/charges/boat2.svg b/public/charges/boat2.svg similarity index 100% rename from charges/boat2.svg rename to public/charges/boat2.svg diff --git a/charges/bone.svg b/public/charges/bone.svg similarity index 100% rename from charges/bone.svg rename to public/charges/bone.svg diff --git a/charges/bookClosed.svg b/public/charges/bookClosed.svg similarity index 100% rename from charges/bookClosed.svg rename to public/charges/bookClosed.svg diff --git a/charges/bookClosed2.svg b/public/charges/bookClosed2.svg similarity index 100% rename from charges/bookClosed2.svg rename to public/charges/bookClosed2.svg diff --git a/charges/bookOpen.svg b/public/charges/bookOpen.svg similarity index 100% rename from charges/bookOpen.svg rename to public/charges/bookOpen.svg diff --git a/charges/bow.svg b/public/charges/bow.svg similarity index 100% rename from charges/bow.svg rename to public/charges/bow.svg diff --git a/charges/bowWithArrow.svg b/public/charges/bowWithArrow.svg similarity index 100% rename from charges/bowWithArrow.svg rename to public/charges/bowWithArrow.svg diff --git a/charges/bowWithThreeArrows.svg b/public/charges/bowWithThreeArrows.svg similarity index 100% rename from charges/bowWithThreeArrows.svg rename to public/charges/bowWithThreeArrows.svg diff --git a/charges/bridge.svg b/public/charges/bridge.svg similarity index 100% rename from charges/bridge.svg rename to public/charges/bridge.svg diff --git a/charges/bridge2.svg b/public/charges/bridge2.svg similarity index 100% rename from charges/bridge2.svg rename to public/charges/bridge2.svg diff --git a/charges/bucket.svg b/public/charges/bucket.svg similarity index 100% rename from charges/bucket.svg rename to public/charges/bucket.svg diff --git a/charges/buckle.svg b/public/charges/buckle.svg similarity index 100% rename from charges/buckle.svg rename to public/charges/buckle.svg diff --git a/charges/bugleHorn.svg b/public/charges/bugleHorn.svg similarity index 100% rename from charges/bugleHorn.svg rename to public/charges/bugleHorn.svg diff --git a/charges/bugleHorn2.svg b/public/charges/bugleHorn2.svg similarity index 100% rename from charges/bugleHorn2.svg rename to public/charges/bugleHorn2.svg diff --git a/charges/bullHeadCaboshed.svg b/public/charges/bullHeadCaboshed.svg similarity index 100% rename from charges/bullHeadCaboshed.svg rename to public/charges/bullHeadCaboshed.svg diff --git a/charges/bullPassant.svg b/public/charges/bullPassant.svg similarity index 100% rename from charges/bullPassant.svg rename to public/charges/bullPassant.svg diff --git a/charges/butterfly.svg b/public/charges/butterfly.svg similarity index 100% rename from charges/butterfly.svg rename to public/charges/butterfly.svg diff --git a/charges/camel.svg b/public/charges/camel.svg similarity index 100% rename from charges/camel.svg rename to public/charges/camel.svg diff --git a/charges/cancer.svg b/public/charges/cancer.svg similarity index 100% rename from charges/cancer.svg rename to public/charges/cancer.svg diff --git a/charges/cannon.svg b/public/charges/cannon.svg similarity index 100% rename from charges/cannon.svg rename to public/charges/cannon.svg diff --git a/charges/caravel.svg b/public/charges/caravel.svg similarity index 100% rename from charges/caravel.svg rename to public/charges/caravel.svg diff --git a/charges/carreau.svg b/public/charges/carreau.svg similarity index 100% rename from charges/carreau.svg rename to public/charges/carreau.svg diff --git a/charges/castle.svg b/public/charges/castle.svg similarity index 100% rename from charges/castle.svg rename to public/charges/castle.svg diff --git a/charges/castle2.svg b/public/charges/castle2.svg similarity index 100% rename from charges/castle2.svg rename to public/charges/castle2.svg diff --git a/charges/catPassantGuardant.svg b/public/charges/catPassantGuardant.svg similarity index 100% rename from charges/catPassantGuardant.svg rename to public/charges/catPassantGuardant.svg diff --git a/charges/cavalier.svg b/public/charges/cavalier.svg similarity index 100% rename from charges/cavalier.svg rename to public/charges/cavalier.svg diff --git a/charges/centaur.svg b/public/charges/centaur.svg similarity index 100% rename from charges/centaur.svg rename to public/charges/centaur.svg diff --git a/charges/chain.svg b/public/charges/chain.svg similarity index 100% rename from charges/chain.svg rename to public/charges/chain.svg diff --git a/charges/chalice.svg b/public/charges/chalice.svg similarity index 100% rename from charges/chalice.svg rename to public/charges/chalice.svg diff --git a/charges/cinquefoil.svg b/public/charges/cinquefoil.svg similarity index 100% rename from charges/cinquefoil.svg rename to public/charges/cinquefoil.svg diff --git a/charges/cock.svg b/public/charges/cock.svg similarity index 100% rename from charges/cock.svg rename to public/charges/cock.svg diff --git a/charges/column.svg b/public/charges/column.svg similarity index 100% rename from charges/column.svg rename to public/charges/column.svg diff --git a/charges/comet.svg b/public/charges/comet.svg similarity index 100% rename from charges/comet.svg rename to public/charges/comet.svg diff --git a/charges/compassRose.svg b/public/charges/compassRose.svg similarity index 100% rename from charges/compassRose.svg rename to public/charges/compassRose.svg diff --git a/charges/cossack.svg b/public/charges/cossack.svg similarity index 100% rename from charges/cossack.svg rename to public/charges/cossack.svg diff --git a/charges/cowHorns.svg b/public/charges/cowHorns.svg similarity index 100% rename from charges/cowHorns.svg rename to public/charges/cowHorns.svg diff --git a/charges/cowStatant.svg b/public/charges/cowStatant.svg similarity index 100% rename from charges/cowStatant.svg rename to public/charges/cowStatant.svg diff --git a/charges/crescent.svg b/public/charges/crescent.svg similarity index 100% rename from charges/crescent.svg rename to public/charges/crescent.svg diff --git a/charges/crocodile.svg b/public/charges/crocodile.svg similarity index 100% rename from charges/crocodile.svg rename to public/charges/crocodile.svg diff --git a/charges/crosier.svg b/public/charges/crosier.svg similarity index 100% rename from charges/crosier.svg rename to public/charges/crosier.svg diff --git a/charges/crossAnkh.svg b/public/charges/crossAnkh.svg similarity index 100% rename from charges/crossAnkh.svg rename to public/charges/crossAnkh.svg diff --git a/charges/crossArrowed.svg b/public/charges/crossArrowed.svg similarity index 100% rename from charges/crossArrowed.svg rename to public/charges/crossArrowed.svg diff --git a/charges/crossAvellane.svg b/public/charges/crossAvellane.svg similarity index 100% rename from charges/crossAvellane.svg rename to public/charges/crossAvellane.svg diff --git a/charges/crossBiparted.svg b/public/charges/crossBiparted.svg similarity index 100% rename from charges/crossBiparted.svg rename to public/charges/crossBiparted.svg diff --git a/charges/crossBottony.svg b/public/charges/crossBottony.svg similarity index 100% rename from charges/crossBottony.svg rename to public/charges/crossBottony.svg diff --git a/charges/crossBurgundy.svg b/public/charges/crossBurgundy.svg similarity index 100% rename from charges/crossBurgundy.svg rename to public/charges/crossBurgundy.svg diff --git a/charges/crossCalvary.svg b/public/charges/crossCalvary.svg similarity index 100% rename from charges/crossCalvary.svg rename to public/charges/crossCalvary.svg diff --git a/charges/crossCarolingian.svg b/public/charges/crossCarolingian.svg similarity index 100% rename from charges/crossCarolingian.svg rename to public/charges/crossCarolingian.svg diff --git a/charges/crossCeltic.svg b/public/charges/crossCeltic.svg similarity index 100% rename from charges/crossCeltic.svg rename to public/charges/crossCeltic.svg diff --git a/charges/crossCeltic2.svg b/public/charges/crossCeltic2.svg similarity index 100% rename from charges/crossCeltic2.svg rename to public/charges/crossCeltic2.svg diff --git a/charges/crossCercelee.svg b/public/charges/crossCercelee.svg similarity index 100% rename from charges/crossCercelee.svg rename to public/charges/crossCercelee.svg diff --git a/charges/crossClechy.svg b/public/charges/crossClechy.svg similarity index 100% rename from charges/crossClechy.svg rename to public/charges/crossClechy.svg diff --git a/charges/crossDouble.svg b/public/charges/crossDouble.svg similarity index 100% rename from charges/crossDouble.svg rename to public/charges/crossDouble.svg diff --git a/charges/crossErminee.svg b/public/charges/crossErminee.svg similarity index 100% rename from charges/crossErminee.svg rename to public/charges/crossErminee.svg diff --git a/charges/crossFitchy.svg b/public/charges/crossFitchy.svg similarity index 100% rename from charges/crossFitchy.svg rename to public/charges/crossFitchy.svg diff --git a/charges/crossFleury.svg b/public/charges/crossFleury.svg similarity index 100% rename from charges/crossFleury.svg rename to public/charges/crossFleury.svg diff --git a/charges/crossFormee.svg b/public/charges/crossFormee.svg similarity index 100% rename from charges/crossFormee.svg rename to public/charges/crossFormee.svg diff --git a/charges/crossFormee2.svg b/public/charges/crossFormee2.svg similarity index 100% rename from charges/crossFormee2.svg rename to public/charges/crossFormee2.svg diff --git a/charges/crossFourchy.svg b/public/charges/crossFourchy.svg similarity index 100% rename from charges/crossFourchy.svg rename to public/charges/crossFourchy.svg diff --git a/charges/crossGamma.svg b/public/charges/crossGamma.svg similarity index 100% rename from charges/crossGamma.svg rename to public/charges/crossGamma.svg diff --git a/charges/crossHummetty.svg b/public/charges/crossHummetty.svg similarity index 100% rename from charges/crossHummetty.svg rename to public/charges/crossHummetty.svg diff --git a/charges/crossJerusalem.svg b/public/charges/crossJerusalem.svg similarity index 100% rename from charges/crossJerusalem.svg rename to public/charges/crossJerusalem.svg diff --git a/charges/crossLatin.svg b/public/charges/crossLatin.svg similarity index 100% rename from charges/crossLatin.svg rename to public/charges/crossLatin.svg diff --git a/charges/crossMaltese.svg b/public/charges/crossMaltese.svg similarity index 100% rename from charges/crossMaltese.svg rename to public/charges/crossMaltese.svg diff --git a/charges/crossMoline.svg b/public/charges/crossMoline.svg similarity index 100% rename from charges/crossMoline.svg rename to public/charges/crossMoline.svg diff --git a/charges/crossOccitan.svg b/public/charges/crossOccitan.svg similarity index 100% rename from charges/crossOccitan.svg rename to public/charges/crossOccitan.svg diff --git a/charges/crossOrthodox.svg b/public/charges/crossOrthodox.svg similarity index 100% rename from charges/crossOrthodox.svg rename to public/charges/crossOrthodox.svg diff --git a/charges/crossPatonce.svg b/public/charges/crossPatonce.svg similarity index 100% rename from charges/crossPatonce.svg rename to public/charges/crossPatonce.svg diff --git a/charges/crossPatriarchal.svg b/public/charges/crossPatriarchal.svg similarity index 100% rename from charges/crossPatriarchal.svg rename to public/charges/crossPatriarchal.svg diff --git a/charges/crossPattee.svg b/public/charges/crossPattee.svg similarity index 100% rename from charges/crossPattee.svg rename to public/charges/crossPattee.svg diff --git a/charges/crossPatteeAlisee.svg b/public/charges/crossPatteeAlisee.svg similarity index 100% rename from charges/crossPatteeAlisee.svg rename to public/charges/crossPatteeAlisee.svg diff --git a/charges/crossPommy.svg b/public/charges/crossPommy.svg similarity index 100% rename from charges/crossPommy.svg rename to public/charges/crossPommy.svg diff --git a/charges/crossPotent.svg b/public/charges/crossPotent.svg similarity index 100% rename from charges/crossPotent.svg rename to public/charges/crossPotent.svg diff --git a/charges/crossSaltire.svg b/public/charges/crossSaltire.svg similarity index 100% rename from charges/crossSaltire.svg rename to public/charges/crossSaltire.svg diff --git a/charges/crossSantiago.svg b/public/charges/crossSantiago.svg similarity index 100% rename from charges/crossSantiago.svg rename to public/charges/crossSantiago.svg diff --git a/charges/crossTau.svg b/public/charges/crossTau.svg similarity index 100% rename from charges/crossTau.svg rename to public/charges/crossTau.svg diff --git a/charges/crossTemplar.svg b/public/charges/crossTemplar.svg similarity index 100% rename from charges/crossTemplar.svg rename to public/charges/crossTemplar.svg diff --git a/charges/crossTriquetra.svg b/public/charges/crossTriquetra.svg similarity index 100% rename from charges/crossTriquetra.svg rename to public/charges/crossTriquetra.svg diff --git a/charges/crossVoided.svg b/public/charges/crossVoided.svg similarity index 100% rename from charges/crossVoided.svg rename to public/charges/crossVoided.svg diff --git a/charges/crossedBones.svg b/public/charges/crossedBones.svg similarity index 100% rename from charges/crossedBones.svg rename to public/charges/crossedBones.svg diff --git a/charges/crosslet.svg b/public/charges/crosslet.svg similarity index 100% rename from charges/crosslet.svg rename to public/charges/crosslet.svg diff --git a/charges/crown.svg b/public/charges/crown.svg similarity index 100% rename from charges/crown.svg rename to public/charges/crown.svg diff --git a/charges/crown2.svg b/public/charges/crown2.svg similarity index 100% rename from charges/crown2.svg rename to public/charges/crown2.svg diff --git a/charges/deerHeadCaboshed.svg b/public/charges/deerHeadCaboshed.svg similarity index 100% rename from charges/deerHeadCaboshed.svg rename to public/charges/deerHeadCaboshed.svg diff --git a/charges/delf.svg b/public/charges/delf.svg similarity index 100% rename from charges/delf.svg rename to public/charges/delf.svg diff --git a/charges/dolphin.svg b/public/charges/dolphin.svg similarity index 100% rename from charges/dolphin.svg rename to public/charges/dolphin.svg diff --git a/charges/donkeyHeadCaboshed.svg b/public/charges/donkeyHeadCaboshed.svg similarity index 100% rename from charges/donkeyHeadCaboshed.svg rename to public/charges/donkeyHeadCaboshed.svg diff --git a/charges/dove.svg b/public/charges/dove.svg similarity index 100% rename from charges/dove.svg rename to public/charges/dove.svg diff --git a/charges/doveDisplayed.svg b/public/charges/doveDisplayed.svg similarity index 100% rename from charges/doveDisplayed.svg rename to public/charges/doveDisplayed.svg diff --git a/charges/dragonPassant.svg b/public/charges/dragonPassant.svg similarity index 100% rename from charges/dragonPassant.svg rename to public/charges/dragonPassant.svg diff --git a/charges/dragonRampant.svg b/public/charges/dragonRampant.svg similarity index 100% rename from charges/dragonRampant.svg rename to public/charges/dragonRampant.svg diff --git a/charges/dragonfly.svg b/public/charges/dragonfly.svg similarity index 100% rename from charges/dragonfly.svg rename to public/charges/dragonfly.svg diff --git a/charges/drakkar.svg b/public/charges/drakkar.svg similarity index 100% rename from charges/drakkar.svg rename to public/charges/drakkar.svg diff --git a/charges/drawingCompass.svg b/public/charges/drawingCompass.svg similarity index 100% rename from charges/drawingCompass.svg rename to public/charges/drawingCompass.svg diff --git a/charges/drum.svg b/public/charges/drum.svg similarity index 100% rename from charges/drum.svg rename to public/charges/drum.svg diff --git a/charges/duck.svg b/public/charges/duck.svg similarity index 100% rename from charges/duck.svg rename to public/charges/duck.svg diff --git a/charges/eagle.svg b/public/charges/eagle.svg similarity index 100% rename from charges/eagle.svg rename to public/charges/eagle.svg diff --git a/charges/eagleTwoHeads.svg b/public/charges/eagleTwoHeads.svg similarity index 100% rename from charges/eagleTwoHeads.svg rename to public/charges/eagleTwoHeads.svg diff --git a/charges/earOfWheat.svg b/public/charges/earOfWheat.svg similarity index 100% rename from charges/earOfWheat.svg rename to public/charges/earOfWheat.svg diff --git a/charges/elephant.svg b/public/charges/elephant.svg similarity index 100% rename from charges/elephant.svg rename to public/charges/elephant.svg diff --git a/charges/elephantHeadErased.svg b/public/charges/elephantHeadErased.svg similarity index 100% rename from charges/elephantHeadErased.svg rename to public/charges/elephantHeadErased.svg diff --git a/charges/escallop.svg b/public/charges/escallop.svg similarity index 100% rename from charges/escallop.svg rename to public/charges/escallop.svg diff --git a/charges/estoile.svg b/public/charges/estoile.svg similarity index 100% rename from charges/estoile.svg rename to public/charges/estoile.svg diff --git a/charges/falchion.svg b/public/charges/falchion.svg similarity index 100% rename from charges/falchion.svg rename to public/charges/falchion.svg diff --git a/charges/falcon.svg b/public/charges/falcon.svg similarity index 100% rename from charges/falcon.svg rename to public/charges/falcon.svg diff --git a/charges/fan.svg b/public/charges/fan.svg similarity index 100% rename from charges/fan.svg rename to public/charges/fan.svg diff --git a/charges/fasces.svg b/public/charges/fasces.svg similarity index 100% rename from charges/fasces.svg rename to public/charges/fasces.svg diff --git a/charges/feather.svg b/public/charges/feather.svg similarity index 100% rename from charges/feather.svg rename to public/charges/feather.svg diff --git a/charges/flamberge.svg b/public/charges/flamberge.svg similarity index 100% rename from charges/flamberge.svg rename to public/charges/flamberge.svg diff --git a/charges/flangedMace.svg b/public/charges/flangedMace.svg similarity index 100% rename from charges/flangedMace.svg rename to public/charges/flangedMace.svg diff --git a/charges/fleurDeLis.svg b/public/charges/fleurDeLis.svg similarity index 100% rename from charges/fleurDeLis.svg rename to public/charges/fleurDeLis.svg diff --git a/charges/fly.svg b/public/charges/fly.svg similarity index 100% rename from charges/fly.svg rename to public/charges/fly.svg diff --git a/charges/foot.svg b/public/charges/foot.svg similarity index 100% rename from charges/foot.svg rename to public/charges/foot.svg diff --git a/charges/fountain.svg b/public/charges/fountain.svg similarity index 100% rename from charges/fountain.svg rename to public/charges/fountain.svg diff --git a/charges/frog.svg b/public/charges/frog.svg similarity index 100% rename from charges/frog.svg rename to public/charges/frog.svg diff --git a/charges/fusil.svg b/public/charges/fusil.svg similarity index 100% rename from charges/fusil.svg rename to public/charges/fusil.svg diff --git a/charges/garb.svg b/public/charges/garb.svg similarity index 100% rename from charges/garb.svg rename to public/charges/garb.svg diff --git a/charges/gauntlet.svg b/public/charges/gauntlet.svg similarity index 100% rename from charges/gauntlet.svg rename to public/charges/gauntlet.svg diff --git a/charges/gear.svg b/public/charges/gear.svg similarity index 100% rename from charges/gear.svg rename to public/charges/gear.svg diff --git a/charges/goat.svg b/public/charges/goat.svg similarity index 100% rename from charges/goat.svg rename to public/charges/goat.svg diff --git a/charges/goutte.svg b/public/charges/goutte.svg similarity index 100% rename from charges/goutte.svg rename to public/charges/goutte.svg diff --git a/charges/grapeBunch.svg b/public/charges/grapeBunch.svg similarity index 100% rename from charges/grapeBunch.svg rename to public/charges/grapeBunch.svg diff --git a/charges/grapeBunch2.svg b/public/charges/grapeBunch2.svg similarity index 100% rename from charges/grapeBunch2.svg rename to public/charges/grapeBunch2.svg diff --git a/charges/grenade.svg b/public/charges/grenade.svg similarity index 100% rename from charges/grenade.svg rename to public/charges/grenade.svg diff --git a/charges/greyhoundCourant.svg b/public/charges/greyhoundCourant.svg similarity index 100% rename from charges/greyhoundCourant.svg rename to public/charges/greyhoundCourant.svg diff --git a/charges/greyhoundRampant.svg b/public/charges/greyhoundRampant.svg similarity index 100% rename from charges/greyhoundRampant.svg rename to public/charges/greyhoundRampant.svg diff --git a/charges/greyhoundSejant.svg b/public/charges/greyhoundSejant.svg similarity index 100% rename from charges/greyhoundSejant.svg rename to public/charges/greyhoundSejant.svg diff --git a/charges/griffinPassant.svg b/public/charges/griffinPassant.svg similarity index 100% rename from charges/griffinPassant.svg rename to public/charges/griffinPassant.svg diff --git a/charges/griffinRampant.svg b/public/charges/griffinRampant.svg similarity index 100% rename from charges/griffinRampant.svg rename to public/charges/griffinRampant.svg diff --git a/charges/hand.svg b/public/charges/hand.svg similarity index 100% rename from charges/hand.svg rename to public/charges/hand.svg diff --git a/charges/harp.svg b/public/charges/harp.svg similarity index 100% rename from charges/harp.svg rename to public/charges/harp.svg diff --git a/charges/hatchet.svg b/public/charges/hatchet.svg similarity index 100% rename from charges/hatchet.svg rename to public/charges/hatchet.svg diff --git a/charges/head.svg b/public/charges/head.svg similarity index 100% rename from charges/head.svg rename to public/charges/head.svg diff --git a/charges/headWreathed.svg b/public/charges/headWreathed.svg similarity index 100% rename from charges/headWreathed.svg rename to public/charges/headWreathed.svg diff --git a/charges/heart.svg b/public/charges/heart.svg similarity index 100% rename from charges/heart.svg rename to public/charges/heart.svg diff --git a/charges/hedgehog.svg b/public/charges/hedgehog.svg similarity index 100% rename from charges/hedgehog.svg rename to public/charges/hedgehog.svg diff --git a/charges/helmet.svg b/public/charges/helmet.svg similarity index 100% rename from charges/helmet.svg rename to public/charges/helmet.svg diff --git a/charges/helmetCorinthian.svg b/public/charges/helmetCorinthian.svg similarity index 100% rename from charges/helmetCorinthian.svg rename to public/charges/helmetCorinthian.svg diff --git a/charges/helmetGreat.svg b/public/charges/helmetGreat.svg similarity index 100% rename from charges/helmetGreat.svg rename to public/charges/helmetGreat.svg diff --git a/charges/helmetZischagge.svg b/public/charges/helmetZischagge.svg similarity index 100% rename from charges/helmetZischagge.svg rename to public/charges/helmetZischagge.svg diff --git a/charges/heron.svg b/public/charges/heron.svg similarity index 100% rename from charges/heron.svg rename to public/charges/heron.svg diff --git a/charges/hindStatant.svg b/public/charges/hindStatant.svg similarity index 100% rename from charges/hindStatant.svg rename to public/charges/hindStatant.svg diff --git a/charges/hook.svg b/public/charges/hook.svg similarity index 100% rename from charges/hook.svg rename to public/charges/hook.svg diff --git a/charges/horseHeadCouped.svg b/public/charges/horseHeadCouped.svg similarity index 100% rename from charges/horseHeadCouped.svg rename to public/charges/horseHeadCouped.svg diff --git a/charges/horsePassant.svg b/public/charges/horsePassant.svg similarity index 100% rename from charges/horsePassant.svg rename to public/charges/horsePassant.svg diff --git a/charges/horseRampant.svg b/public/charges/horseRampant.svg similarity index 100% rename from charges/horseRampant.svg rename to public/charges/horseRampant.svg diff --git a/charges/horseSalient.svg b/public/charges/horseSalient.svg similarity index 100% rename from charges/horseSalient.svg rename to public/charges/horseSalient.svg diff --git a/charges/horseshoe.svg b/public/charges/horseshoe.svg similarity index 100% rename from charges/horseshoe.svg rename to public/charges/horseshoe.svg diff --git a/charges/hourglass.svg b/public/charges/hourglass.svg similarity index 100% rename from charges/hourglass.svg rename to public/charges/hourglass.svg diff --git a/charges/key.svg b/public/charges/key.svg similarity index 100% rename from charges/key.svg rename to public/charges/key.svg diff --git a/charges/ladder.svg b/public/charges/ladder.svg similarity index 100% rename from charges/ladder.svg rename to public/charges/ladder.svg diff --git a/charges/ladder2.svg b/public/charges/ladder2.svg similarity index 100% rename from charges/ladder2.svg rename to public/charges/ladder2.svg diff --git a/charges/ladybird.svg b/public/charges/ladybird.svg similarity index 100% rename from charges/ladybird.svg rename to public/charges/ladybird.svg diff --git a/charges/lamb.svg b/public/charges/lamb.svg similarity index 100% rename from charges/lamb.svg rename to public/charges/lamb.svg diff --git a/charges/lambPassantReguardant.svg b/public/charges/lambPassantReguardant.svg similarity index 100% rename from charges/lambPassantReguardant.svg rename to public/charges/lambPassantReguardant.svg diff --git a/charges/lanceHead.svg b/public/charges/lanceHead.svg similarity index 100% rename from charges/lanceHead.svg rename to public/charges/lanceHead.svg diff --git a/charges/lanceWithBanner.svg b/public/charges/lanceWithBanner.svg similarity index 100% rename from charges/lanceWithBanner.svg rename to public/charges/lanceWithBanner.svg diff --git a/charges/laurelWreath.svg b/public/charges/laurelWreath.svg similarity index 100% rename from charges/laurelWreath.svg rename to public/charges/laurelWreath.svg diff --git a/charges/laurelWreath2.svg b/public/charges/laurelWreath2.svg similarity index 100% rename from charges/laurelWreath2.svg rename to public/charges/laurelWreath2.svg diff --git a/charges/lighthouse.svg b/public/charges/lighthouse.svg similarity index 100% rename from charges/lighthouse.svg rename to public/charges/lighthouse.svg diff --git a/charges/lionHeadCaboshed.svg b/public/charges/lionHeadCaboshed.svg similarity index 100% rename from charges/lionHeadCaboshed.svg rename to public/charges/lionHeadCaboshed.svg diff --git a/charges/lionHeadErased.svg b/public/charges/lionHeadErased.svg similarity index 100% rename from charges/lionHeadErased.svg rename to public/charges/lionHeadErased.svg diff --git a/charges/lionPassant.svg b/public/charges/lionPassant.svg similarity index 100% rename from charges/lionPassant.svg rename to public/charges/lionPassant.svg diff --git a/charges/lionPassantGuardant.svg b/public/charges/lionPassantGuardant.svg similarity index 100% rename from charges/lionPassantGuardant.svg rename to public/charges/lionPassantGuardant.svg diff --git a/charges/lionRampant.svg b/public/charges/lionRampant.svg similarity index 100% rename from charges/lionRampant.svg rename to public/charges/lionRampant.svg diff --git a/charges/lionSejant.svg b/public/charges/lionSejant.svg similarity index 100% rename from charges/lionSejant.svg rename to public/charges/lionSejant.svg diff --git a/charges/lizard.svg b/public/charges/lizard.svg similarity index 100% rename from charges/lizard.svg rename to public/charges/lizard.svg diff --git a/charges/lochaberAxe.svg b/public/charges/lochaberAxe.svg similarity index 100% rename from charges/lochaberAxe.svg rename to public/charges/lochaberAxe.svg diff --git a/charges/log.svg b/public/charges/log.svg similarity index 100% rename from charges/log.svg rename to public/charges/log.svg diff --git a/charges/lozenge.svg b/public/charges/lozenge.svg similarity index 100% rename from charges/lozenge.svg rename to public/charges/lozenge.svg diff --git a/charges/lozengeFaceted.svg b/public/charges/lozengeFaceted.svg similarity index 100% rename from charges/lozengeFaceted.svg rename to public/charges/lozengeFaceted.svg diff --git a/charges/lozengePloye.svg b/public/charges/lozengePloye.svg similarity index 100% rename from charges/lozengePloye.svg rename to public/charges/lozengePloye.svg diff --git a/charges/lute.svg b/public/charges/lute.svg similarity index 100% rename from charges/lute.svg rename to public/charges/lute.svg diff --git a/charges/lymphad.svg b/public/charges/lymphad.svg similarity index 100% rename from charges/lymphad.svg rename to public/charges/lymphad.svg diff --git a/charges/lyre.svg b/public/charges/lyre.svg similarity index 100% rename from charges/lyre.svg rename to public/charges/lyre.svg diff --git a/charges/mace.svg b/public/charges/mace.svg similarity index 100% rename from charges/mace.svg rename to public/charges/mace.svg diff --git a/charges/maces.svg b/public/charges/maces.svg similarity index 100% rename from charges/maces.svg rename to public/charges/maces.svg diff --git a/charges/mallet.svg b/public/charges/mallet.svg similarity index 100% rename from charges/mallet.svg rename to public/charges/mallet.svg diff --git a/charges/mantle.svg b/public/charges/mantle.svg similarity index 100% rename from charges/mantle.svg rename to public/charges/mantle.svg diff --git a/charges/mapleLeaf.svg b/public/charges/mapleLeaf.svg similarity index 100% rename from charges/mapleLeaf.svg rename to public/charges/mapleLeaf.svg diff --git a/charges/martenCourant.svg b/public/charges/martenCourant.svg similarity index 100% rename from charges/martenCourant.svg rename to public/charges/martenCourant.svg diff --git a/charges/mascle.svg b/public/charges/mascle.svg similarity index 100% rename from charges/mascle.svg rename to public/charges/mascle.svg diff --git a/charges/mastiffStatant.svg b/public/charges/mastiffStatant.svg similarity index 100% rename from charges/mastiffStatant.svg rename to public/charges/mastiffStatant.svg diff --git a/charges/millstone.svg b/public/charges/millstone.svg similarity index 100% rename from charges/millstone.svg rename to public/charges/millstone.svg diff --git a/charges/mitre.svg b/public/charges/mitre.svg similarity index 100% rename from charges/mitre.svg rename to public/charges/mitre.svg diff --git a/charges/monk.svg b/public/charges/monk.svg similarity index 100% rename from charges/monk.svg rename to public/charges/monk.svg diff --git a/charges/moonInCrescent.svg b/public/charges/moonInCrescent.svg similarity index 100% rename from charges/moonInCrescent.svg rename to public/charges/moonInCrescent.svg diff --git a/charges/mullet.svg b/public/charges/mullet.svg similarity index 100% rename from charges/mullet.svg rename to public/charges/mullet.svg diff --git a/charges/mullet10.svg b/public/charges/mullet10.svg similarity index 100% rename from charges/mullet10.svg rename to public/charges/mullet10.svg diff --git a/charges/mullet4.svg b/public/charges/mullet4.svg similarity index 100% rename from charges/mullet4.svg rename to public/charges/mullet4.svg diff --git a/charges/mullet6.svg b/public/charges/mullet6.svg similarity index 100% rename from charges/mullet6.svg rename to public/charges/mullet6.svg diff --git a/charges/mullet6Faceted.svg b/public/charges/mullet6Faceted.svg similarity index 100% rename from charges/mullet6Faceted.svg rename to public/charges/mullet6Faceted.svg diff --git a/charges/mullet6Pierced.svg b/public/charges/mullet6Pierced.svg similarity index 100% rename from charges/mullet6Pierced.svg rename to public/charges/mullet6Pierced.svg diff --git a/charges/mullet7.svg b/public/charges/mullet7.svg similarity index 100% rename from charges/mullet7.svg rename to public/charges/mullet7.svg diff --git a/charges/mullet8.svg b/public/charges/mullet8.svg similarity index 100% rename from charges/mullet8.svg rename to public/charges/mullet8.svg diff --git a/charges/mulletFaceted.svg b/public/charges/mulletFaceted.svg similarity index 100% rename from charges/mulletFaceted.svg rename to public/charges/mulletFaceted.svg diff --git a/charges/mulletPierced.svg b/public/charges/mulletPierced.svg similarity index 100% rename from charges/mulletPierced.svg rename to public/charges/mulletPierced.svg diff --git a/charges/oak.svg b/public/charges/oak.svg similarity index 100% rename from charges/oak.svg rename to public/charges/oak.svg diff --git a/charges/orb.svg b/public/charges/orb.svg similarity index 100% rename from charges/orb.svg rename to public/charges/orb.svg diff --git a/charges/ouroboros.svg b/public/charges/ouroboros.svg similarity index 100% rename from charges/ouroboros.svg rename to public/charges/ouroboros.svg diff --git a/charges/owl.svg b/public/charges/owl.svg similarity index 100% rename from charges/owl.svg rename to public/charges/owl.svg diff --git a/charges/owlDisplayed.svg b/public/charges/owlDisplayed.svg similarity index 100% rename from charges/owlDisplayed.svg rename to public/charges/owlDisplayed.svg diff --git a/charges/palace.svg b/public/charges/palace.svg similarity index 100% rename from charges/palace.svg rename to public/charges/palace.svg diff --git a/charges/palmTree.svg b/public/charges/palmTree.svg similarity index 100% rename from charges/palmTree.svg rename to public/charges/palmTree.svg diff --git a/charges/parrot.svg b/public/charges/parrot.svg similarity index 100% rename from charges/parrot.svg rename to public/charges/parrot.svg diff --git a/charges/peacock.svg b/public/charges/peacock.svg similarity index 100% rename from charges/peacock.svg rename to public/charges/peacock.svg diff --git a/charges/peacockInPride.svg b/public/charges/peacockInPride.svg similarity index 100% rename from charges/peacockInPride.svg rename to public/charges/peacockInPride.svg diff --git a/charges/pear.svg b/public/charges/pear.svg similarity index 100% rename from charges/pear.svg rename to public/charges/pear.svg diff --git a/charges/pegasus.svg b/public/charges/pegasus.svg similarity index 100% rename from charges/pegasus.svg rename to public/charges/pegasus.svg diff --git a/charges/pike.svg b/public/charges/pike.svg similarity index 100% rename from charges/pike.svg rename to public/charges/pike.svg diff --git a/charges/pillar.svg b/public/charges/pillar.svg similarity index 100% rename from charges/pillar.svg rename to public/charges/pillar.svg diff --git a/charges/pincers.svg b/public/charges/pincers.svg similarity index 100% rename from charges/pincers.svg rename to public/charges/pincers.svg diff --git a/charges/pineCone.svg b/public/charges/pineCone.svg similarity index 100% rename from charges/pineCone.svg rename to public/charges/pineCone.svg diff --git a/charges/pineTree.svg b/public/charges/pineTree.svg similarity index 100% rename from charges/pineTree.svg rename to public/charges/pineTree.svg diff --git a/charges/pique.svg b/public/charges/pique.svg similarity index 100% rename from charges/pique.svg rename to public/charges/pique.svg diff --git a/charges/plaice.svg b/public/charges/plaice.svg similarity index 100% rename from charges/plaice.svg rename to public/charges/plaice.svg diff --git a/charges/plough.svg b/public/charges/plough.svg similarity index 100% rename from charges/plough.svg rename to public/charges/plough.svg diff --git a/charges/ploughshare.svg b/public/charges/ploughshare.svg similarity index 100% rename from charges/ploughshare.svg rename to public/charges/ploughshare.svg diff --git a/charges/porcupine.svg b/public/charges/porcupine.svg similarity index 100% rename from charges/porcupine.svg rename to public/charges/porcupine.svg diff --git a/charges/portcullis.svg b/public/charges/portcullis.svg similarity index 100% rename from charges/portcullis.svg rename to public/charges/portcullis.svg diff --git a/charges/pot.svg b/public/charges/pot.svg similarity index 100% rename from charges/pot.svg rename to public/charges/pot.svg diff --git a/charges/quatrefoil.svg b/public/charges/quatrefoil.svg similarity index 100% rename from charges/quatrefoil.svg rename to public/charges/quatrefoil.svg diff --git a/charges/rabbitSejant.svg b/public/charges/rabbitSejant.svg similarity index 100% rename from charges/rabbitSejant.svg rename to public/charges/rabbitSejant.svg diff --git a/charges/raft.svg b/public/charges/raft.svg similarity index 100% rename from charges/raft.svg rename to public/charges/raft.svg diff --git a/charges/rake.svg b/public/charges/rake.svg similarity index 100% rename from charges/rake.svg rename to public/charges/rake.svg diff --git a/charges/ramHeadErased.svg b/public/charges/ramHeadErased.svg similarity index 100% rename from charges/ramHeadErased.svg rename to public/charges/ramHeadErased.svg diff --git a/charges/ramPassant.svg b/public/charges/ramPassant.svg similarity index 100% rename from charges/ramPassant.svg rename to public/charges/ramPassant.svg diff --git a/charges/ramsHorn.svg b/public/charges/ramsHorn.svg similarity index 100% rename from charges/ramsHorn.svg rename to public/charges/ramsHorn.svg diff --git a/charges/rapier.svg b/public/charges/rapier.svg similarity index 100% rename from charges/rapier.svg rename to public/charges/rapier.svg diff --git a/charges/ratRampant.svg b/public/charges/ratRampant.svg similarity index 100% rename from charges/ratRampant.svg rename to public/charges/ratRampant.svg diff --git a/charges/raven.svg b/public/charges/raven.svg similarity index 100% rename from charges/raven.svg rename to public/charges/raven.svg diff --git a/charges/rhinoceros.svg b/public/charges/rhinoceros.svg similarity index 100% rename from charges/rhinoceros.svg rename to public/charges/rhinoceros.svg diff --git a/charges/ribbon1.svg b/public/charges/ribbon1.svg similarity index 100% rename from charges/ribbon1.svg rename to public/charges/ribbon1.svg diff --git a/charges/ribbon2.svg b/public/charges/ribbon2.svg similarity index 100% rename from charges/ribbon2.svg rename to public/charges/ribbon2.svg diff --git a/charges/ribbon3.svg b/public/charges/ribbon3.svg similarity index 100% rename from charges/ribbon3.svg rename to public/charges/ribbon3.svg diff --git a/charges/ribbon4.svg b/public/charges/ribbon4.svg similarity index 100% rename from charges/ribbon4.svg rename to public/charges/ribbon4.svg diff --git a/charges/ribbon5.svg b/public/charges/ribbon5.svg similarity index 100% rename from charges/ribbon5.svg rename to public/charges/ribbon5.svg diff --git a/charges/ribbon6.svg b/public/charges/ribbon6.svg similarity index 100% rename from charges/ribbon6.svg rename to public/charges/ribbon6.svg diff --git a/charges/ribbon7.svg b/public/charges/ribbon7.svg similarity index 100% rename from charges/ribbon7.svg rename to public/charges/ribbon7.svg diff --git a/charges/ribbon8.svg b/public/charges/ribbon8.svg similarity index 100% rename from charges/ribbon8.svg rename to public/charges/ribbon8.svg diff --git a/charges/rose.svg b/public/charges/rose.svg similarity index 100% rename from charges/rose.svg rename to public/charges/rose.svg diff --git a/charges/roundel.svg b/public/charges/roundel.svg similarity index 100% rename from charges/roundel.svg rename to public/charges/roundel.svg diff --git a/charges/roundel2.svg b/public/charges/roundel2.svg similarity index 100% rename from charges/roundel2.svg rename to public/charges/roundel2.svg diff --git a/charges/rustre.svg b/public/charges/rustre.svg similarity index 100% rename from charges/rustre.svg rename to public/charges/rustre.svg diff --git a/charges/sabre.svg b/public/charges/sabre.svg similarity index 100% rename from charges/sabre.svg rename to public/charges/sabre.svg diff --git a/charges/sabre2.svg b/public/charges/sabre2.svg similarity index 100% rename from charges/sabre2.svg rename to public/charges/sabre2.svg diff --git a/charges/sabresCrossed.svg b/public/charges/sabresCrossed.svg similarity index 100% rename from charges/sabresCrossed.svg rename to public/charges/sabresCrossed.svg diff --git a/charges/sagittarius.svg b/public/charges/sagittarius.svg similarity index 100% rename from charges/sagittarius.svg rename to public/charges/sagittarius.svg diff --git a/charges/salmon.svg b/public/charges/salmon.svg similarity index 100% rename from charges/salmon.svg rename to public/charges/salmon.svg diff --git a/charges/saw.svg b/public/charges/saw.svg similarity index 100% rename from charges/saw.svg rename to public/charges/saw.svg diff --git a/charges/scale.svg b/public/charges/scale.svg similarity index 100% rename from charges/scale.svg rename to public/charges/scale.svg diff --git a/charges/scaleImbalanced.svg b/public/charges/scaleImbalanced.svg similarity index 100% rename from charges/scaleImbalanced.svg rename to public/charges/scaleImbalanced.svg diff --git a/charges/scalesHanging.svg b/public/charges/scalesHanging.svg similarity index 100% rename from charges/scalesHanging.svg rename to public/charges/scalesHanging.svg diff --git a/charges/sceptre.svg b/public/charges/sceptre.svg similarity index 100% rename from charges/sceptre.svg rename to public/charges/sceptre.svg diff --git a/charges/scissors.svg b/public/charges/scissors.svg similarity index 100% rename from charges/scissors.svg rename to public/charges/scissors.svg diff --git a/charges/scissors2.svg b/public/charges/scissors2.svg similarity index 100% rename from charges/scissors2.svg rename to public/charges/scissors2.svg diff --git a/charges/scorpion.svg b/public/charges/scorpion.svg similarity index 100% rename from charges/scorpion.svg rename to public/charges/scorpion.svg diff --git a/charges/scrollClosed.svg b/public/charges/scrollClosed.svg similarity index 100% rename from charges/scrollClosed.svg rename to public/charges/scrollClosed.svg diff --git a/charges/scythe.svg b/public/charges/scythe.svg similarity index 100% rename from charges/scythe.svg rename to public/charges/scythe.svg diff --git a/charges/scythe2.svg b/public/charges/scythe2.svg similarity index 100% rename from charges/scythe2.svg rename to public/charges/scythe2.svg diff --git a/charges/serpent.svg b/public/charges/serpent.svg similarity index 100% rename from charges/serpent.svg rename to public/charges/serpent.svg diff --git a/charges/sextifoil.svg b/public/charges/sextifoil.svg similarity index 100% rename from charges/sextifoil.svg rename to public/charges/sextifoil.svg diff --git a/charges/shears.svg b/public/charges/shears.svg similarity index 100% rename from charges/shears.svg rename to public/charges/shears.svg diff --git a/charges/shield.svg b/public/charges/shield.svg similarity index 100% rename from charges/shield.svg rename to public/charges/shield.svg diff --git a/charges/shipWheel.svg b/public/charges/shipWheel.svg similarity index 100% rename from charges/shipWheel.svg rename to public/charges/shipWheel.svg diff --git a/charges/sickle.svg b/public/charges/sickle.svg similarity index 100% rename from charges/sickle.svg rename to public/charges/sickle.svg diff --git a/charges/skeleton.svg b/public/charges/skeleton.svg similarity index 100% rename from charges/skeleton.svg rename to public/charges/skeleton.svg diff --git a/charges/skull.svg b/public/charges/skull.svg similarity index 100% rename from charges/skull.svg rename to public/charges/skull.svg diff --git a/charges/skull2.svg b/public/charges/skull2.svg similarity index 100% rename from charges/skull2.svg rename to public/charges/skull2.svg diff --git a/charges/snail.svg b/public/charges/snail.svg similarity index 100% rename from charges/snail.svg rename to public/charges/snail.svg diff --git a/charges/snake.svg b/public/charges/snake.svg similarity index 100% rename from charges/snake.svg rename to public/charges/snake.svg diff --git a/charges/snowflake.svg b/public/charges/snowflake.svg similarity index 100% rename from charges/snowflake.svg rename to public/charges/snowflake.svg diff --git a/charges/spear.svg b/public/charges/spear.svg similarity index 100% rename from charges/spear.svg rename to public/charges/spear.svg diff --git a/charges/spiral.svg b/public/charges/spiral.svg similarity index 100% rename from charges/spiral.svg rename to public/charges/spiral.svg diff --git a/charges/squirrel.svg b/public/charges/squirrel.svg similarity index 100% rename from charges/squirrel.svg rename to public/charges/squirrel.svg diff --git a/charges/stagLodgedRegardant.svg b/public/charges/stagLodgedRegardant.svg similarity index 100% rename from charges/stagLodgedRegardant.svg rename to public/charges/stagLodgedRegardant.svg diff --git a/charges/stagPassant.svg b/public/charges/stagPassant.svg similarity index 100% rename from charges/stagPassant.svg rename to public/charges/stagPassant.svg diff --git a/charges/stagsAttires.svg b/public/charges/stagsAttires.svg similarity index 100% rename from charges/stagsAttires.svg rename to public/charges/stagsAttires.svg diff --git a/charges/stirrup.svg b/public/charges/stirrup.svg similarity index 100% rename from charges/stirrup.svg rename to public/charges/stirrup.svg diff --git a/charges/sun.svg b/public/charges/sun.svg similarity index 100% rename from charges/sun.svg rename to public/charges/sun.svg diff --git a/charges/sunInSplendour.svg b/public/charges/sunInSplendour.svg similarity index 100% rename from charges/sunInSplendour.svg rename to public/charges/sunInSplendour.svg diff --git a/charges/sunInSplendour2.svg b/public/charges/sunInSplendour2.svg similarity index 100% rename from charges/sunInSplendour2.svg rename to public/charges/sunInSplendour2.svg diff --git a/charges/swallow.svg b/public/charges/swallow.svg similarity index 100% rename from charges/swallow.svg rename to public/charges/swallow.svg diff --git a/charges/swan.svg b/public/charges/swan.svg similarity index 100% rename from charges/swan.svg rename to public/charges/swan.svg diff --git a/charges/swanErased.svg b/public/charges/swanErased.svg similarity index 100% rename from charges/swanErased.svg rename to public/charges/swanErased.svg diff --git a/charges/sword.svg b/public/charges/sword.svg similarity index 100% rename from charges/sword.svg rename to public/charges/sword.svg diff --git a/charges/talbotPassant.svg b/public/charges/talbotPassant.svg similarity index 100% rename from charges/talbotPassant.svg rename to public/charges/talbotPassant.svg diff --git a/charges/talbotSejant.svg b/public/charges/talbotSejant.svg similarity index 100% rename from charges/talbotSejant.svg rename to public/charges/talbotSejant.svg diff --git a/charges/template.svg b/public/charges/template.svg similarity index 100% rename from charges/template.svg rename to public/charges/template.svg diff --git a/charges/thistle.svg b/public/charges/thistle.svg similarity index 100% rename from charges/thistle.svg rename to public/charges/thistle.svg diff --git a/charges/tower.svg b/public/charges/tower.svg similarity index 100% rename from charges/tower.svg rename to public/charges/tower.svg diff --git a/charges/tree.svg b/public/charges/tree.svg similarity index 100% rename from charges/tree.svg rename to public/charges/tree.svg diff --git a/charges/trefle.svg b/public/charges/trefle.svg similarity index 100% rename from charges/trefle.svg rename to public/charges/trefle.svg diff --git a/charges/trefoil.svg b/public/charges/trefoil.svg similarity index 100% rename from charges/trefoil.svg rename to public/charges/trefoil.svg diff --git a/charges/triangle.svg b/public/charges/triangle.svg similarity index 100% rename from charges/triangle.svg rename to public/charges/triangle.svg diff --git a/charges/trianglePierced.svg b/public/charges/trianglePierced.svg similarity index 100% rename from charges/trianglePierced.svg rename to public/charges/trianglePierced.svg diff --git a/charges/trowel.svg b/public/charges/trowel.svg similarity index 100% rename from charges/trowel.svg rename to public/charges/trowel.svg diff --git a/charges/unicornRampant.svg b/public/charges/unicornRampant.svg similarity index 100% rename from charges/unicornRampant.svg rename to public/charges/unicornRampant.svg diff --git a/charges/wasp.svg b/public/charges/wasp.svg similarity index 100% rename from charges/wasp.svg rename to public/charges/wasp.svg diff --git a/charges/wheatStalk.svg b/public/charges/wheatStalk.svg similarity index 100% rename from charges/wheatStalk.svg rename to public/charges/wheatStalk.svg diff --git a/charges/wheel.svg b/public/charges/wheel.svg similarity index 100% rename from charges/wheel.svg rename to public/charges/wheel.svg diff --git a/charges/windmill.svg b/public/charges/windmill.svg similarity index 100% rename from charges/windmill.svg rename to public/charges/windmill.svg diff --git a/charges/wing.svg b/public/charges/wing.svg similarity index 100% rename from charges/wing.svg rename to public/charges/wing.svg diff --git a/charges/wingSword.svg b/public/charges/wingSword.svg similarity index 100% rename from charges/wingSword.svg rename to public/charges/wingSword.svg diff --git a/charges/wolfHeadErased.svg b/public/charges/wolfHeadErased.svg similarity index 100% rename from charges/wolfHeadErased.svg rename to public/charges/wolfHeadErased.svg diff --git a/charges/wolfPassant.svg b/public/charges/wolfPassant.svg similarity index 100% rename from charges/wolfPassant.svg rename to public/charges/wolfPassant.svg diff --git a/charges/wolfRampant.svg b/public/charges/wolfRampant.svg similarity index 100% rename from charges/wolfRampant.svg rename to public/charges/wolfRampant.svg diff --git a/charges/wolfStatant.svg b/public/charges/wolfStatant.svg similarity index 100% rename from charges/wolfStatant.svg rename to public/charges/wolfStatant.svg diff --git a/charges/wyvern.svg b/public/charges/wyvern.svg similarity index 100% rename from charges/wyvern.svg rename to public/charges/wyvern.svg diff --git a/charges/wyvernWithWingsDisplayed.svg b/public/charges/wyvernWithWingsDisplayed.svg similarity index 100% rename from charges/wyvernWithWingsDisplayed.svg rename to public/charges/wyvernWithWingsDisplayed.svg diff --git a/components/fill-box.js b/public/components/fill-box.js similarity index 100% rename from components/fill-box.js rename to public/components/fill-box.js diff --git a/components/slider-input.js b/public/components/slider-input.js similarity index 100% rename from components/slider-input.js rename to public/components/slider-input.js diff --git a/config/heightmap-templates.js b/public/config/heightmap-templates.js similarity index 100% rename from config/heightmap-templates.js rename to public/config/heightmap-templates.js diff --git a/config/precreated-heightmaps.js b/public/config/precreated-heightmaps.js similarity index 100% rename from config/precreated-heightmaps.js rename to public/config/precreated-heightmaps.js diff --git a/dropbox.html b/public/dropbox.html similarity index 100% rename from dropbox.html rename to public/dropbox.html diff --git a/heightmaps/africa-centric.png b/public/heightmaps/africa-centric.png similarity index 100% rename from heightmaps/africa-centric.png rename to public/heightmaps/africa-centric.png diff --git a/heightmaps/arabia.png b/public/heightmaps/arabia.png similarity index 100% rename from heightmaps/arabia.png rename to public/heightmaps/arabia.png diff --git a/heightmaps/atlantics.png b/public/heightmaps/atlantics.png similarity index 100% rename from heightmaps/atlantics.png rename to public/heightmaps/atlantics.png diff --git a/heightmaps/britain.png b/public/heightmaps/britain.png similarity index 100% rename from heightmaps/britain.png rename to public/heightmaps/britain.png diff --git a/heightmaps/caribbean.png b/public/heightmaps/caribbean.png similarity index 100% rename from heightmaps/caribbean.png rename to public/heightmaps/caribbean.png diff --git a/heightmaps/east-asia.png b/public/heightmaps/east-asia.png similarity index 100% rename from heightmaps/east-asia.png rename to public/heightmaps/east-asia.png diff --git a/heightmaps/eurasia.png b/public/heightmaps/eurasia.png similarity index 100% rename from heightmaps/eurasia.png rename to public/heightmaps/eurasia.png diff --git a/heightmaps/europe-accented.png b/public/heightmaps/europe-accented.png similarity index 100% rename from heightmaps/europe-accented.png rename to public/heightmaps/europe-accented.png diff --git a/heightmaps/europe-and-central-asia.png b/public/heightmaps/europe-and-central-asia.png similarity index 100% rename from heightmaps/europe-and-central-asia.png rename to public/heightmaps/europe-and-central-asia.png diff --git a/heightmaps/europe-central.png b/public/heightmaps/europe-central.png similarity index 100% rename from heightmaps/europe-central.png rename to public/heightmaps/europe-central.png diff --git a/heightmaps/europe-north.png b/public/heightmaps/europe-north.png similarity index 100% rename from heightmaps/europe-north.png rename to public/heightmaps/europe-north.png diff --git a/heightmaps/europe.png b/public/heightmaps/europe.png similarity index 100% rename from heightmaps/europe.png rename to public/heightmaps/europe.png diff --git a/heightmaps/greenland.png b/public/heightmaps/greenland.png similarity index 100% rename from heightmaps/greenland.png rename to public/heightmaps/greenland.png diff --git a/heightmaps/hellenica.png b/public/heightmaps/hellenica.png similarity index 100% rename from heightmaps/hellenica.png rename to public/heightmaps/hellenica.png diff --git a/heightmaps/iceland.png b/public/heightmaps/iceland.png similarity index 100% rename from heightmaps/iceland.png rename to public/heightmaps/iceland.png diff --git a/heightmaps/import-rules.txt b/public/heightmaps/import-rules.txt similarity index 100% rename from heightmaps/import-rules.txt rename to public/heightmaps/import-rules.txt diff --git a/heightmaps/indian-ocean.png b/public/heightmaps/indian-ocean.png similarity index 100% rename from heightmaps/indian-ocean.png rename to public/heightmaps/indian-ocean.png diff --git a/heightmaps/mediterranean-sea.png b/public/heightmaps/mediterranean-sea.png similarity index 100% rename from heightmaps/mediterranean-sea.png rename to public/heightmaps/mediterranean-sea.png diff --git a/heightmaps/middle-east.png b/public/heightmaps/middle-east.png similarity index 100% rename from heightmaps/middle-east.png rename to public/heightmaps/middle-east.png diff --git a/heightmaps/north-america.png b/public/heightmaps/north-america.png similarity index 100% rename from heightmaps/north-america.png rename to public/heightmaps/north-america.png diff --git a/heightmaps/us-centric.png b/public/heightmaps/us-centric.png similarity index 100% rename from heightmaps/us-centric.png rename to public/heightmaps/us-centric.png diff --git a/heightmaps/us-mainland.png b/public/heightmaps/us-mainland.png similarity index 100% rename from heightmaps/us-mainland.png rename to public/heightmaps/us-mainland.png diff --git a/heightmaps/world-from-pacific.png b/public/heightmaps/world-from-pacific.png similarity index 100% rename from heightmaps/world-from-pacific.png rename to public/heightmaps/world-from-pacific.png diff --git a/heightmaps/world.png b/public/heightmaps/world.png similarity index 100% rename from heightmaps/world.png rename to public/heightmaps/world.png diff --git a/icons.css b/public/icons.css similarity index 100% rename from icons.css rename to public/icons.css diff --git a/images/Discord.png b/public/images/Discord.png similarity index 100% rename from images/Discord.png rename to public/images/Discord.png diff --git a/images/Facebook.png b/public/images/Facebook.png similarity index 100% rename from images/Facebook.png rename to public/images/Facebook.png diff --git a/images/Pinterest.png b/public/images/Pinterest.png similarity index 100% rename from images/Pinterest.png rename to public/images/Pinterest.png diff --git a/images/Reddit.png b/public/images/Reddit.png similarity index 100% rename from images/Reddit.png rename to public/images/Reddit.png diff --git a/images/Twitter.png b/public/images/Twitter.png similarity index 100% rename from images/Twitter.png rename to public/images/Twitter.png diff --git a/images/icons/favicon-16x16.png b/public/images/icons/favicon-16x16.png similarity index 100% rename from images/icons/favicon-16x16.png rename to public/images/icons/favicon-16x16.png diff --git a/images/icons/favicon-32x32.png b/public/images/icons/favicon-32x32.png similarity index 100% rename from images/icons/favicon-32x32.png rename to public/images/icons/favicon-32x32.png diff --git a/images/icons/icon_x512.png b/public/images/icons/icon_x512.png similarity index 100% rename from images/icons/icon_x512.png rename to public/images/icons/icon_x512.png diff --git a/images/icons/maskable_icon_x128.png b/public/images/icons/maskable_icon_x128.png similarity index 100% rename from images/icons/maskable_icon_x128.png rename to public/images/icons/maskable_icon_x128.png diff --git a/images/icons/maskable_icon_x192.png b/public/images/icons/maskable_icon_x192.png similarity index 100% rename from images/icons/maskable_icon_x192.png rename to public/images/icons/maskable_icon_x192.png diff --git a/images/icons/maskable_icon_x384.png b/public/images/icons/maskable_icon_x384.png similarity index 100% rename from images/icons/maskable_icon_x384.png rename to public/images/icons/maskable_icon_x384.png diff --git a/images/icons/maskable_icon_x512.png b/public/images/icons/maskable_icon_x512.png similarity index 100% rename from images/icons/maskable_icon_x512.png rename to public/images/icons/maskable_icon_x512.png diff --git a/images/kiwiroo.png b/public/images/kiwiroo.png similarity index 100% rename from images/kiwiroo.png rename to public/images/kiwiroo.png diff --git a/images/pattern1.png b/public/images/pattern1.png similarity index 100% rename from images/pattern1.png rename to public/images/pattern1.png diff --git a/images/pattern2.png b/public/images/pattern2.png similarity index 100% rename from images/pattern2.png rename to public/images/pattern2.png diff --git a/images/pattern3.png b/public/images/pattern3.png similarity index 100% rename from images/pattern3.png rename to public/images/pattern3.png diff --git a/images/pattern4.png b/public/images/pattern4.png similarity index 100% rename from images/pattern4.png rename to public/images/pattern4.png diff --git a/images/pattern5.png b/public/images/pattern5.png similarity index 100% rename from images/pattern5.png rename to public/images/pattern5.png diff --git a/images/pattern6.png b/public/images/pattern6.png similarity index 100% rename from images/pattern6.png rename to public/images/pattern6.png diff --git a/images/preview.png b/public/images/preview.png similarity index 100% rename from images/preview.png rename to public/images/preview.png diff --git a/images/textures/antique-big.jpg b/public/images/textures/antique-big.jpg similarity index 100% rename from images/textures/antique-big.jpg rename to public/images/textures/antique-big.jpg diff --git a/images/textures/antique-small.jpg b/public/images/textures/antique-small.jpg similarity index 100% rename from images/textures/antique-small.jpg rename to public/images/textures/antique-small.jpg diff --git a/images/textures/folded-paper-big.jpg b/public/images/textures/folded-paper-big.jpg similarity index 100% rename from images/textures/folded-paper-big.jpg rename to public/images/textures/folded-paper-big.jpg diff --git a/images/textures/folded-paper-small.jpg b/public/images/textures/folded-paper-small.jpg similarity index 100% rename from images/textures/folded-paper-small.jpg rename to public/images/textures/folded-paper-small.jpg diff --git a/images/textures/gray-paper.jpg b/public/images/textures/gray-paper.jpg similarity index 100% rename from images/textures/gray-paper.jpg rename to public/images/textures/gray-paper.jpg diff --git a/images/textures/iran-small.jpg b/public/images/textures/iran-small.jpg similarity index 100% rename from images/textures/iran-small.jpg rename to public/images/textures/iran-small.jpg diff --git a/images/textures/marble-big.jpg b/public/images/textures/marble-big.jpg similarity index 100% rename from images/textures/marble-big.jpg rename to public/images/textures/marble-big.jpg diff --git a/images/textures/marble-blue-big.jpg b/public/images/textures/marble-blue-big.jpg similarity index 100% rename from images/textures/marble-blue-big.jpg rename to public/images/textures/marble-blue-big.jpg diff --git a/images/textures/marble-blue-small.jpg b/public/images/textures/marble-blue-small.jpg similarity index 100% rename from images/textures/marble-blue-small.jpg rename to public/images/textures/marble-blue-small.jpg diff --git a/images/textures/marble-small.jpg b/public/images/textures/marble-small.jpg similarity index 100% rename from images/textures/marble-small.jpg rename to public/images/textures/marble-small.jpg diff --git a/images/textures/mars-big.jpg b/public/images/textures/mars-big.jpg similarity index 100% rename from images/textures/mars-big.jpg rename to public/images/textures/mars-big.jpg diff --git a/images/textures/mars-small.jpg b/public/images/textures/mars-small.jpg similarity index 100% rename from images/textures/mars-small.jpg rename to public/images/textures/mars-small.jpg diff --git a/images/textures/mauritania-small.jpg b/public/images/textures/mauritania-small.jpg similarity index 100% rename from images/textures/mauritania-small.jpg rename to public/images/textures/mauritania-small.jpg diff --git a/images/textures/mercury-big.jpg b/public/images/textures/mercury-big.jpg similarity index 100% rename from images/textures/mercury-big.jpg rename to public/images/textures/mercury-big.jpg diff --git a/images/textures/mercury-small.jpg b/public/images/textures/mercury-small.jpg similarity index 100% rename from images/textures/mercury-small.jpg rename to public/images/textures/mercury-small.jpg diff --git a/images/textures/ocean.jpg b/public/images/textures/ocean.jpg similarity index 100% rename from images/textures/ocean.jpg rename to public/images/textures/ocean.jpg diff --git a/images/textures/pergamena-small.jpg b/public/images/textures/pergamena-small.jpg similarity index 100% rename from images/textures/pergamena-small.jpg rename to public/images/textures/pergamena-small.jpg diff --git a/images/textures/plaster.jpg b/public/images/textures/plaster.jpg similarity index 100% rename from images/textures/plaster.jpg rename to public/images/textures/plaster.jpg diff --git a/images/textures/soiled-paper-vertical.png b/public/images/textures/soiled-paper-vertical.png similarity index 100% rename from images/textures/soiled-paper-vertical.png rename to public/images/textures/soiled-paper-vertical.png diff --git a/images/textures/soiled-paper.jpg b/public/images/textures/soiled-paper.jpg similarity index 100% rename from images/textures/soiled-paper.jpg rename to public/images/textures/soiled-paper.jpg diff --git a/images/textures/spain-small.jpg b/public/images/textures/spain-small.jpg similarity index 100% rename from images/textures/spain-small.jpg rename to public/images/textures/spain-small.jpg diff --git a/images/textures/timbercut-big.jpg b/public/images/textures/timbercut-big.jpg similarity index 100% rename from images/textures/timbercut-big.jpg rename to public/images/textures/timbercut-big.jpg diff --git a/images/textures/timbercut-small.jpg b/public/images/textures/timbercut-small.jpg similarity index 100% rename from images/textures/timbercut-small.jpg rename to public/images/textures/timbercut-small.jpg diff --git a/index.css b/public/index.css similarity index 100% rename from index.css rename to public/index.css diff --git a/libs/alea.min.js b/public/libs/alea.min.js similarity index 100% rename from libs/alea.min.js rename to public/libs/alea.min.js diff --git a/libs/d3.min.js b/public/libs/d3.min.js similarity index 100% rename from libs/d3.min.js rename to public/libs/d3.min.js diff --git a/libs/delaunator.min.js b/public/libs/delaunator.min.js similarity index 100% rename from libs/delaunator.min.js rename to public/libs/delaunator.min.js diff --git a/libs/dropbox-sdk.min.js b/public/libs/dropbox-sdk.min.js similarity index 100% rename from libs/dropbox-sdk.min.js rename to public/libs/dropbox-sdk.min.js diff --git a/libs/flatqueue.js b/public/libs/flatqueue.js similarity index 100% rename from libs/flatqueue.js rename to public/libs/flatqueue.js diff --git a/libs/indexedDB.js b/public/libs/indexedDB.js similarity index 100% rename from libs/indexedDB.js rename to public/libs/indexedDB.js diff --git a/libs/jquery-3.1.1.min.js b/public/libs/jquery-3.1.1.min.js similarity index 100% rename from libs/jquery-3.1.1.min.js rename to public/libs/jquery-3.1.1.min.js diff --git a/libs/jquery-ui.css b/public/libs/jquery-ui.css similarity index 100% rename from libs/jquery-ui.css rename to public/libs/jquery-ui.css diff --git a/libs/jquery-ui.min.js b/public/libs/jquery-ui.min.js similarity index 100% rename from libs/jquery-ui.min.js rename to public/libs/jquery-ui.min.js diff --git a/libs/jquery.ui.touch-punch.min.js b/public/libs/jquery.ui.touch-punch.min.js similarity index 100% rename from libs/jquery.ui.touch-punch.min.js rename to public/libs/jquery.ui.touch-punch.min.js diff --git a/libs/jszip.min.js b/public/libs/jszip.min.js similarity index 100% rename from libs/jszip.min.js rename to public/libs/jszip.min.js diff --git a/libs/lineclip.min.js b/public/libs/lineclip.min.js similarity index 100% rename from libs/lineclip.min.js rename to public/libs/lineclip.min.js diff --git a/libs/loopsubdivison.min.js b/public/libs/loopsubdivison.min.js similarity index 100% rename from libs/loopsubdivison.min.js rename to public/libs/loopsubdivison.min.js diff --git a/libs/mapControls.min.js b/public/libs/mapControls.min.js similarity index 100% rename from libs/mapControls.min.js rename to public/libs/mapControls.min.js diff --git a/libs/objexporter.min.js b/public/libs/objexporter.min.js similarity index 100% rename from libs/objexporter.min.js rename to public/libs/objexporter.min.js diff --git a/libs/openwidget.min.js b/public/libs/openwidget.min.js similarity index 100% rename from libs/openwidget.min.js rename to public/libs/openwidget.min.js diff --git a/libs/orbitControls.min.js b/public/libs/orbitControls.min.js similarity index 100% rename from libs/orbitControls.min.js rename to public/libs/orbitControls.min.js diff --git a/libs/polylabel.min.js b/public/libs/polylabel.min.js similarity index 100% rename from libs/polylabel.min.js rename to public/libs/polylabel.min.js diff --git a/libs/rgbquant.min.js b/public/libs/rgbquant.min.js similarity index 100% rename from libs/rgbquant.min.js rename to public/libs/rgbquant.min.js diff --git a/libs/simplify.js b/public/libs/simplify.js similarity index 100% rename from libs/simplify.js rename to public/libs/simplify.js diff --git a/libs/three.min.js b/public/libs/three.min.js similarity index 100% rename from libs/three.min.js rename to public/libs/three.min.js diff --git a/libs/tinymce/icons/default/icons.min.js b/public/libs/tinymce/icons/default/icons.min.js similarity index 100% rename from libs/tinymce/icons/default/icons.min.js rename to public/libs/tinymce/icons/default/icons.min.js diff --git a/libs/tinymce/langs/README.md b/public/libs/tinymce/langs/README.md similarity index 100% rename from libs/tinymce/langs/README.md rename to public/libs/tinymce/langs/README.md diff --git a/libs/tinymce/license.md b/public/libs/tinymce/license.md similarity index 100% rename from libs/tinymce/license.md rename to public/libs/tinymce/license.md diff --git a/libs/tinymce/models/dom/model.min.js b/public/libs/tinymce/models/dom/model.min.js similarity index 100% rename from libs/tinymce/models/dom/model.min.js rename to public/libs/tinymce/models/dom/model.min.js diff --git a/libs/tinymce/plugins/accordion/plugin.min.js b/public/libs/tinymce/plugins/accordion/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/accordion/plugin.min.js rename to public/libs/tinymce/plugins/accordion/plugin.min.js diff --git a/libs/tinymce/plugins/advlist/plugin.min.js b/public/libs/tinymce/plugins/advlist/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/advlist/plugin.min.js rename to public/libs/tinymce/plugins/advlist/plugin.min.js diff --git a/libs/tinymce/plugins/anchor/plugin.min.js b/public/libs/tinymce/plugins/anchor/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/anchor/plugin.min.js rename to public/libs/tinymce/plugins/anchor/plugin.min.js diff --git a/libs/tinymce/plugins/autolink/plugin.min.js b/public/libs/tinymce/plugins/autolink/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/autolink/plugin.min.js rename to public/libs/tinymce/plugins/autolink/plugin.min.js diff --git a/libs/tinymce/plugins/autoresize/plugin.min.js b/public/libs/tinymce/plugins/autoresize/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/autoresize/plugin.min.js rename to public/libs/tinymce/plugins/autoresize/plugin.min.js diff --git a/libs/tinymce/plugins/autosave/plugin.min.js b/public/libs/tinymce/plugins/autosave/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/autosave/plugin.min.js rename to public/libs/tinymce/plugins/autosave/plugin.min.js diff --git a/libs/tinymce/plugins/charmap/plugin.min.js b/public/libs/tinymce/plugins/charmap/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/charmap/plugin.min.js rename to public/libs/tinymce/plugins/charmap/plugin.min.js diff --git a/libs/tinymce/plugins/code/plugin.min.js b/public/libs/tinymce/plugins/code/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/code/plugin.min.js rename to public/libs/tinymce/plugins/code/plugin.min.js diff --git a/libs/tinymce/plugins/codesample/plugin.min.js b/public/libs/tinymce/plugins/codesample/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/codesample/plugin.min.js rename to public/libs/tinymce/plugins/codesample/plugin.min.js diff --git a/libs/tinymce/plugins/directionality/plugin.min.js b/public/libs/tinymce/plugins/directionality/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/directionality/plugin.min.js rename to public/libs/tinymce/plugins/directionality/plugin.min.js diff --git a/libs/tinymce/plugins/emoticons/js/emojiimages.js b/public/libs/tinymce/plugins/emoticons/js/emojiimages.js similarity index 100% rename from libs/tinymce/plugins/emoticons/js/emojiimages.js rename to public/libs/tinymce/plugins/emoticons/js/emojiimages.js diff --git a/libs/tinymce/plugins/emoticons/js/emojiimages.min.js b/public/libs/tinymce/plugins/emoticons/js/emojiimages.min.js similarity index 100% rename from libs/tinymce/plugins/emoticons/js/emojiimages.min.js rename to public/libs/tinymce/plugins/emoticons/js/emojiimages.min.js diff --git a/libs/tinymce/plugins/emoticons/js/emojis.js b/public/libs/tinymce/plugins/emoticons/js/emojis.js similarity index 100% rename from libs/tinymce/plugins/emoticons/js/emojis.js rename to public/libs/tinymce/plugins/emoticons/js/emojis.js diff --git a/libs/tinymce/plugins/emoticons/js/emojis.min.js b/public/libs/tinymce/plugins/emoticons/js/emojis.min.js similarity index 100% rename from libs/tinymce/plugins/emoticons/js/emojis.min.js rename to public/libs/tinymce/plugins/emoticons/js/emojis.min.js diff --git a/libs/tinymce/plugins/emoticons/plugin.min.js b/public/libs/tinymce/plugins/emoticons/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/emoticons/plugin.min.js rename to public/libs/tinymce/plugins/emoticons/plugin.min.js diff --git a/libs/tinymce/plugins/fullscreen/plugin.min.js b/public/libs/tinymce/plugins/fullscreen/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/fullscreen/plugin.min.js rename to public/libs/tinymce/plugins/fullscreen/plugin.min.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ar.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ar.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ar.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ar.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ca.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ca.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ca.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ca.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/cs.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/cs.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/cs.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/cs.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/da.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/da.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/da.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/da.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/de.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/de.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/de.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/de.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/el.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/el.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/el.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/el.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/en.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/en.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/en.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/en.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/es.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/es.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/es.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/es.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/eu.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/eu.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/eu.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/eu.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/fa.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/fa.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/fa.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/fa.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/fi.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/fi.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/fi.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/fi.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/hi.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/hi.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/hi.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/hi.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/hr.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/hr.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/hr.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/hr.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/id.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/id.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/id.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/id.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/it.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/it.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/it.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/it.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ja.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ja.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ja.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ja.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/kk.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/kk.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/kk.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/kk.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ms.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ms.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ms.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ms.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/nl.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/nl.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/nl.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/nl.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/pl.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/pl.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/pl.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/pl.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ro.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ro.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ro.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ro.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ru.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ru.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/ru.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ru.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/sk.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/sk.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/sk.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/sk.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/tr.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/tr.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/tr.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/tr.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/uk.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/uk.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/uk.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/uk.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/vi.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/vi.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/vi.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/vi.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js similarity index 100% rename from libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js rename to public/libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js diff --git a/libs/tinymce/plugins/help/plugin.min.js b/public/libs/tinymce/plugins/help/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/help/plugin.min.js rename to public/libs/tinymce/plugins/help/plugin.min.js diff --git a/libs/tinymce/plugins/image/plugin.min.js b/public/libs/tinymce/plugins/image/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/image/plugin.min.js rename to public/libs/tinymce/plugins/image/plugin.min.js diff --git a/libs/tinymce/plugins/importcss/plugin.min.js b/public/libs/tinymce/plugins/importcss/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/importcss/plugin.min.js rename to public/libs/tinymce/plugins/importcss/plugin.min.js diff --git a/libs/tinymce/plugins/insertdatetime/plugin.min.js b/public/libs/tinymce/plugins/insertdatetime/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/insertdatetime/plugin.min.js rename to public/libs/tinymce/plugins/insertdatetime/plugin.min.js diff --git a/libs/tinymce/plugins/link/plugin.min.js b/public/libs/tinymce/plugins/link/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/link/plugin.min.js rename to public/libs/tinymce/plugins/link/plugin.min.js diff --git a/libs/tinymce/plugins/lists/plugin.min.js b/public/libs/tinymce/plugins/lists/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/lists/plugin.min.js rename to public/libs/tinymce/plugins/lists/plugin.min.js diff --git a/libs/tinymce/plugins/media/plugin.min.js b/public/libs/tinymce/plugins/media/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/media/plugin.min.js rename to public/libs/tinymce/plugins/media/plugin.min.js diff --git a/libs/tinymce/plugins/nonbreaking/plugin.min.js b/public/libs/tinymce/plugins/nonbreaking/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/nonbreaking/plugin.min.js rename to public/libs/tinymce/plugins/nonbreaking/plugin.min.js diff --git a/libs/tinymce/plugins/pagebreak/plugin.min.js b/public/libs/tinymce/plugins/pagebreak/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/pagebreak/plugin.min.js rename to public/libs/tinymce/plugins/pagebreak/plugin.min.js diff --git a/libs/tinymce/plugins/preview/plugin.min.js b/public/libs/tinymce/plugins/preview/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/preview/plugin.min.js rename to public/libs/tinymce/plugins/preview/plugin.min.js diff --git a/libs/tinymce/plugins/quickbars/plugin.min.js b/public/libs/tinymce/plugins/quickbars/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/quickbars/plugin.min.js rename to public/libs/tinymce/plugins/quickbars/plugin.min.js diff --git a/libs/tinymce/plugins/save/plugin.min.js b/public/libs/tinymce/plugins/save/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/save/plugin.min.js rename to public/libs/tinymce/plugins/save/plugin.min.js diff --git a/libs/tinymce/plugins/searchreplace/plugin.min.js b/public/libs/tinymce/plugins/searchreplace/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/searchreplace/plugin.min.js rename to public/libs/tinymce/plugins/searchreplace/plugin.min.js diff --git a/libs/tinymce/plugins/table/plugin.min.js b/public/libs/tinymce/plugins/table/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/table/plugin.min.js rename to public/libs/tinymce/plugins/table/plugin.min.js diff --git a/libs/tinymce/plugins/visualblocks/plugin.min.js b/public/libs/tinymce/plugins/visualblocks/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/visualblocks/plugin.min.js rename to public/libs/tinymce/plugins/visualblocks/plugin.min.js diff --git a/libs/tinymce/plugins/visualchars/plugin.min.js b/public/libs/tinymce/plugins/visualchars/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/visualchars/plugin.min.js rename to public/libs/tinymce/plugins/visualchars/plugin.min.js diff --git a/libs/tinymce/plugins/wordcount/plugin.min.js b/public/libs/tinymce/plugins/wordcount/plugin.min.js similarity index 100% rename from libs/tinymce/plugins/wordcount/plugin.min.js rename to public/libs/tinymce/plugins/wordcount/plugin.min.js diff --git a/libs/tinymce/skins/content/dark/content.js b/public/libs/tinymce/skins/content/dark/content.js similarity index 100% rename from libs/tinymce/skins/content/dark/content.js rename to public/libs/tinymce/skins/content/dark/content.js diff --git a/libs/tinymce/skins/content/dark/content.min.css b/public/libs/tinymce/skins/content/dark/content.min.css similarity index 100% rename from libs/tinymce/skins/content/dark/content.min.css rename to public/libs/tinymce/skins/content/dark/content.min.css diff --git a/libs/tinymce/skins/content/default/content.css b/public/libs/tinymce/skins/content/default/content.css similarity index 100% rename from libs/tinymce/skins/content/default/content.css rename to public/libs/tinymce/skins/content/default/content.css diff --git a/libs/tinymce/skins/content/default/content.js b/public/libs/tinymce/skins/content/default/content.js similarity index 100% rename from libs/tinymce/skins/content/default/content.js rename to public/libs/tinymce/skins/content/default/content.js diff --git a/libs/tinymce/skins/content/document/content.js b/public/libs/tinymce/skins/content/document/content.js similarity index 100% rename from libs/tinymce/skins/content/document/content.js rename to public/libs/tinymce/skins/content/document/content.js diff --git a/libs/tinymce/skins/content/document/content.min.css b/public/libs/tinymce/skins/content/document/content.min.css similarity index 100% rename from libs/tinymce/skins/content/document/content.min.css rename to public/libs/tinymce/skins/content/document/content.min.css diff --git a/libs/tinymce/skins/content/tinymce-5-dark/content.js b/public/libs/tinymce/skins/content/tinymce-5-dark/content.js similarity index 100% rename from libs/tinymce/skins/content/tinymce-5-dark/content.js rename to public/libs/tinymce/skins/content/tinymce-5-dark/content.js diff --git a/libs/tinymce/skins/content/tinymce-5-dark/content.min.css b/public/libs/tinymce/skins/content/tinymce-5-dark/content.min.css similarity index 100% rename from libs/tinymce/skins/content/tinymce-5-dark/content.min.css rename to public/libs/tinymce/skins/content/tinymce-5-dark/content.min.css diff --git a/libs/tinymce/skins/content/tinymce-5/content.js b/public/libs/tinymce/skins/content/tinymce-5/content.js similarity index 100% rename from libs/tinymce/skins/content/tinymce-5/content.js rename to public/libs/tinymce/skins/content/tinymce-5/content.js diff --git a/libs/tinymce/skins/content/tinymce-5/content.min.css b/public/libs/tinymce/skins/content/tinymce-5/content.min.css similarity index 100% rename from libs/tinymce/skins/content/tinymce-5/content.min.css rename to public/libs/tinymce/skins/content/tinymce-5/content.min.css diff --git a/libs/tinymce/skins/content/writer/content.js b/public/libs/tinymce/skins/content/writer/content.js similarity index 100% rename from libs/tinymce/skins/content/writer/content.js rename to public/libs/tinymce/skins/content/writer/content.js diff --git a/libs/tinymce/skins/content/writer/content.min.css b/public/libs/tinymce/skins/content/writer/content.min.css similarity index 100% rename from libs/tinymce/skins/content/writer/content.min.css rename to public/libs/tinymce/skins/content/writer/content.min.css diff --git a/libs/tinymce/skins/ui/oxide-dark/content.inline.js b/public/libs/tinymce/skins/ui/oxide-dark/content.inline.js similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/content.inline.js rename to public/libs/tinymce/skins/ui/oxide-dark/content.inline.js diff --git a/libs/tinymce/skins/ui/oxide-dark/content.inline.min.css b/public/libs/tinymce/skins/ui/oxide-dark/content.inline.min.css similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/content.inline.min.css rename to public/libs/tinymce/skins/ui/oxide-dark/content.inline.min.css diff --git a/libs/tinymce/skins/ui/oxide-dark/content.js b/public/libs/tinymce/skins/ui/oxide-dark/content.js similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/content.js rename to public/libs/tinymce/skins/ui/oxide-dark/content.js diff --git a/libs/tinymce/skins/ui/oxide-dark/content.min.css b/public/libs/tinymce/skins/ui/oxide-dark/content.min.css similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/content.min.css rename to public/libs/tinymce/skins/ui/oxide-dark/content.min.css diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.js b/public/libs/tinymce/skins/ui/oxide-dark/skin.js similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/skin.js rename to public/libs/tinymce/skins/ui/oxide-dark/skin.js diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.min.css b/public/libs/tinymce/skins/ui/oxide-dark/skin.min.css similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/skin.min.css rename to public/libs/tinymce/skins/ui/oxide-dark/skin.min.css diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js b/public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js rename to public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css b/public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css similarity index 100% rename from libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css rename to public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css diff --git a/libs/tinymce/skins/ui/oxide/content.css b/public/libs/tinymce/skins/ui/oxide/content.css similarity index 100% rename from libs/tinymce/skins/ui/oxide/content.css rename to public/libs/tinymce/skins/ui/oxide/content.css diff --git a/libs/tinymce/skins/ui/oxide/content.inline.css b/public/libs/tinymce/skins/ui/oxide/content.inline.css similarity index 100% rename from libs/tinymce/skins/ui/oxide/content.inline.css rename to public/libs/tinymce/skins/ui/oxide/content.inline.css diff --git a/libs/tinymce/skins/ui/oxide/content.inline.js b/public/libs/tinymce/skins/ui/oxide/content.inline.js similarity index 100% rename from libs/tinymce/skins/ui/oxide/content.inline.js rename to public/libs/tinymce/skins/ui/oxide/content.inline.js diff --git a/libs/tinymce/skins/ui/oxide/content.js b/public/libs/tinymce/skins/ui/oxide/content.js similarity index 100% rename from libs/tinymce/skins/ui/oxide/content.js rename to public/libs/tinymce/skins/ui/oxide/content.js diff --git a/libs/tinymce/skins/ui/oxide/skin.css b/public/libs/tinymce/skins/ui/oxide/skin.css similarity index 100% rename from libs/tinymce/skins/ui/oxide/skin.css rename to public/libs/tinymce/skins/ui/oxide/skin.css diff --git a/libs/tinymce/skins/ui/oxide/skin.js b/public/libs/tinymce/skins/ui/oxide/skin.js similarity index 100% rename from libs/tinymce/skins/ui/oxide/skin.js rename to public/libs/tinymce/skins/ui/oxide/skin.js diff --git a/libs/tinymce/skins/ui/oxide/skin.shadowdom.css b/public/libs/tinymce/skins/ui/oxide/skin.shadowdom.css similarity index 100% rename from libs/tinymce/skins/ui/oxide/skin.shadowdom.css rename to public/libs/tinymce/skins/ui/oxide/skin.shadowdom.css diff --git a/libs/tinymce/skins/ui/oxide/skin.shadowdom.js b/public/libs/tinymce/skins/ui/oxide/skin.shadowdom.js similarity index 100% rename from libs/tinymce/skins/ui/oxide/skin.shadowdom.js rename to public/libs/tinymce/skins/ui/oxide/skin.shadowdom.js diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/content.js rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.js diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/content.min.css rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.js rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.js diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5/content.inline.js b/public/libs/tinymce/skins/ui/tinymce-5/content.inline.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/content.inline.js rename to public/libs/tinymce/skins/ui/tinymce-5/content.inline.js diff --git a/libs/tinymce/skins/ui/tinymce-5/content.inline.min.css b/public/libs/tinymce/skins/ui/tinymce-5/content.inline.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/content.inline.min.css rename to public/libs/tinymce/skins/ui/tinymce-5/content.inline.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5/content.js b/public/libs/tinymce/skins/ui/tinymce-5/content.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/content.js rename to public/libs/tinymce/skins/ui/tinymce-5/content.js diff --git a/libs/tinymce/skins/ui/tinymce-5/content.min.css b/public/libs/tinymce/skins/ui/tinymce-5/content.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/content.min.css rename to public/libs/tinymce/skins/ui/tinymce-5/content.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.js b/public/libs/tinymce/skins/ui/tinymce-5/skin.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/skin.js rename to public/libs/tinymce/skins/ui/tinymce-5/skin.js diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.min.css b/public/libs/tinymce/skins/ui/tinymce-5/skin.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/skin.min.css rename to public/libs/tinymce/skins/ui/tinymce-5/skin.min.css diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js b/public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js rename to public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css b/public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css similarity index 100% rename from libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css rename to public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css diff --git a/libs/tinymce/themes/silver/theme.min.js b/public/libs/tinymce/themes/silver/theme.min.js similarity index 100% rename from libs/tinymce/themes/silver/theme.min.js rename to public/libs/tinymce/themes/silver/theme.min.js diff --git a/libs/tinymce/tinymce.d.ts b/public/libs/tinymce/tinymce.d.ts similarity index 100% rename from libs/tinymce/tinymce.d.ts rename to public/libs/tinymce/tinymce.d.ts diff --git a/libs/tinymce/tinymce.min.js b/public/libs/tinymce/tinymce.min.js similarity index 100% rename from libs/tinymce/tinymce.min.js rename to public/libs/tinymce/tinymce.min.js diff --git a/libs/umami.js b/public/libs/umami.js similarity index 100% rename from libs/umami.js rename to public/libs/umami.js diff --git a/main.js b/public/main.js similarity index 98% rename from main.js rename to public/main.js index 29760109..c0ac9d11 100644 --- a/main.js +++ b/public/main.js @@ -13,12 +13,6 @@ const ERROR = true; // detect device const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile; -// typed arrays max values -const INT8_MAX = 127; -const UINT8_MAX = 255; -const UINT16_MAX = 65535; -const UINT32_MAX = 4294967295; - if (PRODUCTION && "serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker.register("./sw.js").catch(err => { @@ -91,7 +85,7 @@ let fogging = viewbox .attr("id", "fogging") .style("display", "none"); let ruler = viewbox.append("g").attr("id", "ruler").style("display", "none"); -let debug = viewbox.append("g").attr("id", "debug"); +var debug = viewbox.append("g").attr("id", "debug"); lakes.append("g").attr("id", "freshwater"); lakes.append("g").attr("id", "salt"); @@ -140,9 +134,9 @@ legend .on("click", () => clearLegend()); // main data variables -let grid = {}; // initial graph based on jittered square grid and data -let pack = {}; // packed graph and data -let seed; +var grid = {}; // initial graph based on jittered square grid and data +var pack = {}; // packed graph and data +var seed; let mapId; let mapHistory = []; let elSelected; @@ -193,7 +187,7 @@ const onZoom = debounce(function () { }, 50); const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoom); -let mapCoordinates = {}; // map coordinates on globe +var mapCoordinates = {}; // map coordinates on globe let populationRate = +byId("populationRateInput").value; let distanceScale = +byId("distanceScaleInput").value; let urbanization = +byId("urbanizationInput").value; @@ -202,8 +196,8 @@ let urbanDensity = +byId("urbanDensityInput").value; applyStoredOptions(); // voronoi graph extension, cannot be changed after generation -let graphWidth = +mapWidthInput.value; -let graphHeight = +mapHeightInput.value; +var graphWidth = +mapWidthInput.value; +var graphHeight = +mapHeightInput.value; // svg canvas resolution, can be changed let svgWidth = graphWidth; @@ -638,6 +632,8 @@ async function generate(options) { Biomes.define(); Features.defineGroups(); + Ice.generate(); + rankCells(); Cultures.generate(); Cultures.expand(); @@ -1233,8 +1229,12 @@ function showStatistics() { Cultures: ${pack.cultures.length - 1}`; mapId = Date.now(); // unique map id is it's creation date number + window.mapId = mapId; // expose for test automation mapHistory.push({seed, width: graphWidth, height: graphHeight, template: heightmap, created: mapId}); INFO && console.info(stats); + + // Dispatch event for test automation and external integrations + window.dispatchEvent(new CustomEvent('map:generated', { detail: { seed, mapId } })); } const regenerateMap = debounce(async function (options) { diff --git a/manifest.webmanifest b/public/manifest.webmanifest similarity index 100% rename from manifest.webmanifest rename to public/manifest.webmanifest diff --git a/modules/coa-generator.js b/public/modules/coa-generator.js similarity index 100% rename from modules/coa-generator.js rename to public/modules/coa-generator.js diff --git a/modules/coa-renderer.js b/public/modules/coa-renderer.js similarity index 100% rename from modules/coa-renderer.js rename to public/modules/coa-renderer.js diff --git a/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js similarity index 87% rename from modules/dynamic/auto-update.js rename to public/modules/dynamic/auto-update.js index 124abfb2..0b1cd227 100644 --- a/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -253,8 +253,8 @@ export function resolveVersionConflicts(mapVersion) { const source = findCell(s.x, s.y); const mouth = findCell(e.x, e.y); const name = Rivers.getName(mouth); - const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; - pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type}); + const type = length < 25 ? rw({ Creek: 9, River: 3, Brook: 3, Stream: 1 }) : "River"; + pack.rivers.push({ i, parent: 0, length, source, mouth, basin: i, name, type }); }); } @@ -270,7 +270,7 @@ export function resolveVersionConflicts(mapVersion) { const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era"; const eraShort = era[0] + "E"; const military = Military.getDefaultOptions(); - options = {winds, year, era, eraShort, military}; + options = { winds, year, era, eraShort, military }; // v1.3 added campaings data for all states States.generateCampaigns(); @@ -481,7 +481,7 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.65.0")) { // v1.65 changed rivers data d3.select("#rivers").attr("style", null); // remove style to unhide layer - const {cells, rivers} = pack; + const { cells, rivers } = pack; const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); for (const river of rivers) { @@ -497,8 +497,8 @@ export function resolveVersionConflicts(mapVersion) { for (let i = 0; i <= segments; i++) { const shift = increment * i; - const {x: x1, y: y1} = node.getPointAtLength(length + shift); - const {x: x2, y: y2} = node.getPointAtLength(length - shift); + const { x: x1, y: y1 } = node.getPointAtLength(length + shift); + const { x: x2, y: y2 } = node.getPointAtLength(length - shift); const x = rn((x1 + x2) / 2, 1); const y = rn((y1 + y2) / 2, 1); @@ -565,7 +565,7 @@ export function resolveVersionConflicts(mapVersion) { const fill = circle && circle.getAttribute("fill"); const stroke = circle && circle.getAttribute("stroke"); - const marker = {i, icon, type, x, y, size, cell}; + const marker = { i, icon, type, x, y, size, cell }; if (size && size !== 30) marker.size = size; if (!isNaN(px) && px !== 12) marker.px = px; if (!isNaN(dx) && dx !== 50) marker.dx = dx; @@ -631,7 +631,7 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.88.0")) { // v1.87 may have incorrect shield for some reason - pack.states.forEach(({coa}) => { + pack.states.forEach(({ coa }) => { if (coa?.shield === "state") delete coa.shield; }); } @@ -639,13 +639,13 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.91.0")) { // from 1.91.00 custom coa is moved to coa object pack.states.forEach(state => { - if (state.coa === "custom") state.coa = {custom: true}; + if (state.coa === "custom") state.coa = { custom: true }; }); pack.provinces.forEach(province => { - if (province.coa === "custom") province.coa = {custom: true}; + if (province.coa === "custom") province.coa = { custom: true }; }); pack.burgs.forEach(burg => { - if (burg.coa === "custom") burg.coa = {custom: true}; + if (burg.coa === "custom") burg.coa = { custom: true }; }); // from 1.91.00 emblems don't have transform attribute @@ -747,7 +747,7 @@ export function resolveVersionConflicts(mapVersion) { const skip = terrs.attr("skip"); const relax = terrs.attr("relax"); - const curveTypes = {0: "curveBasisClosed", 1: "curveLinear", 2: "curveStep"}; + const curveTypes = { 0: "curveBasisClosed", 1: "curveLinear", 2: "curveStep" }; const curve = curveTypes[terrs.attr("curve")] || "curveBasisClosed"; terrs @@ -882,7 +882,7 @@ export function resolveVersionConflicts(mapVersion) { const secondCellId = points[1][2]; const feature = pack.cells.f[secondCellId]; - pack.routes.push({i: pack.routes.length, group, feature, points}); + pack.routes.push({ i: pack.routes.length, group, feature, points }); } } routes.selectAll("path").remove(); @@ -914,7 +914,7 @@ export function resolveVersionConflicts(mapVersion) { const type = this.dataset.type; const color = this.getAttribute("fill"); const cells = this.dataset.cells.split(",").map(Number); - pack.zones.push({i, name, type, cells, color}); + pack.zones.push({ i, name, type, cells, color }); }); zones.style("display", null).selectAll("*").remove(); if (layerIsOn("toggleZones")) drawZones(); @@ -975,49 +975,45 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.109.0")) { // v1.109.0 added customizable burg groups and icons - options.burgs = {groups: []}; + options.burgs = { groups: [] }; - // default groups were 'cities' and 'towns' - const iconGroups = burgIcons.selectAll("g"); - const citiesGroup = burgIcons.select("#cities"); - const townsGroup = burgIcons.select("#towns"); - if (!iconGroups.size() || (iconGroups.size() === 2 && citiesGroup.size() && townsGroup.size())) { - // it looks the loaded map has old default groups - options.burgs.groups = Burgs.getDefaultGroups(); - } else { - burgIcons.selectAll("circle, use").each(function () { - const group = this.parentNode.id; - const id = this.id.replace(/^burg/, ""); - const burg = pack.burgs[id]; - if (group && burg) burg.group = group; - }); + burgIcons.selectAll("circle, use").each(function () { + const group = this.parentNode.id; + const id = this.id.replace(/^burg/, ""); + const burg = pack.burgs[id]; + if (group && burg) burg.group = group; + }); - burgIcons.selectAll("g").each(function (_el, index) { - const name = this.id; - const isDefault = name === "towns"; - options.burgs.groups.push({name, active: true, order: index + 1, isDefault, preview: "watabou-city"}); + burgIcons.selectAll("g").each(function (_el, index) { + const name = this.id; + const isDefault = name === "towns"; + options.burgs.groups.push({ name, active: true, order: index + 1, isDefault, preview: "watabou-city" }); + if (!this.dataset.icon) this.dataset.icon = "#icon-circle"; - const size = Number(this.getAttribute("size") || 2) * 2; - this.removeAttribute("size"); - this.setAttribute("font-size", size); + const size = Number(this.getAttribute("size") || 2) * 2; + this.removeAttribute("size"); + this.setAttribute("font-size", size); - this.setAttribute("stroke-width", 1); - }); + this.setAttribute("stroke-width", 1); + }); - if (options.burgs.groups.filter(g => g.isDefault).length === 0) { - options.burgs.groups[0].isDefault = true; - } - - anchors.selectAll("g").each(function () { - const size = Number(this.getAttribute("size") || 1); - this.removeAttribute("size"); - this.setAttribute("font-size", size); - }); + if (options.burgs.groups.filter(g => g.isDefault).length === 0) { + options.burgs.groups[0].isDefault = true; } - const iconSymbol = byId("icon-anchor"); - if (iconSymbol) { - iconSymbol.outerHTML = /* html */ ` + anchors.selectAll("g").each(function () { + const size = Number(this.getAttribute("size") || 1); + this.removeAttribute("size"); + this.setAttribute("font-size", size); + }); + + burgLabels.selectAll("g").each(function () { + if (!this.dataset.dy) this.dataset.dy = -0.4; + }); + + const anchorSymbol = byId("icon-anchor"); + if (anchorSymbol) { + anchorSymbol.outerHTML = /* html */ ` `; } @@ -1028,15 +1024,86 @@ export function resolveVersionConflicts(mapVersion) { if (!burg.group) Burgs.defineGroup(burg, populations); if (burg.MFCG) { - burg.link = getBurgLink(burg); + burg.link = Burgs.getPreview(burg)?.link; delete burg.MFCG; } }); layerIsOn("toggleBurgIcons") && drawBurgIcons(); + layerIsOn("toggleLabels") && drawBurgLabels(); delete options.showBurgPreview; delete options.showMFCGMap; delete options.villageMaxPopulation; } + + if (isOlderThan("1.111.0")) { + // v1.111.0 moved ice data from SVG to data model + // Migrate old ice SVG elements to new pack.ice structure + if (!pack.ice.length) { + pack.ice = []; + let iceId = 0; + + const iceLayer = document.getElementById("ice"); + if (iceLayer) { + // Migrate glaciers (type="iceShield") + iceLayer.querySelectorAll("polygon[type='iceShield']").forEach(polygon => { + // Parse points string "x1,y1 x2,y2 x3,y3 ..." into array [[x1,y1], [x2,y2], ...] + const points = [...polygon.points].map(svgPoint => [svgPoint.x, svgPoint.y]); + + const transform = polygon.getAttribute("transform"); + const iceElement = { + i: iceId++, + points, + type: "glacier" + }; + if (transform) { + iceElement.offset = parseTransform(transform); + } + pack.ice.push(iceElement); + }); + + // Migrate icebergs + iceLayer.querySelectorAll("polygon:not([type])").forEach(polygon => { + const cellId = +polygon.getAttribute("cell"); + const size = +polygon.getAttribute("size"); + + // points string must exist, cell attribute must be present, and size must be non-zero + if (polygon.getAttribute("cell") === null || !size) return; + + // Parse points string "x1,y1 x2,y2 x3,y3 ..." into array [[x1,y1], [x2,y2], ...] + const points = [...polygon.points].map(svgPoint => [svgPoint.x, svgPoint.y]); + + const transform = polygon.getAttribute("transform"); + const iceElement = { + i: iceId++, + points, + type: "iceberg", + cellId, + size + }; + if (transform) { + iceElement.offset = parseTransform(transform); + } + pack.ice.push(iceElement); + }); + + // Clear old SVG elements + iceLayer.querySelectorAll("*").forEach(el => el.remove()); + } else { + // If ice layer element doesn't exist, create it + ice = viewbox.insert("g", "#coastline").attr("id", "ice"); + ice + .attr("opacity", null) + .attr("fill", "#e8f0f6") + .attr("stroke", "#e8f0f6") + .attr("stroke-width", 1) + .attr("filter", "url(#dropShadow05)"); + } + + // Re-render ice from migrated data + if (layerIsOn("toggleIce")) drawIce(); + } + + } } diff --git a/modules/dynamic/editors/cultures-editor.js b/public/modules/dynamic/editors/cultures-editor.js similarity index 100% rename from modules/dynamic/editors/cultures-editor.js rename to public/modules/dynamic/editors/cultures-editor.js diff --git a/modules/dynamic/editors/religions-editor.js b/public/modules/dynamic/editors/religions-editor.js similarity index 100% rename from modules/dynamic/editors/religions-editor.js rename to public/modules/dynamic/editors/religions-editor.js diff --git a/modules/dynamic/editors/states-editor.js b/public/modules/dynamic/editors/states-editor.js similarity index 100% rename from modules/dynamic/editors/states-editor.js rename to public/modules/dynamic/editors/states-editor.js diff --git a/modules/dynamic/export-json.js b/public/modules/dynamic/export-json.js similarity index 100% rename from modules/dynamic/export-json.js rename to public/modules/dynamic/export-json.js diff --git a/modules/dynamic/heightmap-selection.js b/public/modules/dynamic/heightmap-selection.js similarity index 100% rename from modules/dynamic/heightmap-selection.js rename to public/modules/dynamic/heightmap-selection.js diff --git a/modules/dynamic/hierarchy-tree.js b/public/modules/dynamic/hierarchy-tree.js similarity index 100% rename from modules/dynamic/hierarchy-tree.js rename to public/modules/dynamic/hierarchy-tree.js diff --git a/modules/dynamic/installation.js b/public/modules/dynamic/installation.js similarity index 100% rename from modules/dynamic/installation.js rename to public/modules/dynamic/installation.js diff --git a/modules/dynamic/overview/charts-overview.js b/public/modules/dynamic/overview/charts-overview.js similarity index 100% rename from modules/dynamic/overview/charts-overview.js rename to public/modules/dynamic/overview/charts-overview.js diff --git a/modules/dynamic/supporters.js b/public/modules/dynamic/supporters.js similarity index 100% rename from modules/dynamic/supporters.js rename to public/modules/dynamic/supporters.js diff --git a/modules/fonts.js b/public/modules/fonts.js similarity index 100% rename from modules/fonts.js rename to public/modules/fonts.js diff --git a/public/modules/ice.js b/public/modules/ice.js new file mode 100644 index 00000000..90c7c3e6 --- /dev/null +++ b/public/modules/ice.js @@ -0,0 +1,170 @@ +"use strict"; + +// Ice layer data model - separates ice data from SVG rendering +window.Ice = (function () { + + // Find next available id for new ice element idealy filling gaps + function getNextId() { + if (pack.ice.length === 0) return 0; + // find gaps in existing ids + const existingIds = pack.ice.map(e => e.i).sort((a, b) => a - b); + for (let id = 0; id < existingIds[existingIds.length - 1]; id++) { + if (!existingIds.includes(id)) return id; + } + return existingIds[existingIds.length - 1] + 1; + } + + // Generate glaciers and icebergs based on temperature and height + function generate() { + clear(); + const { cells, features } = grid; + const { temp, h } = cells; + Math.random = aleaPRNG(seed); + + const ICEBERG_MAX_TEMP = 0; + const GLACIER_MAX_TEMP = -8; + const minMaxTemp = d3.min(temp); + + // Generate glaciers on cold land + { + const type = "iceShield"; + const getType = cellId => + h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null; + const isolines = getIsolines(grid, getType, { polygons: true }); + + if (isolines[type]?.polygons) { + isolines[type].polygons.forEach(points => { + const clipped = clipPoly(points); + pack.ice.push({ + i: getNextId(), + points: clipped, + type: "glacier" + }); + }); + } + } + + // Generate icebergs on cold water + for (const cellId of grid.cells.i) { + const t = temp[cellId]; + if (h[cellId] >= 20) continue; // no icebergs on land + if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs + if (features[cells.f[cellId]].type === "lake") continue; // no icebergs on lakes + if (P(0.8)) continue; // skip most of eligible cells + + const randomFactor = 0.8 + rand() * 0.4; // random size factor + let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero, 1 = full + if (cells.t[cellId] === -1) baseSize /= 1.3; // coastline: smaller icebergs + const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1); + + const [cx, cy] = grid.points[cellId]; + const points = getGridPolygon(cellId).map(([x, y]) => [ + rn(lerp(cx, x, size), 2), + rn(lerp(cy, y, size), 2) + ]); + + pack.ice.push({ + i: getNextId(), + points, + type: "iceberg", + cellId, + size + }); + } + } + + function addIceberg(cellId, size) { + const [cx, cy] = grid.points[cellId]; + const points = getGridPolygon(cellId).map(([x, y]) => [ + rn(lerp(cx, x, size), 2), + rn(lerp(cy, y, size), 2) + ]); + const id = getNextId(); + pack.ice.push({ + i: id, + points, + type: "iceberg", + cellId, + size + }); + redrawIceberg(id); + } + + function removeIce(id) { + const index = pack.ice.findIndex(element => element.i === id); + if (index !== -1) { + const type = pack.ice.find(element => element.i === id).type; + pack.ice.splice(index, 1); + if (type === "glacier") { + redrawGlacier(id); + } else { + redrawIceberg(id); + } + + } + } + + function updateIceberg(id, points, size) { + const iceberg = pack.ice.find(element => element.i === id); + if (iceberg) { + iceberg.points = points; + iceberg.size = size; + } + } + + function randomizeIcebergShape(id) { + const iceberg = pack.ice.find(element => element.i === id); + if (!iceberg) return; + + const cellId = iceberg.cellId; + const size = iceberg.size; + const [cx, cy] = grid.points[cellId]; + + // Get a different random cell for the polygon template + const i = ra(grid.cells.i); + const cn = grid.points[i]; + const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]); + const points = poly.map(p => [ + rn(cx + p[0] * size, 2), + rn(cy + p[1] * size, 2) + ]); + + iceberg.points = points; + } + + function changeIcebergSize(id, newSize) { + const iceberg = pack.ice.find(element => element.i === id); + if (!iceberg) return; + + const cellId = iceberg.cellId; + const [cx, cy] = grid.points[cellId]; + const oldSize = iceberg.size; + + const flat = iceberg.points.flat(); + const pairs = []; + while (flat.length) pairs.push(flat.splice(0, 2)); + const poly = pairs.map(p => [(p[0] - cx) / oldSize, (p[1] - cy) / oldSize]); + const points = poly.map(p => [ + rn(cx + p[0] * newSize, 2), + rn(cy + p[1] * newSize, 2) + ]); + + iceberg.points = points; + iceberg.size = newSize; + } + + // Clear all ice + function clear() { + pack.ice = []; + } + + return { + generate, + addIceberg, + removeIce, + updateIceberg, + randomizeIcebergShape, + changeIcebergSize, + clear + }; +})(); diff --git a/modules/io/cloud.js b/public/modules/io/cloud.js similarity index 100% rename from modules/io/cloud.js rename to public/modules/io/cloud.js diff --git a/modules/io/export.js b/public/modules/io/export.js similarity index 100% rename from modules/io/export.js rename to public/modules/io/export.js diff --git a/modules/io/load.js b/public/modules/io/load.js similarity index 99% rename from modules/io/load.js rename to public/modules/io/load.js index 15d85358..9b401733 100644 --- a/modules/io/load.js +++ b/public/modules/io/load.js @@ -406,6 +406,7 @@ async function parseLoadedData(data, mapVersion) { pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length); // data[28] had deprecated cells.crossroad pack.cells.routes = data[36] ? JSON.parse(data[36]) : {}; + pack.ice = data[39] ? JSON.parse(data[39]) : []; if (data[31]) { const namesDL = data[31].split("/"); @@ -449,7 +450,7 @@ async function parseLoadedData(data, mapVersion) { if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes"); if (hasChildren(temperature)) turnOn("toggleTemperature"); if (hasChild(population, "line")) turnOn("togglePopulation"); - if (hasChildren(ice)) turnOn("toggleIce"); + if (isVisible(ice)) turnOn("toggleIce"); if (hasChild(prec, "circle")) turnOn("togglePrecipitation"); if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems"); if (isVisible(labels)) turnOn("toggleLabels"); @@ -472,7 +473,7 @@ async function parseLoadedData(data, mapVersion) { { // dynamically import and run auto-update script - const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.108.0"); + const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.109.4"); resolveVersionConflicts(mapVersion); } diff --git a/modules/io/save.js b/public/modules/io/save.js similarity index 94% rename from modules/io/save.js rename to public/modules/io/save.js index 304fef59..25cd7493 100644 --- a/modules/io/save.js +++ b/public/modules/io/save.js @@ -32,12 +32,13 @@ async function saveMap(method) { $(this).dialog("close"); } }, - position: {my: "center", at: "center", of: "svg"} + position: { my: "center", at: "center", of: "svg" } }); } } function prepareMapData() { + const date = new Date(); const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; @@ -89,8 +90,8 @@ function prepareMapData() { const serializedSVG = new XMLSerializer().serializeToString(cloneEl); - const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid; - const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired}); + const { spacing, cellsX, cellsY, boundary, points, features, cellsDesired } = grid; + const gridGeneral = JSON.stringify({ spacing, cellsX, cellsY, boundary, points, features, cellsDesired }); const packFeatures = JSON.stringify(pack.features); const cultures = JSON.stringify(pack.cultures); const states = JSON.stringify(pack.states); @@ -102,6 +103,7 @@ function prepareMapData() { const cellRoutes = JSON.stringify(pack.cells.routes); const routes = JSON.stringify(pack.routes); const zones = JSON.stringify(pack.zones); + const ice = JSON.stringify(pack.ice); // store name array only if not the same as default const defaultNB = Names.getNameBases(); @@ -155,21 +157,22 @@ function prepareMapData() { markers, cellRoutes, routes, - zones + zones, + ice ].join("\r\n"); return mapData; } // save map file to indexedDB async function saveToStorage(mapData, showTip = false) { - const blob = new Blob([mapData], {type: "text/plain"}); + const blob = new Blob([mapData], { type: "text/plain" }); await ldb.set("lastMap", blob); showTip && tip("Map is saved to the browser storage", false, "success"); } // download map file function saveToMachine(mapData, filename) { - const blob = new Blob([mapData], {type: "text/plain"}); + const blob = new Blob([mapData], { type: "text/plain" }); const URL = window.URL.createObjectURL(blob); const link = document.createElement("a"); diff --git a/modules/markers-generator.js b/public/modules/markers-generator.js similarity index 100% rename from modules/markers-generator.js rename to public/modules/markers-generator.js diff --git a/modules/military-generator.js b/public/modules/military-generator.js similarity index 99% rename from modules/military-generator.js rename to public/modules/military-generator.js index 5aea87db..34c705d8 100644 --- a/modules/military-generator.js +++ b/public/modules/military-generator.js @@ -271,7 +271,7 @@ window.Military = (function () { } if (node.t > expected) return; const r = (expected - node.t) / (node.s ? 40 : 20); // search radius - const candidates = tree.findAll(node.x, node.y, r); + const candidates = findAllInQuadtree(node.x, node.y, r, tree); for (const c of candidates) { if (c.t < expected && mergeable(node, c)) { merge(node, c); diff --git a/modules/resample.js b/public/modules/resample.js similarity index 99% rename from modules/resample.js rename to public/modules/resample.js index 819214b1..b64dde1f 100644 --- a/modules/resample.js +++ b/public/modules/resample.js @@ -28,6 +28,7 @@ window.Resample = (function () { reGraph(); Features.markupPack(); + Ice.generate() createDefaultRuler(); restoreCellData(parentMap, inverse, scale); diff --git a/modules/submap.js b/public/modules/submap.js similarity index 100% rename from modules/submap.js rename to public/modules/submap.js diff --git a/modules/ui/3d.js b/public/modules/ui/3d.js similarity index 100% rename from modules/ui/3d.js rename to public/modules/ui/3d.js diff --git a/modules/ui/ai-generator.js b/public/modules/ui/ai-generator.js similarity index 100% rename from modules/ui/ai-generator.js rename to public/modules/ui/ai-generator.js diff --git a/modules/ui/battle-screen.js b/public/modules/ui/battle-screen.js similarity index 100% rename from modules/ui/battle-screen.js rename to public/modules/ui/battle-screen.js diff --git a/modules/ui/biomes-editor.js b/public/modules/ui/biomes-editor.js similarity index 98% rename from modules/ui/biomes-editor.js rename to public/modules/ui/biomes-editor.js index 8c50993d..125aa0da 100644 --- a/modules/ui/biomes-editor.js +++ b/public/modules/ui/biomes-editor.js @@ -136,11 +136,13 @@ function editBiomes() { body.innerHTML = lines; // update footer + const totalMapArea = getArea(d3.sum(pack.cells.area)); biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length; biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length; biomesFooterArea.innerHTML = si(totalArea) + unit; biomesFooterPopulation.innerHTML = si(totalPopulation); biomesFooterArea.dataset.area = totalArea; + biomesFooterArea.dataset.mapArea = totalMapArea; biomesFooterPopulation.dataset.population = totalPopulation; // add listeners @@ -255,6 +257,7 @@ function editBiomes() { body.dataset.type = "percentage"; const totalCells = +biomesFooterCells.innerHTML; const totalArea = +biomesFooterArea.dataset.area; + const totalMapArea = +biomesFooterArea.dataset.mapArea; const totalPopulation = +biomesFooterPopulation.dataset.population; body.querySelectorAll(":scope> div").forEach(function (el) { @@ -262,6 +265,9 @@ function editBiomes() { el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100) + "%"; el.querySelector(".biomePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + "%"; }); + + // update footer to show land percentage of total map + biomesFooterArea.innerHTML = rn((totalArea / totalMapArea) * 100) + "%"; } else { body.dataset.type = "absolute"; biomesEditorAddLines(); diff --git a/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js similarity index 100% rename from modules/ui/burg-editor.js rename to public/modules/ui/burg-editor.js diff --git a/modules/ui/burg-group-editor.js b/public/modules/ui/burg-group-editor.js similarity index 94% rename from modules/ui/burg-group-editor.js rename to public/modules/ui/burg-group-editor.js index 09efc130..48fc8089 100644 --- a/modules/ui/burg-group-editor.js +++ b/public/modules/ui/burg-group-editor.js @@ -13,7 +13,7 @@ function editBurgGroups() { byId("burgGroupsForm").requestSubmit(); }, Add: () => { - byId("burgGroupsBody").innerHTML += createLine({name: "", active: true, preview: null}); + byId("burgGroupsBody").insertAdjacentHTML("beforeend", createLine({name: "", active: true, preview: null})); }, Restore: () => { options.burgs.groups = Burgs.getDefaultGroups(); @@ -69,9 +69,9 @@ function editBurgGroups() { - - - + + + diff --git a/modules/ui/burgs-overview.js b/public/modules/ui/burgs-overview.js similarity index 95% rename from modules/ui/burgs-overview.js rename to public/modules/ui/burgs-overview.js index ac18ab56..5b061fd4 100644 --- a/modules/ui/burgs-overview.js +++ b/public/modules/ui/burgs-overview.js @@ -28,6 +28,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { byId("burgsChart").addEventListener("click", showBurgsChart); byId("burgsFilterState").addEventListener("change", burgsOverviewAddLines); byId("burgsFilterCulture").addEventListener("change", burgsOverviewAddLines); + byId("burgsSearch").addEventListener("input", burgsOverviewAddLines); byId("regenerateBurgNames").addEventListener("click", regenerateNames); byId("addNewBurg").addEventListener("click", enterAddBurgMode); byId("burgsExport").addEventListener("click", downloadBurgsData); @@ -63,9 +64,30 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { // add line for each burg function burgsOverviewAddLines() { + const searchText = byId("burgsSearch").value.toLowerCase().trim(); const selectedStateId = +byId("burgsFilterState").value; const selectedCultureId = +byId("burgsFilterCulture").value; - let filtered = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs + + const validBurgs = pack.burgs.filter(b => b.i && !b.removed); + let filtered = validBurgs; + + if (searchText) { + // filter by search text + filtered = filtered.filter(b => { + const name = b.name.toLowerCase(); + const state = (pack.states[b.state]?.name || "").toLowerCase(); + const prov = pack.cells.province[b.cell]; + const province = prov ? pack.provinces[prov]?.name.toLowerCase() : ""; + const culture = (pack.cultures[b.culture]?.name || "").toLowerCase(); + return ( + name.includes(searchText) || + state.includes(searchText) || + province.includes(searchText) || + culture.includes(searchText) || + b.group.toLowerCase().includes(searchText) + ); + }); + } if (selectedStateId !== -1) filtered = filtered.filter(b => b.state === selectedStateId); // filtered by state if (selectedCultureId !== -1) filtered = filtered.filter(b => b.culture === selectedCultureId); // filtered by culture @@ -119,7 +141,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { body.insertAdjacentHTML("beforeend", lines); // update footer - burgsFooterBurgs.innerHTML = filtered.length; + burgsFooterBurgs.innerHTML = `${filtered.length} of ${validBurgs.length}`; burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0; // add listeners diff --git a/modules/ui/coastline-editor.js b/public/modules/ui/coastline-editor.js similarity index 100% rename from modules/ui/coastline-editor.js rename to public/modules/ui/coastline-editor.js diff --git a/modules/ui/diplomacy-editor.js b/public/modules/ui/diplomacy-editor.js similarity index 100% rename from modules/ui/diplomacy-editor.js rename to public/modules/ui/diplomacy-editor.js diff --git a/modules/ui/editors.js b/public/modules/ui/editors.js similarity index 99% rename from modules/ui/editors.js rename to public/modules/ui/editors.js index 77c391ee..50eaf1c7 100644 --- a/modules/ui/editors.js +++ b/public/modules/ui/editors.js @@ -26,7 +26,7 @@ function clicked() { else if (ancestor.id === "labels" && el.tagName === "tspan") editLabel(); else if (grand.id === "burgLabels") editBurg(); else if (grand.id === "burgIcons") editBurg(); - else if (parent.id === "ice") editIce(); + else if (parent.id === "ice") editIce(el); else if (parent.id === "terrain") editReliefIcon(); else if (grand.id === "markers" || great.id === "markers") editMarker(); else if (grand.id === "coastline") editCoastline(); diff --git a/modules/ui/elevation-profile.js b/public/modules/ui/elevation-profile.js similarity index 100% rename from modules/ui/elevation-profile.js rename to public/modules/ui/elevation-profile.js diff --git a/modules/ui/emblems-editor.js b/public/modules/ui/emblems-editor.js similarity index 100% rename from modules/ui/emblems-editor.js rename to public/modules/ui/emblems-editor.js diff --git a/modules/ui/general.js b/public/modules/ui/general.js similarity index 100% rename from modules/ui/general.js rename to public/modules/ui/general.js diff --git a/modules/ui/heightmap-editor.js b/public/modules/ui/heightmap-editor.js similarity index 99% rename from modules/ui/heightmap-editor.js rename to public/modules/ui/heightmap-editor.js index d655e39d..6e76c4bb 100644 --- a/modules/ui/heightmap-editor.js +++ b/public/modules/ui/heightmap-editor.js @@ -259,6 +259,8 @@ function editHeightmap(options) { Rivers.specify(); Lakes.defineNames(); + Ice.generate(); + Military.generate(); Markers.generate(); Zones.generate(); @@ -465,6 +467,10 @@ function editHeightmap(options) { .attr("id", d => base + d); }); + // recalculate ice + Ice.generate(); + ice.selectAll("*").remove(); + TIME && console.timeEnd("restoreRiskedData"); INFO && console.groupEnd("Edit Heightmap"); } @@ -669,7 +675,7 @@ function editHeightmap(options) { if (power === 0) return tip("Power should not be zero", false, "error"); const heights = grid.cells.h; - const operation = power > 0 ? HeightmapGenerator.addRange : HeightmapGenerator.addTrough; + const operation = power > 0 ? HeightmapGenerator.addRange.bind(HeightmapGenerator) : HeightmapGenerator.addTrough.bind(HeightmapGenerator); HeightmapGenerator.setGraph(grid); operation("1", String(Math.abs(power)), null, null, fromCell, toCell); const changedHeights = HeightmapGenerator.getHeights(); diff --git a/modules/ui/hotkeys.js b/public/modules/ui/hotkeys.js similarity index 100% rename from modules/ui/hotkeys.js rename to public/modules/ui/hotkeys.js diff --git a/modules/ui/ice-editor.js b/public/modules/ui/ice-editor.js similarity index 53% rename from modules/ui/ice-editor.js rename to public/modules/ui/ice-editor.js index a9e6ff28..16818b4c 100644 --- a/modules/ui/ice-editor.js +++ b/public/modules/ui/ice-editor.js @@ -1,26 +1,32 @@ "use strict"; -function editIce() { +function editIce(element) { if (customization) return; + if (elSelected && element === elSelected.node()) return; + closeDialogs(".stable"); if (!layerIsOn("toggleIce")) toggleIce(); elSelected = d3.select(d3.event.target); - const type = elSelected.attr("type") ? "Glacier" : "Iceberg"; - document.getElementById("iceRandomize").style.display = type === "Glacier" ? "none" : "inline-block"; - document.getElementById("iceSize").style.display = type === "Glacier" ? "none" : "inline-block"; - if (type === "Iceberg") document.getElementById("iceSize").value = +elSelected.attr("size"); + const id = +elSelected.attr("data-id"); + const iceElement = pack.ice.find(el => el.i === id); + const isGlacier = elSelected.attr("type") === "glacier"; + const type = isGlacier ? "Glacier" : "Iceberg"; + + document.getElementById("iceRandomize").style.display = isGlacier ? "none" : "inline-block"; + document.getElementById("iceSize").style.display = isGlacier ? "none" : "inline-block"; + if (!isGlacier) document.getElementById("iceSize").value = iceElement?.size || ""; + ice.selectAll("*").classed("draggable", true).call(d3.drag().on("drag", dragElement)); $("#iceEditor").dialog({ title: "Edit " + type, resizable: false, - position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"}, + position: { my: "center top+60", at: "top", of: d3.event, collision: "fit" }, close: closeEditor }); if (modules.editIce) return; modules.editIce = true; - // add listeners document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice")); document.getElementById("iceRandomize").addEventListener("click", randomizeShape); @@ -28,29 +34,18 @@ function editIce() { document.getElementById("iceNew").addEventListener("click", toggleAdd); document.getElementById("iceRemove").addEventListener("click", removeIce); + function randomizeShape() { - const c = grid.points[+elSelected.attr("cell")]; - const s = +elSelected.attr("size"); - const i = ra(grid.cells.i), - cn = grid.points[i]; - const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]); - const points = poly.map(p => [rn(c[0] + p[0] * s, 2), rn(c[1] + p[1] * s, 2)]); - elSelected.attr("points", points); + const selectedId = +elSelected.attr("data-id"); + Ice.randomizeIcebergShape(selectedId); + redrawIceberg(selectedId); } function changeSize() { - const c = grid.points[+elSelected.attr("cell")]; - const s = +elSelected.attr("size"); - const flat = elSelected - .attr("points") - .split(",") - .map(el => +el); - const pairs = []; - while (flat.length) pairs.push(flat.splice(0, 2)); - const poly = pairs.map(p => [(p[0] - c[0]) / s, (p[1] - c[1]) / s]); - const size = +this.value; - const points = poly.map(p => [rn(c[0] + p[0] * size, 2), rn(c[1] + p[1] * size, 2)]); - elSelected.attr("points", points).attr("size", size); + const newSize = +this.value; + const selectedId = +elSelected.attr("data-id"); + Ice.changeIcebergSize(selectedId, newSize); + redrawIceberg(selectedId); } function toggleAdd() { @@ -67,17 +62,15 @@ function editIce() { function addIcebergOnClick() { const [x, y] = d3.mouse(this); const i = findGridCell(x, y, grid); - const [cx, cy] = grid.points[i]; const size = +document.getElementById("iceSize")?.value || 1; - const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]); - const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size); - iceberg.call(d3.drag().on("drag", dragElement)); + Ice.addIceberg(i, size); + if (d3.event.shiftKey === false) toggleAdd(); } function removeIce() { - const type = elSelected.attr("type") ? "Glacier" : "Iceberg"; + const type = elSelected.attr("type") === "glacier" ? "Glacier" : "Iceberg"; alertMessage.innerHTML = /* html */ `Are you sure you want to remove the ${type}?`; $("#alert").dialog({ resizable: false, @@ -85,7 +78,7 @@ function editIce() { buttons: { Remove: function () { $(this).dialog("close"); - elSelected.remove(); + Ice.removeIce(+elSelected.attr("data-id")); $("#iceEditor").dialog("close"); }, Cancel: function () { @@ -96,14 +89,24 @@ function editIce() { } function dragElement() { - const tr = parseTransform(this.getAttribute("transform")); - const dx = +tr[0] - d3.event.x, - dy = +tr[1] - d3.event.y; + const selectedId = +elSelected.attr("data-id"); + const initialTransform = parseTransform(this.getAttribute("transform")); + const dx = initialTransform[0] - d3.event.x; + const dy = initialTransform[1] - d3.event.y; d3.event.on("drag", function () { - const x = d3.event.x, - y = d3.event.y; - this.setAttribute("transform", `translate(${dx + x},${dy + y})`); + const x = d3.event.x; + const y = d3.event.y; + const transform = `translate(${dx + x},${dy + y})`; + this.setAttribute("transform", transform); + + // Update data model with new position + const offset = [dx + x, dy + y]; + const iceData = pack.ice.find(element => element.i === selectedId); + if (iceData) { + // Store offset for visual positioning, actual geometry stays in points + iceData.offset = offset; + } }); } @@ -114,3 +117,4 @@ function editIce() { unselect(); } } + diff --git a/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js similarity index 100% rename from modules/ui/labels-editor.js rename to public/modules/ui/labels-editor.js diff --git a/modules/ui/lakes-editor.js b/public/modules/ui/lakes-editor.js similarity index 100% rename from modules/ui/lakes-editor.js rename to public/modules/ui/lakes-editor.js diff --git a/modules/ui/layers.js b/public/modules/ui/layers.js similarity index 95% rename from modules/ui/layers.js rename to public/modules/ui/layers.js index 5037a5ee..f2f04a4b 100644 --- a/modules/ui/layers.js +++ b/public/modules/ui/layers.js @@ -417,49 +417,6 @@ function toggleIce(event) { } } -function drawIce() { - TIME && console.time("drawIce"); - - const {cells, features} = grid; - const {temp, h} = cells; - Math.random = aleaPRNG(seed); - - const ICEBERG_MAX_TEMP = 0; - const GLACIER_MAX_TEMP = -8; - const minMaxTemp = d3.min(temp); - - // cold land: draw glaciers - { - const type = "iceShield"; - const getType = cellId => (h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null); - const isolines = getIsolines(grid, getType, {polygons: true}); - isolines[type]?.polygons?.forEach(points => { - const clipped = clipPoly(points); - ice.append("polygon").attr("points", clipped).attr("type", type); - }); - } - - // cold water: draw icebergs - for (const cellId of grid.cells.i) { - const t = temp[cellId]; - if (h[cellId] >= 20) continue; // no icebergs on land - if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs - if (features[cells.f[cellId]].type === "lake") continue; // no icebers on lakes - if (P(0.8)) continue; // skip most of eligible cells - - const randomFactor = 0.8 + rand() * 0.4; // random size factor - let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero size, 1 = full size - if (cells.t[cellId] === -1) baseSize /= 1.3; // coasline: smaller icebergs - const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1); - - const [cx, cy] = grid.points[cellId]; - const points = getGridPolygon(cellId).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]); - ice.append("polygon").attr("points", points).attr("cell", cellId).attr("size", size); - } - - TIME && console.timeEnd("drawIce"); -} - function toggleCultures(event) { const cultures = pack.cultures.filter(c => c.i && !c.removed); const empty = !cults.selectAll("path").size(); diff --git a/modules/ui/markers-editor.js b/public/modules/ui/markers-editor.js similarity index 100% rename from modules/ui/markers-editor.js rename to public/modules/ui/markers-editor.js diff --git a/modules/ui/markers-overview.js b/public/modules/ui/markers-overview.js similarity index 83% rename from modules/ui/markers-overview.js rename to public/modules/ui/markers-overview.js index 02999eb0..dd59d0aa 100644 --- a/modules/ui/markers-overview.js +++ b/public/modules/ui/markers-overview.js @@ -4,18 +4,19 @@ function overviewMarkers() { closeDialogs("#markersOverview, .stable"); if (!layerIsOn("toggleMarkers")) toggleMarkers(); - const markerGroup = document.getElementById("markers"); - const body = document.getElementById("markersBody"); - const markersInverPin = document.getElementById("markersInverPin"); - const markersInverLock = document.getElementById("markersInverLock"); - const markersFooterNumber = document.getElementById("markersFooterNumber"); - const markersOverviewRefresh = document.getElementById("markersOverviewRefresh"); - const markersAddFromOverview = document.getElementById("markersAddFromOverview"); - const markersGenerationConfig = document.getElementById("markersGenerationConfig"); - const markersRemoveAll = document.getElementById("markersRemoveAll"); - const markersExport = document.getElementById("markersExport"); - const markerTypeInput = document.getElementById("addedMarkerType"); - const markerTypeSelector = document.getElementById("markerTypeSelector"); + const markerGroup = byId("markers"); + const body = byId("markersBody"); + const markersInverPin = byId("markersInverPin"); + const markersInverLock = byId("markersInverLock"); + const markersFooterNumber = byId("markersFooterNumber"); + const markersOverviewRefresh = byId("markersOverviewRefresh"); + const markersAddFromOverview = byId("markersAddFromOverview"); + const markersGenerationConfig = byId("markersGenerationConfig"); + const markersRemoveAll = byId("markersRemoveAll"); + const markersExport = byId("markersExport"); + const markerTypeInput = byId("addedMarkerType"); + const markerTypeSelector = byId("markerTypeSelector"); + const markersSearch = byId("markersSearch"); addLines(); @@ -36,7 +37,8 @@ function overviewMarkers() { listen(markersGenerationConfig, "click", configMarkersGeneration), listen(markersRemoveAll, "click", triggerRemoveAll), listen(markersExport, "click", exportMarkers), - listen(markerTypeSelector, "click", toggleMarkerTypeMenu) + listen(markerTypeSelector, "click", toggleMarkerTypeMenu), + listen(markersSearch, "input", addLines) ]; const types = [{type: "empty", icon: "❓"}, ...Markers.getConfig()]; @@ -67,7 +69,17 @@ function overviewMarkers() { } function addLines() { - const lines = pack.markers + let markers = pack.markers; + + const searchText = byId("markersSearch").value.toLowerCase().trim(); + if (searchText) { + markers = markers.filter(marker => { + const type = (marker.type || "").toLowerCase(); + return type.includes(searchText); + }); + } + + const lines = markers .map(({i, type, icon, pinned, lock}) => { return /* html */ `
@@ -91,7 +103,8 @@ function overviewMarkers() { .join(""); body.innerHTML = lines; - markersFooterNumber.innerText = pack.markers.length; + markersFooterNumber.innerText = markers.length; + markersFooterTotal.innerText = pack.markers.length; applySorting(markersHeader); } @@ -127,7 +140,7 @@ function overviewMarkers() { } function focusOnMarker(i) { - highlightElement(document.getElementById(`marker${i}`), 2); + highlightElement(byId(`marker${i}`), 2); } function pinMarker(el, i) { @@ -165,7 +178,7 @@ function overviewMarkers() { } function toggleMarkerTypeMenu() { - document.getElementById("markerTypeSelectMenu").classList.toggle("visible"); + byId("markerTypeSelectMenu").classList.toggle("visible"); } function toggleAddMarker() { @@ -182,7 +195,7 @@ function overviewMarkers() { function removeMarker(i) { notes = notes.filter(note => note.id !== `marker${i}`); pack.markers = pack.markers.filter(marker => marker.i !== i); - document.getElementById(`marker${i}`)?.remove(); + byId(`marker${i}`)?.remove(); addLines(); } @@ -200,7 +213,7 @@ function overviewMarkers() { if (lock) return true; const id = `marker${i}`; - document.getElementById(id)?.remove(); + byId(id)?.remove(); notes = notes.filter(note => note.id !== id); return false; }); diff --git a/modules/ui/measurers.js b/public/modules/ui/measurers.js similarity index 100% rename from modules/ui/measurers.js rename to public/modules/ui/measurers.js diff --git a/modules/ui/military-overview.js b/public/modules/ui/military-overview.js similarity index 100% rename from modules/ui/military-overview.js rename to public/modules/ui/military-overview.js diff --git a/modules/ui/namesbase-editor.js b/public/modules/ui/namesbase-editor.js similarity index 100% rename from modules/ui/namesbase-editor.js rename to public/modules/ui/namesbase-editor.js diff --git a/modules/ui/notes-editor.js b/public/modules/ui/notes-editor.js similarity index 100% rename from modules/ui/notes-editor.js rename to public/modules/ui/notes-editor.js diff --git a/modules/ui/options.js b/public/modules/ui/options.js similarity index 100% rename from modules/ui/options.js rename to public/modules/ui/options.js diff --git a/modules/ui/provinces-editor.js b/public/modules/ui/provinces-editor.js similarity index 100% rename from modules/ui/provinces-editor.js rename to public/modules/ui/provinces-editor.js diff --git a/modules/ui/regiment-editor.js b/public/modules/ui/regiment-editor.js similarity index 100% rename from modules/ui/regiment-editor.js rename to public/modules/ui/regiment-editor.js diff --git a/modules/ui/regiments-overview.js b/public/modules/ui/regiments-overview.js similarity index 100% rename from modules/ui/regiments-overview.js rename to public/modules/ui/regiments-overview.js diff --git a/modules/ui/relief-editor.js b/public/modules/ui/relief-editor.js similarity index 99% rename from modules/ui/relief-editor.js rename to public/modules/ui/relief-editor.js index abb800cd..44a2c727 100644 --- a/modules/ui/relief-editor.js +++ b/public/modules/ui/relief-editor.js @@ -201,7 +201,7 @@ function editReliefIcon() { d3.event.on("drag", function () { const p = d3.mouse(this); moveCircle(p[0], p[1], r); - tree.findAll(p[0], p[1], r).forEach(f => f[2].remove()); + findAllInQuadtree(p[0], p[1], r, tree).forEach(f => f[2].remove()); }); } diff --git a/modules/ui/rivers-creator.js b/public/modules/ui/rivers-creator.js similarity index 100% rename from modules/ui/rivers-creator.js rename to public/modules/ui/rivers-creator.js diff --git a/modules/ui/rivers-editor.js b/public/modules/ui/rivers-editor.js similarity index 100% rename from modules/ui/rivers-editor.js rename to public/modules/ui/rivers-editor.js diff --git a/modules/ui/rivers-overview.js b/public/modules/ui/rivers-overview.js similarity index 74% rename from modules/ui/rivers-overview.js rename to public/modules/ui/rivers-overview.js index 7fc32b45..c062424f 100644 --- a/modules/ui/rivers-overview.js +++ b/public/modules/ui/rivers-overview.js @@ -5,7 +5,7 @@ function overviewRivers() { closeDialogs("#riversOverview, .stable"); if (!layerIsOn("toggleRivers")) toggleRivers(); - const body = document.getElementById("riversBody"); + const body = byId("riversBody"); riversOverviewAddLines(); $("#riversOverview").dialog(); @@ -20,12 +20,13 @@ function overviewRivers() { }); // add listeners - document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines); - document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver); - document.getElementById("riverCreateNew").addEventListener("click", createRiver); - document.getElementById("riversBasinHighlight").addEventListener("click", toggleBasinsHightlight); - document.getElementById("riversExport").addEventListener("click", downloadRiversData); - document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove); + byId("riversOverviewRefresh").on("click", riversOverviewAddLines); + byId("addNewRiver").on("click", toggleAddRiver); + byId("riverCreateNew").on("click", createRiver); + byId("riversBasinHighlight").on("click", toggleBasinsHightlight); + byId("riversExport").on("click", downloadRiversData); + byId("riversRemoveAll").on("click", triggerAllRiversRemove); + byId("riversSearch").on("input", riversOverviewAddLines); // add line for each river function riversOverviewAddLines() { @@ -33,11 +34,26 @@ function overviewRivers() { let lines = ""; const unit = distanceUnitInput.value; - for (const r of pack.rivers) { + // Precompute a lookup map from river id to river for efficient basin lookup + const riversById = new Map(pack.rivers.map(river => [river.i, river])); + + let filteredRivers = pack.rivers; + const searchText = byId("riversSearch").value.toLowerCase().trim(); + if (searchText) { + filteredRivers = filteredRivers.filter(r => { + const name = (r.name || "").toLowerCase(); + const type = (r.type || "").toLowerCase(); + const basin = riversById.get(r.basin); + const basinName = basin ? (basin.name || "").toLowerCase() : ""; + return name.includes(searchText) || type.includes(searchText) || basinName.includes(searchText); + }); + } + + for (const r of filteredRivers) { const discharge = r.discharge + " m³/s"; const length = rn(r.length * distanceScale) + " " + unit; const width = rn(r.width * distanceScale, 3) + " " + unit; - const basin = pack.rivers.find(river => river.i === r.basin)?.name; + const basin = riversById.get(r.basin)?.name; lines += /* html */ `
r.discharge))); + riversFooterNumber.innerHTML = `${filteredRivers.length} of ${pack.rivers.length}`; + const averageDischarge = rn(d3.mean(filteredRivers.map(r => r.discharge))) || 0; riversFooterDischarge.innerHTML = averageDischarge + " m³/s"; - const averageLength = rn(d3.mean(pack.rivers.map(r => r.length))); + const averageLength = rn(d3.mean(filteredRivers.map(r => r.length))) || 0; riversFooterLength.innerHTML = averageLength * distanceScale + " " + unit; - const averageWidth = rn(d3.mean(pack.rivers.map(r => r.width)), 3); + const averageWidth = rn(d3.mean(filteredRivers.map(r => r.width)), 3) || 0; riversFooterWidth.innerHTML = rn(averageWidth * distanceScale, 3) + " " + unit; // add listeners - body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => riverHighlightOn(ev))); - body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev))); - body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver)); - body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor)); - body - .querySelectorAll("div > span.icon-trash-empty") - .forEach(el => el.addEventListener("click", triggerRiverRemove)); + body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", ev => riverHighlightOn(ev))); + body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", ev => riverHighlightOff(ev))); + body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRiver)); + body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRiverEditor)); + body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRiverRemove)); applySorting(riversHeader); } diff --git a/modules/ui/route-group-editor.js b/public/modules/ui/route-group-editor.js similarity index 100% rename from modules/ui/route-group-editor.js rename to public/modules/ui/route-group-editor.js diff --git a/modules/ui/routes-creator.js b/public/modules/ui/routes-creator.js similarity index 100% rename from modules/ui/routes-creator.js rename to public/modules/ui/routes-creator.js diff --git a/modules/ui/routes-editor.js b/public/modules/ui/routes-editor.js similarity index 100% rename from modules/ui/routes-editor.js rename to public/modules/ui/routes-editor.js diff --git a/modules/ui/routes-overview.js b/public/modules/ui/routes-overview.js similarity index 88% rename from modules/ui/routes-overview.js rename to public/modules/ui/routes-overview.js index cf731068..883df3de 100644 --- a/modules/ui/routes-overview.js +++ b/public/modules/ui/routes-overview.js @@ -25,13 +25,25 @@ function overviewRoutes() { byId("routesExport").on("click", downloadRoutesData); byId("routesLockAll").on("click", toggleLockAll); byId("routesRemoveAll").on("click", triggerAllRoutesRemove); + byId("routesSearch").on("input", routesOverviewAddLines); // add line for each route function routesOverviewAddLines() { body.innerHTML = ""; let lines = ""; - for (const route of pack.routes) { + let filteredRoutes = pack.routes; + + const searchText = byId("routesSearch").value.toLowerCase().trim(); + if (searchText) { + filteredRoutes = filteredRoutes.filter(route => { + const name = (route.name || "").toLowerCase(); + const group = (route.group || "").toLowerCase(); + return name.includes(searchText) || group.includes(searchText); + }); + } + + for (const route of filteredRoutes) { if (!route.points || route.points.length < 2) continue; route.name = route.name || Routes.generateName(route); route.length = route.length || Routes.getLength(route.i); @@ -58,8 +70,8 @@ function overviewRoutes() { body.insertAdjacentHTML("beforeend", lines); // update footer - routesFooterNumber.innerHTML = pack.routes.length; - const averageLength = rn(d3.mean(pack.routes.map(r => r.length)) || 0); + routesFooterNumber.innerHTML = `${filteredRoutes.length} of ${pack.routes.length}`; + const averageLength = rn(d3.mean(filteredRoutes.map(r => r.length)) || 0) || 0; routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value; // add listeners @@ -67,7 +79,7 @@ function overviewRoutes() { body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", routeHighlightOff)); body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRoute)); body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRouteEditor)); - body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleLockStatus)); + body.querySelectorAll("div > span.locks").forEach(el => el.on("click", toggleLockStatus)); body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove)); applySorting(routesHeader); diff --git a/modules/ui/style-presets.js b/public/modules/ui/style-presets.js similarity index 100% rename from modules/ui/style-presets.js rename to public/modules/ui/style-presets.js diff --git a/modules/ui/style.js b/public/modules/ui/style.js similarity index 100% rename from modules/ui/style.js rename to public/modules/ui/style.js diff --git a/modules/ui/submap-tool.js b/public/modules/ui/submap-tool.js similarity index 100% rename from modules/ui/submap-tool.js rename to public/modules/ui/submap-tool.js diff --git a/modules/ui/temperature-graph.js b/public/modules/ui/temperature-graph.js similarity index 100% rename from modules/ui/temperature-graph.js rename to public/modules/ui/temperature-graph.js diff --git a/modules/ui/tools.js b/public/modules/ui/tools.js similarity index 99% rename from modules/ui/tools.js rename to public/modules/ui/tools.js index a3df5c00..eade993f 100644 --- a/modules/ui/tools.js +++ b/public/modules/ui/tools.js @@ -555,7 +555,7 @@ function regenerateMilitary() { function regenerateIce() { if (!layerIsOn("toggleIce")) toggleIce(); - ice.selectAll("*").remove(); + Ice.generate(); drawIce(); } diff --git a/modules/ui/transform-tool.js b/public/modules/ui/transform-tool.js similarity index 100% rename from modules/ui/transform-tool.js rename to public/modules/ui/transform-tool.js diff --git a/modules/ui/units-editor.js b/public/modules/ui/units-editor.js similarity index 100% rename from modules/ui/units-editor.js rename to public/modules/ui/units-editor.js diff --git a/modules/ui/world-configurator.js b/public/modules/ui/world-configurator.js similarity index 100% rename from modules/ui/world-configurator.js rename to public/modules/ui/world-configurator.js diff --git a/modules/ui/zones-editor.js b/public/modules/ui/zones-editor.js similarity index 100% rename from modules/ui/zones-editor.js rename to public/modules/ui/zones-editor.js diff --git a/styles/ancient.json b/public/styles/ancient.json similarity index 100% rename from styles/ancient.json rename to public/styles/ancient.json diff --git a/styles/atlas.json b/public/styles/atlas.json similarity index 100% rename from styles/atlas.json rename to public/styles/atlas.json diff --git a/styles/clean.json b/public/styles/clean.json similarity index 100% rename from styles/clean.json rename to public/styles/clean.json diff --git a/styles/cyberpunk.json b/public/styles/cyberpunk.json similarity index 100% rename from styles/cyberpunk.json rename to public/styles/cyberpunk.json diff --git a/styles/darkSeas.json b/public/styles/darkSeas.json similarity index 100% rename from styles/darkSeas.json rename to public/styles/darkSeas.json diff --git a/styles/default.json b/public/styles/default.json similarity index 100% rename from styles/default.json rename to public/styles/default.json diff --git a/styles/gloom.json b/public/styles/gloom.json similarity index 100% rename from styles/gloom.json rename to public/styles/gloom.json diff --git a/styles/light.json b/public/styles/light.json similarity index 100% rename from styles/light.json rename to public/styles/light.json diff --git a/styles/monochrome.json b/public/styles/monochrome.json similarity index 100% rename from styles/monochrome.json rename to public/styles/monochrome.json diff --git a/styles/night.json b/public/styles/night.json similarity index 100% rename from styles/night.json rename to public/styles/night.json diff --git a/styles/pale.json b/public/styles/pale.json similarity index 100% rename from styles/pale.json rename to public/styles/pale.json diff --git a/styles/watercolor.json b/public/styles/watercolor.json similarity index 100% rename from styles/watercolor.json rename to public/styles/watercolor.json diff --git a/sw.js b/public/sw.js similarity index 100% rename from sw.js rename to public/sw.js diff --git a/versioning.js b/public/versioning.js similarity index 97% rename from versioning.js rename to public/versioning.js index c3a6da86..fd2a67a2 100644 --- a/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.109.1"; +const VERSION = "1.112.1"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { @@ -37,6 +37,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
    Latest changes: +
  • Search input in Overview dialogs
  • Custom burg grouping and icon selection
  • Ability to set custom image as Marker or Regiment icon
  • Submap and Transform tools rework
  • @@ -48,8 +49,6 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
  • New routes generation algorithm
  • Routes overview tool
  • Configurable longitude
  • -
  • Preview villages map
  • -
  • Ability to render ocean heightmap

Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.

diff --git a/run_php_server.bat b/run_php_server.bat deleted file mode 100644 index e168921d..00000000 --- a/run_php_server.bat +++ /dev/null @@ -1,3 +0,0 @@ -start chrome.exe http://localhost:3000/ -@echo off -php -S localhost:3000 diff --git a/run_python_server.bat b/run_python_server.bat deleted file mode 100644 index b74d34c1..00000000 --- a/run_python_server.bat +++ /dev/null @@ -1,3 +0,0 @@ -start chrome.exe http://localhost:8000/ -@echo off -python -m http.server 8000 \ No newline at end of file diff --git a/run_python_server.sh b/run_python_server.sh deleted file mode 100644 index 7ac82957..00000000 --- a/run_python_server.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env sh -if command -v python3 >/dev/null 2>&1; then - PYTHON=python3 -elif command -v python >/dev/null 2>&1; then - PYTHON=python -else - echo "Neither 'python' nor 'python3' was found. Please install Python 3 package." >&2 - exit 1 -fi - -chromium http://localhost:8000 - -$PYTHON -m http.server 8000 diff --git a/index.html b/src/index.html similarity index 99% rename from index.html rename to src/index.html index 210513c3..6173e519 100644 --- a/index.html +++ b/src/index.html @@ -5369,17 +5369,29 @@
-
- - +
+ - - + + +
- Burgs: 0 + Burgs: 0 of 0
@@ -5420,12 +5432,7 @@ Order Name Preview generator - Population - - Percentile - + Population Biomes States Cultures @@ -5460,7 +5467,7 @@
- Total routes: 0 + Routes: 0
Average length: 0 @@ -5481,11 +5488,14 @@ > +