-
- ${items.map((r) => {
- const isSelected =
- r.type === "pattern"
- ? (r.cosmetic === null && this.selectedPattern === null) ||
- (r.cosmetic !== null &&
- this.selectedPattern?.name === r.cosmetic.name &&
- (this.selectedPattern?.colorPalette?.name ?? null) ===
- (r.colorPalette?.name ?? null))
- : (() => {
- const skinName = (r.cosmetic as Skin | null)?.name ?? null;
- return (
- (skinName === null && this.selectedSkinName === null) ||
- (skinName !== null && this.selectedSkinName === skinName)
- );
- })();
- return html`
- this.selectCosmetic(rc)}
- >
- `;
- })}
-
+
+ ${items.map((r) => {
+ const isSelected =
+ r.type === "pattern"
+ ? (r.cosmetic === null && this.selectedPattern === null) ||
+ (r.cosmetic !== null &&
+ this.selectedPattern?.name === r.cosmetic.name &&
+ (this.selectedPattern?.colorPalette?.name ?? null) ===
+ (r.colorPalette?.name ?? null))
+ : (() => {
+ const skinName = (r.cosmetic as Skin | null)?.name ?? null;
+ return (
+ (skinName === null && this.selectedSkinName === null) ||
+ (skinName !== null && this.selectedSkinName === skinName)
+ );
+ })();
+ return html`
+ this.selectCosmetic(rc)}
+ >
+ `;
+ })}
`;
}
@@ -140,7 +152,7 @@ export class TerritoryPatternsModal extends BaseModal {
class="relative flex flex-col border-b border-white/10 pb-4 shrink-0"
>
${modalHeader({
- title: translateText("territory_patterns.title"),
+ title: translateText("cosmetics.title"),
onBack: () => this.close(),
ariaLabel: translateText("common.back"),
rightContent: html`
`,
@@ -152,7 +164,7 @@ export class TerritoryPatternsModal extends BaseModal {
rounded-xl shadow-inner text-xl text-center focus:outline-none
focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 text-white placeholder-white/30 transition-all"
type="text"
- placeholder=${translateText("territory_patterns.search")}
+ placeholder=${translateText("cosmetics.search")}
.value=${this.search}
@change=${this.handleSearch}
@keyup=${this.handleSearch}
@@ -162,7 +174,19 @@ export class TerritoryPatternsModal extends BaseModal {
`;
}
- protected renderBody() {
+ protected renderBody(tab: string) {
+ let grid: TemplateResult;
+ if (tab === "effects") {
+ grid = html`
`;
+ } else {
+ grid = this.renderSkinGrid();
+ }
return html`
-
${this.renderSkinGrid()}
+
${grid}
`;
}
@@ -202,8 +226,8 @@ export class TerritoryPatternsModal extends BaseModal {
);
this.selectedSkinName = skinName;
this.selectedPattern = null;
+ // Stay open — the tile highlight moves to the new selection.
this.refresh();
- this.close();
}
private selectPattern(pattern: PlayerPattern | null) {
@@ -219,9 +243,9 @@ export class TerritoryPatternsModal extends BaseModal {
}
this.selectedPattern = pattern;
this.selectedSkinName = null;
+ // Stay open — the tile highlight moves to the new selection.
this.refresh();
this.showSkinSelectedPopup();
- this.close();
}
private showSkinSelectedPopup() {
diff --git a/src/client/EffectsInput.ts b/src/client/EffectsInput.ts
deleted file mode 100644
index e7d5c21183..0000000000
--- a/src/client/EffectsInput.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { html, LitElement } from "lit";
-import { customElement, state } from "lit/decorators.js";
-import {
- findEffect,
- isTrailEffect,
- TRAIL_EFFECT_TYPES,
- TrailEffectAttributes,
-} from "../core/CosmeticSchemas";
-import {
- EFFECTS_KEY,
- USER_SETTINGS_CHANGED_EVENT,
-} from "../core/game/UserSettings";
-import "./components/EffectPreview"; // registers
-import { fetchCosmetics, getPlayerCosmetics } from "./Cosmetics";
-import { crazyGamesSDK } from "./CrazyGamesSDK";
-import { translateText } from "./Utils";
-
-@customElement("effects-input")
-export class EffectsInput extends LitElement {
- // The selected trail effect's attributes for the button preview, if any.
- // Not named `attributes` — that collides with HTMLElement.attributes.
- @state() private trailAttributes: TrailEffectAttributes | null = null;
-
- private _abortController: AbortController | null = null;
-
- // PlayerEffect is just { name, effectType }; resolve the visual style from the
- // cosmetics catalog by (effectType, name). The button shows a single trail
- // swatch, so preview the first selected trail effect across trail effectTypes
- // (boat trail before nuke trail, per TRAIL_EFFECT_TYPES order). nukeExplosion
- // is not a trail and has no swatch preview here.
- private async resolveTrailAttributes(): Promise {
- const cosmetics = await getPlayerCosmetics();
- const catalog = await fetchCosmetics();
- for (const effectType of TRAIL_EFFECT_TYPES) {
- const name = cosmetics.effects?.[effectType]?.name;
- if (!name) continue;
- const effect = findEffect(catalog, effectType, name);
- if (effect && isTrailEffect(effect)) return effect.attributes;
- }
- return null;
- }
-
- private _onCosmeticSelected = async () => {
- this.trailAttributes = await this.resolveTrailAttributes();
- };
-
- private onInputClick(e: Event) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent("effects-input-click", {
- bubbles: true,
- composed: true,
- }),
- );
- }
-
- async connectedCallback() {
- super.connectedCallback();
- this._abortController = new AbortController();
- this.trailAttributes = await this.resolveTrailAttributes();
- window.addEventListener(
- `${USER_SETTINGS_CHANGED_EVENT}:${EFFECTS_KEY}`,
- this._onCosmeticSelected,
- { signal: this._abortController.signal },
- );
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- if (this._abortController) {
- this._abortController.abort();
- this._abortController = null;
- }
- }
-
- createRenderRoot() {
- return this;
- }
-
- render() {
- if (crazyGamesSDK.isOnCrazyGames()) {
- return html``;
- }
-
- const preview =
- this.trailAttributes === null
- ? html`
- ${translateText("effects.title")}
- `
- : html`
-
- `;
-
- return html`
-
- `;
- }
-}
diff --git a/src/client/EffectsModal.ts b/src/client/EffectsModal.ts
deleted file mode 100644
index 98eb8766a0..0000000000
--- a/src/client/EffectsModal.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { html } from "lit";
-import { customElement, state } from "lit/decorators.js";
-import { UserMeResponse } from "../core/ApiSchemas";
-import { Cosmetics, EFFECT_TYPES } from "../core/CosmeticSchemas";
-import { BaseModal } from "./components/BaseModal";
-import "./components/EffectsGrid";
-import "./components/NotLoggedInWarning";
-import { modalHeader } from "./components/ui/ModalHeader";
-import { fetchCosmetics } from "./Cosmetics";
-import { translateText } from "./Utils";
-
-@customElement("effects-modal")
-export class EffectsModal extends BaseModal {
- protected routerName = "effects";
-
- @state() private cosmetics: Cosmetics | null = null;
- @state() private userMeResponse: UserMeResponse | false = false;
- @state() private search = "";
-
- // One tab per trail effectType; BaseModal owns activeTab + renders the bar.
- protected modalConfig() {
- return {
- tabs: EFFECT_TYPES.map((type) => ({
- key: type,
- label: translateText(`effects.type.${type}`),
- })),
- };
- }
-
- private handleSearch(event: Event) {
- this.search = (event.target as HTMLInputElement).value;
- }
-
- connectedCallback() {
- super.connectedCallback();
- document.addEventListener(
- "userMeResponse",
- (event: CustomEvent) => {
- this.onUserMe(event.detail);
- },
- );
- }
-
- async onUserMe(userMeResponse: UserMeResponse | false) {
- this.userMeResponse = userMeResponse;
- this.cosmetics = await fetchCosmetics();
- this.requestUpdate();
- }
-
- protected renderHeaderSlot() {
- return html`
-
- ${modalHeader({
- title: translateText("effects.title"),
- onBack: () => this.close(),
- ariaLabel: translateText("common.back"),
- rightContent: html`
`,
- })}
-
-
-
-
-
- `;
- }
-
- protected renderBody(tab: string) {
- return html`
-
-
- {
- this.close();
- window.showPage?.("page-item-store");
- }}
- >
-
-
-
- `;
- }
-}
diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts
index 2c245583b4..b5f4274349 100644
--- a/src/client/LangSelector.ts
+++ b/src/client/LangSelector.ts
@@ -225,10 +225,10 @@ export class LangSelector extends LitElement {
"user-setting",
"o-modal",
"o-button",
- "territory-patterns-modal",
+ "cosmetics-modal",
"store-modal",
"custom-currency-card",
- "pattern-input",
+ "cosmetics-input",
"fluent-slider",
"news-modal",
"news-button",
@@ -236,8 +236,6 @@ export class LangSelector extends LitElement {
"leaderboard-modal",
"flag-input-modal",
"flag-input",
- "effects-modal",
- "effects-input",
"effects-grid",
"matchmaking-button",
"token-login",
diff --git a/src/client/Main.ts b/src/client/Main.ts
index 855b9bad1d..ca9f6e8f75 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -19,11 +19,11 @@ import { reauthAfterCrazyGamesChange, userAuth } from "./Auth";
import "./ClanModal";
import { joinLobby, type JoinLobbyResult } from "./ClientGameRunner";
import { getPlayerCosmeticsRefs } from "./Cosmetics";
+import "./CosmeticsInput";
+import "./CosmeticsModal";
+import { CosmeticsModal } from "./CosmeticsModal";
import { updateCrazyGamesNavButton } from "./CrazyGamesAccountButton";
import { crazyGamesSDK } from "./CrazyGamesSDK";
-import "./EffectsInput";
-import "./EffectsModal";
-import { EffectsModal } from "./EffectsModal";
import "./FlagInput";
import { FlagInput } from "./FlagInput";
import "./FlagInputModal";
@@ -47,12 +47,9 @@ import { MatchmakingModal } from "./Matchmaking";
import { modalRouter } from "./ModalRouter";
import { initNavigation } from "./Navigation";
import "./NewsModal";
-import "./PatternInput";
import { RewardsModal } from "./RewardsModal";
import "./SinglePlayerModal";
import { StoreModal } from "./Store";
-import "./TerritoryPatternsModal";
-import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
import { TokenLoginModal } from "./TokenLoginModal";
import {
SendKickPlayerIntentEvent,
@@ -321,11 +318,8 @@ class Client {
tag: "troubleshooting-modal",
pageId: "page-troubleshooting",
});
- modalRouter.register("territory-patterns", {
- tag: "territory-patterns-modal",
- });
+ modalRouter.register("cosmetics", { tag: "cosmetics-modal" });
modalRouter.register("flag-input", { tag: "flag-input-modal" });
- modalRouter.register("effects", { tag: "effects-modal" });
// Prefetch turnstile token so it is available when
// the user joins a lobby.
@@ -436,52 +430,30 @@ class Client {
});
});
- const effectsModal = document.querySelector(
- "effects-modal",
- ) as EffectsModal;
- if (!effectsModal || !(effectsModal instanceof EffectsModal)) {
- console.warn("Effects modal element not found");
- }
- document.querySelectorAll("effects-input").forEach((effectsInput) => {
- effectsInput.addEventListener("effects-input-click", () => {
- if (effectsModal && effectsModal instanceof EffectsModal) {
- effectsModal.open();
- }
- });
- });
-
this.storeModal = document.getElementById("page-item-store") as StoreModal;
if (!this.storeModal || !(this.storeModal instanceof StoreModal)) {
console.warn("Store modal element not found");
}
- const patternsModal = document.getElementById(
- "territory-patterns-modal",
- ) as TerritoryPatternsModal;
- if (!patternsModal || !(patternsModal instanceof TerritoryPatternsModal)) {
- console.warn("Patterns modal element not found");
+ const cosmeticsModal = document.getElementById(
+ "cosmetics-modal",
+ ) as CosmeticsModal;
+ if (!cosmeticsModal || !(cosmeticsModal instanceof CosmeticsModal)) {
+ console.warn("Cosmetics modal element not found");
}
- // Attach listener to any pattern-input component
- document.querySelectorAll("pattern-input").forEach((patternInput) => {
- patternInput.addEventListener("pattern-input-click", () => {
- patternsModal.open();
+ // Attach listener to any cosmetics-input component
+ document.querySelectorAll("cosmetics-input").forEach((cosmeticsInput) => {
+ cosmeticsInput.addEventListener("cosmetics-input-click", () => {
+ cosmeticsModal.open();
});
});
if (isInIframe()) {
- const mobilePat = document.getElementById("pattern-input-mobile");
- if (mobilePat) mobilePat.style.display = "none";
+ const mobileCosmetics = document.getElementById("cosmetics-input-mobile");
+ if (mobileCosmetics) mobileCosmetics.style.display = "none";
}
- if (!this.storeModal || !(this.storeModal instanceof StoreModal)) {
- console.warn("Store modal element not found");
- }
-
- // We no longer need to manually manage the preview button as PatternInput handles it component-side.
- // However, we still want to ensure the modal can be opened.
- // The setupPatternInput above handles the click event for the new buttons.
-
this.storeModal.refresh();
window.addEventListener("showPage", (e: any) => {
@@ -958,12 +930,11 @@ class Client {
"help-modal",
"user-setting",
"troubleshooting-modal",
- "territory-patterns-modal",
+ "cosmetics-modal",
"store-modal",
"language-modal",
"news-modal",
"flag-input-modal",
- "effects-modal",
"account-button",
"leaderboard-button",
"token-login",
diff --git a/src/client/components/PlayPage.ts b/src/client/components/PlayPage.ts
index 01c73baa60..c5803414f3 100644
--- a/src/client/components/PlayPage.ts
+++ b/src/client/components/PlayPage.ts
@@ -121,40 +121,32 @@ export class PlayPage extends LitElement {
-
-
+
-
+