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