diff --git a/index.html b/index.html index 58c17fc2dd..fc658e653a 100644 --- a/index.html +++ b/index.html @@ -282,16 +282,11 @@ inline class="hidden w-full h-full page-content relative z-50" > - - + > @@ -144,7 +149,7 @@ export class PatternInput extends LitElement { return html` diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/CosmeticsModal.ts similarity index 71% rename from src/client/TerritoryPatternsModal.ts rename to src/client/CosmeticsModal.ts index 728aa37a76..1b9fe529f9 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/CosmeticsModal.ts @@ -11,6 +11,7 @@ import { import { PlayerPattern } from "../core/Schemas"; import { BaseModal } from "./components/BaseModal"; import "./components/CosmeticButton"; +import "./components/EffectsGrid"; import "./components/NotLoggedInWarning"; import { modalHeader } from "./components/ui/ModalHeader"; import { @@ -22,12 +23,16 @@ import { } from "./Cosmetics"; import { translateText } from "./Utils"; -@customElement("territory-patterns-modal") -export class TerritoryPatternsModal extends BaseModal { - protected routerName = "territory-patterns"; - public previewButton: HTMLElement | null = null; +/** + * One modal for every non-flag cosmetic: a Skins tab (patterns + image skins) + * and an Effects tab (all effect types via the tabbed effects-grid). Opened + * from the lobby's "Cosmetics" button. + */ +@customElement("cosmetics-modal") +export class CosmeticsModal extends BaseModal { + protected routerName = "cosmetics"; - @state() private selectedPattern: PlayerPattern | null; + @state() private selectedPattern: PlayerPattern | null = null; @state() private selectedColor: string | null = null; @state() private selectedSkinName: string | null = null; @state() private search = ""; @@ -36,7 +41,16 @@ export class TerritoryPatternsModal extends BaseModal { private userSettings: UserSettings = new UserSettings(); private userMeResponse: UserMeResponse | false = false; - private _onPatternSelected = async () => { + protected modalConfig() { + return { + tabs: [ + { key: "skins", label: translateText("store.patterns") }, + { key: "effects", label: translateText("store.effects") }, + ], + }; + } + + private _onCosmeticSelected = async () => { await this.updateFromSettings(); this.refresh(); }; @@ -51,7 +65,7 @@ export class TerritoryPatternsModal extends BaseModal { ); window.addEventListener( `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, - this._onPatternSelected, + this._onCosmeticSelected, ); } @@ -59,7 +73,7 @@ export class TerritoryPatternsModal extends BaseModal { super.disconnectedCallback(); window.removeEventListener( `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, - this._onPatternSelected, + this._onCosmeticSelected, ); } @@ -102,34 +116,32 @@ export class TerritoryPatternsModal extends BaseModal { ); return html` -
-
- ${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 { - - +
- +