diff --git a/src/client/graphics/layers/ControlPanel2.ts b/src/client/graphics/layers/ControlPanel2.ts
index af737c0ae..1da67e2cc 100644
--- a/src/client/graphics/layers/ControlPanel2.ts
+++ b/src/client/graphics/layers/ControlPanel2.ts
@@ -33,12 +33,8 @@ import { ToggleUpgradeModeEvent } from "../../events/ToggleUpgradeModeEvent";
import { AttackRatioEvent } from "../../InputHandler";
import "../../StatisticsModal"; // ensure statistics modal is registered
import {
- SendAllianceRequestIntentEvent,
SendBomberIntentEvent,
- SendBreakAllianceIntentEvent,
- SendDeclareWarIntentEvent,
SendEmbargoIntentEvent,
- SendPeaceRequestIntentEvent,
SendSetInvestmentRateEvent,
SendSetResearchInvestmentEvent,
SendSetRoadInvestmentEvent,
@@ -104,8 +100,7 @@ export class ControlPanel2 extends LitElement implements Layer {
private init_: boolean = false;
@state()
- private activeTab: "Build" | "Attack" | "Economy" | "Trade" | "Diplomacy" =
- "Build";
+ private activeTab: "Build" | "Attack" | "Economy" | "Trade" = "Build";
@state()
private _hasAirfields: boolean = false;
@@ -862,9 +857,7 @@ export class ControlPanel2 extends LitElement implements Layer {
return el;
}
- private _changeTab(
- tab: "Build" | "Attack" | "Economy" | "Trade" | "Diplomacy",
- ) {
+ private _changeTab(tab: "Build" | "Attack" | "Economy" | "Trade") {
this.activeTab = tab;
if (this.uiState.pendingBuildUnitType) {
this.uiState.pendingBuildUnitType = null;
@@ -1128,16 +1121,6 @@ export class ControlPanel2 extends LitElement implements Layer {
>
Trade
-
`;
@@ -1899,220 +1881,8 @@ export class ControlPanel2 extends LitElement implements Layer {
}
private renderDiplomacyTab() {
- const me = this.game.myPlayer();
- if (!me) return html``;
-
- const players = this.game
- .players()
- .filter(
- (p) =>
- p.isAlive() &&
- p.id() !== me.id() &&
- (p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman),
- );
-
- // Icons and colors reused from radial menu
- const warIcon = "/images/waricon.png";
- const peaceIcon = "/images/dove.png";
- const allianceIcon = "/images/AllianceIconWhite.svg";
- const traitorIcon = "/images/TraitorIconWhite.svg";
-
- // Colors matching radial menu
- const warColor = "#8B0000"; // dark red for declare war
- const peaceColor = "#e5e7eb"; // light gray for peace
- const allianceColor = "#53ac75"; // green for alliance
- const betrayColor = "#c74848"; // red for betray
-
- const iconBtn = (
- src: string,
- bgColor: string,
- titleKey: string,
- onClick: () => void,
- ) => html`
-
- `;
-
- const renderName = (p: PlayerView) => html`
-
- ${p.name()}
-
- `;
-
- const renderBtn = (btn: ReturnType) => html`
- ${btn}
- `;
-
- const renderEmpty = () => html`
`;
-
- // Build rows for each player
- const rows = players.map((p) => {
- const atWar = me.isAtWarWith(p);
- const allied = me.isAlliedWith(p);
- const neutral = !atWar && !allied;
-
- // At War column cell
- let atWarCell;
- if (atWar) {
- atWarCell = renderName(p);
- } else if (neutral) {
- atWarCell = renderBtn(
- iconBtn(
- warIcon,
- warColor,
- "control_panel2.diplomacy_declare_war_tooltip",
- () => this.eventBus.emit(new SendDeclareWarIntentEvent(me, p)),
- ),
- );
- } else if (allied) {
- atWarCell = renderBtn(
- iconBtn(
- traitorIcon,
- betrayColor,
- "control_panel2.diplomacy_betray_tooltip",
- () => this.eventBus.emit(new SendBreakAllianceIntentEvent(me, p)),
- ),
- );
- } else {
- atWarCell = renderEmpty();
- }
-
- // Allied column cell
- let alliedCell;
- if (allied) {
- alliedCell = renderName(p);
- } else {
- // Can request alliance from both neutral and at-war players
- alliedCell = renderBtn(
- iconBtn(
- allianceIcon,
- allianceColor,
- "control_panel2.diplomacy_request_alliance_tooltip",
- () => this.eventBus.emit(new SendAllianceRequestIntentEvent(me, p)),
- ),
- );
- }
-
- // Neutral column cell
- let neutralCell;
- if (neutral) {
- neutralCell = renderName(p);
- } else if (atWar) {
- neutralCell = renderBtn(
- iconBtn(
- peaceIcon,
- peaceColor,
- "control_panel2.diplomacy_request_peace_tooltip",
- () => this.eventBus.emit(new SendPeaceRequestIntentEvent(me, p)),
- ),
- );
- } else {
- neutralCell = renderEmpty();
- }
-
- return html`
-
-
- ${atWarCell}
-
-
- ${alliedCell}
-
-
- ${neutralCell}
-
-
- `;
- });
-
- // Bulk action handlers
- const declareWarOnAll = () => {
- players.forEach((p) => {
- if (me.isAlliedWith(p)) {
- // Break alliance first (betray), then declare war
- this.eventBus.emit(new SendBreakAllianceIntentEvent(me, p));
- }
- if (!me.isAtWarWith(p)) {
- this.eventBus.emit(new SendDeclareWarIntentEvent(me, p));
- }
- });
- };
-
- const requestAllianceWithAll = () => {
- players.forEach((p) => {
- if (!me.isAlliedWith(p)) {
- this.eventBus.emit(new SendAllianceRequestIntentEvent(me, p));
- }
- });
- };
-
- const requestPeaceWithAll = () => {
- players.forEach((p) => {
- if (me.isAtWarWith(p)) {
- this.eventBus.emit(new SendPeaceRequestIntentEvent(me, p));
- }
- });
- };
-
- // Small icon button for header bulk actions
- const headerBtn = (
- icon: string,
- bgColor: string,
- titleKey: string,
- onClick: () => void,
- ) => html`
-
- `;
-
- return html`
-
-
-
-
- At War
- ${headerBtn(
- warIcon,
- warColor,
- "control_panel2.diplomacy_war_all_tooltip",
- declareWarOnAll,
- )}
-
-
- Allied
- ${headerBtn(
- allianceIcon,
- allianceColor,
- "control_panel2.diplomacy_ally_all_tooltip",
- requestAllianceWithAll,
- )}
-
-
- Neutral
- ${headerBtn(
- peaceIcon,
- peaceColor,
- "control_panel2.diplomacy_peace_all_tooltip",
- requestPeaceWithAll,
- )}
-
-
-
- ${rows}
-
- `;
+ // Diplomacy tab removed - relations now shown via NameLayer icons
+ return html``;
}
private _handleEmbargoAll() {
diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts
index 8768bfcba..92a5c3f28 100644
--- a/src/client/graphics/layers/NameLayer.ts
+++ b/src/client/graphics/layers/NameLayer.ts
@@ -1,3 +1,4 @@
+import doveIcon from "../../../../proprietary/images/dove.png";
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
import allianceRequestBlackIcon from "../../../../resources/images/AllianceRequestBlackIcon.svg";
import allianceRequestWhiteIcon from "../../../../resources/images/AllianceRequestWhiteIcon.svg";
@@ -8,12 +9,18 @@ import embargoWhiteIcon from "../../../../resources/images/EmbargoWhiteIcon.svg"
import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg";
import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
+import swordIconBlack from "../../../../resources/images/SwordIcon.svg";
import targetIcon from "../../../../resources/images/TargetIcon.svg";
import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
-import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
+import {
+ AllPlayers,
+ Cell,
+ nukeTypes,
+ PlayerType,
+} from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent } from "../../InputHandler";
@@ -55,6 +62,8 @@ export class NameLayer implements Layer {
private nukeWhiteIconImage: HTMLImageElement;
private nukeRedIconImage: HTMLImageElement;
private shieldIconImage: HTMLImageElement;
+ private warIconImage: HTMLImageElement;
+ private doveIconImage: HTMLImageElement;
private container: HTMLDivElement;
private firstPlace: PlayerView | null = null;
private theme: Theme = this.game.config().theme();
@@ -90,6 +99,10 @@ export class NameLayer implements Layer {
this.nukeRedIconImage.src = nukeRedIcon;
this.shieldIconImage = new Image();
this.shieldIconImage.src = shieldIcon;
+ this.warIconImage = new Image();
+ this.warIconImage.src = swordIconBlack;
+ this.doveIconImage = new Image();
+ this.doveIconImage.src = doveIcon;
}
resizeCanvas() {
@@ -424,7 +437,16 @@ export class NameLayer implements Layer {
// Alliance icon
const existingAlliance = iconsDiv.querySelector('[data-icon="alliance"]');
- if (myPlayer !== null && myPlayer.isAlliedWith(render.player)) {
+ const isSelf = myPlayer !== null && render.player === myPlayer;
+ const isHumanOrFakeHuman =
+ render.player.type() === PlayerType.Human ||
+ render.player.type() === PlayerType.FakeHuman;
+ if (
+ !isSelf &&
+ isHumanOrFakeHuman &&
+ myPlayer !== null &&
+ myPlayer.isAlliedWith(render.player)
+ ) {
if (!existingAlliance) {
iconsDiv.appendChild(
this.createIconElement(
@@ -438,6 +460,41 @@ export class NameLayer implements Layer {
existingAlliance.remove();
}
+ // War icon
+ const existingWar = iconsDiv.querySelector('[data-icon="war"]');
+ if (
+ !isSelf &&
+ isHumanOrFakeHuman &&
+ myPlayer !== null &&
+ myPlayer.isAtWarWith(render.player)
+ ) {
+ if (!existingWar) {
+ iconsDiv.appendChild(
+ this.createIconElement(this.warIconImage.src, iconSize, "war"),
+ );
+ }
+ } else if (existingWar) {
+ existingWar.remove();
+ }
+
+ // Neutral icon
+ const existingNeutral = iconsDiv.querySelector('[data-icon="neutral"]');
+ if (
+ !isSelf &&
+ isHumanOrFakeHuman &&
+ myPlayer !== null &&
+ !myPlayer.isAlliedWith(render.player) &&
+ !myPlayer.isAtWarWith(render.player)
+ ) {
+ if (!existingNeutral) {
+ iconsDiv.appendChild(
+ this.createIconElement(this.doveIconImage.src, iconSize, "neutral"),
+ );
+ }
+ } else if (existingNeutral) {
+ existingNeutral.remove();
+ }
+
// Alliance request icon
let existingRequestAlliance = iconsDiv.querySelector(
'[data-icon="alliance-request"]',
@@ -584,12 +641,6 @@ export class NameLayer implements Layer {
iconsDiv.appendChild(this.createIconElement(icon, iconSize, "nuke"));
}
}
- // Update all icon sizes
- const icons = iconsDiv.getElementsByTagName("img");
- for (const icon of icons) {
- icon.style.width = `${iconSize}px`;
- icon.style.height = `${iconSize}px`;
- }
// Position element with scale
if (render.location && render.location !== oldLocation) {
@@ -606,10 +657,29 @@ export class NameLayer implements Layer {
): HTMLImageElement {
const icon = document.createElement("img");
icon.src = src;
- icon.style.width = `${size}px`;
- icon.style.height = `${size}px`;
+
+ // Make war icon 20% smaller
+ const actualSize = id === "war" ? size * 0.8 : size;
+ icon.style.width = `${actualSize}px`;
+ icon.style.height = `${actualSize}px`;
icon.setAttribute("data-icon", id);
icon.setAttribute("dark-mode", this.userSettings.darkMode().toString());
+
+ if (id === "war") {
+ // Use CSS mask with exact warColor #8B0000 from radial menu
+ icon.style.backgroundColor = "#8B0000";
+ icon.style.webkitMaskImage = `url(${src})`;
+ icon.style.maskImage = `url(${src})`;
+ icon.style.webkitMaskSize = "contain";
+ icon.style.maskSize = "contain";
+ icon.style.webkitMaskRepeat = "no-repeat";
+ icon.style.maskRepeat = "no-repeat";
+ icon.style.webkitMaskPosition = "center";
+ icon.style.maskPosition = "center";
+ // Clear the src to prevent img from loading
+ icon.removeAttribute("src");
+ }
+
if (center) {
icon.style.position = "absolute";
icon.style.top = "50%";
diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts
index 80ab23d01..61d874ffc 100644
--- a/src/client/graphics/layers/PlayerInfoOverlay.ts
+++ b/src/client/graphics/layers/PlayerInfoOverlay.ts
@@ -178,11 +178,24 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
let displayRelation = false;
let relationClass = "";
let relationName = "";
+ // Icons are not shown in overlay; text only
if (myPlayer.isFriendly(player)) {
relationClass = this.getRelationClass(Relation.Friendly);
relationName = translateText("relation.allied");
displayRelation = true;
+ } else if (myPlayer.isAtWarWith(player)) {
+ relationClass = "text-red-500";
+ relationName = translateText("relation.hostile");
+ displayRelation = true;
+ } else if (
+ !myPlayer.isAlliedWith(player) &&
+ !myPlayer.isAtWarWith(player)
+ ) {
+ // Neutral
+ relationClass = "text-yellow-300";
+ relationName = translateText("relation.neutral");
+ displayRelation = true;
} else if (player.type() === PlayerType.FakeHuman) {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;