mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:30:43 +00:00
update ui (#1368)
## Description: Center pop & gold information, and split up & remove the top bar. Before: <img width="1913" alt="Screenshot 2025-07-07 at 7 54 55 PM" src="https://github.com/user-attachments/assets/8ab59102-1f0e-4023-905f-e0b1b2fa73a2" /> After: <img width="1916" alt="Screenshot 2025-07-07 at 7 53 02 PM" src="https://github.com/user-attachments/assets/06c885e3-a179-4447-9d1d-0da3849e726d" /> reference:  ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
@@ -77,7 +77,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
render() {
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-[90px] left-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg transition-transform duration-300 ease-out transform ${
|
||||
class=${`fixed top-[20px] left-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg transition-transform duration-300 ease-out transform ${
|
||||
this.isVisible ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -5,16 +5,21 @@ import pauseIcon from "../../../../resources/images/PauseIconWhite.svg";
|
||||
import playIcon from "../../../../resources/images/PlayIconWhite.svg";
|
||||
import replayRegularIcon from "../../../../resources/images/ReplayRegularIconWhite.svg";
|
||||
import replaySolidIcon from "../../../../resources/images/ReplaySolidIconWhite.svg";
|
||||
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { PauseGameEvent } from "../../Transport";
|
||||
import { Layer } from "./Layer";
|
||||
import { ShowReplayPanelEvent } from "./ReplayPanel";
|
||||
import { ShowSettingsModalEvent } from "./SettingsModal";
|
||||
|
||||
@customElement("game-right-sidebar")
|
||||
export class GameRightSidebar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
|
||||
@state()
|
||||
private _isSinglePlayer: boolean = false;
|
||||
|
||||
@@ -28,7 +33,9 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
private isPaused: boolean = false;
|
||||
|
||||
@state()
|
||||
private isExistButtonVisible: boolean = true;
|
||||
private timer: number = 0;
|
||||
|
||||
private hasWinner = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -44,13 +51,31 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
this.isExistButtonVisible = false;
|
||||
// Timer logic
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = 0;
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
|
||||
this.timer++;
|
||||
}
|
||||
}
|
||||
|
||||
private secondsToHms = (d: number): string => {
|
||||
const h = Math.floor(d / 3600);
|
||||
const m = Math.floor((d % 3600) / 60);
|
||||
const s = Math.floor((d % 3600) % 60);
|
||||
let time = d === 0 ? "-" : `${s}s`;
|
||||
if (m > 0) time = `${m}m` + time;
|
||||
if (h > 0) time = `${h}h` + time;
|
||||
return time;
|
||||
};
|
||||
|
||||
private toggleReplayPanel(): void {
|
||||
this._isReplayVisible = !this._isReplayVisible;
|
||||
this.eventBus.emit(new ShowReplayPanelEvent(this._isReplayVisible));
|
||||
}
|
||||
|
||||
private onPauseButtonClick() {
|
||||
@@ -68,10 +93,16 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
private onSettingsButtonClick() {
|
||||
this.eventBus.emit(new ShowSettingsModalEvent(true));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.game === undefined) return html``;
|
||||
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-[90px] right-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tl-lg rounded-bl-lg transition-transform duration-300 ease-out transform ${
|
||||
class=${`flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-tl-lg rounded-bl-lg transition-transform duration-300 ease-out transform ${
|
||||
this._isVisible ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
@@ -81,59 +112,60 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
this._isReplayVisible ? "mb-2" : ""
|
||||
}`}
|
||||
>
|
||||
${this._isSinglePlayer || this.game?.config()?.isReplay()
|
||||
? html`
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.toggleReplayPanel}
|
||||
>
|
||||
<img
|
||||
src=${this._isReplayVisible
|
||||
? replaySolidIcon
|
||||
: replayRegularIcon}
|
||||
alt="replay"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.onPauseButtonClick}
|
||||
>
|
||||
<img
|
||||
src=${this.isPaused ? playIcon : pauseIcon}
|
||||
alt="play/pause"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>
|
||||
${this.isExistButtonVisible
|
||||
? html`
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.onExitButtonClick}
|
||||
>
|
||||
<img
|
||||
src=${exitIcon}
|
||||
alt="exit"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
`
|
||||
: null}
|
||||
${this.maybeRenderReplayButtons()}
|
||||
<div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.onSettingsButtonClick}
|
||||
>
|
||||
<img
|
||||
src=${settingsIcon}
|
||||
alt="settings"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-6 h-6 cursor-pointer" @click=${this.onExitButtonClick}>
|
||||
<img src=${exitIcon} alt="exit" width="20" height="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="block lg:flex flex-wrap gap-2">
|
||||
<replay-panel
|
||||
.isSingleplayer="${this._isSinglePlayer}"
|
||||
.visible="${this._isReplayVisible}"
|
||||
></replay-panel>
|
||||
<!-- Timer display below buttons -->
|
||||
<div class="flex justify-center items-center mt-2">
|
||||
<div
|
||||
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center justify-center text-white px-1"
|
||||
>
|
||||
${this.secondsToHms(this.timer)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
|
||||
maybeRenderReplayButtons() {
|
||||
if (this._isSinglePlayer || this.game?.config()?.isReplay()) {
|
||||
return html` <div
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.toggleReplayPanel}
|
||||
>
|
||||
<img
|
||||
src=${this._isReplayVisible ? replaySolidIcon : replayRegularIcon}
|
||||
alt="replay"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-6 h-6 cursor-pointer" @click=${this.onPauseButtonClick}>
|
||||
<img
|
||||
src=${this.isPaused ? playIcon : pauseIcon}
|
||||
alt="play/pause"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
</div>`;
|
||||
} else {
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
||||
import populationIcon from "../../../../resources/images/PopulationIconSolidWhite.svg";
|
||||
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
|
||||
import troopIcon from "../../../../resources/images/TroopIconWhite.svg";
|
||||
import workerIcon from "../../../../resources/images/WorkerIconWhite.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
@@ -11,7 +10,6 @@ import { GameView } from "../../../core/game/GameView";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
import { ShowSettingsModalEvent } from "./SettingsModal";
|
||||
|
||||
@customElement("game-top-bar")
|
||||
export class GameTopBar extends LitElement implements Layer {
|
||||
@@ -25,9 +23,6 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
private _popRateIsIncreasing = false;
|
||||
private hasWinner = false;
|
||||
|
||||
@state()
|
||||
private timer: number = 0;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -46,18 +41,9 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = 0;
|
||||
} else if (!this.hasWinner && this.game.ticks() % 10 === 0) {
|
||||
this.timer++;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onSettingsButtonClick() {
|
||||
this.eventBus.emit(new ShowSettingsModalEvent(true));
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
@@ -69,16 +55,6 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private secondsToHms = (d: number): string => {
|
||||
const h = Math.floor(d / 3600);
|
||||
const m = Math.floor((d % 3600) / 60);
|
||||
const s = Math.floor((d % 3600) % 60);
|
||||
let time = d === 0 ? "-" : `${s}s`;
|
||||
if (m > 0) time = `${m}m` + time;
|
||||
if (h > 0) time = `${h}h` + time;
|
||||
return time;
|
||||
};
|
||||
|
||||
render() {
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!this.game || !myPlayer || this.game.inSpawnPhase()) {
|
||||
@@ -89,14 +65,8 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
if (isAlt) {
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 left-auto right-0 z-[1100] bg-slate-800/40 backdrop-blur-sm p-2 flex justify-center items-center"
|
||||
>
|
||||
<div
|
||||
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center text-white px-1"
|
||||
>
|
||||
${this.secondsToHms(this.timer)}
|
||||
</div>
|
||||
</div>
|
||||
class="absolute top-4 left-1/2 transform -translate-x-1/2 flex justify-center items-center p-2"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
const popRate = myPlayer
|
||||
@@ -109,19 +79,17 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 min-h-[50px] lg:min-h-[80px] z-[1100] flex flex-wrap bg-slate-800/40 backdrop-blur-sm shadow-xs text-white text-xs md:text-sm lg:text-base left-0 right-0 grid-cols-4 p-1 md:px-1.5 lg:px-4"
|
||||
class="absolute top-4 left-1/2 transform -translate-x-1/2 flex justify-center items-center p-1 md:px-1.5 lg:px-4 z-[1100]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-1 basis-full justify-between items-center gap-1 w-full"
|
||||
>
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
${myPlayer?.isAlive() && !this.game.inSpawnPhase()
|
||||
? html`
|
||||
<div class="overflow-x-auto hide-scrollbar flex-1 max-w-[85vw]">
|
||||
<div class="overflow-x-auto hide-scrollbar">
|
||||
<div
|
||||
class="grid gap-1 grid-cols-[80px_100px_80px] w-max md:gap-2 md:grid-cols-[90px_120px_90px]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap gap-1 flex-col bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
class="flex flex-wrap gap-1 flex-col bg-gray-800/70 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
@@ -131,12 +99,16 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
+${renderNumber(goldPerSecond)}
|
||||
<span class="text-white"
|
||||
>+${renderNumber(goldPerSecond)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-white">
|
||||
${renderNumber(myPlayer.gold())}
|
||||
</div>
|
||||
<div>${renderNumber(myPlayer.gold())}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap gap-1 flex-col bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
class="flex flex-wrap gap-1 flex-col bg-gray-800/70 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
@@ -155,13 +127,13 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
+${renderTroops(popRate)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-white">
|
||||
${renderTroops(myPlayer.population())} /
|
||||
${renderTroops(maxPop)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
class="flex bg-gray-800/70 border border-slate-400 p-0.5 md:px-1 lg:px-2"
|
||||
>
|
||||
<div class="flex flex-col flex-grow gap-1 w-full ">
|
||||
<div class="flex gap-1">
|
||||
@@ -172,7 +144,9 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderTroops(this._troops)}
|
||||
<span class="text-white"
|
||||
>${renderTroops(this._troops)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
@@ -182,7 +156,9 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderTroops(this._workers)}
|
||||
<span class="text-white"
|
||||
>${renderTroops(this._workers)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,22 +166,6 @@ export class GameTopBar extends LitElement implements Layer {
|
||||
</div>
|
||||
`
|
||||
: html`<div></div>`}
|
||||
<div class="flex gap-1 items-center">
|
||||
<div
|
||||
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center px-1"
|
||||
>
|
||||
${this.secondsToHms(this.timer)}
|
||||
</div>
|
||||
<img
|
||||
class="cursor-pointer bg-slate-800/20 border border-slate-400 p-0.5"
|
||||
src=${settingsIcon}
|
||||
alt="settings"
|
||||
width="28"
|
||||
height="28"
|
||||
style="vertical-align: middle;"
|
||||
@click=${this.onSettingsButtonClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -193,10 +193,10 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="grid bg-slate-800/70 w-full text-xs md:text-sm lg:text-base"
|
||||
class="grid bg-gray-800/70 w-full text-xs md:text-sm lg:text-base"
|
||||
style="grid-template-columns: 35px 100px 85px 65px 65px;"
|
||||
>
|
||||
<div class="contents font-bold bg-slate-700/50">
|
||||
<div class="contents font-bold bg-gray-700/50">
|
||||
<div class="py-1.5 md:py-2.5 text-center border-b border-slate-500">
|
||||
#
|
||||
</div>
|
||||
|
||||
@@ -356,7 +356,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
class="bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
>
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
import { translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class ShowReplayPanelEvent {
|
||||
constructor(public visible: boolean = true) {}
|
||||
}
|
||||
|
||||
@customElement("replay-panel")
|
||||
export class ReplayPanel extends LitElement implements Layer {
|
||||
public game: GameView | undefined;
|
||||
@@ -28,7 +32,13 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
return this; // Enable Tailwind CSS
|
||||
}
|
||||
|
||||
init() {}
|
||||
init() {
|
||||
if (this.eventBus) {
|
||||
this.eventBus.on(ShowReplayPanelEvent, (event: ShowReplayPanelEvent) => {
|
||||
this.visible = event.visible;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.visible) return;
|
||||
@@ -52,7 +62,7 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-es-sm lg:rounded-lg backdrop-blur-md"
|
||||
class="flex-shrink-0 bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-es-sm lg:rounded-lg backdrop-blur-md"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<label class="block mb-1 text-white" translate="no">
|
||||
|
||||
@@ -91,7 +91,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] bg-slate-800/40 backdrop-blur-sm border border-slate-400 rounded-lg p-2 hidden lg:block"
|
||||
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] bg-gray-800/70 backdrop-blur-sm border border-slate-400 rounded-lg p-2 hidden lg:block"
|
||||
>
|
||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
|
||||
${this.renderUnitItem(cityIcon, this._cities, UnitType.City, "city")}
|
||||
|
||||
@@ -361,7 +361,10 @@
|
||||
<game-starting-modal></game-starting-modal>
|
||||
<game-top-bar></game-top-bar>
|
||||
<unit-display></unit-display>
|
||||
<game-right-sidebar></game-right-sidebar>
|
||||
<div class="flex fixed top-[20px] right-[20px] z-[1000] items-start gap-2">
|
||||
<replay-panel></replay-panel>
|
||||
<game-right-sidebar></game-right-sidebar>
|
||||
</div>
|
||||
<settings-modal></settings-modal>
|
||||
<player-panel></player-panel>
|
||||
<help-modal></help-modal>
|
||||
|
||||
Reference in New Issue
Block a user