mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-28 13:09:51 +01:00
Compare commits
3
Commits
885c260605
...
d9b22431fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9b22431fa | ||
|
|
0a186399b6 | ||
|
|
d2b4cc3e7d |
No files matched your search
+2
-1
@@ -6,6 +6,7 @@
|
||||
"uasset",
|
||||
"oodle",
|
||||
"blitzkit",
|
||||
"webp"
|
||||
"webp",
|
||||
"hocs"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"price": {
|
||||
"free": "Free"
|
||||
},
|
||||
|
||||
"nations": {
|
||||
"0": "Unspecified",
|
||||
"1": "USSR",
|
||||
@@ -198,6 +202,20 @@
|
||||
},
|
||||
|
||||
"tanks": {
|
||||
"characteristics": {
|
||||
"titles": {
|
||||
"meta": "Meta",
|
||||
"firepower": "Firepower",
|
||||
"maneuverability": "Maneuverability",
|
||||
"survivability": "Survivability"
|
||||
}
|
||||
},
|
||||
|
||||
"navigation": {
|
||||
"tank": "Tank",
|
||||
"characteristics": "Characteristics"
|
||||
},
|
||||
|
||||
"loadout": {
|
||||
"consumables": "Consumables",
|
||||
"modules": "Modules",
|
||||
|
||||
@@ -6,23 +6,35 @@ import { useLocale } from "../../hooks/useLocale";
|
||||
import { classNames } from "../../ui/classNames";
|
||||
import { Text } from "../Text";
|
||||
import styles from "./index.module.css";
|
||||
import { useStrings } from "../../hooks/useStrings";
|
||||
|
||||
interface PriceProps extends ComponentProps<"div"> {
|
||||
price: StandardPrice;
|
||||
price?: StandardPrice;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classNames(styles.price, className)} {...props}>
|
||||
{price.price_list.map((entry, index) => (
|
||||
{normalized.map((entry, index) => (
|
||||
<PriceEntry key={index} entry={entry} />
|
||||
))}
|
||||
|
||||
{price.price_list.length === 0 && (
|
||||
<Text className={styles.entry} lowContrast>
|
||||
Free
|
||||
</Text>
|
||||
)}
|
||||
{price === undefined ||
|
||||
(normalized.length === 0 && (
|
||||
<Text className={styles.entry} lowContrast>
|
||||
{strings.price.free}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.characteristics {
|
||||
padding: var(--space-5);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
box-sizing: border-box;
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: var(--space-4);
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
|
||||
.title {
|
||||
.title-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: var(--space-1);
|
||||
flex: 1;
|
||||
|
||||
background-color: var(--gray-5);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { CaretDownIcon, CaretRightIcon } from "@radix-ui/react-icons";
|
||||
import { useStrings } from "../../hooks/useStrings";
|
||||
import { Tankopedia } from "../../stores/tankopedia";
|
||||
import {
|
||||
characteristicsOrder,
|
||||
type CharacteristicRenderConfig,
|
||||
type CharacteristicsGroup,
|
||||
} from "../../tankopedia/characteristicsOrder";
|
||||
import { Heading } from "../Heading";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export function TankopediaCharacteristics() {
|
||||
return (
|
||||
<div className={styles.characteristics}>
|
||||
{characteristicsOrder.map((group) => (
|
||||
<Group {...group} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Group({ group, order }: CharacteristicsGroup) {
|
||||
const isOpen = Tankopedia.use((state) => state.groups[group]);
|
||||
const strings = useStrings();
|
||||
|
||||
return (
|
||||
<div className={styles.group}>
|
||||
<div className={styles["title-wrapper"]}>
|
||||
<Heading className={styles.title} size="3">
|
||||
<div className={styles["title-content"]}>
|
||||
{isOpen ? <CaretDownIcon /> : <CaretRightIcon />}
|
||||
{strings.tanks.characteristics.titles[group]}
|
||||
</div>
|
||||
</Heading>
|
||||
|
||||
<div className={styles.separator} />
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
{order.map((item) =>
|
||||
"toy" in item ? <Toy {...item} /> : <Item {...item} />,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToyProps {
|
||||
toy: string;
|
||||
}
|
||||
|
||||
function Toy({ toy }: ToyProps) {
|
||||
return <span>{toy}</span>;
|
||||
}
|
||||
|
||||
function Item({ name }: CharacteristicRenderConfig) {
|
||||
return <span>{name}</span>;
|
||||
}
|
||||
@@ -18,25 +18,32 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
|
||||
.consumable {
|
||||
font-size: var(--space-5);
|
||||
.consumable-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
.consumable {
|
||||
font-size: var(--space-5);
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.entry {
|
||||
.info {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-direction: column;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
.entry {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { romanize } from "@blitzkit/core";
|
||||
import type { ConsumableComponent } from "@protos/blitz_static_consumable_component";
|
||||
import type { BlitzStaticEquipmentPresetComponent_EquipmentSlot } from "@protos/blitz_static_equipment_preset_component";
|
||||
import type { BlitzStaticPurchaseComponent } from "@protos/blitz_static_purchase_component";
|
||||
import { Grade } from "@protos/blitz_static_standard_grades_enum";
|
||||
import { StandardPrice } from "@protos/blitz_static_standard_price";
|
||||
import type { UpgradeLine } from "@protos/blitz_static_tank_upgrade_line";
|
||||
@@ -24,25 +26,20 @@ import {
|
||||
import { useCompatibility } from "../../hooks/useCompatibility";
|
||||
import { useConsumables } from "../../hooks/useConsumables";
|
||||
import { useEquipment } from "../../hooks/useEquipment";
|
||||
import { useGameStrings } from "../../hooks/useGameStrings";
|
||||
import { useProtagonist } from "../../hooks/useProtagonist";
|
||||
import { useStrings } from "../../hooks/useStrings";
|
||||
import { useTierPrices } from "../../hooks/useTierPrices";
|
||||
import { useUpgradePreset } from "../../hooks/useUpgradePreset";
|
||||
import { Tankopedia, TankopediaCompare } from "../../stores/tankopedia";
|
||||
import { Tankopedia } from "../../stores/tankopedia";
|
||||
import type { ComputedCharacteristics } from "../../tankopedia/computeCharacteristics";
|
||||
import { Button } from "../Button";
|
||||
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";
|
||||
import type { ComputedCharacteristics } from "../../tankopedia/computeCharacteristics";
|
||||
import { Tooltip } from "../Tooltip";
|
||||
import { useGameStrings } from "../../hooks/useGameStrings";
|
||||
import { TankopediaCollapsible } from "../TankopediaCollapsible";
|
||||
import type { ConsumableComponent } from "@protos/blitz_static_consumable_component";
|
||||
import { Tank } from "@protos/tank";
|
||||
import { select } from "three/src/nodes/math/ConditionalNode.js";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
interface TankopediaLoadoutProps {
|
||||
characteristics: ComputedCharacteristics;
|
||||
@@ -95,7 +92,13 @@ function Consumables({ characteristics }: ConsumablesProps) {
|
||||
{Object.entries(consumables.consumables).map(
|
||||
([id, { compatibility, consumable, purchase }]) => {
|
||||
if (!isCompatible(compatibility!)) return null;
|
||||
return <Consumable id={id} consumable={consumable!} />;
|
||||
return (
|
||||
<Consumable
|
||||
id={id}
|
||||
consumable={consumable!}
|
||||
purchase={purchase!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
@@ -106,51 +109,72 @@ function Consumables({ characteristics }: ConsumablesProps) {
|
||||
interface ConsumableProps {
|
||||
id: string;
|
||||
consumable: ConsumableComponent;
|
||||
purchase: BlitzStaticPurchaseComponent;
|
||||
}
|
||||
|
||||
function Consumable({ id, consumable }: ConsumableProps) {
|
||||
function Consumable({ id, consumable, purchase }: ConsumableProps) {
|
||||
const gameStrings = useGameStrings("ConsumableEntity");
|
||||
const isSelected = Tankopedia.use((state) =>
|
||||
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 (
|
||||
<Tooltip tooltip={gameStrings[consumable!.name_key]}>
|
||||
<Button
|
||||
radius="1"
|
||||
variant={isSelected ? "surface" : "soft"}
|
||||
color={isSelected ? undefined : "gray"}
|
||||
className={styles.consumable}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
const index = draft.protagonist.consumables.indexOf(id);
|
||||
<div className={styles["consumable-wrapper"]}>
|
||||
<Tooltip tooltip={gameStrings[consumable!.name_key]}>
|
||||
<Button
|
||||
radius="1"
|
||||
variant={isSelected ? "surface" : "soft"}
|
||||
color={isSelected ? undefined : "gray"}
|
||||
className={styles.consumable}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
const index = draft.protagonist.consumables.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
draft.protagonist.consumables.push(id);
|
||||
} else {
|
||||
draft.protagonist.consumables.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img src={`/media/consumables/${id}.webp`} />
|
||||
<div className={styles.info}>
|
||||
<Text lowContrast size="minor">
|
||||
<div className={styles.entry}>
|
||||
<ClockIcon />
|
||||
10s
|
||||
</div>
|
||||
</Text>
|
||||
if (index === -1) {
|
||||
draft.protagonist.consumables.push(id);
|
||||
} else {
|
||||
draft.protagonist.consumables.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img src={`/media/consumables/${id}.webp`} />
|
||||
<div className={styles.info}>
|
||||
<Text lowContrast size="minor">
|
||||
<div className={styles.entry}>
|
||||
<ClockIcon />
|
||||
10s
|
||||
</div>
|
||||
</Text>
|
||||
|
||||
<Text lowContrast size="minor">
|
||||
<div className={styles.entry}>
|
||||
<ResetIcon />
|
||||
10s
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Text lowContrast size="minor">
|
||||
<div className={styles.entry}>
|
||||
<ResetIcon />
|
||||
10s
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Price price={price} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -484,8 +508,8 @@ function LineElement({ index, lineName, stage }: LineElementProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip tooltip={gameStrings[stage.display_name]}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.wrapper}>
|
||||
<Tooltip tooltip={gameStrings[stage.display_name]}>
|
||||
<Button
|
||||
className={styles.stage}
|
||||
color={isSelected ? undefined : "gray"}
|
||||
@@ -569,9 +593,9 @@ function LineElement({ index, lineName, stage }: LineElementProps) {
|
||||
: romanize(stage.number)}
|
||||
</Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{<Price className={styles.price} price={price} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{<Price className={styles.price} price={price} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
.sandbox {
|
||||
.wrapper {
|
||||
height: calc(100dvh - 16rem);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.canvas {
|
||||
.sandbox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
import { Canvas, useLoader } from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import styles from "./index.module.css";
|
||||
import { Canvas, useLoader } from "@react-three/fiber";
|
||||
import { GLTFLoader } from "three-stdlib";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export function TankopediaSandbox() {
|
||||
return (
|
||||
<div className={styles.sandbox}>
|
||||
<Canvas shadows className={styles.canvas}>
|
||||
{!import.meta.env.SSR && <Content />}
|
||||
</Canvas>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.sandbox}>
|
||||
<Canvas
|
||||
// scene={{ fog: new Fog("black", 15, 40) }}
|
||||
shadows
|
||||
className={styles.canvas}
|
||||
frameloop="demand"
|
||||
>
|
||||
{!import.meta.env.SSR && <Content />}
|
||||
</Canvas>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,8 +33,8 @@ function Content() {
|
||||
<pointLight castShadow position={[3, 10, 5]} intensity={1000} />
|
||||
|
||||
<mesh receiveShadow position={[0, -1, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[30, 30]} />
|
||||
<meshStandardMaterial color="green" />
|
||||
<planeGeometry args={[2 ** 8, 2 ** 8]} />
|
||||
<meshStandardMaterial color="black" />
|
||||
</mesh>
|
||||
|
||||
<OrbitControls />
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
.sandbox {
|
||||
flex-grow: 1;
|
||||
background-color: black;
|
||||
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.stats {
|
||||
.loadout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -16,5 +17,7 @@
|
||||
padding: var(--space-5);
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
background-color: var(--gray-2);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { api } from "../../../../api/dynamic";
|
||||
import { TankopediaCharacteristics } from "../../../../components/TankopediaCharacteristics";
|
||||
import { TankopediaLoadout } from "../../../../components/TankopediaLoadout";
|
||||
import { TankopediaSandbox } from "../../../../components/TankopediaSandbox";
|
||||
import { withErrorWrapper } from "../../../../hocs/withErrorWrapper";
|
||||
import { withLocale } from "../../../../hocs/withLocale";
|
||||
import { useAwait } from "../../../../hooks/useAwait";
|
||||
@@ -10,7 +12,6 @@ import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import { characteristicsOrder } from "../../../../tankopedia/characteristicsOrder";
|
||||
import { computeCharacteristics } from "../../../../tankopedia/computeCharacteristics";
|
||||
import styles from "./_index.module.css";
|
||||
import { TankopediaSandbox } from "../../../../components/TankopediaSandbox";
|
||||
|
||||
interface PageProps {
|
||||
id: string;
|
||||
@@ -22,8 +23,6 @@ export const Page = withErrorWrapper(
|
||||
|
||||
Tankopedia.useInitialization(protagonistTank);
|
||||
|
||||
// const renderCharacteristic = useCharacteristicRenderer();
|
||||
|
||||
const protagonist = Tankopedia.use((state) => state.protagonist);
|
||||
const protagonistEquipment = useEquipment(protagonistTank.tank!);
|
||||
|
||||
@@ -39,12 +38,13 @@ export const Page = withErrorWrapper(
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.sandbox}>
|
||||
<TankopediaSandbox />
|
||||
<div className={styles.loadout}>
|
||||
<TankopediaLoadout characteristics={characteristics} />
|
||||
</div>
|
||||
|
||||
<div className={styles.stats}>
|
||||
<TankopediaLoadout characteristics={characteristics} />
|
||||
<div className={styles.sandbox}>
|
||||
<TankopediaSandbox />
|
||||
<TankopediaCharacteristics />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,12 @@ import type { Tank } from "@protos/tank";
|
||||
import { Varuna } from "varuna";
|
||||
import { createTankState } from "../tankopedia/createTankState";
|
||||
import { type TankState } from "../tankopedia/tankState";
|
||||
import { characteristicsOrder } from "../tankopedia/characteristicsOrder";
|
||||
|
||||
interface Tankopedia {
|
||||
compare: TankopediaCompare;
|
||||
groups: Record<string, boolean>;
|
||||
|
||||
protagonist: TankState;
|
||||
}
|
||||
|
||||
@@ -14,7 +17,15 @@ export enum TankopediaCompare {
|
||||
All,
|
||||
}
|
||||
|
||||
const groups: Record<string, boolean> = {};
|
||||
|
||||
for (const { group } of characteristicsOrder) {
|
||||
groups[group] = true;
|
||||
}
|
||||
|
||||
export const Tankopedia = new Varuna<Tankopedia, Tank>((tank) => ({
|
||||
compare: TankopediaCompare.TierAndClass,
|
||||
groups,
|
||||
|
||||
protagonist: createTankState(tank),
|
||||
}));
|
||||
@@ -29,7 +29,7 @@ export interface CharacteristicRenderConfig {
|
||||
}) => ReactNode;
|
||||
}
|
||||
|
||||
type CharacteristicsOrder = {
|
||||
export type CharacteristicsGroup = {
|
||||
group: string;
|
||||
order: (
|
||||
| CharacteristicRenderConfig
|
||||
@@ -37,9 +37,9 @@ type CharacteristicsOrder = {
|
||||
toy: string;
|
||||
}
|
||||
)[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export const characteristicsOrder: CharacteristicsOrder = [
|
||||
export const characteristicsOrder: CharacteristicsGroup[] = [
|
||||
{
|
||||
group: "meta",
|
||||
order: [
|
||||
|
||||
Reference in new issue
Block a user