TankopediaLoadout

This commit is contained in:
tresabhi committed 2026-09-27 13:49:02 -05:00
1 parent 558a02b6ae
commit 488451d71f
9 files changed
+309 -299

No files matched your search

+5 -1
View File
@@ -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);
+1
View File
@@ -0,0 +1 @@
export const equipmentColumns = 3;
+1 -1
View File
@@ -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],
+2 -3
View File
@@ -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;