Compare commits

...
11 Commits
Author SHA1 Message Date
TrèsAbhiandCopilot 249f62759e feat: characteristics
Co-authored-by: Copilot <[email protected]>
2026-04-28 22:31:28 -05:00
TrèsAbhi 7767481633 armor groups 2026-04-28 22:19:38 -05:00
TrèsAbhi a97b1383d8 fix: patch stage 0 2026-04-28 22:15:18 -05:00
TrèsAbhi 1b224b3949 fix: upgrade requirements 2026-04-28 22:11:57 -05:00
TrèsAbhiandCopilot 018aa33a7f feat: some aggregation
Co-authored-by: Copilot <[email protected]>
2026-04-28 21:22:50 -05:00
TrèsAbhiandCopilot 4b7b8b3392 feat: module selection
Co-authored-by: Copilot <[email protected]>
2026-04-28 16:56:38 -05:00
TrèsAbhi 272824a72f fix: tool banner padding 2026-04-28 16:12:06 -05:00
TrèsAbhi ac02d65db0 fix: name width 2026-04-28 15:52:46 -05:00
TrèsAbhi 9bcaff9fd3 fix: light mode skeleton style 2026-04-28 15:50:32 -05:00
TrèsAbhiandCopilot 7b650bf39b feat: incremental loader
Co-authored-by: Copilot <[email protected]>
2026-04-28 15:31:21 -05:00
TrèsAbhi ee1ecb83b7 feat: class and type sorting 2026-04-28 15:11:20 -05:00
20 changed files with 479 additions and 333 deletions

No files matched your search

+4 -1
View File
@@ -136,9 +136,10 @@
"name": "varuna",
"dependencies": {
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.2.14",
"immer": "^10.1.3",
"lodash-es": "^4.17.21",
"react": "^19.1.1",
"react": "^19.2.5",
},
},
"packages/website": {
@@ -3217,6 +3218,8 @@
"utif2/pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"varuna/react": ["[email protected]", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"vscode-json-languageservice/jsonc-parser": ["[email protected]", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"which-builtin-type/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+2 -1
View File
@@ -4,8 +4,9 @@
"main": "src/index.ts",
"dependencies": {
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.2.14",
"immer": "^10.1.3",
"lodash-es": "^4.17.21",
"react": "^19.1.1"
"react": "^19.2.5"
}
}
+2 -2
View File
@@ -11,7 +11,7 @@ export class Varuna<Type, Arguments = void> {
constructor(
private creator: Type | ((...args: Arguments[]) => Type),
private persistence?: string
private persistence?: string,
) {
if (typeof creator !== "function") this.initialize(creator);
}
@@ -100,7 +100,7 @@ export class Varuna<Type, Arguments = void> {
}
use<Slice = Type>(
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
slicer: (state: Type) => Slice = (state) => state as unknown as Slice,
) {
return this.useDeferred(slicer, slicer(this.state));
}
@@ -1,17 +0,0 @@
@keyframes skeleton-animation {
0% {
background-color: var(--black-a8);
}
50% {
background-color: var(--black-a5);
}
100% {
background-color: var(--black-a8);
}
}
.skeleton {
animation: skeleton-animation var(--duration-subtle) ease infinite;
border-radius: var(--space-2);
}
@@ -0,0 +1,25 @@
@keyframes skeleton-animation {
0% {
background-color: var(--color-1);
}
50% {
background-color: var(--color-2);
}
100% {
background-color: var(--color-1);
}
}
.skeleton {
--color-1: var(--black-a5);
--color-2: var(--black-a4);
:global(:root.dark) & {
--color-1: var(--white-a3);
--color-2: var(--white-a2);
}
animation: skeleton-animation var(--duration-subtle) ease infinite;
border-radius: var(--space-2);
}
@@ -1,9 +1,9 @@
import type { ComponentProps } from "react";
import { classNames } from "../../core/ui/classNames";
import "./index.css";
import styles from "./index.module.css";
export interface SkeletonProps extends ComponentProps<"div"> {}
export function Skeleton({ className, ...props }: SkeletonProps) {
return <div className={classNames("skeleton", className)} {...props} />;
return <div className={classNames(styles.skeleton, className)} {...props} />;
}
@@ -5,7 +5,6 @@
gap: var(--space-2);
overflow: hidden;
min-width: min-content;
.icon-wrapper {
position: relative;
@@ -31,19 +30,29 @@
background-size: 140%;
background-position: 40% center;
.cover {
width: 100%;
height: 100%;
background: linear-gradient(transparent 75%, var(--gray-1));
}
}
}
.name {
white-space: nowrap;
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
display: flex;
width: 100%;
overflow-x: hidden;
justify-content: center;
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&[data-skeleton="true"] {
width: 100%;
}
.skeleton {
width: 100%;
}
}
}
}
@@ -2,7 +2,10 @@ import { api } from "../../core/api/dynamic";
import { useAwait } from "../../hooks/useAwait";
import { useGameStrings } from "../../hooks/useGameStrings";
import { useLocale } from "../../hooks/useLocale";
import type { PropsWithSkeleton } from "../../types/propsWithSkeleton";
import { InlineSkeleton } from "../InlineSkeleton";
import { LinkI18n } from "../LinkI18n";
import { Skeleton } from "../Skeleton";
import { Text } from "../Text";
import styles from "./index.module.css";
@@ -11,37 +14,48 @@ interface TankCardProps {
subtitle?: string;
}
export function TankCard({ id }: TankCardProps) {
const tank = useAwait(() => api.tank(id), `tank-${id}`);
const gameStrings = useGameStrings("TankEntity");
export function TankCard(props: PropsWithSkeleton<TankCardProps>) {
const tank = props.skeleton
? undefined
: useAwait(() => api.tank(props.id), `tank-${props.id}`);
const gameStrings = props.skeleton ? undefined : useGameStrings("TankEntity");
const locale = useLocale();
return (
<LinkI18n
href={`/tanks/${tank.slug}`}
href={props.skeleton ? undefined : `/tanks/${tank!.slug}`}
underline="never"
locale={locale}
className={styles["tank-card"]}
>
<div className={styles["icon-wrapper"]}>
<div
className={styles.flag}
style={{
backgroundImage: `url(/api/nations/${tank.tank!.nation}.webp)`,
}}
/>
{!props.skeleton && (
<div
className={styles.flag}
style={{
backgroundImage: `url(/api/nations/${tank!.tank!.nation}/card.webp)`,
}}
/>
)}
<div
className={styles.icon}
style={{
backgroundImage: `url(/api/tanks/${id}.webp)`,
}}
>
<div className={styles.cover} />
</div>
{!props.skeleton && (
<div
className={styles.icon}
style={{
backgroundImage: `url(/api/tanks/${props.id}.webp)`,
}}
/>
)}
{props.skeleton && <Skeleton className={styles.icon} />}
</div>
<Text className={styles.name}>{gameStrings[tank.tank!.name!.value]}</Text>
<div className={styles.name}>
<Text className={styles.text} data-skeleton={props.skeleton}>
{props.skeleton && <InlineSkeleton className={styles.skeleton} />}
{!props.skeleton && gameStrings![tank!.tank!.name!.value]}
</Text>
</div>
</LinkI18n>
);
}
@@ -26,7 +26,7 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
display: flex;
justify-content: center;
gap: var(--space-4);
padding: var(--space-5) var(--space-5);
padding: var(--space-5) var(--space-4);
box-sizing: border-box;
backdrop-filter: var(--blur-tint);
@@ -36,7 +36,7 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
}
@media (width > 40rem) {
padding: var(--space-6) var(--space-5);
padding: var(--space-6) var(--space-4);
}
.centered {
@@ -86,7 +86,7 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
</div>
<div class="centered">
<Heading weight="medium" size="2">
<Heading weight="bold" size="2">
{toolStrings.name}
</Heading>
<Text>
@@ -1,4 +1,4 @@
import { TankAttributeChange } from "@protos/game/proto/legacy/auto_items";
import type { TankCatalogComponent } from "@blitzkit/closed/protos/game/proto/legacy/blitz_static_tank_component";
import { StandardSinglePrice } from "@protos/game/proto/legacy/blitz_static_standard_single_price";
import { PenetrationGroup } from "@protos/game/proto/legacy/blitz_static_tank_penetration_group";
import {
@@ -7,13 +7,14 @@ import {
ShellUpgrade,
ShellUpgradeSingleChange,
StageParameters,
TankAttributeChange,
TankAttributeChange_AttributeName,
TankAttributeChange_Modifier,
VisualChanges,
} from "@protos/game/proto/legacy/blitz_static_tank_upgrade_single_stage";
function patch(stage0: StageParameters, stage1: StageParameters) {
if (stage1.stage_number !== ++stage0.stage_number) {
if (stage1.number !== ++stage0.number) {
throw new Error("Change stage number must be 1 greater than base");
}
@@ -181,26 +182,21 @@ function patch(stage0: StageParameters, stage1: StageParameters) {
}
}
export function aggregateStageParameters(
base: StageParameters,
stages: StageParameters[],
export function aggregateParameters(
tank: TankCatalogComponent,
upgrades: Record<string, number>,
) {
const stage0 = StageParameters.create({ ...base, stage_number: 0 });
const stage0 = StageParameters.create({});
/**
* Bug in Reforged: base stats are stage 1, so is the first upgrade. Manually
* setting it to stage 0 below and if the bug's been fixed, throw an error
* in dev mode to allow clean up in the future. Also, update the initial
* stage number to -1.
*/
for (const line of tank.upgrade_lines) {
const stage = upgrades[line.name];
if (base.stage_number === 0 && import.meta.env.DEV) {
throw new Error(
"Base stats are now stage 0. In-game bug fixed, please remove hack.",
);
stage0.number = 0;
for (let i = 0; i <= stage; i++) {
patch(stage0, line.stages[i]);
}
}
stages.forEach((stage) => patch(stage0, stage));
return stage0;
}
@@ -20,7 +20,6 @@ type Characteristic = (helpers: {
state: TankState;
tank: Tank;
parameters: StageParameters;
id: string;
}) => CharacteristicOutput;
export type CharacteristicName = keyof typeof characteristics;
@@ -409,8 +408,8 @@ export const characteristics = {
return tank.tank!.tank_type;
},
id({ id }) {
return id;
id({ state }) {
return state.id;
},
slug({ tank }) {
@@ -1,9 +1,9 @@
import type { Tank } from "@blitzkit/closed/protos/blitzkit/tank";
import type { Tank } from "@protos/blitzkit/tank";
import {
ShellUpgradeSingleChange_AttributeName,
TankAttributeChange_AttributeName,
} from "@protos/game/proto/legacy/blitz_static_tank_upgrade_single_stage";
import { aggregateStageParameters } from "./aggregateStageParameters";
import { aggregateParameters } from "./aggregateParameters";
import {
characteristics,
type CharacteristicName,
@@ -16,15 +16,8 @@ export type ComputedCharacteristics = Record<
CharacteristicOutput
>;
export function computeCharacteristics(
id: string,
tank: Tank,
state: TankState,
) {
const parameters = aggregateStageParameters(
tank.tank!.base_stats!,
tank.tank!.upgrade_stages.slice(0, state.stage),
);
export function computeCharacteristics(tank: Tank, state: TankState) {
const parameters = aggregateParameters(tank.tank!, state.upgrades);
const computed: Partial<ComputedCharacteristics> = {};
@@ -94,7 +87,6 @@ export function computeCharacteristics(
state,
tank,
parameters,
id,
});
computed[name] = value;
}
@@ -1,16 +1,26 @@
import type { TankCatalogComponent } from "@protos/blitz_static_tank_component";
import { TerrainHardness } from "./tankState";
import type { Tank } from "@protos/blitzkit/tank";
import { TerrainHardness, type TankState } from "./tankState";
export function createTankState({ tank, id }: Tank) {
const upgrades: Record<string, number> = {};
for (const upgradeLine of tank!.upgrade_lines) {
upgrades[upgradeLine.name] = upgradeLine.stages.length - 1;
}
export function createTankState(tank: TankCatalogComponent) {
return {
stage: tank.upgrade_stages.length,
id,
upgrades,
shell: 0,
isGunDamaged: false,
isHullTraversing: false,
isShooting: false,
isTurretTraversing: false,
speed: 0,
terrainHardness: TerrainHardness.Hard,
isShooting: false,
isGunDamaged: false,
isTurretTraversing: false,
isHullTraversing: false,
isCaughtOnFire: false,
};
} satisfies TankState;
}
@@ -5,13 +5,17 @@ export enum TerrainHardness {
}
export interface TankState {
stage: number;
id: string;
upgrades: Record<string, number>;
shell: number;
speed: number;
isHullTraversing: boolean;
isTurretTraversing: boolean;
terrainHardness: TerrainHardness;
isShooting: boolean;
isGunDamaged: boolean;
terrainHardness: TerrainHardness;
isTurretTraversing: boolean;
isHullTraversing: boolean;
isCaughtOnFire: boolean;
}
@@ -1,251 +1,343 @@
import { times } from "lodash-es";
import { Suspense, useMemo } from "react";
import { TankopediaCharacteristic } from "../../../../components/TankopediaCharacteristic";
import { TankopediaToy } from "../../../../components/TankopediaToy";
import { Grade } from "@blitzkit/closed/protos/game/proto/legacy/blitz_static_standard_grades_enum";
import { StageParameters_StageType } from "@blitzkit/closed/protos/game/proto/legacy/blitz_static_tank_upgrade_single_stage";
import { useMemo } from "react";
import { api } from "../../../../core/api/dynamic";
import { characteristicsOrder } from "../../../../core/tankopedia/characteristicsOrder";
import { computeCharacteristics } from "../../../../core/tankopedia/computeCharacteristics";
import { TerrainHardness } from "../../../../core/tankopedia/tankState";
import { useAwait } from "../../../../hooks/useAwait";
import { Tankopedia, TankopediaCompare } from "../../../../stores/tankopedia";
import { useGameStrings } from "../../../../hooks/useGameStrings";
import { LocaleProvider } from "../../../../hooks/useLocale";
import { Tankopedia } from "../../../../stores/tankopedia";
interface PageProps {
id: string;
locale: string;
}
export function Page({ id, locale }: PageProps) {
const tank = useAwait(() => api.tank(id), `tank-${id}`);
Tankopedia.useInitialization(tank);
export function Page({ id }: { id: string }) {
return (
<Suspense fallback={null}>
<Content id={id} />
</Suspense>
<LocaleProvider locale={locale}>
<Content />
</LocaleProvider>
);
}
function Content({ id }: { id: string }) {
const tank = useAwait(() => api.tank(id), `tank-${id}`);
// const tankList = useAwait(() => api.tankList(), "tank-list");
// const otherTanks = useAwait(() => api.tanks(), "tanks");
Tankopedia.useInitialization(tank.tank!);
function Content() {
const tankEntityGameStrings = useGameStrings("TankEntity");
const protagonist = Tankopedia.use((state) => state.protagonist);
const compare = Tankopedia.use((state) => state.compare);
const characteristics = useMemo(
() => computeCharacteristics(id, tank, protagonist),
[protagonist]
const protagonistTank = useAwait(
() => api.tank(protagonist.id),
`tank-${protagonist.id}`,
);
// const otherCharacteristics = useMemo(
// () =>
// tankList.list
// .filter((tank) => tank.id !== id)
// .map(({ id }) => {
// const state = { ...protagonist };
// const tank = otherTanks.tanks[id];
// return computeCharacteristics(
// id,
// tank.tank!,
// tank.compensation!,
// state
// );
// }),
// [protagonist]
// );
const { characteristics } = useMemo(
() => computeCharacteristics(protagonistTank, protagonist),
[protagonist],
);
return (
<>
<h1>{id}</h1>
<div style={{ display: "flex", flexWrap: "wrap", gap: "1rem" }}>
{tank.tank?.feature_screens?.tags.map((tag) => (
<span>{tag.value}</span>
))}
</div>
<h1>{protagonist.id}</h1>
<br />
<div style={{ display: "flex", flexWrap: "wrap" }}>
{times(tank.tank!.upgrade_stages.length + 1, (index) => (
<button
key={index}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.stage = index;
});
}}
>
Stage {index} {index === protagonist.stage && "(selected)"}
</button>
))}
</div>
<h2>upgrades</h2>
<br />
{protagonistTank.tank!.upgrade_lines.map((line) => {
return (
<>
<h3>
{line.name} ({protagonist.upgrades[line.name]})
</h3>
<div style={{ display: "flex", flexWrap: "wrap" }}>
{characteristics.parameters.shells_upgrades.map((shell) => (
<button
key={shell.shell_id}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.shell = shell.shell_id;
});
}}
>
Shell {shell.shell_id}{" "}
{shell.shell_id === protagonist.shell && "(selected)"}
</button>
))}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 8,
}}
>
{line.stages.map((stage, index) => {
if (stage.custom_picture) {
throw new Error("Custom picture not supported");
}
<br />
return (
<button
key={stage.tech_name}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.upgrades[line.name] = index;
<div>
<span>speed (0 - {characteristics.characteristics.speed_forward})</span>
<input
type="range"
min={0}
max={characteristics.characteristics.speed_forward as number}
value={protagonist.speed}
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.protagonist.speed = event.target.valueAsNumber;
});
}}
/>
</div>
for (const required of stage.required_upgrades) {
for (const line of protagonistTank.tank!
.upgrade_lines) {
let i = 0;
<br />
for (const stage of line.stages) {
if (stage.tech_name === required) {
draft.protagonist.upgrades[line.name] =
Math.max(
draft.protagonist.upgrades[line.name],
i,
);
}
<div>
<span>isHullTraversing</span>
<input
type="checkbox"
value={`${protagonist.isHullTraversing}`}
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.protagonist.isHullTraversing = event.target.checked;
});
}}
/>
</div>
i++;
}
}
}
<br />
for (const line of protagonistTank.tank!
.upgrade_lines) {
let i = draft.protagonist.upgrades[line.name];
<div>
<span>isTurretTraversing</span>
<input
type="checkbox"
value={`${protagonist.isTurretTraversing}`}
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.protagonist.isTurretTraversing = event.target.checked;
});
}}
/>
</div>
while (i > 0) {
const currentStage = line.stages[i];
const valid = currentStage.required_upgrades.every(
(required) => {
for (const line of protagonistTank.tank!
.upgrade_lines) {
const idx =
draft.protagonist.upgrades[line.name];
<br />
if (
line.stages[idx]?.tech_name === required
) {
return true;
}
}
<div>
<span>isShooting</span>
<input
type="checkbox"
value={`${protagonist.isShooting}`}
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.protagonist.isShooting = event.target.checked;
});
}}
/>
</div>
return false;
},
);
<br />
if (valid) break;
<div>
<span>isGunDamaged</span>
<input
type="checkbox"
value={`${protagonist.isGunDamaged}`}
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.protagonist.isGunDamaged = event.target.checked;
});
}}
/>
</div>
i--;
}
<br />
draft.protagonist.upgrades[line.name] = i;
}
});
}}
style={{
backgroundColor:
protagonist.upgrades[line.name] === index
? "green"
: undefined,
}}
>
{stage.tech_name}: {Grade[stage.grade]}
{StageParameters_StageType[stage.stage_type]}
{tankEntityGameStrings[stage.display_name]}
</button>
);
})}
</div>
</>
);
})}
<div style={{ display: "flex", flexWrap: "wrap" }}>
{Object.values(TerrainHardness).map(
(hardness) =>
typeof hardness === "number" && (
<button
key={hardness}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.terrainHardness = hardness;
});
}}
>
{TerrainHardness[hardness]}{" "}
{hardness === protagonist.terrainHardness && "(selected)"}
</button>
)
)}
</div>
<h2>characteristics</h2>
<br />
<div>
<span>isCaughtOnFire</span>
<input
type="checkbox"
value={`${protagonist.isCaughtOnFire}`}
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.protagonist.isCaughtOnFire = event.target.checked;
});
}}
/>
</div>
<br />
<div style={{ display: "flex", flexWrap: "wrap" }}>
{Object.values(TankopediaCompare).map(
(c) =>
typeof c === "number" && (
<button
key={c}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.compare = c;
});
}}
>
{TankopediaCompare[c]} {c === compare && "(selected)"}
</button>
)
)}
</div>
<br />
{characteristicsOrder.map(({ group, order }) => (
<>
<h2 key={group}>{group}</h2>
{order.map((config) =>
"toy" in config ? (
<TankopediaToy key={`toy-${config.toy}`} toy={config.toy} />
) : (
<TankopediaCharacteristic
key={`characteristic-${config.name}`}
characteristic={characteristics.characteristics[config.name]}
others={[]}
config={config}
/>
)
)}
</>
{Object.entries(characteristics).map(([key, value]) => (
<p>
{key}: {value}
</p>
))}
</>
);
// const parameters = aggregateParameters(
// protagonistTank.tank!,
// protagonist.upgrades,
// );
// if (parameters.modules_upgrades.length > 0) {
// throw new Error("Modules upgrades display not implemented");
// }
// return (
// <>
// <h1>{protagonist.id}</h1>
// <br />
// <h2>upgrades</h2>
// {protagonistTank.tank!.upgrade_lines.map((line) => {
// return (
// <>
// <h3>
// {line.name} ({protagonist.upgrades[line.name]})
// </h3>
// <div
// style={{
// display: "flex",
// flexDirection: "column",
// alignItems: "flex-start",
// gap: 8,
// }}
// >
// {line.stages.map((stage, index) => {
// if (stage.custom_picture) {
// throw new Error("Custom picture not supported");
// }
// return (
// <button
// key={stage.tech_name}
// onClick={() => {
// Tankopedia.mutate((draft) => {
// draft.protagonist.upgrades[line.name] = index;
// for (const required of stage.required_upgrades) {
// for (const line of protagonistTank.tank!
// .upgrade_lines) {
// let i = 0;
// for (const stage of line.stages) {
// if (stage.tech_name === required) {
// draft.protagonist.upgrades[line.name] =
// Math.max(
// draft.protagonist.upgrades[line.name],
// i,
// );
// }
// i++;
// }
// }
// }
// for (const line of protagonistTank.tank!
// .upgrade_lines) {
// let i = draft.protagonist.upgrades[line.name];
// while (i > 0) {
// const currentStage = line.stages[i];
// const valid = currentStage.required_upgrades.every(
// (required) => {
// for (const line of protagonistTank.tank!
// .upgrade_lines) {
// const idx =
// draft.protagonist.upgrades[line.name];
// if (
// line.stages[idx]?.tech_name === required
// ) {
// return true;
// }
// }
// return false;
// },
// );
// if (valid) break;
// i--;
// }
// draft.protagonist.upgrades[line.name] = i;
// }
// });
// }}
// style={{
// backgroundColor:
// protagonist.upgrades[line.name] === index
// ? "green"
// : undefined,
// }}
// >
// {stage.tech_name}: {Grade[stage.grade]}
// {StageParameters_StageType[stage.stage_type]}
// {tankEntityGameStrings[stage.display_name]}
// </button>
// );
// })}
// </div>
// </>
// );
// })}
// <h2>characteristics</h2>
// {}
// <h2>parameters</h2>
// <h3>attributes</h3>
// {parameters.attributes.map((attribute) => {
// return (
// <span>
// {TankAttributeChange_AttributeName[attribute.attribute_name]}:{" "}
// {attribute.value}
// </span>
// );
// })}
// <h3>penetration groups</h3>
// {parameters.penetration_groups_upgrades.map((upgrade) => {
// return (
// <>
// <h4>{PenetrationGroupUpgrade_TankPart[upgrade.tank_part]}</h4>
// <span>
// primary armor ({upgrade.primary_armor.length}):{" "}
// {upgrade.primary_armor}
// </span>
// <br />
// {upgrade.penetration_groups.map((group) => {
// return (
// <span>
// {group.group_name}: {group.armor} (
// {PenetrationGroup_CommonData[group.common_data]})
// </span>
// );
// })}
// </>
// );
// })}
// <h2>modules ({parameters.modules_upgrades.length})</h2>
// {parameters.modules_upgrades.map((upgrade) => {
// return (
// <span>{ModuleUpgrade_AttributeName[upgrade.attribute_name]}</span>
// );
// })}
// <h2>shells</h2>
// {parameters.shells_upgrades.map((upgrade) => {
// return (
// <>
// <h3>
// {upgrade.shell_id}: {ShellUpgrade_ShellType[upgrade.shell_type]}
// </h3>
// <pre>
// silver_price: {JSON.stringify(upgrade.silver_price, null, 2)}
// </pre>
// <pre>
// tray_shells: {JSON.stringify(upgrade.tray_shells, null, 2)}
// </pre>
// </>
// );
// })}
// </>
// );
}
@@ -19,5 +19,5 @@ const { id, locale } = Astro.props;
---
<BaseLayout {locale} accent="violet">
<Page {id} client:load />
<Page {locale} {id} client:load />
</BaseLayout>
@@ -1,5 +1,6 @@
import fuzzysort from "fuzzysort";
import { useMemo } from "react";
import { IncrementalLoader } from "../../../components/IncrementalLoader";
import { TankCard } from "../../../components/TankCard";
import { TankCards } from "../../../components/TankCards";
import { TankSearch } from "../../../components/TankSearch";
@@ -34,11 +35,14 @@ function Content() {
const tanksOrdered = useMemo(() => {
let list = Object.values(tanks.tanks);
list = list.sort((a, b) => a.tank!.tank_type - b.tank!.tank_type);
list = list.sort((a, b) => a.tank!.tank_class - b.tank!.tank_class);
list = list.sort((a, b) => a.tank!.nation - b.tank!.nation);
list = list.sort((a, b) =>
compareTiers(a.tank!.tier_catalog_id, b.tank!.tier_catalog_id),
);
return list;
return list.map(({ id }) => id);
}, []);
const tanksFiltered = useMemo(() => {
if (!search) return tanksOrdered;
@@ -47,17 +51,32 @@ function Content() {
keys: ["id", "name", "deburred"],
});
return results.map(({ obj }) => obj);
return results.map(({ obj }) => obj.id);
}, [search]);
return (
<>
<TankSearch />
<TankCards>
{tanksFiltered.slice(0, 32).map(({ id }) => (
<TankCard key={id} id={id} />
))}
</TankCards>
<List tanks={tanksFiltered} />
</>
);
}
interface ListProps {
tanks: string[];
}
function List({ tanks }: ListProps) {
const data = useMemo(() => tanks.map((id) => ({ key: id, id })), [tanks]);
return (
<TankCards>
<IncrementalLoader
initial={7 * 5}
intermediate={7 * 3}
data={data}
Component={TankCard}
/>
</TankCards>
);
}
@@ -1,6 +1,6 @@
import { Nation } from "@protos/game/proto/legacy/blitz_static_tank_component";
import type { APIContext, GetStaticPaths } from "astro";
import { game } from "../../../core/game/game";
import { game } from "../../../../core/game/game";
export const getStaticPaths = (async () => {
return Object.values(Nation)
+6 -7
View File
@@ -1,4 +1,4 @@
import type { TankCatalogComponent } from "@protos/game/proto/legacy/blitz_static_tank_component";
import type { Tank } from "@protos/blitzkit/tank";
import { Varuna } from "varuna";
import { createTankState } from "../core/tankopedia/createTankState";
import { type TankState } from "../core/tankopedia/tankState";
@@ -14,9 +14,8 @@ export enum TankopediaCompare {
All,
}
export const Tankopedia = new Varuna<Tankopedia, TankCatalogComponent>(
(tank) => ({
compare: TankopediaCompare.TierAndClass,
protagonist: createTankState(tank),
}),
);
export const Tankopedia = new Varuna<Tankopedia, Tank>((tank) => ({
compare: TankopediaCompare.TierAndClass,
protagonist: createTankState(tank),
}));
@@ -1,3 +1,3 @@
export type PropsWithSkeleton<Props> =
| { skeleton: true }
| { skeleton: true; onIntersection?: () => void }
| (Props & { skeleton?: false });