From 84951fed9f7d9a9bb7b9ac12ebea08df787972a2 Mon Sep 17 00:00:00 2001 From: Readixyee <49241765+Readixyee@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:25:51 +0100 Subject: [PATCH] Sam anti nuke missile launcher (#176) now with better name --------- Co-authored-by: evanpelle --- src/client/graphics/layers/BuildMenu.ts | 6 + src/client/graphics/layers/StructureLayer.ts | 6 + src/client/graphics/layers/UnitLayer.ts | 39 ++++++ src/core/configuration/DefaultConfig.ts | 19 +++ src/core/execution/ConstructionExecution.ts | 4 + src/core/execution/NukeExecution.ts | 8 ++ src/core/execution/SAMLauncherExecution.ts | 123 +++++++++++++++++++ src/core/execution/SAMMissileExecution.ts | 104 ++++++++++++++++ src/core/game/Game.ts | 2 + src/core/game/PlayerImpl.ts | 2 + 10 files changed, 313 insertions(+) create mode 100644 src/core/execution/SAMLauncherExecution.ts create mode 100644 src/core/execution/SAMMissileExecution.ts diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index d35c9ac09..b01f91a3c 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -61,6 +61,12 @@ const buildTable: BuildItemDisplay[][] = [ icon: missileSiloIcon, description: "Used to launch nukes", }, + // needs new icon + { + unitType: UnitType.SAMLauncher, + icon: shieldIcon, + description: "Defends against incoming nukes", + }, { unitType: UnitType.DefensePost, icon: shieldIcon, diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 6499d1101..58f81e621 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -5,6 +5,7 @@ import { EventBus } from "../../../core/EventBus"; import anchorIcon from "../../../../resources/images/buildings/port1.png"; import missileSiloIcon from "../../../../resources/images/buildings/silo1.png"; +import SAMMissileIcon from "../../../../resources/images/buildings/extra/silo4.png"; import shieldIcon from "../../../../resources/images/buildings/fortAlt2.png"; import cityIcon from "../../../../resources/images/buildings/cityAlt1.png"; import { GameView, UnitView } from "../../../core/game/GameView"; @@ -48,6 +49,11 @@ export class StructureLayer implements Layer { borderRadius: 8, territoryRadius: 6, }, + [UnitType.SAMLauncher]: { + icon: SAMMissileIcon, + borderRadius: 8, + territoryRadius: 6, + }, }; constructor( diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 83e485eff..5529cf76f 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -215,6 +215,9 @@ export class UnitLayer implements Layer { case UnitType.Shell: this.handleShellEvent(unit); break; + case UnitType.SAMMissile: + this.handleMissileEvent(unit); + break; case UnitType.TradeShip: this.handleTradeShipEvent(unit); break; @@ -324,6 +327,42 @@ export class UnitLayer implements Layer { ); } + // interception missle from SAM + private handleMissileEvent(unit: UnitView) { + const rel = this.relationship(unit); + const range = 2; + + for (const t of this.game.bfs( + unit.lastTile(), + euclDistFN(unit.lastTile(), range), + )) { + this.clearCell(this.game.x(t), this.game.y(t)); + } + + if (unit.isActive()) { + for (const t of this.game.bfs( + unit.tile(), + euclDistFN(unit.tile(), range), + )) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.spawnHighlightColor(), + 255, + ); + } + + this.paintCell( + this.game.x(unit.tile()), + this.game.y(unit.tile()), + rel, + this.theme.borderColor(unit.owner().info()), + 255, + ); + } + } + private handleNuke(unit: UnitView) { const rel = this.relationship(unit); let range = 0; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index c1169273b..113cddf3d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -159,6 +159,11 @@ export class DefaultConfig implements Config { territoryBound: false, damage: 250, }; + case UnitType.SAMMissile: + return { + cost: () => 0, + territoryBound: false, + }; case UnitType.Port: return { cost: (p: Player) => @@ -225,6 +230,20 @@ export class DefaultConfig implements Config { territoryBound: true, constructionDuration: this.instantBuild() ? 0 : 5 * 10, }; + case UnitType.SAMLauncher: + return { + cost: (p: Player) => + p.type() == PlayerType.Human && this.infiniteGold() + ? 0 + : Math.min( + 1_000_000, + (p.unitsIncludingConstruction(UnitType.SAMLauncher).length + + 1) * + 1_000_000, + ), + territoryBound: true, + constructionDuration: this.instantBuild() ? 0 : 10 * 10, + }; case UnitType.City: return { cost: (p: Player) => diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index f6a545528..efe416033 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -12,6 +12,7 @@ import { import { TileRef } from "../game/GameMap"; import { CityExecution } from "./CityExecution"; import { DefensePostExecution } from "./DefensePostExecution"; +import { SAMLauncherExecution } from "./SAMLauncherExecution"; import { MirvExecution } from "./MIRVExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; import { NukeExecution } from "./NukeExecution"; @@ -111,6 +112,9 @@ export class ConstructionExecution implements Execution { case UnitType.DefensePost: this.mg.addExecution(new DefensePostExecution(player.id(), this.tile)); break; + case UnitType.SAMLauncher: + this.mg.addExecution(new SAMLauncherExecution(player.id(), this.tile)); + break; case UnitType.City: this.mg.addExecution(new CityExecution(player.id(), this.tile)); break; diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index a752a7eaa..65dfea8d9 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -82,6 +82,14 @@ export class NukeExecution implements Execution { ); } } + + // make the nuke unactive if it was intercepted + if (!this.nuke.isActive()) { + consolex.warn(`Nuke destroyed before reaching target`); + this.active = false; + return; + } + if (this.waitTicks > 0) { this.waitTicks--; return; diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts new file mode 100644 index 000000000..c5403e292 --- /dev/null +++ b/src/core/execution/SAMLauncherExecution.ts @@ -0,0 +1,123 @@ +import { consolex } from "../Consolex"; +import { + Cell, + Execution, + Game, + Player, + Unit, + PlayerID, + UnitType, +} from "../game/Game"; +import { manhattanDistFN, TileRef } from "../game/GameMap"; +import { SAMMissileExecution } from "./SAMMissileExecution"; +import { PseudoRandom } from "../PseudoRandom"; + +export class SAMLauncherExecution implements Execution { + private player: Player; + private mg: Game; + private post: Unit; + private active: boolean = true; + + private target: Unit = null; + + private searchRange = 100; + + private missileAttackRate = 50; + private lastMissileAttack = 0; + + private pseudoRandom: PseudoRandom; + + constructor( + private ownerId: PlayerID, + private tile: TileRef, + ) {} + + init(mg: Game, ticks: number): void { + this.mg = mg; + if (!mg.hasPlayer(this.ownerId)) { + console.warn(`SAMLauncherExecution: owner ${this.ownerId} not found`); + this.active = false; + return; + } + this.player = mg.player(this.ownerId); + } + + tick(ticks: number): void { + if (this.post == null) { + const spawnTile = this.player.canBuild(UnitType.SAMLauncher, this.tile); + if (spawnTile == false) { + consolex.warn("cannot build SAM Launcher"); + this.active = false; + return; + } + this.post = this.player.buildUnit(UnitType.SAMLauncher, 0, spawnTile); + } + if (!this.post.isActive()) { + this.active = false; + return; + } + + if (!this.pseudoRandom) { + this.pseudoRandom = new PseudoRandom(this.post.id()); + } + + const nukes = this.mg + .units(UnitType.AtomBomb, UnitType.HydrogenBomb) + .filter( + (u) => + this.mg.manhattanDist(u.tile(), this.post.tile()) < this.searchRange, + ) + .filter((u) => u.owner() !== this.player) + .filter((u) => !u.owner().isAlliedWith(this.player)); + + this.target = + nukes.sort((a, b) => { + // Prioritize HydrogenBombs first + if ( + a.type() === UnitType.HydrogenBomb && + b.type() !== UnitType.HydrogenBomb + ) { + return -1; + } + if ( + a.type() !== UnitType.HydrogenBomb && + b.type() === UnitType.HydrogenBomb + ) { + return 1; + } + // If both are the same type, sort by distance + return ( + this.mg.manhattanDist(this.post.tile(), a.tile()) - + this.mg.manhattanDist(this.post.tile(), b.tile()) + ); + })[0] ?? null; + + if (this.target != null) { + if (this.mg.ticks() - this.lastMissileAttack > this.missileAttackRate) { + this.lastMissileAttack = this.mg.ticks(); + this.mg.addExecution( + new SAMMissileExecution( + this.post.tile(), + this.post.owner(), + this.post, + this.target, + this.mg, + this.pseudoRandom.next(), + ), + ); + } + } + } + + owner(): Player { + return null; + } + + isActive(): boolean { + return this.active; + } + + activeDuringSpawnPhase(): boolean { + return false; + } +} diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts new file mode 100644 index 000000000..df7d22682 --- /dev/null +++ b/src/core/execution/SAMMissileExecution.ts @@ -0,0 +1,104 @@ +import { + Execution, + Game, + MessageType, + Player, + Unit, + UnitType, +} from "../game/Game"; +import { PathFinder } from "../pathfinding/PathFinding"; +import { PathFindResultType } from "../pathfinding/AStar"; +import { consolex } from "../Consolex"; +import { TileRef } from "../game/GameMap"; + +export class SAMMissileExecution implements Execution { + private active = true; + private pathFinder: PathFinder; + private SAMMissile: Unit; + + constructor( + private spawn: TileRef, + private _owner: Player, + private ownerUnit: Unit, + private target: Unit, + private mg: Game, + private pseudoRandom: number, + private speed: number = 6, + private hittingChance: number = 0.75, + ) {} + + init(mg: Game, ticks: number): void { + this.pathFinder = PathFinder.Mini(mg, 2000, true, 10); + } + + tick(ticks: number): void { + if (this.SAMMissile == null) { + this.SAMMissile = this._owner.buildUnit( + UnitType.SAMMissile, + 0, + this.spawn, + ); + } + if (!this.SAMMissile.isActive()) { + this.active = false; + return; + } + if ( + !this.target.isActive() || + !this.ownerUnit.isActive() || + this.target.owner() == this.SAMMissile.owner() + ) { + this.SAMMissile.delete(false); + this.active = false; + return; + } + for (let i = 0; i < this.speed; i++) { + const result = this.pathFinder.nextTile( + this.SAMMissile.tile(), + this.target.tile(), + 3, + ); + switch (result.type) { + case PathFindResultType.Completed: + this.active = false; + if (this.pseudoRandom < this.hittingChance) { + this.target.delete(); + + this.mg.displayMessage( + `Missile succesfully intercepted ${this.target.type()}`, + MessageType.SUCCESS, + this._owner.id(), + ); + } else { + this.mg.displayMessage( + `Missile failed to intercept ${this.target.type()}`, + MessageType.ERROR, + this._owner.id(), + ); + } + this.SAMMissile.delete(false); + return; + case PathFindResultType.NextTile: + this.SAMMissile.move(result.tile); + break; + case PathFindResultType.Pending: + return; + case PathFindResultType.PathNotFound: + consolex.log(`Missile ${this.SAMMissile} could not find target`); + this.active = false; + this.SAMMissile.delete(false); + return; + } + } + } + + owner(): Player { + return null; + } + isActive(): boolean { + return this.active; + } + activeDuringSpawnPhase(): boolean { + return false; + } +} diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 558d3c413..9c3c214eb 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -71,12 +71,14 @@ export enum UnitType { TransportShip = "Transport", Warship = "Warship", Shell = "Shell", + SAMMissile = "SAMMissile", Port = "Port", AtomBomb = "Atom Bomb", HydrogenBomb = "Hydrogen Bomb", TradeShip = "Trade Ship", MissileSilo = "Missile Silo", DefensePost = "Defense Post", + SAMLauncher = "SAM Launcher", City = "City", MIRV = "MIRV", MIRVWarhead = "MIRV Warhead", diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 02341d4b9..d9e5f972a 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -689,6 +689,7 @@ export class PlayerImpl implements Player { case UnitType.Warship: return this.warshipSpawn(targetTile); case UnitType.Shell: + case UnitType.SAMMissile: return targetTile; case UnitType.TransportShip: return this.transportShipSpawn(targetTile); @@ -696,6 +697,7 @@ export class PlayerImpl implements Player { return this.tradeShipSpawn(targetTile); case UnitType.MissileSilo: case UnitType.DefensePost: + case UnitType.SAMLauncher: case UnitType.City: case UnitType.Construction: return this.landBasedStructureSpawn(targetTile);