consumable prices

This commit is contained in:
tresabhi committed 2026-05-28 13:14:38 -05:00
1 parent 885c260605
commit d2b4cc3e7d
4 files changed
+113 -62

No files matched your search

+4
View File
@@ -1,4 +1,8 @@
{ {
"price": {
"free": "Free"
},
"nations": { "nations": {
"0": "Unspecified", "0": "Unspecified",
"1": "USSR", "1": "USSR",
@@ -6,23 +6,35 @@ import { useLocale } from "../../hooks/useLocale";
import { classNames } from "../../ui/classNames"; import { classNames } from "../../ui/classNames";
import { Text } from "../Text"; import { Text } from "../Text";
import styles from "./index.module.css"; import styles from "./index.module.css";
import { useStrings } from "../../hooks/useStrings";
interface PriceProps extends ComponentProps<"div"> { interface PriceProps extends ComponentProps<"div"> {
price: StandardPrice; price?: StandardPrice;
} }
export function Price({ price, className, ...props }: PriceProps) { export function Price({ price, className, ...props }: PriceProps) {
const strings = useStrings();
const normalized =
price === undefined
? []
: price.price_list.filter(
(entry) =>
entry.currency_price === undefined ||
entry.currency_price.amount !== 0,
);
return ( return (
<div className={classNames(styles.price, className)} {...props}> <div className={classNames(styles.price, className)} {...props}>
{price.price_list.map((entry, index) => ( {normalized.map((entry, index) => (
<PriceEntry key={index} entry={entry} /> <PriceEntry key={index} entry={entry} />
))} ))}
{price.price_list.length === 0 && ( {price === undefined ||
<Text className={styles.entry} lowContrast> (normalized.length === 0 && (
Free <Text className={styles.entry} lowContrast>
</Text> {strings.price.free}
)} </Text>
))}
</div> </div>
); );
} }
@@ -18,25 +18,32 @@
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3); gap: var(--space-3);
.consumable { .consumable-wrapper {
font-size: var(--space-5); display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
img { .consumable {
width: 1em; font-size: var(--space-5);
height: 1em;
}
.info { img {
display: flex; width: 1em;
flex-direction: column; height: 1em;
}
.entry { .info {
display: flex; display: flex;
gap: var(--space-2); flex-direction: column;
svg { .entry {
width: 1em; display: flex;
height: 1em; gap: var(--space-2);
svg {
width: 1em;
height: 1em;
}
} }
} }
} }
@@ -43,6 +43,7 @@ import { TankopediaCollapsible } from "../TankopediaCollapsible";
import type { ConsumableComponent } from "@protos/blitz_static_consumable_component"; import type { ConsumableComponent } from "@protos/blitz_static_consumable_component";
import { Tank } from "@protos/tank"; import { Tank } from "@protos/tank";
import { select } from "three/src/nodes/math/ConditionalNode.js"; import { select } from "three/src/nodes/math/ConditionalNode.js";
import type { BlitzStaticPurchaseComponent } from "@protos/blitz_static_purchase_component";
interface TankopediaLoadoutProps { interface TankopediaLoadoutProps {
characteristics: ComputedCharacteristics; characteristics: ComputedCharacteristics;
@@ -95,7 +96,13 @@ function Consumables({ characteristics }: ConsumablesProps) {
{Object.entries(consumables.consumables).map( {Object.entries(consumables.consumables).map(
([id, { compatibility, consumable, purchase }]) => { ([id, { compatibility, consumable, purchase }]) => {
if (!isCompatible(compatibility!)) return null; if (!isCompatible(compatibility!)) return null;
return <Consumable id={id} consumable={consumable!} />; return (
<Consumable
id={id}
consumable={consumable!}
purchase={purchase!}
/>
);
}, },
)} )}
</div> </div>
@@ -106,51 +113,72 @@ function Consumables({ characteristics }: ConsumablesProps) {
interface ConsumableProps { interface ConsumableProps {
id: string; id: string;
consumable: ConsumableComponent; consumable: ConsumableComponent;
purchase: BlitzStaticPurchaseComponent;
} }
function Consumable({ id, consumable }: ConsumableProps) { function Consumable({ id, consumable, purchase }: ConsumableProps) {
const gameStrings = useGameStrings("ConsumableEntity"); const gameStrings = useGameStrings("ConsumableEntity");
const isSelected = Tankopedia.use((state) => const isSelected = Tankopedia.use((state) =>
state.protagonist.consumables.includes(id), state.protagonist.consumables.includes(id),
); );
const tank = useProtagonist();
const tierPrices = useTierPrices();
const price = useMemo(() => {
const { prices } = tierPrices.prices[purchase.price_per_tier_catalog_id];
for (const { tier_catalog_id, unlock_price } of prices) {
if (tier_catalog_id === tank.tank!.tier_catalog_id) {
return unlock_price;
}
}
throw new Error(
`No price found for tier catalog id ${tank.tank!.tier_catalog_id}`,
);
}, [tank.id, purchase.price_per_tier_catalog_id]);
return ( return (
<Tooltip tooltip={gameStrings[consumable!.name_key]}> <div className={styles["consumable-wrapper"]}>
<Button <Tooltip tooltip={gameStrings[consumable!.name_key]}>
radius="1" <Button
variant={isSelected ? "surface" : "soft"} radius="1"
color={isSelected ? undefined : "gray"} variant={isSelected ? "surface" : "soft"}
className={styles.consumable} color={isSelected ? undefined : "gray"}
onClick={() => { className={styles.consumable}
Tankopedia.mutate((draft) => { onClick={() => {
const index = draft.protagonist.consumables.indexOf(id); Tankopedia.mutate((draft) => {
const index = draft.protagonist.consumables.indexOf(id);
if (index === -1) { if (index === -1) {
draft.protagonist.consumables.push(id); draft.protagonist.consumables.push(id);
} else { } else {
draft.protagonist.consumables.splice(index, 1); draft.protagonist.consumables.splice(index, 1);
} }
}); });
}} }}
> >
<img src={`/media/consumables/${id}.webp`} /> <img src={`/media/consumables/${id}.webp`} />
<div className={styles.info}> <div className={styles.info}>
<Text lowContrast size="minor"> <Text lowContrast size="minor">
<div className={styles.entry}> <div className={styles.entry}>
<ClockIcon /> <ClockIcon />
10s 10s
</div> </div>
</Text> </Text>
<Text lowContrast size="minor"> <Text lowContrast size="minor">
<div className={styles.entry}> <div className={styles.entry}>
<ResetIcon /> <ResetIcon />
10s 10s
</div> </div>
</Text> </Text>
</div> </div>
</Button> </Button>
</Tooltip> </Tooltip>
<Price price={price} />
</div>
); );
} }
@@ -484,8 +512,8 @@ function LineElement({ index, lineName, stage }: LineElementProps) {
} }
return ( return (
<Tooltip tooltip={gameStrings[stage.display_name]}> <div className={styles.wrapper}>
<div className={styles.wrapper}> <Tooltip tooltip={gameStrings[stage.display_name]}>
<Button <Button
className={styles.stage} className={styles.stage}
color={isSelected ? undefined : "gray"} color={isSelected ? undefined : "gray"}
@@ -569,9 +597,9 @@ function LineElement({ index, lineName, stage }: LineElementProps) {
: romanize(stage.number)} : romanize(stage.number)}
</Text> </Text>
</Button> </Button>
</Tooltip>
{<Price className={styles.price} price={price} />} {<Price className={styles.price} price={price} />}
</div> </div>
</Tooltip>
); );
} }