mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:30:43 +00:00
Better CrazyGames integration (#3055)
## Description: Better integration with CrazyGames: * Don't show login because accounts have not been integrated with CrazyGames yet * Integrate CG invite links & usernames * Refactor match making logic to Matchmaking.ts * Allow periods to support crazy game usernames * Create a no-crazygames class that disabled elements when on crazygames ## 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: evan
This commit is contained in:
+155
-6
@@ -3,6 +3,28 @@ declare global {
|
||||
CrazyGames?: {
|
||||
SDK: {
|
||||
init: () => Promise<void>;
|
||||
user: {
|
||||
getUser(): Promise<{
|
||||
username: string;
|
||||
} | null>;
|
||||
addAuthListener: (
|
||||
listener: (
|
||||
user: {
|
||||
username: string;
|
||||
} | null,
|
||||
) => void,
|
||||
) => void;
|
||||
};
|
||||
ad: {
|
||||
requestAd: (
|
||||
adType: string,
|
||||
callbacks: {
|
||||
adStarted: () => void;
|
||||
adFinished: () => void;
|
||||
adError: (error: any) => void;
|
||||
},
|
||||
) => void;
|
||||
};
|
||||
game: {
|
||||
gameplayStart: () => Promise<void>;
|
||||
gameplayStop: () => Promise<void>;
|
||||
@@ -14,7 +36,9 @@ declare global {
|
||||
[key: string]: string | number;
|
||||
}) => string;
|
||||
hideInviteButton: () => void;
|
||||
inviteLink: (params: { [key: string]: string | number }) => string;
|
||||
getInviteParam: (paramName: string) => string | null;
|
||||
isInstantMultiplayer?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -24,6 +48,24 @@ declare global {
|
||||
export class CrazyGamesSDK {
|
||||
private initialized = false;
|
||||
private isGameplayActive = false;
|
||||
private readyPromise: Promise<void>;
|
||||
private resolveReady!: () => void;
|
||||
|
||||
constructor() {
|
||||
this.readyPromise = new Promise((resolve) => {
|
||||
this.resolveReady = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
async ready(): Promise<boolean> {
|
||||
const timeout = new Promise<boolean>((resolve) => {
|
||||
setTimeout(() => resolve(false), 3000);
|
||||
});
|
||||
|
||||
const ready = this.readyPromise.then(() => true);
|
||||
|
||||
return Promise.race([ready, timeout]);
|
||||
}
|
||||
|
||||
isOnCrazyGames(): boolean {
|
||||
try {
|
||||
@@ -34,9 +76,17 @@ export class CrazyGamesSDK {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log("[CrazyGames]: ", e);
|
||||
// If we get a cross-origin error, we're definitely iframed
|
||||
// Check our own referrer as fallback
|
||||
return document.referrer.includes("crazygames");
|
||||
const isCrazyGames = document.referrer.includes("crazygames");
|
||||
console.log("[CrazyGames], contains referrer: ", isCrazyGames);
|
||||
if (isCrazyGames) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: on safari private we can't get referrer, so just assume we are in crazygames if in iframe
|
||||
return window.self !== window.top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,12 +120,63 @@ export class CrazyGamesSDK {
|
||||
try {
|
||||
await window.CrazyGames.SDK.init();
|
||||
this.initialized = true;
|
||||
this.resolveReady();
|
||||
console.log("CrazyGames SDK initialized");
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize CrazyGames SDK:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async getUsername(): Promise<string | null> {
|
||||
const isReady = await this.ready();
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (await window.CrazyGames!.SDK.user.getUser())?.username ?? null;
|
||||
} catch (e) {
|
||||
console.log("error getting CrazyGames username: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async addAuthListener(
|
||||
listener: (
|
||||
user: {
|
||||
username: string;
|
||||
} | null,
|
||||
) => void,
|
||||
): Promise<void> {
|
||||
if (!(await this.ready())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("registering CrazyGames auth listener");
|
||||
window.CrazyGames!.SDK.user.addAuthListener(listener);
|
||||
} catch (error) {
|
||||
console.error("Failed to add auth listener:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async isInstantMultiplayer(): Promise<boolean> {
|
||||
const isReady = await this.ready();
|
||||
if (!isReady) {
|
||||
return false;
|
||||
}
|
||||
const gameId = await this.getInviteGameId();
|
||||
if (gameId !== null) {
|
||||
// Game id exists, meaning we are joining the game, not hosting it.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return window.CrazyGames!.SDK.game.isInstantMultiplayer ?? false;
|
||||
} catch (e) {
|
||||
console.log("Error getting instant multiplayer: ", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async gameplayStart(): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return;
|
||||
@@ -156,7 +257,6 @@ export class CrazyGamesSDK {
|
||||
if (!this.isReady()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const options: {
|
||||
gameId: string | number;
|
||||
@@ -165,6 +265,9 @@ export class CrazyGamesSDK {
|
||||
gameId,
|
||||
};
|
||||
const link = window.CrazyGames!.SDK.game.showInviteButton(options);
|
||||
// Store the game so we know that we are host. This way when player refreshes page,
|
||||
// It won't show up as "joining" a game we created.
|
||||
localStorage.setItem(gameId, "true");
|
||||
console.log("CrazyGames: invite button shown, link:", link);
|
||||
return link;
|
||||
} catch (error) {
|
||||
@@ -186,20 +289,66 @@ export class CrazyGamesSDK {
|
||||
}
|
||||
}
|
||||
|
||||
getInviteGameId(): string | null {
|
||||
createInviteLink(gameId: string): string | null {
|
||||
if (!this.isReady()) {
|
||||
console.warn("CrazyGames SDK not ready, cannot create invite link");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const value = window.CrazyGames!.SDK.game.getInviteParam("gameId");
|
||||
console.log(`CrazyGames: got invite gameId:`, value);
|
||||
return value;
|
||||
const link = window.CrazyGames!.SDK.game.inviteLink({ gameId });
|
||||
console.log("CrazyGames: created invite link:", link);
|
||||
return link;
|
||||
} catch (error) {
|
||||
console.error("Failed to create invite link:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getInviteGameId(): Promise<string | null> {
|
||||
if (!(await this.ready())) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const gameId = window.CrazyGames!.SDK.game.getInviteParam("gameId");
|
||||
if (gameId) {
|
||||
console.log("[CrazyGames] found invite link", gameId);
|
||||
// We already created this game, can't join a game we created.
|
||||
return localStorage.getItem(gameId) === "true" ? null : gameId;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Failed to get invite gameId:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
requestMidgameAd(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (!this.isReady()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const callbacks = {
|
||||
adFinished: () => {
|
||||
console.log("End midgame ad");
|
||||
resolve();
|
||||
},
|
||||
adError: (error: any) => {
|
||||
console.log("Error midgame ad", error);
|
||||
resolve();
|
||||
},
|
||||
adStarted: () => console.log("Start midgame ad"),
|
||||
};
|
||||
window.CrazyGames!.SDK.ad.requestAd("midgame", callbacks);
|
||||
} catch (error) {
|
||||
console.error("Failed to request midgame ad:", error);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const crazyGamesSDK = new CrazyGamesSDK();
|
||||
|
||||
@@ -113,6 +113,12 @@ export class HostLobbyModal extends BaseModal {
|
||||
}
|
||||
|
||||
private async buildLobbyUrl(): Promise<string> {
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
const link = crazyGamesSDK.createInviteLink(this.lobbyId);
|
||||
if (link !== null) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
const config = await getServerConfigFromClient();
|
||||
return `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}?lobby&s=${encodeURIComponent(this.lobbyUrlSuffix)}`;
|
||||
}
|
||||
@@ -123,7 +129,9 @@ export class HostLobbyModal extends BaseModal {
|
||||
}
|
||||
|
||||
private updateHistory(url: string): void {
|
||||
history.replaceState(null, "", url);
|
||||
if (!crazyGamesSDK.isOnCrazyGames()) {
|
||||
history.replaceState(null, "", url);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -228,6 +228,7 @@ export class LangSelector extends LitElement {
|
||||
"stats-modal",
|
||||
"flag-input-modal",
|
||||
"flag-input",
|
||||
"matchmaking-button",
|
||||
"token-login",
|
||||
];
|
||||
|
||||
|
||||
+56
-55
@@ -7,7 +7,7 @@ import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { GameType } from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./AccountModal";
|
||||
import { getUserMe, hasLinkedAccount } from "./Api";
|
||||
import { getUserMe } from "./Api";
|
||||
import { userAuth } from "./Auth";
|
||||
import { joinLobby } from "./ClientGameRunner";
|
||||
import { fetchCosmetics } from "./Cosmetics";
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
} from "./Transport";
|
||||
import { UserSettingModal } from "./UserSettingModal";
|
||||
import "./UsernameInput";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { genAnonUsername, UsernameInput } from "./UsernameInput";
|
||||
import {
|
||||
getDiscordAvatarUrl,
|
||||
incrementGamesPlayed,
|
||||
@@ -209,6 +209,7 @@ class Client {
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
|
||||
private hostModal: HostPrivateLobbyModal;
|
||||
private joinModal: JoinPrivateLobbyModal;
|
||||
private publicLobby: PublicLobby;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
@@ -426,56 +427,14 @@ class Client {
|
||||
) {
|
||||
console.warn("Matchmaking modal element not found");
|
||||
}
|
||||
const matchmakingButton = document.getElementById("matchmaking-button");
|
||||
const matchmakingButtonLoggedOut = document.getElementById(
|
||||
"matchmaking-button-logged-out",
|
||||
);
|
||||
|
||||
const updateMatchmakingButton = (loggedIn: boolean) => {
|
||||
if (!loggedIn) {
|
||||
matchmakingButton?.classList.add("hidden");
|
||||
matchmakingButtonLoggedOut?.classList.remove("hidden");
|
||||
} else {
|
||||
matchmakingButton?.classList.remove("hidden");
|
||||
matchmakingButtonLoggedOut?.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
if (matchmakingButton) {
|
||||
matchmakingButton.addEventListener("click", () => {
|
||||
if (this.usernameInput?.isValid()) {
|
||||
window.showPage?.("page-matchmaking");
|
||||
this.publicLobby.leaveLobby();
|
||||
} else {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: this.usernameInput?.validationError,
|
||||
color: "red",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (matchmakingButtonLoggedOut) {
|
||||
matchmakingButtonLoggedOut.addEventListener("click", () => {
|
||||
window.showPage?.("page-account");
|
||||
});
|
||||
}
|
||||
|
||||
const onUserMe = async (userMeResponse: UserMeResponse | false) => {
|
||||
// Check if user has actual authentication (discord or email), not just a publicId
|
||||
const isLinked: boolean = hasLinkedAccount(userMeResponse);
|
||||
updateMatchmakingButton(isLinked);
|
||||
updateAccountNavButton(userMeResponse);
|
||||
const adsEnabled =
|
||||
const hasLinkedAccount =
|
||||
!crazyGamesSDK.isOnCrazyGames() &&
|
||||
((userMeResponse || null)?.player?.flares?.length ?? 0) === 0;
|
||||
console.log("ads enabled: ", adsEnabled);
|
||||
window.adsEnabled = adsEnabled;
|
||||
((userMeResponse || null)?.player?.flares?.length ?? 0) > 0;
|
||||
console.log("ads enabled: ", hasLinkedAccount);
|
||||
window.adsEnabled = !hasLinkedAccount && !crazyGamesSDK.isOnCrazyGames();
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("userMeResponse", {
|
||||
detail: userMeResponse,
|
||||
@@ -516,10 +475,10 @@ class Client {
|
||||
}
|
||||
});
|
||||
|
||||
const hostModal = document.querySelector(
|
||||
this.hostModal = document.querySelector(
|
||||
"host-lobby-modal",
|
||||
) as HostPrivateLobbyModal;
|
||||
if (!hostModal || !(hostModal instanceof HostPrivateLobbyModal)) {
|
||||
if (!this.hostModal || !(this.hostModal instanceof HostPrivateLobbyModal)) {
|
||||
console.warn("Host private lobby modal element not found");
|
||||
}
|
||||
const hostLobbyButton = document.getElementById("host-lobby-button");
|
||||
@@ -575,7 +534,11 @@ class Client {
|
||||
}
|
||||
|
||||
// Attempt to join lobby
|
||||
this.handleUrl();
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => this.handleUrl());
|
||||
} else {
|
||||
this.handleUrl();
|
||||
}
|
||||
|
||||
const onHashUpdate = () => {
|
||||
// Reset the UI to its initial state
|
||||
@@ -647,17 +610,36 @@ class Client {
|
||||
});
|
||||
}
|
||||
|
||||
private handleUrl() {
|
||||
private async handleUrl() {
|
||||
// Wait for modal custom elements to be defined
|
||||
await Promise.all([
|
||||
customElements.whenDefined("join-private-lobby-modal"),
|
||||
customElements.whenDefined("host-lobby-modal"),
|
||||
]);
|
||||
|
||||
// Check if CrazyGames SDK is enabled first (no hash needed in CrazyGames)
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
const lobbyId = crazyGamesSDK.getInviteGameId();
|
||||
const lobbyId = await crazyGamesSDK.getInviteGameId();
|
||||
console.log("got game id", lobbyId);
|
||||
if (lobbyId && GAME_ID_REGEX.test(lobbyId)) {
|
||||
console.log("game parsed successfully");
|
||||
// Wait 2 seconds to ensure all elements are actually loaded,
|
||||
// On low end-chromebooks the join modal was not registered in time.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
window.showPage?.("page-join-private-lobby");
|
||||
this.joinModal?.open(lobbyId);
|
||||
console.log(`CrazyGames: joining lobby ${lobbyId} from invite param`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
crazyGamesSDK.isInstantMultiplayer().then((isInstant) => {
|
||||
if (isInstant) {
|
||||
console.log(
|
||||
`CrazyGames: joining instant multiplayer lobby from CrazyGames`,
|
||||
);
|
||||
this.hostModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
const strip = () =>
|
||||
history.replaceState(
|
||||
@@ -780,7 +762,8 @@ class Client {
|
||||
: this.flagInput.getCurrentFlag(),
|
||||
},
|
||||
turnstileToken: await this.getTurnstileToken(lobby),
|
||||
playerName: this.usernameInput?.getCurrentUsername() ?? "",
|
||||
playerName:
|
||||
this.usernameInput?.getCurrentUsername() ?? genAnonUsername(),
|
||||
clientID: lobby.clientID,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info,
|
||||
gameRecord: lobby.gameRecord,
|
||||
@@ -926,7 +909,8 @@ class Client {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.turnstileTokenPromise === null) {
|
||||
// Always request a new token on crazygames.
|
||||
if (this.turnstileTokenPromise === null || crazyGamesSDK.isOnCrazyGames()) {
|
||||
console.log("No prefetched turnstile token, getting new token");
|
||||
return (await getTurnstileToken())?.token ?? null;
|
||||
}
|
||||
@@ -942,6 +926,7 @@ class Client {
|
||||
const tokenTTL = 3 * 60 * 1000;
|
||||
if (Date.now() < token.createdAt + tokenTTL) {
|
||||
console.log("Prefetched turnstile token is valid");
|
||||
|
||||
return token.token;
|
||||
} else {
|
||||
console.log("Turnstile token expired, getting new token");
|
||||
@@ -950,11 +935,27 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Hide elements with no-crazygames class if on CrazyGames
|
||||
const hideCrazyGamesElements = () => {
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
document.querySelectorAll(".no-crazygames").forEach((el) => {
|
||||
(el as HTMLElement).style.display = "none";
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
const bootstrap = () => {
|
||||
initLayout();
|
||||
new Client().initialize();
|
||||
initNavigation();
|
||||
|
||||
// Hide elements immediately
|
||||
hideCrazyGamesElements();
|
||||
|
||||
// Also hide elements after a short delay to catch late-rendered components
|
||||
setTimeout(hideCrazyGamesElements, 100);
|
||||
setTimeout(hideCrazyGamesElements, 500);
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customElement, query, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { generateID } from "../core/Util";
|
||||
import { getUserMe } from "./Api";
|
||||
import { getUserMe, hasLinkedAccount } from "./Api";
|
||||
import { getPlayToken } from "./Auth";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/Difficulties";
|
||||
@@ -240,6 +240,7 @@ export class MatchmakingModal extends BaseModal {
|
||||
@customElement("matchmaking-button")
|
||||
export class MatchmakingButton extends LitElement {
|
||||
@query("matchmaking-modal") private matchmakingModal?: MatchmakingModal;
|
||||
@state() private isLoggedIn = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -247,6 +248,14 @@ export class MatchmakingButton extends LitElement {
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Listen for user authentication changes
|
||||
document.addEventListener("userMeResponse", (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
if (customEvent.detail) {
|
||||
const userMeResponse = customEvent.detail as UserMeResponse | false;
|
||||
this.isLoggedIn = hasLinkedAccount(userMeResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -254,19 +263,65 @@ export class MatchmakingButton extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.isLoggedIn) {
|
||||
return html`
|
||||
<button
|
||||
@click="${this.handleLoggedInClick}"
|
||||
class="no-crazygames w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center group overflow-hidden relative"
|
||||
title="${translateText("matchmaking_modal.title")}"
|
||||
>
|
||||
<span class="relative z-10 text-2xl">
|
||||
${translateText("matchmaking_button.play_ranked")}
|
||||
</span>
|
||||
<span
|
||||
class="relative z-10 text-xs font-medium text-purple-100 opacity-90 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
${translateText("matchmaking_button.description")}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<matchmaking-modal></matchmaking-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="z-9999">
|
||||
<o-button
|
||||
@click="${this.open}"
|
||||
translationKey="matchmaking_modal.title"
|
||||
block
|
||||
secondary
|
||||
></o-button>
|
||||
</div>
|
||||
<button
|
||||
@click="${this.handleLoggedOutClick}"
|
||||
class="no-crazygames w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center overflow-hidden relative cursor-pointer"
|
||||
>
|
||||
<span class="relative z-10 text-2xl">
|
||||
${translateText("matchmaking_button.login_required")}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<matchmaking-modal></matchmaking-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleLoggedInClick() {
|
||||
const usernameInput = document.querySelector("username-input") as any;
|
||||
const publicLobby = document.querySelector("public-lobby") as any;
|
||||
|
||||
if (usernameInput?.isValid()) {
|
||||
this.open();
|
||||
publicLobby?.leaveLobby();
|
||||
} else {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: usernameInput?.validationError,
|
||||
color: "red",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleLoggedOutClick() {
|
||||
window.showPage?.("page-account");
|
||||
}
|
||||
|
||||
private open() {
|
||||
this.matchmakingModal?.open();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UserSettings } from "../core/game/UserSettings";
|
||||
import { PlayerPattern } from "../core/Schemas";
|
||||
import { renderPatternPreview } from "./components/PatternButton";
|
||||
import { fetchCosmetics } from "./Cosmetics";
|
||||
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
||||
import { translateText } from "./Utils";
|
||||
|
||||
@customElement("pattern-input")
|
||||
@@ -73,6 +74,10 @@ export class PatternInput extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const isDefault = this.pattern === null && this.selectedColor === null;
|
||||
const showSelect = this.showSelectLabel && isDefault;
|
||||
const buttonTitle = translateText("territory_patterns.title");
|
||||
|
||||
@@ -27,6 +27,7 @@ import "./components/FluentSlider";
|
||||
import "./components/Maps";
|
||||
import { modalHeader } from "./components/ui/ModalHeader";
|
||||
import { fetchCosmetics } from "./Cosmetics";
|
||||
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
||||
import { FlagInput } from "./FlagInput";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
@@ -89,6 +90,9 @@ export class SinglePlayerModal extends BaseModal {
|
||||
};
|
||||
|
||||
private renderNotLoggedInBanner(): TemplateResult {
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div
|
||||
class="px-3 py-2 text-xs font-bold uppercase tracking-wider transition-colors duration-200 rounded-lg bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 whitespace-nowrap shrink-0"
|
||||
>
|
||||
@@ -1057,6 +1061,8 @@ export class SinglePlayerModal extends BaseModal {
|
||||
|
||||
const selectedColor = this.userSettings.getSelectedColor();
|
||||
|
||||
await crazyGamesSDK.requestMidgameAd();
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
|
||||
+24
-11
@@ -8,6 +8,7 @@ import {
|
||||
MIN_USERNAME_LENGTH,
|
||||
validateUsername,
|
||||
} from "../core/validations/username";
|
||||
import { crazyGamesSDK } from "./CrazyGamesSDK";
|
||||
|
||||
const usernameKey: string = "username";
|
||||
|
||||
@@ -39,8 +40,18 @@ export class UsernameInput extends LitElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const stored = this.getStoredUsername();
|
||||
const stored = this.getUsername();
|
||||
this.parseAndSetUsername(stored);
|
||||
crazyGamesSDK.getUsername().then((username) => {
|
||||
this.parseAndSetUsername(username ?? genAnonUsername());
|
||||
this.requestUpdate();
|
||||
});
|
||||
crazyGamesSDK.addAuthListener((user) => {
|
||||
if (user) {
|
||||
this.parseAndSetUsername(user?.username);
|
||||
}
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private parseAndSetUsername(fullUsername: string) {
|
||||
@@ -52,6 +63,8 @@ export class UsernameInput extends LitElement {
|
||||
this.clanTag = "";
|
||||
this.baseUsername = fullUsername;
|
||||
}
|
||||
|
||||
this.validateAndStore();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -161,7 +174,7 @@ export class UsernameInput extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private getStoredUsername(): string {
|
||||
private getUsername(): string {
|
||||
const storedUsername = localStorage.getItem(usernameKey);
|
||||
if (storedUsername) {
|
||||
return storedUsername;
|
||||
@@ -176,20 +189,20 @@ export class UsernameInput extends LitElement {
|
||||
}
|
||||
|
||||
private generateNewUsername(): string {
|
||||
const newUsername = "Anon" + this.uuidToThreeDigits();
|
||||
const newUsername = genAnonUsername();
|
||||
this.storeUsername(newUsername);
|
||||
return newUsername;
|
||||
}
|
||||
|
||||
private uuidToThreeDigits(): string {
|
||||
const uuid = uuidv4();
|
||||
const cleanUuid = uuid.replace(/-/g, "").toLowerCase();
|
||||
const decimal = BigInt(`0x${cleanUuid}`);
|
||||
const threeDigits = decimal % 1000n;
|
||||
return threeDigits.toString().padStart(3, "0");
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this._isValid;
|
||||
}
|
||||
}
|
||||
|
||||
export function genAnonUsername(): string {
|
||||
const uuid = uuidv4();
|
||||
const cleanUuid = uuid.replace(/-/g, "").toLowerCase();
|
||||
const decimal = BigInt(`0x${cleanUuid}`);
|
||||
const threeDigits = decimal % 1000n;
|
||||
return "Anon" + threeDigits.toString().padStart(3, "0");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { getServerConfigFromClient } from "../../core/configuration/ConfigLoader";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { crazyGamesSDK } from "../CrazyGamesSDK";
|
||||
import { copyToClipboard, translateText } from "../Utils";
|
||||
|
||||
@customElement("copy-button")
|
||||
@@ -73,15 +74,21 @@ export class CopyButton extends LitElement {
|
||||
return url;
|
||||
}
|
||||
|
||||
private async resolveCopyText(): Promise<string> {
|
||||
private async resolveCopyText(): Promise<string | null> {
|
||||
if (this.copyText) return this.copyText;
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
return crazyGamesSDK.createInviteLink(this.lobbyId);
|
||||
}
|
||||
if (!this.lobbyId) return "";
|
||||
return await this.buildCopyUrl();
|
||||
}
|
||||
|
||||
private async handleCopy() {
|
||||
const text = await this.resolveCopyText();
|
||||
if (!text) return;
|
||||
if (!text) {
|
||||
alert("Error copying game id");
|
||||
return;
|
||||
}
|
||||
await copyToClipboard(
|
||||
text,
|
||||
() => (this.copySuccess = true),
|
||||
|
||||
@@ -105,7 +105,7 @@ export class DesktopNavBar extends LitElement {
|
||||
data-i18n="main.news"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500 relative"
|
||||
class="nav-menu-item no-crazygames text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500 relative"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
></button>
|
||||
@@ -127,14 +127,14 @@ export class DesktopNavBar extends LitElement {
|
||||
<lang-selector></lang-selector>
|
||||
<button
|
||||
id="nav-account-button"
|
||||
class="nav-menu-item relative h-10 rounded-full overflow-hidden flex items-center justify-center gap-2 px-3 bg-transparent border border-white/20 text-white/80 hover:text-white cursor-pointer transition-colors [&.active]:text-white"
|
||||
class="no-crazygames nav-menu-item relative h-10 rounded-full overflow-hidden flex items-center justify-center gap-2 px-3 bg-transparent border border-white/20 text-white/80 hover:text-white cursor-pointer transition-colors [&.active]:text-white"
|
||||
data-page="page-account"
|
||||
data-i18n-aria-label="main.account"
|
||||
data-i18n-title="main.account"
|
||||
>
|
||||
<img
|
||||
id="nav-account-avatar"
|
||||
class="hidden w-8 h-8 rounded-full object-cover"
|
||||
class="no-crazygames hidden w-8 h-8 rounded-full object-cover"
|
||||
alt=""
|
||||
data-i18n-alt="main.discord_avatar_alt"
|
||||
referrerpolicy="no-referrer"
|
||||
|
||||
@@ -125,7 +125,7 @@ export class MobileNavBar extends LitElement {
|
||||
data-i18n="main.stats"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
class="no-crazygames nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
></button>
|
||||
@@ -135,7 +135,7 @@ export class MobileNavBar extends LitElement {
|
||||
data-i18n="main.settings"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
class="no-crazygames nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-account"
|
||||
data-i18n="main.account"
|
||||
></button>
|
||||
|
||||
@@ -72,7 +72,7 @@ export class PatternButton extends LitElement {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-col items-center justify-between gap-2 p-3 bg-white/5 backdrop-blur-sm border rounded-xl w-48 h-full transition-all duration-200 ${this
|
||||
class="no-crazygames flex flex-col items-center justify-between gap-2 p-3 bg-white/5 backdrop-blur-sm border rounded-xl w-48 h-full transition-all duration-200 ${this
|
||||
.selected
|
||||
? "border-green-500 shadow-[0_0_15px_rgba(34,197,94,0.5)]"
|
||||
: "hover:bg-white/10 hover:border-white/20 hover:shadow-xl border-white/10"}"
|
||||
|
||||
@@ -138,32 +138,7 @@ export class PlayPage extends LitElement {
|
||||
|
||||
<!-- Matchmaking Buttons (Full Width across entire grid) -->
|
||||
<div class="lg:col-span-12 flex flex-col gap-6">
|
||||
<!-- Not Logged In Button -->
|
||||
<button
|
||||
id="matchmaking-button-logged-out"
|
||||
class="w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center overflow-hidden relative cursor-pointer"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="matchmaking_button.login_required"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<!-- Logged In Button -->
|
||||
<button
|
||||
id="matchmaking-button"
|
||||
class="hidden w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center group overflow-hidden relative"
|
||||
data-i18n-title="matchmaking_modal.title"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="matchmaking_button.play_ranked"
|
||||
></span>
|
||||
<span
|
||||
class="relative z-10 text-xs font-medium text-purple-100 opacity-90 group-hover:opacity-100 transition-opacity"
|
||||
data-i18n="matchmaking_button.description"
|
||||
></span>
|
||||
</button>
|
||||
<matchmaking-button></matchmaking-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,10 +105,15 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
private onPauseButtonClick() {
|
||||
this.isPaused = !this.isPaused;
|
||||
if (this.isPaused) {
|
||||
crazyGamesSDK.gameplayStop();
|
||||
} else {
|
||||
crazyGamesSDK.gameplayStart();
|
||||
}
|
||||
this.eventBus.emit(new PauseGameIntentEvent(this.isPaused));
|
||||
}
|
||||
|
||||
private onExitButtonClick() {
|
||||
private async onExitButtonClick() {
|
||||
const isAlive = this.game.myPlayer()?.isAlive();
|
||||
if (isAlive) {
|
||||
const isConfirmed = confirm(
|
||||
@@ -116,10 +121,10 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
crazyGamesSDK.gameplayStop().then(() => {
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
});
|
||||
await crazyGamesSDK.requestMidgameAd();
|
||||
await crazyGamesSDK.gameplayStop();
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
private onSettingsButtonClick() {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
|
||||
import { PauseGameIntentEvent } from "src/client/Transport";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
|
||||
import { PauseGameIntentEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
import SoundManager from "../../sound/SoundManager";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -105,8 +106,14 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private pauseGame(pause: boolean) {
|
||||
if (this.shouldPause && !this.wasPausedWhenOpened)
|
||||
if (this.shouldPause && !this.wasPausedWhenOpened) {
|
||||
if (pause) {
|
||||
crazyGamesSDK.gameplayStop();
|
||||
} else {
|
||||
crazyGamesSDK.gameplayStart();
|
||||
}
|
||||
this.eventBus.emit(new PauseGameIntentEvent(pause));
|
||||
}
|
||||
}
|
||||
|
||||
private onTerrainButtonClick() {
|
||||
|
||||
@@ -130,7 +130,7 @@ export class UnitDisplay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="hidden 2xl:flex lg:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-1100 2xl:flex-row xl:flex-col lg:flex-col 2xl:gap-5 xl:gap-2 lg:gap-2 justify-center items-center"
|
||||
class="hidden min-[1200px]:flex fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] 2xl:flex-row xl:flex-col min-[1200px]:flex-col 2xl:gap-5 xl:gap-2 min-[1200px]:gap-2 justify-center items-center"
|
||||
>
|
||||
<div class="bg-gray-800/70 backdrop-blur-xs rounded-lg p-0.5">
|
||||
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1 w-fit">
|
||||
|
||||
@@ -250,6 +250,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
async show() {
|
||||
crazyGamesSDK.gameplayStop();
|
||||
await this.loadPatternContent();
|
||||
this.isVisible = true;
|
||||
this.requestUpdate();
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
|
||||
|
||||
export const UsernameSchema = z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9_ [\]üÜ]+$/u)
|
||||
.regex(/^[a-zA-Z0-9_ [\]üÜ.]+$/u)
|
||||
.min(3)
|
||||
.max(27);
|
||||
const countryCodes = countries.filter((c) => !c.restricted).map((c) => c.code);
|
||||
|
||||
Reference in New Issue
Block a user