mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
refactor: generate relations
This commit is contained in:
parent
907e916b45
commit
73431ec743
10 changed files with 204 additions and 14 deletions
|
|
@ -673,7 +673,7 @@ function getFileName(dataType) {
|
|||
return name + " " + type + dateString;
|
||||
}
|
||||
|
||||
function downloadFile(data, name, type = "text/plain") {
|
||||
export function downloadFile(data, name, type = "text/plain") {
|
||||
const dataBlob = new Blob([data], {type});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {initLayers, renderLayer, restoreLayers} from "layers";
|
|||
// @ts-expect-error js module
|
||||
import {drawScaleBar, Rulers} from "modules/measurers";
|
||||
// @ts-expect-error js module
|
||||
import {unfog} from "modules/ui/editors";
|
||||
import {unfog, downloadFile} from "modules/ui/editors";
|
||||
// @ts-expect-error js module
|
||||
import {applyMapSize, randomizeOptions} from "modules/ui/options";
|
||||
// @ts-expect-error js module
|
||||
|
|
@ -26,7 +26,6 @@ import {createGrid} from "./grid/grid";
|
|||
import {createPack} from "./pack/pack";
|
||||
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
||||
import {calculateMapCoordinates} from "modules/coordinates";
|
||||
import {drawPoint} from "utils/debugUtils";
|
||||
|
||||
const {Zoom, ThreeD} = window;
|
||||
|
||||
|
|
@ -77,6 +76,8 @@ async function generate(options?: IGenerationOptions) {
|
|||
// if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});
|
||||
// });
|
||||
|
||||
downloadDiplomacyData();
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||
// showStatistics();
|
||||
INFO && console.groupEnd();
|
||||
|
|
@ -85,6 +86,19 @@ async function generate(options?: IGenerationOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
function downloadDiplomacyData() {
|
||||
const states = pack.states.filter(s => s.i && !s.removed);
|
||||
const valid = states.map(s => s.i);
|
||||
|
||||
let data = "," + states.map(s => s.name).join(",") + "\n"; // headers
|
||||
states.forEach(s => {
|
||||
const rels = s.diplomacy.filter((v, i) => valid.includes(i));
|
||||
data += s.name + "," + rels.join(",") + "\n";
|
||||
});
|
||||
|
||||
// downloadFile(data, "relations.csv");
|
||||
}
|
||||
|
||||
function showGenerationError(error: Error) {
|
||||
clearMainTip();
|
||||
ERROR && console.error(error);
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function generatePrecipitation(heights: Uint8Array, temperatures: Int8Arr
|
|||
return precipitation;
|
||||
}
|
||||
|
||||
// TODO: move to renderer
|
||||
// TODO: move to renderers
|
||||
function drawWindDirection() {
|
||||
const wind = prec.append("g").attr("id", "wind");
|
||||
|
||||
|
|
|
|||
|
|
@ -106,3 +106,10 @@ export const adjectivalForms = [
|
|||
"Horde",
|
||||
"Marches"
|
||||
];
|
||||
|
||||
export const relations = {
|
||||
neighbors: {Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9},
|
||||
neighborsOfNeighbors: {Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1},
|
||||
farStates: {Friendly: 1, Neutral: 12, Suspicion: 2},
|
||||
navalToNaval: {Neutral: 2, Suspicion: 2, Rival: 1}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {getMixedColor} from "utils/colorUtils";
|
||||
import {ra} from "utils/probabilityUtils";
|
||||
import type {TStateStatistics} from "./collectStatistics";
|
||||
|
||||
export function defineStateColors(statistics: TStateStatistics) {
|
||||
const scheme: Hex[] = d3.shuffle(["#e78ac3", "#a6d854", "#ffd92f", "#66c2a5", "#fc8d62", "#8da0cb"]);
|
||||
const colors: Record<number, Hex> = {};
|
||||
|
||||
// assign colors using greedy algorithm
|
||||
for (const i in statistics) {
|
||||
const {neighbors} = statistics[i];
|
||||
const schemeColor = scheme.find(schemeColor => neighbors.every(neighbor => colors[neighbor] !== schemeColor));
|
||||
colors[i] = schemeColor || ra(scheme);
|
||||
scheme.push(scheme.shift()!);
|
||||
}
|
||||
|
||||
// make each color unique
|
||||
for (const i in colors) {
|
||||
const isColorReused = Object.values(colors).some(color => color === colors[i]);
|
||||
if (isColorReused) colors[i] = getMixedColor(colors[i], 0.3);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import {NAMEBASE} from "config/namebases";
|
||||
import * as d3 from "d3";
|
||||
|
||||
import {NAMEBASE} from "config/namebases";
|
||||
import {getInputNumber} from "utils/nodeUtils";
|
||||
import {P, rand, rw} from "utils/probabilityUtils";
|
||||
import type {TStateStatistics} from "./collectStatistics";
|
||||
import {AreaTiers, culturalMonarchyFormsMap, culturalTheocracyFormsMap, StateForms} from "./config";
|
||||
import type {TStateStatistics} from "./collectStatistics";
|
||||
|
||||
// create 5 area tiers, 4 is the biggest, 0 the smallest
|
||||
export function createAreaTiers(statistics: TStateStatistics) {
|
||||
|
|
@ -59,7 +59,7 @@ function defineFormName(
|
|||
if (form === "Theocracy") return defineTheocracyForm(nameBase, areaTier);
|
||||
if (form === "Anarchy") return rw(StateForms.anarchy);
|
||||
|
||||
return "test";
|
||||
throw new Error("Unknown state form: " + form);
|
||||
}
|
||||
|
||||
// Default name depends on area tier, some name bases have special names for tiers
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {createCapitals} from "./createCapitals";
|
|||
import {createStateData} from "./createStateData";
|
||||
import {createTowns} from "./createTowns";
|
||||
import {expandStates} from "./expandStates";
|
||||
import {generateDiplomacy} from "./generateDiplomacy";
|
||||
import {specifyBurgs} from "./specifyBurgs";
|
||||
import {specifyStates} from "./specifyStates";
|
||||
|
||||
|
|
@ -86,7 +87,8 @@ export function generateBurgsAndStates(
|
|||
const burgIds = assignBurgIds(burgs);
|
||||
|
||||
const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs);
|
||||
const states = specifyStates(statesData, statistics, cultures, burgs);
|
||||
const {chronicle, diplomacy} = generateDiplomacy(statesData, statistics, pick(cells, "f"));
|
||||
const states = specifyStates(statesData, statistics, diplomacy, cultures, burgs);
|
||||
|
||||
return {burgIds, stateIds, burgs, states};
|
||||
|
||||
|
|
|
|||
126
src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts
Normal file
126
src/scripts/generation/pack/burgsAndStates/generateDiplomacy.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {TIME} from "config/logging";
|
||||
import {TStateStatistics} from "./collectStatistics";
|
||||
import {TStateData} from "./createStateData";
|
||||
import {P, rw} from "utils/probabilityUtils";
|
||||
import {relations} from "./config";
|
||||
|
||||
export type TDiplomacy = {[key: number]: TRelations[]};
|
||||
|
||||
interface IDiplomacyData {
|
||||
i: number;
|
||||
type: TCultureType;
|
||||
center: number;
|
||||
area: number;
|
||||
neighbors: number[];
|
||||
}
|
||||
|
||||
export function generateDiplomacy(
|
||||
statesData: TStateData[],
|
||||
statistics: TStateStatistics,
|
||||
cells: Pick<IPack["cells"], "f">
|
||||
) {
|
||||
TIME && console.time("generateDiplomacy");
|
||||
|
||||
const chronicle: string[] = [];
|
||||
const diplomacy = getBlankDiplomacyMatrix(statesData);
|
||||
|
||||
if (statesData.length < 2) {
|
||||
return {chronicle, diplomacy};
|
||||
}
|
||||
|
||||
const stateAreas = Object.values(statistics).map(({area}) => area);
|
||||
const averageStateArea = d3.mean(stateAreas)!;
|
||||
|
||||
for (let i = 0; i < statesData.length; i++) {
|
||||
const from = getDiplomacyData(statesData[i], statistics);
|
||||
|
||||
if (diplomacy[from.i].includes("Vassal")) {
|
||||
// Vassal copy relations from its Suzerain
|
||||
const suzerain = diplomacy[from.i].indexOf("Vassal");
|
||||
|
||||
for (const to of statesData) {
|
||||
if (from.i === to.i || to.i === suzerain) continue;
|
||||
diplomacy[from.i][to.i] = diplomacy[suzerain][to.i];
|
||||
|
||||
// vassals are Ally to each other
|
||||
if (diplomacy[suzerain][to.i] === "Suzerain") diplomacy[from.i][to.i] = "Ally";
|
||||
|
||||
for (let e = 0; e < statesData.length; e++) {
|
||||
const nested = statesData[e];
|
||||
if (nested.i === from.i || nested.i === suzerain) continue;
|
||||
if (diplomacy[nested.i][suzerain] === "Suzerain" || diplomacy[nested.i][suzerain] === "Vassal") continue;
|
||||
diplomacy[nested.i][from.i] = diplomacy[nested.i][suzerain];
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < statesData.length; j++) {
|
||||
const to = getDiplomacyData(statesData[j], statistics);
|
||||
|
||||
if (diplomacy[to.i].includes("Vassal")) {
|
||||
// relations to vassal is the same as to its Suzerain
|
||||
const suzerain = diplomacy[to.i].indexOf("Vassal");
|
||||
diplomacy[from.i][to.i] = diplomacy[from.i][suzerain];
|
||||
continue;
|
||||
}
|
||||
|
||||
const isVassal = detectVassalState(from, to, averageStateArea);
|
||||
if (isVassal) {
|
||||
diplomacy[from.i][to.i] = "Suzerain";
|
||||
diplomacy[to.i][from.i] = "Vassal";
|
||||
continue;
|
||||
}
|
||||
|
||||
const relations = defineRelations(from, to, cells.f);
|
||||
diplomacy[from.i][to.i] = relations;
|
||||
diplomacy[to.i][from.i] = relations;
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("generateDiplomacy");
|
||||
return {chronicle, diplomacy};
|
||||
}
|
||||
|
||||
function getBlankDiplomacyMatrix(statesData: TStateData[]) {
|
||||
const length = statesData.length + 1;
|
||||
|
||||
return statesData.reduce((acc, {i}) => {
|
||||
acc[i] = new Array(length).fill("x");
|
||||
return acc;
|
||||
}, {} as TDiplomacy);
|
||||
}
|
||||
|
||||
function getDiplomacyData(stateData: TStateData, statistics: TStateStatistics): IDiplomacyData {
|
||||
const {i, type, center} = stateData;
|
||||
const {neighbors, area} = statistics[i];
|
||||
return {i, type, center, neighbors, area};
|
||||
}
|
||||
|
||||
function detectVassalState(from: IDiplomacyData, to: IDiplomacyData, averageStateArea: number) {
|
||||
if (P(0.2)) return false;
|
||||
|
||||
const isNeighbor = from.neighbors.includes(to.i);
|
||||
if (!isNeighbor) return false;
|
||||
|
||||
const isMuchSmaller = from.area * 2 < to.area && from.area < averageStateArea && to.area > averageStateArea;
|
||||
if (isMuchSmaller) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function defineRelations(from: IDiplomacyData, to: IDiplomacyData, featureIds: Uint16Array) {
|
||||
const isNeighbor = from.neighbors.includes(to.i);
|
||||
if (isNeighbor) return rw(relations.neighbors); // relations between neighboring states
|
||||
|
||||
const isNeighborOfNeighbor = from.neighbors.some(neighbor => to.neighbors.includes(neighbor));
|
||||
if (isNeighborOfNeighbor) return rw(relations.neighborsOfNeighbors); // relations between neighbors of neighbors
|
||||
|
||||
const isNaval = from.type === "Naval" && to.type === "Naval" && featureIds[from.center] !== featureIds[to.center];
|
||||
if (isNaval) return rw(relations.navalToNaval); // relations between naval states on different islands
|
||||
|
||||
return rw(relations.farStates); // relations between far states
|
||||
}
|
||||
|
|
@ -1,28 +1,31 @@
|
|||
import {TIME} from "config/logging";
|
||||
import {getColors} from "utils/colorUtils";
|
||||
import {NEUTRALS} from "./config";
|
||||
import {createAreaTiers, defineStateForm} from "./defineStateForm";
|
||||
import {defineFullStateName, defineStateName} from "./defineStateName";
|
||||
import {defineStateColors} from "./defineStateColors";
|
||||
import {isBurg} from "utils/typeUtils";
|
||||
|
||||
import type {TStateStatistics} from "./collectStatistics";
|
||||
import type {TStateData} from "./createStateData";
|
||||
import type {TDiplomacy} from "./generateDiplomacy";
|
||||
|
||||
export function specifyStates(
|
||||
statesData: TStateData[],
|
||||
statistics: TStateStatistics,
|
||||
diplomacy: TDiplomacy,
|
||||
cultures: TCultures,
|
||||
burgs: TBurgs
|
||||
): TStates {
|
||||
TIME && console.time("specifyStates");
|
||||
|
||||
const colors = getColors(statesData.length);
|
||||
const colors = defineStateColors(statistics);
|
||||
const getAreaTier = createAreaTiers(statistics);
|
||||
const getNameBase = (cultureId: number) => cultures[cultureId].base;
|
||||
|
||||
const states: IState[] = statesData.map((stateData, index) => {
|
||||
const states: IState[] = statesData.map(stateData => {
|
||||
const {i, center, type, culture, capital} = stateData;
|
||||
const {area, burgs: burgsNumber, ...stats} = statistics[i];
|
||||
const color = colors[i];
|
||||
|
||||
const capitalBurg = burgs[capital];
|
||||
const capitalName = isBurg(capitalBurg) ? capitalBurg.name : null;
|
||||
|
|
@ -34,9 +37,18 @@ export function specifyStates(
|
|||
const name = defineStateName(center, capitalName, nameBase, formName);
|
||||
const fullName = defineFullStateName(name, formName);
|
||||
|
||||
const color = colors[index];
|
||||
|
||||
const state: IState = {name, ...stateData, form, formName, fullName, color, area, burgs: burgsNumber, ...stats};
|
||||
const state: IState = {
|
||||
name,
|
||||
...stateData,
|
||||
form,
|
||||
formName,
|
||||
fullName,
|
||||
color,
|
||||
area,
|
||||
burgs: burgsNumber,
|
||||
...stats,
|
||||
diplomacy: diplomacy[i]
|
||||
};
|
||||
return state;
|
||||
});
|
||||
|
||||
|
|
|
|||
3
src/types/pack/states.d.ts
vendored
3
src/types/pack/states.d.ts
vendored
|
|
@ -18,6 +18,7 @@ interface IState {
|
|||
rural: number;
|
||||
urban: number;
|
||||
neighbors: number[];
|
||||
diplomacy: TRelations[];
|
||||
removed?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -36,3 +37,5 @@ interface ICoa {
|
|||
shield: string;
|
||||
t1: string;
|
||||
}
|
||||
|
||||
type TRelations = "Ally" | "Friendly" | "Neutral" | "Suspicion" | "Rival" | "Unknown" | "Suzerain" | "Vassal" | "x";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue