Fantasy-Map-Generator/src/modules/zones.js
2022-07-05 21:12:55 +03:00

452 lines
13 KiB
JavaScript

import * as d3 from "d3";
import FlatQueue from "flatqueue";
import {TIME} from "config/logging";
import {findCell, getPackPolygon} from "utils/graphUtils";
import {getAdjective} from "utils/languageUtils";
import {rn} from "utils/numberUtils";
import {P, ra, rand, rw} from "utils/probabilityUtils";
import {byId} from "utils/shorthands";
// generate zones
export function addZones(number = 1) {
TIME && console.time("addZones");
const {cells, states, burgs} = pack;
const used = new Uint8Array(cells.i.length); // to store used cells
const zonesData = [];
const randomize = modifier => rn(Math.random() * modifier * number);
for (let i = 0; i < randomize(1.8); i++) addInvasion(); // invasion of enemy lands
for (let i = 0; i < randomize(1.6); i++) addRebels(); // rebels along a state border
for (let i = 0; i < randomize(1.6); i++) addProselytism(); // proselitism of organized religion
for (let i = 0; i < randomize(1.6); i++) addCrusade(); // crusade on heresy lands
for (let i = 0; i < randomize(1.8); i++) addDisease(); // disease starting in a random city
for (let i = 0; i < randomize(1.4); i++) addDisaster(); // disaster starting in a random city
for (let i = 0; i < randomize(1.4); i++) addEruption(); // volcanic eruption aroung volcano
for (let i = 0; i < randomize(1.0); i++) addAvalanche(); // avalanche impacting highland road
for (let i = 0; i < randomize(1.4); i++) addFault(); // fault line in elevated areas
for (let i = 0; i < randomize(1.4); i++) addFlood(); // flood on river banks
for (let i = 0; i < randomize(1.2); i++) addTsunami(); // tsunami starting near coast
drawZones();
function addInvasion() {
const atWar = states.filter(s => s.diplomacy && s.diplomacy.some(d => d === "Enemy"));
if (!atWar.length) return;
const invader = ra(atWar);
const target = invader.diplomacy.findIndex(d => d === "Enemy");
const cell = ra(
cells.i.filter(i => cells.state[i] === target && cells.c[i].some(c => cells.state[c] === invader.i))
);
if (!cell) return;
const cellsArray = [],
queue = [cell],
power = rand(5, 30);
while (queue.length) {
const q = P(0.4) ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.state[e] !== target) return;
used[e] = 1;
queue.push(e);
});
}
const invasion = rw({
Invasion: 4,
Occupation: 3,
Raid: 2,
Conquest: 2,
Subjugation: 1,
Foray: 1,
Skirmishes: 1,
Incursion: 2,
Pillaging: 1,
Intervention: 1
});
const name = getAdjective(invader.name) + " " + invasion;
zonesData.push({name, type: "Invasion", cells: cellsArray, fill: "url(#hatch1)"});
}
function addRebels() {
const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(n => n)));
if (!state) return;
const neib = ra(state.neighbors.filter(n => n && !states[n].removed));
if (!neib) return;
const cell = cells.i.find(
i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib)
);
const cellsArray = [];
const queue = [];
if (cell) queue.push(cell);
const power = rand(10, 30);
while (queue.length) {
const q = queue.shift();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.state[e] !== state.i) return;
used[e] = 1;
if (e % 4 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return;
queue.push(e);
});
}
const rebels = rw({
Rebels: 5,
Insurgents: 2,
Mutineers: 1,
Rioters: 1,
Separatists: 1,
Secessionists: 1,
Insurrection: 2,
Rebellion: 1,
Conspiracy: 2
});
const name = getAdjective(states[neib].name) + " " + rebels;
zonesData.push({name, type: "Rebels", cells: cellsArray, fill: "url(#hatch3)"});
}
function addProselytism() {
const organized = ra(pack.religions.filter(r => r.type === "Organized"));
if (!organized) return;
const cell = ra(
cells.i.filter(
i =>
cells.religion[i] &&
cells.religion[i] !== organized.i &&
cells.c[i].some(c => cells.religion[c] === organized.i)
)
);
if (!cell) return;
const target = cells.religion[cell];
const cellsArray = [],
queue = [cell],
power = rand(10, 30);
while (queue.length) {
const q = queue.shift();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.religion[e] !== target) return;
if (cells.h[e] < 20) return;
used[e] = 1;
//if (e%2 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return;
queue.push(e);
});
}
const name = getAdjective(organized.name.split(" ")[0]) + " Proselytism";
zonesData.push({name, type: "Proselytism", cells: cellsArray, fill: "url(#hatch6)"});
}
function addCrusade() {
const heresy = ra(pack.religions.filter(r => r.type === "Heresy"));
if (!heresy) return;
const cellsArray = cells.i.filter(i => !used[i] && cells.religion[i] === heresy.i);
if (!cellsArray.length) return;
cellsArray.forEach(i => (used[i] = 1));
const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade";
zonesData.push({name, type: "Crusade", cells: cellsArray, fill: "url(#hatch6)"});
}
function addDisease() {
const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [];
const costs = [];
const power = rand(20, 37);
const queue = new FlatQueue();
queue.push(burg.cell, 0);
while (queue.length) {
const priority = queue.peekValue();
const next = queue.pop();
if (cells.burg[next] || cells.pop[next]) cellsArray.push(next);
used[next] = 1;
cells.c[next].forEach(neibCellId => {
const roadValue = cells.road[next];
const cost = roadValue ? Math.max(10 - roadValue, 1) : 100;
const totalPriority = priority + cost;
if (totalPriority > power) return;
if (!costs[neibCellId] || totalPriority < costs[neibCellId]) {
costs[neibCellId] = totalPriority;
queue.push(neibCellId, totalPriority);
}
});
}
const adjective = () =>
ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]);
const animal = () =>
ra([
"Ape",
"Bear",
"Boar",
"Cat",
"Cow",
"Dog",
"Pig",
"Fox",
"Bird",
"Horse",
"Rat",
"Raven",
"Sheep",
"Spider",
"Wolf"
]);
const color = () =>
ra([
"Golden",
"White",
"Black",
"Red",
"Pink",
"Purple",
"Blue",
"Green",
"Yellow",
"Amber",
"Orange",
"Brown",
"Grey"
]);
const type = rw({
Fever: 5,
Pestilence: 2,
Flu: 2,
Pox: 2,
Smallpox: 2,
Plague: 4,
Cholera: 2,
Dropsy: 1,
Leprosy: 2
});
const name = rw({[color()]: 4, [animal()]: 2, [adjective()]: 1}) + " " + type;
zonesData.push({name, type: "Disease", cells: cellsArray, fill: "url(#hatch12)"});
}
function addDisaster() {
const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [];
const costs = [];
const power = rand(5, 25);
const queue = new FlatQueue();
queue.push(burg.cell, 0);
while (queue.length) {
const priority = queue.peekValue();
const next = queue.pop();
if (cells.burg[next] || cells.pop[next]) cellsArray.push(next);
used[next] = 1;
cells.c[next].forEach(neibCellId => {
const cost = rand(1, 10);
const totalPriority = priority + cost;
if (totalPriority > power) return;
if (!costs[neibCellId] || totalPriority < costs[neibCellId]) {
costs[neibCellId] = totalPriority;
queue.push(neibCellId, totalPriority);
}
});
}
const type = rw({Famine: 5, Dearth: 1, Drought: 3, Earthquake: 3, Tornadoes: 1, Wildfires: 1});
const name = getAdjective(burg.name) + " " + type;
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"});
}
function addEruption() {
const volcano = byId("markers").querySelector("use[data-id='#marker_volcano']");
if (!volcano) return;
const x = +volcano.dataset.x,
y = +volcano.dataset.y,
cell = findCell(x, y);
const id = volcano.id;
const note = notes.filter(n => n.id === id);
if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano");
const name = note[0] ? note[0].name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption";
const cellsArray = [];
const queue = [cell];
const power = rand(10, 30);
while (queue.length) {
const q = P(0.5) ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.h[e] < 20) return;
used[e] = 1;
queue.push(e);
});
}
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch7)"});
}
function addAvalanche() {
const roads = cells.i.filter(i => !used[i] && cells.road[i] && cells.h[i] >= 70);
if (!roads.length) return;
const cell = +ra(roads);
const cellsArray = [];
const queue = [cell];
const power = rand(3, 15);
while (queue.length) {
const q = P(0.3) ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.h[e] < 65) return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Avalanche";
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"});
}
function addFault() {
const elevated = cells.i.filter(i => !used[i] && cells.h[i] > 50 && cells.h[i] < 70);
if (!elevated.length) return;
const cell = ra(elevated);
const cellsArray = [];
const queue = [cell];
const power = rand(3, 15);
while (queue.length) {
const q = queue.pop();
if (cells.h[q] >= 20) cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.r[e]) return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Fault";
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch2)"});
}
function addFlood() {
const fl = cells.fl.filter(fl => fl),
meanFlux = d3.mean(fl),
maxFlux = d3.max(fl),
flux = (maxFlux - meanFlux) / 2 + meanFlux;
const rivers = cells.i.filter(
i => !used[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > flux && cells.burg[i]
);
if (!rivers.length) return;
const cell = +ra(rivers),
river = cells.r[cell];
const cellsArray = [];
const queue = [cell];
const power = rand(5, 30);
while (queue.length) {
const q = queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.h[e] < 20 || cells.r[e] !== river || cells.h[e] > 50 || cells.fl[e] < meanFlux) return;
used[e] = 1;
queue.push(e);
});
}
const name = getAdjective(burgs[cells.burg[cell]].name) + " Flood";
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"});
}
function addTsunami() {
const coastal = cells.i.filter(i => !used[i] && cells.t[i] === -1 && pack.features[cells.f[i]].type !== "lake");
if (!coastal.length) return;
const cell = +ra(coastal);
const cellsArray = [];
const queue = [cell];
const power = rand(10, 30);
while (queue.length) {
const q = queue.shift();
if (cells.t[q] === 1) cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.t[e] > 2) return;
if (pack.features[cells.f[e]].type === "lake") return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Tsunami";
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"});
}
function drawZones() {
zones
.selectAll("g")
.data(zonesData)
.enter()
.append("g")
.attr("id", (d, i) => "zone" + i)
.attr("data-description", d => d.name)
.attr("data-type", d => d.type)
.attr("data-cells", d => d.cells.join(","))
.attr("fill", d => d.fill)
.selectAll("polygon")
.data(d => d.cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", function (d) {
return this.parentNode.id + "_" + d;
});
}
TIME && console.timeEnd("addZones");
}