diff --git a/public/modules/zones-generator.js b/public/modules/zones-generator.js
deleted file mode 100644
index 641a0784..00000000
--- a/public/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/src/index.html b/src/index.html
index cc360142..6173e519 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8497,7 +8497,6 @@
-
diff --git a/src/modules/index.ts b/src/modules/index.ts
index aca8bc37..a9ebf2b8 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -10,5 +10,6 @@ import "./biomes";
import "./cultures-generator";
import "./routes-generator";
import "./states-generator";
+import "./zones-generator";
import "./religions-generator";
import "./provinces-generator";
diff --git a/src/modules/zones-generator.ts b/src/modules/zones-generator.ts
new file mode 100644
index 00000000..bef9ad9b
--- /dev/null
+++ b/src/modules/zones-generator.ts
@@ -0,0 +1,668 @@
+import { max, mean } from "d3";
+import { gauss, getAdjective, P, ra, rand, rw } from "../utils";
+
+declare global {
+ var Zones: ZonesModule;
+}
+
+export interface Zone {
+ i: number;
+ name: string;
+ type: string;
+ cells: number[];
+ color: string;
+}
+
+type ZoneGenerator = (usedCells: Uint8Array) => void;
+
+interface ZoneConfig {
+ quantity: number;
+ generate: ZoneGenerator;
+}
+
+class ZonesModule {
+ private config: Record;
+
+ constructor() {
+ this.config = {
+ invasion: { quantity: 2, generate: (u) => this.addInvasion(u) },
+ rebels: { quantity: 1.5, generate: (u) => this.addRebels(u) },
+ proselytism: { quantity: 1.6, generate: (u) => this.addProselytism(u) },
+ crusade: { quantity: 1.6, generate: (u) => this.addCrusade(u) },
+ disease: { quantity: 1.4, generate: (u) => this.addDisease(u) },
+ disaster: { quantity: 1, generate: (u) => this.addDisaster(u) },
+ eruption: { quantity: 1, generate: (u) => this.addEruption(u) },
+ avalanche: { quantity: 0.8, generate: (u) => this.addAvalanche(u) },
+ fault: { quantity: 1, generate: (u) => this.addFault(u) },
+ flood: { quantity: 1, generate: (u) => this.addFlood(u) },
+ tsunami: { quantity: 1, generate: (u) => this.addTsunami(u) },
+ };
+ }
+
+ generate(globalModifier = 1) {
+ TIME && console.time("generateZones");
+
+ const usedCells = new Uint8Array(pack.cells.i.length);
+ pack.zones = [];
+
+ Object.values(this.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");
+ }
+
+ private addInvasion(usedCells: Uint8Array) {
+ const { cells, states } = pack;
+
+ const ongoingConflicts = states
+ .filter((s) => s.i && !s.removed && s.campaigns)
+ .flatMap((s) => s.campaigns!)
+ .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: number[] = [];
+ 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)",
+ });
+ }
+
+ private addRebels(usedCells: Uint8Array) {
+ 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: number) => n && !states[n].removed),
+ );
+ if (!neibStateId) return;
+
+ const cellsArray: number[] = [];
+ const queue: number[] = [];
+ 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)",
+ });
+ }
+
+ private addProselytism(usedCells: Uint8Array) {
+ 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: number[] = [];
+ 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[neibCellId]) 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)",
+ });
+ }
+
+ private addCrusade(usedCells: Uint8Array) {
+ 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;
+ for (const i of crusadeCells) {
+ 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)",
+ });
+ }
+
+ private addDisease(usedCells: Uint8Array) {
+ const { cells, burgs } = pack;
+
+ const burg = ra(
+ burgs.filter((b) => !usedCells[b.cell] && b.i && !b.removed),
+ );
+ if (!burg) return;
+
+ const cellsArray: number[] = [];
+ const cost: number[] = [];
+ 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);
+ }
+ });
+ }
+
+ const colorName = this.getDiseaseName("color");
+ const animalName = this.getDiseaseName("animal");
+ const adjectiveName = this.getDiseaseName("adjective");
+
+ const model = rw({ color: 2, animal: 1, adjective: 1 });
+ const prefix =
+ model === "color"
+ ? colorName
+ : model === "animal"
+ ? animalName
+ : adjectiveName;
+
+ const disease = 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,
+ });
+ const name = `${prefix} ${disease}`;
+
+ pack.zones.push({
+ i: pack.zones.length,
+ name,
+ type: "Disease",
+ cells: cellsArray,
+ color: "url(#hatch12)",
+ });
+ }
+
+ private getDiseaseName(model: "color" | "animal" | "adjective"): string {
+ 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",
+ ]);
+ return ra([
+ "Blind",
+ "Bloody",
+ "Brutal",
+ "Burning",
+ "Deadly",
+ "Fatal",
+ "Furious",
+ "Great",
+ "Grim",
+ "Horrible",
+ "Invisible",
+ "Lethal",
+ "Loud",
+ "Mortal",
+ "Savage",
+ "Severe",
+ "Silent",
+ "Unknown",
+ "Venomous",
+ "Vicious",
+ ]);
+ }
+
+ private addDisaster(usedCells: Uint8Array) {
+ 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: number[] = [];
+ const cost: number[] = [];
+ 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((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)",
+ });
+ }
+
+ private addEruption(usedCells: Uint8Array) {
+ 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: number[] = [];
+ 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)",
+ });
+ }
+
+ private addAvalanche(usedCells: Uint8Array) {
+ 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: number[] = [];
+ 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)",
+ });
+ }
+
+ private addFault(usedCells: Uint8Array) {
+ 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: number[] = [];
+ 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)",
+ });
+ }
+
+ private addFlood(usedCells: Uint8Array) {
+ const cells = pack.cells;
+
+ const fl = cells.fl.filter(Boolean);
+ const meanFlux = mean(fl) ?? 0;
+ const maxFlux = max(fl) ?? 0;
+ 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: number[] = [];
+ 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)",
+ });
+ }
+
+ private addTsunami(usedCells: Uint8Array) {
+ 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: number[] = [];
+ 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)",
+ });
+ }
+}
+
+window.Zones = new ZonesModule();
diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts
index 5e003af1..b8749f0a 100644
--- a/src/types/PackedGraph.ts
+++ b/src/types/PackedGraph.ts
@@ -5,6 +5,7 @@ import type { Province } from "../modules/provinces-generator";
import type { River } from "../modules/river-generator";
import type { Route } from "../modules/routes-generator";
import type { State } from "../modules/states-generator";
+import type { Zone } from "../modules/zones-generator";
type TypedArray =
| Uint8Array
@@ -58,7 +59,8 @@ export interface PackedGraph {
cultures: Culture[];
routes: Route[];
religions: any[];
- ice: any[];
+ zones: Zone[];
markers: any[];
+ ice: any[];
provinces: Province[];
}