Skip to content

Commit 3ce5785

Browse files
nuke icon (openfrontio#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)
1 parent 5cf75c7 commit 3ce5785

11 files changed

Lines changed: 191 additions & 40 deletions

File tree

resources/images/NukeIconRed.svg

Lines changed: 54 additions & 0 deletions
Loading

src/client/graphics/layers/NameLayer.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import {
22
AllPlayers,
33
Cell,
44
Game,
5+
NukeType,
6+
nukeTypes,
57
Player,
68
PlayerType,
9+
UnitType,
710
} from "../../../core/game/Game";
811
import { PseudoRandom } from "../../../core/PseudoRandom";
912
import { Theme } from "../../../core/configuration/Config";
@@ -15,6 +18,8 @@ import allianceRequestIcon from "../../../../resources/images/AllianceRequestIco
1518
import crownIcon from "../../../../resources/images/CrownIcon.svg";
1619
import targetIcon from "../../../../resources/images/TargetIcon.svg";
1720
import embargoIcon from "../../../../resources/images/EmbargoIcon.svg";
21+
import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg";
22+
import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg";
1823
import { ClientID } from "../../../core/Schemas";
1924
import { GameView, PlayerView } from "../../../core/game/GameView";
2025
import { createCanvas, renderTroops } from "../../Utils";
@@ -47,6 +52,8 @@ export class NameLayer implements Layer {
4752
private targetIconImage: HTMLImageElement;
4853
private crownIconImage: HTMLImageElement;
4954
private embargoIconImage: HTMLImageElement;
55+
private nukeWhiteIconImage: HTMLImageElement;
56+
private nukeRedIconImage: HTMLImageElement;
5057
private container: HTMLDivElement;
5158
private myPlayer: PlayerView | null = null;
5259
private firstPlace: PlayerView | null = null;
@@ -69,6 +76,10 @@ export class NameLayer implements Layer {
6976
this.targetIconImage.src = targetIcon;
7077
this.embargoIconImage = new Image();
7178
this.embargoIconImage.src = embargoIcon;
79+
this.nukeWhiteIconImage = new Image();
80+
this.nukeWhiteIconImage.src = nukeWhiteIcon;
81+
this.nukeRedIconImage = new Image();
82+
this.nukeRedIconImage.src = nukeRedIcon;
7283
}
7384

7485
resizeCanvas() {
@@ -405,6 +416,47 @@ export class NameLayer implements Layer {
405416
existingEmbargo.remove();
406417
}
407418

419+
const nukesSentByOtherPlayer = this.game.units().filter((unit) => {
420+
const isSendingNuke = render.player.id() == unit.owner().id();
421+
const notMyPlayer = unit.owner().id() != myPlayer.id();
422+
return (
423+
nukeTypes.includes(unit.type()) &&
424+
isSendingNuke &&
425+
notMyPlayer &&
426+
unit.isActive()
427+
);
428+
});
429+
const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
430+
const detonationDst = unit.detonationDst();
431+
const targetId = this.game.owner(detonationDst).id();
432+
return targetId == this.myPlayer.id();
433+
});
434+
const existingNuke = iconsDiv.querySelector(
435+
'[data-icon="nuke"]',
436+
) as HTMLImageElement;
437+
438+
if (existingNuke) {
439+
if (nukesSentByOtherPlayer.length == 0) {
440+
existingNuke.remove();
441+
} else if (
442+
isMyPlayerTarget &&
443+
existingNuke.src != this.nukeRedIconImage.src
444+
) {
445+
existingNuke.src = this.nukeRedIconImage.src;
446+
} else if (
447+
!isMyPlayerTarget &&
448+
existingNuke.src != this.nukeWhiteIconImage.src
449+
) {
450+
existingNuke.src = this.nukeWhiteIconImage.src;
451+
}
452+
} else if (myPlayer && nukesSentByOtherPlayer.length > 0) {
453+
if (!existingNuke) {
454+
const icon = isMyPlayerTarget
455+
? this.nukeRedIconImage.src
456+
: this.nukeWhiteIconImage.src;
457+
iconsDiv.appendChild(this.createIconElement(icon, iconSize, "nuke"));
458+
}
459+
}
408460
// Update all icon sizes
409461
const icons = iconsDiv.getElementsByTagName("img");
410462
for (const icon of icons) {

src/client/graphics/layers/UnitLayer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,10 @@ export class UnitLayer implements Layer {
248248
}
249249

250250
let outerColor = this.theme.territoryColor(unit.owner().info());
251-
if (unit.targetId()) {
251+
if (unit.warshipTargetId()) {
252252
const targetOwner = this.game
253253
.units()
254-
.find((u) => u.id() == unit.targetId())
254+
.find((u) => u.id() == unit.warshipTargetId())
255255
?.owner();
256256
if (targetOwner == this.myPlayer) {
257257
outerColor = colord({ r: 200, b: 0, g: 0 });

src/core/execution/NukeExecution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ export class NukeExecution implements Execution {
5555
this.active = false;
5656
return;
5757
}
58-
this.nuke = this.player.buildUnit(this.type, 0, spawn);
58+
this.nuke = this.player.buildUnit(this.type, 0, spawn, {
59+
detonationDst: this.dst,
60+
});
5961
if (this.mg.hasOwner(this.dst)) {
6062
const target = this.mg.owner(this.dst) as Player;
6163
if (this.type == UnitType.AtomBomb) {

src/core/execution/TradeShipExecution.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,9 @@ export class TradeShipExecution implements Execution {
4747
this.active = false;
4848
return;
4949
}
50-
this.tradeShip = this.origOwner.buildUnit(
51-
UnitType.TradeShip,
52-
0,
53-
spawn,
54-
this._dstPort,
55-
);
50+
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn, {
51+
dstPort: this._dstPort,
52+
});
5653
}
5754

5855
if (!this.tradeShip.isActive()) {

src/core/execution/WarshipExecution.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ export class WarshipExecution implements Execution {
7979
.filter((u) => u != this.warship)
8080
.filter((u) => !u.owner().isAlliedWith(this.warship.owner()))
8181
.filter((u) => !this.alreadySentShell.has(u))
82-
.filter(
83-
(u) =>
84-
u.type() != UnitType.TradeShip || u.dstPort().owner() != this.owner(),
85-
);
82+
.filter((u) => {
83+
const portOwner = u.dstPort() ? u.dstPort().owner() : null;
84+
return u.type() != UnitType.TradeShip || portOwner != this.owner();
85+
});
8686

8787
this.target =
8888
ships.sort((a, b) => {
@@ -110,7 +110,7 @@ export class WarshipExecution implements Execution {
110110
return distSortUnit(this.mg, this.warship)(a, b);
111111
})[0] ?? null;
112112

113-
this.warship.setTarget(this.target);
113+
this.warship.setWarshipTarget(this.target);
114114
if (this.target == null || this.target.type() != UnitType.TradeShip) {
115115
// Patrol unless we are hunting down a tradeship
116116
const result = this.pathfinder.nextTile(

src/core/game/Game.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,14 @@ export enum UnitType {
8484
MIRVWarhead = "MIRV Warhead",
8585
Construction = "Construction",
8686
}
87-
export type NukeType =
88-
| UnitType.AtomBomb
89-
| UnitType.HydrogenBomb
90-
| UnitType.MIRVWarhead
91-
| UnitType.MIRV;
87+
88+
export const nukeTypes = [
89+
UnitType.AtomBomb,
90+
UnitType.HydrogenBomb,
91+
UnitType.MIRVWarhead,
92+
UnitType.MIRV,
93+
] as UnitType[];
94+
export type NukeType = (typeof nukeTypes)[number];
9295

9396
export enum Relation {
9497
Hostile = 0,
@@ -201,6 +204,13 @@ export class PlayerInfo {
201204
) {}
202205
}
203206

207+
// Some units have info specific to them
208+
export interface UnitSpecificInfos {
209+
dstPort?: Unit; // Only for trade ships
210+
detonationDst?: TileRef; // Only for nukes
211+
warshipTarget?: Unit;
212+
}
213+
204214
export interface Unit {
205215
id(): number;
206216

@@ -220,9 +230,12 @@ export interface Unit {
220230
hasHealth(): boolean;
221231
health(): number;
222232
modifyHealth(delta: number): void;
223-
// State for warships (currently)
224-
setTarget(target: Unit): void;
225-
target(): Unit;
233+
234+
setWarshipTarget(target: Unit): void; // warship only
235+
warshipTarget(): Unit;
236+
237+
dstPort(): Unit; // Only for trade ships
238+
detonationDst(): TileRef; // Only for nukes
226239

227240
// Mutations
228241
setTroops(troops: number): void;
@@ -234,9 +247,6 @@ export interface Unit {
234247

235248
// Updates
236249
toUpdate(): UnitUpdate;
237-
238-
// Only for some types, otherwise return null
239-
dstPort(): Unit;
240250
}
241251

242252
export interface TerraNullius {
@@ -294,7 +304,7 @@ export interface Player {
294304
type: UnitType,
295305
troops: number,
296306
tile: TileRef,
297-
dstPort?: Unit,
307+
unitSpecificInfos?: UnitSpecificInfos,
298308
): Unit;
299309
captureUnit(unit: Unit): void;
300310

src/core/game/GameUpdates.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ export interface UnitUpdate {
6969
pos: TileRef;
7070
lastPos: TileRef;
7171
isActive: boolean;
72+
dstPortId?: number; // Only for trade ships
73+
detonationDst?: TileRef; // Only for nukes
74+
warshipTargetId?: number;
7275
health?: number;
7376
constructionType?: UnitType;
74-
targetId?: number;
7577
}
7678

7779
export interface AttackUpdate {

src/core/game/GameView.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
GameUpdates,
33
MapPos,
44
MessageType,
5+
nukeTypes,
56
Player,
67
PlayerActions,
78
PlayerProfile,
@@ -30,6 +31,7 @@ import { WorkerClient } from "../worker/WorkerClient";
3031
import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap";
3132
import { GameUpdateViewData } from "./GameUpdates";
3233
import { DefenseGrid } from "./DefensePostGrid";
34+
import { consolex } from "../Consolex";
3335

3436
export class UnitView {
3537
public _wasUpdated = true;
@@ -91,8 +93,23 @@ export class UnitView {
9193
constructionType(): UnitType | undefined {
9294
return this.data.constructionType;
9395
}
94-
targetId() {
95-
return this.data.targetId;
96+
dstPortId(): number {
97+
if (this.type() != UnitType.TradeShip) {
98+
throw Error("Must be a trade ship");
99+
}
100+
return this.data.dstPortId;
101+
}
102+
detonationDst(): TileRef {
103+
if (!nukeTypes.includes(this.type())) {
104+
throw Error("Must be a nuke");
105+
}
106+
return this.data.detonationDst;
107+
}
108+
warshipTargetId(): number {
109+
if (this.type() != UnitType.Warship) {
110+
throw Error("Must be a warship");
111+
}
112+
return this.data.warshipTargetId;
96113
}
97114
}
98115

src/core/game/PlayerImpl.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
EmojiMessage,
1919
PlayerProfile,
2020
Attack,
21+
UnitSpecificInfos,
2122
} from "./Game";
2223
import { AttackUpdate, PlayerUpdate } from "./GameUpdates";
2324
import { GameUpdateType } from "./GameUpdates";
@@ -648,7 +649,7 @@ export class PlayerImpl implements Player {
648649
type: UnitType,
649650
troops: number,
650651
spawnTile: TileRef,
651-
dstPort?: Unit,
652+
unitSpecificInfos: UnitSpecificInfos = {},
652653
): UnitImpl {
653654
const cost = this.mg.unitInfo(type).cost(this);
654655
const b = new UnitImpl(
@@ -658,7 +659,7 @@ export class PlayerImpl implements Player {
658659
troops,
659660
this.mg.nextUnitID(),
660661
this,
661-
dstPort,
662+
unitSpecificInfos,
662663
);
663664
this._units.push(b);
664665
this.removeGold(cost);

0 commit comments

Comments
 (0)