hoi4-agent-tools 4.4.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/hoi4_agent_tools/core/managed-outputs.js +26 -6
- package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
- package/dist/hoi4_agent_tools/core/source/parser.js +16 -1
- package/dist/hoi4_agent_tools/focus/drift.js +34 -0
- package/dist/hoi4_agent_tools/focus/service.js +9 -3
- package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
- package/dist/hoi4_agent_tools/map/border-quality.js +25 -7
- package/dist/hoi4_agent_tools/map/derived.js +16 -3
- package/dist/hoi4_agent_tools/map/gameplay/crossing-validation.js +268 -0
- package/dist/hoi4_agent_tools/map/gameplay/crossings.js +961 -0
- package/dist/hoi4_agent_tools/map/gameplay/development.js +168 -0
- package/dist/hoi4_agent_tools/map/gameplay/economy.js +75 -0
- package/dist/hoi4_agent_tools/map/gameplay/facts.js +264 -0
- package/dist/hoi4_agent_tools/map/gameplay/inland-sea.js +66 -0
- package/dist/hoi4_agent_tools/map/gameplay/metrics.js +132 -0
- package/dist/hoi4_agent_tools/map/gameplay/names.js +419 -0
- package/dist/hoi4_agent_tools/map/gameplay/ports.js +244 -0
- package/dist/hoi4_agent_tools/map/gameplay/prepare.js +27 -0
- package/dist/hoi4_agent_tools/map/gameplay/rail-cost.js +77 -0
- package/dist/hoi4_agent_tools/map/gameplay/sites.js +61 -0
- package/dist/hoi4_agent_tools/map/geography/resolve.js +15 -4
- package/dist/hoi4_agent_tools/map/model.js +2 -1
- package/dist/hoi4_agent_tools/map/operations.js +282 -38
- package/dist/hoi4_agent_tools/map/reference-preparation.js +4 -1
- package/dist/hoi4_agent_tools/map/relief/hydrology.js +316 -192
- package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +43 -0
- package/dist/hoi4_agent_tools/map/service.js +93 -16
- package/dist/hoi4_agent_tools/map/state-restructure.js +378 -0
- package/dist/hoi4_agent_tools/map/territory-graph.js +238 -0
- package/dist/hoi4_agent_tools/map/territory-operations.js +944 -0
- package/dist/hoi4_agent_tools/map/territory-quality.js +189 -0
- package/dist/hoi4_agent_tools/map/territory.js +1108 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +119 -1
- package/dist/hoi4_agent_tools/map/validation.js +4 -0
- package/dist/hoi4_agent_tools/map/world-context.js +23 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +22 -10
- package/dist/hoi4_agent_tools/map/world-realms.js +152 -0
- package/dist/hoi4_agent_tools/map/world.js +400 -145
- package/dist/hoi4_agent_tools/schemas/geography.js +12 -1
- package/dist/hoi4_agent_tools/schemas/map.js +277 -34
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +6 -0
- package/docs/map.md +132 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/geography-model.schema.json +13 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1541 -786
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/reference-sheets.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
|
@@ -12,13 +12,20 @@ import { generateWorldProvinces } from './world-provinces.js';
|
|
|
12
12
|
import { clusterRegions, clusterStates, partitionWorldFromRequest, } from './partition.js';
|
|
13
13
|
import { repairSeamCorners, smoothDiagonalContacts } from './partition-fields.js';
|
|
14
14
|
import { PARTITION_PROVINCE_ERROR, PARTITION_PROVINCE_WARNING } from './partition-tuning.js';
|
|
15
|
+
import { buildTerritoryAtoms, territoryGraphFromPartition } from './territory-graph.js';
|
|
16
|
+
import { growTerritory } from './territory.js';
|
|
15
17
|
import { geographyPartitionFields, provinceCountNote, surfacePartitionFields, } from './world-partition.js';
|
|
18
|
+
import { adjacencyRowText, adjacencyRulesText } from './gameplay/crossings.js';
|
|
19
|
+
import { convertInlandSeas } from './gameplay/inland-sea.js';
|
|
20
|
+
import { planEconomy } from './gameplay/economy.js';
|
|
21
|
+
import { prepareGameplay } from './gameplay/prepare.js';
|
|
16
22
|
import { fractal, lattice } from './world-noise.js';
|
|
17
23
|
import { calibratedColourMap, COLOUR_CLASSES, } from './colormap.js';
|
|
18
24
|
import { ambientObjectText, applyEdgePolicy, assertFeaturesOutsideBand, edgeWaterDarkness, RIM_COLOUR_FACTOR, rimAdjacencyRows, rimHeight, rimStrength, } from './world-edge.js';
|
|
19
25
|
import { landDistanceField, lakeHeight, normalizeLakes, waterColourLevel } from './water.js';
|
|
20
26
|
import { overlayGeographyLakes, rasterizeGeographyKind } from './relief/land.js';
|
|
21
|
-
import { synthesizeTerrain } from './relief/terrain-synthesis.js';
|
|
27
|
+
import { checkTerrainRivers, synthesizeTerrain, } from './relief/terrain-synthesis.js';
|
|
28
|
+
import { assignUnreachedRealmLand, resolveRealmRules } from './world-realms.js';
|
|
22
29
|
// ---------------------------------------------------------------------------------------------
|
|
23
30
|
// Deterministic randomness and noise
|
|
24
31
|
export class Random {
|
|
@@ -506,6 +513,48 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
506
513
|
let seamRepaired = geography !== undefined ? repairSeamCorners(surface.kind, width, height) : [];
|
|
507
514
|
// A geography model gets its relief and rivers from the synthesis of ADR 0056; every other world
|
|
508
515
|
// keeps the original elevation classes and drainage, byte for byte.
|
|
516
|
+
if (options.riverCheck === true && geography !== undefined) {
|
|
517
|
+
const checked = checkTerrainRivers({
|
|
518
|
+
geography,
|
|
519
|
+
width,
|
|
520
|
+
height,
|
|
521
|
+
seed: spec.seed,
|
|
522
|
+
kind: surface.kind,
|
|
523
|
+
relief: spec.relief,
|
|
524
|
+
rivers: { density: spec.riverDensity, count: spec.riverCount },
|
|
525
|
+
adjustHeight: edge.mode === 'rim'
|
|
526
|
+
? (x, _y, value) => rimHeight(value, rimStrength(edge, width, x))
|
|
527
|
+
: undefined,
|
|
528
|
+
signal: options.signal,
|
|
529
|
+
});
|
|
530
|
+
for (const note of checked) {
|
|
531
|
+
if (note.severity === 'error')
|
|
532
|
+
throw new ServiceError(note.code, note.message, note.details);
|
|
533
|
+
notes.push({
|
|
534
|
+
code: note.code,
|
|
535
|
+
severity: note.severity,
|
|
536
|
+
message: note.message,
|
|
537
|
+
...(note.details === undefined ? {} : { details: note.details }),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
files: [],
|
|
542
|
+
warnings: [],
|
|
543
|
+
notes,
|
|
544
|
+
summary: {
|
|
545
|
+
width,
|
|
546
|
+
height,
|
|
547
|
+
landProvinces: 0,
|
|
548
|
+
seaProvinces: 0,
|
|
549
|
+
lakeProvinces: 0,
|
|
550
|
+
states: 0,
|
|
551
|
+
landRegions: 0,
|
|
552
|
+
seaRegions: 0,
|
|
553
|
+
countries: spec.countries.length,
|
|
554
|
+
rivers: 0,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
509
558
|
const synthesized = geography === undefined
|
|
510
559
|
? undefined
|
|
511
560
|
: synthesizeTerrain({
|
|
@@ -534,6 +583,26 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
534
583
|
}
|
|
535
584
|
notes.push(terrainSummaryNote(synthesized, geography));
|
|
536
585
|
}
|
|
586
|
+
// A lake the model calls an inland sea becomes navigable water (ADR 0062).
|
|
587
|
+
const inland = geography === undefined || synthesized === undefined
|
|
588
|
+
? undefined
|
|
589
|
+
: convertInlandSeas(surface.kind, surface.elevation, width, height, geography);
|
|
590
|
+
if (inland !== undefined && geography !== undefined) {
|
|
591
|
+
for (const { lake, pixels } of inland.converted)
|
|
592
|
+
notes.push({
|
|
593
|
+
code: 'MAP_INLAND_SEA_CREATED',
|
|
594
|
+
severity: 'info',
|
|
595
|
+
message: `Lake ${geography.lakes[lake].id} is an inland sea: its ${pixels} water pixels are written as sea provinces so ships and ports can use them`,
|
|
596
|
+
details: { lakeId: geography.lakes[lake].id, pixels },
|
|
597
|
+
});
|
|
598
|
+
for (const lake of inland.missing)
|
|
599
|
+
notes.push({
|
|
600
|
+
code: 'MAP_INLAND_SEA_NO_WATER',
|
|
601
|
+
severity: 'info',
|
|
602
|
+
message: `Lake ${geography.lakes[lake].id} is an inland sea but has no separate water body: it joined the open sea or was too small to keep`,
|
|
603
|
+
details: { lakeId: geography.lakes[lake].id },
|
|
604
|
+
});
|
|
605
|
+
}
|
|
537
606
|
const drainage = synthesized === undefined
|
|
538
607
|
? generateWorldDrainage({
|
|
539
608
|
width,
|
|
@@ -627,6 +696,25 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
627
696
|
const countNote = provinceCountNote(count, featurePartition === undefined ? 'legacy' : 'feature-aware');
|
|
628
697
|
if (countNote !== undefined)
|
|
629
698
|
notes.push(countNote);
|
|
699
|
+
// Crossings, passes and walls of a geography world (ADR 0062).
|
|
700
|
+
const gameplay = geography === undefined || synthesized === undefined
|
|
701
|
+
? undefined
|
|
702
|
+
: prepareGameplay({
|
|
703
|
+
width,
|
|
704
|
+
height,
|
|
705
|
+
geography,
|
|
706
|
+
provinces,
|
|
707
|
+
heights: synthesized.heights,
|
|
708
|
+
riverBitmap: drainage.riverPixels,
|
|
709
|
+
fields: synthesized.fields,
|
|
710
|
+
riverPaths: synthesized.riverPaths,
|
|
711
|
+
autoStraits: spec.autoStraits,
|
|
712
|
+
impassableLimit: spec.impassableRowLimit,
|
|
713
|
+
inlandSeas: inland?.converted.map(({ lake }) => lake) ?? [],
|
|
714
|
+
});
|
|
715
|
+
if (gameplay !== undefined)
|
|
716
|
+
for (const note of gameplay.notes)
|
|
717
|
+
notes.push(note);
|
|
630
718
|
const center = (id) => ({
|
|
631
719
|
x: provinces.sumX[id] / provinces.size[id],
|
|
632
720
|
y: provinces.sumY[id] / provinces.size[id],
|
|
@@ -763,7 +851,9 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
763
851
|
const countryCount = spec.countries.length;
|
|
764
852
|
const interpretationWarnings = [];
|
|
765
853
|
const referenceOwners = new Map();
|
|
766
|
-
if (
|
|
854
|
+
if (spec.realms === undefined &&
|
|
855
|
+
reference?.inferredCountries.length === countryCount &&
|
|
856
|
+
countryCount > 1) {
|
|
767
857
|
const indexes = new Map();
|
|
768
858
|
const unresolved = [];
|
|
769
859
|
for (const [index, country] of spec.countries.entries()) {
|
|
@@ -831,7 +921,20 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
831
921
|
}
|
|
832
922
|
return selected;
|
|
833
923
|
};
|
|
834
|
-
|
|
924
|
+
// Sites of the model that anchor a country (its capital) choose that country's capital province.
|
|
925
|
+
const siteAnchors = geography === undefined
|
|
926
|
+
? []
|
|
927
|
+
: geography.sites.flatMap((site) => site.country === undefined || site.onWater
|
|
928
|
+
? []
|
|
929
|
+
: [
|
|
930
|
+
{
|
|
931
|
+
name: site.name ?? site.id,
|
|
932
|
+
x: site.at.x * width,
|
|
933
|
+
y: site.at.y * height,
|
|
934
|
+
countryTag: site.country,
|
|
935
|
+
},
|
|
936
|
+
]);
|
|
937
|
+
const namedCities = [...(spec.referenceCities ?? []), ...siteAnchors].map((city) => {
|
|
835
938
|
const pixel = city.x >= 0 && city.y >= 0 && city.x < width && city.y < height
|
|
836
939
|
? Math.floor(city.y) * width + Math.floor(city.x)
|
|
837
940
|
: undefined;
|
|
@@ -844,6 +947,17 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
844
947
|
});
|
|
845
948
|
const capitals = [];
|
|
846
949
|
const largest = [...landIds].sort((left, right) => provinces.size[right] - provinces.size[left] || left - right);
|
|
950
|
+
const realmRules = spec.realms === undefined
|
|
951
|
+
? undefined
|
|
952
|
+
: resolveRealmRules(spec.realms, {
|
|
953
|
+
width,
|
|
954
|
+
height,
|
|
955
|
+
pixelProvince: provinces.pixel,
|
|
956
|
+
provinceSize: provinces.size,
|
|
957
|
+
isLand: (id) => provinces.kind[id] === LAND,
|
|
958
|
+
});
|
|
959
|
+
if (realmRules !== undefined)
|
|
960
|
+
capitals.push(...realmRules.capitals);
|
|
847
961
|
while (capitals.length < countryCount) {
|
|
848
962
|
const countryIndex = capitals.length;
|
|
849
963
|
const regionCandidates = largest.filter((id) => referenceOwners.get(id) === countryIndex && !capitals.includes(id));
|
|
@@ -879,20 +993,41 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
879
993
|
}
|
|
880
994
|
const shares = spec.countries.map((country) => Math.max(0.05, country.share ?? 1));
|
|
881
995
|
const owned = new Array(countryCount).fill(0);
|
|
882
|
-
for (const [id, country] of referenceOwners) {
|
|
996
|
+
for (const [id, country] of realmRules?.forced ?? referenceOwners) {
|
|
883
997
|
owner.set(id, country);
|
|
884
998
|
owned[country] += provinces.size[id];
|
|
885
999
|
}
|
|
1000
|
+
const unownedLand = realmRules?.unowned ?? new Set();
|
|
1001
|
+
const blockedFor = (index, id) => unownedLand.has(id) || realmRules?.excluded[index].has(id) === true;
|
|
886
1002
|
const frontiers = capitals.map((capital, index) => {
|
|
887
|
-
owner.
|
|
888
|
-
if (!referenceOwners.has(capital))
|
|
1003
|
+
if (!owner.has(capital))
|
|
889
1004
|
owned[index] += provinces.size[capital];
|
|
1005
|
+
owner.set(capital, index);
|
|
1006
|
+
if (realmRules !== undefined)
|
|
1007
|
+
return [...owner]
|
|
1008
|
+
.filter(([, country]) => country === index)
|
|
1009
|
+
.flatMap(([id]) => [...provinces.neighbors[id]]);
|
|
890
1010
|
return [...provinces.neighbors[capital]];
|
|
891
1011
|
});
|
|
892
|
-
|
|
1012
|
+
let grownByTerritory = false;
|
|
1013
|
+
// The feature-aware partition grows generated countries along its natural lines (ADR 0060).
|
|
1014
|
+
// Workspace-owned countries keep the realm growth with its forced and blocked land (ADR 0069).
|
|
1015
|
+
if (featurePartition !== undefined && referenceOwners.size === 0 && realmRules === undefined) {
|
|
1016
|
+
const atoms = buildTerritoryAtoms(territoryGraphFromPartition(featurePartition.graph, provinces.sumX, provinces.sumY), landIds.map((id) => [id]));
|
|
1017
|
+
const grown = growTerritory(atoms, spec.countries.map((country, index) => ({
|
|
1018
|
+
tag: country.tag,
|
|
1019
|
+
weight: shares[index],
|
|
1020
|
+
seeds: [atoms.atomOfProvince[capitals[index]]],
|
|
1021
|
+
})), { seed: spec.seed });
|
|
1022
|
+
for (const [atom, id] of landIds.entries())
|
|
1023
|
+
if (grown.owner[atom] >= 0)
|
|
1024
|
+
owner.set(id, grown.owner[atom]);
|
|
1025
|
+
grownByTerritory = true;
|
|
1026
|
+
}
|
|
1027
|
+
while (!grownByTerritory) {
|
|
893
1028
|
let chosen = -1;
|
|
894
1029
|
for (let index = 0; index < countryCount; index += 1) {
|
|
895
|
-
frontiers[index] = frontiers[index].filter((id) => provinces.kind[id] === LAND && !owner.has(id));
|
|
1030
|
+
frontiers[index] = frontiers[index].filter((id) => provinces.kind[id] === LAND && !owner.has(id) && !blockedFor(index, id));
|
|
896
1031
|
if (frontiers[index].length === 0)
|
|
897
1032
|
continue;
|
|
898
1033
|
if (chosen < 0 || owned[index] / shares[index] < owned[chosen] / shares[chosen])
|
|
@@ -908,9 +1043,21 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
908
1043
|
if (!owner.has(neighbor))
|
|
909
1044
|
frontier.push(neighbor);
|
|
910
1045
|
}
|
|
1046
|
+
// Land no workspace country reached follows the island rule; the rest of the land is unowned.
|
|
1047
|
+
if (realmRules !== undefined)
|
|
1048
|
+
assignUnreachedRealmLand({
|
|
1049
|
+
landIds,
|
|
1050
|
+
owner,
|
|
1051
|
+
unowned: unownedLand,
|
|
1052
|
+
capitals,
|
|
1053
|
+
excluded: realmRules.excluded,
|
|
1054
|
+
islands: spec.realms?.islands ?? 'nearest',
|
|
1055
|
+
part: (id) => landParts.get(id),
|
|
1056
|
+
center,
|
|
1057
|
+
});
|
|
911
1058
|
// Islands no country reached go to the nearest capital's country.
|
|
912
1059
|
for (const id of landIds)
|
|
913
|
-
if (!owner.has(id)) {
|
|
1060
|
+
if (!owner.has(id) && !unownedLand.has(id)) {
|
|
914
1061
|
const c = center(id);
|
|
915
1062
|
let best = 0;
|
|
916
1063
|
for (const [index, capital] of capitals.entries())
|
|
@@ -946,7 +1093,7 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
946
1093
|
// States: within each region and country, clusters of about provincesPerState.
|
|
947
1094
|
const groups = new Map();
|
|
948
1095
|
for (const id of landIds) {
|
|
949
|
-
const key = `${regionOf.get(id)}:${owner.get(id)}`;
|
|
1096
|
+
const key = `${regionOf.get(id)}:${owner.get(id) ?? -1}`;
|
|
950
1097
|
const list = groups.get(key) ?? [];
|
|
951
1098
|
list.push(id);
|
|
952
1099
|
groups.set(key, list);
|
|
@@ -970,7 +1117,7 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
970
1117
|
const draft = {
|
|
971
1118
|
id: states.length + 1,
|
|
972
1119
|
provinces: sorted,
|
|
973
|
-
owner: spec.countries[countryIndex].tag,
|
|
1120
|
+
...(countryIndex < 0 ? {} : { owner: spec.countries[countryIndex].tag }),
|
|
974
1121
|
region,
|
|
975
1122
|
capital: sorted[0],
|
|
976
1123
|
manpower: 0,
|
|
@@ -983,12 +1130,33 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
983
1130
|
resources: new Map(),
|
|
984
1131
|
victoryPoints: new Map(),
|
|
985
1132
|
navalBases: new Map(),
|
|
1133
|
+
forts: new Map(),
|
|
986
1134
|
};
|
|
987
1135
|
states.push(draft);
|
|
988
1136
|
for (const id of sorted)
|
|
989
1137
|
stateOf.set(id, draft);
|
|
990
1138
|
}
|
|
991
1139
|
}
|
|
1140
|
+
// Names from reference labels (the nearest province to each label), which win over any other.
|
|
1141
|
+
const stateNames = new Map();
|
|
1142
|
+
const regionNames = new Map();
|
|
1143
|
+
for (const label of spec.referenceLabels ?? []) {
|
|
1144
|
+
if (label.kind === 'state') {
|
|
1145
|
+
const nearby = nearestProvince(label, landIds);
|
|
1146
|
+
const state = stateOf.get(nearby);
|
|
1147
|
+
const previous = stateNames.get(state.id);
|
|
1148
|
+
stateNames.set(state.id, previous === undefined ? label.name : `${previous} / ${label.name}`);
|
|
1149
|
+
}
|
|
1150
|
+
else if (label.kind === 'sea' || label.kind === 'region') {
|
|
1151
|
+
const members = label.kind === 'sea' ? seaIds : landIds;
|
|
1152
|
+
if (members.length === 0)
|
|
1153
|
+
continue;
|
|
1154
|
+
const region = regionOf.get(nearestProvince(label, members));
|
|
1155
|
+
const previous = regionNames.get(region);
|
|
1156
|
+
regionNames.set(region, previous === undefined ? label.name : `${previous} / ${label.name}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const lakeNames = new Map();
|
|
992
1160
|
// State values from size, terrain and development, with the country capital boosted.
|
|
993
1161
|
const categories = [
|
|
994
1162
|
'wasteland',
|
|
@@ -1016,112 +1184,153 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1016
1184
|
state.capital = capital;
|
|
1017
1185
|
countryCapitalState.set(spec.countries[index].tag, state);
|
|
1018
1186
|
}
|
|
1019
|
-
for (const state of states) {
|
|
1020
|
-
const pixels = state.provinces.reduce((sum, id) => sum + provinces.size[id], 0);
|
|
1021
|
-
const habitability = state.provinces.reduce((sum, id) => {
|
|
1022
|
-
const value = {
|
|
1023
|
-
plains: 1,
|
|
1024
|
-
forest: 0.8,
|
|
1025
|
-
hills: 0.6,
|
|
1026
|
-
jungle: 0.5,
|
|
1027
|
-
marsh: 0.4,
|
|
1028
|
-
desert: 0.25,
|
|
1029
|
-
mountain: 0.3,
|
|
1030
|
-
urban: 1.5,
|
|
1031
|
-
}[terrain.get(id) ?? 'plains'] ?? 0.5;
|
|
1032
|
-
return sum + value * provinces.size[id];
|
|
1033
|
-
}, 0) / pixels;
|
|
1034
|
-
const isCapital = countryCapitalState.get(state.owner) === state;
|
|
1035
|
-
const development = Math.min(1, habitability * (0.5 + random.next()) * (isCapital ? 1.8 : 1));
|
|
1036
|
-
state.manpower = Math.max(1_000, Math.round((pixels * 180 * development) / 1_000) * 1_000);
|
|
1037
|
-
const categoryIndex = Math.max(0, Math.min(categories.length - 1, Math.round(development * 5.2 + (isCapital ? 1 : 0))));
|
|
1038
|
-
state.category = categories[categoryIndex];
|
|
1039
|
-
if (context.stateCategories !== undefined && !context.stateCategories.has(state.category))
|
|
1040
|
-
state.category = context.stateCategories.has('rural')
|
|
1041
|
-
? 'rural'
|
|
1042
|
-
: ([...context.stateCategories][0] ?? 'rural');
|
|
1043
|
-
const slots = slotsFor[state.category] ?? 2;
|
|
1044
|
-
state.infrastructure = Math.max(1, Math.min(5, Math.round(1 + development * 3 + (isCapital ? 1 : 0))));
|
|
1045
|
-
state.civilian = Math.round(slots * (0.25 + random.next() * 0.3));
|
|
1046
|
-
state.military = Math.min(slots - state.civilian, Math.round(slots * random.next() * 0.3));
|
|
1047
|
-
if (isCapital) {
|
|
1048
|
-
terrain.set(state.capital, 'urban');
|
|
1049
|
-
state.airBase = 2;
|
|
1050
|
-
}
|
|
1051
|
-
const coastalProvinces = state.provinces.filter(coastal);
|
|
1052
|
-
if (coastalProvinces.length > 0 && (isCapital || random.next() < 0.35)) {
|
|
1053
|
-
state.navalBases.set(coastalProvinces[0], isCapital ? 3 : 1);
|
|
1054
|
-
if (slots - state.civilian - state.military > 0 && random.next() < 0.5)
|
|
1055
|
-
state.dockyard = 1;
|
|
1056
|
-
}
|
|
1057
|
-
for (const id of state.provinces) {
|
|
1058
|
-
const kindOfLand = terrain.get(id);
|
|
1059
|
-
const roll = random.next();
|
|
1060
|
-
const add = (resource, amount) => state.resources.set(resource, (state.resources.get(resource) ?? 0) + amount);
|
|
1061
|
-
if (kindOfLand === 'desert' && roll < 0.3)
|
|
1062
|
-
add('oil', 2 + random.int(6));
|
|
1063
|
-
if ((kindOfLand === 'mountain' || kindOfLand === 'hills') && roll < 0.35)
|
|
1064
|
-
add('steel', 2 + random.int(8));
|
|
1065
|
-
if (kindOfLand === 'mountain' && roll < 0.15)
|
|
1066
|
-
add('tungsten', 1 + random.int(4));
|
|
1067
|
-
if (kindOfLand === 'hills' && roll > 0.85)
|
|
1068
|
-
add('chromium', 1 + random.int(4));
|
|
1069
|
-
if (kindOfLand === 'jungle' && roll < 0.35)
|
|
1070
|
-
add('rubber', 1 + random.int(5));
|
|
1071
|
-
if (kindOfLand === 'plains' && roll < 0.05)
|
|
1072
|
-
add('aluminium', 1 + random.int(4));
|
|
1073
|
-
}
|
|
1074
|
-
state.victoryPoints.set(state.capital, isCapital ? 25 : Math.max(1, Math.round(development * 8)));
|
|
1075
|
-
}
|
|
1076
1187
|
const cityNames = new Map();
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1188
|
+
// A geography world gets its economy, ports, names and railway costs from the model (ADR 0063);
|
|
1189
|
+
// every other world keeps the random economy below, byte for byte.
|
|
1190
|
+
const economy = gameplay === undefined || spec.developmentModel === 'legacy'
|
|
1191
|
+
? undefined
|
|
1192
|
+
: planEconomy(gameplay, {
|
|
1193
|
+
states: states.map((state) => ({
|
|
1194
|
+
id: state.id,
|
|
1195
|
+
provinces: state.provinces,
|
|
1196
|
+
owner: state.owner,
|
|
1197
|
+
isCapital: state.owner !== undefined && countryCapitalState.get(state.owner) === state,
|
|
1198
|
+
})),
|
|
1199
|
+
terrain,
|
|
1200
|
+
options: {
|
|
1201
|
+
categories,
|
|
1202
|
+
slotsFor,
|
|
1203
|
+
allowedCategories: context.stateCategories,
|
|
1204
|
+
capitalProvinces: new Set(capitals),
|
|
1205
|
+
},
|
|
1206
|
+
regionOf,
|
|
1207
|
+
fixedStateNames: stateNames,
|
|
1208
|
+
fixedRegionNames: regionNames,
|
|
1209
|
+
countryNames: new Map(spec.countries.map(({ tag, name }) => [tag, name])),
|
|
1210
|
+
});
|
|
1211
|
+
if (economy !== undefined) {
|
|
1212
|
+
for (const state of states) {
|
|
1213
|
+
const plan = economy.development.states.get(state.id);
|
|
1214
|
+
state.manpower = plan.manpower;
|
|
1215
|
+
state.category = plan.category;
|
|
1216
|
+
state.infrastructure = plan.infrastructure;
|
|
1217
|
+
state.civilian = plan.civilian;
|
|
1218
|
+
state.military = plan.military;
|
|
1219
|
+
state.dockyard = plan.dockyard;
|
|
1220
|
+
state.airBase = plan.airBase;
|
|
1221
|
+
state.resources = plan.resources;
|
|
1222
|
+
state.victoryPoints = plan.victoryPoints;
|
|
1223
|
+
for (const id of state.provinces) {
|
|
1224
|
+
const port = economy.ports.ports.get(id);
|
|
1225
|
+
if (port !== undefined)
|
|
1226
|
+
state.navalBases.set(id, port.level);
|
|
1227
|
+
const fort = economy.forts.get(id);
|
|
1228
|
+
if (fort !== undefined)
|
|
1229
|
+
state.forts.set(id, fort);
|
|
1230
|
+
}
|
|
1105
1231
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1232
|
+
for (const id of economy.development.urban)
|
|
1233
|
+
terrain.set(id, 'urban');
|
|
1234
|
+
for (const [id, name] of economy.names.victory)
|
|
1235
|
+
cityNames.set(id, name);
|
|
1236
|
+
for (const [id, name] of economy.names.states)
|
|
1237
|
+
stateNames.set(id, name);
|
|
1238
|
+
for (const [id, name] of economy.names.regions)
|
|
1239
|
+
regionNames.set(id, name);
|
|
1240
|
+
for (const [id, name] of economy.names.lakes)
|
|
1241
|
+
lakeNames.set(id, name);
|
|
1242
|
+
for (const note of economy.notes)
|
|
1243
|
+
notes.push(note);
|
|
1108
1244
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1245
|
+
if (economy === undefined) {
|
|
1246
|
+
for (const state of states) {
|
|
1247
|
+
const pixels = state.provinces.reduce((sum, id) => sum + provinces.size[id], 0);
|
|
1248
|
+
const habitability = state.provinces.reduce((sum, id) => {
|
|
1249
|
+
const value = {
|
|
1250
|
+
plains: 1,
|
|
1251
|
+
forest: 0.8,
|
|
1252
|
+
hills: 0.6,
|
|
1253
|
+
jungle: 0.5,
|
|
1254
|
+
marsh: 0.4,
|
|
1255
|
+
desert: 0.25,
|
|
1256
|
+
mountain: 0.3,
|
|
1257
|
+
urban: 1.5,
|
|
1258
|
+
}[terrain.get(id) ?? 'plains'] ?? 0.5;
|
|
1259
|
+
return sum + value * provinces.size[id];
|
|
1260
|
+
}, 0) / pixels;
|
|
1261
|
+
const isCapital = state.owner !== undefined && countryCapitalState.get(state.owner) === state;
|
|
1262
|
+
const development = Math.min(1, habitability * (0.5 + random.next()) * (isCapital ? 1.8 : 1));
|
|
1263
|
+
state.manpower = Math.max(1_000, Math.round((pixels * 180 * development) / 1_000) * 1_000);
|
|
1264
|
+
const categoryIndex = Math.max(0, Math.min(categories.length - 1, Math.round(development * 5.2 + (isCapital ? 1 : 0))));
|
|
1265
|
+
state.category = categories[categoryIndex];
|
|
1266
|
+
if (context.stateCategories !== undefined && !context.stateCategories.has(state.category))
|
|
1267
|
+
state.category = context.stateCategories.has('rural')
|
|
1268
|
+
? 'rural'
|
|
1269
|
+
: ([...context.stateCategories][0] ?? 'rural');
|
|
1270
|
+
const slots = slotsFor[state.category] ?? 2;
|
|
1271
|
+
state.infrastructure = Math.max(1, Math.min(5, Math.round(1 + development * 3 + (isCapital ? 1 : 0))));
|
|
1272
|
+
state.civilian = Math.round(slots * (0.25 + random.next() * 0.3));
|
|
1273
|
+
state.military = Math.min(slots - state.civilian, Math.round(slots * random.next() * 0.3));
|
|
1274
|
+
if (isCapital) {
|
|
1275
|
+
terrain.set(state.capital, 'urban');
|
|
1276
|
+
state.airBase = 2;
|
|
1277
|
+
}
|
|
1278
|
+
const coastalProvinces = state.provinces.filter(coastal);
|
|
1279
|
+
if (coastalProvinces.length > 0 && (isCapital || random.next() < 0.35)) {
|
|
1280
|
+
state.navalBases.set(coastalProvinces[0], isCapital ? 3 : 1);
|
|
1281
|
+
if (slots - state.civilian - state.military > 0 && random.next() < 0.5)
|
|
1282
|
+
state.dockyard = 1;
|
|
1283
|
+
}
|
|
1284
|
+
for (const id of state.provinces) {
|
|
1285
|
+
const kindOfLand = terrain.get(id);
|
|
1286
|
+
const roll = random.next();
|
|
1287
|
+
const add = (resource, amount) => state.resources.set(resource, (state.resources.get(resource) ?? 0) + amount);
|
|
1288
|
+
if (kindOfLand === 'desert' && roll < 0.3)
|
|
1289
|
+
add('oil', 2 + random.int(6));
|
|
1290
|
+
if ((kindOfLand === 'mountain' || kindOfLand === 'hills') && roll < 0.35)
|
|
1291
|
+
add('steel', 2 + random.int(8));
|
|
1292
|
+
if (kindOfLand === 'mountain' && roll < 0.15)
|
|
1293
|
+
add('tungsten', 1 + random.int(4));
|
|
1294
|
+
if (kindOfLand === 'hills' && roll > 0.85)
|
|
1295
|
+
add('chromium', 1 + random.int(4));
|
|
1296
|
+
if (kindOfLand === 'jungle' && roll < 0.35)
|
|
1297
|
+
add('rubber', 1 + random.int(5));
|
|
1298
|
+
if (kindOfLand === 'plains' && roll < 0.05)
|
|
1299
|
+
add('aluminium', 1 + random.int(4));
|
|
1300
|
+
}
|
|
1301
|
+
state.victoryPoints.set(state.capital, isCapital ? 25 : Math.max(1, Math.round(development * 8)));
|
|
1117
1302
|
}
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1303
|
+
for (const city of namedCities) {
|
|
1304
|
+
const state = stateOf.get(city.province);
|
|
1305
|
+
cityNames.set(city.province, city.name);
|
|
1306
|
+
state.victoryPoints.set(city.province, Math.max(5, state.victoryPoints.get(city.province) ?? 0));
|
|
1307
|
+
terrain.set(city.province, 'urban');
|
|
1308
|
+
}
|
|
1309
|
+
// Each coastal territory needs a port even when its capital lies inland or on another island.
|
|
1310
|
+
const portChecked = new Set();
|
|
1311
|
+
for (const start of landIds) {
|
|
1312
|
+
if (portChecked.has(start))
|
|
1121
1313
|
continue;
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1314
|
+
const queue = [start];
|
|
1315
|
+
portChecked.add(start);
|
|
1316
|
+
let bestCoast;
|
|
1317
|
+
let hasPort = false;
|
|
1318
|
+
for (const id of queue) {
|
|
1319
|
+
if (stateOf.get(id).navalBases.has(id))
|
|
1320
|
+
hasPort = true;
|
|
1321
|
+
if (coastal(id) &&
|
|
1322
|
+
(bestCoast === undefined || provinces.size[id] > provinces.size[bestCoast]))
|
|
1323
|
+
bestCoast = id;
|
|
1324
|
+
for (const next of provinces.neighbors[id])
|
|
1325
|
+
if (!portChecked.has(next) &&
|
|
1326
|
+
provinces.kind[next] === LAND &&
|
|
1327
|
+
owner.get(next) === owner.get(start)) {
|
|
1328
|
+
portChecked.add(next);
|
|
1329
|
+
queue.push(next);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (!hasPort && bestCoast !== undefined)
|
|
1333
|
+
stateOf.get(bestCoast).navalBases.set(bestCoast, 1);
|
|
1125
1334
|
}
|
|
1126
1335
|
}
|
|
1127
1336
|
// Province IDs: 1-based in reading order of the province's first pixel; 0 is the null row.
|
|
@@ -1140,6 +1349,7 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1140
1349
|
const files = [];
|
|
1141
1350
|
const add = (relativePath, content, mediaType = 'text/plain') => files.push({ relativePath, content, mediaType });
|
|
1142
1351
|
const countryNames = new Map(spec.countries.map(({ tag, name }) => [tag, name]));
|
|
1352
|
+
const ownerName = (state) => state.owner === undefined ? 'Free Land' : (countryNames.get(state.owner) ?? state.owner);
|
|
1143
1353
|
const map = context.mapRoot;
|
|
1144
1354
|
// provinces.bmp
|
|
1145
1355
|
const rgb = new Array(width * height);
|
|
@@ -1189,8 +1399,11 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1189
1399
|
message: `The rim edge policy wrote ${rimRows.length} impassable adjacency row(s) for provinces that meet across the seam`,
|
|
1190
1400
|
details: { rows: rimRows.length },
|
|
1191
1401
|
});
|
|
1192
|
-
|
|
1193
|
-
add(`${map}/
|
|
1402
|
+
const crossingRows = (gameplay?.crossings.rows ?? []).map(adjacencyRowText);
|
|
1403
|
+
add(`${map}/adjacencies.csv`, text(`From;To;Type;Through;start_x;start_y;stop_x;stop_y;adjacency_rule_name;Comment\r\n${[...crossingRows, ...rimRows].map((row) => `${row}\r\n`).join('')}-1;-1;;-1;-1;-1;-1;-1;-1\r\n`));
|
|
1404
|
+
add(`${map}/adjacency_rules.txt`, text(gameplay === undefined
|
|
1405
|
+
? '# No straits or canals in this world yet.\n'
|
|
1406
|
+
: adjacencyRulesText(gameplay.crossings.rules)));
|
|
1194
1407
|
add(`${map}/positions.txt`, text(''));
|
|
1195
1408
|
add(`${map}/ambient_object.txt`, text(spec.ambientFrame === true
|
|
1196
1409
|
? ambientObjectText(width, height)
|
|
@@ -1529,7 +1742,16 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1529
1742
|
...(state.military > 0 ? [`\t\t\tarms_factory = ${state.military}`] : []),
|
|
1530
1743
|
...(state.dockyard > 0 ? [`\t\t\tdockyard = ${state.dockyard}`] : []),
|
|
1531
1744
|
...(state.airBase > 0 ? [`\t\t\tair_base = ${state.airBase}`] : []),
|
|
1532
|
-
...[
|
|
1745
|
+
...[
|
|
1746
|
+
...state.navalBases.keys(),
|
|
1747
|
+
...[...state.forts.keys()].filter((id) => !state.navalBases.has(id)).sort((a, b) => a - b),
|
|
1748
|
+
].map((id) => {
|
|
1749
|
+
const parts = [
|
|
1750
|
+
...(state.navalBases.has(id) ? [`naval_base = ${state.navalBases.get(id)}`] : []),
|
|
1751
|
+
...(state.forts.has(id) ? [`bunker = ${state.forts.get(id)}`] : []),
|
|
1752
|
+
];
|
|
1753
|
+
return `\t\t\t${provinceId(id)} = { ${parts.join(' ')} }`;
|
|
1754
|
+
}),
|
|
1533
1755
|
];
|
|
1534
1756
|
add(`${context.statesRoot}/${state.id}-${slug(stateNames.get(state.id) ?? `STATE_${state.id}`).slice(0, 80)}.txt`, text([
|
|
1535
1757
|
'state = {',
|
|
@@ -1543,8 +1765,9 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1543
1765
|
`\tresources = {\n${resources.map(([resource, amount]) => `\t\t${resource} = ${amount}`).join('\n')}\n\t}`,
|
|
1544
1766
|
]),
|
|
1545
1767
|
'\thistory = {',
|
|
1546
|
-
|
|
1547
|
-
|
|
1768
|
+
...(state.owner === undefined
|
|
1769
|
+
? []
|
|
1770
|
+
: [`\t\towner = ${state.owner}`, `\t\tadd_core_of = ${state.owner}`]),
|
|
1548
1771
|
...[...state.victoryPoints].map(([id, value]) => `\t\tvictory_points = { ${provinceId(id)} ${value} }`),
|
|
1549
1772
|
'\t\tbuildings = {',
|
|
1550
1773
|
...buildings,
|
|
@@ -1559,7 +1782,8 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1559
1782
|
const countryLocalisation = [];
|
|
1560
1783
|
const countryLines = [];
|
|
1561
1784
|
const countryColors = [];
|
|
1562
|
-
|
|
1785
|
+
// Workspace-owned countries keep their own tags, files, histories, flags and names.
|
|
1786
|
+
for (const [index, country] of (spec.realms === undefined ? spec.countries : []).entries()) {
|
|
1563
1787
|
const color = country.color ?? {
|
|
1564
1788
|
r: 60 + random.int(170),
|
|
1565
1789
|
g: 60 + random.int(170),
|
|
@@ -1658,10 +1882,12 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1658
1882
|
add(`gfx/flags/${folder}${country.tag}.tga`, encodeTga(flagWidth, flagHeight, (_x, y) => y < flagHeight / 3 ? light : y < (flagHeight * 2) / 3 ? color : dark), 'image/x-tga');
|
|
1659
1883
|
void index;
|
|
1660
1884
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1885
|
+
if (spec.realms === undefined) {
|
|
1886
|
+
add('common/country_tags/zz_world_countries.txt', text(`${countryLines.join('\n')}\n`));
|
|
1887
|
+
add('common/countries/colors.txt', text(`${countryColors.join('\n')}\n`));
|
|
1888
|
+
}
|
|
1663
1889
|
if (spec.startingScenario) {
|
|
1664
|
-
for (const tag of context.inheritedCountryTags ?? [])
|
|
1890
|
+
for (const tag of spec.realms === undefined ? (context.inheritedCountryTags ?? []) : [])
|
|
1665
1891
|
if (!countryNames.has(tag))
|
|
1666
1892
|
add(`history/countries/${tag} - inactive world.txt`, text('# Inherited tag has no territory or active starting country in this world.\n'));
|
|
1667
1893
|
add('common/on_actions/world.txt', text('# Old-world startup hooks are replaced for this generated map.\non_actions = { }\n'));
|
|
@@ -1675,44 +1901,70 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1675
1901
|
states
|
|
1676
1902
|
.filter((state) => state.owner === left.tag)
|
|
1677
1903
|
.reduce((sum, state) => sum + state.manpower, 0) || (left.tag < right.tag ? -1 : 1));
|
|
1678
|
-
const majors = ranked.slice(0, Math.min(7, ranked.length));
|
|
1679
|
-
|
|
1680
|
-
'bookmarks
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1904
|
+
const majors = spec.realms === undefined ? ranked.slice(0, Math.min(7, ranked.length)) : [];
|
|
1905
|
+
if (spec.realms === undefined)
|
|
1906
|
+
add('common/bookmarks/world.txt', text([
|
|
1907
|
+
'bookmarks = {',
|
|
1908
|
+
'\tbookmark = {',
|
|
1909
|
+
'\t\tname = "WORLD_BOOKMARK_NAME"',
|
|
1910
|
+
'\t\tdesc = "WORLD_BOOKMARK_DESC"',
|
|
1911
|
+
`\t\tdate = ${spec.startDate}.12`,
|
|
1912
|
+
'\t\tpicture = "GFX_select_date_1936"',
|
|
1913
|
+
`\t\tdefault_country = "${majors[0].tag}"`,
|
|
1914
|
+
'\t\tdefault = yes',
|
|
1915
|
+
...majors.map((country) => `\t\t"${country.tag}" = {\n\t\t\thistory = "${country.tag}_WORLD_DESC"\n\t\t\tideology = ${country.ideology ?? 'neutrality'}\n\t\t}`),
|
|
1916
|
+
'\t\t"---" = {\n\t\t\thistory = "WORLD_OTHER_DESC"\n\t\t}',
|
|
1917
|
+
'\t\teffect = {\n\t\t\trandomize_weather = 22345\n\t\t}',
|
|
1918
|
+
'\t}',
|
|
1919
|
+
'}',
|
|
1920
|
+
'',
|
|
1921
|
+
].join('\n')));
|
|
1922
|
+
const realmSummary = () => {
|
|
1923
|
+
const landPixels = landIds.reduce((sum, id) => sum + provinces.size[id], 0);
|
|
1924
|
+
const weights = spec.countries.reduce((sum, country) => sum + (country.share ?? 1), 0);
|
|
1925
|
+
return {
|
|
1926
|
+
countries: spec.countries.map((country, index) => {
|
|
1927
|
+
const ownedStates = states.filter((state) => state.owner === country.tag);
|
|
1928
|
+
const pixels = ownedStates.reduce((sum, state) => sum + state.provinces.reduce((inner, id) => inner + provinces.size[id], 0), 0);
|
|
1929
|
+
return {
|
|
1930
|
+
tag: country.tag,
|
|
1931
|
+
capitalState: countryCapitalState.get(country.tag).id,
|
|
1932
|
+
capitalProvince: provinceId(capitals[index]),
|
|
1933
|
+
states: ownedStates.map(({ id }) => id),
|
|
1934
|
+
provinces: ownedStates.reduce((sum, state) => sum + state.provinces.length, 0),
|
|
1935
|
+
landShare: Math.round((pixels / landPixels) * 10_000) / 10_000,
|
|
1936
|
+
targetShare: Math.round(((country.share ?? 1) / weights) * 10_000) / 10_000,
|
|
1937
|
+
};
|
|
1938
|
+
}),
|
|
1939
|
+
unownedStates: states.filter((state) => state.owner === undefined).map(({ id }) => id),
|
|
1940
|
+
};
|
|
1941
|
+
};
|
|
1695
1942
|
// Localisation for everything named above.
|
|
1696
1943
|
add(`${context.localisationRoot}/replace/world_l_english.yml`, localisation([
|
|
1697
1944
|
...states.map((state) => [
|
|
1698
1945
|
`STATE_${state.id}`,
|
|
1699
|
-
stateNames.get(state.id) ??
|
|
1700
|
-
`${countryNames.get(state.owner) ?? state.owner} District ${state.id}`,
|
|
1946
|
+
stateNames.get(state.id) ?? `${ownerName(state)} District ${state.id}`,
|
|
1701
1947
|
]),
|
|
1702
1948
|
...states.flatMap((state) => [...state.victoryPoints.keys()].map((id) => [
|
|
1703
1949
|
`VICTORY_POINTS_${provinceId(id)}`,
|
|
1704
|
-
cityNames.get(id) ??
|
|
1705
|
-
`${countryNames.get(state.owner) ?? state.owner} Settlement ${provinceId(id)}`,
|
|
1950
|
+
cityNames.get(id) ?? `${ownerName(state)} Settlement ${provinceId(id)}`,
|
|
1706
1951
|
])),
|
|
1707
1952
|
...[...regionMembers.keys()]
|
|
1708
1953
|
.sort((a, b) => a - b)
|
|
1709
1954
|
.map((region) => [`STRATEGICREGION_${region}`, regionNames.get(region) ?? `Region ${region}`]),
|
|
1710
1955
|
...(spec.referenceLabels ?? []).map((label, index) => [`WORLD_GEOGRAPHY_${index + 1}`, label.name]),
|
|
1956
|
+
...[...lakeNames]
|
|
1957
|
+
.sort((a, b) => a[0] - b[0])
|
|
1958
|
+
.map(([id, name]) => [`PROV${provinceId(id)}`, name]),
|
|
1711
1959
|
...continentNames.map((name, index) => [`world_continent_${index + 1}`, name]),
|
|
1712
1960
|
...countryLocalisation,
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1961
|
+
...(spec.realms === undefined
|
|
1962
|
+
? [
|
|
1963
|
+
['WORLD_BOOKMARK_NAME', spec.worldName ?? 'A New World'],
|
|
1964
|
+
['WORLD_BOOKMARK_DESC', 'Nations of an unfamiliar map at the dawn of the war.'],
|
|
1965
|
+
['WORLD_OTHER_DESC', 'Any other nation of this world.'],
|
|
1966
|
+
]
|
|
1967
|
+
: []),
|
|
1716
1968
|
...majors.map((country) => [
|
|
1717
1969
|
`${country.tag}_WORLD_DESC`,
|
|
1718
1970
|
`${country.name} begins the game among the strongest nations.`,
|
|
@@ -1723,6 +1975,8 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1723
1975
|
warnings: [...interpretationWarnings, ...drainage.warnings],
|
|
1724
1976
|
notes,
|
|
1725
1977
|
...(synthesized === undefined ? {} : { terrain: synthesized }),
|
|
1978
|
+
...(economy === undefined ? {} : { railCost: economy.railCost }),
|
|
1979
|
+
...(gameplay === undefined ? {} : { gameplay: { crossings: gameplay.crossings, economy } }),
|
|
1726
1980
|
summary: {
|
|
1727
1981
|
width,
|
|
1728
1982
|
height,
|
|
@@ -1735,5 +1989,6 @@ export function generateWorld(spec, context, options = {}) {
|
|
|
1735
1989
|
countries: spec.countries.length,
|
|
1736
1990
|
rivers: riverCount,
|
|
1737
1991
|
},
|
|
1992
|
+
...(spec.realms === undefined ? {} : { realms: realmSummary() }),
|
|
1738
1993
|
};
|
|
1739
1994
|
}
|