mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
919 lines
27 KiB
TypeScript
919 lines
27 KiB
TypeScript
import {
|
|
alias,
|
|
MAX_SKILL_LEVEL,
|
|
permanentSkills,
|
|
romanize,
|
|
} from "@blitzkit/core";
|
|
import type { Consumable, EquipmentSlot, Provision } from "@blitzkit/protos";
|
|
import {
|
|
ClockIcon,
|
|
DoubleArrowUpIcon,
|
|
ResetIcon,
|
|
TrashIcon,
|
|
} from "@radix-ui/react-icons";
|
|
import { chunk } from "lodash-es";
|
|
import { api } from "../../api/dynamic";
|
|
import { equipmentColumns } from "../../config/equipment";
|
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
|
import { useEquipmentPreset } from "../../hooks/useEquipmentPreset";
|
|
import { useProvision } from "../../hooks/useProvision";
|
|
import { useStrings } from "../../hooks/useStrings";
|
|
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
|
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
|
import { Tankopedia } from "../../stores/tankopedia";
|
|
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
|
import type { ComputedCharacteristics } from "../../types/characteristics";
|
|
import { Button } from "../Button";
|
|
import { Heading } from "../Heading";
|
|
import { IconButton } from "../IconButton";
|
|
import { Text } from "../Text";
|
|
import { Tooltip } from "../Tooltip";
|
|
import styles from "./index.module.css";
|
|
|
|
const equipment = await api.equipment();
|
|
const provisions = await api.provisions();
|
|
const consumables = await api.consumables();
|
|
const skills = await api.skills();
|
|
|
|
interface TankopediaLoadoutProps {
|
|
characteristics: ComputedCharacteristics;
|
|
}
|
|
|
|
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
|
// const tank = useDuel("protagonist", DuelModule.Tank);
|
|
// const showModules = hasUpgrades(tank);
|
|
|
|
return (
|
|
<>
|
|
{/* {showModules && <Modules />} */}
|
|
<Shells />
|
|
<Equipment />
|
|
<Provisions />
|
|
<Consumables characteristics={characteristics} />
|
|
<CrewSkills />
|
|
<State />
|
|
<Status />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function CrewSkills() {
|
|
const strings = useStrings();
|
|
const selectedSkills = Tankopedia.use((state) => state.environment.skills);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.skills}</Heading>
|
|
|
|
<div className={styles["upgrade-buttons"]}>
|
|
<IconButton
|
|
color="red"
|
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.skills}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
Object.keys(draft.environment.skills).forEach((skill) => {
|
|
draft.environment.skills[skill] = 0;
|
|
});
|
|
});
|
|
}}
|
|
>
|
|
<TrashIcon />
|
|
</IconButton>
|
|
|
|
<IconButton
|
|
color="gray"
|
|
highContrast
|
|
aria-label={`${strings.tanks.loadout.maximize} ${strings.tanks.loadout.skills}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
Object.keys(draft.environment.skills).forEach((skill) => {
|
|
draft.environment.skills[skill] = MAX_SKILL_LEVEL;
|
|
});
|
|
});
|
|
}}
|
|
>
|
|
<DoubleArrowUpIcon />
|
|
</IconButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles["crew-skills"]}>
|
|
{Object.entries(skills.classes).map(([tankClass, group]) => (
|
|
<div key={tankClass} className={styles.row}>
|
|
{group.skills.map((skill) => (
|
|
<CrewSkillEntry
|
|
key={skill}
|
|
id={skill}
|
|
level={selectedSkills[skill]}
|
|
permanent={permanentSkills.includes(skill)}
|
|
/>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CrewSkillEntryProps {
|
|
id: string;
|
|
level: number;
|
|
permanent: boolean;
|
|
}
|
|
|
|
function CrewSkillEntry({ id, level, permanent }: CrewSkillEntryProps) {
|
|
return (
|
|
<IconButton
|
|
size="major"
|
|
radius="1"
|
|
className={styles.skill}
|
|
color={level === 0 ? "gray" : permanent ? undefined : "amber"}
|
|
variant={level === 0 ? "soft" : "surface"}
|
|
onClick={(event) => {
|
|
Tankopedia.mutate((draft) => {
|
|
if (event.shiftKey) {
|
|
draft.environment.skills[id] = level === 0 ? MAX_SKILL_LEVEL : 0;
|
|
} else {
|
|
draft.environment.skills[id] =
|
|
level === 0 ? MAX_SKILL_LEVEL : level - 1;
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<img src={alias("api", `/icons/skills/${id}.webp`)} />
|
|
{level > 0 && <span className={styles.level}>{romanize(level)}</span>}
|
|
</IconButton>
|
|
);
|
|
}
|
|
|
|
function State() {
|
|
const strings = useStrings();
|
|
|
|
const shell = Tankopedia.use((state) => state.protagonist.shell);
|
|
const speed = Tankopedia.use((state) => state.protagonist.speed);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.dynamics}</Heading>
|
|
|
|
<IconButton
|
|
color="red"
|
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.consumables}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
draft.protagonist.consumables = [];
|
|
});
|
|
}}
|
|
>
|
|
<TrashIcon />
|
|
</IconButton>
|
|
</div>
|
|
|
|
<div className={styles.state}>
|
|
<span>shell: {shell}</span>
|
|
<span>speed: {speed}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Status() {
|
|
const strings = useStrings();
|
|
const status = Tankopedia.use((state) => state.protagonist.status);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.status}</Heading>
|
|
|
|
<IconButton
|
|
color="red"
|
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.status}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
draft.protagonist.consumables = [];
|
|
});
|
|
}}
|
|
>
|
|
<TrashIcon />
|
|
</IconButton>
|
|
</div>
|
|
|
|
<div className={styles.status}>
|
|
{vehicleStatusKeys.map((group, index) => (
|
|
<div key={index} className={styles.group}>
|
|
{group.map((key) => (
|
|
<IconButton
|
|
key={key}
|
|
aria-label={key.replaceAll("_", " ")}
|
|
aria-pressed={status[key]}
|
|
radius="1"
|
|
className={styles.state}
|
|
color={status[key] ? "amber" : "gray"}
|
|
variant={status[key] ? "surface" : "soft"}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
draft.protagonist.status[key] =
|
|
!draft.protagonist.status[key];
|
|
});
|
|
}}
|
|
>
|
|
<img src={`/media/status/${key}.png`} />
|
|
</IconButton>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Shells() {
|
|
const strings = useStrings();
|
|
const unwrap = useUnwrapper();
|
|
|
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
|
const shell = useDuel("protagonist", DuelModule.Shell);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.shells}</Heading>
|
|
</div>
|
|
|
|
<div className={styles.shells}>
|
|
{gun.shells.map((candidate) => {
|
|
const isSelected = candidate.id === shell.id;
|
|
const name = unwrap(candidate.name!);
|
|
|
|
return (
|
|
<Tooltip key={candidate.id} tooltip={name}>
|
|
<IconButton
|
|
aria-label={name}
|
|
size="major"
|
|
radius="1"
|
|
color={isSelected ? undefined : "gray"}
|
|
variant={isSelected ? "surface" : "soft"}
|
|
parentListChild
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
draft.protagonist.shell = candidate.id;
|
|
});
|
|
}}
|
|
>
|
|
<img
|
|
alt={name}
|
|
src={alias("api", `/icons/shells/${candidate.icon}.webp`)}
|
|
/>
|
|
</IconButton>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Provisions() {
|
|
const strings = useStrings();
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
|
const isCompatible = useTankCompatibility(tank, gun);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.provisions}</Heading>
|
|
|
|
<IconButton
|
|
color="red"
|
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.provisions}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
draft.protagonist.provisions = [];
|
|
});
|
|
}}
|
|
>
|
|
<TrashIcon />
|
|
</IconButton>
|
|
</div>
|
|
|
|
<div className={styles.provisions}>
|
|
{Object.entries(provisions.provisions).map(([id, provision]) => {
|
|
if (
|
|
provision.game_mode_exclusive ||
|
|
!isCompatible(provision.include, provision.exclude)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<ProvisionEntry key={id} id={Number(id)} provision={provision} />
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ProvisionEntryProps {
|
|
id: number;
|
|
provision: Provision;
|
|
}
|
|
|
|
function ProvisionEntry({ id, provision }: ProvisionEntryProps) {
|
|
const isSelected = useProvision("protagonist", id);
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const selectedCount = Tankopedia.use(
|
|
(state) => state.protagonist.provisions.length,
|
|
);
|
|
|
|
const unwrap = useUnwrapper();
|
|
|
|
const disabled = !isSelected && selectedCount >= tank.max_provisions;
|
|
|
|
return (
|
|
<Tooltip tooltip={unwrap(provision.name)}>
|
|
<IconButton
|
|
aria-label={unwrap(provision.name)}
|
|
size="major"
|
|
radius="1"
|
|
variant={isSelected ? "surface" : "soft"}
|
|
color={isSelected ? undefined : "gray"}
|
|
className={styles.consumable}
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
const index = draft.protagonist.provisions.indexOf(id);
|
|
|
|
if (index === -1) {
|
|
if (draft.protagonist.provisions.length === tank.max_provisions) {
|
|
return;
|
|
}
|
|
|
|
draft.protagonist.provisions.push(id);
|
|
} else {
|
|
draft.protagonist.provisions.splice(index, 1);
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<img src={alias("api", `/icons/provisions/${provision.id}.webp`)} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
interface ConsumablesProps {
|
|
characteristics: ComputedCharacteristics;
|
|
}
|
|
|
|
function Consumables({ characteristics }: ConsumablesProps) {
|
|
const strings = useStrings();
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
|
const isCompatible = useTankCompatibility(tank, gun);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.consumables}</Heading>
|
|
|
|
<IconButton
|
|
color="red"
|
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.consumables}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
draft.protagonist.consumables = [];
|
|
});
|
|
}}
|
|
>
|
|
<TrashIcon />
|
|
</IconButton>
|
|
</div>
|
|
|
|
<div className={styles.consumables}>
|
|
{Object.entries(consumables.consumables).map(([id, consumable]) => {
|
|
if (
|
|
consumable.game_mode_exclusive ||
|
|
!isCompatible(consumable.include, consumable.exclude)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<ConsumableEntry key={id} id={Number(id)} consumable={consumable} />
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ConsumableProps {
|
|
id: number;
|
|
consumable: Consumable;
|
|
}
|
|
|
|
function ConsumableEntry({ id, consumable }: ConsumableProps) {
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const isSelected = Tankopedia.use((state) =>
|
|
state.protagonist.consumables.includes(id),
|
|
);
|
|
const selectedCount = Tankopedia.use(
|
|
(state) => state.protagonist.consumables.length,
|
|
);
|
|
|
|
const unwrap = useUnwrapper();
|
|
|
|
const disabled = !isSelected && selectedCount >= tank.max_consumables;
|
|
|
|
return (
|
|
<div className={styles["consumable-wrapper"]}>
|
|
<Tooltip tooltip={unwrap(consumable.name)}>
|
|
<Button
|
|
aria-label={unwrap(consumable.name)}
|
|
radius="1"
|
|
variant={isSelected ? "surface" : "soft"}
|
|
color={isSelected ? undefined : "gray"}
|
|
className={styles.consumable}
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
const index = draft.protagonist.consumables.indexOf(id);
|
|
|
|
if (index === -1) {
|
|
if (
|
|
draft.protagonist.consumables.length === tank.max_consumables
|
|
) {
|
|
return;
|
|
}
|
|
|
|
draft.protagonist.consumables.push(id);
|
|
} else {
|
|
draft.protagonist.consumables.splice(index, 1);
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<img src={alias("api", `/icons/consumables/${consumable.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>
|
|
|
|
{/* <Price price={price} /> */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Equipment() {
|
|
const strings = useStrings();
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const equipment = useEquipmentPreset(tank.equipment_preset);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.header}>
|
|
<Heading size="4">{strings.tanks.loadout.equipment}</Heading>
|
|
|
|
<IconButton
|
|
color="red"
|
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.equipment}`}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
for (const key in draft.protagonist.equipment) {
|
|
delete draft.protagonist.equipment[key];
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<TrashIcon />
|
|
</IconButton>
|
|
</div>
|
|
|
|
<div className={styles.equipment}>
|
|
{chunk(equipment.slots, equipmentColumns).map((chunk, index) => (
|
|
<EquipmentRow key={index} chunk={chunk} rowIndex={index} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface EquipmentRowProps {
|
|
chunk: EquipmentSlot[];
|
|
rowIndex: number;
|
|
}
|
|
|
|
function EquipmentRow({ chunk, rowIndex }: EquipmentRowProps) {
|
|
return (
|
|
<div className={styles.row}>
|
|
{chunk.map((slot, index) => (
|
|
<EquipmentSlotEntry
|
|
key={index}
|
|
slot={slot}
|
|
rowIndex={rowIndex}
|
|
columnIndex={index}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface EquipmentSlotProps {
|
|
slot: EquipmentSlot;
|
|
rowIndex: number;
|
|
columnIndex: number;
|
|
}
|
|
|
|
function EquipmentSlotEntry({
|
|
slot,
|
|
rowIndex,
|
|
columnIndex,
|
|
}: EquipmentSlotProps) {
|
|
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const equipment = useEquipmentPreset(tank.equipment_preset);
|
|
// const { price } = equipment.price.unlock_slot_prices[equipmentIndex];
|
|
|
|
return (
|
|
<div className={styles.slots}>
|
|
<div className={styles.options}>
|
|
{slot.options.map((id, index) => (
|
|
<EquipmentOption
|
|
key={id}
|
|
optionIndex={index}
|
|
rowIndex={rowIndex}
|
|
columnIndex={columnIndex}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* <Price price={price!} /> */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface EquipmentOptionProps {
|
|
optionIndex: number;
|
|
rowIndex: number;
|
|
columnIndex: number;
|
|
}
|
|
|
|
function EquipmentOption({
|
|
optionIndex,
|
|
rowIndex,
|
|
columnIndex,
|
|
}: EquipmentOptionProps) {
|
|
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
|
|
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
|
const equipmentPreset = useEquipmentPreset(tank.equipment_preset);
|
|
const unwrap = useUnwrapper();
|
|
|
|
const slot = equipmentPreset.slots[equipmentIndex];
|
|
const id = slot.options[optionIndex];
|
|
const specificEquipment = equipment.equipments[id];
|
|
|
|
const isSelected = Tankopedia.use(
|
|
(state) =>
|
|
equipmentIndex in state.protagonist.equipment &&
|
|
state.protagonist.equipment[equipmentIndex] === optionIndex,
|
|
);
|
|
|
|
return (
|
|
<Tooltip tooltip={unwrap(specificEquipment.name)}>
|
|
<IconButton
|
|
aria-label={unwrap(specificEquipment.name)}
|
|
size="major"
|
|
color={isSelected ? undefined : "gray"}
|
|
variant={isSelected ? "surface" : "soft"}
|
|
radius="1"
|
|
parentListChild
|
|
className={styles.slot}
|
|
onClick={() => {
|
|
Tankopedia.mutate((draft) => {
|
|
if (isSelected) {
|
|
delete draft.protagonist.equipment[equipmentIndex];
|
|
} else {
|
|
draft.protagonist.equipment[equipmentIndex] = optionIndex;
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<img
|
|
className={styles.icon}
|
|
alt={unwrap(specificEquipment.name)}
|
|
src={alias("api", `/icons/equipment/${id}.webp`)}
|
|
/>
|
|
</IconButton>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
// function Modules() {
|
|
// const strings = useStrings();
|
|
// const tank = useDuel("protagonist", DuelModule.Tank);
|
|
// const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
|
|
// const alternates = Tankopedia.use((state) => state.protagonist.alternates);
|
|
// const isStock = useMemo(() => {
|
|
// return tank.tank!.upgrade_lines.every((line) => {
|
|
// if (isAlternativeLine(line.name)) return true;
|
|
// return upgrades[line.name] === 0;
|
|
// });
|
|
// }, [upgrades]);
|
|
// const isUpgraded = useMemo(() => {
|
|
// return tank.tank!.upgrade_lines.every((line) => {
|
|
// if (isAlternativeLine(line.name)) return true;
|
|
|
|
// return (
|
|
// upgrades[line.name] === line.stages.length - 1 ||
|
|
// (line.name in alternates && alternates[line.name])
|
|
// );
|
|
// });
|
|
// }, [upgrades]);
|
|
|
|
// return (
|
|
// <div className={styles.section}>
|
|
// <div className={styles.header}>
|
|
// <Heading size="4">{strings.tanks.loadout.modules}</Heading>
|
|
|
|
// <div className={styles["upgrade-buttons"]}>
|
|
// <IconButton
|
|
// variant={isStock ? "solid" : "soft"}
|
|
// size="minor"
|
|
// array
|
|
// onClick={() => {
|
|
// Tankopedia.mutate((draft) => {
|
|
// for (const line of tank.tank!.upgrade_lines) {
|
|
// if (isAlternativeLine(line.name)) continue;
|
|
|
|
// draft.protagonist.upgrades[line.name] = 0;
|
|
// draft.protagonist.alternates[line.name] = false;
|
|
// }
|
|
// });
|
|
// }}
|
|
// color="gray"
|
|
// >
|
|
// <DoubleArrowLeftIcon />
|
|
// </IconButton>
|
|
|
|
// <IconButton
|
|
// variant={isUpgraded ? "solid" : "soft"}
|
|
// size="minor"
|
|
// array
|
|
// onClick={() => {
|
|
// Tankopedia.mutate((draft) => {
|
|
// for (const line of tank.tank!.upgrade_lines) {
|
|
// draft.protagonist.upgrades[line.name] =
|
|
// line.stages.length - 1;
|
|
|
|
// if (isAlternativeLine(line.name)) {
|
|
// const original = originalLineName(line.name)!;
|
|
// draft.protagonist.alternates[original] = true;
|
|
// }
|
|
// }
|
|
// });
|
|
// }}
|
|
// color="gray"
|
|
// >
|
|
// <DoubleArrowRightIcon />
|
|
// </IconButton>
|
|
// </div>
|
|
// </div>
|
|
|
|
// <div className={styles.lines}>
|
|
// {/* TODO: hide lines with 1 module except for alts */}
|
|
// {tank.tank!.upgrade_lines.map((line) => (
|
|
// <Line
|
|
// key={line.name}
|
|
// name={line.name}
|
|
// lines={tank.tank!.upgrade_lines}
|
|
// />
|
|
// ))}
|
|
// </div>
|
|
// </div>
|
|
// );
|
|
// }
|
|
|
|
// interface LineProps {
|
|
// name: string;
|
|
// lines: UpgradeLine[];
|
|
// }
|
|
|
|
// function Line({ name, lines }: LineProps) {
|
|
// const combinedLines = useMemo(() => {
|
|
// const names: string[] = [name];
|
|
|
|
// for (const originalLine in alternativeLines) {
|
|
// const alternativeLine = alternativeLines[originalLine];
|
|
|
|
// if (
|
|
// originalLine === name &&
|
|
// lines.some((line) => line.name === alternativeLine)
|
|
// ) {
|
|
// names.push(alternativeLine);
|
|
// }
|
|
// }
|
|
|
|
// return names;
|
|
// }, [name]);
|
|
|
|
// return (
|
|
// <div className={styles.line}>
|
|
// {!isAlternativeLine(name) &&
|
|
// combinedLines.map((name) => (
|
|
// <div className={styles["chunks-wrapper"]}>
|
|
// <LineInner key={name} name={name} lines={lines} />
|
|
// </div>
|
|
// ))}
|
|
// </div>
|
|
// );
|
|
// }
|
|
|
|
// function LineInner({ name, lines }: LineProps) {
|
|
// const line = lines.find((line) => line.name === name)!;
|
|
|
|
// return chunk(line.stages, maxModulesPerRow).map((chunk, wrapIndex) => (
|
|
// <div className={styles["line-wrap"]}>
|
|
// {wrapIndex > 0 && (
|
|
// <Text size="minor" className={styles["wrap-marker"]} lowContrast>
|
|
// <CornerBottomLeftIcon />
|
|
// </Text>
|
|
// )}
|
|
|
|
// {chunk.map((stage, elementIndex) => (
|
|
// <LineElement
|
|
// key={elementIndex}
|
|
// index={wrapIndex * maxModulesPerRow + elementIndex}
|
|
// lineName={name}
|
|
// stage={stage}
|
|
// />
|
|
// ))}
|
|
// </div>
|
|
// ));
|
|
// }
|
|
|
|
// interface LineElementProps {
|
|
// index: number;
|
|
// lineName: string;
|
|
// stage: StageParameters;
|
|
// }
|
|
|
|
// function LineElement({ index, lineName, stage }: LineElementProps) {
|
|
// const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
|
|
// const alternates = Tankopedia.use((state) => state.protagonist.alternates);
|
|
// const tank = useDuel("protagonist", DuelModule.Tank);
|
|
// const strings = useStrings();
|
|
// const gameStrings = useGameStrings("TankEntity");
|
|
// 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;
|
|
// }
|
|
|
|
// price ??= StandardPrice.create();
|
|
|
|
// let isSelected: boolean;
|
|
|
|
// if (isAlternativeLine(lineName)) {
|
|
// const originalLine = originalLineName(lineName);
|
|
// isSelected = alternates[originalLine!];
|
|
// } else {
|
|
// if (alternates[lineName]) {
|
|
// isSelected = false;
|
|
// } else {
|
|
// isSelected = upgrades[lineName] === index;
|
|
// }
|
|
// }
|
|
|
|
// return (
|
|
// <div className={styles.wrapper}>
|
|
// <Tooltip tooltip={gameStrings[stage.display_name]}>
|
|
// <Button
|
|
// className={styles.stage}
|
|
// color={isSelected ? undefined : "gray"}
|
|
// variant={isSelected ? "surface" : "soft"}
|
|
// radius="1"
|
|
// onClick={() => {
|
|
// // TODO: make minimum selectable upgrade the last free module for tanks like the destiny
|
|
|
|
// Tankopedia.mutate((draft) => {
|
|
// const tankData = tank.tank!;
|
|
|
|
// if (isAlternativeLine(lineName)) {
|
|
// const originalLine = originalLineName(lineName)!;
|
|
|
|
// draft.protagonist.alternates[originalLine] = true;
|
|
// draft.protagonist.upgrades[lineName] = index;
|
|
// } else {
|
|
// draft.protagonist.alternates[lineName] = false;
|
|
// draft.protagonist.upgrades[lineName] = index;
|
|
// }
|
|
|
|
// for (const required of stage.required_upgrades) {
|
|
// for (const line of tankData.upgrade_lines) {
|
|
// let i = 0;
|
|
|
|
// for (const candidateStage of line.stages) {
|
|
// if (candidateStage.tech_name === required) {
|
|
// draft.protagonist.upgrades[line.name] = Math.max(
|
|
// draft.protagonist.upgrades[line.name],
|
|
// i,
|
|
// );
|
|
// }
|
|
|
|
// i++;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// for (const line of tankData.upgrade_lines) {
|
|
// if (draft.protagonist.alternates[line.name]) {
|
|
// continue;
|
|
// }
|
|
|
|
// let i = draft.protagonist.upgrades[line.name];
|
|
|
|
// while (i > 0) {
|
|
// const currentStage = line.stages[i];
|
|
|
|
// const valid = currentStage.required_upgrades.every(
|
|
// (required) => {
|
|
// for (const otherLine of tankData.upgrade_lines) {
|
|
// const idx = draft.protagonist.upgrades[otherLine.name];
|
|
|
|
// if (otherLine.stages[idx]?.tech_name === required) {
|
|
// return true;
|
|
// }
|
|
// }
|
|
|
|
// return false;
|
|
// },
|
|
// );
|
|
|
|
// if (valid) break;
|
|
|
|
// i--;
|
|
// }
|
|
|
|
// draft.protagonist.upgrades[line.name] = i;
|
|
// }
|
|
// });
|
|
// }}
|
|
// >
|
|
// <img
|
|
// className={styles.icon}
|
|
// src={`/media/modules/${stage.stage_type}.webp`}
|
|
// />
|
|
|
|
// <Text weight="light" lowContrast size="minor" className={styles.tier}>
|
|
// {isAlternativeLine(lineName)
|
|
// ? strings.tanks.loadout.alternative
|
|
// : romanize(stage.number)}
|
|
// </Text>
|
|
// </Button>
|
|
// </Tooltip>
|
|
|
|
// {<Price className={styles.price} price={price} />}
|
|
// </div>
|
|
// );
|
|
// }
|