mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-30 22:19:50 +01:00
Compare commits
5
Commits
0817ba18bd
...
d6e37119a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6e37119a8 | ||
|
|
385cc3b6d2 | ||
|
|
b4936bf13b | ||
|
|
e1e9403c60 | ||
|
|
0828d50830 |
No files matched your search
Vendored
+1
@@ -67,6 +67,7 @@
|
||||
"DEBUFF",
|
||||
"debugflags",
|
||||
"deburr",
|
||||
"deburred",
|
||||
"decalmask",
|
||||
"decompressor",
|
||||
"décor",
|
||||
|
||||
+1
-1
Submodule packages/closed updated: 52a5847368...852d7a6290.
@@ -121,5 +121,11 @@
|
||||
"no_docs": "This method has no documentation.",
|
||||
"test": "Test",
|
||||
"view_full": "View full types"
|
||||
},
|
||||
|
||||
"tanks": {
|
||||
"search": {
|
||||
"placeholder": "Search tanks..."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,13 +43,14 @@ import "@fontsource-variable/roboto-flex";
|
||||
--weight-bold: 700;
|
||||
--weight-black: 900;
|
||||
|
||||
--outline-interactive: var(--space-1) solid var(--gray-4);
|
||||
--outline-focus: var(--space-1) solid var(--accent-6);
|
||||
|
||||
--shadow-cavity:
|
||||
/* --shadow-cavity:
|
||||
inset calc(var(--space-1) * sin(45deg)) calc(var(--space-1) * sin(45deg))
|
||||
var(--space-1) var(--black-a3),
|
||||
inset calc(var(--space-1) * sin(45deg) * -1)
|
||||
calc(var(--space-1) * sin(45deg) * -1) var(--space-1) var(--white-a1);
|
||||
calc(var(--space-1) * sin(45deg) * -1) var(--space-1) var(--white-a1); */
|
||||
|
||||
--shadow-bump:
|
||||
inset calc(var(--space-1) * sin(45deg)) calc(var(--space-1) * sin(45deg))
|
||||
@@ -60,7 +61,7 @@ import "@fontsource-variable/roboto-flex";
|
||||
--shadow-outline-width: 1.5px;
|
||||
--shadow-outline-width-negative: calc(var(--shadow-outline-width) * -1);
|
||||
|
||||
--shadow-interactive-background: 0 0 0 var(--shadow-outline-width)
|
||||
/* --shadow-interactive-background: 0 0 0 var(--shadow-outline-width)
|
||||
var(--gray-6);
|
||||
--shadow-interactive-shine: var(--shadow-outline-width-negative)
|
||||
var(--shadow-outline-width-negative) var(--shadow-outline-width)
|
||||
@@ -69,7 +70,7 @@ import "@fontsource-variable/roboto-flex";
|
||||
var(--shadow-outline-width) var(--shadow-outline-width) var(--black-a4);
|
||||
--shadow-interactive:
|
||||
var(--shadow-interactive-shine), var(--shadow-interactive-shadow),
|
||||
var(--shadow-interactive-background);
|
||||
var(--shadow-interactive-background); */
|
||||
|
||||
--shadow-aggressive: var(--space-1) var(--space-1) var(--space-4)
|
||||
var(--black-a11);
|
||||
@@ -82,6 +83,6 @@ import "@fontsource-variable/roboto-flex";
|
||||
--duration-brief: 200ms;
|
||||
--duration-subtle: 2s;
|
||||
|
||||
--tank-card-min-width: 11rem;
|
||||
--tank-card-min-width: 8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -80,8 +80,12 @@ const strings = await api.strings(locale);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
.credit {
|
||||
position: relative;
|
||||
|
||||
:root.light & {
|
||||
color: var(--black-a11) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +108,7 @@ const strings = await api.strings(locale);
|
||||
<Code className="version">2.0</Code>
|
||||
</div>
|
||||
|
||||
<Text className="name" lowContrast>
|
||||
<Text className="credit" lowContrast>
|
||||
{strings.home.credit}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
.content {
|
||||
display: flex;
|
||||
|
||||
gap: 2rem;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
.content {
|
||||
flex: 1;
|
||||
max-width: var(--page-width);
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
.icon {
|
||||
min-width: var(--tank-card-min-width);
|
||||
height: 7rem;
|
||||
aspect-ratio: 4 / 3;
|
||||
position: relative;
|
||||
|
||||
background-size: 140%;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useStrings } from "../hooks/useStrings";
|
||||
import { TankFilters } from "../stores/tankFilters";
|
||||
import { TextField } from "./TextField";
|
||||
|
||||
export function TankSearch() {
|
||||
const strings = useStrings();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
placeholder={strings.tanks.search.placeholder}
|
||||
onChange={(event) => {
|
||||
const trimmed = event.target.value.trim();
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.search = undefined;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.search = trimmed;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
border-radius: var(--max-size);
|
||||
|
||||
background-color: var(--gray-1);
|
||||
box-shadow: var(--shadow-interactive), var(--shadow-cavity);
|
||||
outline: var(--outline-interactive);
|
||||
|
||||
input {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { classNames } from "../../core/ui/classNames";
|
||||
import "./index.css";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export interface TextFieldProps extends ComponentProps<"input"> {
|
||||
children?: ReactNode;
|
||||
@@ -51,7 +51,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames("text-field", className)}
|
||||
className={classNames(styles["text-field"], className)}
|
||||
data-has-icon={children !== undefined}
|
||||
data-disabled={disabled}
|
||||
data-variant={variant}
|
||||
|
||||
@@ -43,10 +43,6 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
:root.light & {
|
||||
filter: brightness(1.25);
|
||||
}
|
||||
|
||||
.gradient {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -62,10 +58,6 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
|
||||
|
||||
backdrop-filter: blur(var(--space-6));
|
||||
|
||||
:root.light & {
|
||||
backdrop-filter: blur(var(--space-6)) brightness(1.25);
|
||||
}
|
||||
|
||||
.readable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { PopularTanks } from "@protos/blitzkit/popular_tanks";
|
||||
import type { Tank } from "@protos/blitzkit/tank";
|
||||
import type { TankList } from "@protos/blitzkit/tank_list";
|
||||
import type { Tanks } from "@protos/blitzkit/tanks";
|
||||
import type { Tiers } from "@protos/blitzkit/tiers";
|
||||
|
||||
export abstract class AbstractAPI {
|
||||
abstract tankList(): Promise<TankList>;
|
||||
@@ -16,6 +17,8 @@ export abstract class AbstractAPI {
|
||||
abstract tanks(): Promise<Tanks>;
|
||||
abstract popularTanks(): Promise<PopularTanks>;
|
||||
|
||||
abstract tiers(): Promise<Tiers>;
|
||||
|
||||
abstract strings(locale: string): Promise<Strings>;
|
||||
abstract gameStrings(locale: string): Promise<Record<string, string>>;
|
||||
protected abstract _groupedGameStrings(
|
||||
|
||||
@@ -2,8 +2,9 @@ import { fetchPB } from "@blitzkit/core";
|
||||
import type { Strings } from "@blitzkit/i18n";
|
||||
import { Avatars } from "@protos/blitzkit/avatars";
|
||||
import { PopularTanks } from "@protos/blitzkit/popular_tanks";
|
||||
import { Tank } from "@protos/blitzkit/tank";
|
||||
import { TankList } from "@protos/blitzkit/tank_list";
|
||||
import { Tanks } from "@protos/blitzkit/tanks";
|
||||
import { Tiers } from "@protos/blitzkit/tiers";
|
||||
import { AbstractAPI, Cache } from "./abstract";
|
||||
|
||||
const rejected = Promise.reject("Not implemented on client");
|
||||
@@ -15,13 +16,14 @@ export class ClientAPI extends AbstractAPI {
|
||||
}
|
||||
|
||||
@Cache()
|
||||
tank(id: string) {
|
||||
return fetchPB(`/api/tanks/${id}.pb`, Tank);
|
||||
async tank(id: string) {
|
||||
const tanks = await this.tanks();
|
||||
return tanks.tanks[id];
|
||||
}
|
||||
|
||||
@UnimplementedOnClient
|
||||
@Cache()
|
||||
tanks() {
|
||||
return rejected;
|
||||
return fetchPB("/api/tanks/all.pb", Tanks);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
@@ -29,6 +31,11 @@ export class ClientAPI extends AbstractAPI {
|
||||
return fetchPB("/api/tanks/popular.pb", PopularTanks);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
tiers() {
|
||||
return fetchPB("/api/tiers.pb", Tiers);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async strings(locale: string) {
|
||||
const response = await fetch(`/api/strings/${locale}.json`);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { PopularTanks } from "@protos/blitzkit/popular_tanks";
|
||||
import type { Tank } from "@protos/blitzkit/tank";
|
||||
import type { TankListEntry } from "@protos/blitzkit/tank_list";
|
||||
import type { Tanks } from "@protos/blitzkit/tanks";
|
||||
import type { Tiers } from "@protos/blitzkit/tiers";
|
||||
import type { RemoteStorageComponent } from "@protos/game/proto/legacy/blitz_static_remote_storage_component";
|
||||
import { existsSync } from "fs";
|
||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
@@ -173,6 +174,18 @@ export class ServerAPI extends AbstractAPI {
|
||||
return JSON.parse(data) as PopularTanks;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async tiers() {
|
||||
const group = this.metadata.group("TankTierEntity");
|
||||
const tiers: Tiers = { tiers: {} };
|
||||
|
||||
for (const item of group) {
|
||||
tiers.tiers[item.name] = item.TankTier();
|
||||
}
|
||||
|
||||
return tiers;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async strings(locale: string) {
|
||||
const localized = (await globbedStrings[
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { deburr } from "lodash-es";
|
||||
import { api } from "../core/api/dynamic";
|
||||
import { useAwait } from "./useAwait";
|
||||
import { useGameStrings } from "./useGameStrings";
|
||||
|
||||
export function useSearchableTanks() {
|
||||
const gameStrings = useGameStrings("TankEntity");
|
||||
const tanks = useAwait(() => api.tanks(), "tanks");
|
||||
|
||||
return Object.values(tanks.tanks).map((tank) => {
|
||||
const { id } = tank;
|
||||
const name = gameStrings[tank.tank!.name!.value];
|
||||
const deburred = deburr(name);
|
||||
|
||||
return { id, name, deburred };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { api } from "../core/api/dynamic";
|
||||
import { useAwait } from "./useAwait";
|
||||
|
||||
export function useTierCompare() {
|
||||
const tiers = useAwait(() => api.tiers(), "tiers");
|
||||
|
||||
function compare(a: string, b: string) {
|
||||
const [, tierNameA] = a.split(".");
|
||||
const [, tierNameB] = b.split(".");
|
||||
const tierA = tiers.tiers[tierNameA];
|
||||
const tierB = tiers.tiers[tierNameB];
|
||||
|
||||
return tierB.carousel_level - tierA.carousel_level;
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { useMemo } from "react";
|
||||
import { TankCard } from "../../../components/TankCard";
|
||||
import { TankCards } from "../../../components/TankCards";
|
||||
import { TankSearch } from "../../../components/TankSearch";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
import { useAwait } from "../../../hooks/useAwait";
|
||||
import { LocaleProvider } from "../../../hooks/useLocale";
|
||||
import { useSearchableTanks } from "../../../hooks/useSearchableTanks";
|
||||
import { useTierCompare } from "../../../hooks/useTierCompare";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
|
||||
const { go } = fuzzysort;
|
||||
|
||||
interface PageProps {
|
||||
locale: string;
|
||||
@@ -17,13 +25,39 @@ export function Page({ locale, ...props }: PageProps) {
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const tankList = useAwait(() => api.tankList(), "tank-list");
|
||||
const tanks = useAwait(() => api.tanks(), "tanks");
|
||||
|
||||
const compareTiers = useTierCompare();
|
||||
const searchableTanks = useSearchableTanks();
|
||||
const search = TankFilters.use((state) => state.search);
|
||||
|
||||
const tanksOrdered = useMemo(() => {
|
||||
let list = Object.values(tanks.tanks);
|
||||
|
||||
list = list.sort((a, b) =>
|
||||
compareTiers(a.tank!.tier_catalog_id, b.tank!.tier_catalog_id),
|
||||
);
|
||||
|
||||
return list;
|
||||
}, []);
|
||||
const tanksFiltered = useMemo(() => {
|
||||
if (!search) return tanksOrdered;
|
||||
|
||||
const results = go(search, searchableTanks, {
|
||||
keys: ["id", "name", "deburred"],
|
||||
});
|
||||
|
||||
return results.map(({ obj }) => obj);
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<TankCards>
|
||||
{tankList.list.slice(0, 32).map(({ id }) => (
|
||||
<TankCard key={id} id={id} />
|
||||
))}
|
||||
</TankCards>
|
||||
<>
|
||||
<TankSearch />
|
||||
<TankCards>
|
||||
{tanksFiltered.slice(0, 32).map(({ id }) => (
|
||||
<TankCard key={id} id={id} />
|
||||
))}
|
||||
</TankCards>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
import "@radix-ui/colors/violet-dark.css";
|
||||
import { Section } from "../../../components/Section";
|
||||
import ToolBanner from "../../../components/ToolBanner.astro";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
import { tools } from "../../../core/tools/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
@@ -10,7 +9,6 @@ import { Page } from "./_index";
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const tankList = await api.tankList();
|
||||
---
|
||||
|
||||
<BaseLayout {locale} accent="purple">
|
||||
@@ -19,8 +17,4 @@ const tankList = await api.tankList();
|
||||
<Section>
|
||||
<Page {locale} client:load />
|
||||
</Section>
|
||||
|
||||
<!-- {
|
||||
|
||||
} -->
|
||||
</BaseLayout>
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { APIContext } from "astro";
|
||||
import type { APIContext, GetStaticPaths } from "astro";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
|
||||
export { getStaticPaths } from "./[id].pb";
|
||||
export const getStaticPaths = (async () => {
|
||||
const { list } = await api.tankList();
|
||||
return list.map(({ id }) => ({ params: { id } }));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
/**
|
||||
* Even Noah can't save these animals.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Tank } from "@protos/blitzkit/tank";
|
||||
import type { APIContext, GetStaticPaths } from "astro";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const { list } = await api.tankList();
|
||||
return list.map(({ id }) => ({ params: { id } }));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
/**
|
||||
* Even Noah can't save these animals.
|
||||
*/
|
||||
export async function GET({ params }: APIContext<never, { id: string }>) {
|
||||
const tank = await api.tank(params.id);
|
||||
return new Response(Tank.encode(tank).finish());
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { APIContext } from "astro";
|
||||
import { api } from "../../../core/api/dynamic";
|
||||
import { game } from "../../../core/game/game";
|
||||
|
||||
export { getStaticPaths } from "./[id].pb";
|
||||
export { getStaticPaths } from "./[id].json";
|
||||
|
||||
export async function GET({ params }: APIContext<never, { id: string }>) {
|
||||
const tank = await api.tank(params.id);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Tiers } from "@protos/blitzkit/tiers";
|
||||
import { api } from "../../core/api/dynamic";
|
||||
|
||||
/**
|
||||
* Woah!
|
||||
*/
|
||||
export async function GET() {
|
||||
const tiers = await api.tiers();
|
||||
return new Response(Tiers.encode(tiers).finish());
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
interface TankFilters {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const TankFilters = new Varuna<TankFilters>({
|
||||
search: undefined,
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TankCatalogComponent } from "@protos/blitz_static_tank_component";
|
||||
import type { TankCatalogComponent } from "@protos/game/proto/legacy/blitz_static_tank_component";
|
||||
import { Varuna } from "varuna";
|
||||
import { createTankState } from "../core/tankopedia/createTankState";
|
||||
import { type TankState } from "../core/tankopedia/tankState";
|
||||
@@ -18,5 +18,5 @@ export const Tankopedia = new Varuna<Tankopedia, TankCatalogComponent>(
|
||||
(tank) => ({
|
||||
compare: TankopediaCompare.TierAndClass,
|
||||
protagonist: createTankState(tank),
|
||||
})
|
||||
}),
|
||||
);
|
||||
Reference in new issue
Block a user