Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
67ef586
feat(game): add capital recalculation execution and update player mod…
1brucben Oct 29, 2025
2d4f560
feat(economy): implement GDP calculation based on population and conf…
1brucben Oct 29, 2025
3ea8703
Implement trade system enhancements and warship interactions
1brucben Oct 29, 2025
7aeb160
feat(trade): update gravity model for trade demand and enhance loggin…
1brucben Oct 29, 2025
6da77d9
feat(trade): enhance logging for active trade ships and queued replac…
1brucben Oct 30, 2025
0255da6
feat(submarine): refine engagement logic for enemy trade ships based …
1brucben Nov 17, 2025
37131c5
feat(trade): implement trade tab in ControlPanel2 and enhance trade s…
1brucben Nov 17, 2025
56546ed
feat(trade): adjust trade gravity model and enhance demand handling i…
1brucben Nov 17, 2025
28f8d74
refactor(tests): remove TradeManager test suite to streamline test co…
1brucben Nov 17, 2025
e3c9daa
feat(tests): add TradeManagerExecution test suite to validate trade r…
1brucben Nov 17, 2025
f7c54c4
feat(trade): enhance trade ship management with pending construction …
1brucben Nov 17, 2025
85559aa
feat(trade): support multiple concurrent trade ship constructions acr…
1brucben Nov 17, 2025
38f3a98
feat(control-panel): improve trade ship display logic in ControlPanel2
1brucben Nov 17, 2025
0464ca9
feat(trade): implement weighted ship selection based on proximity to …
1brucben Nov 17, 2025
6189d0e
feat(trade): implement weighted route assignment for carry-over deman…
1brucben Nov 17, 2025
6a9872f
feat(trade): enhance trade demand indicators and UI integration acros…
1brucben Nov 17, 2025
d7bd955
feat(trade): enhance trade ship lifecycle event handling and UI messa…
1brucben Nov 17, 2025
9d7b1e8
feat(transport): add connection notification handling in Transport class
1brucben Nov 17, 2025
462f662
feat(trade): implement deterministic randomization for route assignme…
1brucben Nov 17, 2025
e62899b
feat(lobby): assign first joiner as lobby creator in private lobbies …
1brucben Nov 17, 2025
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
2 changes: 1 addition & 1 deletion src/client/BuildSettingsModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class BuildSettingsModal extends LitElement {
return {
id,
name: id,
icon: unitIconMap[id] || "",
icon: unitIconMap[id] ?? "",
};
});
const persisted = this._loadPersisted();
Expand Down
6 changes: 6 additions & 0 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ export class Transport {
console.error("socket is null");
return;
}
// Notify the client code that we are connected (mirrors local behavior)
try {
this.onconnect?.();
} catch (err) {
console.error("Error in onconnect handler:", err);
}
while (this.buffer.length > 0) {
console.log("sending dropped message");
const msg = this.buffer.pop();
Expand Down
4 changes: 4 additions & 0 deletions src/client/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export function getMessageTypeClasses(type: MessageType): string {
switch (type) {
case MessageType.SAM_HIT:
case MessageType.CAPTURED_ENEMY_UNIT:
case MessageType.TRADE_SHIP_CAPTURED_ENEMY:
case MessageType.RECEIVED_GOLD_FROM_TRADE:
case MessageType.CONQUERED_PLAYER:
case MessageType.INSURANCE_REFUND:
Expand All @@ -174,6 +175,8 @@ export function getMessageTypeClasses(type: MessageType): string {
case MessageType.ALLIANCE_BROKEN:
case MessageType.UNIT_CAPTURED_BY_ENEMY:
case MessageType.UNIT_DESTROYED:
case MessageType.TRADE_SHIP_CAPTURED:
case MessageType.TRADE_SHIP_SUNK:
return severityColors["fail"];
case MessageType.ATTACK_CANCELLED:
case MessageType.ATTACK_REQUEST:
Expand All @@ -192,6 +195,7 @@ export function getMessageTypeClasses(type: MessageType): string {
case MessageType.PARATROOPER_INBOUND:
case MessageType.WARN:
case MessageType.PEACE_TIMER_BLOCKED:
case MessageType.TRADE_SHIP_TURNED_AROUND:
return severityColors["warn"];
case MessageType.WAR_DECLARED:
return severityColors["warn"]; // war start: highlight prominently
Expand Down
8 changes: 8 additions & 0 deletions src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { NameLayer } from "./layers/NameLayer";
import { OptionsMenu } from "./layers/OptionsMenu";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { PointerCoordsLayer } from "./layers/PointerCoordsLayer";
import { RadialMenu } from "./layers/RadialMenu";
import { RangeOverlayLayer } from "./layers/RangeOverlayLayer";
import { ReplayPanel } from "./layers/ReplayPanel";
Expand All @@ -38,6 +39,9 @@ import { UILayer } from "./layers/UILayer";
import { UnitLayer } from "./layers/UnitLayer";
import { WinModal } from "./layers/WinModal";

// Debug flags (keep off for normal gameplay)
const DEBUG_SHOW_POINTER_COORDS = false;

export function createRenderer(
canvas: HTMLCanvasElement,
game: GameView,
Expand Down Expand Up @@ -246,6 +250,10 @@ export function createRenderer(
new NameLayer(game, transformHandler, eventBus),
// UI layer comes after world-space drawing to minimize save/restore
new UILayer(game, eventBus, transformHandler),
// Pointer coordinates (screen-space, debug only)
...(DEBUG_SHOW_POINTER_COORDS
? [new PointerCoordsLayer(game, eventBus, transformHandler)]
: []),
eventsDisplay,
chatDisplay,
new RadialMenu(
Expand Down
215 changes: 209 additions & 6 deletions src/client/graphics/layers/ControlPanel2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
UnitType,
UpgradeType,
} from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { getTechMeta, RESEARCH_TECH_IDS } from "../../../core/tech/TechEffects";
// Ensure modal custom elements register at runtime
import "../../BuildSettingsModal";
Expand Down Expand Up @@ -87,7 +87,8 @@
private init_: boolean = false;

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

@state()
private _lastAirfieldCount: number = 0;
Expand Down Expand Up @@ -237,6 +238,11 @@
super.disconnectedCallback();
}

// Restore disabled shadow DOM so legacy global CSS and querySelector usage continue working
protected createRenderRoot(): HTMLElement | DocumentFragment {
return this; // Render into light DOM
}

init() {
this.attackRatio = Number(
localStorage.getItem("settings.attackRatio") ?? "0.3",
Expand Down Expand Up @@ -883,7 +889,7 @@

private _openBuildSettings() {
const modal =
(document.querySelector("build-settings-modal") as any) ||
(document.querySelector("build-settings-modal") as any) ??
this._ensureBuildSettingsModal();
if (!modal) {
console.warn("BuildSettingsModal element not found or failed to create");
Expand Down Expand Up @@ -918,7 +924,7 @@

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

Check warning on line 927 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 @@ -950,7 +956,9 @@
return el;
}

private _changeTab(tab: "Build" | "Attack" | "Economy" | "Bombers") {
private _changeTab(
tab: "Build" | "Attack" | "Economy" | "Bombers" | "Trade",
) {
this.activeTab = tab;
if (this.uiState.pendingBuildUnitType) {
this.uiState.pendingBuildUnitType = null;
Expand Down Expand Up @@ -1170,6 +1178,15 @@
>
Economy
</button>
<button
class="py-2 px-4 text-center font-ocr uppercase cp2-tab ${this
.activeTab === "Trade"
? "active"
: ""}"
@click=${() => this._changeTab("Trade")}
>
Trade
</button>
${this._hasAirfields
? html`
<button
Expand Down Expand Up @@ -1834,13 +1851,199 @@
</div>
`
: ""}
${this.activeTab === "Trade" ? this._renderTradeTab() : ""}
</div>
</div>
`;
}

createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
private _renderTradeTab() {
const me = this.game.myPlayer();
if (!me) return html``;
const ships = me.units(UnitType.TradeShip).filter((u) => u.isActive());
const ports = me.units(UnitType.Port).filter((p) => p.isActive());
const ticks = this.game.ticks();
const delay = this.game.config().tradeShipReplacementDelayTicks();
// Multi-build: gather all pending construction due ticks across ports
const pendingEntries: Array<{ port: UnitView; due: number }> = [];
for (const p of ports) {
const arr: number[] = (p as any).pendingTradeShipDueTicks?.() ?? [];
for (const due of arr) {
if (due > ticks) pendingEntries.push({ port: p, due });
}
}
pendingEntries.sort((a, b) => a.due - b.due);
const pendingRows = pendingEntries.map(({ port, due }, idx) => {
const remaining = due - ticks;
const pct = Math.min(
100,
Math.max(0, Math.round(((delay - remaining) / delay) * 100)),
);
return html`<div
class="py-1 px-2 border-b"
style="border-color: var(--ui-panel-border)"
>
<div class="mb-1 text-gray-300">
Trade Ship #${idx + 1} (Port #${port.id()}) constructing…
</div>
<div class="progress-track" style="height:6px;">
<div class="progress-fill" style="width:${pct}%;"></div>
</div>
</div>`;
});

const mapHeight = this.game.height();
const rows = ships.map((ship) => {
const tile = ship.tile();
const x = this.game.x(tile);
const topOriginY = this.game.y(tile);
const y = mapHeight - 1 - topOriginY; // display with bottom-left origin
const status = this._computeTradeShipStatus(ship);
return html`
<div
class="flex items-center justify-between py-1 px-2 border-b"
style="border-color: var(--ui-panel-border)"
>
<div class="truncate">
<span class="text-blue-200">Ship #${ship.id()}</span>
<span class="text-gray-400 ml-2">${status}</span>
</div>
<div class="text-gray-300 font-mono">(${x}, ${y})</div>
</div>
`;
});

// Compute demand indicator (global: all trade ships, not just mine)
const allTradeShips = this.game
.units(UnitType.TradeShip)
.filter((u) => u.isActive());
const totalShips = allTradeShips.length;
const availableShips = allTradeShips.filter((s) => {
const isReturning = s.returning();
const phase = s.tradePhase();
const hasTarget = s.targetUnitId() !== undefined;
const dockOwner = s.dockedAtPortOwner();
return !isReturning && phase === null && !hasTarget && dockOwner !== null;
}).length;
const queueLen = me.tradeDemandQueueLength();
const denom = Math.max(1, totalShips);
const queuedPct = queueLen / denom;
const availablePct = availableShips / denom;
let demandLabel = "Medium";
let demandColor = "var(--ui-text-default)";
if (queuedPct > 0.5) {
demandLabel = "Very High";
demandColor = "var(--ui-alert)";
} else if (queuedPct > 0.25) {
demandLabel = "High";
demandColor = "var(--ui-warning)";
} else if (availablePct > 0.5) {
demandLabel = "Very Low";
demandColor = "var(--ui-info)";
} else if (availablePct > 0.25) {
demandLabel = "Low";
demandColor = "var(--ui-success)";
} else {
demandLabel = "Medium";
demandColor = "var(--ui-text-default)";
}

return html`
<div class="w-full">
<div class="flex items-center justify-between mb-2">
<h3 class="military-heading">Trade Ships</h3>
<div
class="text-sm"
title="Demand is based on queued routes vs total ships and available ships"
>
<span
class="px-2 py-0.5 rounded-full border"
style="border-color: var(--ui-panel-border); color: ${demandColor};"
>
Trade Demand: ${demandLabel}
</span>
</div>
</div>
${pendingRows.length > 0
? html`<div class="mb-2">
<h4 class="text-gray-200 text-sm mb-1">Under Construction</h4>
<style>
/* Reuse research progress bar styling */
.progress-track {
width: 100%;
background: color-mix(
in srgb,
var(--ui-secondary) 25%,
transparent
);
border: 1px solid
color-mix(in srgb, var(--ui-secondary) 35%, transparent);
border-radius: 6px;
overflow: hidden;
margin: 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--ui-info) 90%, transparent) 0%,
color-mix(in srgb, var(--ui-info) 70%, transparent) 100%
);
box-shadow:
0 0 10px color-mix(in srgb, var(--ui-info) 55%, transparent),
0 0 16px color-mix(in srgb, var(--ui-info) 35%, transparent),
inset 0 0 4px
color-mix(in srgb, var(--ui-text-light) 10%, transparent);
}
</style>
<div class="divide-y">${pendingRows}</div>
</div>`
: ""}
${ships.length > 0
? html`<div class="divide-y">${rows}</div>`
: ships.length === 0 && pendingRows.length === 0
? html`<div class="text-gray-400">No active trade ships.</div>`
: ""}
</div>
`;
}

private _computeTradeShipStatus(ship: UnitView): string {
// Debug ship status logging removed
const ownerName = (pv: PlayerView | null) => pv?.displayName() ?? "Unknown";
const dockOwner = ship.dockedAtPortOwner();
const startOwner = ship.tradeRouteStartOwner();
const endOwner = ship.tradeRouteEndOwner();
const targetId = ship.targetUnitId();
const targetUnit =
targetId !== undefined ? this.game.unit(targetId) : undefined;

if (dockOwner && !ship.returning() && targetId === undefined) {
return `in port owned by ${ownerName(dockOwner)}`;
}

if (ship.returning()) {
if (targetUnit && targetUnit.type() === UnitType.Port) {
return `returning to port owned by ${ownerName(targetUnit.owner())}`;
}
return "returning to port";
}

const phase = ship.tradePhase();

if (phase === "toStart") {
return `traveling to start port owned by ${ownerName(startOwner)}`;
}
if (phase === "toEnd") {
if (startOwner || endOwner) {
return `trading between ${ownerName(startOwner)} and ${ownerName(endOwner)}`;
}
if (targetUnit && targetUnit.type() === UnitType.Port) {
return `traveling to port owned by ${ownerName(targetUnit.owner())}`;
}
}

return "at sea";
}
}

Expand Down
Loading
Loading