add skills

This commit is contained in:
tresabhi committed 2026-10-13 20:37:53 -05:00
1 parent bc6257a0c3
commit 1416d64e34
20 files changed
+196 -55

No files matched your search

@@ -1,4 +1,9 @@
import { alias, availableProvisions, permanentSkills } from "@blitzkit/core";
import {
alias,
availableProvisions,
MAX_SKILL_LEVEL,
permanentSkills,
} from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
import {
@@ -153,7 +158,7 @@ export function CompareTable({ stats }: CompareTableProps) {
onClick={() => {
CompareEphemeral.mutate((draft) => {
Object.keys(draft.crewSkills).forEach((skill) => {
draft.crewSkills[skill] = 7;
draft.crewSkills[skill] = MAX_SKILL_LEVEL;
});
});
}}
@@ -1,4 +1,9 @@
import { alias, permanentSkills, romanize } from "@blitzkit/core";
import {
alias,
MAX_SKILL_LEVEL,
permanentSkills,
romanize,
} from "@blitzkit/core";
import { Flex } from "@radix-ui/themes";
import { api } from "../api/dynamic";
import { GenericTankComponentButton } from "./ModuleButtons/GenericTankComponentButton";
@@ -34,10 +39,10 @@ export function CrewSkillManager({
const draft = { ...skillLevels };
if (event.shiftKey) {
draft[skill] = draft[skill] === 0 ? 7 : 0;
draft[skill] = draft[skill] === 0 ? MAX_SKILL_LEVEL : 0;
} else {
draft[skill] = level - 1;
if (draft[skill] < 0) draft[skill] = 7;
if (draft[skill] < 0) draft[skill] = MAX_SKILL_LEVEL;
}
onChange(draft);
+3 -3
View File
@@ -1,4 +1,4 @@
import { alias, SEARCH_KEYS, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { alias, romanize, SEARCH_KEYS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { TankDefinition } from "@blitzkit/protos";
import {
@@ -163,7 +163,7 @@ export function Guesser() {
justify="center"
>
<Icon width="1em" height="1em" />
{TIER_ROMAN_NUMERALS[result.tier]}
{romanize(result.tier)}
</Flex>
}
/>
@@ -215,7 +215,7 @@ export function Guesser() {
}}
>
{literals(strings.website.tools.guess.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
tier: romanize(tier),
})}
</DropdownMenu.CheckboxItem>
);
@@ -1,4 +1,4 @@
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { romanize } from "@blitzkit/core";
import type {
EngineDefinition,
GunDefinition,
@@ -42,7 +42,7 @@ export function ModuleManager({
<Flex gap="1" wrap="wrap">
<ModuleButton
module="turret"
discriminator={TIER_ROMAN_NUMERALS[modules.turret.tier]}
discriminator={romanize(modules.turret.tier)}
onClick={() => setTurretMenuOpen(true)}
/>
@@ -71,7 +71,7 @@ export function ModuleManager({
<ModuleButton
variant="ghost"
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
discriminator={romanize(turret.tier)}
/>{" "}
{unwrap(turret.name!)}
</DropdownMenu.RadioItem>
@@ -83,7 +83,7 @@ export function ModuleManager({
<ModuleButton
module="gun"
discriminator={TIER_ROMAN_NUMERALS[modules.gun.tier]}
discriminator={romanize(modules.gun.tier)}
onClick={() => setGunMenuOpen(true)}
/>
@@ -111,7 +111,7 @@ export function ModuleManager({
<ModuleButton
variant="ghost"
module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
discriminator={romanize(gun.tier)}
/>{" "}
{unwrap(gun.name!)}
</DropdownMenu.RadioItem>
@@ -123,7 +123,7 @@ export function ModuleManager({
<ModuleButton
module="engine"
discriminator={TIER_ROMAN_NUMERALS[modules.engine.tier]}
discriminator={romanize(modules.engine.tier)}
onClick={() => setEngineMenuOpen(true)}
/>
@@ -150,7 +150,7 @@ export function ModuleManager({
<ModuleButton
variant="ghost"
module="engine"
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
discriminator={romanize(engine.tier)}
/>{" "}
{unwrap(engine.name!)}
</DropdownMenu.RadioItem>
@@ -162,7 +162,7 @@ export function ModuleManager({
<ModuleButton
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[modules.track.tier]}
discriminator={romanize(modules.track.tier)}
onClick={() => setTrackMenuOpen(true)}
/>
@@ -189,7 +189,7 @@ export function ModuleManager({
<ModuleButton
variant="ghost"
module="chassis"
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
discriminator={romanize(track.tier)}
/>{" "}
{unwrap(track.name!)}
</DropdownMenu.RadioItem>
@@ -1,4 +1,4 @@
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { romanize } from "@blitzkit/core";
import { Flex, IconButton, Text } from "@radix-ui/themes";
import { memo } from "react";
import { awaitableTiers } from "../../../core/awaitables/tiers";
@@ -41,7 +41,7 @@ export const TierFilter = memo(() => {
});
}}
>
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
<Text size="2">{romanize(tier)}</Text>
</IconButton>
);
})}
@@ -1,7 +1,7 @@
import {
alias,
gunTypeOrder,
TIER_ROMAN_NUMERALS,
romanize,
treeTypeOrder,
} from "@blitzkit/core";
import { tankClassOrder } from "@blitzkit/core/src/config/tankClassOrder";
@@ -203,7 +203,7 @@ function TiersFilter() {
<Flex gap="1">
{tiers.slice(0, MAX_ICONS).map((tier) => (
<Text size="minor" key={tier}>
{TIER_ROMAN_NUMERALS[tier]}
{romanize(tier)}
</Text>
))}
@@ -239,7 +239,7 @@ function TiersFilter() {
key={tier}
>
{literals(strings.website.common.tank_search.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
tier: romanize(tier),
})}
</DropdownMenu.CheckboxItem>
);
@@ -2,7 +2,7 @@ import {
alias,
formatCompact,
tankIcon,
TIER_ROMAN_NUMERALS,
romanize,
} from "@blitzkit/core";
import {
EngineDefinition,
@@ -93,7 +93,7 @@ function ModuleButton({
top: isTank ? 8 : 4,
}}
>
{TIER_ROMAN_NUMERALS[tier]}
{romanize(tier)}
</Text>
{unlock.cost!.value > 0 && (
@@ -1,3 +1,4 @@
import { MAX_SKILL_LEVEL } from "@blitzkit/core";
import { InfoCircledIcon } from "@radix-ui/react-icons";
import {
Button,
@@ -59,7 +60,7 @@ export function Skills() {
onClick={() => {
Tankopedia.mutate((draft) => {
Object.keys(draft.skills).forEach((skill) => {
draft.skills[skill] = 7;
draft.skills[skill] = MAX_SKILL_LEVEL;
});
});
}}
@@ -1,7 +1,7 @@
import {
alias,
resolvePenetrationCoefficient,
TIER_ROMAN_NUMERALS,
romanize,
uniqueGuns,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
@@ -137,7 +137,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
mr="2"
mb="1"
>
{TIER_ROMAN_NUMERALS[antagonistGun.tier]}
{romanize(antagonistGun.tier)}
</Text>
<img
@@ -166,7 +166,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
})
}
selected={gun.id === antagonistGun.id}
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
discriminator={romanize(gun.tier)}
secondaryDiscriminator={
<Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, {
@@ -1,4 +1,4 @@
import { alias, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { alias, romanize } from "@blitzkit/core";
import { TankPriceType, TankType } from "@blitzkit/protos";
import { Box, Flex } from "@radix-ui/themes";
import { Var } from "../../../core/radix/var";
@@ -80,7 +80,7 @@ export function MetaSection() {
</Flex>
</Listing>
<Listing label={strings.website.tools.tankopedia.meta.tier}>
{TIER_ROMAN_NUMERALS[protagonist.tier]}
{romanize(protagonist.tier)}
</Listing>
<Listing
label={strings.website.tools.tankopedia.meta.type}
@@ -1,4 +1,4 @@
import { alias, formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { alias, formatCompact, romanize } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
import { api } from "../../../../api/dynamic";
@@ -90,7 +90,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex direction="column" align="center">
<Flex gap="2" align="center">
<Text color="gray" size="1">
{TIER_ROMAN_NUMERALS[tank.tier]}
{romanize(tank.tier)}
</Text>
<Text wrap="nowrap">{unwrap(tank.name!)}</Text>
</Flex>
@@ -1,4 +1,4 @@
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { romanize } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import { useRef } from "react";
import { characteristicsGroups } from "../../config/characteristicsGroups";
@@ -201,7 +201,7 @@ function CompareOptions() {
value: `${TankopediaRelativeAgainst.ClassAndTier}`,
relativeAgainst: TankopediaRelativeAgainst.ClassAndTier,
label: literals(strings.tanks.characteristics.compare.class, {
tier: TIER_ROMAN_NUMERALS[tank.tier],
tier: romanize(tank.tier),
class: strings.common.tank_class_short_plural_lowercase[tank.class],
}),
},
@@ -209,7 +209,7 @@ function CompareOptions() {
value: `${TankopediaRelativeAgainst.Tier}`,
relativeAgainst: TankopediaRelativeAgainst.Tier,
label: literals(strings.tanks.characteristics.compare.tier, {
tier: TIER_ROMAN_NUMERALS[tank.tier],
tier: romanize(tank.tier),
}),
},
{
@@ -13,6 +13,31 @@
.upgrade-buttons {
display: flex;
gap: var(--space-2);
}
}
.crew-skills {
display: flex;
flex-direction: column;
gap: var(--space-3);
.row {
display: flex;
gap: inherit;
}
.skill {
position: relative;
.level {
position: absolute;
right: var(--space-1);
bottom: var(--space-1);
font-size: var(--font-size-minor);
}
}
}
@@ -1,6 +1,16 @@
import { alias } from "@blitzkit/core";
import {
alias,
MAX_SKILL_LEVEL,
permanentSkills,
romanize,
} from "@blitzkit/core";
import type { Consumable, EquipmentSlot, Provision } from "@blitzkit/protos";
import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons";
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";
@@ -23,6 +33,7 @@ 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;
@@ -39,12 +50,103 @@ export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
<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();
@@ -58,6 +160,7 @@ function State() {
<IconButton
color="red"
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.consumables}`}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
@@ -87,6 +190,7 @@ function Status() {
<IconButton
color="red"
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.status}`}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
@@ -103,6 +207,8 @@ function Status() {
{group.map((key) => (
<IconButton
key={key}
aria-label={key.replaceAll("_", " ")}
aria-pressed={status[key]}
radius="1"
className={styles.state}
color={status[key] ? "amber" : "gray"}
@@ -183,6 +289,7 @@ function Provisions() {
<IconButton
color="red"
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.provisions}`}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.provisions = [];
@@ -230,6 +337,7 @@ function ProvisionEntry({ id, provision }: ProvisionEntryProps) {
return (
<Tooltip tooltip={unwrap(provision.name)}>
<IconButton
aria-label={unwrap(provision.name)}
size="major"
radius="1"
variant={isSelected ? "surface" : "soft"}
@@ -275,6 +383,7 @@ function Consumables({ characteristics }: ConsumablesProps) {
<IconButton
color="red"
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.consumables}`}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.protagonist.consumables = [];
@@ -325,6 +434,7 @@ function ConsumableEntry({ id, consumable }: ConsumableProps) {
<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"}
@@ -384,6 +494,7 @@ function Equipment() {
<IconButton
color="red"
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.equipment}`}
onClick={() => {
Tankopedia.mutate((draft) => {
for (const key in draft.protagonist.equipment) {
@@ -489,6 +600,7 @@ function EquipmentOption({
return (
<Tooltip tooltip={unwrap(specificEquipment.name)}>
<IconButton
aria-label={unwrap(specificEquipment.name)}
size="major"
color={isSelected ? undefined : "gray"}
variant={isSelected ? "surface" : "soft"}
@@ -1,6 +1,6 @@
import {
alias,
TIER_ROMAN_NUMERALS,
romanize,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
@@ -161,7 +161,7 @@ function ModuleChooser({
}
key={turret.id}
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
discriminator={romanize(turret.tier)}
onClick={() => {
onSelect?.(tankDefinition, turret);
}}
@@ -179,7 +179,7 @@ function ModuleChooser({
}
key={gun.id}
module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
discriminator={romanize(gun.tier)}
secondaryDiscriminator={
<Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, {