nuke icon (#207)

- **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)
This commit is contained in:
Ilan Schemoul
2025-03-11 23:44:45 +01:00
committed by Evan
parent 48e70ff0e8
commit 5f0df80f57
11 changed files with 192 additions and 42 deletions
+52
View File
@@ -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) {
+2 -2
View File
@@ -245,10 +245,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 });
+3 -1
View File
@@ -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) {
+3 -6
View File
@@ -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()) {
+5 -5
View File
@@ -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(
+22 -12
View File
@@ -80,11 +80,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,
@@ -197,6 +200,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;
@@ -216,9 +226,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;
@@ -230,9 +243,6 @@ export interface Unit {
// Updates
toUpdate(): UnitUpdate;
// Only for some types, otherwise return null
dstPort(): Unit;
}
export interface TerraNullius {
@@ -290,7 +300,7 @@ export interface Player {
type: UnitType,
troops: number,
tile: TileRef,
dstPort?: Unit,
unitSpecificInfos?: UnitSpecificInfos,
): Unit;
captureUnit(unit: Unit): void;
+3 -1
View File
@@ -70,9 +70,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 {
+18 -2
View File
@@ -2,6 +2,7 @@ import {
GameUpdates,
MapPos,
MessageType,
nukeTypes,
Player,
PlayerActions,
PlayerProfile,
@@ -92,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;
}
}
+3 -2
View File
@@ -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);
+27 -11
View File
@@ -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;
}
}