attack panel

This commit is contained in:
evanpelle
2026-02-07 15:15:44 -08:00
parent e97f4650b7
commit 9761bae201
5 changed files with 477 additions and 294 deletions
+4 -1
View File
@@ -241,7 +241,10 @@
<div
class="fixed left-0 bottom-0 min-[1200px]:left-4 min-[1200px]:bottom-4 w-full flex flex-col sm:flex-row sm:items-end z-50 pointer-events-none"
>
<div class="order-2 sm:order-none w-full sm:w-1/2 min-[1200px]:w-auto">
<div
class="order-2 sm:order-none w-full sm:w-1/2 min-[1200px]:w-auto lg:max-w-[400px]"
>
<attacks-display></attacks-display>
<control-panel></control-panel>
</div>
<div
+12
View File
@@ -7,6 +7,7 @@ import { FrameProfiler } from "./FrameProfiler";
import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState";
import { AlertFrame } from "./layers/AlertFrame";
import { AttacksDisplay } from "./layers/AttacksDisplay";
import { BuildMenu } from "./layers/BuildMenu";
import { ChatDisplay } from "./layers/ChatDisplay";
import { ChatModal } from "./layers/ChatModal";
@@ -123,6 +124,16 @@ export function createRenderer(
eventsDisplay.game = game;
eventsDisplay.uiState = uiState;
const attacksDisplay = document.querySelector(
"attacks-display",
) as AttacksDisplay;
if (!(attacksDisplay instanceof AttacksDisplay)) {
console.error("attacks display not found");
}
attacksDisplay.eventBus = eventBus;
attacksDisplay.game = game;
attacksDisplay.uiState = uiState;
const chatDisplay = document.querySelector("chat-display") as ChatDisplay;
if (!(chatDisplay instanceof ChatDisplay)) {
console.error("chat display not found");
@@ -276,6 +287,7 @@ export function createRenderer(
new DynamicUILayer(game, transformHandler, eventBus),
new NameLayer(game, transformHandler, eventBus),
eventsDisplay,
attacksDisplay,
chatDisplay,
buildMenu,
new MainRadialMenu(
@@ -0,0 +1,457 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { MessageType, PlayerType, UnitType } from "../../../core/game/Game";
import {
AttackUpdate,
GameUpdateType,
UnitIncomingUpdate,
} from "../../../core/game/GameUpdates";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import {
CancelAttackIntentEvent,
CancelBoatIntentEvent,
SendAttackIntentEvent,
} from "../../Transport";
import { renderTroops, translateText } from "../../Utils";
import { getColoredSprite } from "../SpriteLoader";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import {
GoToPlayerEvent,
GoToPositionEvent,
GoToUnitEvent,
} from "./Leaderboard";
import swordIcon from "/images/SwordIcon.svg?url";
@customElement("attacks-display")
export class AttacksDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
public uiState: UIState;
private active: boolean = false;
private incomingBoatIDs: Set<number> = new Set();
private spriteDataURLCache: Map<string, string> = new Map();
@state() private _isVisible: boolean = false;
@state() private incomingAttacks: AttackUpdate[] = [];
@state() private outgoingAttacks: AttackUpdate[] = [];
@state() private outgoingLandAttacks: AttackUpdate[] = [];
@state() private outgoingBoats: UnitView[] = [];
@state() private incomingBoats: UnitView[] = [];
createRenderRoot() {
return this;
}
init() {}
tick() {
this.active = true;
if (!this._isVisible && !this.game.inSpawnPhase()) {
this._isVisible = true;
}
const myPlayer = this.game.myPlayer();
if (!myPlayer || !myPlayer.isAlive()) {
if (this._isVisible) {
this._isVisible = false;
}
return;
}
// Track incoming boat unit IDs from UnitIncoming events
const updates = this.game.updatesSinceLastTick();
if (updates) {
for (const event of updates[
GameUpdateType.UnitIncoming
] as UnitIncomingUpdate[]) {
if (
event.playerID === myPlayer.smallID() &&
event.messageType === MessageType.NAVAL_INVASION_INBOUND
) {
this.incomingBoatIDs.add(event.unitID);
}
}
}
// Resolve incoming boats from tracked IDs, remove inactive ones
const resolvedIncomingBoats: UnitView[] = [];
for (const unitID of this.incomingBoatIDs) {
const unit = this.game.unit(unitID);
if (unit && unit.isActive() && unit.type() === UnitType.TransportShip) {
resolvedIncomingBoats.push(unit);
} else {
this.incomingBoatIDs.delete(unitID);
}
}
this.incomingBoats = resolvedIncomingBoats;
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
return t !== PlayerType.Bot;
});
this.outgoingAttacks = myPlayer
.outgoingAttacks()
.filter((a) => a.targetID !== 0);
this.outgoingLandAttacks = myPlayer
.outgoingAttacks()
.filter((a) => a.targetID === 0);
this.outgoingBoats = myPlayer
.units()
.filter((u) => u.type() === UnitType.TransportShip);
this.requestUpdate();
}
shouldTransform(): boolean {
return false;
}
renderLayer(): void {}
private renderButton(options: {
content: any;
onClick?: () => void;
className?: string;
disabled?: boolean;
translate?: boolean;
hidden?: boolean;
}) {
const {
content,
onClick,
className = "",
disabled = false,
translate = true,
hidden = false,
} = options;
if (hidden) {
return html``;
}
return html`
<button
class="${className}"
@click=${onClick}
?disabled=${disabled}
?translate=${translate}
>
${content}
</button>
`;
}
private emitCancelAttackIntent(id: string) {
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelAttackIntentEvent(id));
}
private emitBoatCancelIntent(id: number) {
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelBoatIntentEvent(id));
}
private emitGoToPlayerEvent(attackerID: number) {
const attacker = this.game.playerBySmallID(attackerID) as PlayerView;
if (!attacker) return;
this.eventBus.emit(new GoToPlayerEvent(attacker));
}
private emitGoToPositionEvent(x: number, y: number) {
this.eventBus.emit(new GoToPositionEvent(x, y));
}
private emitGoToUnitEvent(unit: UnitView) {
this.eventBus.emit(new GoToUnitEvent(unit));
}
private getBoatSpriteDataURL(unit: UnitView): string {
const owner = unit.owner();
const key = `boat-${owner.id()}`;
const cached = this.spriteDataURLCache.get(key);
if (cached) return cached;
try {
const canvas = getColoredSprite(unit, this.game.config().theme());
const dataURL = canvas.toDataURL();
this.spriteDataURLCache.set(key, dataURL);
return dataURL;
} catch {
return "";
}
}
private async attackWarningOnClick(attack: AttackUpdate) {
const playerView = this.game.playerBySmallID(attack.attackerID);
if (playerView !== undefined) {
if (playerView instanceof PlayerView) {
const averagePosition = await playerView.attackAveragePosition(
attack.attackerID,
attack.id,
);
if (averagePosition === null) {
this.emitGoToPlayerEvent(attack.attackerID);
} else {
this.emitGoToPositionEvent(averagePosition.x, averagePosition.y);
}
}
} else {
this.emitGoToPlayerEvent(attack.attackerID);
}
}
private handleRetaliate(attack: AttackUpdate) {
const attacker = this.game.playerBySmallID(attack.attackerID) as PlayerView;
if (!attacker) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
const counterTroops = Math.min(
attack.troops,
this.uiState.attackRatio * myPlayer.troops(),
);
this.eventBus.emit(new SendAttackIntentEvent(attacker.id(), counterTroops));
}
private renderIncomingAttacks() {
if (this.incomingAttacks.length === 0) return html``;
return this.incomingAttacks.map(
(attack) => html`
<div
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-sm rounded px-1.5 py-0.5 overflow-hidden"
>
${this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4 inline-block"
style="filter: brightness(0) saturate(100%) invert(27%) sepia(91%) saturate(4551%) hue-rotate(348deg) brightness(89%) contrast(97%)"
/>
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(attack.troops)}</span
>
<span class="truncate"
>${(
this.game.playerBySmallID(attack.attackerID) as PlayerView
)?.name()}</span
>
${attack.retreating
? `(${translateText("events_display.retreating")}...)`
: ""} `,
onClick: () => this.attackWarningOnClick(attack),
className:
"text-left text-red-400 inline-flex items-center gap-0.5 lg:gap-1 min-w-0",
translate: false,
})}
${!attack.retreating
? this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4"
style="filter: brightness(0) saturate(100%) invert(27%) sepia(91%) saturate(4551%) hue-rotate(348deg) brightness(89%) contrast(97%)"
/>`,
onClick: () => this.handleRetaliate(attack),
className:
"ml-auto inline-flex items-center justify-center cursor-pointer bg-red-900/50 hover:bg-red-800/70 rounded px-1.5 py-1 border border-red-700/50",
translate: false,
})
: ""}
</div>
`,
);
}
private renderOutgoingAttacks() {
if (this.outgoingAttacks.length === 0) return html``;
return this.outgoingAttacks.map(
(attack) => html`
<div
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-sm rounded px-1.5 py-0.5 overflow-hidden"
>
${this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4 inline-block"
style="filter: invert(1)"
/>
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(attack.troops)}</span
>
<span class="truncate"
>${(
this.game.playerBySmallID(attack.targetID) as PlayerView
)?.name()}</span
> `,
onClick: async () => this.attackWarningOnClick(attack),
className:
"text-left text-blue-400 inline-flex items-center gap-0.5 lg:gap-1 min-w-0",
translate: false,
})}
${!attack.retreating
? this.renderButton({
content: "❌",
onClick: () => this.emitCancelAttackIntent(attack.id),
className: "ml-auto text-left shrink-0",
disabled: attack.retreating,
})
: html`<span class="ml-auto shrink-0 text-blue-400"
>(${translateText("events_display.retreating")}...)</span
>`}
</div>
`,
);
}
private renderOutgoingLandAttacks() {
if (this.outgoingLandAttacks.length === 0) return html``;
return this.outgoingLandAttacks.map(
(landAttack) => html`
<div
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-sm rounded px-1.5 py-0.5 overflow-hidden"
>
${this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4 inline-block"
style="filter: invert(1)"
/>
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(landAttack.troops)}</span
>
${translateText("help_modal.ui_wilderness")}`,
className:
"text-left text-gray-400 inline-flex items-center gap-0.5 lg:gap-1 min-w-0",
translate: false,
})}
${!landAttack.retreating
? this.renderButton({
content: "❌",
onClick: () => this.emitCancelAttackIntent(landAttack.id),
className: "ml-auto text-left shrink-0",
disabled: landAttack.retreating,
})
: html`<span class="ml-auto shrink-0 text-blue-400"
>(${translateText("events_display.retreating")}...)</span
>`}
</div>
`,
);
}
private getBoatTargetName(boat: UnitView): string {
const target = boat.targetTile();
if (target === undefined) return "";
const ownerID = this.game.ownerID(target);
if (ownerID === 0) return "";
const player = this.game.playerBySmallID(ownerID) as PlayerView;
return player?.name() ?? "";
}
private renderBoatIcon(boat: UnitView) {
const dataURL = this.getBoatSpriteDataURL(boat);
if (!dataURL) return html``;
return html`<img
src="${dataURL}"
class="h-5 w-5 inline-block"
style="image-rendering: pixelated"
/>`;
}
private renderBoats() {
if (this.outgoingBoats.length === 0) return html``;
return this.outgoingBoats.map(
(boat) => html`
<div
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-sm rounded px-1.5 py-0.5 overflow-hidden"
>
${this.renderButton({
content: html`${this.renderBoatIcon(boat)}
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(boat.troops())}</span
>
<span class="truncate text-xs"
>${this.getBoatTargetName(boat)}</span
>`,
onClick: () => this.emitGoToUnitEvent(boat),
className:
"text-left text-blue-400 inline-flex items-center gap-0.5 lg:gap-1 min-w-0",
translate: false,
})}
${!boat.retreating()
? this.renderButton({
content: "❌",
onClick: () => this.emitBoatCancelIntent(boat.id()),
className: "ml-auto text-left shrink-0",
disabled: boat.retreating(),
})
: html`<span class="ml-auto shrink-0 text-blue-400"
>(${translateText("events_display.retreating")}...)</span
>`}
</div>
`,
);
}
private renderIncomingBoats() {
if (this.incomingBoats.length === 0) return html``;
return this.incomingBoats.map(
(boat) => html`
<div
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-sm rounded px-1.5 py-0.5 overflow-hidden"
>
${this.renderButton({
content: html`${this.renderBoatIcon(boat)}
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(boat.troops())}</span
>
<span class="truncate text-xs">${boat.owner()?.name()}</span>`,
onClick: () => this.emitGoToUnitEvent(boat),
className:
"text-left text-red-400 inline-flex items-center gap-0.5 lg:gap-1 min-w-0",
translate: false,
})}
</div>
`,
);
}
render() {
if (!this.active || !this._isVisible) {
return html``;
}
const hasAnything =
this.outgoingAttacks.length > 0 ||
this.outgoingLandAttacks.length > 0 ||
this.outgoingBoats.length > 0 ||
this.incomingAttacks.length > 0 ||
this.incomingBoats.length > 0;
if (!hasAnything) {
return html``;
}
return html`
<div
class="w-full mb-1 pointer-events-auto grid grid-cols-2 lg:grid-cols-1 gap-1 text-white text-sm lg:text-base"
>
${this.renderOutgoingAttacks()} ${this.renderOutgoingLandAttacks()}
${this.renderBoats()} ${this.renderIncomingAttacks()}
${this.renderIncomingBoats()}
</div>
`;
}
}
+1 -1
View File
@@ -261,7 +261,7 @@ export class ControlPanel extends LitElement implements Layer {
return html`
<div
class="pointer-events-auto ${this._isVisible
? "relative z-[60] w-full max-lg:landscape:fixed max-lg:landscape:bottom-0 max-lg:landscape:left-0 max-lg:landscape:w-1/2 max-lg:landscape:z-50 lg:max-w-[400px] text-sm lg:text-base bg-gray-800/70 p-1.5 pr-2 lg:p-5 shadow-lg lg:rounded-tr-xl min-[1200px]:rounded-xl backdrop-blur-sm"
? "relative z-[60] w-full lg:max-w-[400px] text-sm lg:text-base bg-gray-800/70 p-1.5 pr-2 lg:p-5 shadow-lg lg:rounded-tr-xl min-[1200px]:rounded-xl backdrop-blur-sm"
: "hidden"}"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
+3 -292
View File
@@ -8,15 +8,12 @@ import {
getMessageCategory,
MessageCategory,
MessageType,
PlayerType,
Tick,
UnitType,
} from "../../../core/game/Game";
import {
AllianceExpiredUpdate,
AllianceRequestReplyUpdate,
AllianceRequestUpdate,
AttackUpdate,
BrokeAllianceUpdate,
DisplayChatMessageUpdate,
DisplayMessageUpdate,
@@ -26,22 +23,15 @@ import {
UnitIncomingUpdate,
} from "../../../core/game/GameUpdates";
import {
CancelAttackIntentEvent,
CancelBoatIntentEvent,
SendAllianceExtensionIntentEvent,
SendAllianceReplyIntentEvent,
SendAttackIntentEvent,
} from "../../Transport";
import { Layer } from "./Layer";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { onlyImages } from "../../../core/Util";
import { renderNumber, renderTroops } from "../../Utils";
import {
GoToPlayerEvent,
GoToPositionEvent,
GoToUnitEvent,
} from "./Leaderboard";
import { renderNumber } from "../../Utils";
import { GoToPlayerEvent, GoToUnitEvent } from "./Leaderboard";
import { getMessageTypeClasses, translateText } from "../../Utils";
import { UIState } from "../UIState";
@@ -84,10 +74,6 @@ export class EventsDisplay extends LitElement implements Layer {
// allianceID -> last checked at tick
private alliancesCheckedAt = new Map<number, Tick>();
@state() private incomingAttacks: AttackUpdate[] = [];
@state() private outgoingAttacks: AttackUpdate[] = [];
@state() private outgoingLandAttacks: AttackUpdate[] = [];
@state() private outgoingBoats: UnitView[] = [];
@state() private _hidden: boolean = false;
@state() private _isVisible: boolean = false;
@state() private newEvents: number = 0;
@@ -194,9 +180,6 @@ export class EventsDisplay extends LitElement implements Layer {
constructor() {
super();
this.events = [];
this.incomingAttacks = [];
this.outgoingAttacks = [];
this.outgoingBoats = [];
}
init() {}
@@ -254,24 +237,6 @@ export class EventsDisplay extends LitElement implements Layer {
this.requestUpdate();
}
// Update attacks
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
return t !== PlayerType.Bot;
});
this.outgoingAttacks = myPlayer
.outgoingAttacks()
.filter((a) => a.targetID !== 0);
this.outgoingLandAttacks = myPlayer
.outgoingAttacks()
.filter((a) => a.targetID === 0);
this.outgoingBoats = myPlayer
.units()
.filter((u) => u.type() === UnitType.TransportShip);
this.requestUpdate();
}
@@ -664,28 +629,12 @@ export class EventsDisplay extends LitElement implements Layer {
});
}
emitCancelAttackIntent(id: string) {
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelAttackIntentEvent(id));
}
emitBoatCancelIntent(id: number) {
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelBoatIntentEvent(id));
}
emitGoToPlayerEvent(attackerID: number) {
const attacker = this.game.playerBySmallID(attackerID) as PlayerView;
if (!attacker) return;
this.eventBus.emit(new GoToPlayerEvent(attacker));
}
emitGoToPositionEvent(x: number, y: number) {
this.eventBus.emit(new GoToPositionEvent(x, y));
}
emitGoToUnitEvent(unit: UnitView) {
this.eventBus.emit(new GoToUnitEvent(unit));
}
@@ -753,196 +702,6 @@ export class EventsDisplay extends LitElement implements Layer {
: event.description;
}
private async attackWarningOnClick(attack: AttackUpdate) {
const playerView = this.game.playerBySmallID(attack.attackerID);
if (playerView !== undefined) {
if (playerView instanceof PlayerView) {
const averagePosition = await playerView.attackAveragePosition(
attack.attackerID,
attack.id,
);
if (averagePosition === null) {
this.emitGoToPlayerEvent(attack.attackerID);
} else {
this.emitGoToPositionEvent(averagePosition.x, averagePosition.y);
}
}
} else {
this.emitGoToPlayerEvent(attack.attackerID);
}
}
private handleRetaliate(attack: AttackUpdate) {
const attacker = this.game.playerBySmallID(attack.attackerID) as PlayerView;
if (!attacker) return;
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
const counterTroops = Math.min(
attack.troops,
this.uiState.attackRatio * myPlayer.troops(),
);
this.eventBus.emit(new SendAttackIntentEvent(attacker.id(), counterTroops));
}
private renderIncomingAttacks() {
return html`
${this.incomingAttacks.length > 0
? html`
<div class="flex flex-wrap gap-y-1 gap-x-2">
${this.incomingAttacks.map(
(attack) => html`
<div class="inline-flex items-center gap-1">
${this.renderButton({
content: html`
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(
attack.attackerID,
) as PlayerView
)?.name()}
${attack.retreating
? `(${translateText("events_display.retreating")}...)`
: ""}
`,
onClick: () => this.attackWarningOnClick(attack),
className: "text-left text-red-400",
translate: false,
})}
${!attack.retreating
? this.renderButton({
content: translateText("events_display.retaliate"),
onClick: () => this.handleRetaliate(attack),
className:
"inline-block px-3 py-1 text-white rounded-sm text-md md:text-sm cursor-pointer transition-colors duration-300 bg-red-600 hover:bg-red-700",
translate: true,
})
: ""}
</div>
`,
)}
</div>
`
: ""}
`;
}
private renderOutgoingAttacks() {
return html`
${this.outgoingAttacks.length > 0
? html`
<div class="flex flex-wrap gap-y-1 gap-x-2">
${this.outgoingAttacks.map(
(attack) => html`
<div class="inline-flex items-center gap-1">
${this.renderButton({
content: html`
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(
attack.targetID,
) as PlayerView
)?.name()}
`,
onClick: async () => this.attackWarningOnClick(attack),
className: "text-left text-blue-400",
translate: false,
})}
${!attack.retreating
? this.renderButton({
content: "❌",
onClick: () => this.emitCancelAttackIntent(attack.id),
className: "text-left shrink-0",
disabled: attack.retreating,
})
: html`<span class="shrink-0 text-blue-400"
>(${translateText(
"events_display.retreating",
)}...)</span
>`}
</div>
`,
)}
</div>
`
: ""}
`;
}
private renderOutgoingLandAttacks() {
return html`
${this.outgoingLandAttacks.length > 0
? html`
<div class="flex flex-wrap gap-y-1 gap-x-2">
${this.outgoingLandAttacks.map(
(landAttack) => html`
<div class="inline-flex items-center gap-1">
${this.renderButton({
content: html`${renderTroops(landAttack.troops)}
${translateText("help_modal.ui_wilderness")}`,
className: "text-left text-gray-400",
translate: false,
})}
${!landAttack.retreating
? this.renderButton({
content: "❌",
onClick: () =>
this.emitCancelAttackIntent(landAttack.id),
className: "text-left shrink-0",
disabled: landAttack.retreating,
})
: html`<span class="shrink-0 text-blue-400"
>(${translateText(
"events_display.retreating",
)}...)</span
>`}
</div>
`,
)}
</div>
`
: ""}
`;
}
private renderBoats() {
return html`
${this.outgoingBoats.length > 0
? html`
<div class="flex flex-wrap gap-y-1 gap-x-2">
${this.outgoingBoats.map(
(boat) => html`
<div class="inline-flex items-center gap-1">
${this.renderButton({
content: html`${translateText("events_display.boat")}:
${renderTroops(boat.troops())}`,
onClick: () => this.emitGoToUnitEvent(boat),
className: "text-left text-blue-400",
translate: false,
})}
${!boat.retreating()
? this.renderButton({
content: "❌",
onClick: () => this.emitBoatCancelIntent(boat.id()),
className: "text-left shrink-0",
disabled: boat.retreating(),
})
: html`<span class="shrink-0 text-blue-400"
>(${translateText(
"events_display.retreating",
)}...)</span
>`}
</div>
`,
)}
</div>
`
: ""}
`;
}
private renderBetrayalDebuffTimer() {
const myPlayer = this.game.myPlayer();
if (!myPlayer || !myPlayer.isTraitor()) {
@@ -1161,17 +920,6 @@ export class EventsDisplay extends LitElement implements Layer {
</tr>
`,
)}
<!--- Incoming attacks row -->
${this.incomingAttacks.length > 0
? html`
<tr class="lg:px-2 lg:py-1 p-1">
<td class="lg:px-2 lg:py-1 p-1 text-left">
${this.renderIncomingAttacks()}
</td>
</tr>
`
: ""}
<!--- Betrayal debuff timer row -->
${(() => {
const myPlayer = this.game.myPlayer();
@@ -1190,45 +938,8 @@ export class EventsDisplay extends LitElement implements Layer {
`
: ""}
<!--- Outgoing attacks row -->
${this.outgoingAttacks.length > 0
? html`
<tr class="lg:px-2 lg:py-1 p-1">
<td class="lg:px-2 lg:py-1 p-1 text-left">
${this.renderOutgoingAttacks()}
</td>
</tr>
`
: ""}
<!--- Outgoing land attacks row -->
${this.outgoingLandAttacks.length > 0
? html`
<tr class="lg:px-2 lg:py-1 p-1">
<td class="lg:px-2 lg:py-1 p-1 text-left">
${this.renderOutgoingLandAttacks()}
</td>
</tr>
`
: ""}
<!--- Boats row -->
${this.outgoingBoats.length > 0
? html`
<tr class="lg:px-2 lg:py-1 p-1">
<td class="lg:px-2 lg:py-1 p-1 text-left">
${this.renderBoats()}
</td>
</tr>
`
: ""}
<!--- Empty row when no events or attacks -->
<!--- Empty row when no events -->
${filteredEvents.length === 0 &&
this.incomingAttacks.length === 0 &&
this.outgoingAttacks.length === 0 &&
this.outgoingLandAttacks.length === 0 &&
this.outgoingBoats.length === 0 &&
!(() => {
const myPlayer = this.game.myPlayer();
return (