mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
module price rendering
This commit is contained in:
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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
Reference in new issue
Block a user