mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Improve Ingame UI ✨ (#3212)
## Description: - **Dynamic sidebar offset for top bars** - GameLeftSidebar, GameRightSidebar, and PlayerInfoOverlay now shift down when SpawnTimer and/or ImmunityTimer bars are visible (7px per bar). Implemented via events. - **Fixed text overflow** in HeadsUpMessage.ts (Random spawn message is long) - **Fixed inconsistent text sizing** in EventsDisplay - **Alliance icon horizontal** in PlayerInfoOverlay so the size of the overlay doesn't change if there is an alliance - **Nation relation coloring** - Nation player names are now colored based on their relation - **Background & Blur Unification** - **Border Radius & Page Edge Gap Standardization** - **EventsDisplay collapsed button:** Fixed badge hidden / inline-block CSS conflict (conditional rendering), added gap-2 between text and badge - **Right panel spacing:** Changed right container from sm:w-1/2 to sm:flex-1 to fill remaining space - **Leaderboard**: Rounded grid corners (rounded-lg overflow-hidden), removed last-row border, added `willUpdate` for auto-refresh on hide/show click, plus button styled to match toggle buttons - Other little CSS fixes (margins etc) Showcase: (Note the red mexico name on betrayal) https://github.com/user-attachments/assets/f0ed91de-3a07-4564-a209-3d7723edee55 Two progress bars at the top, mobile UI not cut off: https://github.com/user-attachments/assets/83f1fd64-ceab-4a74-8d16-6e1eeea1709d HeadsUpMessage text overflow fixed, SpawnTimer does not cut off the PlayerInfoOverlay: <img width="516" height="929" alt="Screenshot 2026-02-14 214410" src="https://github.com/user-attachments/assets/74f0edea-8c01-4394-a3d0-a3245922e0da" /> Previous: <img width="306" height="118" alt="Screenshot 2026-02-14 213705" src="https://github.com/user-attachments/assets/a7c7e8f3-f0e8-4213-8a8f-4f3677e9fc98" /> Smaller event panel text: <img width="594" height="975" alt="Screenshot 2026-02-14 215738" src="https://github.com/user-attachments/assets/33e80570-9260-40b0-b810-c71eda4861fc" /> ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
This commit is contained in:
+4
-2
@@ -249,7 +249,7 @@
|
||||
<control-panel></control-panel>
|
||||
</div>
|
||||
<div
|
||||
class="order-1 sm:order-none w-full sm:w-1/2 min-[1200px]:w-auto min-[1200px]:fixed min-[1200px]:right-0 min-[1200px]:bottom-0 flex flex-col sm:items-end pointer-events-none"
|
||||
class="order-1 sm:order-none w-full sm:flex-1 min-[1200px]:w-auto min-[1200px]:fixed min-[1200px]:right-0 min-[1200px]:bottom-0 flex flex-col sm:items-end pointer-events-none"
|
||||
>
|
||||
<chat-display></chat-display>
|
||||
<events-display></events-display>
|
||||
@@ -262,7 +262,9 @@
|
||||
<win-modal></win-modal>
|
||||
<game-starting-modal></game-starting-modal>
|
||||
<unit-display></unit-display>
|
||||
<div class="flex flex-col items-end fixed top-4 right-4 z-1000 gap-2">
|
||||
<div
|
||||
class="flex flex-col items-end fixed top-0 right-0 min-[1200px]:top-4 min-[1200px]:right-4 z-1000 gap-2"
|
||||
>
|
||||
<game-right-sidebar></game-right-sidebar>
|
||||
<replay-panel></replay-panel>
|
||||
</div>
|
||||
|
||||
@@ -714,6 +714,7 @@
|
||||
"show_units": "Show Units"
|
||||
},
|
||||
"events_display": {
|
||||
"events": "Events",
|
||||
"retreating": "retreating",
|
||||
"alliance_request_status": "{name} {status} your alliance request",
|
||||
"alliance_accepted": "accepted",
|
||||
|
||||
@@ -98,6 +98,7 @@ export function createRenderer(
|
||||
console.error("GameLeftSidebar element not found in the DOM");
|
||||
}
|
||||
gameLeftSidebar.game = game;
|
||||
gameLeftSidebar.eventBus = eventBus;
|
||||
|
||||
const teamStats = document.querySelector("team-stats") as TeamStats;
|
||||
if (!teamStats || !(teamStats instanceof TeamStats)) {
|
||||
@@ -246,6 +247,7 @@ export function createRenderer(
|
||||
console.error("spawn timer not found");
|
||||
}
|
||||
spawnTimer.game = game;
|
||||
spawnTimer.eventBus = eventBus;
|
||||
spawnTimer.transformHandler = transformHandler;
|
||||
|
||||
const immunityTimer = document.querySelector(
|
||||
@@ -255,6 +257,7 @@ export function createRenderer(
|
||||
console.error("immunity timer not found");
|
||||
}
|
||||
immunityTimer.game = game;
|
||||
immunityTimer.eventBus = eventBus;
|
||||
|
||||
const inGameHeaderAd = document.querySelector(
|
||||
"in-game-header-ad",
|
||||
|
||||
@@ -221,7 +221,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.incomingAttacks.map(
|
||||
(attack) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
@@ -232,7 +232,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
<span class="inline-block min-w-[3rem] text-right"
|
||||
>${renderTroops(attack.troops)}</span
|
||||
>
|
||||
<span class="truncate"
|
||||
<span class="truncate ml-1"
|
||||
>${(
|
||||
this.game.playerBySmallID(attack.attackerID) as PlayerView
|
||||
)?.name()}</span
|
||||
@@ -254,7 +254,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
/>`,
|
||||
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",
|
||||
"ml-auto inline-flex items-center justify-center cursor-pointer bg-red-900/50 hover:bg-red-800/70 rounded-lg px-1.5 py-1 border border-red-700/50",
|
||||
translate: false,
|
||||
})
|
||||
: ""}
|
||||
@@ -269,7 +269,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.outgoingAttacks.map(
|
||||
(attack) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
@@ -280,7 +280,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
<span class="inline-block min-w-[3rem] text-right"
|
||||
>${renderTroops(attack.troops)}</span
|
||||
>
|
||||
<span class="truncate"
|
||||
<span class="truncate ml-1"
|
||||
>${(
|
||||
this.game.playerBySmallID(attack.targetID) as PlayerView
|
||||
)?.name()}</span
|
||||
@@ -311,7 +311,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.outgoingLandAttacks.map(
|
||||
(landAttack) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg px-1.5 py-0.5 overflow-hidden"
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
@@ -367,14 +367,14 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.outgoingBoats.map(
|
||||
(boat) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg 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"
|
||||
<span class="truncate text-xs ml-1"
|
||||
>${this.getBoatTargetName(boat)}</span
|
||||
>`,
|
||||
onClick: () => this.eventBus.emit(new GoToUnitEvent(boat)),
|
||||
@@ -403,14 +403,16 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
return this.incomingBoats.map(
|
||||
(boat) => html`
|
||||
<div
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs rounded px-1.5 py-0.5 overflow-hidden"
|
||||
class="flex items-center gap-0.5 w-full bg-gray-800/70 backdrop-blur-xs min-[1200px]:rounded-lg sm:rounded-r-lg 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>`,
|
||||
<span class="truncate text-xs ml-1"
|
||||
>${boat.owner()?.name()}</span
|
||||
>`,
|
||||
onClick: () => this.eventBus.emit(new GoToUnitEvent(boat)),
|
||||
className:
|
||||
"text-left text-red-400 inline-flex items-center gap-0.5 lg:gap-1 min-w-0",
|
||||
@@ -439,7 +441,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
|
||||
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"
|
||||
class="w-full mb-1 mt-1 sm:mt-0 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()}
|
||||
|
||||
@@ -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 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 sm:rounded-tr-lg min-[1200px]:rounded-lg backdrop-blur-xs"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -788,17 +788,19 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
${this.renderButton({
|
||||
content: html`
|
||||
Events
|
||||
<span
|
||||
class="${this.newEvents
|
||||
? ""
|
||||
: "hidden"} inline-block px-2 bg-red-500 rounded-lg text-sm"
|
||||
>${this.newEvents}</span
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
${translateText("events_display.events")}
|
||||
${this.newEvents > 0
|
||||
? html`<span
|
||||
class="inline-block px-2 bg-red-500 rounded-lg text-sm"
|
||||
>${this.newEvents}</span
|
||||
>`
|
||||
: ""}
|
||||
</span>
|
||||
`,
|
||||
onClick: this.toggleHidden,
|
||||
className:
|
||||
"text-white cursor-pointer pointer-events-auto w-fit p-2 lg:p-3 rounded-lg bg-gray-800/70 backdrop-blur-sm",
|
||||
"text-white cursor-pointer pointer-events-auto w-fit p-2 lg:p-3 min-[1200px]:rounded-lg sm:rounded-tl-lg bg-gray-800/70 backdrop-blur-xs",
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
@@ -809,9 +811,9 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
<!-- Button Bar -->
|
||||
<div
|
||||
class="w-full p-2 lg:p-3 bg-gray-800/70 min-[1200px]:rounded-t-lg lg:rounded-tl-lg"
|
||||
class="w-full p-2 lg:p-3 bg-gray-800/70 min-[1200px]:rounded-t-lg sm:rounded-tl-lg"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center gap-3">
|
||||
<div class="flex gap-4">
|
||||
${this.renderToggleButton(
|
||||
swordIcon,
|
||||
@@ -857,7 +859,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
<div>
|
||||
<table
|
||||
class="w-full max-h-none border-collapse text-white shadow-lg lg:text-base text-md md:text-xs pointer-events-auto"
|
||||
class="w-full max-h-none border-collapse text-white shadow-lg text-xs lg:text-sm pointer-events-auto"
|
||||
>
|
||||
<tbody>
|
||||
${filteredEvents.map(
|
||||
@@ -896,7 +898,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
${event.buttons.map(
|
||||
(btn) => html`
|
||||
<button
|
||||
class="inline-block px-3 py-1 text-white rounded-sm text-md md:text-sm cursor-pointer transition-colors duration-300
|
||||
class="inline-block px-3 py-1 text-white rounded-sm text-xs lg:text-sm cursor-pointer transition-colors duration-300
|
||||
${btn.className.includes("btn-info")
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: btn.className.includes(
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Colord } from "colord";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameMode } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { translateText } from "../../Utils";
|
||||
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
|
||||
import { Layer } from "./Layer";
|
||||
import { SpawnBarVisibleEvent } from "./SpawnTimer";
|
||||
import leaderboardRegularIcon from "/images/LeaderboardIconRegularWhite.svg?url";
|
||||
import leaderboardSolidIcon from "/images/LeaderboardIconSolidWhite.svg?url";
|
||||
import teamRegularIcon from "/images/TeamIconRegularWhite.svg?url";
|
||||
@@ -22,9 +25,14 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
private isPlayerTeamLabelVisible = false;
|
||||
@state()
|
||||
private playerTeam: string | null = null;
|
||||
@state()
|
||||
private spawnBarVisible = false;
|
||||
@state()
|
||||
private immunityBarVisible = false;
|
||||
|
||||
private playerColor: Colord = new Colord("#FFFFFF");
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private _shownOnInit = false;
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -33,6 +41,12 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
|
||||
init() {
|
||||
this.isVisible = true;
|
||||
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
|
||||
this.spawnBarVisible = e.visible;
|
||||
});
|
||||
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
|
||||
this.immunityBarVisible = e.visible;
|
||||
});
|
||||
if (this.isTeamGame) {
|
||||
this.isPlayerTeamLabelVisible = true;
|
||||
}
|
||||
@@ -68,6 +82,10 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private get barOffset(): number {
|
||||
return (this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
|
||||
}
|
||||
|
||||
private toggleLeaderboard(): void {
|
||||
this.isLeaderboardShow = !this.isLeaderboardShow;
|
||||
}
|
||||
@@ -90,9 +108,10 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
render() {
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-4 left-4 z-1000 flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-xs shadow-xs rounded-lg transition-transform duration-300 ease-out transform ${
|
||||
class=${`fixed top-0 min-[1200px]:top-4 left-0 min-[1200px]:left-4 z-1000 flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-gray-800/70 backdrop-blur-xs shadow-xs min-[1200px]:rounded-lg rounded-br-lg transition-all duration-300 ease-out transform ${
|
||||
this.isVisible ? "translate-x-0" : "hidden"
|
||||
}`}
|
||||
style="margin-top: ${this.barOffset}px;"
|
||||
>
|
||||
<div class="flex items-center gap-4 xl:gap-6 text-white">
|
||||
<div
|
||||
@@ -152,7 +171,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
${this.isPlayerTeamLabelVisible
|
||||
? html`
|
||||
<div
|
||||
class="flex items-center w-full text-white"
|
||||
class="flex items-center w-full text-white mt-2"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
${translateText("help_modal.ui_your_team")}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { GameView } from "../../../core/game/GameView";
|
||||
import { crazyGamesSDK } from "../../CrazyGamesSDK";
|
||||
import { PauseGameIntentEvent, SendWinnerEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
|
||||
import { Layer } from "./Layer";
|
||||
import { ShowReplayPanelEvent } from "./ReplayPanel";
|
||||
import { ShowSettingsModalEvent } from "./SettingsModal";
|
||||
import { SpawnBarVisibleEvent } from "./SpawnTimer";
|
||||
import exitIcon from "/images/ExitIconWhite.svg?url";
|
||||
import FastForwardIconSolid from "/images/FastForwardIconSolidWhite.svg?url";
|
||||
import pauseIcon from "/images/PauseIconWhite.svg?url";
|
||||
@@ -37,6 +39,8 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
private hasWinner = false;
|
||||
private isLobbyCreator = false;
|
||||
private spawnBarVisible = false;
|
||||
private immunityBarVisible = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -49,6 +53,15 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
this._isVisible = true;
|
||||
this.game.inSpawnPhase();
|
||||
|
||||
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
|
||||
this.spawnBarVisible = e.visible;
|
||||
this.updateParentOffset();
|
||||
});
|
||||
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
|
||||
this.immunityBarVisible = e.visible;
|
||||
this.updateParentOffset();
|
||||
});
|
||||
|
||||
this.eventBus.on(SendWinnerEvent, () => {
|
||||
this.hasWinner = true;
|
||||
this.requestUpdate();
|
||||
@@ -91,6 +104,15 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private updateParentOffset(): void {
|
||||
const offset =
|
||||
(this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
|
||||
const parent = this.parentElement as HTMLElement;
|
||||
if (parent) {
|
||||
parent.style.marginTop = `${offset}px`;
|
||||
}
|
||||
}
|
||||
|
||||
private secondsToHms = (d: number): string => {
|
||||
const pad = (n: number) => (n < 10 ? `0${n}` : n);
|
||||
|
||||
@@ -153,7 +175,7 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<aside
|
||||
class=${`w-fit flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/70 backdrop-blur-xs shadow-xs rounded-lg transition-transform duration-300 ease-out transform text-white ${
|
||||
class=${`w-fit flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/70 backdrop-blur-xs shadow-xs min-[1200px]:rounded-lg rounded-bl-lg transition-transform duration-300 ease-out transform text-white ${
|
||||
this._isVisible ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
|
||||
@@ -146,10 +146,10 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
? html`
|
||||
<div
|
||||
class="fixed top-[15%] left-1/2 -translate-x-1/2 z-[11000]
|
||||
inline-flex items-center justify-center h-8 lg:h-10
|
||||
inline-flex items-center justify-center min-h-8 lg:min-h-10
|
||||
w-fit max-w-[90vw]
|
||||
bg-gray-900/60 rounded-md lg:rounded-lg
|
||||
backdrop-blur-md text-white text-md lg:text-xl px-3 lg:px-4
|
||||
bg-gray-800/70 rounded-md lg:rounded-lg
|
||||
backdrop-blur-xs text-white text-md lg:text-xl px-3 lg:px-4 py-1
|
||||
text-center break-words"
|
||||
style="word-wrap: break-word; hyphens: auto;"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { GameMode } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class ImmunityBarVisibleEvent implements GameEvent {
|
||||
constructor(public readonly visible: boolean) {}
|
||||
}
|
||||
|
||||
@customElement("immunity-timer")
|
||||
export class ImmunityTimer extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
|
||||
private isVisible = false;
|
||||
private _barVisible = false;
|
||||
private isActive = false;
|
||||
private progressRatio = 0;
|
||||
|
||||
@@ -46,24 +53,24 @@ export class ImmunityTimer extends LitElement implements Layer {
|
||||
this.game.inSpawnPhase()
|
||||
) {
|
||||
this.setInactive();
|
||||
return;
|
||||
} else {
|
||||
const immunityEnd = spawnPhaseTurns + immunityDuration;
|
||||
const ticks = this.game.ticks();
|
||||
|
||||
if (ticks >= immunityEnd || ticks < spawnPhaseTurns) {
|
||||
this.setInactive();
|
||||
} else {
|
||||
const elapsedTicks = Math.max(0, ticks - spawnPhaseTurns);
|
||||
this.progressRatio = Math.min(
|
||||
1,
|
||||
Math.max(0, elapsedTicks / immunityDuration),
|
||||
);
|
||||
this.isActive = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
const immunityEnd = spawnPhaseTurns + immunityDuration;
|
||||
const ticks = this.game.ticks();
|
||||
|
||||
if (ticks >= immunityEnd || ticks < spawnPhaseTurns) {
|
||||
this.setInactive();
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedTicks = Math.max(0, ticks - spawnPhaseTurns);
|
||||
this.progressRatio = Math.min(
|
||||
1,
|
||||
Math.max(0, elapsedTicks / immunityDuration),
|
||||
);
|
||||
this.isActive = true;
|
||||
this.requestUpdate();
|
||||
this.emitBarVisibility();
|
||||
}
|
||||
|
||||
private setInactive() {
|
||||
@@ -73,6 +80,14 @@ export class ImmunityTimer extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private emitBarVisibility() {
|
||||
const nowVisible = this.isVisible && this.isActive;
|
||||
if (nowVisible !== this._barVisible) {
|
||||
this._barVisible = nowVisible;
|
||||
this.eventBus?.emit(new ImmunityBarVisibleEvent(this._barVisible));
|
||||
}
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,12 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
|
||||
init() {}
|
||||
|
||||
willUpdate(changed: Map<string, unknown>) {
|
||||
if (changed.has("visible") && this.visible) {
|
||||
this.updateLeaderboard();
|
||||
}
|
||||
}
|
||||
|
||||
getTickIntervalMs() {
|
||||
return 1000;
|
||||
}
|
||||
@@ -184,10 +190,10 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="grid bg-gray-800/70 w-full text-xs md:text-xs lg:text-sm"
|
||||
class="grid bg-gray-800/85 w-full text-xs md:text-xs lg:text-sm rounded-lg overflow-hidden"
|
||||
style="grid-template-columns: 30px 100px 70px 55px 105px;"
|
||||
>
|
||||
<div class="contents font-bold bg-gray-700/50">
|
||||
<div class="contents font-bold bg-gray-700/60">
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
#
|
||||
</div>
|
||||
@@ -234,28 +240,51 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
${repeat(
|
||||
this.players,
|
||||
(p) => p.player.id(),
|
||||
(player) => html`
|
||||
(player, index) => html`
|
||||
<div
|
||||
class="contents hover:bg-slate-600/60 ${player.isOnSameTeam
|
||||
? "font-bold"
|
||||
: ""} cursor-pointer"
|
||||
@click=${() => this.handleRowClickPlayer(player.player)}
|
||||
>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
<div
|
||||
class="py-1 md:py-2 text-center ${index <
|
||||
this.players.length - 1
|
||||
? "border-b border-slate-500"
|
||||
: ""}"
|
||||
>
|
||||
${player.position}
|
||||
</div>
|
||||
<div
|
||||
class="py-1 md:py-2 text-center border-b border-slate-500 truncate"
|
||||
class="py-1 md:py-2 text-center ${index <
|
||||
this.players.length - 1
|
||||
? "border-b border-slate-500"
|
||||
: ""} truncate"
|
||||
>
|
||||
${player.name}
|
||||
</div>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
<div
|
||||
class="py-1 md:py-2 text-center ${index <
|
||||
this.players.length - 1
|
||||
? "border-b border-slate-500"
|
||||
: ""}"
|
||||
>
|
||||
${player.score}
|
||||
</div>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
<div
|
||||
class="py-1 md:py-2 text-center ${index <
|
||||
this.players.length - 1
|
||||
? "border-b border-slate-500"
|
||||
: ""}"
|
||||
>
|
||||
${player.gold}
|
||||
</div>
|
||||
<div class="py-1 md:py-2 text-center border-b border-slate-500">
|
||||
<div
|
||||
class="py-1 md:py-2 text-center ${index <
|
||||
this.players.length - 1
|
||||
? "border-b border-slate-500"
|
||||
: ""}"
|
||||
>
|
||||
${player.maxTroops}
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,9 +294,9 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="mt-1 px-1.5 pb-0.5 md:px-2 text-xs md:text-xs lg:text-sm
|
||||
class="mt-2 p-0.5 px-1.5 md:px-2 text-xs md:text-xs lg:text-sm
|
||||
border rounded-md border-slate-500 transition-colors
|
||||
text-white mx-auto block hover:bg-white/10"
|
||||
text-white mx-auto block hover:bg-white/10 bg-gray-700/50"
|
||||
@click=${() => {
|
||||
this.showTopFive = !this.showTopFive;
|
||||
this.updateLeaderboard();
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
} from "../../Utils";
|
||||
import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
|
||||
import { Layer } from "./Layer";
|
||||
import { CloseRadialMenuEvent } from "./RadialMenu";
|
||||
import { SpawnBarVisibleEvent } from "./SpawnTimer";
|
||||
import allianceIcon from "/images/AllianceIcon.svg?url";
|
||||
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
|
||||
import cityIcon from "/images/CityIconWhite.svg?url";
|
||||
@@ -77,8 +79,17 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
@state()
|
||||
private _isInfoVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
private spawnBarVisible = false;
|
||||
@state()
|
||||
private immunityBarVisible = false;
|
||||
|
||||
private _isActive = false;
|
||||
|
||||
private get barOffset(): number {
|
||||
return (this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
|
||||
}
|
||||
|
||||
private lastMouseUpdate = 0;
|
||||
|
||||
init() {
|
||||
@@ -89,6 +100,12 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
this.maybeShow(e.x, e.y),
|
||||
);
|
||||
this.eventBus.on(CloseRadialMenuEvent, () => this.hide());
|
||||
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
|
||||
this.spawnBarVisible = e.visible;
|
||||
});
|
||||
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
|
||||
this.immunityBarVisible = e.visible;
|
||||
});
|
||||
this._isActive = true;
|
||||
}
|
||||
|
||||
@@ -155,6 +172,24 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private getPlayerNameColor(
|
||||
player: PlayerView,
|
||||
myPlayer: PlayerView | null | undefined,
|
||||
isFriendly: boolean,
|
||||
): string {
|
||||
if (isFriendly) return "text-green-500";
|
||||
if (
|
||||
myPlayer &&
|
||||
myPlayer !== player &&
|
||||
player.type() === PlayerType.Nation
|
||||
) {
|
||||
const relation =
|
||||
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
|
||||
return this.getRelationClass(relation);
|
||||
}
|
||||
return "text-white";
|
||||
}
|
||||
|
||||
private getRelationClass(relation: Relation): string {
|
||||
switch (relation) {
|
||||
case Relation.Hostile:
|
||||
@@ -255,7 +290,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
.find((alliance) => alliance.other === player.id());
|
||||
if (alliance !== undefined) {
|
||||
allianceHtml = html` <div
|
||||
class="flex flex-col items-center ml-auto mr-0 text-sm font-bold leading-tight"
|
||||
class="flex items-center ml-auto mr-0 gap-1 text-sm font-bold leading-tight"
|
||||
>
|
||||
<img src=${allianceIcon} width="20" height="20" />
|
||||
${this.allianceExpirationText(alliance)}
|
||||
@@ -293,9 +328,11 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
<!-- Right: Player identity + Units below -->
|
||||
<div class="flex flex-col justify-between self-stretch">
|
||||
<div
|
||||
class="flex items-center gap-2 font-bold text-sm lg:text-lg ${isFriendly
|
||||
? "text-green-500"
|
||||
: "text-white"}"
|
||||
class="flex items-center gap-2 font-bold text-sm lg:text-lg ${this.getPlayerNameColor(
|
||||
player,
|
||||
myPlayer,
|
||||
isFriendly ?? false,
|
||||
)}"
|
||||
>
|
||||
${player.cosmetics.flag
|
||||
? player.cosmetics.flag!.startsWith("!")
|
||||
@@ -452,11 +489,12 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 lg:top-4 left-0 right-0 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-[1001]"
|
||||
class="fixed top-0 min-[1200px]:top-4 left-0 right-0 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-[1001]"
|
||||
style="margin-top: ${this.barOffset}px;"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/70 backdrop-blur-xs shadow-xs lg:rounded-lg shadow-lg transition-all duration-300 text-white text-lg lg:text-base w-full sm:w-auto sm:min-w-[400px] overflow-hidden ${containerClasses}"
|
||||
class="bg-gray-800/70 backdrop-blur-xs shadow-xs min-[1200px]:rounded-lg sm:rounded-b-lg shadow-lg transition-all duration-300 text-white text-lg lg:text-base w-full sm:w-auto sm:min-w-[400px] overflow-hidden ${containerClasses}"
|
||||
>
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="p-2 bg-gray-800/70 backdrop-blur-xs shadow-xs rounded-lg"
|
||||
class="p-2 bg-gray-800/70 backdrop-blur-xs shadow-xs min-[1200px]:rounded-lg rounded-l-lg"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<label class="block mb-2 text-white" translate="no">
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { GameMode, Team } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class SpawnBarVisibleEvent implements GameEvent {
|
||||
constructor(public readonly visible: boolean) {}
|
||||
}
|
||||
|
||||
@customElement("spawn-timer")
|
||||
export class SpawnTimer extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
public transformHandler: TransformHandler;
|
||||
|
||||
private ratios = [0];
|
||||
private _barVisible = false;
|
||||
private colors = ["rgba(0, 128, 255, 0.7)", "rgba(0, 0, 0, 0.5)"];
|
||||
|
||||
private isVisible = false;
|
||||
@@ -37,39 +44,41 @@ export class SpawnTimer extends LitElement implements Layer {
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns(),
|
||||
];
|
||||
this.colors = ["rgba(0, 128, 255, 0.7)"];
|
||||
this.requestUpdate();
|
||||
return;
|
||||
} else {
|
||||
this.ratios = [];
|
||||
this.colors = [];
|
||||
|
||||
if (this.game.config().gameConfig().gameMode === GameMode.Team) {
|
||||
const teamTiles: Map<Team, number> = new Map();
|
||||
for (const player of this.game.players()) {
|
||||
const team = player.team();
|
||||
if (team === null) continue;
|
||||
const tiles = teamTiles.get(team) ?? 0;
|
||||
teamTiles.set(team, tiles + player.numTilesOwned());
|
||||
}
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total > 0) {
|
||||
for (const [team, count] of teamTiles) {
|
||||
const ratio = count / total;
|
||||
this.ratios.push(ratio);
|
||||
this.colors.push(theme.teamColor(team).toRgbString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ratios = [];
|
||||
this.colors = [];
|
||||
|
||||
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
const teamTiles: Map<Team, number> = new Map();
|
||||
for (const player of this.game.players()) {
|
||||
const team = player.team();
|
||||
if (team === null) throw new Error("Team is null");
|
||||
const tiles = teamTiles.get(team) ?? 0;
|
||||
teamTiles.set(team, tiles + player.numTilesOwned());
|
||||
}
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total === 0) {
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [team, count] of teamTiles) {
|
||||
const ratio = count / total;
|
||||
this.ratios.push(ratio);
|
||||
this.colors.push(theme.teamColor(team).toRgbString());
|
||||
}
|
||||
this.requestUpdate();
|
||||
this.emitBarVisibility();
|
||||
}
|
||||
|
||||
private emitBarVisibility() {
|
||||
const nowVisible = this.isVisible && this.ratios.length > 0;
|
||||
if (nowVisible !== this._barVisible) {
|
||||
this._barVisible = nowVisible;
|
||||
this.eventBus?.emit(new SpawnBarVisibleEvent(this._barVisible));
|
||||
}
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
|
||||
@@ -132,7 +132,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="max-h-[30vh] overflow-y-auto grid bg-slate-800/70 w-full text-white text-xs md:text-sm mt-2"
|
||||
class="max-h-[30vh] overflow-x-hidden overflow-y-auto grid bg-slate-800/85 w-full text-white text-xs md:text-sm mt-2 rounded-lg"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
@@ -140,7 +140,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
style="--cols:${this.showUnits ? 5 : 4};"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="contents font-bold bg-slate-700/50">
|
||||
<div class="contents font-bold bg-slate-700/60">
|
||||
<div class="p-1.5 md:p-2.5 text-center border-b border-slate-500">
|
||||
${translateText("leaderboard.team")}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user