mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:11:54 +00:00
test
This commit is contained in:
+10
-1
@@ -27,7 +27,8 @@
|
|||||||
"click_to_copy": "Click to copy",
|
"click_to_copy": "Click to copy",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"map_default": "Map default"
|
"map_default": "Map default",
|
||||||
|
"show_less": "Show Less"
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
"title": "OpenFront (ALPHA)",
|
"title": "OpenFront (ALPHA)",
|
||||||
@@ -537,6 +538,12 @@
|
|||||||
"error": "An error occurred. Please try again or contact support.",
|
"error": "An error occurred. Please try again or contact support.",
|
||||||
"joined_waiting": "Lobby joined! Waiting for host to start...",
|
"joined_waiting": "Lobby joined! Waiting for host to start...",
|
||||||
"version_mismatch": "This game was created with a different version. Cannot join.",
|
"version_mismatch": "This game was created with a different version. Cannot join.",
|
||||||
|
"listed_private_games": "Listed Private Games",
|
||||||
|
"no_listed_private_games": "No listed private games available.",
|
||||||
|
"show_all_listed_private_games": "Show More Lobbies (+{count})",
|
||||||
|
"show_all_settings": "Show All Settings (+{count})",
|
||||||
|
"show_fewer_settings": "Show Fewer Settings",
|
||||||
|
"join_by_lobby_id": "Join by Lobby ID",
|
||||||
"disabled_units": "Disabled Units",
|
"disabled_units": "Disabled Units",
|
||||||
"game_length": "Game length",
|
"game_length": "Game length",
|
||||||
"pvp_immunity": "PVP immunity duration",
|
"pvp_immunity": "PVP immunity duration",
|
||||||
@@ -619,6 +626,8 @@
|
|||||||
"starting_gold": "Starting Gold (Millions)",
|
"starting_gold": "Starting Gold (Millions)",
|
||||||
"starting_gold_placeholder": "5",
|
"starting_gold_placeholder": "5",
|
||||||
"host_cheats": "Host Cheats",
|
"host_cheats": "Host Cheats",
|
||||||
|
"listed_private_game": "Listed Private Game",
|
||||||
|
"listed_private_game_desc": "Show this private lobby in the Join Lobby menu.",
|
||||||
"leave_confirmation": "Are you sure you want to leave the lobby?"
|
"leave_confirmation": "Are you sure you want to leave the lobby?"
|
||||||
},
|
},
|
||||||
"team_colors": {
|
"team_colors": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
GameMode,
|
GameMode,
|
||||||
UnitType,
|
UnitType,
|
||||||
} from "../core/game/Game";
|
} from "../core/game/Game";
|
||||||
|
import { hasListedPrivateGameFlare } from "../core/ListedPrivateGame";
|
||||||
import {
|
import {
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
GameConfig,
|
GameConfig,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
isValidGameID,
|
isValidGameID,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { generateID } from "../core/Util";
|
import { generateID } from "../core/Util";
|
||||||
|
import { getUserMe } from "./Api";
|
||||||
import { getPlayToken } from "./Auth";
|
import { getPlayToken } from "./Auth";
|
||||||
import "./components/baseComponents/Modal";
|
import "./components/baseComponents/Modal";
|
||||||
import { BaseModal } from "./components/BaseModal";
|
import { BaseModal } from "./components/BaseModal";
|
||||||
@@ -85,6 +87,8 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
@state() private hostCheatGoldMultiplierValue: number | undefined = undefined;
|
@state() private hostCheatGoldMultiplierValue: number | undefined = undefined;
|
||||||
@state() private hostCheatStartingGold: boolean = false;
|
@state() private hostCheatStartingGold: boolean = false;
|
||||||
@state() private hostCheatStartingGoldValue: number | undefined = undefined;
|
@state() private hostCheatStartingGoldValue: number | undefined = undefined;
|
||||||
|
@state() private canListPrivateGame: boolean = false;
|
||||||
|
@state() private listedPrivateGame: boolean = false;
|
||||||
@state() private lobbyCreatorClientID: string = "";
|
@state() private lobbyCreatorClientID: string = "";
|
||||||
|
|
||||||
@property({ attribute: false }) eventBus: EventBus | null = null;
|
@property({ attribute: false }) eventBus: EventBus | null = null;
|
||||||
@@ -352,6 +356,12 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
labelKey: "host_modal.host_cheats",
|
labelKey: "host_modal.host_cheats",
|
||||||
checked: this.hostCheatsEnabled,
|
checked: this.hostCheatsEnabled,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
labelKey: "host_modal.listed_private_game",
|
||||||
|
descriptionKey: "host_modal.listed_private_game_desc",
|
||||||
|
checked: this.listedPrivateGame,
|
||||||
|
hidden: !this.canListPrivateGame,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
inputCards,
|
inputCards,
|
||||||
},
|
},
|
||||||
@@ -419,6 +429,7 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
|
|
||||||
protected onOpen(): void {
|
protected onOpen(): void {
|
||||||
this.startLobbyUpdates();
|
this.startLobbyUpdates();
|
||||||
|
void this.loadListedPrivateGameAccess();
|
||||||
this.lobbyId = generateID();
|
this.lobbyId = generateID();
|
||||||
// Note: clientID will be assigned by server when we join the lobby
|
// Note: clientID will be assigned by server when we join the lobby
|
||||||
// lobbyCreatorClientID stays empty until then
|
// lobbyCreatorClientID stays empty until then
|
||||||
@@ -537,6 +548,7 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
this.hostCheatGoldMultiplierValue = undefined;
|
this.hostCheatGoldMultiplierValue = undefined;
|
||||||
this.hostCheatStartingGold = false;
|
this.hostCheatStartingGold = false;
|
||||||
this.hostCheatStartingGoldValue = undefined;
|
this.hostCheatStartingGoldValue = undefined;
|
||||||
|
this.listedPrivateGame = false;
|
||||||
|
|
||||||
this.leaveLobbyOnClose = true;
|
this.leaveLobbyOnClose = true;
|
||||||
}
|
}
|
||||||
@@ -625,6 +637,10 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
this.hostCheatsEnabled = checked;
|
this.hostCheatsEnabled = checked;
|
||||||
this.putGameConfig();
|
this.putGameConfig();
|
||||||
break;
|
break;
|
||||||
|
case "host_modal.listed_private_game":
|
||||||
|
this.listedPrivateGame = this.canListPrivateGame && checked;
|
||||||
|
this.putGameConfig();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -944,6 +960,8 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
instantBuild: this.instantBuild,
|
instantBuild: this.instantBuild,
|
||||||
randomSpawn: this.randomSpawn,
|
randomSpawn: this.randomSpawn,
|
||||||
gameMode: this.gameMode,
|
gameMode: this.gameMode,
|
||||||
|
listedPrivateGame:
|
||||||
|
this.canListPrivateGame && this.listedPrivateGame,
|
||||||
disabledUnits: this.disabledUnits,
|
disabledUnits: this.disabledUnits,
|
||||||
spawnImmunityDuration: this.spawnImmunity
|
spawnImmunityDuration: this.spawnImmunity
|
||||||
? spawnImmunityTicks
|
? spawnImmunityTicks
|
||||||
@@ -1030,6 +1048,16 @@ export class HostLobbyModal extends BaseModal {
|
|||||||
// Leave existing values unchanged so the UI stays consistent
|
// Leave existing values unchanged so the UI stays consistent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadListedPrivateGameAccess() {
|
||||||
|
const userMe = await getUserMe();
|
||||||
|
const flares = userMe === false ? [] : (userMe.player.flares ?? []);
|
||||||
|
this.canListPrivateGame = hasListedPrivateGameFlare(flares);
|
||||||
|
if (!this.canListPrivateGame && this.listedPrivateGame) {
|
||||||
|
this.listedPrivateGame = false;
|
||||||
|
this.putGameConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createLobby(gameID: string): Promise<GameInfo> {
|
async function createLobby(gameID: string): Promise<GameInfo> {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "../client/Utils";
|
} from "../client/Utils";
|
||||||
import { assetUrl } from "../core/AssetUrls";
|
import { assetUrl } from "../core/AssetUrls";
|
||||||
import { EventBus } from "../core/EventBus";
|
import { EventBus } from "../core/EventBus";
|
||||||
|
import { LISTED_PRIVATE_GAME_TYPE } from "../core/ListedPrivateGame";
|
||||||
import {
|
import {
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
GAME_ID_REGEX,
|
GAME_ID_REGEX,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
GameRecordSchema,
|
GameRecordSchema,
|
||||||
LobbyInfoEvent,
|
LobbyInfoEvent,
|
||||||
PublicGameInfo,
|
PublicGameInfo,
|
||||||
|
PublicGames,
|
||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import {
|
import {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
@@ -55,6 +57,9 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
@state() private serverTimeOffset: number = 0;
|
@state() private serverTimeOffset: number = 0;
|
||||||
@state() private isConnecting: boolean = true;
|
@state() private isConnecting: boolean = true;
|
||||||
@state() private lobbyCreatorClientID: string | null = null;
|
@state() private lobbyCreatorClientID: string | null = null;
|
||||||
|
@state() private publicGames: PublicGames | null = null;
|
||||||
|
@state() private listedPrivateGamesExpanded: boolean = false;
|
||||||
|
@state() private expandedListedPrivateGameSettings: Set<string> = new Set();
|
||||||
|
|
||||||
private leaveLobbyOnClose = true;
|
private leaveLobbyOnClose = true;
|
||||||
private countdownTimerId: number | null = null;
|
private countdownTimerId: number | null = null;
|
||||||
@@ -77,6 +82,11 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly handlePublicLobbiesUpdate = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<{ payload: PublicGames }>;
|
||||||
|
this.publicGames = customEvent.detail.payload;
|
||||||
|
};
|
||||||
|
|
||||||
protected renderHeaderSlot() {
|
protected renderHeaderSlot() {
|
||||||
if (!this.currentLobbyId) {
|
if (!this.currentLobbyId) {
|
||||||
return modalHeader({
|
return modalHeader({
|
||||||
@@ -217,8 +227,25 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderJoinForm() {
|
private renderJoinForm() {
|
||||||
|
const listedPrivateGames =
|
||||||
|
this.publicGames?.games?.[LISTED_PRIVATE_GAME_TYPE] ?? [];
|
||||||
|
const visibleListedPrivateGames = this.listedPrivateGamesExpanded
|
||||||
|
? listedPrivateGames
|
||||||
|
: listedPrivateGames.slice(0, 3);
|
||||||
|
const hiddenListedPrivateGameCount =
|
||||||
|
listedPrivateGames.length - visibleListedPrivateGames.length;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<form @submit=${this.joinLobbyFromInput} class="custom-scrollbar p-6 space-y-4 mr-1">
|
<form
|
||||||
|
@submit=${this.joinLobbyFromInput}
|
||||||
|
class="custom-scrollbar p-6 space-y-4 mr-1"
|
||||||
|
>
|
||||||
|
<div class="pt-2 border-t border-white/10">
|
||||||
|
<h3
|
||||||
|
class="mb-3 text-sm font-bold text-white uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
${translateText("private_lobby.join_by_lobby_id")}
|
||||||
|
</h3>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -256,10 +283,219 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
></o-button>
|
></o-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 pt-2 border-t border-white/10">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<h3 class="text-sm font-bold text-white uppercase tracking-wider">
|
||||||
|
${translateText("private_lobby.listed_private_games")}
|
||||||
|
</h3>
|
||||||
|
${listedPrivateGames.length > 3
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-xs font-bold uppercase tracking-wider text-malibu-blue hover:text-aquarius transition-colors"
|
||||||
|
@click=${this.toggleListedPrivateGamesExpanded}
|
||||||
|
>
|
||||||
|
${this.listedPrivateGamesExpanded
|
||||||
|
? translateText("common.show_less")
|
||||||
|
: translateText(
|
||||||
|
"private_lobby.show_all_listed_private_games",
|
||||||
|
{
|
||||||
|
count: String(hiddenListedPrivateGameCount),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
${listedPrivateGames.length === 0
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="px-4 py-3 rounded-xl border border-white/10 bg-white/5 text-sm text-white/50"
|
||||||
|
>
|
||||||
|
${translateText("private_lobby.no_listed_private_games")}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div class="grid grid-cols-1 gap-2">
|
||||||
|
${visibleListedPrivateGames.map((lobby) =>
|
||||||
|
this.renderListedPrivateGame(lobby),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderListedPrivateGame(lobby: PublicGameInfo): TemplateResult {
|
||||||
|
const mapName = getMapName(lobby.gameConfig?.gameMap);
|
||||||
|
const settings = this.listedPrivateGameSettings(lobby.gameConfig);
|
||||||
|
const settingsExpanded = this.expandedListedPrivateGameSettings.has(
|
||||||
|
lobby.gameID,
|
||||||
|
);
|
||||||
|
const visibleSettings = settingsExpanded ? settings : settings.slice(0, 3);
|
||||||
|
const hiddenSettingsCount = settings.length - visibleSettings.length;
|
||||||
|
const mode =
|
||||||
|
lobby.gameConfig?.gameMode === GameMode.Team
|
||||||
|
? translateText("game_mode.teams")
|
||||||
|
: translateText("game_mode.ffa");
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="w-full min-w-0 px-3 py-3 rounded-xl border border-white/10 bg-white/5 text-left"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="min-w-0 space-y-1">
|
||||||
|
<div class="text-sm font-bold text-white truncate">
|
||||||
|
${mapName ?? lobby.gameConfig?.gameMap ?? lobby.gameID}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-white/55 uppercase tracking-wider">
|
||||||
|
${mode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${settings.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="grid grid-cols-1 gap-1">
|
||||||
|
${visibleSettings.map(
|
||||||
|
(setting) => html`
|
||||||
|
<div
|
||||||
|
class="w-full min-w-0 rounded bg-black/25 border border-white/10 px-2 py-1 text-[11px] font-bold uppercase tracking-wider text-white/70 truncate"
|
||||||
|
>
|
||||||
|
${setting}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${settings.length > 3
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="self-start text-[11px] font-bold uppercase tracking-wider text-malibu-blue hover:text-aquarius transition-colors"
|
||||||
|
@click=${() =>
|
||||||
|
this.toggleListedPrivateGameSettings(lobby.gameID)}
|
||||||
|
>
|
||||||
|
${settingsExpanded
|
||||||
|
? translateText("private_lobby.show_fewer_settings")
|
||||||
|
: translateText("private_lobby.show_all_settings", {
|
||||||
|
count: String(hiddenSettingsCount),
|
||||||
|
})}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="shrink-0 text-xs font-bold text-white/80">
|
||||||
|
${lobby.numClients}/${lobby.gameConfig?.maxPlayers ?? "-"}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2 rounded-lg bg-malibu-blue hover:bg-aquarius text-white text-xs font-bold uppercase tracking-wider transition-colors"
|
||||||
|
@click=${() => this.joinListedPrivateGame(lobby)}
|
||||||
|
>
|
||||||
|
${translateText("private_lobby.join_lobby")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private listedPrivateGameSettings(config: GameConfig | undefined): string[] {
|
||||||
|
if (!config) return [];
|
||||||
|
const settings: string[] = [];
|
||||||
|
const isTeam = config.gameMode === GameMode.Team;
|
||||||
|
const isCompact =
|
||||||
|
config.gameMapSize === GameMapSize.Compact ||
|
||||||
|
config.publicGameModifiers?.isCompact;
|
||||||
|
|
||||||
|
if (isCompact) settings.push(translateText("host_modal.compact_map"));
|
||||||
|
if (config.difficulty !== Difficulty.Easy) {
|
||||||
|
settings.push(
|
||||||
|
translateText(`difficulty.${config.difficulty.toLowerCase()}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.infiniteTroops)
|
||||||
|
settings.push(translateText("host_modal.infinite_troops"));
|
||||||
|
if (config.infiniteGold)
|
||||||
|
settings.push(translateText("host_modal.infinite_gold"));
|
||||||
|
if (config.instantBuild)
|
||||||
|
settings.push(translateText("host_modal.instant_build"));
|
||||||
|
if (config.randomSpawn)
|
||||||
|
settings.push(translateText("host_modal.random_spawn"));
|
||||||
|
if (config.maxTimerValue)
|
||||||
|
settings.push(
|
||||||
|
`${translateText("private_lobby.game_length")}: ${config.maxTimerValue} min`,
|
||||||
|
);
|
||||||
|
if (config.spawnImmunityDuration) {
|
||||||
|
const seconds = Math.round(config.spawnImmunityDuration / 10);
|
||||||
|
settings.push(
|
||||||
|
`${translateText("private_lobby.pvp_immunity")}: ${renderDuration(seconds)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.startingGold) {
|
||||||
|
const millions = parseFloat(
|
||||||
|
(config.startingGold / 1_000_000).toPrecision(12),
|
||||||
|
);
|
||||||
|
settings.push(
|
||||||
|
`${translateText("private_lobby.starting_gold")}: ${millions}M`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.goldMultiplier) {
|
||||||
|
settings.push(
|
||||||
|
`${translateText("host_modal.gold_multiplier")}: x${config.goldMultiplier}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.disableAlliances) {
|
||||||
|
settings.push(translateText("host_modal.disable_alliances"));
|
||||||
|
}
|
||||||
|
if (config.waterNukes) {
|
||||||
|
settings.push(translateText("host_modal.water_nukes"));
|
||||||
|
}
|
||||||
|
if ((isTeam && !config.donateGold) || (!isTeam && config.donateGold)) {
|
||||||
|
settings.push(translateText("host_modal.donate_gold"));
|
||||||
|
}
|
||||||
|
if ((isTeam && !config.donateTroops) || (!isTeam && config.donateTroops)) {
|
||||||
|
settings.push(translateText("host_modal.donate_troops"));
|
||||||
|
}
|
||||||
|
if (config.disabledUnits && config.disabledUnits.length > 0) {
|
||||||
|
settings.push(translateText("private_lobby.disabled_units"));
|
||||||
|
}
|
||||||
|
if (config.hostCheats) {
|
||||||
|
settings.push(translateText("private_lobby.host_cheats"));
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleListedPrivateGameSettings(gameID: string) {
|
||||||
|
const expanded = new Set(this.expandedListedPrivateGameSettings);
|
||||||
|
if (expanded.has(gameID)) {
|
||||||
|
expanded.delete(gameID);
|
||||||
|
} else {
|
||||||
|
expanded.add(gameID);
|
||||||
|
}
|
||||||
|
this.expandedListedPrivateGameSettings = expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleListedPrivateGamesExpanded = () => {
|
||||||
|
this.listedPrivateGamesExpanded = !this.listedPrivateGamesExpanded;
|
||||||
|
};
|
||||||
|
|
||||||
|
private joinListedPrivateGame(lobby: PublicGameInfo) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("join-lobby", {
|
||||||
|
detail: {
|
||||||
|
gameID: lobby.gameID,
|
||||||
|
source: "listed-private",
|
||||||
|
publicLobbyInfo: lobby,
|
||||||
|
} as JoinLobbyEvent,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected onOpen(args?: Record<string, unknown>): void {
|
protected onOpen(args?: Record<string, unknown>): void {
|
||||||
const lobbyId = typeof args?.lobbyId === "string" ? args.lobbyId : "";
|
const lobbyId = typeof args?.lobbyId === "string" ? args.lobbyId : "";
|
||||||
const lobbyInfo = args?.lobbyInfo as GameInfo | PublicGameInfo | undefined;
|
const lobbyInfo = args?.lobbyInfo as GameInfo | PublicGameInfo | undefined;
|
||||||
@@ -374,14 +610,28 @@ export class JoinLobbyModal extends BaseModal {
|
|||||||
this.lobbyCreatorClientID = null;
|
this.lobbyCreatorClientID = null;
|
||||||
this.isConnecting = true;
|
this.isConnecting = true;
|
||||||
this.leaveLobbyOnClose = true;
|
this.leaveLobbyOnClose = true;
|
||||||
|
this.listedPrivateGamesExpanded = false;
|
||||||
|
this.expandedListedPrivateGameSettings = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.clearCountdownTimer();
|
this.clearCountdownTimer();
|
||||||
this.stopLobbyUpdates();
|
this.stopLobbyUpdates();
|
||||||
|
document.removeEventListener(
|
||||||
|
"public-lobbies-update",
|
||||||
|
this.handlePublicLobbiesUpdate,
|
||||||
|
);
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
document.addEventListener(
|
||||||
|
"public-lobbies-update",
|
||||||
|
this.handlePublicLobbiesUpdate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public closeAndLeave() {
|
public closeAndLeave() {
|
||||||
this.leaveLobby();
|
this.leaveLobby();
|
||||||
try {
|
try {
|
||||||
|
|||||||
+8
-2
@@ -240,7 +240,13 @@ export interface JoinLobbyEvent {
|
|||||||
gameStartInfo?: GameStartInfo;
|
gameStartInfo?: GameStartInfo;
|
||||||
// GameRecord exists when replaying an archived game.
|
// GameRecord exists when replaying an archived game.
|
||||||
gameRecord?: GameRecord;
|
gameRecord?: GameRecord;
|
||||||
source?: "public" | "private" | "host" | "matchmaking" | "singleplayer";
|
source?:
|
||||||
|
| "public"
|
||||||
|
| "listed-private"
|
||||||
|
| "private"
|
||||||
|
| "host"
|
||||||
|
| "matchmaking"
|
||||||
|
| "singleplayer";
|
||||||
publicLobbyInfo?: GameInfo | PublicGameInfo;
|
publicLobbyInfo?: GameInfo | PublicGameInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +830,7 @@ class Client {
|
|||||||
this.lobbyHandle.stop(true);
|
this.lobbyHandle.stop(true);
|
||||||
document.body.classList.remove("in-game");
|
document.body.classList.remove("in-game");
|
||||||
}
|
}
|
||||||
if (lobby.source === "public") {
|
if (lobby.source === "public" || lobby.source === "listed-private") {
|
||||||
this.joinModal?.open({
|
this.joinModal?.open({
|
||||||
lobbyId: lobby.gameID,
|
lobbyId: lobby.gameID,
|
||||||
lobbyInfo: lobby.publicLobbyInfo,
|
lobbyInfo: lobby.publicLobbyInfo,
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ function renderSectionHeader(
|
|||||||
|
|
||||||
export interface ToggleOptionConfig {
|
export interface ToggleOptionConfig {
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
|
descriptionKey?: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
@@ -284,6 +285,27 @@ export class GameConfigSettings extends LitElement {
|
|||||||
private renderOptionToggle(toggle: ToggleOptionConfig): TemplateResult {
|
private renderOptionToggle(toggle: ToggleOptionConfig): TemplateResult {
|
||||||
if (toggle.hidden) return html``;
|
if (toggle.hidden) return html``;
|
||||||
|
|
||||||
|
if (toggle.descriptionKey) {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="${cardClass(toggle.checked, "p-4 text-center")}"
|
||||||
|
@click=${() => this.handleOptionToggle(toggle)}
|
||||||
|
aria-pressed=${toggle.checked}
|
||||||
|
>
|
||||||
|
<span class="${CARD_LABEL_CLASS} ${stateTextClass(toggle.checked)}">
|
||||||
|
${translateText(toggle.labelKey)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="block mt-2 text-[11px] normal-case font-medium tracking-normal leading-snug ${toggle.checked
|
||||||
|
? "text-white/80"
|
||||||
|
: "text-white/45"}"
|
||||||
|
>
|
||||||
|
${translateText(toggle.descriptionKey)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return renderTextCardButton(
|
return renderTextCardButton(
|
||||||
translateText(toggle.labelKey),
|
translateText(toggle.labelKey),
|
||||||
toggle.checked,
|
toggle.checked,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const LISTED_PRIVATE_GAME_FLARE = "game:*";
|
||||||
|
export const LISTED_PRIVATE_GAME_TYPE = "listed-private";
|
||||||
|
|
||||||
|
export function hasListedPrivateGameFlare(flares: readonly string[]): boolean {
|
||||||
|
return true;
|
||||||
|
// return flares.includes(LISTED_PRIVATE_GAME_FLARE);
|
||||||
|
}
|
||||||
+8
-1
@@ -20,6 +20,7 @@ import {
|
|||||||
Trios,
|
Trios,
|
||||||
UnitType,
|
UnitType,
|
||||||
} from "./game/Game";
|
} from "./game/Game";
|
||||||
|
import { LISTED_PRIVATE_GAME_TYPE } from "./ListedPrivateGame";
|
||||||
import { PlayerStatsSchema } from "./StatsSchemas";
|
import { PlayerStatsSchema } from "./StatsSchemas";
|
||||||
import { flattenedEmojiTable } from "./Util";
|
import { flattenedEmojiTable } from "./Util";
|
||||||
|
|
||||||
@@ -139,7 +140,12 @@ export type PublicGames = z.infer<typeof PublicGamesSchema>;
|
|||||||
export type PublicGameInfo = z.infer<typeof PublicGameInfoSchema>;
|
export type PublicGameInfo = z.infer<typeof PublicGameInfoSchema>;
|
||||||
export type PublicGameType = z.infer<typeof PublicGameTypeSchema>;
|
export type PublicGameType = z.infer<typeof PublicGameTypeSchema>;
|
||||||
|
|
||||||
export const PublicGameTypeSchema = z.enum(["ffa", "team", "special"]);
|
export const PublicGameTypeSchema = z.enum([
|
||||||
|
"ffa",
|
||||||
|
"team",
|
||||||
|
"special",
|
||||||
|
LISTED_PRIVATE_GAME_TYPE,
|
||||||
|
] as const);
|
||||||
|
|
||||||
export const UsernameSchema = z
|
export const UsernameSchema = z
|
||||||
.string()
|
.string()
|
||||||
@@ -221,6 +227,7 @@ export const GameConfigSchema = z.object({
|
|||||||
donateTroops: z.boolean(), // Configures donations to humans only
|
donateTroops: z.boolean(), // Configures donations to humans only
|
||||||
gameType: z.enum(GameType),
|
gameType: z.enum(GameType),
|
||||||
gameMode: z.enum(GameMode),
|
gameMode: z.enum(GameMode),
|
||||||
|
listedPrivateGame: z.boolean().optional(),
|
||||||
rankedType: z.enum(RankedType).optional(), // Only set for ranked games.
|
rankedType: z.enum(RankedType).optional(), // Only set for ranked games.
|
||||||
gameMapSize: z.enum(GameMapSize),
|
gameMapSize: z.enum(GameMapSize),
|
||||||
publicGameModifiers: z
|
publicGameModifiers: z
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ export class GameManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public listedPrivateLobbies(): GameServer[] {
|
||||||
|
return Array.from(this.games.values()).filter(
|
||||||
|
(g) =>
|
||||||
|
g.phase() === GamePhase.Lobby &&
|
||||||
|
!g.isPublic() &&
|
||||||
|
g.gameConfig.listedPrivateGame === true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
joinClient(
|
joinClient(
|
||||||
client: Client,
|
client: Client,
|
||||||
gameID: GameID,
|
gameID: GameID,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|||||||
import { isAdminRole } from "../core/ApiSchemas";
|
import { isAdminRole } from "../core/ApiSchemas";
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
|
import { hasListedPrivateGameFlare } from "../core/ListedPrivateGame";
|
||||||
import {
|
import {
|
||||||
ClientID,
|
ClientID,
|
||||||
ClientMessageSchema,
|
ClientMessageSchema,
|
||||||
@@ -177,6 +178,9 @@ export class GameServer {
|
|||||||
if (gameConfig.waterNukes !== undefined) {
|
if (gameConfig.waterNukes !== undefined) {
|
||||||
this.gameConfig.waterNukes = gameConfig.waterNukes ?? undefined;
|
this.gameConfig.waterNukes = gameConfig.waterNukes ?? undefined;
|
||||||
}
|
}
|
||||||
|
if (gameConfig.listedPrivateGame !== undefined) {
|
||||||
|
this.gameConfig.listedPrivateGame = gameConfig.listedPrivateGame;
|
||||||
|
}
|
||||||
this.gameConfig.hostCheats = gameConfig.hostCheats;
|
this.gameConfig.hostCheats = gameConfig.hostCheats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +477,20 @@ export class GameServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
stampedIntent.config.listedPrivateGame === true &&
|
||||||
|
!hasListedPrivateGameFlare(client.flares ?? [])
|
||||||
|
) {
|
||||||
|
this.log.warn(
|
||||||
|
`Client without required flare attempted to list private game`,
|
||||||
|
{
|
||||||
|
gameID: this.id,
|
||||||
|
clientID: client.clientID,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.log.info(
|
this.log.info(
|
||||||
`Lobby creator updated game config via WebSocket`,
|
`Lobby creator updated game config via WebSocket`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -189,14 +189,16 @@ const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [
|
|||||||
["isNukesDisabled", "isWaterNukes"],
|
["isNukesDisabled", "isWaterNukes"],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type ScheduledPublicGameType = Exclude<PublicGameType, "listed-private">;
|
||||||
|
|
||||||
export class MapPlaylist {
|
export class MapPlaylist {
|
||||||
private playlists: Record<PublicGameType, GameMapType[]> = {
|
private playlists: Record<ScheduledPublicGameType, GameMapType[]> = {
|
||||||
ffa: [],
|
ffa: [],
|
||||||
special: [],
|
special: [],
|
||||||
team: [],
|
team: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
public async gameConfig(type: PublicGameType): Promise<GameConfig> {
|
public async gameConfig(type: ScheduledPublicGameType): Promise<GameConfig> {
|
||||||
if (type === "special") {
|
if (type === "special") {
|
||||||
return this.getSpecialConfig();
|
return this.getSpecialConfig();
|
||||||
}
|
}
|
||||||
@@ -488,7 +490,7 @@ export class MapPlaylist {
|
|||||||
} satisfies GameConfig;
|
} satisfies GameConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNextMap(type: PublicGameType): GameMapType {
|
private getNextMap(type: ScheduledPublicGameType): GameMapType {
|
||||||
const playlist = this.playlists[type];
|
const playlist = this.playlists[type];
|
||||||
if (playlist.length === 0) {
|
if (playlist.length === 0) {
|
||||||
playlist.push(...this.generateNewPlaylist(type));
|
playlist.push(...this.generateNewPlaylist(type));
|
||||||
@@ -496,7 +498,7 @@ export class MapPlaylist {
|
|||||||
return playlist.shift()!;
|
return playlist.shift()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateNewPlaylist(type: PublicGameType): GameMapType[] {
|
private generateNewPlaylist(type: ScheduledPublicGameType): GameMapType[] {
|
||||||
const maps = this.buildMapsList(type);
|
const maps = this.buildMapsList(type);
|
||||||
const rand = new PseudoRandom(Date.now());
|
const rand = new PseudoRandom(Date.now());
|
||||||
const playlist: GameMapType[] = [];
|
const playlist: GameMapType[] = [];
|
||||||
@@ -545,7 +547,7 @@ export class MapPlaylist {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMapsList(type: PublicGameType): GameMapType[] {
|
private buildMapsList(type: ScheduledPublicGameType): GameMapType[] {
|
||||||
const maps: GameMapType[] = [];
|
const maps: GameMapType[] = [];
|
||||||
(Object.keys(GameMapType) as GameMapName[]).forEach((key) => {
|
(Object.keys(GameMapType) as GameMapName[]).forEach((key) => {
|
||||||
const map = GameMapType[key];
|
const map = GameMapType[key];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Worker } from "cluster";
|
import { Worker } from "cluster";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
|
import { LISTED_PRIVATE_GAME_TYPE } from "../core/ListedPrivateGame";
|
||||||
import { PublicGameInfo, PublicGameType } from "../core/Schemas";
|
import { PublicGameInfo, PublicGameType } from "../core/Schemas";
|
||||||
import { generateID } from "../core/Util";
|
import { generateID } from "../core/Util";
|
||||||
import {
|
import {
|
||||||
@@ -85,9 +86,13 @@ export class MasterLobbyService {
|
|||||||
ffa: [],
|
ffa: [],
|
||||||
team: [],
|
team: [],
|
||||||
special: [],
|
special: [],
|
||||||
|
[LISTED_PRIVATE_GAME_TYPE]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const lobby of lobbies) {
|
for (const lobby of lobbies) {
|
||||||
|
if (!result[lobby.publicGameType]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
result[lobby.publicGameType].push(lobby);
|
result[lobby.publicGameType].push(lobby);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +136,7 @@ export class MasterLobbyService {
|
|||||||
private async maybeScheduleLobby() {
|
private async maybeScheduleLobby() {
|
||||||
const lobbiesByType = this.getAllLobbies();
|
const lobbiesByType = this.getAllLobbies();
|
||||||
|
|
||||||
for (const type of Object.keys(lobbiesByType) as PublicGameType[]) {
|
for (const type of ["ffa", "team", "special"] as const) {
|
||||||
const lobbies = lobbiesByType[type];
|
const lobbies = lobbiesByType[type];
|
||||||
|
|
||||||
// Always ensure the next lobby has a timer, even if we already have 2+
|
// Always ensure the next lobby has a timer, even if we already have 2+
|
||||||
|
|||||||
+22
-2
@@ -9,6 +9,7 @@ import { WebSocket, WebSocketServer } from "ws";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
|
import { hasListedPrivateGameFlare } from "../core/ListedPrivateGame";
|
||||||
import {
|
import {
|
||||||
ClientMessageSchema,
|
ClientMessageSchema,
|
||||||
GameID,
|
GameID,
|
||||||
@@ -145,10 +146,11 @@ export async function startWorker() {
|
|||||||
// Extract persistentID from Authorization header token
|
// Extract persistentID from Authorization header token
|
||||||
// Never accept persistentID directly from client
|
// Never accept persistentID directly from client
|
||||||
let creatorPersistentID: string | undefined;
|
let creatorPersistentID: string | undefined;
|
||||||
|
let creatorToken: string | undefined;
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (authHeader?.startsWith("Bearer ")) {
|
if (authHeader?.startsWith("Bearer ")) {
|
||||||
const token = authHeader.substring("Bearer ".length);
|
creatorToken = authHeader.substring("Bearer ".length);
|
||||||
const result = await verifyClientToken(token);
|
const result = await verifyClientToken(creatorToken);
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
creatorPersistentID = result.persistentId;
|
creatorPersistentID = result.persistentId;
|
||||||
} else {
|
} else {
|
||||||
@@ -186,6 +188,24 @@ export async function startWorker() {
|
|||||||
return res.status(401).send("Unauthorized");
|
return res.status(401).send("Unauthorized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gc?.listedPrivateGame === true) {
|
||||||
|
if (creatorToken === undefined) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
const userMe = await getUserMe(creatorToken);
|
||||||
|
if (userMe.type === "error") {
|
||||||
|
log.warn(`cannot verify listed private game flare: ${userMe.message}`);
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
const creatorFlares = userMe.response.player.flares ?? [];
|
||||||
|
if (!hasListedPrivateGameFlare(creatorFlares)) {
|
||||||
|
log.warn(
|
||||||
|
`Forbidden: player without game:* flare attempted to create listed private game`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Double-check this worker should host this game
|
// Double-check this worker should host this game
|
||||||
const expectedWorkerId = ServerEnv.workerIndex(id);
|
const expectedWorkerId = ServerEnv.workerIndex(id);
|
||||||
if (expectedWorkerId !== workerId) {
|
if (expectedWorkerId !== workerId) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import http from "http";
|
import http from "http";
|
||||||
import { WebSocket, WebSocketServer } from "ws";
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { PublicGameInfo, PublicGames } from "../core/Schemas";
|
import { LISTED_PRIVATE_GAME_TYPE } from "../core/ListedPrivateGame";
|
||||||
|
import { GameInfo, PublicGameInfo, PublicGames } from "../core/Schemas";
|
||||||
import { GameManager } from "./GameManager";
|
import { GameManager } from "./GameManager";
|
||||||
import {
|
import {
|
||||||
MasterMessageSchema,
|
MasterMessageSchema,
|
||||||
@@ -83,18 +84,27 @@ export class WorkerLobbyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendMyLobbiesToMaster() {
|
private sendMyLobbiesToMaster() {
|
||||||
const lobbies = this.gm
|
const toLobbyInfo = (
|
||||||
|
gi: GameInfo,
|
||||||
|
publicGameType: PublicGameInfo["publicGameType"],
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
gameID: gi.gameID,
|
||||||
|
numClients: gi.clients?.length ?? 0,
|
||||||
|
startsAt: gi.startsAt,
|
||||||
|
gameConfig: gi.gameConfig,
|
||||||
|
publicGameType,
|
||||||
|
} satisfies PublicGameInfo;
|
||||||
|
};
|
||||||
|
const publicLobbies = this.gm
|
||||||
.publicLobbies()
|
.publicLobbies()
|
||||||
.map((g) => g.gameInfo())
|
.map((g) => g.gameInfo())
|
||||||
.map((gi) => {
|
.map((gi) => toLobbyInfo(gi, gi.publicGameType!));
|
||||||
return {
|
const listedPrivateLobbies = this.gm
|
||||||
gameID: gi.gameID,
|
.listedPrivateLobbies()
|
||||||
numClients: gi.clients?.length ?? 0,
|
.map((g) => g.gameInfo())
|
||||||
startsAt: gi.startsAt,
|
.map((gi) => toLobbyInfo(gi, LISTED_PRIVATE_GAME_TYPE));
|
||||||
gameConfig: gi.gameConfig,
|
const lobbies = [...publicLobbies, ...listedPrivateLobbies];
|
||||||
publicGameType: gi.publicGameType!,
|
|
||||||
} satisfies PublicGameInfo;
|
|
||||||
});
|
|
||||||
process.send?.({ type: "lobbyList", lobbies } satisfies WorkerLobbyList);
|
process.send?.({ type: "lobbyList", lobbies } satisfies WorkerLobbyList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user