From 74b3bd275be7e804e54dfd89d40ae74da62d18cc Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:57:00 +0200 Subject: [PATCH] =?UTF-8?q?Allow=20mappers=20to=20omit=20nation=20coordina?= =?UTF-8?q?tes=20in=20manifest.json=20for=20random=20spawn=20=F0=9F=8E=B2?= =?UTF-8?q?=20(#4156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- src/core/game/NationCreation.ts | 4 +- src/core/game/TerrainMapLoader.ts | 12 +++-- tests/MapConsistency.test.ts | 83 +++++++++++++++++++++---------- tests/NationCreation.test.ts | 28 +++++++++++ 4 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/core/game/NationCreation.ts b/src/core/game/NationCreation.ts index 527b98161..62068257e 100644 --- a/src/core/game/NationCreation.ts +++ b/src/core/game/NationCreation.ts @@ -34,7 +34,9 @@ export function createNationsForGame( ): Nation[] { const toNation = (n: ManifestNation): Nation => new Nation( - new Cell(n.coordinates[0], n.coordinates[1]), + n.coordinates !== undefined + ? new Cell(n.coordinates[0], n.coordinates[1]) + : undefined, new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()), ); diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 5b9423a55..69e8b9888 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -32,7 +32,7 @@ export interface MapManifest { } export interface Nation { - coordinates: [number, number]; + coordinates?: [number, number]; flag?: string; name: string; } @@ -69,10 +69,12 @@ export async function loadTerrainMap( if (mapSize === GameMapSize.Compact) { manifest.nations.forEach((nation) => { - nation.coordinates = [ - Math.floor(nation.coordinates[0] / 2), - Math.floor(nation.coordinates[1] / 2), - ]; + if (nation.coordinates !== undefined) { + nation.coordinates = [ + Math.floor(nation.coordinates[0] / 2), + Math.floor(nation.coordinates[1] / 2), + ]; + } }); manifest.additionalNations?.forEach((nation) => { if (nation.coordinates !== undefined) { diff --git a/tests/MapConsistency.test.ts b/tests/MapConsistency.test.ts index ade0b7ee6..ff1e71f96 100644 --- a/tests/MapConsistency.test.ts +++ b/tests/MapConsistency.test.ts @@ -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}`); } diff --git a/tests/NationCreation.test.ts b/tests/NationCreation.test.ts index ebac8eb2d..128a6543a 100644 --- a/tests/NationCreation.test.ts +++ b/tests/NationCreation.test.ts @@ -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"]);