Sam anti nuke missile launcher (#176)

now with better name

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Readixyee
2025-03-09 21:25:51 +01:00
committed by GitHub
parent fe9a73c967
commit 84951fed9f
10 changed files with 313 additions and 0 deletions
+6
View File
@@ -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,
@@ -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(
+39
View File
@@ -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;
+19
View File
@@ -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) =>
@@ -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;
+8
View File
@@ -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;
+123
View File
@@ -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;
}
}
+104
View File
@@ -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;
}
}
+2
View File
@@ -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",
+2
View File
@@ -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);