mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 12:09:51 +02:00
TankopediaLoadout
This commit is contained in:
1 parent
558a02b6ae
commit
488451d71f
9 files changed
+309
-299
No files matched your search
@@ -2,7 +2,11 @@ import locales from "@blitzkit/i18n/locales.json";
|
|||||||
import { I18nString } from "@blitzkit/protos";
|
import { I18nString } from "@blitzkit/protos";
|
||||||
|
|
||||||
export function unwrapper(locale: string) {
|
export function unwrapper(locale: string) {
|
||||||
return function (i18nString: I18nString) {
|
return function (i18nString?: I18nString) {
|
||||||
|
if (i18nString === undefined) {
|
||||||
|
throw new Error("i18nString is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
if (locale in i18nString.locales) {
|
if (locale in i18nString.locales) {
|
||||||
return i18nString.locales[locale];
|
return i18nString.locales[locale];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
syntax = "proto2";
|
syntax = "proto3";
|
||||||
|
|
||||||
package blitzkit;
|
package blitzkit;
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ message EquipmentDefinitions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message Equipment {
|
message Equipment {
|
||||||
required I18nString name = 1;
|
I18nString name = 1;
|
||||||
required I18nString description = 2;
|
I18nString description = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EquipmentPreset {
|
message EquipmentPreset {
|
||||||
@@ -19,6 +19,5 @@ message EquipmentPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message EquipmentSlot {
|
message EquipmentSlot {
|
||||||
required uint32 left = 1;
|
repeated uint32 options = 1;
|
||||||
required uint32 right = 2;
|
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,38 @@
|
|||||||
import type { Consumable } from "@blitzkit/core";
|
import type { Consumable, EquipmentSlot } from "@blitzkit/protos";
|
||||||
import { TrashIcon } from "@radix-ui/react-icons";
|
import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
|
import { chunk } from "lodash-es";
|
||||||
|
import { api } from "../../api/dynamic";
|
||||||
|
import { equipmentColumns } from "../../config/equipment";
|
||||||
import { useConsumables } from "../../hooks/useConsumables";
|
import { useConsumables } from "../../hooks/useConsumables";
|
||||||
import { useProtagonistGun } from "../../hooks/useProtagonistGun";
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
||||||
import { useProtagonistTank } from "../../hooks/useProtagonistTank";
|
import { useEquipmentPreset } from "../../hooks/useEquipmentPreset";
|
||||||
import { useStrings } from "../../hooks/useStrings";
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
||||||
|
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
||||||
import { Tankopedia } from "../../stores/tankopedia";
|
import { Tankopedia } from "../../stores/tankopedia";
|
||||||
import { hasUpgrades } from "../../tankopedia/hasUpgrades";
|
import { hasUpgrades } from "../../tankopedia/hasUpgrades";
|
||||||
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
||||||
import type { ComputedCharacteristics } from "../../types/characteristics";
|
import type { ComputedCharacteristics } from "../../types/characteristics";
|
||||||
|
import { Button } from "../Button";
|
||||||
import { Heading } from "../Heading";
|
import { Heading } from "../Heading";
|
||||||
import { IconButton } from "../IconButton";
|
import { IconButton } from "../IconButton";
|
||||||
|
import { Text } from "../Text";
|
||||||
import { Tooltip } from "../Tooltip";
|
import { Tooltip } from "../Tooltip";
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
const equipment = await api.equipment();
|
||||||
|
|
||||||
interface TankopediaLoadoutProps {
|
interface TankopediaLoadoutProps {
|
||||||
characteristics: ComputedCharacteristics;
|
characteristics: ComputedCharacteristics;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
||||||
const tank = useProtagonistTank();
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const showModules = hasUpgrades(tank);
|
const showModules = hasUpgrades(tank);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showModules && <Modules />}
|
{/* {showModules && <Modules />} */}
|
||||||
<Equipment />
|
<Equipment />
|
||||||
<Consumables characteristics={characteristics} />
|
<Consumables characteristics={characteristics} />
|
||||||
<State />
|
<State />
|
||||||
@@ -120,8 +128,8 @@ interface ConsumablesProps {
|
|||||||
function Consumables({ characteristics }: ConsumablesProps) {
|
function Consumables({ characteristics }: ConsumablesProps) {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const consumables = useConsumables();
|
const consumables = useConsumables();
|
||||||
const tank = useProtagonistTank();
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const gun = useProtagonistGun();
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
||||||
const isCompatible = useTankCompatibility(tank, gun);
|
const isCompatible = useTankCompatibility(tank, gun);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -148,7 +156,7 @@ function Consumables({ characteristics }: ConsumablesProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Consumable key={id} id={Number(id)} consumable={consumable} />
|
<ConsumableEntry key={id} id={Number(id)} consumable={consumable} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -161,12 +169,12 @@ interface ConsumableProps {
|
|||||||
consumable: Consumable;
|
consumable: Consumable;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Consumable({ id, consumable }: ConsumableProps) {
|
function ConsumableEntry({ id, consumable }: ConsumableProps) {
|
||||||
const isSelected = Tankopedia.use((state) =>
|
const isSelected = Tankopedia.use((state) =>
|
||||||
state.protagonist.consumables.includes(id),
|
state.protagonist.consumables.includes(id),
|
||||||
);
|
);
|
||||||
const tank = useProtagonistTank();
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const unwrap = useUnwrap();
|
const unwrap = useUnwrapper();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["consumable-wrapper"]}>
|
<div className={styles["consumable-wrapper"]}>
|
||||||
@@ -207,15 +215,15 @@ function Consumable({ id, consumable }: ConsumableProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Price price={price} />
|
{/* <Price price={price} /> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Equipment() {
|
function Equipment() {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const tank = useProtagonistTank();
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const equipment = useEquipment(tank.tank!);
|
const equipment = useEquipmentPreset(tank.equipment_preset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
@@ -237,7 +245,7 @@ function Equipment() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.equipment}>
|
<div className={styles.equipment}>
|
||||||
{chunk(equipment.preset.slots, equipmentColumns).map((chunk, index) => (
|
{chunk(equipment.slots, equipmentColumns).map((chunk, index) => (
|
||||||
<EquipmentRow key={index} chunk={chunk} rowIndex={index} />
|
<EquipmentRow key={index} chunk={chunk} rowIndex={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +254,7 @@ function Equipment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EquipmentRowProps {
|
interface EquipmentRowProps {
|
||||||
chunk: BlitzStaticEquipmentPresetComponent_EquipmentSlot[];
|
chunk: EquipmentSlot[];
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +262,7 @@ function EquipmentRow({ chunk, rowIndex }: EquipmentRowProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
{chunk.map((slot, index) => (
|
{chunk.map((slot, index) => (
|
||||||
<EquipmentSlot
|
<EquipmentSlotEntry
|
||||||
key={index}
|
key={index}
|
||||||
slot={slot}
|
slot={slot}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
@@ -266,21 +274,25 @@ function EquipmentRow({ chunk, rowIndex }: EquipmentRowProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EquipmentSlotProps {
|
interface EquipmentSlotProps {
|
||||||
slot: BlitzStaticEquipmentPresetComponent_EquipmentSlot;
|
slot: EquipmentSlot;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EquipmentSlot({ slot, rowIndex, columnIndex }: EquipmentSlotProps) {
|
function EquipmentSlotEntry({
|
||||||
|
slot,
|
||||||
|
rowIndex,
|
||||||
|
columnIndex,
|
||||||
|
}: EquipmentSlotProps) {
|
||||||
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
||||||
const tank = useProtagonistTank();
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const equipment = useEquipment(tank.tank!);
|
const equipment = useEquipmentPreset(tank.equipment_preset);
|
||||||
const { price } = equipment.price.unlock_slot_prices[equipmentIndex];
|
// const { price } = equipment.price.unlock_slot_prices[equipmentIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.slots}>
|
<div className={styles.slots}>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
{slot.options_catalog_i_ds.map((id, index) => (
|
{slot.options.map((id, index) => (
|
||||||
<EquipmentOption
|
<EquipmentOption
|
||||||
key={id}
|
key={id}
|
||||||
optionIndex={index}
|
optionIndex={index}
|
||||||
@@ -290,7 +302,7 @@ function EquipmentSlot({ slot, rowIndex, columnIndex }: EquipmentSlotProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Price price={price!} />
|
{/* <Price price={price!} /> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -307,11 +319,14 @@ function EquipmentOption({
|
|||||||
columnIndex,
|
columnIndex,
|
||||||
}: EquipmentOptionProps) {
|
}: EquipmentOptionProps) {
|
||||||
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
||||||
const tank = useProtagonistTank();
|
|
||||||
const equipment = useEquipment(tank.tank!);
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const slot = equipment.preset.slots[equipmentIndex];
|
const equipmentPreset = useEquipmentPreset(tank.equipment_preset);
|
||||||
const id = slot.options_catalog_i_ds[optionIndex];
|
const unwrap = useUnwrapper();
|
||||||
const gameStrings = useGameStrings("EquipmentEntity");
|
|
||||||
|
const slot = equipmentPreset.slots[equipmentIndex];
|
||||||
|
const id = slot.options[optionIndex];
|
||||||
|
const specificEquipment = equipment.equipments[id];
|
||||||
|
|
||||||
const isSelected = Tankopedia.use(
|
const isSelected = Tankopedia.use(
|
||||||
(state) =>
|
(state) =>
|
||||||
@@ -320,13 +335,13 @@ function EquipmentOption({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltip={gameStrings[equipment.equipments[id].equipment_name]}>
|
<Tooltip tooltip={unwrap(specificEquipment.name)}>
|
||||||
<Button
|
<Button
|
||||||
color={isSelected ? undefined : "gray"}
|
color={isSelected ? undefined : "gray"}
|
||||||
variant={isSelected ? "surface" : "soft"}
|
variant={isSelected ? "surface" : "soft"}
|
||||||
data-selected={isSelected}
|
data-selected={isSelected}
|
||||||
radius="1"
|
radius="1"
|
||||||
parentArray
|
listChild
|
||||||
className={styles.slot}
|
className={styles.slot}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
@@ -344,292 +359,292 @@ function EquipmentOption({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modules() {
|
// function Modules() {
|
||||||
const strings = useStrings();
|
// const strings = useStrings();
|
||||||
const tank = useProtagonistTank();
|
// const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
|
// const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
|
||||||
const alternates = Tankopedia.use((state) => state.protagonist.alternates);
|
// const alternates = Tankopedia.use((state) => state.protagonist.alternates);
|
||||||
const isStock = useMemo(() => {
|
// const isStock = useMemo(() => {
|
||||||
return tank.tank!.upgrade_lines.every((line) => {
|
// return tank.tank!.upgrade_lines.every((line) => {
|
||||||
if (isAlternativeLine(line.name)) return true;
|
// if (isAlternativeLine(line.name)) return true;
|
||||||
return upgrades[line.name] === 0;
|
// return upgrades[line.name] === 0;
|
||||||
});
|
// });
|
||||||
}, [upgrades]);
|
// }, [upgrades]);
|
||||||
const isUpgraded = useMemo(() => {
|
// const isUpgraded = useMemo(() => {
|
||||||
return tank.tank!.upgrade_lines.every((line) => {
|
// return tank.tank!.upgrade_lines.every((line) => {
|
||||||
if (isAlternativeLine(line.name)) return true;
|
// if (isAlternativeLine(line.name)) return true;
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
upgrades[line.name] === line.stages.length - 1 ||
|
// upgrades[line.name] === line.stages.length - 1 ||
|
||||||
(line.name in alternates && alternates[line.name])
|
// (line.name in alternates && alternates[line.name])
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
}, [upgrades]);
|
// }, [upgrades]);
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className={styles.section}>
|
// <div className={styles.section}>
|
||||||
<div className={styles.header}>
|
// <div className={styles.header}>
|
||||||
<Heading size="4">{strings.tanks.loadout.modules}</Heading>
|
// <Heading size="4">{strings.tanks.loadout.modules}</Heading>
|
||||||
|
|
||||||
<div className={styles["upgrade-buttons"]}>
|
// <div className={styles["upgrade-buttons"]}>
|
||||||
<IconButton
|
// <IconButton
|
||||||
variant={isStock ? "solid" : "soft"}
|
// variant={isStock ? "solid" : "soft"}
|
||||||
size="minor"
|
// size="minor"
|
||||||
array
|
// array
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
// Tankopedia.mutate((draft) => {
|
||||||
for (const line of tank.tank!.upgrade_lines) {
|
// for (const line of tank.tank!.upgrade_lines) {
|
||||||
if (isAlternativeLine(line.name)) continue;
|
// if (isAlternativeLine(line.name)) continue;
|
||||||
|
|
||||||
draft.protagonist.upgrades[line.name] = 0;
|
// draft.protagonist.upgrades[line.name] = 0;
|
||||||
draft.protagonist.alternates[line.name] = false;
|
// draft.protagonist.alternates[line.name] = false;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
color="gray"
|
// color="gray"
|
||||||
>
|
// >
|
||||||
<DoubleArrowLeftIcon />
|
// <DoubleArrowLeftIcon />
|
||||||
</IconButton>
|
// </IconButton>
|
||||||
|
|
||||||
<IconButton
|
// <IconButton
|
||||||
variant={isUpgraded ? "solid" : "soft"}
|
// variant={isUpgraded ? "solid" : "soft"}
|
||||||
size="minor"
|
// size="minor"
|
||||||
array
|
// array
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
// Tankopedia.mutate((draft) => {
|
||||||
for (const line of tank.tank!.upgrade_lines) {
|
// for (const line of tank.tank!.upgrade_lines) {
|
||||||
draft.protagonist.upgrades[line.name] =
|
// draft.protagonist.upgrades[line.name] =
|
||||||
line.stages.length - 1;
|
// line.stages.length - 1;
|
||||||
|
|
||||||
if (isAlternativeLine(line.name)) {
|
// if (isAlternativeLine(line.name)) {
|
||||||
const original = originalLineName(line.name)!;
|
// const original = originalLineName(line.name)!;
|
||||||
draft.protagonist.alternates[original] = true;
|
// draft.protagonist.alternates[original] = true;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
color="gray"
|
// color="gray"
|
||||||
>
|
// >
|
||||||
<DoubleArrowRightIcon />
|
// <DoubleArrowRightIcon />
|
||||||
</IconButton>
|
// </IconButton>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
<div className={styles.lines}>
|
// <div className={styles.lines}>
|
||||||
{/* TODO: hide lines with 1 module except for alts */}
|
// {/* TODO: hide lines with 1 module except for alts */}
|
||||||
{tank.tank!.upgrade_lines.map((line) => (
|
// {tank.tank!.upgrade_lines.map((line) => (
|
||||||
<Line
|
// <Line
|
||||||
key={line.name}
|
// key={line.name}
|
||||||
name={line.name}
|
// name={line.name}
|
||||||
lines={tank.tank!.upgrade_lines}
|
// lines={tank.tank!.upgrade_lines}
|
||||||
/>
|
// />
|
||||||
))}
|
// ))}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
interface LineProps {
|
// interface LineProps {
|
||||||
name: string;
|
// name: string;
|
||||||
lines: UpgradeLine[];
|
// lines: UpgradeLine[];
|
||||||
}
|
// }
|
||||||
|
|
||||||
function Line({ name, lines }: LineProps) {
|
// function Line({ name, lines }: LineProps) {
|
||||||
const combinedLines = useMemo(() => {
|
// const combinedLines = useMemo(() => {
|
||||||
const names: string[] = [name];
|
// const names: string[] = [name];
|
||||||
|
|
||||||
for (const originalLine in alternativeLines) {
|
// for (const originalLine in alternativeLines) {
|
||||||
const alternativeLine = alternativeLines[originalLine];
|
// const alternativeLine = alternativeLines[originalLine];
|
||||||
|
|
||||||
if (
|
// if (
|
||||||
originalLine === name &&
|
// originalLine === name &&
|
||||||
lines.some((line) => line.name === alternativeLine)
|
// lines.some((line) => line.name === alternativeLine)
|
||||||
) {
|
// ) {
|
||||||
names.push(alternativeLine);
|
// names.push(alternativeLine);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return names;
|
// return names;
|
||||||
}, [name]);
|
// }, [name]);
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className={styles.line}>
|
// <div className={styles.line}>
|
||||||
{!isAlternativeLine(name) &&
|
// {!isAlternativeLine(name) &&
|
||||||
combinedLines.map((name) => (
|
// combinedLines.map((name) => (
|
||||||
<div className={styles["chunks-wrapper"]}>
|
// <div className={styles["chunks-wrapper"]}>
|
||||||
<LineInner key={name} name={name} lines={lines} />
|
// <LineInner key={name} name={name} lines={lines} />
|
||||||
</div>
|
// </div>
|
||||||
))}
|
// ))}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
function LineInner({ name, lines }: LineProps) {
|
// function LineInner({ name, lines }: LineProps) {
|
||||||
const line = lines.find((line) => line.name === name)!;
|
// const line = lines.find((line) => line.name === name)!;
|
||||||
|
|
||||||
return chunk(line.stages, maxModulesPerRow).map((chunk, wrapIndex) => (
|
// return chunk(line.stages, maxModulesPerRow).map((chunk, wrapIndex) => (
|
||||||
<div className={styles["line-wrap"]}>
|
// <div className={styles["line-wrap"]}>
|
||||||
{wrapIndex > 0 && (
|
// {wrapIndex > 0 && (
|
||||||
<Text size="minor" className={styles["wrap-marker"]} lowContrast>
|
// <Text size="minor" className={styles["wrap-marker"]} lowContrast>
|
||||||
<CornerBottomLeftIcon />
|
// <CornerBottomLeftIcon />
|
||||||
</Text>
|
// </Text>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{chunk.map((stage, elementIndex) => (
|
// {chunk.map((stage, elementIndex) => (
|
||||||
<LineElement
|
// <LineElement
|
||||||
key={elementIndex}
|
// key={elementIndex}
|
||||||
index={wrapIndex * maxModulesPerRow + elementIndex}
|
// index={wrapIndex * maxModulesPerRow + elementIndex}
|
||||||
lineName={name}
|
// lineName={name}
|
||||||
stage={stage}
|
// stage={stage}
|
||||||
/>
|
// />
|
||||||
))}
|
// ))}
|
||||||
</div>
|
// </div>
|
||||||
));
|
// ));
|
||||||
}
|
// }
|
||||||
|
|
||||||
interface LineElementProps {
|
// interface LineElementProps {
|
||||||
index: number;
|
// index: number;
|
||||||
lineName: string;
|
// lineName: string;
|
||||||
stage: StageParameters;
|
// stage: StageParameters;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function LineElement({ index, lineName, stage }: LineElementProps) {
|
// function LineElement({ index, lineName, stage }: LineElementProps) {
|
||||||
const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
|
// const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
|
||||||
const alternates = Tankopedia.use((state) => state.protagonist.alternates);
|
// const alternates = Tankopedia.use((state) => state.protagonist.alternates);
|
||||||
const tank = useProtagonistTank();
|
// const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const strings = useStrings();
|
// const strings = useStrings();
|
||||||
const gameStrings = useGameStrings("TankEntity");
|
// const gameStrings = useGameStrings("TankEntity");
|
||||||
const upgradePreset = useUpgradePreset(tank.tank!.tank_upgrade_preset);
|
// const upgradePreset = useUpgradePreset(tank.tank!.tank_upgrade_preset);
|
||||||
|
|
||||||
let price: StandardPrice | undefined;
|
// let price: StandardPrice | undefined;
|
||||||
|
|
||||||
switch (stage.grade) {
|
// switch (stage.grade) {
|
||||||
case Grade.GRADE_COMMON:
|
// case Grade.GRADE_COMMON:
|
||||||
price = upgradePreset.common_grade_price;
|
// price = upgradePreset.common_grade_price;
|
||||||
break;
|
// break;
|
||||||
|
|
||||||
case Grade.GRADE_RARE:
|
// case Grade.GRADE_RARE:
|
||||||
price = upgradePreset.rare_grade_price;
|
// price = upgradePreset.rare_grade_price;
|
||||||
break;
|
// break;
|
||||||
|
|
||||||
case Grade.GRADE_EPIC:
|
// case Grade.GRADE_EPIC:
|
||||||
price = upgradePreset.unique_grade_price;
|
// price = upgradePreset.unique_grade_price;
|
||||||
break;
|
// break;
|
||||||
|
|
||||||
case Grade.GRADE_LEGENDARY:
|
// case Grade.GRADE_LEGENDARY:
|
||||||
price = upgradePreset.legendary_grade_price;
|
// price = upgradePreset.legendary_grade_price;
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (const override of upgradePreset.prices_overrides) {
|
// for (const override of upgradePreset.prices_overrides) {
|
||||||
if (override.stage_tech_name !== stage.tech_name) continue;
|
// if (override.stage_tech_name !== stage.tech_name) continue;
|
||||||
price = override.price;
|
// price = override.price;
|
||||||
}
|
// }
|
||||||
|
|
||||||
price ??= StandardPrice.create();
|
// price ??= StandardPrice.create();
|
||||||
|
|
||||||
let isSelected: boolean;
|
// let isSelected: boolean;
|
||||||
|
|
||||||
if (isAlternativeLine(lineName)) {
|
// if (isAlternativeLine(lineName)) {
|
||||||
const originalLine = originalLineName(lineName);
|
// const originalLine = originalLineName(lineName);
|
||||||
isSelected = alternates[originalLine!];
|
// isSelected = alternates[originalLine!];
|
||||||
} else {
|
// } else {
|
||||||
if (alternates[lineName]) {
|
// if (alternates[lineName]) {
|
||||||
isSelected = false;
|
// isSelected = false;
|
||||||
} else {
|
// } else {
|
||||||
isSelected = upgrades[lineName] === index;
|
// isSelected = upgrades[lineName] === index;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className={styles.wrapper}>
|
// <div className={styles.wrapper}>
|
||||||
<Tooltip tooltip={gameStrings[stage.display_name]}>
|
// <Tooltip tooltip={gameStrings[stage.display_name]}>
|
||||||
<Button
|
// <Button
|
||||||
className={styles.stage}
|
// className={styles.stage}
|
||||||
color={isSelected ? undefined : "gray"}
|
// color={isSelected ? undefined : "gray"}
|
||||||
variant={isSelected ? "surface" : "soft"}
|
// variant={isSelected ? "surface" : "soft"}
|
||||||
radius="1"
|
// radius="1"
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
// TODO: make minimum selectable upgrade the last free module for tanks like the destiny
|
// // TODO: make minimum selectable upgrade the last free module for tanks like the destiny
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
// Tankopedia.mutate((draft) => {
|
||||||
const tankData = tank.tank!;
|
// const tankData = tank.tank!;
|
||||||
|
|
||||||
if (isAlternativeLine(lineName)) {
|
// if (isAlternativeLine(lineName)) {
|
||||||
const originalLine = originalLineName(lineName)!;
|
// const originalLine = originalLineName(lineName)!;
|
||||||
|
|
||||||
draft.protagonist.alternates[originalLine] = true;
|
// draft.protagonist.alternates[originalLine] = true;
|
||||||
draft.protagonist.upgrades[lineName] = index;
|
// draft.protagonist.upgrades[lineName] = index;
|
||||||
} else {
|
// } else {
|
||||||
draft.protagonist.alternates[lineName] = false;
|
// draft.protagonist.alternates[lineName] = false;
|
||||||
draft.protagonist.upgrades[lineName] = index;
|
// draft.protagonist.upgrades[lineName] = index;
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (const required of stage.required_upgrades) {
|
// for (const required of stage.required_upgrades) {
|
||||||
for (const line of tankData.upgrade_lines) {
|
// for (const line of tankData.upgrade_lines) {
|
||||||
let i = 0;
|
// let i = 0;
|
||||||
|
|
||||||
for (const candidateStage of line.stages) {
|
// for (const candidateStage of line.stages) {
|
||||||
if (candidateStage.tech_name === required) {
|
// if (candidateStage.tech_name === required) {
|
||||||
draft.protagonist.upgrades[line.name] = Math.max(
|
// draft.protagonist.upgrades[line.name] = Math.max(
|
||||||
draft.protagonist.upgrades[line.name],
|
// draft.protagonist.upgrades[line.name],
|
||||||
i,
|
// i,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
i++;
|
// i++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (const line of tankData.upgrade_lines) {
|
// for (const line of tankData.upgrade_lines) {
|
||||||
if (draft.protagonist.alternates[line.name]) {
|
// if (draft.protagonist.alternates[line.name]) {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
let i = draft.protagonist.upgrades[line.name];
|
// let i = draft.protagonist.upgrades[line.name];
|
||||||
|
|
||||||
while (i > 0) {
|
// while (i > 0) {
|
||||||
const currentStage = line.stages[i];
|
// const currentStage = line.stages[i];
|
||||||
|
|
||||||
const valid = currentStage.required_upgrades.every(
|
// const valid = currentStage.required_upgrades.every(
|
||||||
(required) => {
|
// (required) => {
|
||||||
for (const otherLine of tankData.upgrade_lines) {
|
// for (const otherLine of tankData.upgrade_lines) {
|
||||||
const idx = draft.protagonist.upgrades[otherLine.name];
|
// const idx = draft.protagonist.upgrades[otherLine.name];
|
||||||
|
|
||||||
if (otherLine.stages[idx]?.tech_name === required) {
|
// if (otherLine.stages[idx]?.tech_name === required) {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return false;
|
// return false;
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (valid) break;
|
// if (valid) break;
|
||||||
|
|
||||||
i--;
|
// i--;
|
||||||
}
|
// }
|
||||||
|
|
||||||
draft.protagonist.upgrades[line.name] = i;
|
// draft.protagonist.upgrades[line.name] = i;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
>
|
// >
|
||||||
<img
|
// <img
|
||||||
className={styles.icon}
|
// className={styles.icon}
|
||||||
src={`/media/modules/${stage.stage_type}.webp`}
|
// src={`/media/modules/${stage.stage_type}.webp`}
|
||||||
/>
|
// />
|
||||||
|
|
||||||
<Text weight="light" lowContrast size="minor" className={styles.tier}>
|
// <Text weight="light" lowContrast size="minor" className={styles.tier}>
|
||||||
{isAlternativeLine(lineName)
|
// {isAlternativeLine(lineName)
|
||||||
? strings.tanks.loadout.alternative
|
// ? strings.tanks.loadout.alternative
|
||||||
: romanize(stage.number)}
|
// : romanize(stage.number)}
|
||||||
</Text>
|
// </Text>
|
||||||
</Button>
|
// </Button>
|
||||||
</Tooltip>
|
// </Tooltip>
|
||||||
|
|
||||||
{<Price className={styles.price} price={price} />}
|
// {<Price className={styles.price} price={price} />}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
@@ -10,8 +10,6 @@ import { modelTransformEvent } from "../../core/blitzkit/modelTransform";
|
|||||||
import { Pose, poseEvent } from "../../core/blitzkit/pose";
|
import { Pose, poseEvent } from "../../core/blitzkit/pose";
|
||||||
import { useEquipment } from "../../hooks/useEquipment";
|
import { useEquipment } from "../../hooks/useEquipment";
|
||||||
import { useModel } from "../../hooks/useModel";
|
import { useModel } from "../../hooks/useModel";
|
||||||
import { useProtagonistGun } from "../../hooks/useProtagonistGun";
|
|
||||||
import { useProtagonistTank } from "../../hooks/useProtagonistTank";
|
|
||||||
import { useProtagonistTurret } from "../../hooks/useProtagonistTurret";
|
import { useProtagonistTurret } from "../../hooks/useProtagonistTurret";
|
||||||
import { Tankopedia } from "../../stores/tankopedia";
|
import { Tankopedia } from "../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||||
@@ -44,8 +42,8 @@ export const TankopediaSandbox = forwardRef<
|
|||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||||
|
|
||||||
const tank = useProtagonistTank();
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const gun = useProtagonistGun();
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
||||||
const turret = useProtagonistTurret();
|
const turret = useProtagonistTurret();
|
||||||
|
|
||||||
const model = Tankopedia.use((state) => state.protagonist.model);
|
const model = Tankopedia.use((state) => state.protagonist.model);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const equipmentColumns = 3;
|
||||||
@@ -7,7 +7,7 @@ const tanks = await api.tanks();
|
|||||||
|
|
||||||
export type DuelSide = "protagonist" | "antagonist";
|
export type DuelSide = "protagonist" | "antagonist";
|
||||||
|
|
||||||
export function useEquipment(id: number, side: DuelSide) {
|
export function useEquipment(side: DuelSide, id: number) {
|
||||||
const member = Tankopedia.use((state) => state[side]);
|
const member = Tankopedia.use((state) => state[side]);
|
||||||
const tank = tanks.tanks[member.tank];
|
const tank = tanks.tanks[member.tank];
|
||||||
const preset = equipment.presets[tank.equipment_preset];
|
const preset = equipment.presets[tank.equipment_preset];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { api } from "../api/dynamic";
|
import { api } from "../api/dynamic";
|
||||||
|
|
||||||
const equipmentDefinitions = await api.equipment();
|
const equipment = await api.equipment();
|
||||||
|
|
||||||
export function useEquipmentPreset(preset: string) {
|
export function useEquipmentPreset(preset: string) {
|
||||||
return equipmentDefinitions.presets[preset];
|
return equipment.presets[preset];
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,6 @@ const models = await api.models();
|
|||||||
const provisions = await api.provisions();
|
const provisions = await api.provisions();
|
||||||
const scripts = await api.scripts();
|
const scripts = await api.scripts();
|
||||||
|
|
||||||
export const genericDefaultEquipmentMatrix: EquipmentMatrix = [
|
|
||||||
[-1, 1, -1],
|
|
||||||
[0, 0, 0],
|
|
||||||
[0, 0, 0],
|
|
||||||
];
|
|
||||||
|
|
||||||
export function createTankState(tank: TankDefinition) {
|
export function createTankState(tank: TankDefinition) {
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
@@ -35,7 +29,7 @@ export function createTankState(tank: TankDefinition) {
|
|||||||
assault_distance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
|
assault_distance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
|
|
||||||
equipment_matrix: genericDefaultEquipmentMatrix,
|
equipment: {},
|
||||||
status: createTankStatus(),
|
status: createTankStatus(),
|
||||||
|
|
||||||
model: models.models[turret.id],
|
model: models.models[turret.id],
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ModelDefinition } from "@blitzkit/core";
|
import type { ModelDefinition } from "@blitzkit/protos";
|
||||||
import type { EquipmentMatrix } from "./createTankState";
|
|
||||||
|
|
||||||
export interface TankState {
|
export interface TankState {
|
||||||
tank: number;
|
tank: number;
|
||||||
@@ -13,7 +12,7 @@ export interface TankState {
|
|||||||
assault_distance: number;
|
assault_distance: number;
|
||||||
speed: number;
|
speed: number;
|
||||||
|
|
||||||
equipment_matrix: EquipmentMatrix;
|
equipment: Record<number, number>;
|
||||||
status: Record<VehicleStatusKey, boolean>;
|
status: Record<VehicleStatusKey, boolean>;
|
||||||
|
|
||||||
model: ModelDefinition;
|
model: ModelDefinition;
|
||||||
|
|||||||
Reference in new issue
Block a user