mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
ComputedCharacteristics
This commit is contained in:
1 parent
7d02f6311f
commit
7592381b12
4 files changed
+841
-4
No files matched your search
@@ -1,5 +1,7 @@
|
||||
export function computeCharacteristics() {
|
||||
const characteristics = {};
|
||||
import type { ComputedCharacteristics } from "../types/characteristics";
|
||||
|
||||
return characteristics;
|
||||
export function computeCharacteristics() {
|
||||
const characteristics: Partial<ComputedCharacteristics> = {};
|
||||
|
||||
return characteristics as ComputedCharacteristics;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
|
||||
.upgrade-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
|
||||
.state {
|
||||
font-size: 1.25em;
|
||||
|
||||
&[data-selected="true"] {
|
||||
img {
|
||||
filter: sepia(1) saturate(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.consumables {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
|
||||
.consumable-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
.consumable {
|
||||
font-size: var(--space-5);
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.equipment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
|
||||
.slots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
|
||||
.slot {
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: var(--space-3);
|
||||
|
||||
font-size: var(--space-5);
|
||||
|
||||
&[data-selected="true"] {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: var(--space-3);
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
|
||||
gap: var(--space-3);
|
||||
|
||||
.chunks-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
|
||||
.line-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap: var(--space-3);
|
||||
|
||||
.wrap-marker {
|
||||
padding-bottom: var(--font-size-minor);
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
.stage {
|
||||
position: relative;
|
||||
|
||||
padding: var(--space-3);
|
||||
|
||||
font-size: 2em;
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
transform: translate(var(--space-1), var(--space-1));
|
||||
}
|
||||
|
||||
.tier {
|
||||
position: absolute;
|
||||
left: var(--space-2);
|
||||
top: var(--space-2);
|
||||
}
|
||||
|
||||
.price {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: var(--space-2);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
import type { ComputedCharacteristics } from "../../types/characteristics";
|
||||
|
||||
interface TankopediaLoadoutProps {
|
||||
characteristics: ComputedCharacteristics;
|
||||
}
|
||||
|
||||
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
||||
const tank = useProtagonist();
|
||||
|
||||
const showModules = tank.tank!.upgrade_lines.some(
|
||||
(line) => line.stages.length > 1 || isAlternativeLine(line.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModules && <Modules />}
|
||||
<Equipment />
|
||||
<Consumables characteristics={characteristics} />
|
||||
<State />
|
||||
<Status />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
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"
|
||||
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}
|
||||
radius="1"
|
||||
className={styles.state}
|
||||
data-selected={status[key]}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConsumablesProps {
|
||||
characteristics: ComputedCharacteristics;
|
||||
}
|
||||
|
||||
function Consumables({ characteristics }: ConsumablesProps) {
|
||||
const strings = useStrings();
|
||||
const consumables = useConsumables();
|
||||
const tank = useProtagonist();
|
||||
const isCompatible = useCompatibility(tank, characteristics);
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.header}>
|
||||
<Heading size="4">{strings.tanks.loadout.consumables}</Heading>
|
||||
|
||||
<IconButton
|
||||
color="red"
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.protagonist.consumables = [];
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.consumables}>
|
||||
{Object.entries(consumables.consumables).map(
|
||||
([id, { compatibility, consumable, purchase }]) => {
|
||||
if (!isCompatible(compatibility!)) return null;
|
||||
|
||||
return (
|
||||
<Consumable
|
||||
key={id}
|
||||
id={id}
|
||||
consumable={consumable!}
|
||||
purchase={purchase!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConsumableProps {
|
||||
id: string;
|
||||
consumable: ConsumableComponent;
|
||||
purchase: BlitzStaticPurchaseComponent;
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<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 = useProtagonist();
|
||||
const equipment = useEquipment(tank.tank!);
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.header}>
|
||||
<Heading size="4">{strings.tanks.loadout.equipment}</Heading>
|
||||
|
||||
<IconButton
|
||||
color="red"
|
||||
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.preset.slots, equipmentColumns).map((chunk, index) => (
|
||||
<EquipmentRow key={index} chunk={chunk} rowIndex={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EquipmentRowProps {
|
||||
chunk: BlitzStaticEquipmentPresetComponent_EquipmentSlot[];
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
function EquipmentRow({ chunk, rowIndex }: EquipmentRowProps) {
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
{chunk.map((slot, index) => (
|
||||
<EquipmentSlot
|
||||
key={index}
|
||||
slot={slot}
|
||||
rowIndex={rowIndex}
|
||||
columnIndex={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EquipmentSlotProps {
|
||||
slot: BlitzStaticEquipmentPresetComponent_EquipmentSlot;
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
}
|
||||
|
||||
function EquipmentSlot({ slot, rowIndex, columnIndex }: EquipmentSlotProps) {
|
||||
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
|
||||
const tank = useProtagonist();
|
||||
const equipment = useEquipment(tank.tank!);
|
||||
const { price } = equipment.price.unlock_slot_prices[equipmentIndex];
|
||||
|
||||
return (
|
||||
<div className={styles.slots}>
|
||||
<div className={styles.options}>
|
||||
{slot.options_catalog_i_ds.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 = useProtagonist();
|
||||
const equipment = useEquipment(tank.tank!);
|
||||
const slot = equipment.preset.slots[equipmentIndex];
|
||||
const id = slot.options_catalog_i_ds[optionIndex];
|
||||
const gameStrings = useGameStrings("EquipmentEntity");
|
||||
|
||||
const isSelected = Tankopedia.use(
|
||||
(state) =>
|
||||
equipmentIndex in state.protagonist.equipment &&
|
||||
state.protagonist.equipment[equipmentIndex] === optionIndex,
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip tooltip={gameStrings[equipment.equipments[id].equipment_name]}>
|
||||
<Button
|
||||
color={isSelected ? undefined : "gray"}
|
||||
variant={isSelected ? "surface" : "soft"}
|
||||
data-selected={isSelected}
|
||||
radius="1"
|
||||
parentArray
|
||||
className={styles.slot}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
if (isSelected) {
|
||||
delete draft.protagonist.equipment[equipmentIndex];
|
||||
} else {
|
||||
draft.protagonist.equipment[equipmentIndex] = optionIndex;
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img src={`/media/equipment/${id}.webp`} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function Modules() {
|
||||
const strings = useStrings();
|
||||
const tank = useProtagonist();
|
||||
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 = useProtagonist();
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ interface CharacteristicContext {
|
||||
assault_distance: number;
|
||||
|
||||
characteristic(
|
||||
name: keyof typeof characteristics,
|
||||
name: CharacteristicName,
|
||||
): ReturnType<Characteristic["compute"]>;
|
||||
}
|
||||
|
||||
@@ -31,3 +31,11 @@ export enum CharacteristicType {
|
||||
Enum,
|
||||
Number,
|
||||
}
|
||||
|
||||
type CharacteristicName = keyof typeof characteristics;
|
||||
type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
|
||||
|
||||
export type ComputedCharacteristics = Record<
|
||||
CharacteristicName,
|
||||
CharacteristicReturnType
|
||||
>;
|
||||
Reference in new issue
Block a user