Compare commits

...
5 Commits
Author SHA1 Message Date
TrèsAbhi 033660c2d9 fix: webp 2026-01-11 13:48:27 -06:00
TrèsAbhi 1a639c51e1 feat: update backgrounds 2026-01-11 13:46:54 -06:00
TrèsAbhi 1366579dd1 feat: some unification 2026-01-11 13:40:01 -06:00
TrèsAbhi f5ed2c2c40 feat: concurrency 2026-01-11 11:52:28 -06:00
TrèsAbhi 1598de9f5f feat: imageProxy 2026-01-11 11:51:02 -06:00
16 changed files with 266 additions and 94 deletions

No files matched your search

+1
View File
@@ -208,6 +208,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-promise-suspense": "^0.3.4",
"sharp": "^0.34.5",
"three": "^0.180.0",
"three-stdlib": "^2.36.0",
"typescript": "^5.9.3",
+1 -1
View File
@@ -32,7 +32,7 @@ export default defineConfig({
outDir: "../../dist/website",
prefetch: { defaultStrategy: "hover", prefetchAll: true },
// build: { concurrency: 4 },
build: { concurrency: 4 },
adapter: target === "app" ? node({ mode: "standalone" }) : undefined,
integrations: [
+1
View File
@@ -37,6 +37,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-promise-suspense": "^0.3.4",
"sharp": "^0.34.5",
"three": "^0.180.0",
"three-stdlib": "^2.36.0",
"typescript": "^5.9.3",
@@ -3,6 +3,7 @@
display: flex;
justify-content: center;
margin-top: calc(var(--space-4) * -1);
padding: var(--space-6) var(--space-4);
background-color: var(--gray-3);
+99 -15
View File
@@ -1,20 +1,43 @@
import type { CatalogItemAccessor } from "@blitzkit/closed";
import type { Strings } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import type { Avatar } from "../../protos/avatar";
import type { Avatars } from "../../protos/avatars";
import type { Background } from "../../protos/background";
import type { Backgrounds } from "../../protos/backgrounds";
import type { Tank } from "../../protos/tank";
import type { TankList } from "../../protos/tank_list";
import type { Tanks } from "../../protos/tanks";
export abstract class AbstractAPI {
abstract tanks(): Promise<Tanks>;
private _tankListCache: TankList | undefined;
protected abstract _tankList(): Promise<TankList>;
async tankList() {
if (this._tankListCache === undefined) {
this._tankListCache = await this._tankList();
}
abstract tankList(): Promise<TankList>;
return this._tankListCache;
}
abstract _strings(locale: string): Promise<Strings>;
abstract _gameStrings(
locale: string,
prefix: string
): Promise<Record<string, string>>;
private _tankCache: Record<string, Tank> = {};
protected abstract _tank(id: string): Promise<Tank>;
async tank(id: string) {
if (this._tankCache[id] === undefined) {
this._tankCache[id] = await this._tank(id);
}
return this._tankCache[id];
}
private _tanksCache: Tanks | undefined;
protected abstract _tanks(): Promise<Tanks>;
async tanks() {
if (this._tanksCache === undefined) {
this._tanksCache = await this._tanks();
}
return this._tanksCache;
}
private assertLocale(locale: string) {
const supported = locales.supported.find(
@@ -27,20 +50,81 @@ export abstract class AbstractAPI {
return supported;
}
private _stringsCache: Record<string, Strings> = {};
protected abstract _strings(locale: string): Promise<Strings>;
async strings(locale: string) {
this.assertLocale(locale);
return await this._strings(locale);
if (this._stringsCache[locale] === undefined) {
this._stringsCache[locale] = await this._strings(locale);
}
return this._stringsCache[locale];
}
private _gameStringsCache: Record<
string,
Record<string, Record<string, string>>
> = {};
protected abstract _gameStrings(
locale: string,
prefix: string
): Promise<Record<string, string>>;
async gameStrings(locale: string, prefix: string) {
this.assertLocale(locale);
return await this._gameStrings(locale, prefix);
if (this._gameStringsCache[locale] === undefined) {
this._gameStringsCache[locale] = {};
}
if (this._gameStringsCache[locale][prefix] === undefined) {
this._gameStringsCache[locale][prefix] = await this._gameStrings(
locale,
prefix
);
}
return this._gameStringsCache[locale][prefix];
}
abstract tank(id: string): Promise<Tank>;
private _avatarCache: Record<string, Avatar> = {};
protected abstract _avatar(id: string): Promise<Avatar>;
async avatar(id: string) {
if (this._avatarCache[id] === undefined) {
this._avatarCache[id] = await this._avatar(id);
}
abstract avatars(): Promise<CatalogItemAccessor[]>;
abstract avatar(id: string): Promise<CatalogItemAccessor>;
return this._avatarCache[id];
}
abstract backgrounds(): Promise<CatalogItemAccessor[]>;
abstract background(id: string): Promise<CatalogItemAccessor>;
private _avatarsCache: Avatars | undefined;
protected abstract _avatars(): Promise<Avatars>;
async avatars() {
if (this._avatarsCache === undefined) {
this._avatarsCache = await this._avatars();
}
return this._avatarsCache;
}
private _backgroundCache: Record<string, Background> = {};
protected abstract _background(id: string): Promise<Background>;
async background(id: string) {
if (this._backgroundCache[id] === undefined) {
this._backgroundCache[id] = await this._background(id);
}
return this._backgroundCache[id];
}
private _backgroundsCache: Backgrounds | undefined;
protected abstract _backgrounds(): Promise<Backgrounds>;
async backgrounds() {
if (this._backgroundsCache === undefined) {
this._backgroundsCache = await this._backgrounds();
}
return this._backgroundsCache;
}
}
@@ -20,3 +20,5 @@ if (typeof window === "undefined") {
}
export const api = dynamicAPI;
@@ -0,0 +1,23 @@
import sharp from "sharp";
export async function imageProxy(url: string) {
if (import.meta.env.DEV) {
return Response.redirect(url);
}
let response: Response;
try {
response = await fetch(url);
} catch (error) {
console.warn(`Failed to fetch image proxy ${url}:`, error);
return new Response(null, { status: 404 });
}
const buffer = await response.arrayBuffer();
const image = sharp(buffer);
const webp = await image.webp().toBuffer();
const array = new Uint8Array(webp);
return new Response(array);
}
+56 -31
View File
@@ -5,8 +5,10 @@ import locales from "@blitzkit/i18n/locales.json";
import type { RemoteStorageComponent } from "@protos/blitz_static_remote_storage_component";
import { merge } from "lodash-es";
import { parse } from "yaml";
import type { Avatar } from "../../protos/avatar";
import type { Background } from "../../protos/background";
import type { Tank } from "../../protos/tank";
import type { TankList, TankListEntry } from "../../protos/tank_list";
import type { TankListEntry } from "../../protos/tank_list";
import type { Tanks } from "../../protos/tanks";
import { AbstractAPI } from "./abstract";
@@ -27,7 +29,7 @@ export class ServerAPI extends AbstractAPI {
super();
}
async _gameStrings(locale: string, prefix: string) {
protected async _gameStrings(locale: string, prefix: string) {
const group = this.metadata.group("ClientConfigsEntity");
if (group.length !== 1) {
@@ -107,7 +109,7 @@ export class ServerAPI extends AbstractAPI {
return filtered;
}
async _strings(locale: string) {
protected async _strings(locale: string) {
const localized = (await globbedStrings[
`../../../../i18n/strings/${locale}.json`
]()) as DeepPartial<Strings>;
@@ -119,35 +121,30 @@ export class ServerAPI extends AbstractAPI {
return strings;
}
_tankList: TankList | undefined;
async tankList() {
if (this._tankList === undefined) {
const group = this.metadata.group("TankEntity");
const list: TankListEntry[] = [];
const strings = await this.gameStrings("en", "TankEntity");
protected async _tankList() {
const group = this.metadata.group("TankEntity");
const list: TankListEntry[] = [];
const strings = await this.gameStrings("en", "TankEntity");
for (const item of group) {
const tankCatalog = item.TankCatalog();
for (const item of group) {
const tankCatalog = item.TankCatalog();
if (!tankCatalog.name) continue;
if (!tankCatalog.name) continue;
const name = strings[tankCatalog.name.value];
const name = strings[tankCatalog.name.value];
if (!name) continue;
if (!name) continue;
const id = item.name;
const slug = sluggify(name);
const id = item.name;
const slug = sluggify(name);
list.push({ id, slug });
}
this._tankList = { list };
list.push({ id, slug });
}
return this._tankList;
return { list };
}
async tanks() {
protected async _tanks() {
const tankList = await this.tankList();
const data: Tanks = { tanks: {} };
@@ -158,7 +155,7 @@ export class ServerAPI extends AbstractAPI {
return data;
}
async tank(id: string) {
protected async _tank(id: string) {
const tankList = await this.tankList();
const tankListEntry = tankList.list.find((tank) => tank.id === id);
@@ -174,19 +171,47 @@ export class ServerAPI extends AbstractAPI {
return { tank, compensation, slug } satisfies Tank;
}
async avatars() {
return this.metadata.group("ProfileAvatarEntity");
protected async _avatars() {
const avatars: Avatar[] = [];
for (const item of this.metadata.group("ProfileAvatarEntity")) {
avatars.push(await this.avatar(item.name));
}
return { avatars };
}
async avatar(id: string) {
return this.metadata.item(`ProfileAvatarEntity.${id}`);
protected async _avatar(id: string) {
const avatar = this.metadata.item(`ProfileAvatarEntity.${id}`);
const name = avatar.name;
const stuff_ui = avatar.StuffUI("UIComponent");
const profile_avatar = avatar.ProfileAvatar();
const sellable = avatar.components.sellableComponent
? avatar.Sellable()
: undefined;
return { name, stuff_ui, profile_avatar, sellable };
}
async backgrounds() {
return this.metadata.group("ProfileBackgroundEntity");
protected async _backgrounds() {
const backgrounds: Background[] = [];
for (const item of this.metadata.group("ProfileBackgroundEntity")) {
backgrounds.push(await this.background(item.name));
}
return { backgrounds };
}
async background(id: string) {
return this.metadata.item(`ProfileBackgroundEntity.${id}`);
protected async _background(id: string) {
const background = this.metadata.item(`ProfileBackgroundEntity.${id}`);
const name = background.name;
const stuff_ui = background.StuffUI("UIComponent");
const profile_background = background.ProfileBackground();
const sellable = background.components.sellableComponent
? background.Sellable()
: undefined;
return { name, stuff_ui, profile_background, sellable };
}
}
@@ -6,30 +6,31 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
export { getStaticPaths } from "../index.astro";
const avatars = await api.avatars();
const { avatars } = await api.avatars();
---
<BaseLayout accent="gold">
{
avatars.map((avatar) => {
const ui = avatar.StuffUI("UIComponent");
const profileAvatar = avatar.ProfileAvatar();
return (
<>
<h3>
{ui.display_name} {Grade[ui.grade]}{" "}
{ProfileAvatarComponent_Category[profileAvatar.category]}
{avatar.stuff_ui!.display_name} {Grade[avatar.stuff_ui!.grade]}{" "}
{ProfileAvatarComponent_Category[avatar.profile_avatar!.category]}
</h3>
{ui.description && <p>{ui.description}</p>}
{ui.obtaining_methods && <p>{ui.obtaining_methods}</p>}
{ui.features && <p>{ui.features}</p>}
{avatar.stuff_ui!.description && (
<p>{avatar.stuff_ui!.description}</p>
)}
{avatar.stuff_ui!.obtaining_methods && (
<p>{avatar.stuff_ui!.obtaining_methods}</p>
)}
{avatar.stuff_ui!.features && <p>{avatar.stuff_ui!.features}</p>}
<img width="128" src={`/api/avatars/${avatar.name}.webp`} />
{avatar.components.sellableComponent && (
<pre>{JSON.stringify(avatar.Sellable(), null, 2)}</pre>
{avatar.sellable && (
<pre>{JSON.stringify(avatar.sellable, null, 2)}</pre>
)}
<br />
@@ -5,31 +5,35 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
export { getStaticPaths } from "../index.astro";
const backgrounds = await api.backgrounds();
const { backgrounds } = await api.backgrounds();
---
<BaseLayout accent="amber">
{
backgrounds.map((background) => {
const ui = background.StuffUI("UIComponent");
const profileBackground = background.ProfileBackground();
return (
<>
<h3>
{ui.display_name} {Grade[ui.grade]}{" "}
{background.stuff_ui!.display_name}{" "}
{Grade[background.stuff_ui!.grade]}{" "}
</h3>
<pre>{JSON.stringify(profileBackground, null, 2)}</pre>
<pre>{JSON.stringify(background.profile_background, null, 2)}</pre>
{ui.description && <p>{ui.description}</p>}
{ui.obtaining_methods && <p>{ui.obtaining_methods}</p>}
{ui.features && <p>{ui.features}</p>}
{background.stuff_ui!.description && (
<p>{background.stuff_ui!.description}</p>
)}
{background.stuff_ui!.obtaining_methods && (
<p>{background.stuff_ui!.obtaining_methods}</p>
)}
{background.stuff_ui!.features && (
<p>{background.stuff_ui!.features}</p>
)}
<img width="128" src={`/api/backgrounds/${background.name}.webp`} />
{background.components.sellableComponent && (
<pre>{JSON.stringify(background.Sellable(), null, 2)}</pre>
{background.sellable && (
<pre>{JSON.stringify(background.sellable, null, 2)}</pre>
)}
<br />
@@ -1,8 +1,9 @@
import type { APIContext, GetStaticPaths } from "astro";
import { api } from "../../../core/api/dynamic";
import { imageProxy } from "../../../core/api/imageProxy";
export const getStaticPaths = (async () => {
const avatars = await api.avatars();
const { avatars } = await api.avatars();
return avatars.map((avatar) => ({
params: { id: avatar.name },
@@ -14,14 +15,5 @@ export const getStaticPaths = (async () => {
*/
export async function GET({ params }: APIContext<never, { id: string }>) {
const avatar = await api.avatar(params.id);
const profileAvatar = avatar.ProfileAvatar();
if (import.meta.env.DEV) {
return Response.redirect(profileAvatar.avatar);
}
const response = await fetch(profileAvatar.avatar);
const buffer = await response.arrayBuffer();
return new Response(buffer);
return await imageProxy(avatar.profile_avatar!.avatar);
}
@@ -1,11 +1,12 @@
import type { APIContext, GetStaticPaths } from "astro";
import { api } from "../../../core/api/dynamic";
import { imageProxy } from "../../../core/api/imageProxy";
export const getStaticPaths = (async () => {
const backgrounds = await api.backgrounds();
const { backgrounds } = await api.backgrounds();
return backgrounds.map((avatar) => ({
params: { id: avatar.name },
return backgrounds.map((background) => ({
params: { id: background.name },
}));
}) satisfies GetStaticPaths;
@@ -13,15 +14,6 @@ export const getStaticPaths = (async () => {
* One day I will write documentation for this
*/
export async function GET({ params }: APIContext<never, { id: string }>) {
const avatar = await api.background(params.id);
const profileBackground = avatar.ProfileBackground();
if (import.meta.env.DEV) {
return Response.redirect(profileBackground.background);
}
const response = await fetch(profileBackground.background);
const buffer = await response.arrayBuffer();
return new Response(buffer);
const background = await api.background(params.id);
return await imageProxy(background.profile_background!.background);
}
@@ -0,0 +1,14 @@
syntax = "proto3";
package blitzkit;
import "blitz_static_profile_avatar_component.proto";
import "blitz_static_sellable_component.proto";
import "blitz_static_stuff_ui_component.proto";
message Avatar {
string name = 1;
blitz_static_stuff_ui_component.StuffUIComponent stuff_ui = 2;
blitz_static_profile_avatar_component.ProfileAvatarComponent profile_avatar = 3;
optional blitz_static_sellable_component.SellableComponent sellable = 4;
}
@@ -0,0 +1,9 @@
syntax = "proto3";
package blitzkit;
import "avatar.proto";
message Avatars {
repeated Avatar avatars = 1;
}
@@ -0,0 +1,14 @@
syntax = "proto3";
package blitzkit;
import "blitz_static_profile_background_component.proto";
import "blitz_static_sellable_component.proto";
import "blitz_static_stuff_ui_component.proto";
message Background {
string name = 1;
blitz_static_stuff_ui_component.StuffUIComponent stuff_ui = 2;
blitz_static_profile_background_component.ProfileBackgroundComponent profile_background = 3;
optional blitz_static_sellable_component.SellableComponent sellable = 4;
}
@@ -0,0 +1,9 @@
syntax = "proto3";
package blitzkit;
import "background.proto";
message Backgrounds {
repeated Background backgrounds = 1;
}