mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 04:03:35 +00:00
Better In Game UI (#1243)
## Description: Top Bar Refactor – UI & UX Improvement Proposal This update overhauls the top game bar to improve clarity, responsiveness, and overall user experience across desktop and mobile. It consolidates player resources (e.g., building counts), integrates game controls (pause, settings, time), and enhances visual contrast. Key changes: Redesigned top bar with player data and game options. Team color indicator bar (team games only). Countdown bar during "Choose Starting Position" phase. Removed redundant info (e.g., troop/worker counts shown elsewhere). Inspired by strategy games like Travian Legends, this refactor offers a cleaner and more intuitive layout, especially for smaller screens. ⚠️ Note: This is a large change and likely contains visual or functional bugs I can’t currently spot due to fatigue. Thorough testing is required before approval. ## 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 - [ ] 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: Diessel  om/user-attachments/assets/3a0edbef-e621-4fc4-b6b7-c1ed  8f9a8219)  Closes #1165 --------- Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com> Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
+1
-1
@@ -366,7 +366,7 @@ class Client {
|
||||
"host-lobby-modal",
|
||||
"join-private-lobby-modal",
|
||||
"game-starting-modal",
|
||||
"top-bar",
|
||||
"game-top-bar",
|
||||
"help-modal",
|
||||
"user-setting",
|
||||
].forEach((tag) => {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { EmojiTable } from "./layers/EmojiTable";
|
||||
import { EventsDisplay } from "./layers/EventsDisplay";
|
||||
import { FxLayer } from "./layers/FxLayer";
|
||||
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
||||
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
||||
import { GameTopBar } from "./layers/GameTopBar";
|
||||
import { GutterAdModal } from "./layers/GutterAdModal";
|
||||
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
||||
import { Layer } from "./layers/Layer";
|
||||
@@ -21,7 +23,6 @@ import { Leaderboard } from "./layers/Leaderboard";
|
||||
import { MainRadialMenu } from "./layers/MainRadialMenu";
|
||||
import { MultiTabModal } from "./layers/MultiTabModal";
|
||||
import { NameLayer } from "./layers/NameLayer";
|
||||
import { OptionsMenu } from "./layers/OptionsMenu";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { RailroadLayer } from "./layers/RailroadLayer";
|
||||
@@ -33,7 +34,6 @@ import { StructureLayer } from "./layers/StructureLayer";
|
||||
import { TeamStats } from "./layers/TeamStats";
|
||||
import { TerrainLayer } from "./layers/TerrainLayer";
|
||||
import { TerritoryLayer } from "./layers/TerritoryLayer";
|
||||
import { TopBar } from "./layers/TopBar";
|
||||
import { UILayer } from "./layers/UILayer";
|
||||
import { UnitInfoModal } from "./layers/UnitInfoModal";
|
||||
import { UnitLayer } from "./layers/UnitLayer";
|
||||
@@ -135,25 +135,28 @@ export function createRenderer(
|
||||
winModal.eventBus = eventBus;
|
||||
winModal.game = game;
|
||||
|
||||
const optionsMenu = document.querySelector("options-menu") as OptionsMenu;
|
||||
if (!(optionsMenu instanceof OptionsMenu)) {
|
||||
console.error("options menu not found");
|
||||
}
|
||||
optionsMenu.eventBus = eventBus;
|
||||
optionsMenu.game = game;
|
||||
|
||||
const replayPanel = document.querySelector("replay-panel") as ReplayPanel;
|
||||
if (!(replayPanel instanceof ReplayPanel)) {
|
||||
console.error("ReplayPanel element not found in the DOM");
|
||||
console.error("replay panel not found");
|
||||
}
|
||||
replayPanel.eventBus = eventBus;
|
||||
replayPanel.game = game;
|
||||
|
||||
const topBar = document.querySelector("top-bar") as TopBar;
|
||||
if (!(topBar instanceof TopBar)) {
|
||||
const gameRightSidebar = document.querySelector(
|
||||
"game-right-sidebar",
|
||||
) as GameRightSidebar;
|
||||
if (!(gameRightSidebar instanceof GameRightSidebar)) {
|
||||
console.error("Game Right bar not found");
|
||||
}
|
||||
gameRightSidebar.game = game;
|
||||
gameRightSidebar.eventBus = eventBus;
|
||||
|
||||
const gameTopBar = document.querySelector("game-top-bar") as GameTopBar;
|
||||
if (!(gameTopBar instanceof GameTopBar)) {
|
||||
console.error("top bar not found");
|
||||
}
|
||||
topBar.game = game;
|
||||
gameTopBar.game = game;
|
||||
gameTopBar.eventBus = eventBus;
|
||||
|
||||
const playerPanel = document.querySelector("player-panel") as PlayerPanel;
|
||||
if (!(playerPanel instanceof PlayerPanel)) {
|
||||
@@ -248,13 +251,13 @@ export function createRenderer(
|
||||
new SpawnTimer(game, transformHandler),
|
||||
leaderboard,
|
||||
gameLeftSidebar,
|
||||
gameTopBar,
|
||||
gameRightSidebar,
|
||||
controlPanel,
|
||||
playerInfo,
|
||||
winModal,
|
||||
optionsMenu,
|
||||
replayPanel,
|
||||
teamStats,
|
||||
topBar,
|
||||
playerPanel,
|
||||
headsUpMessage,
|
||||
unitInfoModal,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("leaderboard-regular-icon")
|
||||
export class LeaderboardRegularIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 19h4v-8H4zm6 0h4V5h-4zm6 0h4v-6h-4zM2 21V9h6V3h8v8h6v10z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("leaderboard-solid-icon")
|
||||
export class LeaderboardSolidIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 21V9h5.5v12zm7.25 0V3h5.5v18zm7.25 0V11H22v10z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("team-regular-icon")
|
||||
export class TeamRegularIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5M4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5S5.5 6.57 5.5 8.5S7.07 12 9 12m0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7m7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44M15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35c.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("team-solid-icon")
|
||||
export class TeamSolidIcon extends LitElement {
|
||||
@property({ type: String }) size = "24"; // Accepts "24", "32", etc.
|
||||
@property({ type: String }) color = "currentColor";
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="${this.size}"
|
||||
height="${this.size}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="${this.color}"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3s1.34 3 3 3m-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5S5 6.34 5 8s1.34 3 3 3m0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5m8 0c-.29 0-.62.02-.97.05c1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Gold, UnitType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { AttackRatioEvent } from "../../InputHandler";
|
||||
import { SendSetTargetTroopRatioEvent } from "../../Transport";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { renderTroops } from "../../Utils";
|
||||
import { UIState } from "../UIState";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@@ -29,37 +27,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
@state()
|
||||
private _population: number;
|
||||
|
||||
@state()
|
||||
private _maxPopulation: number;
|
||||
|
||||
@state()
|
||||
private popRate: number;
|
||||
|
||||
@state()
|
||||
private _troops: number;
|
||||
|
||||
@state()
|
||||
private _workers: number;
|
||||
|
||||
@state()
|
||||
private _isVisible = false;
|
||||
|
||||
@state()
|
||||
private _manpower: number = 0;
|
||||
|
||||
@state()
|
||||
private _gold: Gold;
|
||||
|
||||
@state()
|
||||
private _goldPerSecond: Gold;
|
||||
|
||||
@state()
|
||||
private _factories: number;
|
||||
|
||||
private _lastPopulationIncreaseRate: number;
|
||||
|
||||
private _popRateIsIncreasing: boolean = true;
|
||||
|
||||
private init_: boolean = false;
|
||||
|
||||
init() {
|
||||
@@ -118,22 +92,13 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
|
||||
this._population = player.population();
|
||||
this._maxPopulation = this.game.config().maxPopulation(player);
|
||||
this._gold = player.gold();
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
|
||||
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10n;
|
||||
|
||||
this.currentTroopRatio = player.troops() / player.population();
|
||||
this.requestUpdate();
|
||||
this._factories = player.units(UnitType.Factory).length;
|
||||
}
|
||||
|
||||
onAttackRatioChange(newRatio: number) {
|
||||
@@ -209,51 +174,21 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
</style>
|
||||
<div
|
||||
class="${this._isVisible
|
||||
? "w-[320px] text-sm lg:text-m bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
||||
? "text-sm lg:text-m md:w-[320px] bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg lg:rounded-lg backdrop-blur"
|
||||
: "hidden"}"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div class="hidden lg:block bg-black/30 text-white mb-4 p-2 rounded">
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.pop")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderTroops(this._population)} /
|
||||
${renderTroops(this._maxPopulation)}
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>(+${renderTroops(this.popRate)})</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold"
|
||||
>${translateText("control_panel.gold")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderNumber(this._gold)}
|
||||
(+${renderNumber(this._goldPerSecond)}
|
||||
${renderNumber(this._factories)}
|
||||
<img
|
||||
src="${factoryIcon}"
|
||||
style="display: inline"
|
||||
width="15"
|
||||
/>)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-4 lg:mb-4">
|
||||
<label class="block text-white mb-1" translate="no"
|
||||
>${translateText("control_panel.troops")}:
|
||||
<span translate="no">${renderTroops(this._troops)}</span> |
|
||||
${translateText("control_panel.workers")}:
|
||||
<span translate="no">${renderTroops(this._workers)}</span></label
|
||||
>
|
||||
<label class="flex justify-between text-white mb-1" translate="no">
|
||||
<span>
|
||||
${translateText("control_panel.troops")}:
|
||||
${(this.currentTroopRatio * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span>
|
||||
${translateText("control_panel.workers")}:
|
||||
${((1 - this.currentTroopRatio) * 100).toFixed(0)}%
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative h-8">
|
||||
<!-- Background track -->
|
||||
<div
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Colord } from "colord";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import leaderboardRegularIcon from "../../../../resources/images/LeaderboardIconRegularWhite.svg";
|
||||
import leaderboardSolidIcon from "../../../../resources/images/LeaderboardIconSolidWhite.svg";
|
||||
import teamRegularIcon from "../../../../resources/images/TeamIconRegularWhite.svg";
|
||||
import teamSolidIcon from "../../../../resources/images/TeamIconSolidWhite.svg";
|
||||
import { GameMode } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { translateText } from "../../Utils";
|
||||
import "../icons/LeaderboardRegularIcon";
|
||||
import "../icons/LeaderboardSolidIcon";
|
||||
import "../icons/TeamRegularIcon";
|
||||
import "../icons/TeamSolidIcon";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-left-sidebar")
|
||||
@@ -77,7 +77,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
render() {
|
||||
return html`
|
||||
<aside
|
||||
class=${`fixed top-[50px] lg:top-[10px] 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-[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 ${
|
||||
this.isVisible ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
@@ -94,11 +94,20 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div class="flex items-center gap-2 space-x-2 text-white mb-2">
|
||||
<div
|
||||
class=${`flex items-center gap-2 space-x-2 text-white ${
|
||||
this.isLeaderboardShow || this.isTeamLeaderboardShow ? "mb-2" : ""
|
||||
}`}
|
||||
>
|
||||
<div class="w-6 h-6 cursor-pointer" @click=${this.toggleLeaderboard}>
|
||||
${this.isLeaderboardShow
|
||||
? html` <leaderboard-solid-icon></leaderboard-solid-icon>`
|
||||
: html` <leaderboard-regular-icon></leaderboard-regular-icon>`}
|
||||
<img
|
||||
src=${this.isLeaderboardShow
|
||||
? leaderboardSolidIcon
|
||||
: leaderboardRegularIcon}
|
||||
alt="treeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
${this.isTeamGame
|
||||
? html`
|
||||
@@ -106,9 +115,14 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
class="w-6 h-6 cursor-pointer"
|
||||
@click=${this.toggleTeamLeaderboard}
|
||||
>
|
||||
${this.isTeamLeaderboardShow
|
||||
? html` <team-solid-icon></team-solid-icon>`
|
||||
: html` <team-regular-icon></team-regular-icon>`}
|
||||
<img
|
||||
src=${this.isTeamLeaderboardShow
|
||||
? teamSolidIcon
|
||||
: teamRegularIcon}
|
||||
alt="treeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
|
||||
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 { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { PauseGameEvent } from "../../Transport";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-right-sidebar")
|
||||
export class GameRightSidebar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
@state()
|
||||
private _isSinglePlayer: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isReplayVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isVisible: boolean = true;
|
||||
|
||||
@state()
|
||||
private isPaused: boolean = false;
|
||||
|
||||
@state()
|
||||
private isExistButtonVisible: boolean = true;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this._isSinglePlayer =
|
||||
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
|
||||
this.game.config().isReplay();
|
||||
this._isVisible = true;
|
||||
this.game.inSpawnPhase();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
this.isExistButtonVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private toggleReplayPanel(): void {
|
||||
this._isReplayVisible = !this._isReplayVisible;
|
||||
}
|
||||
|
||||
private onPauseButtonClick() {
|
||||
this.isPaused = !this.isPaused;
|
||||
this.eventBus.emit(new PauseGameEvent(this.isPaused));
|
||||
}
|
||||
|
||||
private onExitButtonClick() {
|
||||
const isAlive = this.game.myPlayer()?.isAlive();
|
||||
if (isAlive) {
|
||||
const isConfirmed = confirm("Are you sure you want to exit the game?");
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
render() {
|
||||
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 ${
|
||||
this._isVisible ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class=${`flex justify-end items-center gap-2 text-white ${
|
||||
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}
|
||||
</div>
|
||||
<div class="block lg:flex flex-wrap gap-2">
|
||||
<replay-panel
|
||||
.isSingleplayer="${this._isSinglePlayer}"
|
||||
.visible="${this._isReplayVisible}"
|
||||
></replay-panel>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
|
||||
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
|
||||
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
|
||||
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
|
||||
import explosionIcon from "../../../../resources/images/ExplosionIconWhite.svg";
|
||||
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
|
||||
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
|
||||
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
|
||||
import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
|
||||
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
|
||||
import populationIcon from "../../../../resources/images/PopulationIconSolidWhite.svg";
|
||||
import portIcon from "../../../../resources/images/PortIcon.svg";
|
||||
import samLauncherIcon from "../../../../resources/images/SamLauncherUnitWhite.png";
|
||||
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
|
||||
import defensePostIcon from "../../../../resources/images/ShieldIconWhite.svg";
|
||||
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
|
||||
import troopIcon from "../../../../resources/images/TroopIconWhite.svg";
|
||||
import workerIcon from "../../../../resources/images/WorkerIconWhite.svg";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("game-top-bar")
|
||||
export class GameTopBar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private _userSettings: UserSettings = new UserSettings();
|
||||
private _population = 0;
|
||||
private _troops = 0;
|
||||
private _cities = 0;
|
||||
private _factories = 0;
|
||||
private _workers = 0;
|
||||
private _missileSilo = 0;
|
||||
private _port = 0;
|
||||
private _defensePost = 0;
|
||||
private _samLauncher = 0;
|
||||
private _lastPopulationIncreaseRate = 0;
|
||||
private _popRateIsIncreasing = false;
|
||||
private hasWinner = false;
|
||||
|
||||
@state()
|
||||
private showSettingsMenu = false;
|
||||
@state()
|
||||
private alternateView: boolean = false;
|
||||
|
||||
@state()
|
||||
private timer: number = 0;
|
||||
|
||||
@query(".settings-container")
|
||||
private settingsContainer!: HTMLElement;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.updatePopulationIncrease();
|
||||
const player = this.game?.myPlayer();
|
||||
if (!player) return;
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
this._cities = player.totalUnitLevels(UnitType.City);
|
||||
this._missileSilo = player.totalUnitLevels(UnitType.MissileSilo);
|
||||
this._port = player.totalUnitLevels(UnitType.Port);
|
||||
this._defensePost = player.totalUnitLevels(UnitType.DefensePost);
|
||||
this._samLauncher = player.totalUnitLevels(UnitType.SAMLauncher);
|
||||
this._factories = player.totalUnitLevels(UnitType.Factory);
|
||||
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++;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("click", this.handleOutsideClick, true);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("click", this.handleOutsideClick, true);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
private handleOutsideClick = (event: MouseEvent) => {
|
||||
if (
|
||||
this.showSettingsMenu &&
|
||||
this.settingsContainer &&
|
||||
!this.settingsContainer.contains(event.target as Node)
|
||||
) {
|
||||
this.showSettingsMenu = false;
|
||||
}
|
||||
};
|
||||
|
||||
private onExitButtonClick() {
|
||||
const isAlive = this.game.myPlayer()?.isAlive();
|
||||
if (isAlive) {
|
||||
const isConfirmed = confirm("Are you sure you want to exit the game?");
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
private onTerrainButtonClick() {
|
||||
this.alternateView = !this.alternateView;
|
||||
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleEmojisButtonClick() {
|
||||
this._userSettings.toggleEmojis();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleSpecialEffectsButtonClick() {
|
||||
this._userSettings.toggleFxLayer();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onToggleDarkModeButtonClick() {
|
||||
this._userSettings.toggleDarkMode();
|
||||
this.requestUpdate();
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
|
||||
private onToggleRandomNameModeButtonClick() {
|
||||
this._userSettings.toggleRandomName();
|
||||
}
|
||||
private onToggleLeftClickOpensMenu() {
|
||||
this._userSettings.toggleLeftClickOpenMenu();
|
||||
}
|
||||
|
||||
private toggleSettingsMenu() {
|
||||
this.showSettingsMenu = !this.showSettingsMenu;
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAlt = this.game.config().isReplay();
|
||||
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>
|
||||
`;
|
||||
}
|
||||
const popRate = myPlayer
|
||||
? this.game.config().populationIncreaseRate(myPlayer) * 10
|
||||
: 0;
|
||||
const maxPop = myPlayer ? this.game.config().maxPopulation(myPlayer) : 0;
|
||||
const goldPerSecond = myPlayer
|
||||
? this.game.config().goldAdditionRate(myPlayer) * 10n
|
||||
: 0n;
|
||||
|
||||
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"
|
||||
>
|
||||
<div
|
||||
class="flex flex-1 basis-full justify-between items-center gap-1 w-full"
|
||||
>
|
||||
${myPlayer?.isAlive() && !this.game.inSpawnPhase()
|
||||
? html`
|
||||
<div class="overflow-x-auto hide-scrollbar flex-1 max-w-[85vw]">
|
||||
<div
|
||||
class="grid gap-1 grid-cols-[80px_100px_80px_minmax(80px,auto)] w-max md:gap-2 md:grid-cols-[90px_120px_90px_minmax(100px,auto)]"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
+${renderNumber(goldPerSecond)}
|
||||
</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"
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<img
|
||||
src=${populationIcon}
|
||||
alt="population"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
<span
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
translate="no"
|
||||
>
|
||||
+${renderTroops(popRate)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
${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"
|
||||
>
|
||||
<div class="flex flex-col flex-grow gap-1 w-full ">
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
src=${troopIcon}
|
||||
alt="troops"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderTroops(this._troops)}
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img
|
||||
src=${workerIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderTroops(this._workers)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 bg-slate-800/20 border border-slate-400 p-0.5 md:px-1 lg:px-2 md:gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${cityIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._cities)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${factoryIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._factories)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${portIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._port)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${defensePostIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._defensePost)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${missileSiloIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._missileSilo)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${samLauncherIcon}
|
||||
alt="gold"
|
||||
width="20"
|
||||
height="20"
|
||||
style="vertical-align: middle;"
|
||||
/>
|
||||
${renderNumber(this._samLauncher)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div class="relative settings-container">
|
||||
<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.toggleSettingsMenu}
|
||||
/>
|
||||
${this.showSettingsMenu
|
||||
? html`
|
||||
<div
|
||||
class="absolute right-0 mt-1.5 bg-slate-700 border border-slate-500 rounded shadow-lg z-[1100] w-max min-w-[10rem] whitespace-nowrap"
|
||||
>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onTerrainButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${treeIcon}
|
||||
alt="treeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
Toggle Terrain ${this.alternateView ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleEmojisButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${emojiIcon}
|
||||
alt="emojiIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.emojis_label")}
|
||||
${this._userSettings.emojis() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleDarkModeButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${darkModeIcon}
|
||||
alt="darkModeIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.dark_mode_label")}
|
||||
${this._userSettings.darkMode() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleSpecialEffectsButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${explosionIcon}
|
||||
alt="onExitButtonClick"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.special_effects_label")}
|
||||
${this._userSettings.fxLayer() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleRandomNameModeButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${ninjaIcon}
|
||||
alt="ninjaIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
${translateText("user_setting.anonymous_names_label")}
|
||||
${this._userSettings.anonymousNames() ? "On" : "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onToggleLeftClickOpensMenu}"
|
||||
>
|
||||
<img
|
||||
src=${mouseIcon}
|
||||
alt="mouseIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
Left click
|
||||
${this._userSettings.leftClickOpensMenu()
|
||||
? "On"
|
||||
: "Off"}
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-1 items-center w-full text-left px-2 py-1 hover:bg-slate-600 text-white text-sm"
|
||||
@click="${this.onExitButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${exitIcon}
|
||||
alt="exitIcon"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
Exit game
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,8 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex items-center
|
||||
w-full justify-evenly h-8 lg:h-10 top-0 lg:top-4 left-0 lg:left-4
|
||||
class="flex items-center relative
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
|
||||
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
|
||||
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
|
||||
@@ -352,11 +352,11 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex w-full z-50 flex-col"
|
||||
class="hidden lg:flex fixed top-[245px] right-0 w-full z-50 flex-col max-w-[180px]"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
|
||||
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}"
|
||||
>
|
||||
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
|
||||
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { ReplaySpeedChangeEvent } from "../../InputHandler";
|
||||
import {
|
||||
@@ -16,27 +15,26 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
public game: GameView | undefined;
|
||||
public eventBus: EventBus | undefined;
|
||||
|
||||
@property({ type: Boolean })
|
||||
visible: boolean = false;
|
||||
|
||||
@state()
|
||||
private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier;
|
||||
private _isSinglePlayer: boolean = false;
|
||||
|
||||
@state()
|
||||
private _isVisible = false;
|
||||
@property({ type: Boolean })
|
||||
isSingleplayer = false;
|
||||
|
||||
init() {
|
||||
this._isSinglePlayer =
|
||||
this.game?.config().gameConfig().gameType === GameType.Singleplayer;
|
||||
if (this._isSinglePlayer) {
|
||||
this.setVisible(true);
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this; // Enable Tailwind CSS
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this._isVisible && this.game?.config().isReplay()) {
|
||||
this.setVisible(true);
|
||||
}
|
||||
init() {}
|
||||
|
||||
this.requestUpdate();
|
||||
tick() {
|
||||
if (!this.visible) return;
|
||||
if (this.game!.ticks() % 10 === 0) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onReplaySpeedChange(value: ReplaySpeedMultiplier) {
|
||||
@@ -44,85 +42,45 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
this.eventBus?.emit(new ReplaySpeedChangeEvent(value));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
// Render any necessary canvas elements
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
renderLayer(_ctx: CanvasRenderingContext2D) {}
|
||||
shouldTransform() {
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this._isVisible = visible;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._isVisible) {
|
||||
return html``;
|
||||
}
|
||||
if (!this.visible) return html``;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-es-sm lg:rounded-lg backdrop-blur-md"
|
||||
@contextmenu=${(e) => e.preventDefault()}
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<label class="block mb-1 text-white" translate="no">
|
||||
${this._isSinglePlayer
|
||||
${this.isSingleplayer
|
||||
? translateText("replay_panel.game_speed")
|
||||
: translateText("replay_panel.replay_speed")}
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.slow
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.slow);
|
||||
}}
|
||||
>
|
||||
×0.5
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.normal
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.normal);
|
||||
}}
|
||||
>
|
||||
×1
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.fast
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.fast);
|
||||
}}
|
||||
>
|
||||
×2
|
||||
</button>
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${this
|
||||
._replaySpeedMultiplier === ReplaySpeedMultiplier.fastest
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => {
|
||||
this.onReplaySpeedChange(ReplaySpeedMultiplier.fastest);
|
||||
}}
|
||||
>
|
||||
max
|
||||
</button>
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.slow, "×0.5")}
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.normal, "×1")}
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.fast, "×2")}
|
||||
${this.renderSpeedButton(ReplaySpeedMultiplier.fastest, "max")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // Disable shadow DOM to allow Tailwind styles
|
||||
private renderSpeedButton(value: ReplaySpeedMultiplier, label: string) {
|
||||
const isActive = this._replaySpeedMultiplier === value;
|
||||
return html`
|
||||
<button
|
||||
class="text-white font-bold py-0 rounded border transition ${isActive
|
||||
? "bg-blue-500 border-gray-400"
|
||||
: "border-gray-500"}"
|
||||
@click=${() => this.onReplaySpeedChange(value)}
|
||||
>
|
||||
${label}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ export class SpawnTimer implements Layer {
|
||||
|
||||
tick() {
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.ratios[0] =
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns();
|
||||
// During spawn phase, only one segment filling full width
|
||||
this.ratios = [
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns(),
|
||||
];
|
||||
this.colors = ["rgba(0, 128, 255, 0.7)"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,18 +36,17 @@ export class SpawnTimer implements Layer {
|
||||
const team = player.team();
|
||||
if (team === null) throw new Error("Team is null");
|
||||
const tiles = teamTiles.get(team) ?? 0;
|
||||
const sum = tiles + player.numTilesOwned();
|
||||
teamTiles.set(team, sum);
|
||||
teamTiles.set(team, tiles + player.numTilesOwned());
|
||||
}
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total === 0) return;
|
||||
|
||||
for (const [team, count] of teamTiles) {
|
||||
const ratio = count / total;
|
||||
const color = theme.teamColor(team).toRgbString();
|
||||
this.ratios.push(ratio);
|
||||
this.colors.push(color);
|
||||
this.colors.push(theme.teamColor(team).toRgbString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +55,23 @@ export class SpawnTimer implements Layer {
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (this.ratios === null) return;
|
||||
if (this.ratios.length === 0) return;
|
||||
if (this.colors.length === 0) return;
|
||||
if (this.ratios.length === 0 || this.colors.length === 0) return;
|
||||
|
||||
const barHeight = 10;
|
||||
const barWidth = this.transformHandler.width();
|
||||
let yOffset: number;
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
// At spawn time, draw at top
|
||||
yOffset = 0;
|
||||
} else if (this.game.config().gameConfig().gameMode === GameMode.Team) {
|
||||
// After spawn, only in team mode, offset based on screen width
|
||||
const screenW = window.innerWidth;
|
||||
yOffset = screenW > 1024 ? 80 : 58;
|
||||
} else {
|
||||
// Not spawn and not team mode: no bar
|
||||
return;
|
||||
}
|
||||
|
||||
let x = 0;
|
||||
let filledRatio = 0;
|
||||
@@ -67,7 +80,7 @@ export class SpawnTimer implements Layer {
|
||||
const segmentWidth = barWidth * ratio;
|
||||
|
||||
context.fillStyle = this.colors[i];
|
||||
context.fillRect(x, 0, segmentWidth, barHeight);
|
||||
context.fillRect(x, yOffset, segmentWidth, barHeight);
|
||||
|
||||
x += segmentWidth;
|
||||
filledRatio += ratio;
|
||||
@@ -76,8 +89,6 @@ export class SpawnTimer implements Layer {
|
||||
}
|
||||
|
||||
function sumIterator(values: MapIterator<number>) {
|
||||
// To use reduce, we'd need to allocate an array:
|
||||
// return Array.from(values).reduce((sum, v) => sum + v, 0);
|
||||
let total = 0;
|
||||
for (const value of values) {
|
||||
total += value;
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { renderNumber, renderTroops } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("top-bar")
|
||||
export class TopBar extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
private isVisible = false;
|
||||
private _population = 0;
|
||||
private _lastPopulationIncreaseRate = 0;
|
||||
private _popRateIsIncreasing = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.updatePopulationIncrease();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updatePopulationIncrease() {
|
||||
const player = this.game?.myPlayer();
|
||||
if (player === null) return;
|
||||
const popIncreaseRate = player.population() - this._population;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this._popRateIsIncreasing =
|
||||
popIncreaseRate >= this._lastPopulationIncreaseRate;
|
||||
this._lastPopulationIncreaseRate = popIncreaseRate;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
|
||||
const maxPop = this.game.config().maxPopulation(myPlayer);
|
||||
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10n;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 z-50 bg-slate-800/40 backdrop-blur-sm shadow-xs text-white text-sm p-1 rounded-ee-sm lg:rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
|
||||
>
|
||||
<!-- Pop section (takes 2 columns on desktop) -->
|
||||
<div
|
||||
class="sm:col-span-1 flex items-center space-x-1 overflow-x-auto whitespace-nowrap"
|
||||
>
|
||||
<span class="font-bold shrink-0"
|
||||
>${translateText("control_panel.pop")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderTroops(myPlayer.population())} /
|
||||
${renderTroops(maxPop)}</span
|
||||
>
|
||||
<span
|
||||
translate="no"
|
||||
class="${this._popRateIsIncreasing
|
||||
? "text-green-500"
|
||||
: "text-yellow-500"}"
|
||||
>(+${renderTroops(popRate)})</span
|
||||
>
|
||||
</div>
|
||||
<!-- Gold section (takes 1 column on desktop) -->
|
||||
<div
|
||||
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
|
||||
>
|
||||
<span class="font-bold shrink-0"
|
||||
>${translateText("control_panel.gold")}:</span
|
||||
>
|
||||
<span translate="no"
|
||||
>${renderNumber(myPlayer.gold())}
|
||||
(+${renderNumber(goldPerSecond)})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,6 @@
|
||||
<div id="radialMenu" class="radial-menu"></div>
|
||||
<div class="flex gap-2 fixed right-[10px] top-[10px] z-50 flex-col">
|
||||
<options-menu></options-menu>
|
||||
<replay-panel></replay-panel>
|
||||
<player-info-overlay></player-info-overlay>
|
||||
</div>
|
||||
<div
|
||||
@@ -279,16 +278,13 @@
|
||||
style="position: fixed; pointer-events: none"
|
||||
>
|
||||
<div
|
||||
class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex flex-col items-end"
|
||||
class="w-full md:w-2/3 md:fixed sm:right-0 md:bottom-0 md:flex flex-col items-end"
|
||||
style="pointer-events: none"
|
||||
>
|
||||
<chat-display></chat-display>
|
||||
<events-display></events-display>
|
||||
</div>
|
||||
<div
|
||||
class="w-[320px] flex flex-col items-center"
|
||||
style="pointer-events: auto"
|
||||
>
|
||||
<div style="pointer-events: auto">
|
||||
<control-panel></control-panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,7 +359,8 @@
|
||||
<build-menu></build-menu>
|
||||
<win-modal></win-modal>
|
||||
<game-starting-modal></game-starting-modal>
|
||||
<top-bar></top-bar>
|
||||
<game-top-bar></game-top-bar>
|
||||
<game-right-sidebar></game-right-sidebar>
|
||||
|
||||
<player-panel></player-panel>
|
||||
<help-modal></help-modal>
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari */
|
||||
}
|
||||
|
||||
.start-game-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
|
||||
@@ -264,10 +264,17 @@ export class PlayerView {
|
||||
targetTroopRatio(): number {
|
||||
return this.data.targetTroopRatio;
|
||||
}
|
||||
|
||||
troops(): number {
|
||||
return this.data.troops;
|
||||
}
|
||||
|
||||
totalUnitLevels(type: UnitType): number {
|
||||
return this.units(type)
|
||||
.map((unit) => unit.level())
|
||||
.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
isAlliedWith(other: PlayerView): boolean {
|
||||
return this.data.allies.some((n) => other.smallID() === n);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user