Compare commits

...
3 Commits
Author SHA1 Message Date
tresabhi 7a7c73d77b adrenaline inclusivitiy 2026-05-26 11:55:51 -05:00
tresabhi f945786e2f unselect all equipment button 2026-05-26 11:41:08 -05:00
tresabhi c678830fd2 pretty and visually functional stock and upgraded buttons 2026-05-26 11:36:45 -05:00
10 changed files with 147 additions and 74 deletions

No files matched your search

+3 -1
View File
@@ -2443,7 +2443,7 @@
"potpack": ["[email protected]", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="], "potpack": ["[email protected]", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
"prettier": ["[email protected].1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "prettier": ["[email protected].3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"pretty-bytes": ["[email protected]", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], "pretty-bytes": ["[email protected]", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
@@ -3247,6 +3247,8 @@
"yaml-language-server/lodash": ["[email protected]", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "yaml-language-server/lodash": ["[email protected]", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"yaml-language-server/prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"yaml-language-server/request-light": ["[email protected]", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/request-light": ["[email protected]", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
"yaml-language-server/yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], "yaml-language-server/yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
@@ -10,12 +10,28 @@
border: none; border: none;
font-family: var(--font-family-regular); font-family: var(--font-family-regular);
font-size: var(--font-size-minor); font-size: 0.75rem;
color: var(--gray-1); color: var(--gray-1);
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
cursor: pointer; cursor: pointer;
&[data-size="minor"] {
font-size: 0.75rem;
}
&[data-array="true"] {
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
svg, svg,
img { img {
width: 1em; width: 1em;
@@ -33,6 +49,7 @@
&[data-variant="surface"], &[data-variant="surface"],
&[data-variant="soft"] { &[data-variant="soft"] {
background-color: var(--accent-3); background-color: var(--accent-3);
color: var(--gray-12);
} }
&[data-variant="inverted"] { &[data-variant="inverted"] {
@@ -7,7 +7,8 @@ export interface ButtonProps extends ComponentProps<"button"> {
color?: Color; color?: Color;
radius?: `${1 | 2 | 3}` | "max"; radius?: `${1 | 2 | 3}` | "max";
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline"; variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
size?: "regular"; size?: "regular" | "minor";
array?: boolean;
} }
export const LIGHT_TEXT_COLORS = new Set<Color>([ export const LIGHT_TEXT_COLORS = new Set<Color>([
@@ -32,6 +33,7 @@ export function Button({
style, style,
radius = "max", radius = "max",
size = "regular", size = "regular",
array = false,
...props ...props
}: ButtonProps) { }: ButtonProps) {
return ( return (
@@ -42,6 +44,7 @@ export function Button({
data-light-text={variant === "solid"} data-light-text={variant === "solid"}
data-radius={radius} data-radius={radius}
data-size={size} data-size={size}
data-array={array}
{...props} {...props}
/> />
); );
@@ -2,6 +2,4 @@
padding: var(--space-3); padding: var(--space-3);
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
font-size: var(--font-size-major);
} }
@@ -6,7 +6,11 @@
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-4);
.upgrade-buttons {
display: flex;
}
} }
.equipment { .equipment {
@@ -33,16 +37,6 @@
font-size: var(--space-5); font-size: var(--space-5);
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&[data-selected="true"] { &[data-selected="true"] {
z-index: 1; z-index: 1;
} }
@@ -8,6 +8,7 @@ import {
CornerBottomLeftIcon, CornerBottomLeftIcon,
DoubleArrowLeftIcon, DoubleArrowLeftIcon,
DoubleArrowRightIcon, DoubleArrowRightIcon,
TrashIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
import { useMemo } from "react"; import { useMemo } from "react";
@@ -28,13 +29,18 @@ import { useUpgradePreset } from "../../hooks/useUpgradePreset";
import { Tankopedia } from "../../stores/tankopedia"; import { Tankopedia } from "../../stores/tankopedia";
import { Button } from "../Button"; import { Button } from "../Button";
import { Heading } from "../Heading"; import { Heading } from "../Heading";
import { Link } from "../Link"; import { IconButton } from "../IconButton";
import { Price } from "../Price"; import { Price } from "../Price";
import { Section } from "../Section"; import { Section } from "../Section";
import { Text } from "../Text"; import { Text } from "../Text";
import styles from "./index.module.css"; import styles from "./index.module.css";
import type { ComputedCharacteristics } from "../../tankopedia/computeCharacteristics";
export function TankopediaLoadout() { interface TankopediaLoadoutProps {
characteristics: ComputedCharacteristics;
}
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
const tank = useProtagonist(); const tank = useProtagonist();
const showModules = tank.tank!.upgrade_lines.some( const showModules = tank.tank!.upgrade_lines.some(
@@ -45,22 +51,30 @@ export function TankopediaLoadout() {
<Section> <Section>
{showModules && <Modules />} {showModules && <Modules />}
<Equipment /> <Equipment />
<Consumables /> <Consumables characteristics={characteristics} />
</Section> </Section>
); );
} }
function Consumables() { interface ConsumablesProps {
characteristics: ComputedCharacteristics;
}
function Consumables({ characteristics }: ConsumablesProps) {
const consumables = useConsumables(); const consumables = useConsumables();
const tierPrices = useTierPrices(); const tierPrices = useTierPrices();
const tank = useProtagonist(); const tank = useProtagonist();
const isCompatible = useCompatibility(tank); const isCompatible = useCompatibility(tank, characteristics);
return Object.values(consumables.consumables).map( return Object.entries(consumables.consumables).map(
({ compatibility, consumable, purchase }) => { ([id, { compatibility, consumable, purchase }]) => {
if (!isCompatible(compatibility!)) return null; if (!isCompatible(compatibility!)) return null;
return <span>{JSON.stringify(consumable, null, 2)}</span>; return (
<span>
{id} {JSON.stringify(consumable, null, 2)}
</span>
);
}, },
); );
} }
@@ -74,6 +88,19 @@ function Equipment() {
<div className={styles.section}> <div className={styles.section}>
<div className={styles.header}> <div className={styles.header}>
<Heading size="4">{strings.tanks.loadout.equipment}</Heading> <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>
<div className={styles.equipment}> <div className={styles.equipment}>
@@ -164,6 +191,7 @@ function EquipmentOption({
variant={isSelected ? "surface" : "soft"} variant={isSelected ? "surface" : "soft"}
data-selected={isSelected} data-selected={isSelected}
radius="1" radius="1"
array
className={styles.slot} className={styles.slot}
onClick={() => { onClick={() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -183,45 +211,72 @@ function EquipmentOption({
function Modules() { function Modules() {
const strings = useStrings(); const strings = useStrings();
const tank = useProtagonist(); 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 ( 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>
<Link <div className={styles["upgrade-buttons"]}>
onClick={() => { <IconButton
Tankopedia.mutate((draft) => { variant={isStock ? "solid" : "soft"}
for (const line of tank.tank!.upgrade_lines) { size="minor"
if (isAlternativeLine(line.name)) continue; 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.upgrades[line.name] = 0;
draft.protagonist.alternates[line.name] = false; draft.protagonist.alternates[line.name] = false;
}
});
}}
color="gray"
>
<DoubleArrowLeftIcon />
</Link>
<Link
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"
color="gray" >
> <DoubleArrowLeftIcon />
<DoubleArrowRightIcon /> </IconButton>
</Link>
<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>
<div className={styles.lines}> <div className={styles.lines}>
@@ -5,8 +5,13 @@ import { TankAttributeChange_AttributeName } from "@protos/blitz_static_tank_upg
import type { Sets } from "@protos/sets"; import type { Sets } from "@protos/sets";
import type { Tank } from "@protos/tank"; import type { Tank } from "@protos/tank";
import { useSets } from "./useSets"; import { useSets } from "./useSets";
import type { ComputedCharacteristics } from "../tankopedia/computeCharacteristics";
import { GunType } from "../tankopedia/characteristics";
export function useCompatibility(tank: Tank) { export function useCompatibility(
tank: Tank,
characteristics: ComputedCharacteristics,
) {
const sets = useSets(); const sets = useSets();
return function (compatibility: CompatibilityComponent) { return function (compatibility: CompatibilityComponent) {
@@ -26,11 +31,13 @@ export function useCompatibility(tank: Tank) {
compatibility.tank_compatibility.include, compatibility.tank_compatibility.include,
tank, tank,
sets, sets,
characteristics,
) && ) &&
!tankCompatibilityApplies( !tankCompatibilityApplies(
compatibility.tank_compatibility.exclude, compatibility.tank_compatibility.exclude,
tank, tank,
sets, sets,
characteristics,
)) ))
); );
}; };
@@ -40,6 +47,7 @@ function tankCompatibilityApplies(
include: TankCompatibility_Include | undefined, include: TankCompatibility_Include | undefined,
tank: Tank, tank: Tank,
sets: Sets, sets: Sets,
characteristics: ComputedCharacteristics,
) { ) {
if (include === undefined) { if (include === undefined) {
return true; return true;
@@ -57,7 +65,9 @@ function tankCompatibilityApplies(
) || ) ||
include.classes.some((_class) => _class === tank.tank!.tank_class) || include.classes.some((_class) => _class === tank.tank!.tank_class) ||
include.catalogs_id.some((id) => id === tank.id) || include.catalogs_id.some((id) => id === tank.id) ||
include.tank_traits.some((trait) => tankHasTraits(trait, tank)) || include.tank_traits.some((trait) =>
tankHasTraits(trait, tank, characteristics),
) ||
include.types.some((type) => type === tank.tank!.tank_type) || include.types.some((type) => type === tank.tank!.tank_type) ||
include.tank_sets_catalog_ids.some((id) => include.tank_sets_catalog_ids.some((id) =>
Object.entries(sets.sets).some( Object.entries(sets.sets).some(
@@ -74,23 +84,14 @@ function tankCompatibilityApplies(
); );
} }
function tankHasTraits(traits: TankTraits, tank: Tank) { function tankHasTraits(
traits: TankTraits,
tank: Tank,
characteristics: ComputedCharacteristics,
) {
switch (traits) { switch (traits) {
case TankTraits.TANK_TRAITS_HAS_CLIP: case TankTraits.TANK_TRAITS_HAS_CLIP:
// TODO: use current tank state to check if the current gun has a clip return characteristics.gun_type !== GunType.Regular;
return tank.tank!.upgrade_lines.some((line) =>
line.stages.some((stage) =>
stage.attributes.every((attribute) =>
attribute.attribute_name !==
TankAttributeChange_AttributeName.ATTRIBUTE_NAME_IS_PUMP &&
attribute.attribute_name ===
TankAttributeChange_AttributeName.ATTRIBUTE_NAME_CLIP_SIZE
? attribute.value === 1
: true,
),
),
);
default: default:
throw new Error(`Unknown trait: ${traits}`); throw new Error(`Unknown trait: ${traits}`);
@@ -41,7 +41,7 @@ export const Page = withErrorWrapper(
<br /> <br />
<TankopediaLoadout /> <TankopediaLoadout characteristics={characteristics} />
<h2>characteristics</h2> <h2>characteristics</h2>
@@ -5,6 +5,7 @@ import { mixStaticPaths } from "../../../../astro/mixStaticPaths";
import BaseLayout from "../../../../layouts/BaseLayout.astro"; import BaseLayout from "../../../../layouts/BaseLayout.astro";
import { getStaticPaths as _getStaticPaths } from "../index.astro"; import { getStaticPaths as _getStaticPaths } from "../index.astro";
import { Page } from "./_index"; import { Page } from "./_index";
import ColorScale from "../../../../components/ColorScale.astro";
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => { export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
const { list } = await api.tankList(); const { list } = await api.tankList();
@@ -19,5 +20,7 @@ const { id, locale } = Astro.props;
--- ---
<BaseLayout {locale} accent="violet"> <BaseLayout {locale} accent="violet">
<ColorScale color="red" />
<Page {locale} {id} client:load /> <Page {locale} {id} client:load />
</BaseLayout> </BaseLayout>
@@ -31,7 +31,7 @@ export enum GunType {
} }
export const characteristics = { export const characteristics = {
gun_type({ attributeSafe, attribute }) { gun_type({ attributeSafe }) {
const isPump = const isPump =
attributeSafe(TankAttributeChange_AttributeName.ATTRIBUTE_NAME_IS_PUMP) ?? attributeSafe(TankAttributeChange_AttributeName.ATTRIBUTE_NAME_IS_PUMP) ??
0; 0;