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()); +}