From 3ce5785d1e45cab7fd7d19f8f53240f265192c43 Mon Sep 17 00:00:00 2001 From: Ilan Schemoul Date: Tue, 11 Mar 2025 23:44:45 +0100 Subject: [PATCH] nuke icon (#207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **feat: white nuke icon next to name if player nukes you** ![Capture d'écran 2025-03-10 220439](https://github.com/user-attachments/assets/1b717b2d-bffb-45fc-96ea-2feb57d25de0) - **feat: red nuke icon if player sends nuke towards you** - ![Capture d'écran 2025-03-10 220358](https://github.com/user-attachments/assets/b755fa06-9510-4bd1-8312-7180dc681d85) --- resources/images/NukeIconRed.svg | 54 ++++++++++++++++++++++++ src/client/graphics/layers/NameLayer.ts | 52 +++++++++++++++++++++++ src/client/graphics/layers/UnitLayer.ts | 4 +- src/core/execution/NukeExecution.ts | 4 +- src/core/execution/TradeShipExecution.ts | 9 ++-- src/core/execution/WarshipExecution.ts | 10 ++--- src/core/game/Game.ts | 34 +++++++++------ src/core/game/GameUpdates.ts | 4 +- src/core/game/GameView.ts | 21 ++++++++- src/core/game/PlayerImpl.ts | 5 ++- src/core/game/UnitImpl.ts | 38 ++++++++++++----- 11 files changed, 193 insertions(+), 42 deletions(-) create mode 100644 resources/images/NukeIconRed.svg diff --git a/resources/images/NukeIconRed.svg b/resources/images/NukeIconRed.svg new file mode 100644 index 000000000..5d6b7d5f6 --- /dev/null +++ b/resources/images/NukeIconRed.svg @@ -0,0 +1,54 @@ + + diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 3a2ee91b8..d5ba46fa0 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -2,8 +2,11 @@ import { AllPlayers, Cell, Game, + NukeType, + nukeTypes, Player, PlayerType, + UnitType, } from "../../../core/game/Game"; import { PseudoRandom } from "../../../core/PseudoRandom"; import { Theme } from "../../../core/configuration/Config"; @@ -15,6 +18,8 @@ import allianceRequestIcon from "../../../../resources/images/AllianceRequestIco import crownIcon from "../../../../resources/images/CrownIcon.svg"; import targetIcon from "../../../../resources/images/TargetIcon.svg"; import embargoIcon from "../../../../resources/images/EmbargoIcon.svg"; +import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg"; +import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg"; import { ClientID } from "../../../core/Schemas"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { createCanvas, renderTroops } from "../../Utils"; @@ -47,6 +52,8 @@ export class NameLayer implements Layer { private targetIconImage: HTMLImageElement; private crownIconImage: HTMLImageElement; private embargoIconImage: HTMLImageElement; + private nukeWhiteIconImage: HTMLImageElement; + private nukeRedIconImage: HTMLImageElement; private container: HTMLDivElement; private myPlayer: PlayerView | null = null; private firstPlace: PlayerView | null = null; @@ -69,6 +76,10 @@ export class NameLayer implements Layer { this.targetIconImage.src = targetIcon; this.embargoIconImage = new Image(); this.embargoIconImage.src = embargoIcon; + this.nukeWhiteIconImage = new Image(); + this.nukeWhiteIconImage.src = nukeWhiteIcon; + this.nukeRedIconImage = new Image(); + this.nukeRedIconImage.src = nukeRedIcon; } resizeCanvas() { @@ -405,6 +416,47 @@ export class NameLayer implements Layer { existingEmbargo.remove(); } + const nukesSentByOtherPlayer = this.game.units().filter((unit) => { + const isSendingNuke = render.player.id() == unit.owner().id(); + const notMyPlayer = unit.owner().id() != myPlayer.id(); + return ( + nukeTypes.includes(unit.type()) && + isSendingNuke && + notMyPlayer && + unit.isActive() + ); + }); + const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => { + const detonationDst = unit.detonationDst(); + const targetId = this.game.owner(detonationDst).id(); + return targetId == this.myPlayer.id(); + }); + const existingNuke = iconsDiv.querySelector( + '[data-icon="nuke"]', + ) as HTMLImageElement; + + if (existingNuke) { + if (nukesSentByOtherPlayer.length == 0) { + existingNuke.remove(); + } else if ( + isMyPlayerTarget && + existingNuke.src != this.nukeRedIconImage.src + ) { + existingNuke.src = this.nukeRedIconImage.src; + } else if ( + !isMyPlayerTarget && + existingNuke.src != this.nukeWhiteIconImage.src + ) { + existingNuke.src = this.nukeWhiteIconImage.src; + } + } else if (myPlayer && nukesSentByOtherPlayer.length > 0) { + if (!existingNuke) { + const icon = isMyPlayerTarget + ? this.nukeRedIconImage.src + : this.nukeWhiteIconImage.src; + iconsDiv.appendChild(this.createIconElement(icon, iconSize, "nuke")); + } + } // Update all icon sizes const icons = iconsDiv.getElementsByTagName("img"); for (const icon of icons) { diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 3ac64c441..8be306b10 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -248,10 +248,10 @@ export class UnitLayer implements Layer { } let outerColor = this.theme.territoryColor(unit.owner().info()); - if (unit.targetId()) { + if (unit.warshipTargetId()) { const targetOwner = this.game .units() - .find((u) => u.id() == unit.targetId()) + .find((u) => u.id() == unit.warshipTargetId()) ?.owner(); if (targetOwner == this.myPlayer) { outerColor = colord({ r: 200, b: 0, g: 0 }); diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 65dfea8d9..f27c47b9d 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -55,7 +55,9 @@ export class NukeExecution implements Execution { this.active = false; return; } - this.nuke = this.player.buildUnit(this.type, 0, spawn); + this.nuke = this.player.buildUnit(this.type, 0, spawn, { + detonationDst: this.dst, + }); if (this.mg.hasOwner(this.dst)) { const target = this.mg.owner(this.dst) as Player; if (this.type == UnitType.AtomBomb) { diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index c6587ec2a..8bc234e5a 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -47,12 +47,9 @@ export class TradeShipExecution implements Execution { this.active = false; return; } - this.tradeShip = this.origOwner.buildUnit( - UnitType.TradeShip, - 0, - spawn, - this._dstPort, - ); + this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn, { + dstPort: this._dstPort, + }); } if (!this.tradeShip.isActive()) { diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index a388ec65e..901fe6fd5 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -79,10 +79,10 @@ export class WarshipExecution implements Execution { .filter((u) => u != this.warship) .filter((u) => !u.owner().isAlliedWith(this.warship.owner())) .filter((u) => !this.alreadySentShell.has(u)) - .filter( - (u) => - u.type() != UnitType.TradeShip || u.dstPort().owner() != this.owner(), - ); + .filter((u) => { + const portOwner = u.dstPort() ? u.dstPort().owner() : null; + return u.type() != UnitType.TradeShip || portOwner != this.owner(); + }); this.target = ships.sort((a, b) => { @@ -110,7 +110,7 @@ export class WarshipExecution implements Execution { return distSortUnit(this.mg, this.warship)(a, b); })[0] ?? null; - this.warship.setTarget(this.target); + this.warship.setWarshipTarget(this.target); if (this.target == null || this.target.type() != UnitType.TradeShip) { // Patrol unless we are hunting down a tradeship const result = this.pathfinder.nextTile( diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 9c3c214eb..d796eb244 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -84,11 +84,14 @@ export enum UnitType { MIRVWarhead = "MIRV Warhead", Construction = "Construction", } -export type NukeType = - | UnitType.AtomBomb - | UnitType.HydrogenBomb - | UnitType.MIRVWarhead - | UnitType.MIRV; + +export const nukeTypes = [ + UnitType.AtomBomb, + UnitType.HydrogenBomb, + UnitType.MIRVWarhead, + UnitType.MIRV, +] as UnitType[]; +export type NukeType = (typeof nukeTypes)[number]; export enum Relation { Hostile = 0, @@ -201,6 +204,13 @@ export class PlayerInfo { ) {} } +// Some units have info specific to them +export interface UnitSpecificInfos { + dstPort?: Unit; // Only for trade ships + detonationDst?: TileRef; // Only for nukes + warshipTarget?: Unit; +} + export interface Unit { id(): number; @@ -220,9 +230,12 @@ export interface Unit { hasHealth(): boolean; health(): number; modifyHealth(delta: number): void; - // State for warships (currently) - setTarget(target: Unit): void; - target(): Unit; + + setWarshipTarget(target: Unit): void; // warship only + warshipTarget(): Unit; + + dstPort(): Unit; // Only for trade ships + detonationDst(): TileRef; // Only for nukes // Mutations setTroops(troops: number): void; @@ -234,9 +247,6 @@ export interface Unit { // Updates toUpdate(): UnitUpdate; - - // Only for some types, otherwise return null - dstPort(): Unit; } export interface TerraNullius { @@ -294,7 +304,7 @@ export interface Player { type: UnitType, troops: number, tile: TileRef, - dstPort?: Unit, + unitSpecificInfos?: UnitSpecificInfos, ): Unit; captureUnit(unit: Unit): void; diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 20cc9f1bc..7efaab80f 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -69,9 +69,11 @@ export interface UnitUpdate { pos: TileRef; lastPos: TileRef; isActive: boolean; + dstPortId?: number; // Only for trade ships + detonationDst?: TileRef; // Only for nukes + warshipTargetId?: number; health?: number; constructionType?: UnitType; - targetId?: number; } export interface AttackUpdate { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 2c685c3b0..e0ebf40f8 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -2,6 +2,7 @@ import { GameUpdates, MapPos, MessageType, + nukeTypes, Player, PlayerActions, PlayerProfile, @@ -30,6 +31,7 @@ import { WorkerClient } from "../worker/WorkerClient"; import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; import { GameUpdateViewData } from "./GameUpdates"; import { DefenseGrid } from "./DefensePostGrid"; +import { consolex } from "../Consolex"; export class UnitView { public _wasUpdated = true; @@ -91,8 +93,23 @@ export class UnitView { constructionType(): UnitType | undefined { return this.data.constructionType; } - targetId() { - return this.data.targetId; + dstPortId(): number { + if (this.type() != UnitType.TradeShip) { + throw Error("Must be a trade ship"); + } + return this.data.dstPortId; + } + detonationDst(): TileRef { + if (!nukeTypes.includes(this.type())) { + throw Error("Must be a nuke"); + } + return this.data.detonationDst; + } + warshipTargetId(): number { + if (this.type() != UnitType.Warship) { + throw Error("Must be a warship"); + } + return this.data.warshipTargetId; } } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 77378ecf4..de96e2cb6 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -18,6 +18,7 @@ import { EmojiMessage, PlayerProfile, Attack, + UnitSpecificInfos, } from "./Game"; import { AttackUpdate, PlayerUpdate } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates"; @@ -648,7 +649,7 @@ export class PlayerImpl implements Player { type: UnitType, troops: number, spawnTile: TileRef, - dstPort?: Unit, + unitSpecificInfos: UnitSpecificInfos = {}, ): UnitImpl { const cost = this.mg.unitInfo(type).cost(this); const b = new UnitImpl( @@ -658,7 +659,7 @@ export class PlayerImpl implements Player { troops, this.mg.nextUnitID(), this, - dstPort, + unitSpecificInfos, ); this._units.push(b); this.removeGold(cost); diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 7affc8859..72dd2dc59 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,4 +1,4 @@ -import { MessageType } from "./Game"; +import { MessageType, nukeTypes, UnitSpecificInfos } from "./Game"; import { UnitUpdate } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates"; import { simpleHash, toInt, within, withinInt } from "../Util"; @@ -6,6 +6,7 @@ import { Unit, TerraNullius, UnitType, Player, UnitInfo } from "./Game"; import { GameImpl } from "./GameImpl"; import { PlayerImpl } from "./PlayerImpl"; import { TileRef } from "./GameMap"; +import { consolex } from "../Consolex"; export class UnitImpl implements Unit { private _active = true; @@ -16,6 +17,10 @@ export class UnitImpl implements Unit { private _constructionType: UnitType = undefined; + private _dstPort: Unit | null = null; // Only for trade ships + private _detonationDst: TileRef | null = null; // Only for nukes + private _warshipTarget: Unit | null = null; + constructor( private _type: UnitType, private mg: GameImpl, @@ -23,10 +28,13 @@ export class UnitImpl implements Unit { private _troops: number, private _id: number, public _owner: PlayerImpl, - private _dstPort?: Unit, + unitsSpecificInfos: UnitSpecificInfos = {}, ) { this._health = toInt(this.mg.unitInfo(_type).maxHealth ?? 1); this._lastTile = _tile; + this._dstPort = unitsSpecificInfos.dstPort; + this._detonationDst = unitsSpecificInfos.detonationDst; + this._warshipTarget = unitsSpecificInfos.warshipTarget; } id() { @@ -34,6 +42,8 @@ export class UnitImpl implements Unit { } toUpdate(): UnitUpdate { + const warshipTarget = this.warshipTarget(); + const dstPort = this.dstPort(); return { type: GameUpdateType.Unit, unitType: this._type, @@ -45,7 +55,9 @@ export class UnitImpl implements Unit { lastPos: this._lastTile, health: this.hasHealth() ? Number(this._health) : undefined, constructionType: this._constructionType, - targetId: this.target() ? this.target().id() : null, + dstPortId: dstPort ? dstPort.id() : null, + warshipTargetId: warshipTarget ? warshipTarget.id() : null, + detonationDst: this.detonationDst(), }; } @@ -153,15 +165,19 @@ export class UnitImpl implements Unit { return `Unit:${this._type},owner:${this.owner().name()}`; } + setWarshipTarget(target: Unit) { + this._warshipTarget = target; + } + + warshipTarget(): Unit { + return this._warshipTarget; + } + + detonationDst(): TileRef { + return this._detonationDst; + } + dstPort(): Unit { return this._dstPort; } - - setTarget(target: Unit) { - this._target = target; - } - - target() { - return this._target; - } }