From e4a4c5dc12ebafa327ddf15a65c8fadd1a815510 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Mon, 25 Aug 2025 18:07:57 -0400 Subject: [PATCH] Nations build SAM launchers (#1931) ## Description: Fixes #201 by adding the ability for nations to build SAM launchers. image ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced --- src/core/execution/FakeHumanExecution.ts | 1 + src/core/execution/SAMLauncherExecution.ts | 3 +- .../nation/structureSpawnTileValue.ts | 79 +++++++++++++++---- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index d55161654..083f23e6a 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -437,6 +437,7 @@ export class FakeHumanExecution implements Execution { this.maybeSpawnStructure(UnitType.Port) || this.maybeSpawnWarship() || this.maybeSpawnStructure(UnitType.Factory) || + this.maybeSpawnStructure(UnitType.SAMLauncher) || this.maybeSpawnStructure(UnitType.MissileSilo) ); } diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 0c444ef91..c1b3f71c8 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -50,7 +50,8 @@ class SAMTargetingSystem { private isInRange(tile: TileRef) { const samTile = this.sam.tile(); - const rangeSquared = this.mg.config().defaultSamRange() ** 2; + const range = this.mg.config().defaultSamRange(); + const rangeSquared = range * range; return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared; } diff --git a/src/core/execution/nation/structureSpawnTileValue.ts b/src/core/execution/nation/structureSpawnTileValue.ts index 563f42f80..55e1cfa90 100644 --- a/src/core/execution/nation/structureSpawnTileValue.ts +++ b/src/core/execution/nation/structureSpawnTileValue.ts @@ -13,24 +13,9 @@ export function structureSpawnTileValue( const borderSpacing = mg.config().nukeMagnitudes(UnitType.AtomBomb).outer; const structureSpacing = borderSpacing * 2; switch (type) { - case UnitType.Port: - return (tile) => { - let w = 0; - - // Prefer to be far away from other structures of the same type - const otherTiles: Set = new Set(otherUnits.map((u) => u.tile())); - otherTiles.delete(tile); - const closestOther = closestTwoTiles(mg, otherTiles, [tile]); - if (closestOther !== null) { - const d = mg.manhattanDist(closestOther.x, tile); - w += Math.min(d, structureSpacing); - } - - return w; - }; case UnitType.City: case UnitType.Factory: - case UnitType.MissileSilo: + case UnitType.MissileSilo: { return (tile) => { let w = 0; @@ -56,6 +41,68 @@ export function structureSpawnTileValue( // TODO: Cities and factories should consider train range limits return w; }; + } + case UnitType.Port: { + return (tile) => { + let w = 0; + + // Prefer to be far away from other structures of the same type + const otherTiles: Set = new Set(otherUnits.map((u) => u.tile())); + otherTiles.delete(tile); + const closestOther = closestTwoTiles(mg, otherTiles, [tile]); + if (closestOther !== null) { + const d = mg.manhattanDist(closestOther.x, tile); + w += Math.min(d, structureSpacing); + } + + return w; + }; + } + case UnitType.SAMLauncher: { + const protectTiles: Set = new Set(); + for (const unit of player.units()) { + switch(unit.type()) { + case UnitType.City: + case UnitType.Factory: + case UnitType.MissileSilo: + case UnitType.Port: + protectTiles.add(unit.tile()); + } + } + const range = mg.config().defaultSamRange(); + const rangeSquared = range * range; + return (tile) => { + let w = 0; + + // Prefer higher elevations + w += mg.magnitude(tile); + + // Prefer to be away from the border + const closestBorder = closestTwoTiles(mg, borderTiles, [tile]); + if (closestBorder !== null) { + const d = mg.manhattanDist(closestBorder.x, tile); + w += Math.min(d, borderSpacing); + } + + // Prefer to be far away from other structures of the same type + const otherTiles: Set = new Set(otherUnits.map((u) => u.tile())); + otherTiles.delete(tile); + const closestOther = closestTwoTiles(mg, otherTiles, [tile]); + if (closestOther !== null) { + const d = mg.manhattanDist(closestOther.x, tile); + w += Math.min(d, structureSpacing); + } + + // Prefer to be in range of other structures + for (const maybeProtected of protectTiles) { + const distanceSquared = mg.euclideanDistSquared(tile, maybeProtected); + if (distanceSquared > rangeSquared) continue; + w += structureSpacing; + } + + return w; + }; + } default: throw new Error(`Value function not implemented for ${type}`); }