mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
Refactor layers rendering (#1120)
* feat: render states - use global fn * feat: render states - separate pole detection from layer render * feat: render provinces * chore: unify drawFillWithGap * refactor: drawIce * refactor: drawBorders * refactor: drawHeightmap * refactor: drawTemperature * refactor: drawBiomes * refactor: drawPrec * refactor: drawPrecipitation * refactor: drawPopulation * refactor: drawCells * refactor: geColor * refactor: drawMarkers * refactor: drawScaleBar * refactor: drawScaleBar * refactor: drawMilitary * refactor: pump version to 1.104.00 * refactor: pump version to 1.104.00 * refactor: drawCoastline and createDefaultRuler * refactor: drawCoastline * refactor: Features module start * refactor: features - define distance fields * feat: drawFeatures * feat: drawIce don't hide * feat: detect coastline - fix issue with border feature * feat: separate labels rendering from generation process * feat: auto-update and restore layers * refactor - change layers * refactor - sort layers * fix: regenerate burgs to re-render layers * fix: getColor is not defined * fix: burgs overview - don't auto-show labels on hover * fix: redraw population on change * refactor: improve tooltip logic for burg labels and icons * chore: pump version to 1.104.0 * fefactor: edit coastline and lake * fix: minot fixes * fix: submap --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
ec993d1a9b
commit
05de284e02
52 changed files with 2473 additions and 2713 deletions
|
|
@ -13,6 +13,8 @@ window.BurgsAndStates = (() => {
|
|||
placeTowns();
|
||||
expandStates();
|
||||
normalizeStates();
|
||||
getPoles();
|
||||
|
||||
specifyBurgs();
|
||||
|
||||
collectStatistics();
|
||||
|
|
@ -20,7 +22,6 @@ window.BurgsAndStates = (() => {
|
|||
|
||||
generateCampaigns();
|
||||
generateDiplomacy();
|
||||
drawBurgs();
|
||||
|
||||
function placeCapitals() {
|
||||
TIME && console.time("placeCapitals");
|
||||
|
|
@ -272,103 +273,6 @@ window.BurgsAndStates = (() => {
|
|||
});
|
||||
};
|
||||
|
||||
const drawBurgs = () => {
|
||||
TIME && console.time("drawBurgs");
|
||||
|
||||
// remove old data
|
||||
burgIcons.selectAll("circle").remove();
|
||||
burgLabels.selectAll("text").remove();
|
||||
icons.selectAll("use").remove();
|
||||
|
||||
// capitals
|
||||
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
|
||||
const capitalIcons = burgIcons.select("#cities");
|
||||
const capitalLabels = burgLabels.select("#cities");
|
||||
const capitalSize = capitalIcons.attr("size") || 1;
|
||||
const capitalAnchors = anchors.selectAll("#cities");
|
||||
const caSize = capitalAnchors.attr("size") || 2;
|
||||
|
||||
capitalIcons
|
||||
.selectAll("circle")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", capitalSize);
|
||||
|
||||
capitalLabels
|
||||
.selectAll("text")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${capitalSize * -1.5}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
capitalAnchors
|
||||
.selectAll("use")
|
||||
.data(capitals.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - caSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - caSize * 0.47, 2))
|
||||
.attr("width", caSize)
|
||||
.attr("height", caSize);
|
||||
|
||||
// towns
|
||||
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
|
||||
const townIcons = burgIcons.select("#towns");
|
||||
const townLabels = burgLabels.select("#towns");
|
||||
const townSize = townIcons.attr("size") || 0.5;
|
||||
const townsAnchors = anchors.selectAll("#towns");
|
||||
const taSize = townsAnchors.attr("size") || 1;
|
||||
|
||||
townIcons
|
||||
.selectAll("circle")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", townSize);
|
||||
|
||||
townLabels
|
||||
.selectAll("text")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${townSize * -1.5}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
townsAnchors
|
||||
.selectAll("use")
|
||||
.data(towns.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - taSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - taSize * 0.47, 2))
|
||||
.attr("width", taSize)
|
||||
.attr("height", taSize);
|
||||
|
||||
TIME && console.timeEnd("drawBurgs");
|
||||
};
|
||||
|
||||
// expand cultures across the map (Dijkstra-like algorithm)
|
||||
const expandStates = () => {
|
||||
TIME && console.time("expandStates");
|
||||
|
|
@ -468,8 +372,7 @@ window.BurgsAndStates = (() => {
|
|||
|
||||
const normalizeStates = () => {
|
||||
TIME && console.time("normalizeStates");
|
||||
const cells = pack.cells,
|
||||
burgs = pack.burgs;
|
||||
const {cells, burgs} = pack;
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
|
||||
|
|
@ -486,26 +389,30 @@ window.BurgsAndStates = (() => {
|
|||
TIME && console.timeEnd("normalizeStates");
|
||||
};
|
||||
|
||||
// Resets the cultures of all burgs and states to their
|
||||
// cell or center cell's (respectively) culture.
|
||||
// calculate pole of inaccessibility for each state
|
||||
const getPoles = () => {
|
||||
const getType = cellId => pack.cells.state[cellId];
|
||||
const poles = getPolesOfInaccessibility(pack, getType);
|
||||
|
||||
pack.states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
s.pole = poles[s.i] || [0, 0];
|
||||
});
|
||||
};
|
||||
|
||||
// Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
|
||||
const updateCultures = () => {
|
||||
TIME && console.time("updateCulturesForBurgsAndStates");
|
||||
|
||||
// Assign the culture associated with the burgs cell.
|
||||
// Assign the culture associated with the burgs cell
|
||||
pack.burgs = pack.burgs.map((burg, index) => {
|
||||
// Ignore metadata burg
|
||||
if (index === 0) {
|
||||
return burg;
|
||||
}
|
||||
if (index === 0) return burg;
|
||||
return {...burg, culture: pack.cells.culture[burg.cell]};
|
||||
});
|
||||
|
||||
// Assign the culture associated with the states' center cell.
|
||||
// Assign the culture associated with the states' center cell
|
||||
pack.states = pack.states.map((state, index) => {
|
||||
// Ignore neutrals state
|
||||
if (index === 0) {
|
||||
return state;
|
||||
}
|
||||
if (index === 0) return state;
|
||||
return {...state, culture: pack.cells.culture[state.center]};
|
||||
});
|
||||
|
||||
|
|
@ -949,253 +856,12 @@ window.BurgsAndStates = (() => {
|
|||
return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
|
||||
};
|
||||
|
||||
const generateProvinces = (regenerate = false, regenerateInLockedStates = false) => {
|
||||
TIME && console.time("generateProvinces");
|
||||
const localSeed = regenerate ? generateSeed() : seed;
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const {cells, states, burgs} = pack;
|
||||
const provinces = [0];
|
||||
const provinceIds = new Uint16Array(cells.i.length);
|
||||
|
||||
const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock);
|
||||
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
|
||||
|
||||
if (regenerate) {
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed || !isProvinceLocked(province)) return;
|
||||
|
||||
const newId = provinces.length;
|
||||
for (const i of cells.i) {
|
||||
if (cells.province[i] === province.i) provinceIds[i] = newId;
|
||||
}
|
||||
|
||||
province.i = newId;
|
||||
provinces.push(province);
|
||||
});
|
||||
}
|
||||
|
||||
const provincesRatio = +byId("provincesRatio").value;
|
||||
const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
|
||||
|
||||
const forms = {
|
||||
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
|
||||
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
|
||||
Theocracy: {Parish: 3, Deanery: 1},
|
||||
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
|
||||
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
|
||||
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
|
||||
};
|
||||
|
||||
// generate provinces for selected burgs
|
||||
states.forEach(s => {
|
||||
s.provinces = [];
|
||||
if (!s.i || s.removed) return;
|
||||
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
|
||||
if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state
|
||||
|
||||
const stateBurgs = burgs
|
||||
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
|
||||
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
|
||||
.sort((a, b) => b.capital - a.capital);
|
||||
if (stateBurgs.length < 2) return; // at least 2 provinces are required
|
||||
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
|
||||
|
||||
const form = Object.assign({}, forms[s.form]);
|
||||
|
||||
for (let i = 0; i < provincesNumber; i++) {
|
||||
const provinceId = provinces.length;
|
||||
const center = stateBurgs[i].cell;
|
||||
const burg = stateBurgs[i].i;
|
||||
const c = stateBurgs[i].culture;
|
||||
const nameByBurg = P(0.5);
|
||||
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
|
||||
const formName = rw(form);
|
||||
form[formName] += 10;
|
||||
const fullName = name + " " + formName;
|
||||
const color = getMixedColor(s.color);
|
||||
const kinship = nameByBurg ? 0.8 : 0.4;
|
||||
const type = getType(center, burg.port);
|
||||
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
s.provinces.push(provinceId);
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
}
|
||||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
||||
const land = cells.h[e] >= 20;
|
||||
if (!land && !cells.t[e]) return; // cannot pass deep ocean
|
||||
if (land && cells.state[e] !== state) return;
|
||||
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
|
||||
const totalCost = p + evevation;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land) provinceIds[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, province, state});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// justify provinces shapes a bit
|
||||
for (const i of cells.i) {
|
||||
if (cells.burg[i]) continue; // do not overwrite burgs
|
||||
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
|
||||
|
||||
const neibs = cells.c[i]
|
||||
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
|
||||
.map(c => provinceIds[c]);
|
||||
const adversaries = neibs.filter(c => c !== provinceIds[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
|
||||
const buddies = neibs.filter(c => c === provinceIds[i]).length;
|
||||
if (buddies.length > 2) continue;
|
||||
|
||||
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
|
||||
const max = d3.max(competitors);
|
||||
if (buddies >= max) continue;
|
||||
|
||||
provinceIds[i] = adversaries[competitors.indexOf(max)];
|
||||
}
|
||||
|
||||
// add "wild" provinces if some cells don't have a province assigned
|
||||
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
|
||||
states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
if (s.lock && !regenerateInLockedStates) return;
|
||||
if (!s.provinces.length) return;
|
||||
|
||||
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
|
||||
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
|
||||
const getColonyName = () => {
|
||||
if (colonyNamePool.length < 1) return null;
|
||||
|
||||
const index = rand(colonyNamePool.length - 1);
|
||||
const spliced = colonyNamePool.splice(index, 1);
|
||||
return spliced[0] ? `New ${spliced[0]}` : null;
|
||||
};
|
||||
|
||||
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
|
||||
while (stateNoProvince.length) {
|
||||
// add new province
|
||||
const provinceId = provinces.length;
|
||||
const burgCell = stateNoProvince.find(i => cells.burg[i]);
|
||||
const center = burgCell ? burgCell : stateNoProvince[0];
|
||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
||||
provinceIds[center] = provinceId;
|
||||
|
||||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
while (queue.length) {
|
||||
const {e, p} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
const land = cells.h[nextCellId] >= 20;
|
||||
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
|
||||
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
|
||||
const totalCost = p + ter;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// generate "wild" province name
|
||||
const c = cells.culture[center];
|
||||
const f = pack.features[cells.f[center]];
|
||||
const color = getMixedColor(s.color);
|
||||
|
||||
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
|
||||
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
|
||||
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
|
||||
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
|
||||
|
||||
const name = (() => {
|
||||
const colonyName = colony && P(0.8) && getColonyName();
|
||||
if (colonyName) return colonyName;
|
||||
if (burgCell && P(0.5)) return burgs[burg].name;
|
||||
return Names.getState(Names.getCultureShort(c), c);
|
||||
})();
|
||||
|
||||
const formName = (() => {
|
||||
if (singleIsle) return "Island";
|
||||
if (isleGroup) return "Islands";
|
||||
if (colony) return "Colony";
|
||||
return rw(forms["Wild"]);
|
||||
})();
|
||||
|
||||
const fullName = name + " " + formName;
|
||||
|
||||
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
|
||||
const kinship = dominion ? 0 : 0.4;
|
||||
const type = getType(center, burgs[burg]?.port);
|
||||
const coa = COA.generate(s.coa, kinship, dominion, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
s.provinces.push(provinceId);
|
||||
|
||||
// check if there is a land way within the same state between two cells
|
||||
function isPassable(from, to) {
|
||||
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
||||
const queue = [from],
|
||||
used = new Uint8Array(cells.i.length),
|
||||
state = cells.state[from];
|
||||
while (queue.length) {
|
||||
const current = queue.pop();
|
||||
if (current === to) return true; // way is found
|
||||
cells.c[current].forEach(c => {
|
||||
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
|
||||
queue.push(c);
|
||||
used[c] = 1;
|
||||
});
|
||||
}
|
||||
return false; // way is not found
|
||||
}
|
||||
|
||||
// re-check
|
||||
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
|
||||
}
|
||||
});
|
||||
|
||||
cells.province = provinceIds;
|
||||
pack.provinces = provinces;
|
||||
|
||||
TIME && console.timeEnd("generateProvinces");
|
||||
};
|
||||
|
||||
return {
|
||||
generate,
|
||||
expandStates,
|
||||
normalizeStates,
|
||||
getPoles,
|
||||
assignColors,
|
||||
drawBurgs,
|
||||
specifyBurgs,
|
||||
defineBurgFeatures,
|
||||
getType,
|
||||
|
|
@ -1205,7 +871,6 @@ window.BurgsAndStates = (() => {
|
|||
generateDiplomacy,
|
||||
defineStateForms,
|
||||
getFullName,
|
||||
generateProvinces,
|
||||
updateCultures
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue