diff --git a/.vscode/settings.json b/.vscode/settings.json index 76d627e64..e0a3622e9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -192,6 +192,7 @@ "TYPELESS", "ubjson", "Uncompiled", + "undiscriminated", "Unicum", "UNLOCALIZED", "UNORM", diff --git a/_package.json b/_package.json index de8f5957c..0aa464703 100644 --- a/_package.json +++ b/_package.json @@ -41,7 +41,6 @@ "satori": "^0.10.14", "@resvg/resvg-js": "^2.6.2", "discord.js": "^14.16.3", - "fuzzysort": "^2.0.4", "bepaint": "^0.1.0-beta.3", "markdown-escape": "^2.0.0", "@types/markdown-escape": "^1.1.3", diff --git a/bun.lockb b/bun.lockb index 87fce89e2..12cb0af5e 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/core/src/blitz/catalogItemAccessor.ts b/packages/core/src/blitz/catalogItemAccessor.ts index 256788d1e..0dca3cf8f 100644 --- a/packages/core/src/blitz/catalogItemAccessor.ts +++ b/packages/core/src/blitz/catalogItemAccessor.ts @@ -22,6 +22,10 @@ export class CatalogItemAccessor { } } + undiscriminatedId() { + return this.id.split('.').slice(1).join('.'); + } + static fromComponent(id: string, component: Any) { return this.fromComponents(id, [component]); } diff --git a/packages/core/src/blitz/metadata/abstract.ts b/packages/core/src/blitz/metadata/abstract.ts index 7b23a433c..9f274b5c5 100644 --- a/packages/core/src/blitz/metadata/abstract.ts +++ b/packages/core/src/blitz/metadata/abstract.ts @@ -1,9 +1,12 @@ import { LocalizationResourcesComponent } from '@protos/blitz_static_localization_resources_component'; +import { ProfileAvatarComponent } from '@protos/blitz_static_profile_avatar_component'; import { RemoteStorageComponent } from '@protos/blitz_static_remote_storage_component'; +import { SellableComponent } from '@protos/blitz_static_sellable_component'; +import { StuffUIComponent } from '@protos/blitz_static_stuff_ui_component'; import { BlitzkitAllAvatarsComponent } from '@protos/blitzkit_static_all_avatars_component'; import { load } from 'js-yaml'; import { minimatch } from 'minimatch'; -import { CatalogItemAccessor } from '../catalogItemAccessor'; +import { CatalogItemAccessor } from 'packages/core/src/blitz/catalogItemAccessor'; interface LocalizationConfig { namespaces: string[]; @@ -102,11 +105,26 @@ export abstract class MetadataAccessor { } injectBlitzkitProfileAvatars() { - const allAvatars: BlitzkitAllAvatarsComponent = { - avatars: this.filter((item) => - item.startsWith('ProfileAvatarEntity.'), - ).map((avatar) => avatar.pack()), - }; + const allAvatars: BlitzkitAllAvatarsComponent = { avatars: [] }; + + for (const item of this.filter((item) => + item.startsWith('ProfileAvatarEntity.'), + )) { + const stuff = item.get(StuffUIComponent, 'UIComponent'); + const avatar = item.get(ProfileAvatarComponent, 'profileAvatarComponent'); + const sellable = item.getOptional(SellableComponent, 'sellableComponent'); + + allAvatars.avatars.push({ + id: item.undiscriminatedId(), + name: stuff.display_name, + obtaining: stuff.obtaining_methods, + description: stuff.description, + grade: stuff.grade, + category: avatar.category, + hidden_if_not_obtained: avatar.hidden_if_not_obtained, + sale: sellable?.reward, + }); + } this.add( CatalogItemAccessor.fromComponent( diff --git a/packages/core/src/blitz/metadata/socket.ts b/packages/core/src/blitz/metadata/socket.ts index 432ddd8d3..7ecca8912 100644 --- a/packages/core/src/blitz/metadata/socket.ts +++ b/packages/core/src/blitz/metadata/socket.ts @@ -16,7 +16,6 @@ export class SocketMetadataAccessor extends MetadataAccessor { async get(item: string) { if (item in this.items) return new CatalogItemAccessor(this.items[item]); - throw new Error(`Item ${item} does not exist in socket metadata`); } diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index deba93510..e5c97dd6b 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -1270,14 +1270,16 @@ "no_description": "This avatar has no description.", "download": "Download", + "enlarge": "Enlarge", "hidden": "Hidden", "unnamed_avatars": "Unnamed avatars", "search": { "hint": "Search avatars...", "results": "%s1 avatars" }, + "categories": { - "0": "Unspecified category", + "0": "Unspecified", "1": "Premium", "2": "Special", "3": "Accumulative", diff --git a/packages/website-ue/package.json b/packages/website-ue/package.json index d850d814e..56ea0302e 100644 --- a/packages/website-ue/package.json +++ b/packages/website-ue/package.json @@ -26,12 +26,14 @@ "@types/semver": "^7.7.0", "@types/three": "^0.160.0", "astro": "^5.8.0", + "fuzzysort": "^3.1.0", "immer": "^10.1.1", "lodash-es": "^4.17.21", "minimatch": "^10.0.1", "react": "^19.1.0", "react-dom": "^19.1.0", "semver": "^7.7.2", + "sharp": "^0.34.2", "three": "^0.169.0", "zustand": "^4.5.5" } diff --git a/packages/website-ue/public/assets/sad-ghost.png b/packages/website-ue/public/assets/sad-ghost.png new file mode 100644 index 000000000..7043f5704 Binary files /dev/null and b/packages/website-ue/public/assets/sad-ghost.png differ diff --git a/packages/website-ue/src/components/Avatars/Group.tsx b/packages/website-ue/src/components/Avatars/Group.tsx index 1cd9bf465..e562a8b10 100644 --- a/packages/website-ue/src/components/Avatars/Group.tsx +++ b/packages/website-ue/src/components/Avatars/Group.tsx @@ -1,6 +1,4 @@ -import { type ProfileAvatarComponent } from '@protos/blitz_static_profile_avatar_component'; -import type { SellableComponent } from '@protos/blitz_static_sellable_component'; -import type { StuffUIComponent } from '@protos/blitz_static_stuff_ui_component'; +import type { BlitzKitAvatar } from '@protos/blitzkit_static_all_avatars_component'; import { Dialog } from '@radix-ui/themes'; import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps'; import { Content } from './Content'; @@ -8,11 +6,7 @@ import { Popup } from './Popup'; export interface AvatarGroupProps { name?: string; - avatars: { - stuff: StuffUIComponent; - avatar: ProfileAvatarComponent; - sellable?: SellableComponent; - }[]; + avatars: { avatar: BlitzKitAvatar }[]; } export function AvatarGroup( diff --git a/packages/website-ue/src/components/Avatars/Popup.tsx b/packages/website-ue/src/components/Avatars/Popup.tsx index 610f6a546..35cde9458 100644 --- a/packages/website-ue/src/components/Avatars/Popup.tsx +++ b/packages/website-ue/src/components/Avatars/Popup.tsx @@ -1,6 +1,12 @@ import { Grade } from '@protos/blitz_static_standard_grades_enum'; -import { DownloadIcon, StarFilledIcon } from '@radix-ui/react-icons'; -import { Badge, Box, Button, Dialog, Flex, Text } from '@radix-ui/themes'; +import { + CardStackIcon, + DownloadIcon, + ExternalLinkIcon, + EyeNoneIcon, + StarFilledIcon, +} from '@radix-ui/react-icons'; +import { Badge, Box, Button, Dialog, Flex, Link, Text } from '@radix-ui/themes'; import { Var } from '../../core/radix/var'; import { useLocale } from '../../hooks/useLocale'; import { GRADE_COLORS } from './Colors'; @@ -13,8 +19,8 @@ export function Popup({ name, avatars }: AvatarGroupProps) { <> {name} - - {avatars.map(({ avatar, stuff }, index) => ( + + {[...avatars].reverse().map(({ avatar }, index) => ( + + - {strings.common.grades[stuff.grade]} + {strings.common.grades[avatar.grade]} + + + + {strings.website.tools.avatars.categories[avatar.category]} + + + {avatar.hidden_if_not_obtained && ( + + {strings.website.tools.avatars.hidden} + + )} + + {avatar.sale && ( + + {JSON.stringify( + avatar.sale.reward_list[0].currency_reward?.amount, + )} + + )} + + - {gameStrings[stuff.description] ?? - (gameStrings[stuff.obtaining_methods] + {gameStrings[avatar.description] ?? + (gameStrings[avatar.obtaining] ? undefined : strings.website.tools.avatars.no_description)}{' '} - {gameStrings[stuff.obtaining_methods]} + {gameStrings[avatar.obtaining]} - - + + + + + + + + diff --git a/packages/website-ue/src/components/Avatars/Stack.tsx b/packages/website-ue/src/components/Avatars/Stack.tsx index 91f71bff2..872cec724 100644 --- a/packages/website-ue/src/components/Avatars/Stack.tsx +++ b/packages/website-ue/src/components/Avatars/Stack.tsx @@ -11,7 +11,7 @@ export function Stack({ avatars }: { avatars: AvatarGroupProps['avatars'] }) { return ( <> - {avatars.map((avatar, index) => ( + {avatars.map(({ avatar }, index) => ( - {avatars.length > 1 && ( + + + {avatars.length > 1 && index === avatars.length - 1 && ( (null); + const [search, setSearch] = useState(null); + const [searching, setSearching] = useState(false); const groups = useMemo(() => { const avatars = allAvatars - .map((id) => { - const item = new CatalogItemAccessor(id); - const stuff = item.get(StuffUIComponent, 'UIComponent'); - const avatar = item.get( - ProfileAvatarComponent, - 'profileAvatarComponent', - ); - const sellable = item.getOptional( - SellableComponent, - 'sellableComponent', - ); - const name = gameStrings[stuff.display_name] as string | undefined; - - return { avatar, name, stuff, sellable }; - }) - .sort((a, b) => a.stuff.grade - b.stuff.grade) + .map((avatar) => ({ + avatar, + name: gameStrings[avatar.name] as string | undefined, + })) + .sort((a, b) => a.avatar.grade - b.avatar.grade) .sort((a, b) => a.name === undefined ? 1 @@ -75,10 +65,50 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) { return [...groups].map(([name, avatars]) => ({ name, avatars })); }, []); const [loadedAvatars, setLoadedAvatars] = useState(DEFAULT_LOADED); - const filtered = useMemo(() => groups, []); + const filtered = useMemo(() => { + setSearching(false); + + if (search === null || search.trim().length === 0) return groups; + + return fuzzysort + .go(search, groups, { key: 'name' }) + .map((result) => result.obj); + }, [search]); + const requestSearch = useCallback( + debounce(() => { + if (!input.current) return; + setSearch(input.current.value); + }, 500), + [], + ); return ( + + { + if (event.target.value.trim().length === 0) { + setSearching(false); + return; + } + + requestSearch(); + setSearching(true); + }} + > + + {searching ? : } + + + + { + const paths: GetStaticPathsItem[] = []; + + for (const item in socketMetadata.items) { + if (!item.startsWith('ProfileAvatarEntity.')) continue; + + const id = await socketMetadata + .get(item) + .then((item) => item.undiscriminatedId()); + + paths.push({ params: { id } }); + } + + return paths; +}; + +export const GET: APIRoute<{}, { id: string }> = async ({ params }) => { + const item = await socketMetadata.get(`ProfileAvatarEntity.${params.id}`); + const avatar = item.get(ProfileAvatarComponent, 'profileAvatarComponent'); + + try { + return await fetch(avatar.avatar); + } catch { + return new Response(null, { status: 404 }); + } +}; diff --git a/packages/website-ue/src/pages/api/metadata/catalog/[item].pb.ts b/packages/website-ue/src/pages/api/internal/catalog/[item].pb.ts similarity index 100% rename from packages/website-ue/src/pages/api/metadata/catalog/[item].pb.ts rename to packages/website-ue/src/pages/api/internal/catalog/[item].pb.ts diff --git a/submodules/blitzkit-closed b/submodules/blitzkit-closed index c390ed62f..7f4ec5f5b 160000 --- a/submodules/blitzkit-closed +++ b/submodules/blitzkit-closed @@ -1 +1 @@ -Subproject commit c390ed62f367e7b5559ac45f5290eae313e7b952 +Subproject commit 7f4ec5f5bc17d7dbd778398e888bcefc846e0080