mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-20 01:00:10 +00:00
## Description: Makes so that when clicking on the attack warning message in chat, the camera focuses on the "average position" of the attack, instead of just the player. The average position is calculated by taking the average position of all attacking border cells. It makes the calculation for every AttackUpdate, which adds some calculations every tick, but it shouldn't affect performance that much, as it's just a sum of coordinates. If you have a better way of getting the averagePosition information (calculating it only when necessary instead of every tick), it would be great. closes #703 ## Please complete the following: - [x] I have added screenshots for all UI updates - [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: leo21_ --------- Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com> Co-authored-by: evanpelle <evanpelle@gmail.com>
344 lines
8.1 KiB
TypeScript
344 lines
8.1 KiB
TypeScript
import { LitElement, css, html } from "lit";
|
|
import { customElement, state } from "lit/decorators.js";
|
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
import { translateText } from "../../../client/Utils";
|
|
import { EventBus, GameEvent } from "../../../core/EventBus";
|
|
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
|
import { ClientID } from "../../../core/Schemas";
|
|
import { renderNumber } from "../../Utils";
|
|
import { Layer } from "./Layer";
|
|
|
|
interface Entry {
|
|
name: string;
|
|
position: number;
|
|
score: string;
|
|
gold: string;
|
|
troops: string;
|
|
isMyPlayer: boolean;
|
|
player: PlayerView;
|
|
}
|
|
|
|
export class GoToPlayerEvent implements GameEvent {
|
|
constructor(public player: PlayerView) {}
|
|
}
|
|
|
|
export class GoToPositionEvent implements GameEvent {
|
|
constructor(
|
|
public x: number,
|
|
public y: number,
|
|
) {}
|
|
}
|
|
|
|
export class GoToUnitEvent implements GameEvent {
|
|
constructor(public unit: UnitView) {}
|
|
}
|
|
|
|
@customElement("leader-board")
|
|
export class Leaderboard extends LitElement implements Layer {
|
|
public game: GameView | null = null;
|
|
public clientID: ClientID | null = null;
|
|
public eventBus: EventBus | null = null;
|
|
|
|
players: Entry[] = [];
|
|
|
|
@state()
|
|
private _leaderboardHidden = true;
|
|
private _shownOnInit = false;
|
|
private showTopFive = true;
|
|
|
|
init() {}
|
|
|
|
tick() {
|
|
if (this.game === null) throw new Error("Not initialized");
|
|
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
|
|
this._shownOnInit = true;
|
|
this.showLeaderboard();
|
|
this.updateLeaderboard();
|
|
}
|
|
if (this._leaderboardHidden) {
|
|
return;
|
|
}
|
|
|
|
if (this.game.ticks() % 10 === 0) {
|
|
this.updateLeaderboard();
|
|
}
|
|
}
|
|
|
|
private updateLeaderboard() {
|
|
if (this.game === null) throw new Error("Not initialized");
|
|
if (this.clientID === null) {
|
|
return;
|
|
}
|
|
const myPlayer =
|
|
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
|
|
null;
|
|
|
|
const sorted = this.game
|
|
.playerViews()
|
|
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
|
|
|
const numTilesWithoutFallout =
|
|
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
|
|
|
const playersToShow = this.showTopFive ? sorted.slice(0, 5) : sorted;
|
|
|
|
this.players = playersToShow.map((player, index) => {
|
|
let troops = player.troops() / 10;
|
|
if (!player.isAlive()) {
|
|
troops = 0;
|
|
}
|
|
return {
|
|
name: player.displayName(),
|
|
position: index + 1,
|
|
score: formatPercentage(
|
|
player.numTilesOwned() / numTilesWithoutFallout,
|
|
),
|
|
gold: renderNumber(player.gold()),
|
|
troops: renderNumber(troops),
|
|
isMyPlayer: player === myPlayer,
|
|
player: player,
|
|
};
|
|
});
|
|
|
|
if (
|
|
myPlayer !== null &&
|
|
this.players.find((p) => p.isMyPlayer) === undefined
|
|
) {
|
|
let place = 0;
|
|
for (const p of sorted) {
|
|
place++;
|
|
if (p === myPlayer) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let myPlayerTroops = myPlayer.troops() / 10;
|
|
if (!myPlayer.isAlive()) {
|
|
myPlayerTroops = 0;
|
|
}
|
|
this.players.pop();
|
|
this.players.push({
|
|
name: myPlayer.displayName(),
|
|
position: place,
|
|
score: formatPercentage(
|
|
myPlayer.numTilesOwned() / this.game.numLandTiles(),
|
|
),
|
|
gold: renderNumber(myPlayer.gold()),
|
|
troops: renderNumber(myPlayerTroops),
|
|
isMyPlayer: true,
|
|
player: myPlayer,
|
|
});
|
|
}
|
|
|
|
this.requestUpdate();
|
|
}
|
|
|
|
private handleRowClickPlayer(player: PlayerView) {
|
|
if (this.eventBus === null) return;
|
|
this.eventBus.emit(new GoToPlayerEvent(player));
|
|
}
|
|
|
|
renderLayer(context: CanvasRenderingContext2D) {}
|
|
shouldTransform(): boolean {
|
|
return false;
|
|
}
|
|
|
|
static styles = css`
|
|
:host {
|
|
display: block;
|
|
}
|
|
img.emoji {
|
|
height: 1em;
|
|
width: auto;
|
|
}
|
|
.leaderboard {
|
|
position: fixed;
|
|
top: 10px;
|
|
left: 10px;
|
|
z-index: 9998;
|
|
background-color: rgb(31 41 55 / 0.7);
|
|
padding: 10px;
|
|
padding-top: 0px;
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
border-radius: 10px;
|
|
max-width: 500px;
|
|
max-height: 30vh;
|
|
overflow-y: auto;
|
|
width: 400px;
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
th,
|
|
td {
|
|
padding: 5px;
|
|
text-align: center;
|
|
border-bottom: 1px solid rgba(51, 51, 51, 0.2);
|
|
color: white;
|
|
}
|
|
th {
|
|
background-color: rgb(31 41 55 / 0.5);
|
|
color: white;
|
|
}
|
|
.myPlayer {
|
|
font-weight: bold;
|
|
font-size: 1.2em;
|
|
}
|
|
.otherPlayer {
|
|
font-size: 1em;
|
|
}
|
|
tr:nth-child(even) {
|
|
background-color: rgb(31 41 55 / 0.5);
|
|
}
|
|
tbody tr {
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
tbody tr:hover {
|
|
background-color: rgba(78, 78, 78, 0.8);
|
|
}
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
.leaderboard-button {
|
|
position: fixed;
|
|
left: 10px;
|
|
top: 10px;
|
|
z-index: 9998;
|
|
background-color: rgb(31 41 55 / 0.7);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.leaderboard-close-button {
|
|
background: none;
|
|
border: none;
|
|
color: white;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.leaderboard-top-five-button {
|
|
background: none;
|
|
border: none;
|
|
color: white;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.player-name {
|
|
max-width: 10ch;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
@media (max-width: 1000px) {
|
|
.leaderboard {
|
|
top: 70px;
|
|
left: 0px;
|
|
}
|
|
|
|
.leaderboard-button {
|
|
left: 0px;
|
|
top: 52px;
|
|
}
|
|
}
|
|
`;
|
|
|
|
render() {
|
|
return html`
|
|
<button
|
|
@click=${() => this.toggleLeaderboard()}
|
|
class="leaderboard-button ${this._shownOnInit && this._leaderboardHidden
|
|
? ""
|
|
: "hidden"}"
|
|
>
|
|
${translateText("leaderboard.title")}
|
|
</button>
|
|
<div
|
|
class="leaderboard ${this._leaderboardHidden ? "hidden" : ""}"
|
|
@contextmenu=${(e) => e.preventDefault()}
|
|
>
|
|
<button
|
|
class="leaderboard-close-button"
|
|
@click=${() => this.hideLeaderboard()}
|
|
>
|
|
${translateText("leaderboard.hide")}
|
|
</button>
|
|
<button
|
|
class="leaderboard-top-five-button"
|
|
@click=${() => {
|
|
this.showTopFive = !this.showTopFive;
|
|
this.updateLeaderboard();
|
|
}}
|
|
>
|
|
${this.showTopFive ? "Show All" : "Show Top 5"}
|
|
</button>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>${translateText("leaderboard.rank")}</th>
|
|
<th>${translateText("leaderboard.player")}</th>
|
|
<th>${translateText("leaderboard.owned")}</th>
|
|
<th>${translateText("leaderboard.gold")}</th>
|
|
<th>${translateText("leaderboard.troops")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${this.players.map(
|
|
(player) => html`
|
|
<tr
|
|
class="${player.isMyPlayer ? "myPlayer" : "otherPlayer"}"
|
|
@click=${() => this.handleRowClickPlayer(player.player)}
|
|
>
|
|
<td>${player.position}</td>
|
|
<td class="player-name">${unsafeHTML(player.name)}</td>
|
|
<td>${player.score}</td>
|
|
<td>${player.gold}</td>
|
|
<td>${player.troops}</td>
|
|
</tr>
|
|
`,
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
toggleLeaderboard() {
|
|
this._leaderboardHidden = !this._leaderboardHidden;
|
|
this.requestUpdate();
|
|
}
|
|
|
|
hideLeaderboard() {
|
|
this._leaderboardHidden = true;
|
|
this.requestUpdate();
|
|
}
|
|
|
|
showLeaderboard() {
|
|
this._leaderboardHidden = false;
|
|
this.requestUpdate();
|
|
}
|
|
|
|
get isVisible() {
|
|
return !this._leaderboardHidden;
|
|
}
|
|
}
|
|
|
|
function formatPercentage(value: number): string {
|
|
const perc = value * 100;
|
|
if (perc > 99.5) {
|
|
return "100%";
|
|
}
|
|
if (perc < 0.01) {
|
|
return "0%";
|
|
}
|
|
if (perc < 0.1) {
|
|
return perc.toPrecision(1) + "%";
|
|
}
|
|
return perc.toPrecision(2) + "%";
|
|
}
|