mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
attack panel
This commit is contained in:
+4
-1
@@ -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
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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()}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user