refactor: migrate zones

This commit is contained in:
Marc Emmanuel 2026-01-31 22:06:41 +01:00
parent 454178fa99
commit d61dcb35aa
5 changed files with 672 additions and 455 deletions

View file

@ -8498,7 +8498,6 @@
<script defer src="modules/religions-generator.js?v=1.106.0"></script>
<script defer src="modules/military-generator.js?v=1.107.0"></script>
<script defer src="modules/markers-generator.js?v=1.107.0"></script>
<script defer src="modules/zones-generator.js?v=1.106.0"></script>
<script defer src="modules/coa-generator.js?v=1.99.00"></script>
<script defer src="modules/resample.js?v=1.106.4"></script>
<script defer src="libs/alea.min.js?v1.105.0"></script>

View file

@ -10,3 +10,4 @@ import "./biomes";
import "./cultures-generator";
import "./routes-generator";
import "./states-generator";
import "./zones-generator";

View file

@ -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<string, ZoneConfig>;
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();

View file

@ -4,6 +4,7 @@ import type { PackedGraphFeature } from "../modules/features";
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
@ -56,4 +57,6 @@ export interface PackedGraph {
cultures: Culture[];
routes: Route[];
religions: any[];
zones: Zone[];
markers: any[];
}