From e26230275be8b5cddb6468dddde842f0f72bc182 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 16 Nov 2024 11:45:28 -0800 Subject: [PATCH] add missile silo --- TODO.txt | 5 +- resources/images/MissileSiloIconWhite.svg | 86 +++++++++++++ resources/images/MissileSiloUnit.png | Bin 0 -> 115 bytes resources/images/NukeUnitIcon.png | Bin 0 -> 133 bytes src/client/graphics/layers/StructureLayer.ts | 114 ++++++++++++------ .../graphics/layers/radial/BuildMenu.ts | 7 +- src/core/execution/ExecutionManager.ts | 3 + src/core/execution/MissileSiloExecution.ts | 60 +++++++++ src/core/execution/PortExecution.ts | 1 + src/core/game/BuildValidator.ts | 2 + src/core/game/Game.ts | 4 +- 11 files changed, 240 insertions(+), 42 deletions(-) create mode 100644 resources/images/MissileSiloIconWhite.svg create mode 100644 resources/images/MissileSiloUnit.png create mode 100644 resources/images/NukeUnitIcon.png create mode 100644 src/core/execution/MissileSiloExecution.ts diff --git a/TODO.txt b/TODO.txt index d41f42b41..22f221e1e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -182,10 +182,13 @@ * create trade routes DONE 11/15/2024 * add trade ship DONE 11/15/2024 * trade ship gives gold when completes route DONE 11/15/2024 -* add missile silo +* add missile silo DONE 11/15/2024 * nuke spawns from missile silo * destroyer can capture trade ships * add battleship +* add defense post +* add radiation from nuke +* REFACTOR: make TransportShip follow build unit flow * NPC has relations * use twitter emojis * private game shows how many players joined diff --git a/resources/images/MissileSiloIconWhite.svg b/resources/images/MissileSiloIconWhite.svg new file mode 100644 index 000000000..071fbf245 --- /dev/null +++ b/resources/images/MissileSiloIconWhite.svg @@ -0,0 +1,86 @@ + + + + + + + + + + diff --git a/resources/images/MissileSiloUnit.png b/resources/images/MissileSiloUnit.png new file mode 100644 index 0000000000000000000000000000000000000000..bc94a189437a2428e1e415ba6eca7784c8d19c46 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VjKx9jP7LeL$-D$|bUj@hLo|Yu zQx35Fmz0o@nAjR1|%O$WD@{VjKx9jP7LeL$-D$|>^)r^Lo|Yu zQxXz>oM&L{36Pq}bh=R`q3eR^p`#Z?6EAaYXPP~=z%b^WgW&OH9EbnD=-N=#l;V0~ f*?~1I(o77^XL;B)C->|C8pYu0>gTe~DWM4fJ54HT literal 0 HcmV?d00001 diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 94f904a7c..bb8199485 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -6,28 +6,49 @@ import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; import anchorIcon from '../../../../resources/images/AnchorIcon.png'; +import missileSiloIcon from '../../../../resources/images/MissileSiloUnit.png'; + +interface UnitRenderConfig { + icon: string; + borderRadius: number; + territoryRadius: number; +} + export class StructureLayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private imageData: ImageData; - private anchorImage: HTMLImageElement; - private anchorImageLoaded: boolean = false; - - + private unitImages: Map = new Map(); private theme: Theme = null; + // Configuration for supported unit types only + private readonly unitConfigs: Partial> = { + [UnitType.Port]: { + icon: anchorIcon, + borderRadius: 8, + territoryRadius: 6 + }, + [UnitType.MissileSilo]: { + icon: missileSiloIcon, + borderRadius: 8, + territoryRadius: 6 + } + }; + constructor(private game: Game, private eventBus: EventBus) { this.theme = game.config().theme(); - this.loadAnchorImage(); + this.loadUnitImages(); } - private loadAnchorImage() { - this.anchorImage = new Image(); - this.anchorImage.onload = () => { - this.anchorImageLoaded = true; - }; - this.anchorImage.src = anchorIcon; + private loadUnitImages() { + Object.entries(this.unitConfigs).forEach(([unitType, config]) => { + const image = new Image(); + image.src = config.icon; + image.onload = () => { + this.unitImages.set(unitType, image); + }; + }); } shouldTransform(): boolean { @@ -69,53 +90,74 @@ export class StructureLayer implements Layer { ); } - private handlePortEvent(event: UnitEvent) { - if (!this.anchorImageLoaded) return; + private isUnitTypeSupported(unitType: UnitType): boolean { + return unitType in this.unitConfigs; + } - bfs(event.unit.tile(), euclDist(event.unit.tile(), 8)) + private handleUnitRendering(event: UnitEvent) { + const unitType = event.unit.type(); + if (!this.isUnitTypeSupported(unitType)) return; + + const config = this.unitConfigs[unitType]; + const unitImage = this.unitImages.get(unitType); + + if (!config || !unitImage) return; + + // Clear previous rendering + bfs(event.unit.tile(), euclDist(event.unit.tile(), config.borderRadius)) .forEach(t => this.clearCell(t.cell())); if (!event.unit.isActive()) { - return + return; } - // Create a temporary canvas to process the anchor icon + + // Create temporary canvas for icon processing const tempCanvas = document.createElement('canvas'); const tempContext = tempCanvas.getContext('2d'); - tempCanvas.width = this.anchorImage.width; - tempCanvas.height = this.anchorImage.height; + tempCanvas.width = unitImage.width; + tempCanvas.height = unitImage.height; - // Draw the anchor icon to the temporary canvas - tempContext.drawImage(this.anchorImage, 0, 0); + // Draw the unit icon + tempContext.drawImage(unitImage, 0, 0); const iconData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height); - // Calculate position to center the icon on the port const cell = event.unit.tile().cell(); const startX = cell.x - Math.floor(tempCanvas.width / 2); const startY = cell.y - Math.floor(tempCanvas.height / 2); - bfs(event.unit.tile(), euclDist(event.unit.tile(), 8)) + // Draw border and territory + bfs(event.unit.tile(), euclDist(event.unit.tile(), config.borderRadius)) .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)); - bfs(event.unit.tile(), euclDist(event.unit.tile(), 6)) + bfs(event.unit.tile(), euclDist(event.unit.tile(), config.territoryRadius)) .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 255)); - // Process each pixel of the icon - for (let y = 0; y < tempCanvas.height; y++) { - for (let x = 0; x < tempCanvas.width; x++) { - const iconIndex = (y * tempCanvas.width + x) * 4; + + // Draw the icon + this.renderIcon(iconData, startX, startY, tempCanvas.width, tempCanvas.height, event.unit); + } + + private renderIcon( + iconData: ImageData, + startX: number, + startY: number, + width: number, + height: number, + unit: Unit + ) { + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const iconIndex = (y * width + x) * 4; const alpha = iconData.data[iconIndex + 3]; - if (alpha > 0) { // Only process non-transparent pixels + if (alpha > 0) { const targetX = startX + x; const targetY = startY + y; - // Check if the target pixel is within the game bounds if (targetX >= 0 && targetX < this.game.width() && targetY >= 0 && targetY < this.game.height()) { - - // Color the pixel using the unit owner's colors this.paintCell( new Cell(targetX, targetY), - this.theme.borderColor(event.unit.owner().info()), + this.theme.borderColor(unit.owner().info()), alpha ); } @@ -125,11 +167,7 @@ export class StructureLayer implements Layer { } onUnitEvent(event: UnitEvent) { - switch (event.unit.type()) { - case UnitType.Port: - this.handlePortEvent(event); - break; - } + this.handleUnitRendering(event); } paintCell(cell: Cell, color: Colord, alpha: number) { @@ -144,6 +182,6 @@ export class StructureLayer implements Layer { clearCell(cell: Cell) { const index = (cell.y * this.game.width()) + cell.x; const offset = index * 4; - this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) + this.imageData.data[offset + 3] = 0; } } \ No newline at end of file diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 4cb243426..93f8a487a 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -5,6 +5,7 @@ import { Cell, Game, BuildItem, BuildItems, Player, UnitType } from '../../../.. import { BuildUnitIntentEvent, SendNukeIntentEvent } from '../../../Transport'; import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg'; import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg'; +import missileSiloIcon from '../../../../../resources/images/MissileSiloIconWhite.svg'; import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import portIcon from '../../../../../resources/images/PortIcon.svg'; import { renderNumber } from '../../Utils'; @@ -20,7 +21,8 @@ const buildTable: BuildItemDisplay[][] = [ [ { item: BuildItems.Nuke, icon: nukeIcon }, { item: BuildItems.Destroyer, icon: destroyerIcon }, - { item: BuildItems.Port, icon: portIcon } + { item: BuildItems.Port, icon: portIcon }, + { item: BuildItems.MissileSilo, icon: missileSiloIcon } ] ]; @@ -29,7 +31,6 @@ export class BuildMenu extends LitElement { public game: Game; public eventBus: EventBus; public buildValidator: BuildValidator; - private myPlayer: Player; private clickedCell: Cell; @@ -165,6 +166,8 @@ export class BuildMenu extends LitElement { case BuildItems.Port: this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Port, this.clickedCell)) break + case BuildItems.MissileSilo: + this.eventBus.emit(new BuildUnitIntentEvent(UnitType.MissileSilo, this.clickedCell)) } this.hideMenu() }; diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 1f860b57a..194b425d2 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -18,6 +18,7 @@ import { NukeExecution } from "./NukeExecution"; import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; import { DestroyerExecution } from "./DestroyerExecution"; import { PortExecution } from "./PortExecution"; +import { MissileSiloExecution } from "./MissileSiloExecution"; @@ -88,6 +89,8 @@ export class Executor { return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y)) case UnitType.Port: return new PortExecution(intent.player, new Cell(intent.x, intent.y)) + case UnitType.MissileSilo: + return new MissileSiloExecution(intent.player, new Cell(intent.x, intent.y)) default: throw Error(`unit type ${intent.unit} not supported`) } diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts new file mode 100644 index 000000000..d6afb9db5 --- /dev/null +++ b/src/core/execution/MissileSiloExecution.ts @@ -0,0 +1,60 @@ +import { BuildValidator } from "../game/BuildValidator"; +import { AllPlayers, BuildItem, BuildItems, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; +import { AStar, PathFinder } from "../PathFinding"; +import { PseudoRandom } from "../PseudoRandom"; +import { bfs, dist, manhattanDist } from "../Util"; +import { TradeShipExecution } from "./TradeShipExecution"; + +export class MissileSiloExecution implements Execution { + + private active = true + private mg: MutableGame + private player: MutablePlayer + private silo: MutableUnit + + constructor( + private _owner: PlayerID, + private cell: Cell + ) { } + + + init(mg: MutableGame, ticks: number): void { + this.mg = mg + this.player = mg.player(this._owner) + } + + tick(ticks: number): void { + if (this.silo == null) { + const tile = this.mg.tile(this.cell) + if (!new BuildValidator(this.mg).canBuild(this.player, tile, BuildItems.MissileSilo)) { + console.warn(`player ${this.player} cannot build port at ${this.cell}`) + this.active = false + return + } + this.silo = this.player.addUnit(UnitType.MissileSilo, 0, tile) + } + + if (!this.silo.tile().hasOwner()) { + this.silo.delete() + this.active = false + return + } + if (this.silo.tile().owner() != this.silo.owner()) { + this.silo.setOwner(this.silo.tile().owner() as Player) + this.player = this.silo.owner() + } + } + + owner(): MutablePlayer { + return null + } + + isActive(): boolean { + return this.active + } + + activeDuringSpawnPhase(): boolean { + return false + } + +} \ No newline at end of file diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 8ca92aa31..c4a5d199f 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -55,6 +55,7 @@ export class PortExecution implements Execution { } if (this.port.tile().owner() != this.port.owner()) { this.port.setOwner(this.port.tile().owner() as Player) + this.player = this.port.owner() } const allPorts = this.mg.units(UnitType.Port) diff --git a/src/core/game/BuildValidator.ts b/src/core/game/BuildValidator.ts index 6a61198cf..c27cf87fa 100644 --- a/src/core/game/BuildValidator.ts +++ b/src/core/game/BuildValidator.ts @@ -15,6 +15,8 @@ export class BuildValidator { return this.canBuildPort(player, tile) case BuildItems.Destroyer: return this.canBuildDestroyer(player, tile) + case BuildItems.MissileSilo: + return tile.owner() == player default: throw Error(`item ${item.type} not supported`) } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index a3bbe96cf..936473d26 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -28,6 +28,7 @@ export enum UnitType { Port = "Port", Nuke = "Nuke", TradeShip = "Trade Ship", + MissileSilo = "Missile Silo", } export class BuildItem { @@ -41,7 +42,8 @@ export const BuildItems = { // Nuke: new BuildItem(UnitType.Nuke, 1_000_000), Nuke: new BuildItem(UnitType.Nuke, 10), Destroyer: new BuildItem(UnitType.Destroyer, 10), - Port: new BuildItem(UnitType.Port, 0) + Port: new BuildItem(UnitType.Port, 0), + MissileSilo: new BuildItem(UnitType.MissileSilo, 10), } as const; export class Nation {