module price rendering

This commit is contained in:
tresabhi committed 2026-05-15 10:54:36 -05:00
1 parent 6d2573ee95
commit f0e0d849d5
12 files changed
+269 -91

No files matched your search

@@ -3,5 +3,5 @@
aspect-ratio: 1 / 1;
font-size: 1.75em;
font-size: 2em;
}
@@ -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;
}
}
}
}
@@ -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 (
<div className={classNames(styles.price, className)} {...props}>
{price.price_list.map((entry, index) => (
<PriceEntry key={index} entry={entry} />
))}
</div>
);
}
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 (
<Text size="minor" lowContrast className={styles.entry}>
<div className={styles.wrapper}>
<img
className={styles.icon}
src="https://static.vecteezy.com/system/resources/thumbnails/047/493/988/small/hairy-fluffy-cat-playing-png.png"
/>
{formatCompact(locale, entry.currency_price.amount)}
</div>
</Text>
);
}
@@ -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%);
}
}
}
}
@@ -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 (
<Section>
@@ -25,7 +28,7 @@ export function TankopediaLoadout() {
{tank.tank!.upgrade_lines.map((line) => (
<Line
key={line.name}
line={line.name}
name={line.name}
lines={tank.tank!.upgrade_lines}
/>
))}
@@ -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<string, string>;
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 (
<div className={styles.line}>
{!(line in mergeLines) &&
combinedLines.map((line) => <LineInner line={line} lines={lines} />)}
{!(name in mergeLines) &&
combinedLines.map((name) => (
<LineInner key={name} name={name} lines={lines} />
))}
</div>
);
}
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) => (
<LineElement key={index} index={index} lineName={name} stage={stage} />
));
}
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 (
<IconButton
color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"}
radius="1"
className={styles.stage}
onClick={() => {
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 (
<Fragment key={stage.tech_name}>
<IconButton
color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"}
radius="1"
className={styles.stage}
onClick={() => {
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;
}
});
}}
>
<img
className={styles.icon}
src={`/api/tanks/modules/${stage.stage_type}.webp`}
/>
{/* {stage.display_name} */}
i--;
}
<Text lowContrast size="minor" className={styles.tier}>
{lineName in mergeLines
? strings.tanks.loadout.alternative
: romanize(stage.number)}
</Text>
draft.protagonist.upgrades[line.name] = i;
}
});
}}
>
<img src={`/api/tanks/modules/${stage.stage_type}.webp`} />
{/* {stage.display_name} */}
{/* {stage.grade} */}
<Text lowContrast size="minor" className={styles.tier}>
{line in mergeLines
? strings.tanks.loadout.alternative
: romanize(stage.number)}
</Text>
</IconButton>
</Fragment>
);
});
{price && <Price className={styles.price} price={price} />}
</IconButton>
);
}
@@ -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<TankList>;
abstract tank(id: string): Promise<Tank>;
abstract tanks(): Promise<Tanks>;
abstract popularTanks(): Promise<PopularTanks>;
abstract tankUpgradePreset(
name: string,
): Promise<TankUpgradePricePresetComponent>;
abstract tiers(): Promise<Tiers>;
@@ -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);
+7 -1
View File
@@ -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");
@@ -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,
@@ -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;
}
@@ -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;
}
@@ -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<never, { id: string }>) {
const preset = await api.tankUpgradePreset(params.id);
return new Response(TankUpgradePricePresetComponent.encode(preset).finish());
}