Allow mappers to omit nation coordinates in manifest.json for random spawn 🎲 (#4156)

## Description:

Previously, every nation in a map's manifest.json required explicit
coordinates. Additional nations already supported optional coordinates
to trigger random spawn placement, but regular nations did not.

Idea from PlaysBadly.

Reasoning (copied off discord):

> I've been working on World Inverted by adding realistic 'nations' in
the form sunken ship names with their flags and location. However after
searching around for other possible nation locations that are ocean
related I realised that I might not have enough info for proper
'realisitc' coverage of the map. Currently Im at ~170 nations with
cordinates. This is not including the additional nations with no
locations. This will be reduced to ~62 as the default with the rest
turning into additional nations.
> 
> The problem is the end process is proving difficult. Trying to blance
the nation placment on the map is a little much at this volume. So being
able to add a few no-cordinate nations would be a great way to fill in
the map.

This PR also improves the MapConsistency test to check the additional
nations too.

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
This commit is contained in:
FloPinguin
2026-06-05 00:57:00 +02:00
committed by GitHub
parent bf648a2a58
commit 74b3bd275b
4 changed files with 94 additions and 33 deletions
+56 -27
View File
@@ -294,39 +294,68 @@ describe("Map consistency", () => {
const info = JSON.parse(fs.readFileSync(infoPath, "utf8"));
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
type NationEntry = { name: string; coordinates: [number, number] };
const infoNations: NationEntry[] = (info.nations ?? []).map(
(n: NationEntry) => ({ name: n.name, coordinates: n.coordinates }),
);
const manifestNations: NationEntry[] = (manifest.nations ?? []).map(
(n: NationEntry) => ({ name: n.name, coordinates: n.coordinates }),
);
// ── Compare nations ──────────────────────────────────────────────
type NationEntry = {
name: string;
coordinates?: [number, number];
};
if (infoNations.length !== manifestNations.length) {
errors.push(
`${key}: nation count mismatch — info.json has ${infoNations.length}, manifest.json has ${manifestNations.length}`,
);
continue;
}
// Compare nations by index (order must match; names can be duplicated).
for (let i = 0; i < infoNations.length; i++) {
const inf = infoNations[i];
const man = manifestNations[i];
if (inf.name !== man.name) {
function compareNationArrays(
label: string,
infoArr: NationEntry[],
manifestArr: NationEntry[],
): void {
if (infoArr.length !== manifestArr.length) {
errors.push(
`${key}: nations[${i}] name mismatch — info.json "${inf.name}" vs manifest.json "${man.name}"`,
`${key}: ${label} count mismatch — info.json has ${infoArr.length}, manifest.json has ${manifestArr.length}`,
);
continue;
return;
}
const [ix, iy] = inf.coordinates;
const [mx, my] = man.coordinates;
if (ix !== mx || iy !== my) {
errors.push(
`${key}: nation "${inf.name}" (index ${i}) coordinates differ — info.json [${ix}, ${iy}] vs manifest.json [${mx}, ${my}]`,
);
for (let i = 0; i < infoArr.length; i++) {
const inf = infoArr[i];
const man = manifestArr[i];
if (inf.name !== man.name) {
errors.push(
`${key}: ${label}[${i}] name mismatch — info.json "${inf.name}" vs manifest.json "${man.name}"`,
);
continue;
}
const infHasCoords = inf.coordinates !== undefined;
const manHasCoords = man.coordinates !== undefined;
if (infHasCoords !== manHasCoords) {
errors.push(
`${key}: ${label} "${inf.name}" (index ${i}) coordinate presence differs — info.json ${infHasCoords ? "has" : "missing"} coordinates, manifest.json ${manHasCoords ? "has" : "missing"} coordinates`,
);
continue;
}
if (inf.coordinates && man.coordinates) {
const [ix, iy] = inf.coordinates;
const [mx, my] = man.coordinates;
if (ix !== mx || iy !== my) {
errors.push(
`${key}: ${label} "${inf.name}" (index ${i}) coordinates differ — info.json [${ix}, ${iy}] vs manifest.json [${mx}, ${my}]`,
);
}
}
}
}
const toEntry = (n: NationEntry) => ({
name: n.name,
coordinates: n.coordinates,
});
compareNationArrays(
"nation",
(info.nations ?? []).map(toEntry),
(manifest.nations ?? []).map(toEntry),
);
compareNationArrays(
"additionalNation",
(info.additionalNations ?? []).map(toEntry),
(manifest.additionalNations ?? []).map(toEntry),
);
} catch (err) {
errors.push(`${key}: failed to parse JSON — ${(err as Error).message}`);
}
+28
View File
@@ -243,6 +243,34 @@ describe("createNationsForGame: additionalNations pool", () => {
expect(withoutCoords!.spawnCell).toBeUndefined();
});
test("uses coordinates from manifest nations when provided, undefined when omitted", () => {
const manifest: ManifestNation[] = [
{ name: "WithCoords", coordinates: [10, 20] },
{ name: "WithoutCoords" },
];
const random = new PseudoRandom(5);
const nations = createNationsForGame(
makeGameStart(2),
manifest,
[],
0,
random,
);
expect(nations).toHaveLength(2);
const withCoords = nations.find((n) => n.playerInfo.name === "WithCoords");
const withoutCoords = nations.find(
(n) => n.playerInfo.name === "WithoutCoords",
);
expect(withCoords).toBeDefined();
expect(withoutCoords).toBeDefined();
expect(withCoords!.spawnCell?.x).toBe(10);
expect(withCoords!.spawnCell?.y).toBe(20);
expect(withoutCoords!.spawnCell).toBeUndefined();
});
test("produces unique nation names overall", () => {
const manifest = makeManifestNations(3);
const extras = makeAdditionalNations(["Ex1", "Ex2", "Ex3"]);