From 8ba29b2561a1e2f0c9ce8c96df5a1a7a786e08e8 Mon Sep 17 00:00:00 2001 From: Marc Emmanuel Date: Tue, 3 Feb 2026 17:22:25 +0100 Subject: [PATCH] refactor: migrate zones (#1300) * refactor: migrate zones * refactor: remove duplicate markers property from PackedGraph interface --- public/modules/zones-generator.js | 454 -------------------- src/index.html | 1 - src/modules/index.ts | 1 + src/modules/zones-generator.ts | 668 ++++++++++++++++++++++++++++++ src/types/PackedGraph.ts | 4 +- 5 files changed, 672 insertions(+), 456 deletions(-) delete mode 100644 public/modules/zones-generator.js create mode 100644 src/modules/zones-generator.ts 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[]; }