diff --git a/packages/website-ue/src/components/IconButton/index.module.css b/packages/website-ue/src/components/IconButton/index.module.css
index e80c47788..5f02bf489 100644
--- a/packages/website-ue/src/components/IconButton/index.module.css
+++ b/packages/website-ue/src/components/IconButton/index.module.css
@@ -3,5 +3,5 @@
aspect-ratio: 1 / 1;
- font-size: 1.75em;
+ font-size: 2em;
}
diff --git a/packages/website-ue/src/components/Price/index.module.css b/packages/website-ue/src/components/Price/index.module.css
new file mode 100644
index 000000000..74404e1c7
--- /dev/null
+++ b/packages/website-ue/src/components/Price/index.module.css
@@ -0,0 +1,20 @@
+.price {
+ display: flex;
+ align-items: center;
+
+ gap: var(--space-3);
+
+ .entry {
+ display: block;
+
+ .wrapper {
+ display: flex;
+ gap: var(--space-1);
+
+ .icon {
+ width: 1em;
+ height: 1em;
+ }
+ }
+ }
+}
diff --git a/packages/website-ue/src/components/Price/index.tsx b/packages/website-ue/src/components/Price/index.tsx
new file mode 100644
index 000000000..5cb2d9efe
--- /dev/null
+++ b/packages/website-ue/src/components/Price/index.tsx
@@ -0,0 +1,50 @@
+import { formatCompact } from "@blitzkit/core";
+import type { StandardPrice } from "@protos/game/proto/legacy/blitz_static_standard_price";
+import type { StandardSinglePrice } from "@protos/game/proto/legacy/blitz_static_standard_single_price";
+import type { ComponentProps } from "react";
+import { classNames } from "../../core/ui/classNames";
+import { useLocale } from "../../hooks/useLocale";
+import { Text } from "../Text";
+import styles from "./index.module.css";
+
+interface PriceProps extends ComponentProps<"div"> {
+ price: StandardPrice;
+}
+
+export function Price({ price, className, ...props }: PriceProps) {
+ return (
+
+ {price.price_list.map((entry, index) => (
+
+ ))}
+
+ );
+}
+
+interface PriceEntryProps {
+ entry: StandardSinglePrice;
+}
+
+function PriceEntry({ entry }: PriceEntryProps) {
+ const locale = useLocale();
+
+ if (
+ entry.premium_time_price !== undefined ||
+ entry.stuff_price !== undefined ||
+ entry.currency_price === undefined
+ ) {
+ throw new Error("Unsupported price type");
+ }
+
+ return (
+
+
+

+ {formatCompact(locale, entry.currency_price.amount)}
+
+
+ );
+}
diff --git a/packages/website-ue/src/components/TankopediaLoadout/index.module.css b/packages/website-ue/src/components/TankopediaLoadout/index.module.css
index 294d96371..d9716fdc4 100644
--- a/packages/website-ue/src/components/TankopediaLoadout/index.module.css
+++ b/packages/website-ue/src/components/TankopediaLoadout/index.module.css
@@ -13,11 +13,11 @@
.stage {
position: relative;
- img {
+ .icon {
width: 1em;
height: 1em;
- transform: translate(var(--space-1), var(--space-1));
+ transform: translateX(var(--space-1));
}
.tier {
@@ -25,6 +25,13 @@
left: var(--space-2);
top: var(--space-2);
}
+
+ .price {
+ position: absolute;
+ left: 50%;
+ bottom: var(--space-2);
+ transform: translateX(-50%);
+ }
}
}
}
diff --git a/packages/website-ue/src/components/TankopediaLoadout/index.tsx b/packages/website-ue/src/components/TankopediaLoadout/index.tsx
index bc44cbe2b..66e0d190b 100644
--- a/packages/website-ue/src/components/TankopediaLoadout/index.tsx
+++ b/packages/website-ue/src/components/TankopediaLoadout/index.tsx
@@ -1,12 +1,16 @@
-import type { UpgradeLine } from "@blitzkit/closed/protos/game/proto/legacy/blitz_static_tank_upgrade_line";
import { romanize } from "@blitzkit/core";
-import { Fragment, useMemo } from "react";
-import { api } from "../../core/api/dynamic";
-import { useAwait } from "../../hooks/useAwait";
+import { Grade } from "@protos/game/proto/legacy/blitz_static_standard_grades_enum";
+import type { StandardPrice } from "@protos/game/proto/legacy/blitz_static_standard_price";
+import type { UpgradeLine } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_line";
+import type { StageParameters } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_single_stage";
+import { useMemo } from "react";
+import { useProtagonist } from "../../hooks/useProtagonist";
import { useStrings } from "../../hooks/useStrings";
+import { useUpgradePreset } from "../../hooks/useUpgradePreset";
import { Tankopedia } from "../../stores/tankopedia";
import { Heading } from "../Heading";
import { IconButton } from "../IconButton";
+import { Price } from "../Price";
import { Section } from "../Section";
import { Text } from "../Text";
import styles from "./index.module.css";
@@ -14,8 +18,7 @@ import styles from "./index.module.css";
export function TankopediaLoadout() {
const strings = useStrings();
- const id = Tankopedia.use((state) => state.protagonist.id);
- const tank = useAwait(() => api.tank(id), `tank-${id}`);
+ const tank = useProtagonist();
return (
@@ -25,7 +28,7 @@ export function TankopediaLoadout() {
{tank.tank!.upgrade_lines.map((line) => (
))}
@@ -35,7 +38,7 @@ export function TankopediaLoadout() {
}
interface LineProps {
- line: string;
+ name: string;
lines: UpgradeLine[];
}
@@ -43,107 +46,143 @@ const mergeLines = {
alternative_guns: "guns",
} as Record;
-function Line({ line, lines }: LineProps) {
+function Line({ name, lines }: LineProps) {
const combinedLines = useMemo(() => {
- const lines: string[] = [line];
+ const names: string[] = [name];
for (const key in mergeLines) {
- if (mergeLines[key] === line) {
- lines.push(key);
+ if (mergeLines[key] === name && lines.some((line) => line.name === key)) {
+ names.push(key);
}
}
- return lines;
- }, [line]);
+ return names;
+ }, [name]);
return (
- {!(line in mergeLines) &&
- combinedLines.map((line) => )}
+ {!(name in mergeLines) &&
+ combinedLines.map((name) => (
+
+ ))}
);
}
-function LineInner({ line, lines }: LineProps) {
- const id = Tankopedia.use((state) => state.protagonist.id);
- const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
- const tank = useAwait(() => api.tank(id), `tank-${id}`);
+function LineInner({ name, lines }: LineProps) {
+ const line = lines.find((line) => line.name === name)!;
+ return line.stages.map((stage, index) => (
+
+ ));
+}
+
+interface LineElementProps {
+ index: number;
+ lineName: string;
+ stage: StageParameters;
+}
+
+function LineElement({ index, lineName, stage }: LineElementProps) {
+ const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
+ const tank = useProtagonist();
const strings = useStrings();
+ const upgradePreset = useUpgradePreset(tank.tank!.tank_upgrade_preset);
+
+ let price: StandardPrice | undefined;
+
+ switch (stage.grade) {
+ case Grade.GRADE_COMMON:
+ price = upgradePreset.common_grade_price;
+ break;
+
+ case Grade.GRADE_RARE:
+ price = upgradePreset.rare_grade_price;
+ break;
+
+ case Grade.GRADE_EPIC:
+ price = upgradePreset.unique_grade_price;
+ break;
+
+ case Grade.GRADE_LEGENDARY:
+ price = upgradePreset.legendary_grade_price;
+ break;
+ }
+
+ for (const override of upgradePreset.prices_overrides) {
+ if (override.stage_tech_name !== stage.tech_name) continue;
+ price = override.price;
+ }
+
+ const isSelected = upgrades[lineName] === index;
+
+ return (
+ {
+ Tankopedia.mutate((draft) => {
+ draft.protagonist.upgrades[lineName] = index;
+
+ for (const required of stage.required_upgrades) {
+ for (const line of tank.tank!.upgrade_lines) {
+ let i = 0;
- return lines
- .find((l) => l.name === line)!
- .stages.map((stage, index) => {
- const isSelected = upgrades[line] === index;
-
- return (
-
- {
- Tankopedia.mutate((draft) => {
- draft.protagonist.upgrades[line] = index;
-
- for (const required of stage.required_upgrades) {
- for (const line of tank.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 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 tank.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 tank.tank!.upgrade_lines) {
- let i = draft.protagonist.upgrades[line.name];
+ const idx = draft.protagonist.upgrades[line.name];
+
+ if (line.stages[idx]?.tech_name === required) {
+ return true;
+ }
+ }
- while (i > 0) {
- const currentStage = line.stages[i];
- const valid = currentStage.required_upgrades.every(
- (required) => {
- for (const line of tank.tank!.upgrade_lines) {
- const idx = draft.protagonist.upgrades[line.name];
+ return false;
+ });
- if (line.stages[idx]?.tech_name === required) {
- return true;
- }
- }
+ if (valid) break;
- return false;
- },
- );
+ i--;
+ }
- if (valid) break;
+ draft.protagonist.upgrades[line.name] = i;
+ }
+ });
+ }}
+ >
+
+ {/* {stage.display_name} */}
- i--;
- }
+
+ {lineName in mergeLines
+ ? strings.tanks.loadout.alternative
+ : romanize(stage.number)}
+
- draft.protagonist.upgrades[line.name] = i;
- }
- });
- }}
- >
-
- {/* {stage.display_name} */}
- {/* {stage.grade} */}
-
-
- {line in mergeLines
- ? strings.tanks.loadout.alternative
- : romanize(stage.number)}
-
-
-
- );
- });
+ {price && }
+
+ );
}
diff --git a/packages/website-ue/src/core/api/abstract.ts b/packages/website-ue/src/core/api/abstract.ts
index 077819a49..881416ba7 100644
--- a/packages/website-ue/src/core/api/abstract.ts
+++ b/packages/website-ue/src/core/api/abstract.ts
@@ -1,3 +1,4 @@
+import type { MetadataAccessor } from "@blitzkit/closed";
import type { Strings } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import type { Avatar } from "@protos/blitzkit/avatar";
@@ -10,12 +11,18 @@ 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";
+import type { TankUpgradePricePresetComponent } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_price_preset_component";
export abstract class AbstractAPI {
+ abstract metadata: MetadataAccessor;
+
abstract tankList(): Promise;
abstract tank(id: string): Promise;
abstract tanks(): Promise;
abstract popularTanks(): Promise;
+ abstract tankUpgradePreset(
+ name: string,
+ ): Promise;
abstract tiers(): Promise;
diff --git a/packages/website-ue/src/core/api/client.ts b/packages/website-ue/src/core/api/client.ts
index 9ee60bf85..c042c9eb0 100644
--- a/packages/website-ue/src/core/api/client.ts
+++ b/packages/website-ue/src/core/api/client.ts
@@ -5,11 +5,16 @@ import { PopularTanks } from "@protos/blitzkit/popular_tanks";
import { TankList } from "@protos/blitzkit/tank_list";
import { Tanks } from "@protos/blitzkit/tanks";
import { Tiers } from "@protos/blitzkit/tiers";
+import { TankUpgradePricePresetComponent } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_price_preset_component";
import { AbstractAPI, Cache } from "./abstract";
const rejected = Promise.reject("Not implemented on client");
export class ClientAPI extends AbstractAPI {
+ get metadata(): never {
+ throw new Error("MetadataAccessor is inaccessible on the client");
+ }
+
@Cache()
tankList() {
return fetchPB("/api/tanks/list.pb", TankList);
@@ -31,6 +36,14 @@ export class ClientAPI extends AbstractAPI {
return fetchPB("/api/tanks/popular.pb", PopularTanks);
}
+ @Cache()
+ tankUpgradePreset(name: string) {
+ return fetchPB(
+ `/api/tanks/presets/upgrade/${name}.pb`,
+ TankUpgradePricePresetComponent,
+ );
+ }
+
@Cache()
tiers() {
return fetchPB("/api/tiers.pb", Tiers);
diff --git a/packages/website-ue/src/core/api/server.ts b/packages/website-ue/src/core/api/server.ts
index 70fc7e7e0..19e4a58ad 100644
--- a/packages/website-ue/src/core/api/server.ts
+++ b/packages/website-ue/src/core/api/server.ts
@@ -31,7 +31,7 @@ interface LocalizationConfig {
}
export class ServerAPI extends AbstractAPI {
- constructor(private metadata: MetadataAccessor) {
+ constructor(public metadata: MetadataAccessor) {
super();
}
@@ -174,6 +174,12 @@ export class ServerAPI extends AbstractAPI {
return JSON.parse(data) as PopularTanks;
}
+ @Cache()
+ async tankUpgradePreset(id: string) {
+ const item = this.metadata.item(`TankUpgradePricePresetEntity.${id}`);
+ return item.TankUpgradePricePreset();
+ }
+
@Cache()
async tiers() {
const group = this.metadata.group("TankTierEntity");
diff --git a/packages/website-ue/src/core/tankopedia/aggregateParameters.ts b/packages/website-ue/src/core/tankopedia/aggregateParameters.ts
index 280dc7407..2bae914d3 100644
--- a/packages/website-ue/src/core/tankopedia/aggregateParameters.ts
+++ b/packages/website-ue/src/core/tankopedia/aggregateParameters.ts
@@ -1,5 +1,5 @@
-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 type { TankCatalogComponent } from "@protos/game/proto/legacy/blitz_static_tank_component";
import { PenetrationGroup } from "@protos/game/proto/legacy/blitz_static_tank_penetration_group";
import {
ModuleUpgrade,
diff --git a/packages/website-ue/src/hooks/useProtagonist.ts b/packages/website-ue/src/hooks/useProtagonist.ts
new file mode 100644
index 000000000..57a5a1f2d
--- /dev/null
+++ b/packages/website-ue/src/hooks/useProtagonist.ts
@@ -0,0 +1,10 @@
+import { api } from "../core/api/dynamic";
+import { Tankopedia } from "../stores/tankopedia";
+import { useAwait } from "./useAwait";
+
+export function useProtagonist() {
+ const id = Tankopedia.use((state) => state.protagonist.id);
+ const tank = useAwait(() => api.tank(id), `tank-${id}`);
+
+ return tank;
+}
diff --git a/packages/website-ue/src/hooks/useUpgradePreset.ts b/packages/website-ue/src/hooks/useUpgradePreset.ts
new file mode 100644
index 000000000..202500728
--- /dev/null
+++ b/packages/website-ue/src/hooks/useUpgradePreset.ts
@@ -0,0 +1,12 @@
+import { api } from "../core/api/dynamic";
+import { useAwait } from "./useAwait";
+
+export function useUpgradePreset(name: string) {
+ const [, discriminator] = name.split(".");
+ const preset = useAwait(
+ () => api.tankUpgradePreset(discriminator),
+ `tank-upgrade-preset-${discriminator}`,
+ );
+
+ return preset;
+}
diff --git a/packages/website-ue/src/pages/api/tanks/presets/upgrade/[id].pb.ts b/packages/website-ue/src/pages/api/tanks/presets/upgrade/[id].pb.ts
new file mode 100644
index 000000000..096600790
--- /dev/null
+++ b/packages/website-ue/src/pages/api/tanks/presets/upgrade/[id].pb.ts
@@ -0,0 +1,14 @@
+import { TankUpgradePricePresetComponent } from "@protos/game/proto/legacy/blitz_static_tank_upgrade_price_preset_component";
+import type { APIContext, GetStaticPaths } from "astro";
+import { api } from "../../../../../core/api/dynamic";
+
+export const getStaticPaths = (async () => {
+ return api.metadata
+ .group("TankUpgradePricePresetEntity")
+ .map(({ name }) => ({ params: { id: name } }));
+}) satisfies GetStaticPaths;
+
+export async function GET({ params }: APIContext) {
+ const preset = await api.tankUpgradePreset(params.id);
+ return new Response(TankUpgradePricePresetComponent.encode(preset).finish());
+}