Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added proprietary/images/waricon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export class SendPeaceRequestIntentEvent implements GameEvent {
) {}
}

export class SendDeclareWarIntentEvent implements GameEvent {
constructor(
public readonly requestor: PlayerView,
public readonly recipient: PlayerView,
) {}
}

export class SendAllianceExtensionIntentEvent implements GameEvent {
constructor(public readonly recipient: PlayerView) {}
}
Expand Down Expand Up @@ -278,6 +285,9 @@ export class Transport {
this.eventBus.on(SendPeaceRequestIntentEvent, (e) =>
this.onSendPeaceRequestIntent(e),
);
this.eventBus.on(SendDeclareWarIntentEvent, (e) =>
this.onSendDeclareWarIntent(e),
);
this.eventBus.on(SendSpawnIntentEvent, (e) =>
this.onSendSpawnIntentEvent(e),
);
Expand Down Expand Up @@ -555,6 +565,14 @@ export class Transport {
});
}

private onSendDeclareWarIntent(event: SendDeclareWarIntentEvent) {
this.sendIntent({
type: "declareWar",
clientID: this.lobbyConfig.clientID,
recipient: event.recipient.id(),
});
}

private onSendAllianceExtensionIntent(
event: SendAllianceExtensionIntentEvent,
) {
Expand Down
160 changes: 157 additions & 3 deletions src/client/graphics/layers/ControlPanel2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import "../../StatisticsModal"; // ensure statistics modal is registered
import {
SendBomberIntentEvent,
SendEmbargoIntentEvent,
SendSetAutoBombingEvent,
SendSetInvestmentRateEvent,
SendSetResearchInvestmentEvent,
Expand Down Expand Up @@ -92,8 +93,13 @@
private init_: boolean = false;

@state()
private activeTab: "Build" | "Attack" | "Economy" | "Bombers" | "Trade" =
"Build";
private activeTab:
| "Build"
| "Attack"
| "Economy"
| "Bombers"
| "Trade"
| "Diplomacy" = "Build";

@state()
private _lastAirfieldCount: number = 0;
Expand Down Expand Up @@ -951,7 +957,7 @@

private _openUnitUpgradeSettings() {
const modal =
(document.querySelector("unit-upgrade-settings-modal") as any) ||

Check warning on line 960 in src/client/graphics/layers/ControlPanel2.ts

View workflow job for this annotation

GitHub Actions / 🔍 ESLint

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator. (@typescript-eslint/prefer-nullish-coalescing)
this._ensureUnitUpgradeSettingsModal();
if (!modal) {
console.warn(
Expand Down Expand Up @@ -984,7 +990,7 @@
}

private _changeTab(
tab: "Build" | "Attack" | "Economy" | "Bombers" | "Trade",
tab: "Build" | "Attack" | "Economy" | "Bombers" | "Trade" | "Diplomacy",
) {
this.activeTab = tab;
if (this.uiState.pendingBuildUnitType) {
Expand All @@ -994,7 +1000,7 @@

private _openStatistics() {
const modal =
(document.querySelector("statistics-modal") as any) ||

Check warning on line 1003 in src/client/graphics/layers/ControlPanel2.ts

View workflow job for this annotation

GitHub Actions / 🔍 ESLint

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator. (@typescript-eslint/prefer-nullish-coalescing)
this._ensureStatisticsModal();
if (!modal) {
console.warn("StatisticsModal element not found or failed to create");
Expand Down Expand Up @@ -1245,6 +1251,15 @@
>
Trade
</button>
<button
class="py-2 px-4 text-center font-ocr uppercase cp2-tab ${this
.activeTab === "Diplomacy"
? "active"
: ""}"
@click=${() => this._changeTab("Diplomacy")}
>
Diplomacy
</button>
${this._hasAirfields
? html`
<button
Expand Down Expand Up @@ -1924,6 +1939,7 @@
`
: ""}
${this.activeTab === "Trade" ? this._renderTradeTab() : ""}
${this.activeTab === "Diplomacy" ? this.renderDiplomacyTab() : ""}
</div>
</div>
`;
Expand Down Expand Up @@ -2076,8 +2092,133 @@
: ships.length === 0 && pendingRows.length === 0
? html`<div class="text-gray-400">No active trade ships.</div>`
: ""}

<!-- Embargo Management Buttons -->
<div
class="mt-4 pt-3 border-t"
style="border-color: var(--ui-panel-border)"
>
<h4 class="text-gray-200 text-sm mb-2">Embargo Management</h4>
<div class="flex gap-2">
<button
class="embargo-btn flex-1 px-3 py-2 text-sm font-semibold rounded border-2 transition-all"
style="
border-color: var(--ui-panel-border);
background: var(--ui-primary);
color: var(--ui-text-accent);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.4);
"
@click=${this._handleEmbargoAll}
>
Embargo All
</button>
<button
class="embargo-btn flex-1 px-3 py-2 text-sm font-semibold rounded border-2 transition-all"
style="
border-color: var(--ui-panel-border);
background: var(--ui-primary);
color: var(--ui-text-accent);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.4);
"
@click=${this._handleRemoveAllEmbargos}
>
Remove All Embargos
</button>
</div>
</div>
</div>
`;
}

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),
);

const atWar = players.filter((p) => me.isAtWarWith(p));
const allied = players.filter((p) => me.isAlliedWith(p));
const neutral = players.filter(
(p) => !me.isAtWarWith(p) && !me.isAlliedWith(p),
);

const renderPlayerList = (list: PlayerView[], title: string) => html`
<div class="flex flex-col w-1/3 px-1">
<h3 class="text-center font-bold mb-2 text-gray-300">${title}</h3>
<div class="flex flex-col">
${list.map(
(p) => html`
<div
class="py-1 text-sm text-gray-300 truncate"
title="${p.name()}"
>
${p.name()}
</div>
`,
)}
${list.length === 0
? html`<div class="text-center text-gray-500 italic text-xs">
None
</div>`
: ""}
</div>
</div>
`;

return html`
<div class="flex w-full h-full">
${renderPlayerList(atWar, "At War")}
${renderPlayerList(allied, "Allied")}
${renderPlayerList(neutral, "Neutral")}
</div>
`;
}

private _handleEmbargoAll() {
const me = this.game.myPlayer();
if (!me) return;

const players = this.game
.players()
.filter(
(p) =>
p.isAlive() &&
p.id() !== me.id() &&
(p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman),
);

for (const player of players) {
if (!me.hasEmbargoAgainst(player)) {
this.eventBus.emit(new SendEmbargoIntentEvent(player, "start"));
}
}
}

private _handleRemoveAllEmbargos() {
const me = this.game.myPlayer();
if (!me) return;

const players = this.game
.players()
.filter(
(p) =>
p.isAlive() &&
p.id() !== me.id() &&
(p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman),
);

for (const player of players) {
if (me.hasEmbargoAgainst(player)) {
this.eventBus.emit(new SendEmbargoIntentEvent(player, "stop"));
}
}
}

private _computeTradeShipStatus(ship: UnitView): string {
Expand Down Expand Up @@ -2214,6 +2355,19 @@
pointer-events: none;
display: block;
}
.embargo-btn:hover {
background-color: var(--ui-secondary) !important;
border-color: var(--ui-secondary) !important;
transform: scale(1.05);
}
.embargo-btn:active {
background: linear-gradient(
to bottom,
var(--ui-secondary-hover),
var(--ui-secondary)
) !important;
transform: scale(0.95);
}
`;
if (!document.head.querySelector("style[data-upgrade-button]")) {
style.setAttribute("data-upgrade-button", "true");
Expand Down
29 changes: 23 additions & 6 deletions src/client/graphics/layers/RadialMenu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as d3 from "d3";
import doveIcon from "../../../../proprietary/images/dove.png";
import warIcon from "../../../../proprietary/images/waricon.png";
import airAttackIcon from "../../../../resources/images/AirAttackIconWhite.svg";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
Expand Down Expand Up @@ -28,6 +29,7 @@ import {
SendAttackIntentEvent,
SendBoatAttackIntentEvent,
SendBreakAllianceIntentEvent,
SendDeclareWarIntentEvent,
SendParatrooperAttackIntentEvent,
SendPeaceRequestIntentEvent,
SendSpawnIntentEvent,
Expand Down Expand Up @@ -229,28 +231,32 @@ export class RadialMenu implements Layer {
.append("image")
.attr("xlink:href", (d) => d.data.icon)
.attr("width", (d) =>
d.data.name === "peace"
d.data.name === "peace" && !d.data.disabled
? this.iconSize * this.peaceIconScale
: this.iconSize,
)
.attr("height", (d) =>
d.data.name === "peace"
d.data.name === "peace" && !d.data.disabled
? this.iconSize * this.peaceIconScale
: this.iconSize,
)
.attr("x", (d) => {
const w =
d.data.name === "peace"
d.data.name === "peace" && !d.data.disabled
? this.iconSize * this.peaceIconScale
: this.iconSize;
return arc.centroid(d)[0] - w / 2;
// Offset both peace and war icons when enabled
const offset = d.data.name === "peace" && !d.data.disabled ? 2 : 0;
return arc.centroid(d)[0] - w / 2 + offset;
})
.attr("y", (d) => {
const h =
d.data.name === "peace"
d.data.name === "peace" && !d.data.disabled
? this.iconSize * this.peaceIconScale
: this.iconSize;
return arc.centroid(d)[1] - h / 2;
// Offset both peace and war icons when enabled
const offset = d.data.name === "peace" && !d.data.disabled ? 2 : 0;
return arc.centroid(d)[1] - h / 2 + offset;
})
.style("pointer-events", "none")
.attr("data-name", (d) => d.data.name);
Expand Down Expand Up @@ -412,6 +418,17 @@ export class RadialMenu implements Layer {
);
});
}
if (actions?.interaction?.canDeclareWar) {
// Use dark red for war declaration
this.activateMenuElement(Slot.Peace, "#8B0000", warIcon, () => {
this.eventBus.emit(
new SendDeclareWarIntentEvent(
myPlayer,
this.g.owner(tile) as PlayerView,
),
);
});
}
if (
actions.buildableUnits.find((bu) => bu.type === UnitType.TransportShip)
?.canBuild
Expand Down
7 changes: 7 additions & 0 deletions src/core/GameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,13 @@ export class GameRunner {
canBreakAlliance: player.isAlliedWith(other),
// Only show Peace when at war
canRequestPeace: player.isAtWarWith(other),
// Only show Declare War when not at war and not allied, and target is human/fakehuman
canDeclareWar:
!player.isAtWarWith(other) &&
!player.isAlliedWith(other) &&
other !== player &&
(other.type() === PlayerType.Human ||
other.type() === PlayerType.FakeHuman),
canDonate: player.canDonate(other),
canEmbargo: !player.hasEmbargoAgainst(other),
};
Expand Down
8 changes: 8 additions & 0 deletions src/core/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type Intent =
| AllianceExtensionIntent
| BreakAllianceIntent
| PeaceRequestIntent
| DeclareWarIntent
| TargetPlayerIntent
| EmojiIntent
| DonateGoldIntent
Expand Down Expand Up @@ -71,6 +72,7 @@ export type AllianceRequestReplyIntent = z.infer<
>;
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>;
export type PeaceRequestIntent = z.infer<typeof PeaceRequestIntentSchema>;
export type DeclareWarIntent = z.infer<typeof DeclareWarIntentSchema>;
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>;
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>;
export type DonateGoldIntent = z.infer<typeof DonateGoldIntentSchema>;
Expand Down Expand Up @@ -346,6 +348,11 @@ export const PeaceRequestIntentSchema = BaseIntentSchema.extend({
recipient: ID,
});

export const DeclareWarIntentSchema = BaseIntentSchema.extend({
type: z.literal("declareWar"),
recipient: ID,
});

export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
type: z.literal("targetPlayer"),
target: ID,
Expand Down Expand Up @@ -501,6 +508,7 @@ const IntentSchema = z.discriminatedUnion("type", [
AllianceExtensionIntentSchema,
BreakAllianceIntentSchema,
PeaceRequestIntentSchema,
DeclareWarIntentSchema,
TargetPlayerIntentSchema,
EmojiIntentSchema,
DonateGoldIntentSchema,
Expand Down
Loading
Loading