mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 17:15:19 +00:00
Clan System Part 1 (#3276)
## Description: Properly split out clantags and usernames, a clantag should not be part of a username. <img width="285" height="286" alt="image" src="https://github.com/user-attachments/assets/8ac56e82-b12c-4fc0-9774-e445252a6e61" /> https://api.openfront.dev/game/ojkqZFb2 <img width="296" height="596" alt="image" src="https://github.com/user-attachments/assets/85152f80-c111-4f87-b85b-8516c9c6137b" /> https://api.openfront.dev/game/MF32BkVc requires; https://github.com/openfrontio/infra/pull/264 ## 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: w.o.n
This commit is contained in:
@@ -56,6 +56,7 @@ export interface LobbyConfig {
|
||||
serverConfig: ServerConfig;
|
||||
cosmetics: PlayerCosmeticRefs;
|
||||
playerName: string;
|
||||
playerClanTag: string | null;
|
||||
gameID: GameID;
|
||||
turnstileToken: string | null;
|
||||
// GameStartInfo only exists when playing a singleplayer game.
|
||||
@@ -228,6 +229,7 @@ async function createClientGame(
|
||||
gameMap,
|
||||
clientID,
|
||||
lobbyConfig.playerName,
|
||||
lobbyConfig.playerClanTag,
|
||||
lobbyConfig.gameStartInfo.gameID,
|
||||
lobbyConfig.gameStartInfo.players,
|
||||
);
|
||||
@@ -301,6 +303,7 @@ export class ClientGameRunner {
|
||||
{
|
||||
persistentID: getPersistentID(),
|
||||
username: this.lobby.playerName,
|
||||
clanTag: this.lobby.playerClanTag ?? null,
|
||||
clientID: this.clientID,
|
||||
stats: update.allPlayersStats[this.clientID],
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import { GameEndInfo } from "../core/Schemas";
|
||||
import { GameMapType } from "../core/game/Game";
|
||||
import { fetchGameById } from "./Api";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { renderDuration, translateText } from "./Utils";
|
||||
import {
|
||||
PlayerInfo,
|
||||
@@ -28,7 +27,7 @@ export class GameInfoModal extends LitElement {
|
||||
@property({ type: String }) gameId: string | null = null;
|
||||
@property({ type: String }) rankType = RankType.Lifetime;
|
||||
|
||||
@state() private username: string | null = null;
|
||||
@state() private currentClientID: string | null = null;
|
||||
@state() private isLoadingGame: boolean = true;
|
||||
|
||||
private ranking: Ranking | null = null;
|
||||
@@ -152,7 +151,7 @@ export class GameInfoModal extends LitElement {
|
||||
.score=${this.ranking?.score(player, this.rankType) ?? 0}
|
||||
.rankType=${this.rankType}
|
||||
.bestScore=${bestScore}
|
||||
.currentPlayer=${this.username === player.rawUsername}
|
||||
.currentPlayer=${this.currentClientID === player.id}
|
||||
></player-row>
|
||||
`,
|
||||
)}
|
||||
@@ -183,26 +182,16 @@ export class GameInfoModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public loadUserName() {
|
||||
const usernameInput = document.querySelector(
|
||||
"username-input",
|
||||
) as UsernameInput;
|
||||
if (usernameInput) {
|
||||
this.username = usernameInput.getCurrentUsername();
|
||||
}
|
||||
}
|
||||
|
||||
public async loadGame(gameId: string) {
|
||||
public async loadGame(gameId: string, currentClientID: string | null = null) {
|
||||
try {
|
||||
this.isLoadingGame = true;
|
||||
this.loadUserName();
|
||||
this.currentClientID = currentClientID;
|
||||
const session = await fetchGameById(gameId);
|
||||
if (!session) return;
|
||||
|
||||
this.gameInfo = session.info;
|
||||
this.ranking = new Ranking(session);
|
||||
this.updateRanking();
|
||||
this.isLoadingGame = false;
|
||||
await this.loadMapImage(session.info.config.gameMap);
|
||||
} catch (err) {
|
||||
console.error("Failed to load game:", err);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PublicLobbySocket } from "./LobbySocket";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import {
|
||||
calculateServerTimeOffset,
|
||||
getMapName,
|
||||
@@ -48,20 +49,10 @@ export class GameModeSelector extends LitElement {
|
||||
* Returns true if valid, false otherwise.
|
||||
*/
|
||||
private validateUsername(): boolean {
|
||||
const usernameInput = document.querySelector("username-input") as any;
|
||||
if (usernameInput?.isValid?.() === false) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: usernameInput.validationError,
|
||||
color: "red",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
const usernameInput = document.querySelector(
|
||||
"username-input",
|
||||
) as UsernameInput | null;
|
||||
return usernameInput ? usernameInput.validateOrShowError() : true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import {
|
||||
createPartialGameRecord,
|
||||
decompressGameRecord,
|
||||
getClanTag,
|
||||
replacer,
|
||||
} from "../core/Util";
|
||||
import { getPersistentID } from "./Auth";
|
||||
@@ -273,10 +272,10 @@ export class LocalServer {
|
||||
{
|
||||
persistentID: getPersistentID(),
|
||||
username: this.lobbyConfig.playerName,
|
||||
clanTag: this.lobbyConfig.playerClanTag ?? null,
|
||||
clientID: this.clientID!,
|
||||
stats: this.allPlayersStats[this.clientID!],
|
||||
cosmetics: this.lobbyConfig.gameStartInfo?.players[0].cosmetics,
|
||||
clanTag: getClanTag(this.lobbyConfig.playerName) ?? undefined,
|
||||
},
|
||||
];
|
||||
if (this.lobbyConfig.gameStartInfo === undefined) {
|
||||
|
||||
+6
-2
@@ -732,6 +732,10 @@ class Client {
|
||||
|
||||
private async handleJoinLobby(event: CustomEvent<JoinLobbyEvent>) {
|
||||
const lobby = event.detail;
|
||||
if (this.usernameInput && !this.usernameInput.validateOrShowError()) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`joining lobby ${lobby.gameID}`);
|
||||
if (this.gameStop !== null) {
|
||||
console.log("joining lobby, stopping existing game");
|
||||
@@ -753,8 +757,8 @@ class Client {
|
||||
serverConfig: config,
|
||||
cosmetics: await getPlayerCosmeticsRefs(),
|
||||
turnstileToken: await this.getTurnstileToken(lobby),
|
||||
playerName:
|
||||
this.usernameInput?.getCurrentUsername() ?? genAnonUsername(),
|
||||
playerName: this.usernameInput?.getUsername() ?? genAnonUsername(),
|
||||
playerClanTag: this.usernameInput?.getClanTag() ?? null,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info,
|
||||
gameRecord: lobby.gameRecord,
|
||||
},
|
||||
|
||||
@@ -654,9 +654,6 @@ export class SinglePlayerModal extends BaseModal {
|
||||
const usernameInput = document.querySelector(
|
||||
"username-input",
|
||||
) as UsernameInput;
|
||||
if (!usernameInput) {
|
||||
console.warn("Username input element not found");
|
||||
}
|
||||
|
||||
await crazyGamesSDK.requestMidgameAd();
|
||||
|
||||
@@ -669,7 +666,8 @@ export class SinglePlayerModal extends BaseModal {
|
||||
players: [
|
||||
{
|
||||
clientID,
|
||||
username: usernameInput.getCurrentUsername(),
|
||||
username: usernameInput.getUsername(),
|
||||
clanTag: usernameInput.getClanTag() ?? null,
|
||||
cosmetics: await getPlayerCosmetics(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -399,6 +399,7 @@ export class Transport {
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
// Note: clientID is not sent - server assigns it based on persistentID
|
||||
username: this.lobbyConfig.playerName,
|
||||
clanTag: this.lobbyConfig.playerClanTag ?? null,
|
||||
cosmetics: this.lobbyConfig.cosmetics,
|
||||
turnstileToken: this.lobbyConfig.turnstileToken,
|
||||
token: await getPlayToken(),
|
||||
|
||||
+59
-62
@@ -2,15 +2,19 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { getClanTagOriginalCase, sanitizeClanTag } from "../core/Util";
|
||||
import { sanitizeClanTag } from "../core/Util";
|
||||
import {
|
||||
MAX_CLAN_TAG_LENGTH,
|
||||
MAX_USERNAME_LENGTH,
|
||||
MIN_CLAN_TAG_LENGTH,
|
||||
MIN_USERNAME_LENGTH,
|
||||
validateClanTag,
|
||||
validateUsername,
|
||||
} from "../core/validations/username";
|
||||
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
||||
|
||||
const usernameKey: string = "username";
|
||||
const clanTagKey: string = "clanTag";
|
||||
|
||||
@customElement("username-input")
|
||||
export class UsernameInput extends LitElement {
|
||||
@@ -27,46 +31,45 @@ export class UsernameInput extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
public getCurrentUsername(): string {
|
||||
return this.constructFullUsername();
|
||||
public getUsername(): string {
|
||||
return this.baseUsername.trim();
|
||||
}
|
||||
|
||||
private constructFullUsername(): string {
|
||||
if (this.clanTag.length >= 2) {
|
||||
return `[${this.clanTag}] ${this.baseUsername}`;
|
||||
}
|
||||
return this.baseUsername;
|
||||
public getClanTag(): string | null {
|
||||
return this.clanTag.length >= MIN_CLAN_TAG_LENGTH &&
|
||||
this.clanTag.length <= MAX_CLAN_TAG_LENGTH &&
|
||||
validateClanTag(this.clanTag).isValid
|
||||
? this.clanTag
|
||||
: null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const stored = this.getUsername();
|
||||
this.parseAndSetUsername(stored);
|
||||
this.loadStoredUsername();
|
||||
crazyGamesSDK.getUsername().then((username) => {
|
||||
if (username) {
|
||||
this.parseAndSetUsername(username ?? genAnonUsername());
|
||||
this.requestUpdate();
|
||||
this.baseUsername = username;
|
||||
this.validateAndStore();
|
||||
}
|
||||
});
|
||||
crazyGamesSDK.addAuthListener((user) => {
|
||||
if (user) {
|
||||
this.parseAndSetUsername(user?.username);
|
||||
this.baseUsername = user.username;
|
||||
this.validateAndStore();
|
||||
}
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private parseAndSetUsername(fullUsername: string) {
|
||||
const tag = getClanTagOriginalCase(fullUsername);
|
||||
if (tag) {
|
||||
this.clanTag = tag.toUpperCase();
|
||||
this.baseUsername = fullUsername.replace(`[${tag}]`, "").trim();
|
||||
private loadStoredUsername() {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
this.clanTag = localStorage.getItem(clanTagKey) ?? "";
|
||||
this.baseUsername = storedUsername;
|
||||
this.validateAndStore();
|
||||
} else {
|
||||
this.clanTag = "";
|
||||
this.baseUsername = fullUsername;
|
||||
this.baseUsername = genAnonUsername();
|
||||
this.validateAndStore();
|
||||
}
|
||||
|
||||
this.validateAndStore();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -77,7 +80,8 @@ export class UsernameInput extends LitElement {
|
||||
.value=${this.clanTag}
|
||||
@input=${this.handleClanTagChange}
|
||||
placeholder="${translateText("username.tag")}"
|
||||
maxlength="5"
|
||||
minlength="${MIN_CLAN_TAG_LENGTH}"
|
||||
maxlength="${MAX_CLAN_TAG_LENGTH}"
|
||||
class="w-[6rem] text-xl font-medium tracking-wider text-center uppercase shrink-0 bg-transparent text-white placeholder-white/70 focus:placeholder-transparent border-0 border-b border-white/40 focus:outline-none focus:border-white/60"
|
||||
/>
|
||||
<input
|
||||
@@ -85,6 +89,7 @@ export class UsernameInput extends LitElement {
|
||||
.value=${this.baseUsername}
|
||||
@input=${this.handleUsernameChange}
|
||||
placeholder="${translateText("username.enter_username")}"
|
||||
minlength="${MIN_USERNAME_LENGTH}"
|
||||
maxlength="${MAX_USERNAME_LENGTH}"
|
||||
class="flex-1 min-w-0 border-0 text-2xl font-medium tracking-wider text-left text-white placeholder-white/70 focus:outline-none focus:ring-0 overflow-x-auto whitespace-nowrap text-ellipsis pr-2 bg-transparent"
|
||||
/>
|
||||
@@ -147,59 +152,51 @@ export class UsernameInput extends LitElement {
|
||||
}
|
||||
|
||||
private validateAndStore() {
|
||||
// Prevent empty username even if clan tag is present
|
||||
const trimmedBase = this.baseUsername.trim();
|
||||
if (!trimmedBase || trimmedBase.length < MIN_USERNAME_LENGTH) {
|
||||
const trimmedBase = this.getUsername();
|
||||
|
||||
const clanTagResult = validateClanTag(this.clanTag);
|
||||
if (!clanTagResult.isValid) {
|
||||
this._isValid = false;
|
||||
this.validationError = translateText("username.too_short", {
|
||||
min: MIN_USERNAME_LENGTH,
|
||||
});
|
||||
this.validationError = clanTagResult.error ?? "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate clan tag if present
|
||||
if (this.clanTag.length > 0 && this.clanTag.length < 2) {
|
||||
this._isValid = false;
|
||||
this.validationError = translateText("username.tag_too_short");
|
||||
return;
|
||||
}
|
||||
|
||||
const full = this.constructFullUsername();
|
||||
const trimmedFull = full.trim();
|
||||
|
||||
const result = validateUsername(trimmedFull);
|
||||
const result = validateUsername(trimmedBase);
|
||||
this._isValid = result.isValid;
|
||||
if (result.isValid) {
|
||||
this.storeUsername(trimmedFull);
|
||||
localStorage.setItem(usernameKey, trimmedBase);
|
||||
localStorage.setItem(clanTagKey, this.getClanTag() ?? "");
|
||||
this.validationError = "";
|
||||
} else {
|
||||
this.validationError = result.error ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private getUsername(): string {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
return storedUsername;
|
||||
}
|
||||
return this.generateNewUsername();
|
||||
}
|
||||
|
||||
private storeUsername(username: string) {
|
||||
if (username) {
|
||||
localStorage.setItem(usernameKey, username);
|
||||
}
|
||||
}
|
||||
|
||||
private generateNewUsername(): string {
|
||||
const newUsername = genAnonUsername();
|
||||
this.storeUsername(newUsername);
|
||||
return newUsername;
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this._isValid;
|
||||
}
|
||||
|
||||
public showValidationFeedback(): void {
|
||||
const message =
|
||||
this.validationError || translateText("username.invalid_chars");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message,
|
||||
color: "red",
|
||||
duration: 2500,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public validateOrShowError(): boolean {
|
||||
if (this.isValid()) {
|
||||
return true;
|
||||
}
|
||||
this.showValidationFeedback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function genAnonUsername(): string {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { assignTeamsLobbyPreview } from "../../core/game/TeamAssignment";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { ClientInfo, TeamCountConfig } from "../../core/Schemas";
|
||||
import { createRandomName } from "../../core/Util";
|
||||
import { createRandomName, formatPlayerDisplayName } from "../../core/Util";
|
||||
import { getTranslatedPlayerTeamLabel, translateText } from "../Utils";
|
||||
|
||||
export interface TeamPreviewData {
|
||||
@@ -122,7 +122,7 @@ export class LobbyTeamView extends LitElement {
|
||||
this.clients,
|
||||
(c) => c.clientID ?? c.username,
|
||||
(client) => {
|
||||
const displayName = this.displayUsername(client);
|
||||
const displayName = this.getClientDisplayName(client);
|
||||
return html`<div
|
||||
class="px-2 py-1 rounded-sm bg-gray-700/70 mb-1 text-xs text-white"
|
||||
>
|
||||
@@ -167,7 +167,7 @@ export class LobbyTeamView extends LitElement {
|
||||
this.clients,
|
||||
(c) => c.clientID ?? c.username,
|
||||
(client) => {
|
||||
const displayName = this.displayUsername(client);
|
||||
const displayName = this.getClientDisplayName(client);
|
||||
return html`<span class="player-tag">
|
||||
<span class="text-white">${displayName}</span>
|
||||
${client.clientID === this.lobbyCreatorClientID
|
||||
@@ -226,7 +226,7 @@ export class LobbyTeamView extends LitElement {
|
||||
preview.players,
|
||||
(p) => p.clientID ?? p.username,
|
||||
(p) => {
|
||||
const displayName = this.displayUsername(p);
|
||||
const displayName = this.getClientDisplayName(p);
|
||||
return html` <div
|
||||
class="bg-gray-700/70 px-2 py-1 rounded-sm text-xs flex items-center justify-between"
|
||||
>
|
||||
@@ -318,7 +318,14 @@ export class LobbyTeamView extends LitElement {
|
||||
|
||||
const players = this.clients.map(
|
||||
(c) =>
|
||||
new PlayerInfo(c.username, PlayerType.Human, c.clientID, c.clientID),
|
||||
new PlayerInfo(
|
||||
c.username,
|
||||
PlayerType.Human,
|
||||
c.clientID,
|
||||
c.clientID,
|
||||
false,
|
||||
c.clanTag,
|
||||
),
|
||||
);
|
||||
const assignment = assignTeamsLobbyPreview(
|
||||
players,
|
||||
@@ -358,17 +365,17 @@ export class LobbyTeamView extends LitElement {
|
||||
}));
|
||||
}
|
||||
|
||||
private displayUsername(client: ClientInfo): string {
|
||||
private getClientDisplayName(client: ClientInfo): string {
|
||||
const full = formatPlayerDisplayName(client.username, client.clanTag);
|
||||
if (!this.userSettings.anonymousNames()) {
|
||||
return client.username;
|
||||
return full;
|
||||
}
|
||||
|
||||
if (this.currentClientID && client.clientID === this.currentClientID) {
|
||||
return client.username;
|
||||
return full;
|
||||
}
|
||||
|
||||
return (
|
||||
createRandomName(client.username, PlayerType.Human) ?? client.username
|
||||
);
|
||||
// Keep clan tag visible while anonymizing only the username.
|
||||
const anonymizedUsername =
|
||||
createRandomName(client.username, PlayerType.Human) ?? client.username;
|
||||
return formatPlayerDisplayName(anonymizedUsername, client.clanTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,8 @@ export enum RankType {
|
||||
|
||||
export interface PlayerInfo {
|
||||
id: string;
|
||||
rawUsername: string;
|
||||
username: string;
|
||||
tag?: string;
|
||||
clanTag: string | null;
|
||||
killedAt?: number;
|
||||
gold: bigint[];
|
||||
conquests: bigint[];
|
||||
@@ -77,18 +76,12 @@ export class Ranking {
|
||||
for (const player of session.info.players) {
|
||||
if (player === undefined || !hasPlayed(player)) continue;
|
||||
const stats = player.stats!;
|
||||
const match = player.username.match(/^\[(.*?)\]\s*(.*)$/);
|
||||
let username = player.username;
|
||||
if (player.clanTag && match) {
|
||||
username = match[2];
|
||||
}
|
||||
const gold = (stats.gold ?? []).map((v) => BigInt(v ?? 0));
|
||||
const conquests = (stats.conquests ?? []).map((v) => BigInt(v ?? 0));
|
||||
players[player.clientID] = {
|
||||
id: player.clientID,
|
||||
rawUsername: player.username,
|
||||
username,
|
||||
tag: player.clanTag,
|
||||
username: player.username,
|
||||
clanTag: player.clanTag,
|
||||
conquests,
|
||||
flag: player.cosmetics?.flag ?? undefined,
|
||||
killedAt: stats.killedAt !== null ? Number(stats.killedAt) : undefined,
|
||||
|
||||
@@ -220,7 +220,7 @@ export class PlayerRow extends LitElement {
|
||||
private renderPlayerName() {
|
||||
return html`
|
||||
<div class="flex gap-1 items-center w-50 shrink-0">
|
||||
${this.player.tag ? this.renderTag(this.player.tag) : ""}
|
||||
${this.player.clanTag ? this.renderTag(this.player.clanTag) : ""}
|
||||
<div
|
||||
class="text-xs sm:text-sm font-bold tracking-wide text-white/80 text-ellipsis w-37.5 shrink-0 overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
|
||||
@@ -249,9 +249,7 @@ export class LeaderboardPlayerList extends LitElement {
|
||||
</div>`
|
||||
: ""}
|
||||
<span class="font-bold text-blue-300 truncate text-base"
|
||||
>${player.clanTag
|
||||
? player.username.replace(/^\[.*?\]\s*/, "")
|
||||
: player.username}</span
|
||||
>${player.username}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
@@ -434,14 +432,18 @@ export class LeaderboardPlayerList extends LitElement {
|
||||
"leaderboard_modal.your_ranking",
|
||||
)}</span
|
||||
>
|
||||
<span class="font-bold text-white text-base"
|
||||
>${this.currentUserEntry.clanTag
|
||||
? this.currentUserEntry.username.replace(
|
||||
/^\[.*?\]\s*/,
|
||||
"",
|
||||
)
|
||||
: this.currentUserEntry.username}</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
${this.currentUserEntry.clanTag
|
||||
? html`<div
|
||||
class="px-2 py-0.5 rounded bg-blue-500/10 border border-blue-300/40 text-[10px] font-bold text-blue-100 shrink-0"
|
||||
>
|
||||
${this.currentUserEntry.clanTag}
|
||||
</div>`
|
||||
: ""}
|
||||
<span class="font-bold text-white text-base"
|
||||
>${this.currentUserEntry.username}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end w-20">
|
||||
<div class="font-mono text-white font-bold text-lg">
|
||||
|
||||
@@ -52,7 +52,7 @@ export function placeName(game: Game, player: Player): NameViewData {
|
||||
),
|
||||
);
|
||||
|
||||
const fontSize = calculateFontSize(largestRectangle, player.name());
|
||||
const fontSize = calculateFontSize(largestRectangle, player.displayName());
|
||||
center = new Cell(center.x, center.y - fontSize / 3);
|
||||
|
||||
return {
|
||||
|
||||
@@ -235,7 +235,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
<span class="truncate ml-1"
|
||||
>${(
|
||||
this.game.playerBySmallID(attack.attackerID) as PlayerView
|
||||
)?.name()}</span
|
||||
)?.displayName()}</span
|
||||
>
|
||||
${attack.retreating
|
||||
? `(${translateText("events_display.retreating")}...)`
|
||||
@@ -282,7 +282,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
<span class="truncate ml-1"
|
||||
>${(
|
||||
this.game.playerBySmallID(attack.targetID) as PlayerView
|
||||
)?.name()}</span
|
||||
)?.displayName()}</span
|
||||
> `,
|
||||
onClick: async () => this.attackWarningOnClick(attack),
|
||||
className:
|
||||
@@ -346,7 +346,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
const ownerID = this.game.ownerID(target);
|
||||
if (ownerID === 0) return "";
|
||||
const player = this.game.playerBySmallID(ownerID) as PlayerView;
|
||||
return player?.name() ?? "";
|
||||
return player?.displayName() ?? "";
|
||||
}
|
||||
|
||||
private renderBoatIcon(boat: UnitView) {
|
||||
@@ -409,7 +409,7 @@ export class AttacksDisplay extends LitElement implements Layer {
|
||||
>${renderTroops(boat.troops())}</span
|
||||
>
|
||||
<span class="truncate text-xs ml-1"
|
||||
>${boat.owner()?.name()}</span
|
||||
>${boat.owner()?.displayName()}</span
|
||||
>`,
|
||||
onClick: () => this.eventBus.emit(new GoToUnitEvent(boat)),
|
||||
className:
|
||||
|
||||
@@ -147,7 +147,7 @@ export class ChatModal extends LitElement {
|
||||
.toHex()};"
|
||||
@click=${() => this.selectPlayer(player)}
|
||||
>
|
||||
${player.name()}
|
||||
${player.displayName()}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
@@ -216,7 +216,8 @@ export class ChatModal extends LitElement {
|
||||
private selectPlayer(player: PlayerView) {
|
||||
if (this.previewText) {
|
||||
this.previewText =
|
||||
this.selectedPhraseTemplate?.replace("[P1]", player.name()) ?? null;
|
||||
this.selectedPhraseTemplate?.replace("[P1]", player.displayName()) ??
|
||||
null;
|
||||
this.selectedPlayer = player;
|
||||
this.requiresPlayerSelection = false;
|
||||
this.requestUpdate();
|
||||
@@ -255,13 +256,13 @@ export class ChatModal extends LitElement {
|
||||
|
||||
private getSortedFilteredPlayers(): PlayerView[] {
|
||||
const sorted = [...this.players].sort((a, b) =>
|
||||
a.name().localeCompare(b.name()),
|
||||
a.displayName().localeCompare(b.displayName()),
|
||||
);
|
||||
const filtered = sorted.filter((p) =>
|
||||
p.name().toLowerCase().includes(this.playerSearchQuery),
|
||||
p.displayName().toLowerCase().includes(this.playerSearchQuery),
|
||||
);
|
||||
const others = sorted.filter(
|
||||
(p) => !p.name().toLowerCase().includes(this.playerSearchQuery),
|
||||
(p) => !p.displayName().toLowerCase().includes(this.playerSearchQuery),
|
||||
);
|
||||
return [...filtered, ...others];
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.about_to_expire", {
|
||||
name: other.name(),
|
||||
name: other.displayName(),
|
||||
}),
|
||||
type: MessageType.RENEW_ALLIANCE,
|
||||
duration: this.game.config().allianceExtensionPromptOffset() - 3 * 10, // 3 second buffer
|
||||
@@ -296,7 +296,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
},
|
||||
{
|
||||
text: translateText("events_display.renew_alliance", {
|
||||
name: other.name(),
|
||||
name: other.displayName(),
|
||||
}),
|
||||
className: "btn",
|
||||
action: () =>
|
||||
@@ -460,7 +460,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.request_alliance", {
|
||||
name: requestor.name(),
|
||||
name: requestor.displayName(),
|
||||
}),
|
||||
buttons: [
|
||||
{
|
||||
@@ -525,7 +525,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
) as PlayerView;
|
||||
this.addEvent({
|
||||
description: translateText("events_display.alliance_request_status", {
|
||||
name: recipient.name(),
|
||||
name: recipient.displayName(),
|
||||
status: update.accepted
|
||||
? translateText("events_display.alliance_accepted")
|
||||
: translateText("events_display.alliance_rejected"),
|
||||
@@ -569,7 +569,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.betrayal_description", {
|
||||
name: betrayed.name(),
|
||||
name: betrayed.displayName(),
|
||||
malusPercent: malusPercent,
|
||||
durationText: durationText,
|
||||
}),
|
||||
@@ -589,7 +589,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
];
|
||||
this.addEvent({
|
||||
description: translateText("events_display.betrayed_you", {
|
||||
name: traitor.name(),
|
||||
name: traitor.displayName(),
|
||||
}),
|
||||
type: MessageType.ALLIANCE_BROKEN,
|
||||
highlight: true,
|
||||
@@ -616,7 +616,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.alliance_expired", {
|
||||
name: other.name(),
|
||||
name: other.displayName(),
|
||||
}),
|
||||
type: MessageType.ALLIANCE_EXPIRED,
|
||||
highlight: true,
|
||||
@@ -641,8 +641,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.attack_request", {
|
||||
name: other.name(),
|
||||
target: target.name(),
|
||||
name: other.displayName(),
|
||||
target: target.displayName(),
|
||||
}),
|
||||
type: MessageType.ATTACK_REQUEST,
|
||||
highlight: true,
|
||||
|
||||
@@ -239,7 +239,7 @@ export class NameLayer implements Layer {
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "player-name-span";
|
||||
nameSpan.innerHTML = player.name();
|
||||
nameSpan.textContent = player.displayName();
|
||||
nameDiv.appendChild(nameSpan);
|
||||
element.appendChild(nameDiv);
|
||||
|
||||
@@ -338,7 +338,7 @@ export class NameLayer implements Layer {
|
||||
nameDiv.style.color = render.fontColor;
|
||||
const span = nameDiv.querySelector(".player-name-span");
|
||||
if (span) {
|
||||
span.innerHTML = render.player.name();
|
||||
span.textContent = render.player.displayName();
|
||||
}
|
||||
if (flagDiv) {
|
||||
flagDiv.style.height = `${render.fontSize}px`;
|
||||
|
||||
@@ -380,7 +380,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
|
||||
/>`
|
||||
: html``}
|
||||
<span>${player.name()}</span>
|
||||
<span>${player.displayName()}</span>
|
||||
${playerTeam !== "" && player.type() !== PlayerType.Bot
|
||||
? html`<div class="flex flex-col leading-tight">
|
||||
<span class="text-gray-400 text-xs font-normal"
|
||||
@@ -488,7 +488,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
return html`
|
||||
<div class="p-2">
|
||||
<div class="font-bold mb-1 ${isAlly ? "text-green-500" : "text-white"}">
|
||||
${unit.owner().name()}
|
||||
${unit.owner().displayName()}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="text-sm opacity-80">${unit.type()}</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ export class PlayerModerationModal extends LitElement {
|
||||
if (!targetClientID || targetClientID.length === 0) return;
|
||||
|
||||
const confirmed = confirm(
|
||||
translateText("player_panel.kick_confirm", { name: other.name() }),
|
||||
translateText("player_panel.kick_confirm", { name: other.displayName() }),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -142,9 +142,9 @@ export class PlayerModerationModal extends LitElement {
|
||||
>
|
||||
<div
|
||||
class="text-sm font-semibold text-zinc-100 truncate"
|
||||
title=${other.name()}
|
||||
title=${other.displayName()}
|
||||
>
|
||||
${other.name()}
|
||||
${other.displayName()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -505,9 +505,9 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2
|
||||
class="text-xl font-bold tracking-[-0.01em] text-zinc-50 truncate"
|
||||
title=${other.name()}
|
||||
title=${other.displayName()}
|
||||
>
|
||||
${other.name()}
|
||||
${other.displayName()}
|
||||
</h2>
|
||||
</div>
|
||||
${chip
|
||||
@@ -626,7 +626,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
|
||||
const nameCollator = new Intl.Collator(undefined, { sensitivity: "base" });
|
||||
const alliesSorted = [...allies].sort((a, b) =>
|
||||
nameCollator.compare(a.name(), b.name()),
|
||||
nameCollator.compare(a.displayName(), b.displayName()),
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -669,9 +669,9 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
rounded-md border border-white/10 bg-white/5
|
||||
px-2.5 py-1 text-[14px] text-zinc-100
|
||||
hover:bg-white/8 active:scale-[0.99] transition"
|
||||
title=${p.name()}
|
||||
title=${p.displayName()}
|
||||
>
|
||||
<span class="truncate">${p.name()}</span>
|
||||
<span class="truncate">${p.displayName()}</span>
|
||||
</li>`,
|
||||
)}
|
||||
</ul>
|
||||
|
||||
@@ -334,7 +334,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
crazyGamesSDK.happytime();
|
||||
} else {
|
||||
this._title = translateText("win_modal.other_won", {
|
||||
player: winner.name(),
|
||||
player: winner.displayName(),
|
||||
});
|
||||
this.isWin = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user