mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Merge branch 'main' into preview
This commit is contained in:
commit
2a4375c718
300 files changed
+15602
-7394
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
Path,
|
||||
Quaternion,
|
||||
} from 'three';
|
||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
type ShotLayerNonExternal,
|
||||
@@ -45,6 +46,7 @@ export function ShotDisplay() {
|
||||
const splashRadiusWrapper = useRef<Group>(null);
|
||||
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
|
||||
let animationStartTime: number | null = null;
|
||||
const { locale } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
if (shot?.splashRadius !== undefined) {
|
||||
@@ -180,7 +182,9 @@ export function ShotDisplay() {
|
||||
mt={tracerGoingUp ? '9' : '0'}
|
||||
pt={tracerGoingUp ? '4' : '0'}
|
||||
>
|
||||
<ShotDisplayCard shot={shot} />
|
||||
<LocaleProvider locale={locale}>
|
||||
<ShotDisplayCard shot={shot} />
|
||||
</LocaleProvider>
|
||||
</Box>
|
||||
</Html>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
@@ -8,33 +9,17 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import type {
|
||||
Shot,
|
||||
ShotLayer,
|
||||
ShotStatus,
|
||||
} from '../../../stores/tankopediaEphemeral';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral';
|
||||
import { shotStatusColors } from './ShotDisplay';
|
||||
import { ArmorType } from './SpacedArmorScene';
|
||||
import type { ExternalModuleVariant } from './SpacedArmorSceneComponent';
|
||||
|
||||
export const layerTypeNames: Record<ArmorType | 'null', string> = {
|
||||
[ArmorType.Primary]: 'Primary',
|
||||
[ArmorType.Spaced]: 'Spaced',
|
||||
[ArmorType.External]: 'External',
|
||||
null: 'Gap',
|
||||
};
|
||||
|
||||
const externalLayerNames: Record<ExternalModuleVariant, string> = {
|
||||
gun: 'Gun',
|
||||
track: 'Tracks',
|
||||
};
|
||||
|
||||
const shotStatusNames: Record<ShotStatus, string> = {
|
||||
blocked: 'Blocked',
|
||||
ricochet: 'Ricochet',
|
||||
penetration: 'penetration',
|
||||
splash: 'splash',
|
||||
};
|
||||
export const layerTypeNames = {
|
||||
[ArmorType.Primary]: 'primary',
|
||||
[ArmorType.Spaced]: 'spaced',
|
||||
[ArmorType.External]: 'external',
|
||||
null: 'gap',
|
||||
} as const;
|
||||
|
||||
function LayerEntry({
|
||||
layerIndex,
|
||||
@@ -49,6 +34,8 @@ function LayerEntry({
|
||||
layer: ShotLayer;
|
||||
angle?: number;
|
||||
}) {
|
||||
const { locale, strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex align="center" gap="2">
|
||||
<Text size="1" color="gray" style={{ width: 12 }}>
|
||||
@@ -61,20 +48,26 @@ function LayerEntry({
|
||||
{layer.type === null && (
|
||||
<>
|
||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
||||
{Math.round(layer.distance * 1000).toLocaleString()}
|
||||
<Text size="1">mm</Text>
|
||||
{literals(strings.common.units.mm, [
|
||||
Math.round(layer.distance * 1000).toLocaleString(locale),
|
||||
])}
|
||||
</Text>
|
||||
|
||||
<Text size="2" color="gray">
|
||||
{Math.max(-100, Math.round(-50 * layer.distance))}% penetration
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||
.ricochet_loss,
|
||||
[Math.max(-100, -50 * layer.distance).toFixed(0)],
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{layer.type === ArmorType.External && (
|
||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
||||
{Math.round(layer.thickness).toLocaleString()}
|
||||
<Text size="1">mm</Text>
|
||||
{literals(strings.common.units.mm, [
|
||||
Math.round(layer.thickness).toLocaleString(locale),
|
||||
])}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -82,15 +75,24 @@ function LayerEntry({
|
||||
layer.type === ArmorType.Spaced) && (
|
||||
<>
|
||||
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
|
||||
{Math.round(layer.thicknessAngled).toLocaleString()}
|
||||
<Text size="1">{`mm${
|
||||
angle === undefined ? '' : ` ${Math.round(radToDeg(angle))}°`
|
||||
}`}</Text>
|
||||
{literals(
|
||||
angle === undefined
|
||||
? strings.common.units.mm
|
||||
: strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.stats.thickness_and_angle,
|
||||
[
|
||||
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
||||
radToDeg(angle ?? 0).toFixed(0),
|
||||
],
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text size="2" color="gray">
|
||||
{Math.round(layer.thickness).toLocaleString()}
|
||||
<Text size="1">mm</Text> nominal
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||
.nominal,
|
||||
[Math.round(layer.thickness).toLocaleString(locale)],
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -103,6 +105,8 @@ interface ShotDisplayCardProps extends CardProps {
|
||||
}
|
||||
|
||||
export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
const { locale, strings } = useLocale();
|
||||
|
||||
if (!shot) return null;
|
||||
|
||||
const outTitleColor = shot.out
|
||||
@@ -115,21 +119,23 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text color={inTitleColor} weight="bold">
|
||||
{shot.in.status !== 'blocked' && shot.in.status !== 'ricochet' && (
|
||||
<>
|
||||
{Math.round(shot.damage).toLocaleString()}
|
||||
<Text size="1">hp</Text>
|
||||
</>
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
||||
shot.in.status
|
||||
],
|
||||
[Math.round(shot.damage).toLocaleString(locale)],
|
||||
)}
|
||||
{` ${shotStatusNames[shot.in.status]} `}
|
||||
</Text>
|
||||
|
||||
<Flex direction="column">
|
||||
{shot.in.layers.map((layer, index) => {
|
||||
const layerName =
|
||||
layer.type === ArmorType.External
|
||||
? externalLayerNames[layer.variant]
|
||||
: layerTypeNames[`${layer.type}`];
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.element[
|
||||
layer.type === ArmorType.External
|
||||
? layer.variant
|
||||
: layerTypeNames[`${layer.type}`]
|
||||
];
|
||||
const layerIndex =
|
||||
layer.type === null
|
||||
? undefined
|
||||
@@ -179,22 +185,22 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
</Inset>
|
||||
|
||||
<Text color={outTitleColor} weight="bold">
|
||||
{shot.out.status !== 'blocked' &&
|
||||
shot.out.status !== 'ricochet' && (
|
||||
<>
|
||||
{Math.round(shot.damage).toLocaleString()}
|
||||
<Text size="1">hp</Text>
|
||||
</>
|
||||
)}
|
||||
{` ${shotStatusNames[shot.out.status]} `}
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.status[shot.out.status],
|
||||
[Math.round(shot.damage).toLocaleString(locale)],
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Flex direction="column">
|
||||
{shot.out.layers.map((layer, index) => {
|
||||
const layerName =
|
||||
layer.type === ArmorType.External
|
||||
? externalLayerNames[layer.variant]
|
||||
: layerTypeNames[`${layer.type}`];
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.element[
|
||||
layer.type === ArmorType.External
|
||||
? layer.variant
|
||||
: layerTypeNames[`${layer.type}`]
|
||||
];
|
||||
const shiftedIndex = index + shot.out!.layers.length;
|
||||
const layerIndex =
|
||||
layer.type === null
|
||||
|
||||
@@ -180,6 +180,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
@@ -191,6 +196,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
}
|
||||
@@ -275,6 +281,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
position.set(event.clientX, event.clientY);
|
||||
@@ -285,6 +296,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
}
|
||||
@@ -357,6 +369,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
position.set(event.clientX, event.clientY);
|
||||
@@ -367,6 +384,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Flex, Select, Text } from '@radix-ui/themes';
|
||||
import { Charts, type ChartsPeriod } from '../../stores/charts';
|
||||
|
||||
export function ChartsControl() {
|
||||
const period = Charts.use((state) => state.period);
|
||||
const mutateCharts = Charts.useMutation();
|
||||
|
||||
return (
|
||||
<Flex justify="center" align="center" gap="2">
|
||||
<Text>Player count</Text>
|
||||
<Select.Root
|
||||
value={period}
|
||||
onValueChange={(period) => {
|
||||
mutateCharts((draft) => {
|
||||
draft.period = period as ChartsPeriod;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value="total">Total</Select.Item>
|
||||
<Select.Item value="d_120">Past 120 days</Select.Item>
|
||||
<Select.Item value="d_90">Past 90 days</Select.Item>
|
||||
<Select.Item value="d_60">Past 60 days</Select.Item>
|
||||
<Select.Item value="d_30">This month</Select.Item>
|
||||
<Select.Item value="d_7">This week</Select.Item>
|
||||
<Select.Item value="d_1">Yesterday</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
TANK_CLASSES,
|
||||
TIERS,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
TankClass,
|
||||
formatCompact,
|
||||
} from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Box, Flex, Heading } from '@radix-ui/themes';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { Charts } from '../../stores/charts';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { ThemedBar } from '../Nivo/ThemedBar';
|
||||
|
||||
const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
awaitableAverageDefinitions,
|
||||
]);
|
||||
|
||||
const averageDefinitionsArray = Object.entries(averageDefinitions.averages);
|
||||
|
||||
export function PlayersInPeriod() {
|
||||
const period = Charts.use((state) => state.period);
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const filters = useStore($tankFilters);
|
||||
const data = TIERS.map((tier) => {
|
||||
const players: Record<TankClass, number> = {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
};
|
||||
|
||||
averageDefinitionsArray.forEach(([idString, entry]) => {
|
||||
const id = Number(idString);
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
if (
|
||||
tank === undefined ||
|
||||
tank.tier !== tier ||
|
||||
!filterTank(filters, tank)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
players[tank.class] += entry.samples[period] * ratio;
|
||||
});
|
||||
|
||||
const bar: Record<string, number | string> = {
|
||||
tier: TIER_ROMAN_NUMERALS[tier],
|
||||
};
|
||||
|
||||
TANK_CLASSES.forEach((c) => {
|
||||
bar[strings.common.tank_class_short[c]] = players[c];
|
||||
});
|
||||
|
||||
return bar;
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="4">
|
||||
<Heading align="center">Players by tier & class</Heading>
|
||||
<Box height="25rem">
|
||||
<ThemedBar
|
||||
data={data}
|
||||
keys={TANK_CLASSES.map((c) => strings.common.tank_class_short[c])}
|
||||
indexBy="tier"
|
||||
margin={{ right: 90, bottom: 50, left: 90 }}
|
||||
axisBottom={{
|
||||
legend: 'Tier',
|
||||
legendPosition: 'middle',
|
||||
legendOffset: 32,
|
||||
}}
|
||||
axisLeft={{
|
||||
legend: 'Players',
|
||||
legendPosition: 'middle',
|
||||
legendOffset: -60,
|
||||
format: formatCompact,
|
||||
}}
|
||||
legends={[
|
||||
{
|
||||
dataFrom: 'keys',
|
||||
anchor: 'bottom-right',
|
||||
direction: 'column',
|
||||
translateX: 120,
|
||||
itemWidth: 100,
|
||||
itemHeight: 20,
|
||||
},
|
||||
]}
|
||||
valueFormat={formatCompact}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const FIRST_AVERAGES_ENTRY = 19925;
|
||||
@@ -1,8 +1,14 @@
|
||||
import { assertSecret, idToRegion } from '@blitzkit/core';
|
||||
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
type LocaleAcceptorProps,
|
||||
} from '../../hooks/useLocale';
|
||||
import { App } from '../../stores/app';
|
||||
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants';
|
||||
import { LinkI18n } from '../LinkI18n';
|
||||
import './index.css';
|
||||
|
||||
type Extension =
|
||||
@@ -18,11 +24,13 @@ type Extension =
|
||||
status: 'error';
|
||||
};
|
||||
|
||||
export function Checks() {
|
||||
export function Checks({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<App.Provider>
|
||||
<Content />
|
||||
</App.Provider>
|
||||
<LocaleProvider locale={locale}>
|
||||
<App.Provider>
|
||||
<Content />
|
||||
</App.Provider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +38,7 @@ export function Checks() {
|
||||
* Wargaming: 14 (refresh: 7)
|
||||
*/
|
||||
function Content() {
|
||||
const { locale } = useLocale();
|
||||
const logins = App.use((state) => state.logins);
|
||||
const mutateApp = App.useMutation();
|
||||
const appStore = App.useStore();
|
||||
@@ -118,9 +127,14 @@ function Content() {
|
||||
<Text>
|
||||
This website utilizes cookies to perform analytics and personalize
|
||||
your experience. You can learn more through{' '}
|
||||
<Link href="/docs/legal/privacy-policy">our privacy policy</Link>.
|
||||
By using BlitzKit, you also agree to our{' '}
|
||||
<Link href="/docs/legal/terms-of-service">terms of service</Link>.
|
||||
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
|
||||
our privacy policy
|
||||
</LinkI18n>
|
||||
. By using BlitzKit, you also agree to our{' '}
|
||||
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
|
||||
terms of service
|
||||
</LinkI18n>
|
||||
.
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { Flex, Heading, IconButton, Table } from '@radix-ui/themes';
|
||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
import { CompareRow } from './CompareRow';
|
||||
@@ -15,6 +16,7 @@ export function Body({ stats }: BodyProps) {
|
||||
const hasNonRegularGun = members.some(
|
||||
({ gun }) => gun.gun_type!.$case !== 'regular',
|
||||
);
|
||||
const { unwrap, strings } = useLocale();
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
|
||||
return (
|
||||
@@ -60,7 +62,7 @@ export function Body({ stats }: BodyProps) {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={thisShell.name}
|
||||
alt={unwrap(thisShell.name)}
|
||||
width={16}
|
||||
height={16}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
@@ -76,10 +78,10 @@ export function Body({ stats }: BodyProps) {
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
<CompareRow stats={stats} name="DPM" value="dpm" decimals={0} />
|
||||
<CompareRow stats={stats} value="dpm" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Reload"
|
||||
name={strings.website.tools.tankopedia.characteristics.values.reload}
|
||||
deltaType="lowerIsBetter"
|
||||
value={(stats) =>
|
||||
stats.shellReload ??
|
||||
@@ -96,90 +98,44 @@ export function Body({ stats }: BodyProps) {
|
||||
<>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Intra-clip"
|
||||
deltaType="lowerIsBetter"
|
||||
value="intraClip"
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Shells"
|
||||
value="shells"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} value="shells" decimals={0} />
|
||||
</>
|
||||
)}
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Penetration"
|
||||
value="penetration"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} name="Damage" value="damage" decimals={0} />
|
||||
<CompareRow stats={stats} value="penetration" decimals={0} />
|
||||
<CompareRow stats={stats} value="damage" decimals={0} />
|
||||
{hasNonRegularGun && (
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="clipping potential"
|
||||
indent
|
||||
value="clipDamage"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} indent value="clipDamage" decimals={0} />
|
||||
)}
|
||||
<CompareRow stats={stats} value="moduleDamage" decimals={0} />
|
||||
<CompareRow stats={stats} value="caliber" decimals={0} />
|
||||
<CompareRow stats={stats} value="shellNormalization" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Module damage"
|
||||
value="moduleDamage"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} name="Caliber" value="caliber" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Normalization"
|
||||
value="shellNormalization"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Ricochet"
|
||||
value="shellRicochet"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} value="shellVelocity" decimals={0} />
|
||||
<CompareRow stats={stats} value="shellRange" decimals={0} />
|
||||
<CompareRow stats={stats} value="shellCapacity" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Shell velocity"
|
||||
value="shellVelocity"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Shell range"
|
||||
value="shellRange"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Shell capacity"
|
||||
value="shellCapacity"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Aim time"
|
||||
value="aimTime"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Dispersion"
|
||||
value="dispersion"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="moving"
|
||||
indent
|
||||
value="dispersionMoving"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -187,7 +143,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="hull traversing"
|
||||
indent
|
||||
value="dispersionHullTraversing"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -195,7 +150,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="turret traversing"
|
||||
indent
|
||||
value="dispersionTurretTraversing"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -203,7 +157,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="shooting"
|
||||
indent
|
||||
value="dispersionShooting"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -211,79 +164,42 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="gun damaged"
|
||||
indent
|
||||
value="dispersionGunDamaged"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={3}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Gun depression"
|
||||
value="gunDepression"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="elevation"
|
||||
indent
|
||||
value="gunElevation"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow stats={stats} value="gunDepression" decimals={1} />
|
||||
<CompareRow stats={stats} indent value="gunElevation" decimals={1} />
|
||||
</Table.Body>
|
||||
|
||||
<CompareSectionTitle>Maneuverability</CompareSectionTitle>
|
||||
<Table.Body>
|
||||
<CompareRow stats={stats} value="speedForwards" decimals={0} />
|
||||
<CompareRow stats={stats} indent value="speedBackwards" decimals={0} />
|
||||
<CompareRow stats={stats} value="enginePower" decimals={0} />
|
||||
<CompareRow stats={stats} value="weight" decimals={1} />
|
||||
<CompareRow stats={stats} value="hardTerrainCoefficient" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Speed forwards"
|
||||
value="speedForwards"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="backwards"
|
||||
indent
|
||||
value="speedBackwards"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Engine power"
|
||||
value="enginePower"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow stats={stats} name="Weight" value="weight" decimals={1} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Terrain coefficient"
|
||||
value="hardTerrainCoefficient"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="medium terrain"
|
||||
indent
|
||||
value="mediumTerrainCoefficient"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="soft terrain"
|
||||
indent
|
||||
value="softTerrainCoefficient"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Raw terrain coefficient"
|
||||
value="hardTerrainCoefficientRaw"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={2}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="medium terrain"
|
||||
indent
|
||||
value="mediumTerrainCoefficientRaw"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -291,7 +207,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="soft terrain"
|
||||
indent
|
||||
value="softTerrainCoefficientRaw"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -299,46 +214,35 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Power to weight ratio"
|
||||
value="powerToWeightRatioHardTerrain"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="medium terrain"
|
||||
indent
|
||||
value="powerToWeightRatioMediumTerrain"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="soft terrain"
|
||||
indent
|
||||
value="powerToWeightRatioSoftTerrain"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow stats={stats} value="turretTraverseSpeed" decimals={1} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Turret traverse speed"
|
||||
value="turretTraverseSpeed"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Traverse speed"
|
||||
value="hullTraverseHardTerrain"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="medium terrain"
|
||||
indent
|
||||
value="hullTraverseMediumTerrain"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="soft terrain"
|
||||
indent
|
||||
value="hullTraverseSoftTerrain"
|
||||
decimals={1}
|
||||
@@ -347,31 +251,23 @@ export function Body({ stats }: BodyProps) {
|
||||
|
||||
<CompareSectionTitle>Survivability</CompareSectionTitle>
|
||||
<Table.Body>
|
||||
<CompareRow stats={stats} name="Health" value="health" decimals={0} />
|
||||
<CompareRow stats={stats} value="health" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Fire chance"
|
||||
value="fireChance"
|
||||
deltaType="lowerIsBetter"
|
||||
display={(stats) => (stats.fireChance * 100).toFixed(0)}
|
||||
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
|
||||
/>
|
||||
<CompareRow stats={stats} value="viewRange" decimals={0} />
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="View range"
|
||||
value="viewRange"
|
||||
decimals={0}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Camouflage"
|
||||
value="camouflageStill"
|
||||
display={(stats) => (stats.camouflageStill * 100).toFixed(2)}
|
||||
deltaNominalDisplay={(delta) => (delta * 100).toFixed(2)}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="moving"
|
||||
indent
|
||||
value="camouflageMoving"
|
||||
display={(stats) => (stats.camouflageMoving * 100).toFixed(2)}
|
||||
@@ -379,7 +275,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="shooting"
|
||||
indent
|
||||
value="camouflageShootingStill"
|
||||
display={(stats) => (stats.camouflageShootingStill * 100).toFixed(2)}
|
||||
@@ -387,7 +282,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="shooting & moving"
|
||||
indent
|
||||
value="camouflageShootingMoving"
|
||||
display={(stats) => (stats.camouflageShootingMoving * 100).toFixed(2)}
|
||||
@@ -395,7 +289,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="caught on fire"
|
||||
indent
|
||||
value="camouflageCaughtOnFire"
|
||||
display={(stats) => (stats.camouflageCaughtOnFire * 100).toFixed(2)}
|
||||
@@ -403,14 +296,12 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="Volume"
|
||||
value="volume"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={1}
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="width"
|
||||
indent
|
||||
value="width"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -418,7 +309,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="height"
|
||||
indent
|
||||
value="height"
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -426,7 +316,6 @@ export function Body({ stats }: BodyProps) {
|
||||
/>
|
||||
<CompareRow
|
||||
stats={stats}
|
||||
name="length"
|
||||
indent
|
||||
value="length"
|
||||
deltaType="lowerIsBetter"
|
||||
|
||||
@@ -4,17 +4,15 @@ import {
|
||||
CaretSortIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Flex, IconButton, Table, Text } from '@radix-ui/themes';
|
||||
import { useRef } from 'react';
|
||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
||||
import { Var } from '../../core/radix/var';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { ComparePersistent } from '../../stores/comparePersistent';
|
||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||
|
||||
interface CompareRowProps {
|
||||
name: string;
|
||||
value:
|
||||
| keyof TankCharacteristics
|
||||
| ((member: TankCharacteristics) => number | undefined);
|
||||
type CompareRowProps = {
|
||||
display?: (
|
||||
member: Awaited<TankCharacteristics>,
|
||||
) => number | string | undefined;
|
||||
@@ -23,24 +21,39 @@ interface CompareRowProps {
|
||||
deltaNominalDisplay?: (delta: number) => string | number;
|
||||
indent?: boolean;
|
||||
stats: TankCharacteristics[];
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
name: string;
|
||||
value: (member: TankCharacteristics) => number | undefined;
|
||||
}
|
||||
| {
|
||||
value: keyof TankCharacteristics;
|
||||
}
|
||||
);
|
||||
|
||||
export function CompareRow({
|
||||
value,
|
||||
name,
|
||||
display,
|
||||
deltaType = 'higherIsBetter',
|
||||
decimals,
|
||||
deltaNominalDisplay,
|
||||
indent,
|
||||
stats,
|
||||
...props
|
||||
}: CompareRowProps) {
|
||||
const { strings } = useLocale();
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const sorting = CompareEphemeral.use((state) => state.sorting);
|
||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||
const values = stats.map((stat) =>
|
||||
typeof value === 'function' ? value(stat)! : (stat[value] as number),
|
||||
typeof props.value === 'function'
|
||||
? props.value(stat)!
|
||||
: (stat[props.value] as number),
|
||||
);
|
||||
const id = useRef(Math.random());
|
||||
const name =
|
||||
typeof props.value === 'string'
|
||||
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
||||
: props.name;
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
@@ -51,7 +64,7 @@ export function CompareRow({
|
||||
gap={indent ? { initial: '4', sm: '6' } : { initial: '1', sm: '2' }}
|
||||
>
|
||||
<IconButton
|
||||
color={sorting?.by === name ? undefined : 'gray'}
|
||||
color={sorting?.by === id.current ? undefined : 'gray'}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
@@ -62,26 +75,26 @@ export function CompareRow({
|
||||
const valueB = values[indexB];
|
||||
|
||||
return draft.sorting?.direction === 'ascending' &&
|
||||
draft.sorting.by === name
|
||||
draft.sorting.by === id.current
|
||||
? valueA - valueB
|
||||
: valueB - valueA;
|
||||
});
|
||||
|
||||
draft.sorting = {
|
||||
by: name,
|
||||
by: id.current,
|
||||
direction:
|
||||
draft.sorting?.direction === 'ascending' &&
|
||||
draft.sorting.by === name
|
||||
draft.sorting.by === id.current
|
||||
? 'descending'
|
||||
: 'ascending',
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
{(sorting === undefined || sorting.by !== name) && (
|
||||
{(sorting === undefined || sorting.by !== id.current) && (
|
||||
<CaretSortIcon style={{ transform: 'rotate(90deg)' }} />
|
||||
)}
|
||||
{sorting?.by === name && (
|
||||
{sorting?.by === id.current && (
|
||||
<>
|
||||
{sorting.direction === 'ascending' && <CaretRightIcon />}
|
||||
{sorting.direction === 'descending' && <CaretLeftIcon />}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDe
|
||||
import { awaitableSkillDefinitions } from '../../core/awaitables/skillDefinitions';
|
||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
||||
import { Var } from '../../core/radix/var';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import type { EquipmentMatrix } from '../../stores/duel';
|
||||
import { BlitzkitButtonGrayIcon } from '../BlitzkitButtonGrayIcon';
|
||||
@@ -43,6 +44,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
||||
const members = CompareEphemeral.use((state) => state.members);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const { unwrap, strings } = useLocale();
|
||||
|
||||
return (
|
||||
<StickyTableRoot
|
||||
@@ -140,7 +142,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{strings.website.tools.compare.table.skills.clear}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -152,7 +154,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Maximize
|
||||
{strings.website.tools.compare.table.skills.maximize}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
@@ -255,7 +257,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Stock
|
||||
{strings.website.tools.compare.table.modules.stock}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -272,7 +274,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
{
|
||||
strings.website.tools.compare.table.modules
|
||||
.upgrade
|
||||
}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
@@ -293,10 +298,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Flex direction="column" justify="center">
|
||||
{provisions.map((provision, index) => (
|
||||
<img
|
||||
alt={
|
||||
alt={unwrap(
|
||||
provisionDefinitions.provisions[provision]
|
||||
.name
|
||||
}
|
||||
.name,
|
||||
)}
|
||||
key={provision}
|
||||
src={asset(
|
||||
`/icons/provisions/${provision}.webp`,
|
||||
@@ -355,7 +360,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{
|
||||
strings.website.tools.compare.table.provisions
|
||||
.clear
|
||||
}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
@@ -439,7 +447,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{
|
||||
strings.website.tools.compare.table.equipment
|
||||
.clear
|
||||
}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -453,7 +464,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Apply to all
|
||||
{
|
||||
strings.website.tools.compare.table.equipment
|
||||
.apply_to_all
|
||||
}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
@@ -473,11 +487,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Flex direction="column" justify="center">
|
||||
{consumables.map((consumable, index) => (
|
||||
<img
|
||||
alt={
|
||||
alt={unwrap(
|
||||
consumableDefinitions.consumables[
|
||||
consumable
|
||||
].name
|
||||
}
|
||||
].name,
|
||||
)}
|
||||
key={consumable}
|
||||
src={asset(
|
||||
`/icons/consumables/${consumable}.webp`,
|
||||
@@ -537,7 +551,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{
|
||||
strings.website.tools.compare.table.consumables
|
||||
.clear
|
||||
}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import {
|
||||
ComparePersistent,
|
||||
@@ -23,6 +24,7 @@ export function Controls({
|
||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const mutateComparePersistent = ComparePersistent.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex gap="2" wrap="wrap" justify="center">
|
||||
@@ -32,14 +34,16 @@ export function Controls({
|
||||
>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="soft">
|
||||
<PlusIcon /> Add
|
||||
<PlusIcon /> {strings.website.tools.compare.actions.add.button}
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title align="center">Add tanks</Dialog.Title>
|
||||
<Dialog.Title align="center">
|
||||
{strings.website.tools.compare.actions.add.title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description align="center">
|
||||
Select tanks to add to comparison
|
||||
{strings.website.tools.compare.actions.add.description}
|
||||
</Dialog.Description>
|
||||
|
||||
<Flex gap="4" direction="column">
|
||||
@@ -87,7 +91,7 @@ export function Controls({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon /> Clear
|
||||
<TrashIcon /> {strings.website.tools.compare.actions.clear}
|
||||
</Button>
|
||||
|
||||
<SegmentedControl.Root
|
||||
@@ -99,13 +103,13 @@ export function Controls({
|
||||
}}
|
||||
>
|
||||
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
|
||||
No deltas
|
||||
{strings.website.tools.compare.actions.deltas.none}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
|
||||
Percentage
|
||||
{strings.website.tools.compare.actions.deltas.percentage}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={'absolute' satisfies DeltaMode}>
|
||||
Absolute
|
||||
{strings.website.tools.compare.actions.deltas.absolute}
|
||||
</SegmentedControl.Item>
|
||||
</SegmentedControl.Root>
|
||||
</Flex>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type TankDefinition, tankIcon } from '@blitzkit/core';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Vector2 } from 'three';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
import { InsertionMarker } from './IntersectionMarker';
|
||||
@@ -16,6 +17,7 @@ interface TankCardProps {
|
||||
export function TankCard({ index, tank }: TankCardProps) {
|
||||
const draggable = useRef<HTMLDivElement>(null);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
const initial = new Vector2();
|
||||
@@ -118,7 +120,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
alt={unwrap(tank.name)}
|
||||
src={tankIcon(tank.id)}
|
||||
width={64}
|
||||
height={64}
|
||||
@@ -136,7 +138,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{tank.name}
|
||||
{unwrap(tank.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Dialog, Flex, IconButton, Link } from '@radix-ui/themes';
|
||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { LinkI18n } from '../LinkI18n';
|
||||
import { TankSearch } from '../TankSearch';
|
||||
|
||||
interface TankControlProps {
|
||||
@@ -14,6 +16,7 @@ interface TankControlProps {
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function TankControl({ index, id }: TankControlProps) {
|
||||
const { locale } = useLocale();
|
||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
|
||||
@@ -69,11 +72,15 @@ export function TankControl({ index, id }: TankControlProps) {
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Link href={`/tools/tankopedia/${id}`} target="_blank">
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
href={`/tools/tankopedia/${id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<IconButton variant="ghost">
|
||||
<ExternalLinkIcon />
|
||||
</IconButton>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, type ButtonProps, Tooltip } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
|
||||
interface CopyButtonProps extends ButtonProps {
|
||||
copy: () => string | undefined;
|
||||
@@ -7,9 +8,13 @@ interface CopyButtonProps extends ButtonProps {
|
||||
|
||||
export function CopyButton({ copy, ...props }: CopyButtonProps) {
|
||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Tooltip open={tooltipOpen} content="Copied!">
|
||||
<Tooltip
|
||||
open={tooltipOpen}
|
||||
content={strings.website.common.copy_button.copied}
|
||||
>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={() => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import './index.css';
|
||||
|
||||
const DAY_TITLES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
@@ -37,6 +38,7 @@ export function DatePicker({
|
||||
const [yearOffset, setYearOffset] = useState(0);
|
||||
const [monthOffset, setMonthOffset] = useState(0);
|
||||
const date = new Date(year + yearOffset, month + monthOffset, day);
|
||||
const { locale } = useLocale();
|
||||
|
||||
const daysInLastMonth = new Date(
|
||||
year + yearOffset,
|
||||
@@ -94,11 +96,7 @@ export function DatePicker({
|
||||
</IconButton>
|
||||
|
||||
<Flex gap="1">
|
||||
<Text>
|
||||
{date.toLocaleString(undefined, {
|
||||
month: 'long',
|
||||
})}
|
||||
</Text>
|
||||
<Text>{date.toLocaleString(locale, { month: 'long' })}</Text>
|
||||
|
||||
<Popover.Root onOpenChange={() => setYearScroll(0)}>
|
||||
<Popover.Trigger>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Link, type LinkProps } from '@radix-ui/themes';
|
||||
|
||||
export function DummyLink({ href = '#', onClick, ...props }: LinkProps) {
|
||||
return (
|
||||
<Link href={href} onClick={(event) => event.preventDefault()} {...props} />
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Switch } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
|
||||
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
import { ColorControllerRaw } from './ColorControllerRaw';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Select } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
import type {
|
||||
EmbedConfigItemType,
|
||||
@@ -13,6 +14,7 @@ export function Enum({
|
||||
config: EmbedConfigItemType<EmbedItemType.Enum>;
|
||||
}) {
|
||||
const mutateEmbedState = EmbedState.useMutation();
|
||||
const { locale } = useLocale();
|
||||
|
||||
return (
|
||||
<Select.Root
|
||||
@@ -27,7 +29,7 @@ export function Enum({
|
||||
<Select.Content>
|
||||
{config.options.map((option) => (
|
||||
<Select.Item key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{option.locales[locale]}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type AccountListItem,
|
||||
type AccountListWithServer,
|
||||
} from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { CaretLeftIcon, Link2Icon, PersonIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Badge,
|
||||
@@ -19,6 +19,7 @@ import { debounce } from 'lodash-es';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { stringify } from 'urlon';
|
||||
import type { embedConfigurations } from '../../constants/embeds';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { EmbedState, type EmbedStateStore } from '../../stores/embedState';
|
||||
import { CopyButton } from '../CopyButton';
|
||||
|
||||
@@ -34,6 +35,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [results, setResults] = useState<AccountListWithServer>([]);
|
||||
const { strings } = useLocale();
|
||||
const performSearch = useCallback(
|
||||
debounce(async () => {
|
||||
if (!input.current) return;
|
||||
@@ -72,20 +74,27 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
<Dialog.Trigger>
|
||||
<Button>
|
||||
<Link2Icon />
|
||||
Generate URL
|
||||
{strings.website.tools.embed.configuration.export.generate.button}
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content width="fit-content">
|
||||
<Flex direction="column" gap="1">
|
||||
<Heading size="5">Generate URL</Heading>
|
||||
<Text color="gray">Search an account to track</Text>
|
||||
<Heading size="5">
|
||||
{strings.website.tools.embed.configuration.export.generate.title}
|
||||
</Heading>
|
||||
<Text color="gray">
|
||||
{
|
||||
strings.website.tools.embed.configuration.export.generate
|
||||
.subtitle
|
||||
}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<TextField.Root
|
||||
mt="4"
|
||||
style={{ width: '20rem' }}
|
||||
placeholder="Search players..."
|
||||
placeholder={strings.website.common.player_search.hint}
|
||||
ref={input}
|
||||
onChange={handleChange}
|
||||
>
|
||||
@@ -98,12 +107,14 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
{searching && (
|
||||
<Text color="gray">
|
||||
<Flex align="center" gap="2">
|
||||
<Spinner /> Searching
|
||||
<Spinner /> {strings.website.common.player_search.searching}
|
||||
</Flex>
|
||||
</Text>
|
||||
)}
|
||||
{!searching && results.length === 0 && (
|
||||
<Text color="gray">No results</Text>
|
||||
<Text color="gray">
|
||||
{strings.website.common.player_search.no_results}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!searching && results.length > 0 && (
|
||||
@@ -131,7 +142,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
|
||||
<Dialog.Close>
|
||||
<Button color="red" variant="outline" style={{ width: '100%' }}>
|
||||
Cancel
|
||||
{strings.website.tools.embed.configuration.export.generate.cancel}
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
@@ -140,12 +151,18 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
<Dialog.Root open={copyDialogOpen} onOpenChange={setCopyDialogOpen}>
|
||||
<Dialog.Content style={{ width: 'fit-content' }}>
|
||||
<Flex direction="column" gap="1">
|
||||
<Heading size="5">Copy URL</Heading>
|
||||
<Heading size="5">
|
||||
{
|
||||
strings.website.tools.embed.configuration.export.generate
|
||||
.success.title
|
||||
}
|
||||
</Heading>
|
||||
<Text color="gray">
|
||||
Generated the URL for {copyUser?.nickname}. The embed will start
|
||||
tracking as soon as the URL is pasted. Reset at any time by
|
||||
right-clicking on PC or long-pressing on mobile and then selecting
|
||||
"Reset".
|
||||
{literals(
|
||||
strings.website.tools.embed.configuration.export.generate
|
||||
.success.info,
|
||||
[`${copyUser?.nickname}`],
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -158,7 +175,11 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
setSearchDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<CaretLeftIcon /> Back
|
||||
<CaretLeftIcon />{' '}
|
||||
{
|
||||
strings.website.tools.embed.configuration.export.generate
|
||||
.success.back
|
||||
}
|
||||
</Button>
|
||||
<CopyButton
|
||||
style={{ flex: 1 }}
|
||||
@@ -186,7 +207,11 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
||||
return `${location.origin}/tools/embed/${embed}/host?${searchParams.toString()}`;
|
||||
}}
|
||||
>
|
||||
<Link2Icon /> Copy URL
|
||||
<Link2Icon />{' '}
|
||||
{
|
||||
strings.website.tools.embed.configuration.export.generate
|
||||
.success.copy
|
||||
}
|
||||
</CopyButton>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { useRef, useState } from 'react';
|
||||
import { parse } from 'urlon';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
|
||||
export function Import() {
|
||||
@@ -16,12 +17,15 @@ export function Import() {
|
||||
const [showInvalidURLPrompt, setShowInvalidURLPrompt] = useState(false);
|
||||
const [showConfirmation, setConfirmation] = useState(false);
|
||||
const mutateEmbedState = EmbedState.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>Import</Heading>
|
||||
<Heading>
|
||||
{strings.website.tools.embed.configuration.import.title}
|
||||
</Heading>
|
||||
<Text size="2" color="gray" mb="2">
|
||||
Paste your current URL if you have one already to import your settings
|
||||
{strings.website.tools.embed.configuration.import.description}
|
||||
</Text>
|
||||
|
||||
<AlertDialog.Root
|
||||
@@ -29,15 +33,18 @@ export function Import() {
|
||||
onOpenChange={setShowInvalidURLPrompt}
|
||||
>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>Invalid URL</AlertDialog.Title>
|
||||
<AlertDialog.Title>
|
||||
{strings.website.tools.embed.configuration.import.idiot_title}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Try pasting the URL that BlitzKit previously generated, you silly
|
||||
goose!
|
||||
{strings.website.tools.embed.configuration.import.idiot_description}
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<AlertDialog.Cancel>
|
||||
<Button>Okay, my bad</Button>
|
||||
<Button>
|
||||
{strings.website.tools.embed.configuration.import.idiot_button}
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
@@ -46,7 +53,7 @@ export function Import() {
|
||||
<Flex gap="2" mb="6">
|
||||
<TextField.Root
|
||||
ref={importInput}
|
||||
placeholder="Paste URL..."
|
||||
placeholder={strings.website.tools.embed.configuration.import.hint}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Tooltip content="Applied!" open={showConfirmation}>
|
||||
@@ -72,7 +79,7 @@ export function Import() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
{strings.website.tools.embed.configuration.import.apply}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slider } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState, type RadixRadius } from '../../stores/embedState';
|
||||
|
||||
export function Radius({ configKey }: EmbedPreviewControllerProps) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FontBoldIcon, FontSizeIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Slider, Text } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState';
|
||||
import {
|
||||
radixTextWeights,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slider } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState, type RadixSize } from '../../stores/embedState';
|
||||
|
||||
export function Size({ configKey }: EmbedPreviewControllerProps) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slider } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState';
|
||||
|
||||
export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slider as RadixSlider } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState, type RadixSize } from '../../stores/embedState';
|
||||
import type {
|
||||
EmbedConfigItemType,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TextField } from '@radix-ui/themes';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
|
||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/tools/embed/[embed]/_index';
|
||||
import { EmbedState } from '../../stores/embedState';
|
||||
|
||||
export function TextController({ configKey }: EmbedPreviewControllerProps) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
sumCompositeStats,
|
||||
type IndividualTankStats,
|
||||
} from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import { STRINGS } from '@blitzkit/i18n';
|
||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { ContextMenu } from '@radix-ui/themes';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -20,8 +20,13 @@ import { useEmbedStateCurry } from '../../stores/embedState/utilities';
|
||||
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from '../TanksEmbed';
|
||||
|
||||
export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
|
||||
label: strings.common.composite_stats[value],
|
||||
value,
|
||||
locales: Object.entries(STRINGS).reduce((accumulator, [locale, strings]) => {
|
||||
return {
|
||||
...accumulator,
|
||||
[locale]: strings.common.composite_stats[value],
|
||||
};
|
||||
}, {}),
|
||||
}));
|
||||
|
||||
const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
---
|
||||
import { assertSecret, fetchGameDefinitions } from '@blitzkit/core';
|
||||
import { Code, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
||||
import { Box, Code, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import packageJson from '../../../../package.json';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
import { LinkI18n } from './LinkI18n';
|
||||
import { LocaleSwitcherThemeWrapper } from './LocaleSwitcher';
|
||||
|
||||
const gameDefinitions = await fetchGameDefinitions();
|
||||
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
||||
const strings = getStrings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -32,10 +38,14 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
}}
|
||||
>
|
||||
<Heading>BlitzKit</Heading>
|
||||
<Text color="gray">Everything World of Tanks Blitz</Text>
|
||||
<Text color="gray">{strings.website.footer.motto}</Text>
|
||||
<Text size="1" color="gray">
|
||||
Unaffiliated with Wargaming or any service
|
||||
{strings.website.footer.copyright}
|
||||
</Text>
|
||||
|
||||
<Box mt="2">
|
||||
<LocaleSwitcherThemeWrapper {locale} client:load />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
@@ -61,7 +71,7 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
sm: 'auto',
|
||||
}}
|
||||
>
|
||||
<Heading size="3">About</Heading>
|
||||
<Heading size="3">{strings.website.footer.about.title}</Heading>
|
||||
<Flex
|
||||
direction="column"
|
||||
align={{
|
||||
@@ -69,22 +79,26 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
sm: 'start',
|
||||
}}
|
||||
>
|
||||
<Link href="/docs/about/credits" color="gray"> Credits </Link>
|
||||
<LinkI18n {locale} href="/docs/about/credits" color="gray">
|
||||
{strings.website.footer.about.credits}
|
||||
</LinkI18n>
|
||||
<Link
|
||||
href="https://discord.gg/nDt7AjGJQH"
|
||||
target="_blank"
|
||||
color="gray"
|
||||
>
|
||||
Discord
|
||||
{strings.website.footer.about.discord}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/tresabhi/blitzkit"
|
||||
target="_blank"
|
||||
color="gray"
|
||||
>
|
||||
GitHub
|
||||
{strings.website.footer.about.github}
|
||||
</Link>
|
||||
<Link href="/docs/patches" color="gray"> Patches </Link>
|
||||
<LinkI18n {locale} href="/docs/patches" color="gray">
|
||||
{strings.website.footer.about.patches}
|
||||
</LinkI18n>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -100,14 +114,14 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
sm: 'auto',
|
||||
}}
|
||||
>
|
||||
<Heading size="3">Legal</Heading>
|
||||
<Heading size="3">{strings.website.footer.legal.title}</Heading>
|
||||
<Flex direction="column">
|
||||
<Link href="/docs/legal/privacy-policy" color="gray">
|
||||
Privacy policy
|
||||
</Link>
|
||||
<Link href="/docs/legal/terms-of-service" color="gray">
|
||||
Terms of service
|
||||
</Link>
|
||||
<LinkI18n {locale} href="/docs/legal/privacy-policy" color="gray">
|
||||
{strings.website.footer.legal.privacy}
|
||||
</LinkI18n>
|
||||
<LinkI18n {locale} href="/docs/legal/terms-of-service" color="gray">
|
||||
{strings.website.footer.legal.terms}
|
||||
</LinkI18n>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -119,7 +133,7 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
{
|
||||
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
||||
<Text size="2" color="gray">
|
||||
WoT Blitz
|
||||
WoTB
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -144,7 +158,7 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" align="center" display={{ initial: 'none', sm: 'flex' }}>
|
||||
<Text size="2" color="gray"> BlitzKit </Text>
|
||||
<Text size="2" color="gray">BlitzKit</Text>
|
||||
<Code size="2" color="gray">
|
||||
{packageJson.version}
|
||||
</Code>
|
||||
@@ -153,7 +167,7 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
PUBLIC_ASSET_BRANCH !== 'preview' && (
|
||||
<>
|
||||
<Text size="2" color="gray">
|
||||
WoT Blitz
|
||||
WoTB
|
||||
</Text>
|
||||
<Code size="2" color="gray">
|
||||
{gameDefinitions.version}
|
||||
@@ -162,7 +176,7 @@ const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
)
|
||||
}
|
||||
|
||||
<Text size="2" color="gray"> CDN </Text>
|
||||
<Text size="2" color="gray">CDN</Text>
|
||||
<Code size="2" color="gray">
|
||||
{PUBLIC_ASSET_BRANCH}
|
||||
</Code>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { times } from 'lodash-es';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
||||
import { GalleryCard } from './Card';
|
||||
|
||||
@@ -20,6 +22,7 @@ const PREVIEW_COUNT = 28;
|
||||
export function GalleryList({ avatars }: GalleryListProps) {
|
||||
const search = GalleryEphemeral.use((state) => state.search);
|
||||
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
|
||||
const { locale, strings } = useLocale();
|
||||
const filtered = useMemo(() => {
|
||||
setLoadedCards(DEFAULT_LOADED);
|
||||
|
||||
@@ -35,7 +38,9 @@ export function GalleryList({ avatars }: GalleryListProps) {
|
||||
return (
|
||||
<>
|
||||
<Text align="center" color="gray">
|
||||
{filtered.length.toLocaleString()} avatars
|
||||
{literals(strings.website.tools.gallery.search.results, [
|
||||
filtered.length.toLocaleString(locale),
|
||||
])}
|
||||
</Text>
|
||||
|
||||
<Flex wrap="wrap" gap="4" justify="center">
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { TextField } from '@radix-ui/themes';
|
||||
import { useRef } from 'react';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
||||
|
||||
export function GallerySearch() {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const mutateGalleryEphemeral = GalleryEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<TextField.Root
|
||||
placeholder="Search avatars..."
|
||||
placeholder={strings.website.tools.gallery.search.hint}
|
||||
ref={input}
|
||||
onChange={(event) => {
|
||||
mutateGalleryEphemeral((draft) => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
||||
import { GlobeIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import type { LocaleAcceptorProps } from '../../hooks/useLocale';
|
||||
import { BlitzKitTheme } from '../BlitzKitTheme';
|
||||
import { LocaleSwitcher } from '../LocaleSwitcher';
|
||||
|
||||
export function HomeLocaleSwitcher({
|
||||
locale = DEFAULT_LOCALE,
|
||||
}: LocaleAcceptorProps) {
|
||||
return (
|
||||
<BlitzKitTheme>
|
||||
<Flex justify="center" mt="4" py="6" align="center" gap="3">
|
||||
<Text size="5" trim="end" color="gray">
|
||||
<GlobeIcon width="1em" height="1em" />
|
||||
</Text>
|
||||
|
||||
<LocaleSwitcher locale={locale} />
|
||||
</Flex>
|
||||
</BlitzKitTheme>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
---
|
||||
import { Code, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { resolveBranchName } from '../core/blitzkit/branches';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
|
||||
const branchName = resolveBranchName();
|
||||
const { locale } = Astro.params;
|
||||
const branchName = resolveBranchName(locale);
|
||||
const isBranchNamed = branchName !== undefined;
|
||||
const strings = getStrings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -141,7 +145,9 @@ const isBranchNamed = branchName !== undefined;
|
||||
</Flex>
|
||||
</Heading>
|
||||
|
||||
<Text color="gray" align="center" size="4">By TrèsAbhi ❤️</Text>
|
||||
<Text color="gray" align="center" size="4"
|
||||
>{literals(strings.website.home.hero.credit, ['TrèsAbhi'])}</Text
|
||||
>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -1,8 +1,14 @@
|
||||
import { assertSecret } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { EyeOpenIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { google } from 'googleapis';
|
||||
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions';
|
||||
import {
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
type LocaleAcceptorProps,
|
||||
} from '../hooks/useLocale';
|
||||
import { TankopediaPersistent } from '../stores/tankopediaPersistent';
|
||||
import { TankCard } from './TankCard';
|
||||
|
||||
@@ -63,22 +69,26 @@ const hotTanks = report.data.rows
|
||||
views,
|
||||
}));
|
||||
|
||||
export function HomePageHotTanks() {
|
||||
export function HomePageHotTanks({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<TankopediaPersistent.Provider>
|
||||
<Content />
|
||||
</TankopediaPersistent.Provider>
|
||||
<LocaleProvider locale={locale}>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Content />
|
||||
</TankopediaPersistent.Provider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const { locale, strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt="4" pb="8">
|
||||
<Heading align="center" size="5">
|
||||
Popular tanks
|
||||
{strings.website.home.hot_tanks.title}
|
||||
</Heading>
|
||||
<Text color="gray" align="center" mb="5" size="2">
|
||||
Last 7 days
|
||||
{literals(strings.website.home.hot_tanks.subtitle, ['7'])}
|
||||
</Text>
|
||||
|
||||
<Flex justify="center" gap="4" wrap="wrap">
|
||||
@@ -89,7 +99,7 @@ function Content() {
|
||||
discriminator={
|
||||
<Flex align="center" gap="1" justify="center">
|
||||
<EyeOpenIcon />
|
||||
{Math.round(row.views * (30 / 7)).toLocaleString('en-US')}
|
||||
{Math.round(row.views * (30 / 7)).toLocaleString(locale)}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
||||
import { Link, type LinkProps } from '@radix-ui/themes';
|
||||
import type { LocaleAcceptorProps } from '../hooks/useLocale';
|
||||
|
||||
type LinkI18nProps = LinkProps & LocaleAcceptorProps;
|
||||
|
||||
export function LinkI18n({ locale, href, ...props }: LinkI18nProps) {
|
||||
const prefix =
|
||||
locale === undefined || locale === DEFAULT_LOCALE ? '' : `/${locale}`;
|
||||
|
||||
return (
|
||||
<Link href={href?.startsWith('/') ? `${prefix}${href}` : href} {...props} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {} from '@blitzkit/core';
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
STRINGS,
|
||||
SUPPORTED_LOCALES,
|
||||
SUPPORTED_LOCALE_FLAGS,
|
||||
} from '@blitzkit/i18n';
|
||||
import { Select } from '@radix-ui/themes';
|
||||
import type { LocaleAcceptorProps } from '../hooks/useLocale';
|
||||
import { BlitzKitTheme } from './BlitzKitTheme';
|
||||
|
||||
export function LocaleSwitcherThemeWrapper({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<BlitzKitTheme style={{ background: 'transparent' }}>
|
||||
<LocaleSwitcher locale={locale} />
|
||||
</BlitzKitTheme>
|
||||
);
|
||||
}
|
||||
|
||||
export function LocaleSwitcher({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<Select.Root
|
||||
defaultValue={locale}
|
||||
onValueChange={(locale) => {
|
||||
localStorage.setItem('preferred-locale', locale);
|
||||
|
||||
let rawPath = window.location.pathname;
|
||||
|
||||
for (const supportedLocale of SUPPORTED_LOCALES) {
|
||||
if (window.location.pathname.startsWith(`/${supportedLocale}`)) {
|
||||
rawPath = rawPath.replace(`/${supportedLocale}`, '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.location.pathname = `${
|
||||
locale === DEFAULT_LOCALE ? '' : `/${locale}`
|
||||
}${rawPath}`;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger />
|
||||
|
||||
<Select.Content>
|
||||
{SUPPORTED_LOCALES.map((locale) => (
|
||||
<Select.Item key={locale} value={locale}>
|
||||
{SUPPORTED_LOCALE_FLAGS[locale]}{' '}
|
||||
{STRINGS[locale].common.locales[locale]}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ClockIcon, ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
|
||||
import { useDelta } from '../../hooks/useDelta';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GenericTankComponentButton } from './GenericTankComponentButton';
|
||||
import type { TankComponentButtonProps } from './TankComponentButton';
|
||||
|
||||
@@ -22,11 +23,12 @@ export function ConsumableButton({
|
||||
}: ConsumableButtonProps) {
|
||||
const cooldownDelta = useDelta(cooldown ?? 0);
|
||||
const durationDelta = useDelta(duration ?? 0);
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<GenericTankComponentButton
|
||||
style={{ width: '6rem' }}
|
||||
tooltip={consumableDefinitions.consumables[consumable].name}
|
||||
tooltip={unwrap(consumableDefinitions.consumables[consumable].name)}
|
||||
icon={asset(`icons/consumables/${consumable}.webp`)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GenericTankComponentButton } from './GenericTankComponentButton';
|
||||
import type { TankComponentButtonProps } from './TankComponentButton';
|
||||
|
||||
@@ -10,9 +11,11 @@ interface EquipmentButtonProps extends TankComponentButtonProps {
|
||||
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
||||
|
||||
export function EquipmentButton({ equipment, ...props }: EquipmentButtonProps) {
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<GenericTankComponentButton
|
||||
tooltip={equipmentDefinitions.equipments[equipment].name}
|
||||
tooltip={unwrap(equipmentDefinitions.equipments[equipment].name)}
|
||||
icon={asset(`icons/equipment/${equipment}.webp`)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GenericTankComponentButton } from './GenericTankComponentButton';
|
||||
import type { TankComponentButtonProps } from './TankComponentButton';
|
||||
|
||||
@@ -10,9 +11,11 @@ interface ProvisionButtonProps extends TankComponentButtonProps {
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function ProvisionButton({ provision, ...props }: ProvisionButtonProps) {
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<GenericTankComponentButton
|
||||
tooltip={provisionDefinitions.provisions[provision].name}
|
||||
tooltip={unwrap(provisionDefinitions.provisions[provision].name)}
|
||||
icon={asset(`icons/provisions/${provision}.webp`)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@blitzkit/core';
|
||||
import { DropdownMenu, Flex } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { ModuleButton } from './ModuleButtons/ModuleButton';
|
||||
|
||||
interface ModuleManagerProps {
|
||||
@@ -35,6 +36,7 @@ export function ModuleManager({
|
||||
const [gunMenuOpen, setGunMenuOpen] = useState(false);
|
||||
const [engineMenuOpen, setEngineMenuOpen] = useState(false);
|
||||
const [trackMenuOpen, setTrackMenuOpen] = useState(false);
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex gap="1" wrap="wrap">
|
||||
@@ -71,7 +73,7 @@ export function ModuleManager({
|
||||
module="turret"
|
||||
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
|
||||
/>{' '}
|
||||
{turret.name}
|
||||
{unwrap(turret.name)}
|
||||
</DropdownMenu.RadioItem>
|
||||
);
|
||||
})}
|
||||
@@ -117,7 +119,7 @@ export function ModuleManager({
|
||||
TIER_ROMAN_NUMERALS[gun.gun_type!.value.base.tier]
|
||||
}
|
||||
/>{' '}
|
||||
{gun.gun_type!.value.base.name}
|
||||
{unwrap(gun.gun_type!.value.base.name)}
|
||||
</DropdownMenu.RadioItem>
|
||||
);
|
||||
})}
|
||||
@@ -156,7 +158,7 @@ export function ModuleManager({
|
||||
module="engine"
|
||||
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
|
||||
/>{' '}
|
||||
{engine.name}
|
||||
{unwrap(engine.name)}
|
||||
</DropdownMenu.RadioItem>
|
||||
);
|
||||
})}
|
||||
@@ -195,7 +197,7 @@ export function ModuleManager({
|
||||
module="chassis"
|
||||
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
|
||||
/>{' '}
|
||||
{track.name}
|
||||
{unwrap(track.name)}
|
||||
</DropdownMenu.RadioItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -19,12 +19,15 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { resolveBranchName } from '../core/blitzkit/branches';
|
||||
import { homeTool, TOOLS } from '../constants/tools';
|
||||
import { NavbarAccount } from './NavbarAccount';
|
||||
import { LinkI18n } from './LinkI18n';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
|
||||
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
|
||||
const tools = [homeTool, ...toolsFiltered];
|
||||
const branchName = resolveBranchName();
|
||||
const { locale } = Astro.params;
|
||||
const branchName = resolveBranchName(locale);
|
||||
const isBranchNamed = branchName !== undefined;
|
||||
const strings = getStrings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -108,7 +111,8 @@ const isBranchNamed = branchName !== undefined;
|
||||
<Flex className="expander" data-expanded="false" direction="column" gap="2">
|
||||
<Flex justify="center">
|
||||
<Flex flexGrow="1" maxWidth="1024px" p="3" align="center">
|
||||
<Link
|
||||
<LinkI18n
|
||||
{locale}
|
||||
color="gray"
|
||||
highContrast
|
||||
href="/"
|
||||
@@ -125,7 +129,7 @@ const isBranchNamed = branchName !== undefined;
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
|
||||
<Box flexGrow="1" />
|
||||
|
||||
@@ -136,24 +140,28 @@ const isBranchNamed = branchName !== undefined;
|
||||
(branch) =>
|
||||
branch !== assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||
);
|
||||
const selected = Astro.url.pathname.startsWith(
|
||||
`/tools/${tool.id}`,
|
||||
);
|
||||
const selected = Astro.url.pathname.includes(`/tools/${tool.id}`);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.stringsId ??
|
||||
tool.id) as keyof typeof strings.website.tools
|
||||
];
|
||||
|
||||
if (unavailableOnBranch) return null;
|
||||
|
||||
return (
|
||||
<Fragment key={tool.title}>
|
||||
<Fragment key={tool.id}>
|
||||
{index > 0 && <Separator orientation="vertical" size="1" />}
|
||||
<Link
|
||||
<LinkI18n
|
||||
{locale}
|
||||
color="gray"
|
||||
highContrast={selected}
|
||||
size="2"
|
||||
href={`/tools/${tool.id}`}
|
||||
underline={selected ? 'always' : 'hover'}
|
||||
>
|
||||
{tool.title}
|
||||
</Link>
|
||||
{toolStrings.name}
|
||||
</LinkI18n>
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
@@ -173,16 +181,17 @@ const isBranchNamed = branchName !== undefined;
|
||||
<DiscordLogoIcon />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
<LinkI18n
|
||||
{locale}
|
||||
style={{ display: 'flex', alignContent: 'center' }}
|
||||
color="gray"
|
||||
href="/settings"
|
||||
underline="none"
|
||||
>
|
||||
<GearIcon />
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
|
||||
<NavbarAccount client:idle />
|
||||
<!-- <NavbarAccount client:idle /> -->
|
||||
|
||||
<IconButton variant="ghost" color="gray" className="hamburger">
|
||||
<Cross1Icon data-expanded />
|
||||
@@ -207,6 +216,11 @@ const isBranchNamed = branchName !== undefined;
|
||||
(branch) =>
|
||||
branch !== assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||
);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.stringsId ??
|
||||
tool.id) as keyof typeof strings.website.tools
|
||||
];
|
||||
|
||||
if (unavailableOnBranch) return null;
|
||||
|
||||
@@ -226,7 +240,8 @@ const isBranchNamed = branchName !== undefined;
|
||||
aspectRatio: '2 / 1',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
<LinkI18n
|
||||
{locale}
|
||||
href={tool.id.length === 0 ? '/' : `/tools/${tool.id}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -259,9 +274,9 @@ const isBranchNamed = branchName !== undefined;
|
||||
WebkitBackdropFilter: 'blur(4rem)',
|
||||
}}
|
||||
>
|
||||
<Text>{tool.title}</Text>
|
||||
<Text>{toolStrings.name}</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -4,21 +4,23 @@ import {
|
||||
CaretUpIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Table } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
||||
import {
|
||||
TankPerformanceSortTypeNames,
|
||||
TankPerformanceSortTypeNamesArray,
|
||||
} from '../../stores/tankPerformanceSort/constants';
|
||||
import { TankPerformanceSortTypeNamesArray } from '../../stores/tankPerformanceSort/constants';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
|
||||
export function Header() {
|
||||
const sort = TankPerformanceSort.use();
|
||||
const store = TankPerformanceSort.useStore();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Table.Header>
|
||||
<Table.Row align="center">
|
||||
<StickyColumnHeaderCell>Tank</StickyColumnHeaderCell>
|
||||
<StickyColumnHeaderCell>
|
||||
{strings.website.tools.performance.table.tanks.header}
|
||||
</StickyColumnHeaderCell>
|
||||
|
||||
{TankPerformanceSortTypeNamesArray.map((type) => {
|
||||
const isSelected = sort.type === type;
|
||||
|
||||
@@ -42,7 +44,7 @@ export function Header() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{TankPerformanceSortTypeNames[type]}
|
||||
{strings.website.tools.performance.table.stats[type]}
|
||||
|
||||
{!isSelected && <CaretSortIcon />}
|
||||
{isSelected && (
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { formatCompact } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import { Callout, Flex } from '@radix-ui/themes';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { awaitableDiscoveredIdsDefinitions } from '../../core/awaitables/discoveredIdsDefinitions';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { InlineSkeleton } from '../InlineSkeleton';
|
||||
|
||||
@@ -12,14 +14,21 @@ const [discoveredIdsDefinitions, averageDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function PerformanceInfo({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const samples = formatCompact(Math.round(discoveredIdsDefinitions.count));
|
||||
const { strings, locale } = useLocale();
|
||||
const samples = formatCompact(
|
||||
locale,
|
||||
Math.round(discoveredIdsDefinitions.count),
|
||||
);
|
||||
const minutesAgo = Math.floor(
|
||||
(Date.now() - averageDefinitions.time) / (1000 * 60),
|
||||
);
|
||||
const hoursAgo = Math.floor(minutesAgo / 60);
|
||||
const timeAgo = `${hoursAgo === 0 ? minutesAgo : hoursAgo} ${
|
||||
hoursAgo === 0 ? 'minutes' : 'hours'
|
||||
}`;
|
||||
const timeAgo = literals(
|
||||
strings.website.tools.performance.estimation[
|
||||
hoursAgo === 0 ? 'minutes' : 'hours'
|
||||
],
|
||||
[`${hoursAgo === 0 ? minutesAgo : hoursAgo}`],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex justify="center">
|
||||
@@ -28,9 +37,13 @@ export function PerformanceInfo({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<InfoCircledIcon />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>
|
||||
Estimated stats based on{' '}
|
||||
{skeleton ? <InlineSkeleton width="2rem" /> : samples} players.
|
||||
Updated {skeleton ? <InlineSkeleton width="3rem" /> : timeAgo} ago.
|
||||
{skeleton && <InlineSkeleton width="9rem" />}
|
||||
|
||||
{!skeleton &&
|
||||
literals(strings.website.tools.performance.estimation.body, [
|
||||
samples,
|
||||
timeAgo,
|
||||
])}
|
||||
</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex, Select, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import {
|
||||
TankPerformanceEphemeral,
|
||||
type PlayerCountPeriod,
|
||||
@@ -9,10 +10,11 @@ export function PlayerCountControl() {
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const mutateTankPerformanceSort = TankPerformanceEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex justify="center" align="center" gap="2">
|
||||
<Text>Player count</Text>
|
||||
<Text>{strings.website.tools.performance.player_count.label}</Text>
|
||||
<Select.Root
|
||||
value={playerCountPeriod}
|
||||
onValueChange={(period) => {
|
||||
@@ -23,13 +25,27 @@ export function PlayerCountControl() {
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value="total">Total</Select.Item>
|
||||
<Select.Item value="d_120">Past 120 days</Select.Item>
|
||||
<Select.Item value="d_90">Past 90 days</Select.Item>
|
||||
<Select.Item value="d_60">Past 60 days</Select.Item>
|
||||
<Select.Item value="d_30">This month</Select.Item>
|
||||
<Select.Item value="d_7">This week</Select.Item>
|
||||
<Select.Item value="d_1">Yesterday</Select.Item>
|
||||
<Select.Item value="total">
|
||||
{strings.common.player_count_period.total}
|
||||
</Select.Item>
|
||||
<Select.Item value="d_120">
|
||||
{strings.common.player_count_period.d_120}
|
||||
</Select.Item>
|
||||
<Select.Item value="d_90">
|
||||
{strings.common.player_count_period.d_90}
|
||||
</Select.Item>
|
||||
<Select.Item value="d_60">
|
||||
{strings.common.player_count_period.d_60}
|
||||
</Select.Item>
|
||||
<Select.Item value="d_30">
|
||||
{strings.common.player_count_period.d_30}
|
||||
</Select.Item>
|
||||
<Select.Item value="d_7">
|
||||
{strings.common.player_count_period.d_7}
|
||||
</Select.Item>
|
||||
<Select.Item value="d_1">
|
||||
{strings.common.player_count_period.d_1}
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Table } from '@radix-ui/themes';
|
||||
import { memo } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
||||
|
||||
@@ -19,6 +20,7 @@ export const TankRow = memo<TankRowProps>(
|
||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const { locale } = useLocale();
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
@@ -29,13 +31,14 @@ export const TankRow = memo<TankRowProps>(
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{formatCompact(
|
||||
locale,
|
||||
Math.round(ratio * averages.samples[playerCountPeriod]),
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(
|
||||
averages.mu.damage_dealt / averages.mu.battles,
|
||||
).toLocaleString()}
|
||||
).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(
|
||||
@@ -44,7 +47,9 @@ export const TankRow = memo<TankRowProps>(
|
||||
%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString()}
|
||||
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
|
||||
locale,
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
|
||||
@@ -67,7 +72,7 @@ export const TankRow = memo<TankRowProps>(
|
||||
<Table.Cell align="center">
|
||||
{Math.round(
|
||||
averages.mu.damage_received / averages.mu.battles,
|
||||
).toLocaleString()}
|
||||
).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
(a, b) =>
|
||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots),
|
||||
);
|
||||
case 'capturePoints':
|
||||
case 'capture_points':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
@@ -65,14 +65,14 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
(a.mu.damage_dealt / a.mu.battles -
|
||||
b.mu.damage_dealt / b.mu.battles),
|
||||
);
|
||||
case 'damageRatio':
|
||||
case 'damage_ratio':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_dealt / a.mu.damage_received -
|
||||
b.mu.damage_dealt / b.mu.damage_received),
|
||||
);
|
||||
case 'damageTaken':
|
||||
case 'damage_taken':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Table } from '@radix-ui/themes';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||
|
||||
@@ -25,6 +26,7 @@ export const Total = memo<TotalProps>(
|
||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const { strings, locale } = useLocale();
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const sum = useCallback(
|
||||
(slice: (tank: BlitzkitStats) => number) => {
|
||||
@@ -54,20 +56,23 @@ export const Total = memo<TotalProps>(
|
||||
return (
|
||||
<Table.Row>
|
||||
<StickyRowHeaderCell style={{ overflow: 'hidden', display: 'flex' }}>
|
||||
Total
|
||||
{strings.website.tools.performance.table.tanks.total}
|
||||
</StickyRowHeaderCell>
|
||||
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{formatCompact(ratio * averageDefinitions.samples[playerCountPeriod])}
|
||||
{formatCompact(
|
||||
locale,
|
||||
ratio * averageDefinitions.samples[playerCountPeriod],
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(damage).toLocaleString()}
|
||||
{Math.round(damage).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(survival * 100).toFixed(1)}%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(xp).toLocaleString()}
|
||||
{Math.round(xp).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
|
||||
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
|
||||
@@ -76,7 +81,7 @@ export const Total = memo<TotalProps>(
|
||||
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
|
||||
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(damageReceived).toLocaleString()}
|
||||
{Math.round(damageReceived).toLocaleString(locale)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { asset, TankType, type TankDefinition } from '@blitzkit/core';
|
||||
import { Flex, Link, Text, type TextProps } from '@radix-ui/themes';
|
||||
import {
|
||||
asset,
|
||||
fetchTankDefinitions,
|
||||
TankType,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { Flex, Text, type TextProps } from '@radix-ui/themes';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { forwardRef, type ReactNode } from 'react';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
||||
import { classIcons } from '../ClassIcon';
|
||||
import { LinkI18n } from '../LinkI18n';
|
||||
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants';
|
||||
import './index.css';
|
||||
|
||||
@@ -14,6 +21,8 @@ type TankCardProps = TextProps & {
|
||||
noLink?: boolean;
|
||||
};
|
||||
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
|
||||
export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
(
|
||||
{
|
||||
@@ -26,6 +35,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
}: TankCardProps,
|
||||
ref,
|
||||
) => {
|
||||
const { unwrap, locale } = useLocale();
|
||||
const provideLink = !noLink && onSelect === undefined;
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const Icon = classIcons[tank.class];
|
||||
@@ -46,10 +56,9 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
onClick={() => {
|
||||
onSelect?.(tank);
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.recentlyViewed = uniq([
|
||||
tank.id,
|
||||
...draft.recentlyViewed,
|
||||
]).slice(0, MAX_RECENTLY_VIEWED);
|
||||
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
|
||||
.filter((id) => id in tankDefinitions.tanks)
|
||||
.slice(0, MAX_RECENTLY_VIEWED);
|
||||
});
|
||||
}}
|
||||
className="tank-search-card"
|
||||
@@ -60,7 +69,8 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Link
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
className="link"
|
||||
underline="hover"
|
||||
href={provideLink ? `/tools/tankopedia/${tank.id}` : '#'}
|
||||
@@ -69,7 +79,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
alt={unwrap(tank.name)}
|
||||
src={asset(`icons/tanks/blitzkit/${tank.id}.webp`)}
|
||||
className="image"
|
||||
draggable={false}
|
||||
@@ -85,7 +95,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
>
|
||||
<Icon className="class-icon" />
|
||||
<Text align="center" className="name">
|
||||
{tank.name}
|
||||
{unwrap(tank.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -102,7 +112,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||
{discriminator}
|
||||
</Text>
|
||||
)}
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type TankDefinition, tankIcon, TankType } from '@blitzkit/core';
|
||||
import { Link } from '@radix-ui/themes';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { classIcons } from './ClassIcon';
|
||||
import { ExperimentIcon } from './ExperimentIcon';
|
||||
import { LinkI18n } from './LinkI18n';
|
||||
import { StickyRowHeaderCell } from './StickyRowHeaderCell';
|
||||
|
||||
interface TankRowHeaderCellProps {
|
||||
@@ -9,6 +10,7 @@ interface TankRowHeaderCellProps {
|
||||
}
|
||||
|
||||
export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
|
||||
const { locale, unwrap } = useLocale();
|
||||
const Icon = classIcons[tank.class];
|
||||
|
||||
return (
|
||||
@@ -16,9 +18,13 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
|
||||
width={{ initial: '144px', sm: '240px' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<Link href={`/tools/tankopedia/${tank.id}`} tabIndex={-1}>
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
href={`/tools/tankopedia/${tank.id}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
alt={unwrap(tank.name)}
|
||||
draggable={false}
|
||||
src={tankIcon(tank.id)}
|
||||
style={{
|
||||
@@ -28,9 +34,10 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
|
||||
<Link
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
color={
|
||||
tank.type === TankType.COLLECTOR
|
||||
? 'blue'
|
||||
@@ -63,8 +70,8 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
|
||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||
)}
|
||||
|
||||
{tank.name}
|
||||
</Link>
|
||||
{unwrap(tank.name)}
|
||||
</LinkI18n>
|
||||
</StickyRowHeaderCell>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
||||
|
||||
interface NoResultsProps {
|
||||
@@ -6,10 +7,12 @@ interface NoResultsProps {
|
||||
}
|
||||
|
||||
export function NoResults({ type = 'filters' }: NoResultsProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" align="center" justify="center">
|
||||
<Text color="gray">
|
||||
No tanks found.{' '}
|
||||
{strings.website.common.tank_search.no_tanks_found.body}{' '}
|
||||
<Link
|
||||
href="#"
|
||||
underline="always"
|
||||
@@ -18,9 +21,10 @@ export function NoResults({ type = 'filters' }: NoResultsProps) {
|
||||
$tankFilters.set(initialTankFilters);
|
||||
}}
|
||||
>
|
||||
{type === 'filters' ? 'Try clearing filters' : 'Try searching again'}
|
||||
{type === 'filters'
|
||||
? strings.website.common.tank_search.no_tanks_found.clear_filters
|
||||
: strings.website.common.tank_search.no_tanks_found.search_again}
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { type TankDefinition } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Link, TextField } from '@radix-ui/themes';
|
||||
import { Button, TextField } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import { LinkI18n } from '../../LinkI18n';
|
||||
|
||||
interface QuickLinkProps {
|
||||
topResult?: TankDefinition;
|
||||
@@ -10,16 +12,17 @@ interface QuickLinkProps {
|
||||
|
||||
export function QuickLink({ topResult }: QuickLinkProps) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const { locale, unwrap } = useLocale();
|
||||
|
||||
if (!tankFilters.search || !topResult || tankFilters.searching) return null;
|
||||
|
||||
return (
|
||||
<TextField.Slot>
|
||||
<Link href={`/tools/tankopedia/${topResult.id}`}>
|
||||
<LinkI18n locale={locale} href={`/tools/tankopedia/${topResult.id}`}>
|
||||
<Button variant="ghost">
|
||||
{topResult.name} <CaretRightIcon />
|
||||
{unwrap(topResult.name)} <CaretRightIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
</TextField.Slot>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Flex, Separator, Text } from '@radix-ui/themes';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||
@@ -15,7 +16,9 @@ export function RecentlyViewed() {
|
||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||
const filters = useStore($tankFilters);
|
||||
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
||||
const recentlyViewed = tankopediaPersistentStore.getState().recentlyViewed;
|
||||
const recentlyViewed = tankopediaPersistentStore
|
||||
.getState()
|
||||
.recentlyViewed.filter((id) => id in tankDefinitions.tanks);
|
||||
const sort = useStore($tankopediaSort);
|
||||
const hasFilters = useMemo(
|
||||
() =>
|
||||
@@ -27,6 +30,7 @@ export function RecentlyViewed() {
|
||||
}),
|
||||
[filters],
|
||||
);
|
||||
const { strings } = useLocale();
|
||||
|
||||
if (recentlyViewed.length === 0 || sort.by !== 'meta.none' || hasFilters) {
|
||||
return null;
|
||||
@@ -35,7 +39,7 @@ export function RecentlyViewed() {
|
||||
return (
|
||||
<Flex direction="column" gap="2" mt="2" mb="6">
|
||||
<Text color="gray" align="center">
|
||||
Recently viewed
|
||||
{strings.website.common.tank_search.recent}
|
||||
</Text>
|
||||
<TankCardWrapper>
|
||||
{recentlyViewed.map((id) => (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Spinner, TextField } from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { type KeyboardEventHandler, useCallback, useRef } from 'react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import { QuickLink } from './QuickLink';
|
||||
import { Sort } from './Sort';
|
||||
@@ -14,6 +15,7 @@ interface SearchBarProps {
|
||||
}
|
||||
|
||||
export function SearchBar({ topResult, onSelect }: SearchBarProps) {
|
||||
const { strings } = useLocale();
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const performSearch = useCallback(
|
||||
@@ -67,7 +69,7 @@ export function SearchBar({ topResult, onSelect }: SearchBarProps) {
|
||||
defaultValue={tankFilters.search}
|
||||
style={{ flex: 1 }}
|
||||
ref={input}
|
||||
placeholder="Search tanks..."
|
||||
placeholder={strings.website.common.tank_search.search_bar_hint}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
|
||||
@@ -1,308 +1,122 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||
import { Button, DropdownMenu } from '@radix-ui/themes';
|
||||
import type { TankopediaSortDirection } from '../../../stores/tankopediaPersistent';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import type {
|
||||
TankopediaSortBy,
|
||||
TankopediaSortDirection,
|
||||
} from '../../../stores/tankopediaPersistent';
|
||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||
|
||||
interface ItemProps {
|
||||
by: TankopediaSortBy;
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
by: TankopediaSortBy;
|
||||
}
|
||||
|
||||
function Item({ by }: ItemProps) {
|
||||
const { strings } = useLocale();
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === by}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', by);
|
||||
}}
|
||||
>
|
||||
{strings.website.common.tank_search.sort[by]}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sort() {
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="surface" color="gray">
|
||||
Sort
|
||||
{strings.website.common.tank_search.sort_dropdown.label}
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'meta.none'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'meta.none';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'meta.none');
|
||||
}}
|
||||
>
|
||||
No sort
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.Label>
|
||||
{strings.website.common.tank_search.sort_dropdown.groups.sort.label}
|
||||
</DropdownMenu.Label>
|
||||
|
||||
<DropdownMenu.Label>Sort by</DropdownMenu.Label>
|
||||
<Item by="meta.none" />
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>Fire</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger>
|
||||
{strings.website.common.tank_search.sort_dropdown.groups.sort.fire}
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.dpm'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'fire.dpm';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'fire.dpm');
|
||||
}}
|
||||
>
|
||||
Standard DPM
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.dpmPremium'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'fire.dpmPremium';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'fire.dpmPremium');
|
||||
}}
|
||||
>
|
||||
Premium DPM
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.reload'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'fire.reload';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'fire.reload');
|
||||
}}
|
||||
>
|
||||
Reload
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.caliber'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'fire.caliber';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'fire.caliber');
|
||||
}}
|
||||
>
|
||||
Caliber
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.standardPenetration'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'fire.standardPenetration';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'fire.standardPenetration');
|
||||
}}
|
||||
>
|
||||
Standard penetration
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.premiumPenetration'}
|
||||
onClick={() => {
|
||||
// mutateTankopediaSort((draft) => {
|
||||
// draft.by = 'fire.premiumPenetration';
|
||||
// });
|
||||
$tankopediaSort.setKey('by', 'fire.premiumPenetration');
|
||||
}}
|
||||
>
|
||||
Premium penetration
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.damage'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.damage');
|
||||
}}
|
||||
>
|
||||
Damage
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.moduleDamage'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.moduleDamage');
|
||||
}}
|
||||
>
|
||||
Module damage
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.shellVelocity'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.shellVelocity');
|
||||
}}
|
||||
>
|
||||
Shell velocity
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.shellRange'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.shellRange');
|
||||
}}
|
||||
>
|
||||
Shell range
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.shellCapacity'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.shellCapacity');
|
||||
}}
|
||||
>
|
||||
Shell capacity
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.aimTime'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.aimTime');
|
||||
}}
|
||||
>
|
||||
Aim time
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.dispersionStill'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.dispersionStill');
|
||||
}}
|
||||
>
|
||||
Dispersion still
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.dispersionMoving'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.dispersionMoving');
|
||||
}}
|
||||
>
|
||||
Dispersion moving
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.gunDepression'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.gunDepression');
|
||||
}}
|
||||
>
|
||||
Gun depression
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'fire.gunElevation'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'fire.gunElevation');
|
||||
}}
|
||||
>
|
||||
Gun elevation
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<Item by="fire.dpm" />
|
||||
<Item by="fire.dpmPremium" />
|
||||
<Item by="fire.reload" />
|
||||
<Item by="fire.caliber" />
|
||||
<Item by="fire.standardPenetration" />
|
||||
<Item by="fire.premiumPenetration" />
|
||||
<Item by="fire.damage" />
|
||||
<Item by="fire.moduleDamage" />
|
||||
<Item by="fire.shellVelocity" />
|
||||
<Item by="fire.shellRange" />
|
||||
<Item by="fire.shellCapacity" />
|
||||
<Item by="fire.aimTime" />
|
||||
<Item by="fire.dispersionStill" />
|
||||
<Item by="fire.dispersionMoving" />
|
||||
<Item by="fire.gunDepression" />
|
||||
<Item by="fire.gunElevation" />
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>Maneuverability</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger>
|
||||
{
|
||||
strings.website.common.tank_search.sort_dropdown.groups.sort
|
||||
.maneuverability
|
||||
}
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'maneuverability.forwardsSpeed'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'maneuverability.forwardsSpeed');
|
||||
}}
|
||||
>
|
||||
Forwards speed
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'maneuverability.backwardsSpeed'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'maneuverability.backwardsSpeed');
|
||||
}}
|
||||
>
|
||||
Backwards speed
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'maneuverability.power'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'maneuverability.power');
|
||||
}}
|
||||
>
|
||||
Power
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'maneuverability.powerToWeight'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'maneuverability.powerToWeight');
|
||||
}}
|
||||
>
|
||||
Power to weight
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'maneuverability.weight'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'maneuverability.weight');
|
||||
}}
|
||||
>
|
||||
Weight
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'maneuverability.traverseSpeed'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'maneuverability.traverseSpeed');
|
||||
}}
|
||||
>
|
||||
Traverse speed
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<Item by="maneuverability.forwardsSpeed" />
|
||||
<Item by="maneuverability.backwardsSpeed" />
|
||||
<Item by="maneuverability.power" />
|
||||
<Item by="maneuverability.powerToWeight" />
|
||||
<Item by="maneuverability.weight" />
|
||||
<Item by="maneuverability.traverseSpeed" />
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>Survivability</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger>
|
||||
{
|
||||
strings.website.common.tank_search.sort_dropdown.groups.sort
|
||||
.survivability
|
||||
}
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.health'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'survivability.health');
|
||||
}}
|
||||
>
|
||||
Health
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.viewRange'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'survivability.viewRange');
|
||||
}}
|
||||
>
|
||||
View range
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.camouflageStill'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'survivability.camouflageStill');
|
||||
}}
|
||||
>
|
||||
Camouflage
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.camouflageMoving'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'survivability.camouflageMoving');
|
||||
}}
|
||||
>
|
||||
Camouflage moving
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.camouflageShooting'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey(
|
||||
'by',
|
||||
'survivability.camouflageShooting',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Camouflage shooting
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.volume'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'survivability.volume');
|
||||
}}
|
||||
>
|
||||
Volume
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === 'survivability.length'}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', 'survivability.length');
|
||||
}}
|
||||
>
|
||||
Length
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<Item by="survivability.health" />
|
||||
<Item by="survivability.viewRange" />
|
||||
<Item by="survivability.camouflageStill" />
|
||||
<Item by="survivability.camouflageMoving" />
|
||||
<Item by="survivability.camouflageShooting" />
|
||||
<Item by="survivability.volume" />
|
||||
<Item by="survivability.length" />
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
<DropdownMenu.Label>Direction</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>
|
||||
{
|
||||
strings.website.common.tank_search.sort_dropdown.groups.direction
|
||||
.label
|
||||
}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaSort.direction}
|
||||
onValueChange={(value) => {
|
||||
@@ -313,10 +127,16 @@ export function Sort() {
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="ascending">
|
||||
Ascending
|
||||
{
|
||||
strings.website.common.tank_search.sort_dropdown.groups.direction
|
||||
.ascending
|
||||
}
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="descending">
|
||||
Descending
|
||||
{
|
||||
strings.website.common.tank_search.sort_dropdown.groups.direction
|
||||
.descending
|
||||
}
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
|
||||
@@ -2,9 +2,11 @@ import {
|
||||
metaSortTank,
|
||||
normalizeBoundingBox,
|
||||
resolveDpm,
|
||||
SEARCH_KEYS,
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
@@ -16,12 +18,10 @@ import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions'
|
||||
import { awaitableTankNames } from '../../core/awaitables/tankNames';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
||||
import {
|
||||
SORT_NAMES,
|
||||
SORT_UNITS,
|
||||
} from '../../stores/tankopediaPersistent/constants';
|
||||
import { SORT_UNITS } from '../../stores/tankopediaPersistent/constants';
|
||||
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
||||
import { ExperimentIcon } from '../ExperimentIcon';
|
||||
import { TankSearchCard } from './components/Card';
|
||||
@@ -52,6 +52,7 @@ const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
|
||||
|
||||
export const TankSearch = memo<TankSearchProps>(
|
||||
({ compact, onSelect, onSelectAll, ...props }) => {
|
||||
const { strings, locale } = useLocale();
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||
const tankFilters = useStore($tankFilters);
|
||||
@@ -410,7 +411,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
: sorted.reverse();
|
||||
} else {
|
||||
const searchedRaw = fuzzysort.go(tankFilters.search, tankNames, {
|
||||
keys: ['searchableName', 'searchableNameDeburr', 'camouflages'],
|
||||
keys: SEARCH_KEYS,
|
||||
});
|
||||
const searchedTanks = searchedRaw.map(
|
||||
(result) => tankDefinitions.tanks[result.obj.id],
|
||||
@@ -438,8 +439,11 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
<Flex mt="2" gap="1" align="center" justify="center" direction="column">
|
||||
<Flex gap="2">
|
||||
<Text color="gray">
|
||||
{tanksFiltered.length} tank
|
||||
{tanksFiltered.length === 1 ? '' : 's'}
|
||||
{tanksFiltered.length === 1
|
||||
? strings.website.common.tank_search.count_singular
|
||||
: literals(strings.website.common.tank_search.count_plural, [
|
||||
`${tanksFiltered.length.toLocaleString(locale)}`,
|
||||
])}
|
||||
</Text>
|
||||
|
||||
{onSelectAll && (
|
||||
@@ -453,18 +457,22 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
draft.recentlyViewed = uniq([
|
||||
...tanksFiltered.map(({ id }) => id),
|
||||
...draft.recentlyViewed,
|
||||
]).slice(0, MAX_RECENTLY_VIEWED);
|
||||
])
|
||||
.filter((id) => id in tankDefinitions.tanks)
|
||||
.slice(0, MAX_RECENTLY_VIEWED);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Select all
|
||||
{strings.website.common.tank_search.select_all}
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{tankopediaSort.by !== 'meta.none' && (
|
||||
<Text color="gray">
|
||||
Sorting by {SORT_NAMES[tankopediaSort.by]}
|
||||
{literals(strings.website.common.tank_search.sorting_by, [
|
||||
strings.website.common.tank_search.sort[tankopediaSort.by],
|
||||
])}
|
||||
{SORT_UNITS[tankopediaSort.by] === undefined
|
||||
? ''
|
||||
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
||||
@@ -480,8 +488,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>
|
||||
Tanks in testing are subject to change and may not represent the
|
||||
final product.
|
||||
{strings.website.common.warnings.test}
|
||||
</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assertSecret } from '@blitzkit/core';
|
||||
import { assertSecret, fetchTankDefinitions } from '@blitzkit/core';
|
||||
import { MinusCircledIcon } from '@radix-ui/react-icons';
|
||||
import { Callout, Flex } from '@radix-ui/themes';
|
||||
import { uniq } from 'lodash-es';
|
||||
@@ -9,6 +9,8 @@ import { AesonPlug } from '../AesonPlug';
|
||||
import { ExperimentIcon } from '../ExperimentIcon';
|
||||
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants';
|
||||
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
|
||||
export function CalloutsSection() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
@@ -17,10 +19,9 @@ export function CalloutsSection() {
|
||||
|
||||
useEffect(() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed]).slice(
|
||||
0,
|
||||
MAX_RECENTLY_VIEWED,
|
||||
);
|
||||
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
|
||||
.filter((id) => id in tankDefinitions.tanks)
|
||||
.slice(0, MAX_RECENTLY_VIEWED);
|
||||
});
|
||||
}, [tank]);
|
||||
|
||||
|
||||
+21
-7
@@ -1,6 +1,7 @@
|
||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Flex, Heading, SegmentedControl, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
@@ -13,6 +14,7 @@ export function CharacteristicsHeading() {
|
||||
);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -20,9 +22,13 @@ export function CharacteristicsHeading() {
|
||||
gap="2"
|
||||
direction="column"
|
||||
>
|
||||
<Heading mb="2">Characteristics</Heading>
|
||||
<Heading mb="2">
|
||||
{strings.website.tools.tankopedia.characteristics.title}
|
||||
</Heading>
|
||||
|
||||
<Text color="gray">Compare against</Text>
|
||||
<Text color="gray">
|
||||
{strings.website.tools.tankopedia.characteristics.compare.label}
|
||||
</Text>
|
||||
<SegmentedControl.Root
|
||||
size={{ initial: '1', md: '2' }}
|
||||
value={`${relativeAgainst}`}
|
||||
@@ -33,14 +39,22 @@ export function CharacteristicsHeading() {
|
||||
}}
|
||||
>
|
||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Class}`}>
|
||||
Tier {TIER_ROMAN_NUMERALS[tank.tier]}{' '}
|
||||
{strings.common.tank_class_short_plural_lowercase[tank.class]}
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.characteristics.compare.class,
|
||||
[
|
||||
TIER_ROMAN_NUMERALS[tank.tier],
|
||||
strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||
],
|
||||
)}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
||||
All of tier {TIER_ROMAN_NUMERALS[tank.tier]}
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.characteristics.compare.tier,
|
||||
[TIER_ROMAN_NUMERALS[tank.tier]],
|
||||
)}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
||||
All tanks
|
||||
{strings.website.tools.tankopedia.characteristics.compare.all}
|
||||
</SegmentedControl.Item>
|
||||
</SegmentedControl.Root>
|
||||
</Flex>
|
||||
|
||||
+35
-11
@@ -1,6 +1,6 @@
|
||||
import { createDefaultSkills } from '@blitzkit/core';
|
||||
import { Flex, Progress, Text } from '@radix-ui/themes';
|
||||
import { useMemo, type ComponentProps } from 'react';
|
||||
import { useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { tankToDuelMember } from '../../../../../../core/blitzkit/tankToDuelMember';
|
||||
import { useDelta } from '../../../../../../hooks/useDelta';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
@@ -19,13 +20,18 @@ import {
|
||||
} from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { Info, type InfoProps } from './Info';
|
||||
|
||||
interface InfoWithDeltaProps extends InfoProps {
|
||||
type InfoWithDeltaProps = Omit<InfoProps, 'name'> & {
|
||||
stats: TankCharacteristics;
|
||||
value:
|
||||
| keyof TankCharacteristics
|
||||
| ((tank: TankCharacteristics) => number | undefined);
|
||||
noRanking?: boolean;
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
value: keyof TankCharacteristics;
|
||||
}
|
||||
| {
|
||||
value: (tank: TankCharacteristics) => number | undefined;
|
||||
name: ReactNode;
|
||||
}
|
||||
);
|
||||
|
||||
const [
|
||||
tankDefinitions,
|
||||
@@ -42,18 +48,20 @@ const [
|
||||
]);
|
||||
|
||||
export function InfoWithDelta({
|
||||
value,
|
||||
stats,
|
||||
indent,
|
||||
noRanking,
|
||||
deltaType,
|
||||
...props
|
||||
}: InfoWithDeltaProps) {
|
||||
const { strings } = useLocale();
|
||||
const relativeAgainst = TankopediaEphemeral.use(
|
||||
(state) => state.relativeAgainst,
|
||||
);
|
||||
const uhWhatDoICallThisVariable =
|
||||
typeof value === 'function' ? value(stats)! : (stats[value] as number);
|
||||
typeof props.value === 'function'
|
||||
? props.value(stats)!
|
||||
: (stats[props.value] as number);
|
||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||
const others = useMemo(() => {
|
||||
@@ -102,14 +110,18 @@ export function InfoWithDelta({
|
||||
})
|
||||
.filter((tank) => {
|
||||
const othersValue =
|
||||
typeof value === 'function' ? value(tank)! : (tank[value] as number);
|
||||
typeof props.value === 'function'
|
||||
? props.value(tank)!
|
||||
: (tank[props.value] as number);
|
||||
|
||||
return othersValue !== undefined;
|
||||
});
|
||||
}, [relativeAgainst]);
|
||||
const betterTanks = others.filter((tank) => {
|
||||
const othersValue =
|
||||
typeof value === 'function' ? value(tank)! : (tank[value] as number);
|
||||
typeof props.value === 'function'
|
||||
? props.value(tank)!
|
||||
: (tank[props.value] as number);
|
||||
|
||||
if (othersValue === undefined) return false;
|
||||
return deltaType === 'lowerIsBetter'
|
||||
@@ -126,7 +138,19 @@ export function InfoWithDelta({
|
||||
|
||||
return (
|
||||
<Flex direction="column">
|
||||
<Info deltaType={deltaType} indent={indent} {...props} delta={delta}>
|
||||
<Info
|
||||
deltaType={deltaType}
|
||||
indent={indent}
|
||||
{...props}
|
||||
name={
|
||||
'name' in props
|
||||
? props.name
|
||||
: strings.website.tools.tankopedia.characteristics.values[
|
||||
props.value
|
||||
]
|
||||
}
|
||||
delta={delta}
|
||||
>
|
||||
{uhWhatDoICallThisVariable}
|
||||
</Info>
|
||||
|
||||
|
||||
+154
-125
@@ -3,10 +3,10 @@ import {
|
||||
coefficient,
|
||||
CREW_MEMBER_NAMES,
|
||||
CrewType,
|
||||
GUN_TYPE_NAMES,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { AccessibilityIcon, InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Flex,
|
||||
@@ -27,6 +27,7 @@ import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/pr
|
||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
@@ -39,6 +40,7 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function Characteristics() {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||
@@ -105,6 +107,7 @@ export function Characteristics() {
|
||||
const hasSupercharger = useEquipment(107);
|
||||
const hasCalibratedShells = useEquipment(103);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
@@ -127,9 +130,10 @@ export function Characteristics() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
});
|
||||
}, [hasImprovedVerticalStabilizer]);
|
||||
}, [hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -142,11 +146,13 @@ export function Characteristics() {
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
<Heading size="5">Firepower</Heading>
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.firepower.title}
|
||||
</Heading>
|
||||
|
||||
<Flex>
|
||||
{gun.gun_type!.value.base.shells.map((thisShell, shellIndex) => (
|
||||
<Tooltip content={thisShell.name} key={thisShell.id}>
|
||||
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
||||
<IconButton
|
||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
||||
variant="soft"
|
||||
@@ -172,7 +178,7 @@ export function Characteristics() {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={thisShell.name}
|
||||
alt={unwrap(thisShell.name)}
|
||||
width={16}
|
||||
height={16}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
@@ -182,10 +188,15 @@ export function Characteristics() {
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Info name="Gun type">{GUN_TYPE_NAMES[gun.gun_type!.$case]}</Info>
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.gunType
|
||||
}
|
||||
>
|
||||
{strings.common.gun_types[gun.gun_type!.$case]}
|
||||
</Info>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="DPM"
|
||||
decimals={0}
|
||||
unit="hp / min"
|
||||
value="dpm"
|
||||
@@ -195,26 +206,41 @@ export function Characteristics() {
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
name="Effective at 60s"
|
||||
unit="hp / min"
|
||||
value="dpmEffective"
|
||||
/>
|
||||
)}
|
||||
{gun.gun_type!.$case !== 'regular' && (
|
||||
<InfoWithDelta stats={stats} name="Shells" value="shells" />
|
||||
<InfoWithDelta stats={stats} value="shells" />
|
||||
)}
|
||||
{gun.gun_type!.$case === 'auto_reloader' ? (
|
||||
<>
|
||||
<Info indent name="Most optimal shell index">
|
||||
<Info
|
||||
indent
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.mostOptimalShellIndex
|
||||
}
|
||||
>
|
||||
{stats.mostOptimalShellIndex! + 1}
|
||||
</Info>
|
||||
<Info name="Shell reloads" unit="s" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.shellReloads
|
||||
}
|
||||
unit="s"
|
||||
/>
|
||||
{stats.shellReloads!.map((reload, index) => (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={index}
|
||||
indent
|
||||
name={`Shell ${index + 1}`}
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.shell_index,
|
||||
[`${index + 1}`],
|
||||
)}
|
||||
decimals={2}
|
||||
deltaType="lowerIsBetter"
|
||||
noRanking
|
||||
@@ -226,7 +252,6 @@ export function Characteristics() {
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
name="Reload"
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="shellReload"
|
||||
@@ -238,7 +263,6 @@ export function Characteristics() {
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={2}
|
||||
name="Intra-clip"
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="intraClip"
|
||||
@@ -247,7 +271,6 @@ export function Characteristics() {
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
name="Penetration"
|
||||
unit="mm"
|
||||
value="penetration"
|
||||
/>
|
||||
@@ -257,7 +280,11 @@ export function Characteristics() {
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={0}
|
||||
name={`At ${penetrationDistance}m`}
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.at_distance,
|
||||
[`${penetrationDistance}`],
|
||||
)}
|
||||
noRanking
|
||||
value={() =>
|
||||
lerp(
|
||||
@@ -270,7 +297,12 @@ export function Characteristics() {
|
||||
}
|
||||
/>
|
||||
<Flex align="center" gap="2" style={{ paddingLeft: 24 }}>
|
||||
<Text color="gray">Distance</Text>
|
||||
<Text color="gray">
|
||||
{
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.at_distance_slider_label
|
||||
}
|
||||
</Text>
|
||||
<Slider
|
||||
key={penetrationDistance}
|
||||
min={0}
|
||||
@@ -308,17 +340,10 @@ export function Characteristics() {
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Damage"
|
||||
unit="hp"
|
||||
decimals={0}
|
||||
value="damage"
|
||||
/>
|
||||
<InfoWithDelta stats={stats} unit="hp" decimals={0} value="damage" />
|
||||
{gun.gun_type!.$case !== 'regular' && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Clipping potential"
|
||||
indent
|
||||
decimals={0}
|
||||
value="clipDamage"
|
||||
@@ -326,7 +351,6 @@ export function Characteristics() {
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Module damage"
|
||||
unit="hp"
|
||||
decimals={0}
|
||||
value="moduleDamage"
|
||||
@@ -334,23 +358,15 @@ export function Characteristics() {
|
||||
{isExplosive(shell.type) && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Splash radius"
|
||||
unit="m"
|
||||
noRanking
|
||||
decimals={0}
|
||||
value="explosionRadius"
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta stats={stats} decimals={0} unit="mm" value="caliber" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Caliber"
|
||||
decimals={0}
|
||||
unit="mm"
|
||||
value="caliber"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Normalization"
|
||||
decimals={0}
|
||||
unit="°"
|
||||
noRanking
|
||||
@@ -359,7 +375,6 @@ export function Characteristics() {
|
||||
{!isExplosive(shell.type) && (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Ricochet"
|
||||
noRanking
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -369,39 +384,35 @@ export function Characteristics() {
|
||||
)}
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Shell velocity"
|
||||
unit="m/s"
|
||||
decimals={0}
|
||||
value="shellVelocity"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Shell range"
|
||||
unit="m/s"
|
||||
decimals={0}
|
||||
value="shellRange"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Shell capacity"
|
||||
unit="m/s"
|
||||
decimals={0}
|
||||
value="shellCapacity"
|
||||
/>
|
||||
<InfoWithDelta stats={stats} decimals={0} value="shellCapacity" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
name="Aim time"
|
||||
unit="s"
|
||||
deltaType="lowerIsBetter"
|
||||
value="aimTime"
|
||||
/>
|
||||
<Info name="Dispersion at 100m" />
|
||||
<Info
|
||||
name={literals(
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.dispersion_at_distance,
|
||||
['100'],
|
||||
)}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={3}
|
||||
indent
|
||||
name="Still"
|
||||
unit="m"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersion"
|
||||
@@ -411,7 +422,6 @@ export function Characteristics() {
|
||||
prefix="+"
|
||||
decimals={3}
|
||||
indent
|
||||
name="Moving"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionMoving"
|
||||
/>
|
||||
@@ -420,7 +430,6 @@ export function Characteristics() {
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
name="Hull traversing"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionHullTraversing"
|
||||
/>
|
||||
@@ -429,7 +438,6 @@ export function Characteristics() {
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
name="Turret traversing"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionTurretTraversing"
|
||||
/>
|
||||
@@ -438,7 +446,6 @@ export function Characteristics() {
|
||||
decimals={3}
|
||||
prefix="+"
|
||||
indent
|
||||
name="After shooting"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionShooting"
|
||||
/>
|
||||
@@ -447,24 +454,26 @@ export function Characteristics() {
|
||||
decimals={1}
|
||||
prefix="x "
|
||||
indent
|
||||
name="Gun damaged"
|
||||
unit="scalar"
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionGunDamaged"
|
||||
/>
|
||||
<Info name="Gun flexibility" unit="°" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.gun_flexibility
|
||||
}
|
||||
unit="°"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="gunDepression"
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Depression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Elevation"
|
||||
value="gunElevation"
|
||||
/>
|
||||
{gunModelDefinition.pitch.front && (
|
||||
@@ -473,14 +482,12 @@ export function Characteristics() {
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Frontal depression"
|
||||
value="gunFrontalDepression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Frontal elevation"
|
||||
value="gunFrontalElevation"
|
||||
/>
|
||||
</>
|
||||
@@ -491,14 +498,12 @@ export function Characteristics() {
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Rear depression"
|
||||
value="gunRearDepression"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Rear elevation"
|
||||
value="gunRearElevation"
|
||||
/>
|
||||
</>
|
||||
@@ -509,14 +514,12 @@ export function Characteristics() {
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Azimuth left"
|
||||
value="azimuthLeft"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="Azimuth right"
|
||||
value="azimuthRight"
|
||||
/>
|
||||
</>
|
||||
@@ -525,7 +528,9 @@ export function Characteristics() {
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex gap="2" align="center">
|
||||
<Heading size="5">Crew training</Heading>
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.crew.title}
|
||||
</Heading>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
@@ -537,20 +542,13 @@ export function Characteristics() {
|
||||
<Popover.Content>
|
||||
<Flex gap="1" align="center">
|
||||
<AccessibilityIcon />
|
||||
<Text>
|
||||
represents the other roles a crew member can take.
|
||||
</Text>
|
||||
<Text>{strings.website.tools.tankopedia.crew.info}</Text>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Flex>
|
||||
|
||||
<InfoWithDelta
|
||||
value="crewCount"
|
||||
stats={stats}
|
||||
name="Crew count"
|
||||
decimals={0}
|
||||
/>
|
||||
<InfoWithDelta value="crewCount" stats={stats} decimals={0} />
|
||||
|
||||
{tank.crew.map((member) => {
|
||||
return (
|
||||
@@ -558,8 +556,16 @@ export function Characteristics() {
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
key={`${member.type}-root`}
|
||||
name={`${CREW_MEMBER_NAMES[member.type]}${
|
||||
member.count > 1 ? ` x ${member.count}` : ''
|
||||
name={`${
|
||||
strings.website.tools.tankopedia.crew[
|
||||
CREW_MEMBER_NAMES[member.type]
|
||||
]
|
||||
}${
|
||||
member.count > 1
|
||||
? ` ${literals(strings.common.units.x, [
|
||||
`${member.count}`,
|
||||
])}`
|
||||
: ''
|
||||
}`}
|
||||
unit="%"
|
||||
decimals={0}
|
||||
@@ -584,7 +590,11 @@ export function Characteristics() {
|
||||
<AccessibilityIcon />
|
||||
{member.substitute
|
||||
.map((sub, index) =>
|
||||
index === 0 ? CREW_MEMBER_NAMES[sub] : sub,
|
||||
index === 0
|
||||
? strings.website.tools.tankopedia.crew[
|
||||
CREW_MEMBER_NAMES[sub]
|
||||
]
|
||||
: sub,
|
||||
)
|
||||
.join(', ')}
|
||||
</Flex>
|
||||
@@ -602,52 +612,51 @@ export function Characteristics() {
|
||||
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">Maneuverability</Heading>
|
||||
<Info name="Speed" unit="kph" />
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.maneuverability.title}
|
||||
</Heading>
|
||||
<Info
|
||||
name={strings.website.tools.tankopedia.characteristics.values.speed}
|
||||
unit="kph"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="speedForwards"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
name="Forwards"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="speedBackwards"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
indent
|
||||
name="Backwards"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
name="Engine power"
|
||||
unit="hp"
|
||||
value="enginePower"
|
||||
/>
|
||||
<InfoWithDelta stats={stats} decimals={0} value="enginePower" />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
name="Weight"
|
||||
unit="tn"
|
||||
deltaType="lowerIsBetter"
|
||||
value="weight"
|
||||
/>
|
||||
<Info name="Terrain coefficients" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.terrain_coefficients
|
||||
}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="hardTerrainCoefficient"
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
name="Hard"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
name="Medium"
|
||||
value="mediumTerrainCoefficient"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
@@ -656,9 +665,14 @@ export function Characteristics() {
|
||||
decimals={0}
|
||||
unit="%"
|
||||
indent
|
||||
name="Soft"
|
||||
/>
|
||||
<Info name="Raw terrain coefficients" deltaType="lowerIsBetter" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.raw_terrain_coefficients
|
||||
}
|
||||
deltaType="lowerIsBetter"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value="hardTerrainCoefficientRaw"
|
||||
stats={stats}
|
||||
@@ -666,14 +680,12 @@ export function Characteristics() {
|
||||
unit="%"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
name="Hard"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
name="Medium"
|
||||
deltaType="lowerIsBetter"
|
||||
value="mediumTerrainCoefficientRaw"
|
||||
/>
|
||||
@@ -682,75 +694,77 @@ export function Characteristics() {
|
||||
decimals={2}
|
||||
unit="%"
|
||||
indent
|
||||
name="Soft"
|
||||
deltaType="lowerIsBetter"
|
||||
value="softTerrainCoefficientRaw"
|
||||
/>
|
||||
<Info name="Power to weight ratio" unit="hp/tn" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.power_to_weight_ratio
|
||||
}
|
||||
unit="hp/tn"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="On hard terrain"
|
||||
value="powerToWeightRatioHardTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="On medium terrain"
|
||||
value="powerToWeightRatioMediumTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="On soft terrain"
|
||||
value="powerToWeightRatioSoftTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Turret traverse speed"
|
||||
unit="°/s"
|
||||
decimals={1}
|
||||
value="turretTraverseSpeed"
|
||||
/>
|
||||
<Info name="Hull traverse speed" unit="°/s" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.hull_traverse_speed
|
||||
}
|
||||
unit="°/s"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="On hard terrain"
|
||||
value="hullTraverseHardTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="On medium terrain"
|
||||
value="hullTraverseMediumTerrain"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
indent
|
||||
name="On soft terrain"
|
||||
value="hullTraverseSoftTerrain"
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">Survivability</Heading>
|
||||
<InfoWithDelta
|
||||
value="health"
|
||||
stats={stats}
|
||||
name="Health"
|
||||
unit="hp"
|
||||
decimals={0}
|
||||
/>
|
||||
<Heading size="5">
|
||||
{strings.website.tools.tankopedia.survivability.title}
|
||||
</Heading>
|
||||
<InfoWithDelta value="health" stats={stats} unit="hp" decimals={0} />
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
name="Fire chance"
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.fireChance
|
||||
}
|
||||
unit="%"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={0}
|
||||
@@ -759,48 +773,66 @@ export function Characteristics() {
|
||||
<InfoWithDelta
|
||||
value="viewRange"
|
||||
stats={stats}
|
||||
name="View range"
|
||||
unit="m"
|
||||
decimals={0}
|
||||
/>
|
||||
<Info name="Camouflage" unit="%" />
|
||||
<Info
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values.camouflage
|
||||
}
|
||||
unit="%"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value={(stats) => stats.camouflageStill * 100}
|
||||
stats={stats}
|
||||
indent
|
||||
name="Still"
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageStill
|
||||
}
|
||||
decimals={2}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
value={(stats) => stats.camouflageMoving * 100}
|
||||
stats={stats}
|
||||
indent
|
||||
name="Moving"
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageMoving
|
||||
}
|
||||
decimals={2}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name="Shooting still"
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageShootingStill
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageShootingStill * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name="Shooting moving"
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageShootingMoving
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageShootingMoving * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
name="Caught on fire"
|
||||
name={
|
||||
strings.website.tools.tankopedia.characteristics.values
|
||||
.camouflageCaughtOnFire
|
||||
}
|
||||
decimals={2}
|
||||
value={(stats) => stats.camouflageCaughtOnFire * 100}
|
||||
/>
|
||||
<InfoWithDelta
|
||||
name="Width"
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -808,7 +840,6 @@ export function Characteristics() {
|
||||
value="width"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
name="Height"
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -816,7 +847,6 @@ export function Characteristics() {
|
||||
value="height"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
name="Length"
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
@@ -824,7 +854,6 @@ export function Characteristics() {
|
||||
value="length"
|
||||
/>
|
||||
<InfoWithDelta
|
||||
name="Volume"
|
||||
unit="m"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
|
||||
+10
-4
@@ -10,6 +10,7 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { awaitableConsumableDefinitions } from '../../../../core/awaitables/consumableDefinitions';
|
||||
import { useEquipment } from '../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
import { ConsumablesManager } from '../../../ConsumablesManager';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
@@ -34,12 +35,15 @@ export function Consumables() {
|
||||
);
|
||||
const hasConsumableDeliverySystem = useEquipment(118);
|
||||
const hasHighEndConsumables = useEquipment(101);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Flex gap="4" align="center">
|
||||
<Flex gap="2" align="center">
|
||||
<Heading size="4">Consumables</Heading>
|
||||
<Heading size="4">
|
||||
{strings.website.tools.tankopedia.configuration.consumables.title}
|
||||
</Heading>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
@@ -50,8 +54,10 @@ export function Consumables() {
|
||||
|
||||
<Popover.Content>
|
||||
<Text>
|
||||
<Text color="amber">Yellow consumables</Text> do not affect the
|
||||
statistics.
|
||||
{
|
||||
strings.website.tools.tankopedia.configuration.consumables
|
||||
.info
|
||||
}
|
||||
</Text>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -66,7 +72,7 @@ export function Consumables() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{strings.website.tools.tankopedia.configuration.consumables.clear}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
|
||||
+6
-2
@@ -1,5 +1,6 @@
|
||||
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../core/awaitables/equipmentDefinitions';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
import { EquipmentManager } from '../../../EquipmentManager';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
@@ -14,11 +15,14 @@ export function Equipment() {
|
||||
const equipmentMatrix = Duel.use(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Flex gap="4" align="center">
|
||||
<Heading size="4">Equipment</Heading>
|
||||
<Heading size="4">
|
||||
{strings.website.tools.tankopedia.configuration.equipment.title}
|
||||
</Heading>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="red"
|
||||
@@ -32,7 +36,7 @@ export function Equipment() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{strings.website.tools.tankopedia.configuration.equipment.clear}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
import { GenericTankComponentButton } from '../../../ModuleButtons/GenericTankComponentButton';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
@@ -11,10 +12,13 @@ export function Miscellaneous() {
|
||||
(state) => state.protagonist.cooldownBooster,
|
||||
);
|
||||
const { tank } = Duel.use((state) => state.protagonist);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Heading size="4">Miscellaneous</Heading>
|
||||
<Heading size="4">
|
||||
{strings.website.tools.tankopedia.configuration.miscellaneous.title}
|
||||
</Heading>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
{!tank.fixed_camouflage && (
|
||||
|
||||
+14
-6
@@ -17,11 +17,12 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
import { LinkI18n } from '../../../LinkI18n';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
|
||||
type ModuleDefinition =
|
||||
@@ -63,6 +64,8 @@ function ModuleButton({
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const isTank = unlock.type === ModuleType.VEHICLE;
|
||||
const { locale } = useLocale();
|
||||
|
||||
const button = (
|
||||
<IconButton
|
||||
size="4"
|
||||
@@ -100,7 +103,7 @@ function ModuleButton({
|
||||
gap={isTank ? '1' : undefined}
|
||||
>
|
||||
<Text size="1" color="gray">
|
||||
{formatCompact(unlock.cost.value)}
|
||||
{formatCompact(locale, unlock.cost.value)}
|
||||
</Text>
|
||||
|
||||
<img
|
||||
@@ -132,7 +135,9 @@ function ModuleButton({
|
||||
);
|
||||
|
||||
return isTank ? (
|
||||
<Link href={`/tools/tankopedia/${unlock.id}`}>{button}</Link>
|
||||
<LinkI18n locale={locale} href={`/tools/tankopedia/${unlock.id}`}>
|
||||
{button}
|
||||
</LinkI18n>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
@@ -160,6 +165,7 @@ export function Modules() {
|
||||
const topGun = topTurret.guns.at(-1)!;
|
||||
const topEngine = tank.engines.at(-1)!;
|
||||
const topTrack = tank.tracks.at(-1)!;
|
||||
const { strings } = useLocale();
|
||||
|
||||
function setByUnlock(unlock: Unlock) {
|
||||
mutateDuel((draft) => {
|
||||
@@ -349,7 +355,9 @@ export function Modules() {
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Flex gap="4" align="center">
|
||||
<Heading size="4">Modules</Heading>
|
||||
<Heading size="4">
|
||||
{strings.website.tools.tankopedia.configuration.modules.title}
|
||||
</Heading>
|
||||
|
||||
{hasUpgrades && (
|
||||
<>
|
||||
@@ -367,7 +375,7 @@ export function Modules() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Stock
|
||||
{strings.website.tools.tankopedia.configuration.modules.stock}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -385,7 +393,7 @@ export function Modules() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
{strings.website.tools.tankopedia.configuration.modules.upgrade}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
+6
-2
@@ -1,6 +1,7 @@
|
||||
import { availableProvisions } from '@blitzkit/core';
|
||||
import { Button, Flex, Heading } from '@radix-ui/themes';
|
||||
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
import { ProvisionsManager } from '../../../ProvisionsManager';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
@@ -12,11 +13,14 @@ export function Provisions() {
|
||||
const { tank, gun } = Duel.use((state) => state.protagonist);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Flex gap="4" align="center">
|
||||
<Heading size="4">Provisions</Heading>
|
||||
<Heading size="4">
|
||||
{strings.website.tools.tankopedia.configuration.provisions.title}
|
||||
</Heading>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="red"
|
||||
@@ -26,7 +30,7 @@ export function Provisions() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{strings.website.tools.tankopedia.configuration.provisions.clear}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
|
||||
+8
-10
@@ -4,10 +4,10 @@ import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Kbd,
|
||||
Popover,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import { CrewSkillManager } from '../../../CrewSkillManager';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
@@ -15,12 +15,15 @@ import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
export function Skills() {
|
||||
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
||||
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Flex gap="4" align="center">
|
||||
<Flex gap="2" align="center">
|
||||
<Heading size="4">Crew skills</Heading>
|
||||
<Heading size="4">
|
||||
{strings.website.tools.tankopedia.configuration.skills.title}
|
||||
</Heading>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
@@ -32,12 +35,7 @@ export function Skills() {
|
||||
<Popover.Content>
|
||||
<Flex direction="column" gap="2">
|
||||
<Text>
|
||||
<Text color="amber">Yellow skills</Text> only apply under
|
||||
special circumstances.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Hold <Kbd>Shift</Kbd> to quickly toggle between level 0 and 7.
|
||||
{strings.website.tools.tankopedia.configuration.skills.info}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
@@ -55,7 +53,7 @@ export function Skills() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
{strings.website.tools.tankopedia.configuration.skills.clear}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -67,7 +65,7 @@ export function Skills() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Maximize
|
||||
{strings.website.tools.tankopedia.configuration.skills.maximize}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { Characteristics } from './components/Characteristics';
|
||||
import { CharacteristicsHeading } from './components/Characteristics/components/Heading';
|
||||
import { Consumables } from './components/Consumables';
|
||||
@@ -9,6 +10,8 @@ import { Provisions } from './components/Provisions';
|
||||
import { Skills } from './components/Skills';
|
||||
|
||||
export function CharacteristicsSection() {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex mt="4" px="4" style={{ boxSizing: 'border-box' }} gap="6">
|
||||
<Flex
|
||||
@@ -24,7 +27,9 @@ export function CharacteristicsSection() {
|
||||
maxWidth="21rem"
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
>
|
||||
<Heading>Configuration</Heading>
|
||||
<Heading>
|
||||
{strings.website.tools.tankopedia.configuration.title}
|
||||
</Heading>
|
||||
<Modules />
|
||||
<Equipment />
|
||||
<Provisions />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
|
||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { Duel } from '../../stores/duel';
|
||||
|
||||
const [gameDefinitions, consumableDefinitions, provisionDefinitions] =
|
||||
@@ -13,6 +14,7 @@ const [gameDefinitions, consumableDefinitions, provisionDefinitions] =
|
||||
]);
|
||||
|
||||
export function GameModeSection() {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const roles = Object.entries(tank.roles);
|
||||
|
||||
@@ -20,7 +22,9 @@ export function GameModeSection() {
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="4" align="center">
|
||||
<Heading size="6">Game mode abilities</Heading>
|
||||
<Heading size="6">
|
||||
{strings.website.tools.tankopedia.game_modes.title}
|
||||
</Heading>
|
||||
|
||||
<Flex justify="center" gap="4" wrap="wrap" px="4">
|
||||
{roles.map(([key, value]) => {
|
||||
@@ -67,7 +71,7 @@ export function GameModeSection() {
|
||||
justify="center"
|
||||
>
|
||||
<Text size="4" weight="bold">
|
||||
{gameMode.name}
|
||||
{unwrap(gameMode.name)}
|
||||
</Text>
|
||||
|
||||
<Flex direction="column">
|
||||
@@ -84,9 +88,9 @@ export function GameModeSection() {
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
src={asset(`icons/provisions/${id}.webp`)}
|
||||
alt={provisions.name}
|
||||
alt={unwrap(provisions.name)}
|
||||
/>
|
||||
{provisions.name}
|
||||
{unwrap(provisions.name)}
|
||||
</Flex>
|
||||
</Text>
|
||||
);
|
||||
@@ -104,9 +108,9 @@ export function GameModeSection() {
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
src={asset(`icons/consumables/${id}.webp`)}
|
||||
alt={consumable.name}
|
||||
alt={unwrap(consumable.name)}
|
||||
/>
|
||||
{consumable.name}
|
||||
{unwrap(consumable.name)}
|
||||
</Flex>
|
||||
</Text>
|
||||
);
|
||||
|
||||
+65
-17
@@ -1,4 +1,5 @@
|
||||
import { imgur, ShellType } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { type ChangeEvent } from 'react';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
|
||||
@@ -19,6 +21,7 @@ export function CustomShellButton() {
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Popover.Root
|
||||
@@ -49,13 +52,16 @@ export function CustomShellButton() {
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
{/* <Popover.Title>Custom shell</Popover.Title> */}
|
||||
|
||||
{customShell && (
|
||||
<>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" justify="between" gap="4">
|
||||
<Text>Type</Text>
|
||||
<Text>
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic
|
||||
.custom_shell.type
|
||||
}
|
||||
</Text>
|
||||
<Select.Root
|
||||
value={`${customShell.type}`}
|
||||
onValueChange={(value) => {
|
||||
@@ -99,16 +105,29 @@ export function CustomShellButton() {
|
||||
<Select.Trigger />
|
||||
|
||||
<Select.Content>
|
||||
<Select.Item value={`${ShellType.AP}`}>AP</Select.Item>
|
||||
<Select.Item value={`${ShellType.APCR}`}>APCR</Select.Item>
|
||||
<Select.Item value={`${ShellType.HEAT}`}>HEAT</Select.Item>
|
||||
<Select.Item value={`${ShellType.HE}`}>HE</Select.Item>
|
||||
<Select.Item value={`${ShellType.AP}`}>
|
||||
{strings.common.shells.ap}
|
||||
</Select.Item>
|
||||
<Select.Item value={`${ShellType.APCR}`}>
|
||||
{strings.common.shells.apcr}
|
||||
</Select.Item>
|
||||
<Select.Item value={`${ShellType.HEAT}`}>
|
||||
{strings.common.shells.heat}
|
||||
</Select.Item>
|
||||
<Select.Item value={`${ShellType.HE}`}>
|
||||
{strings.common.shells.he}
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" justify="between" gap="4">
|
||||
<Text>Caliber</Text>
|
||||
<Text>
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic
|
||||
.custom_shell.caliber
|
||||
}
|
||||
</Text>
|
||||
<TextField.Root
|
||||
style={{ maxWidth: '6rem' }}
|
||||
defaultValue={customShell.caliber}
|
||||
@@ -131,12 +150,19 @@ export function CustomShellButton() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="right">mm</TextField.Slot>
|
||||
<TextField.Slot side="right">
|
||||
{literals(strings.common.units.mm, [''])}
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" justify="between" gap="4">
|
||||
<Text>Penetration</Text>
|
||||
<Text>
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic
|
||||
.custom_shell.penetration
|
||||
}
|
||||
</Text>
|
||||
<TextField.Root
|
||||
style={{ maxWidth: '6rem' }}
|
||||
defaultValue={customShell.penetration.near}
|
||||
@@ -160,7 +186,9 @@ export function CustomShellButton() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="right">mm</TextField.Slot>
|
||||
<TextField.Slot side="right">
|
||||
{literals(strings.common.units.mm, [''])}
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
|
||||
@@ -168,7 +196,12 @@ export function CustomShellButton() {
|
||||
customShell.type === ShellType.APCR) && (
|
||||
<>
|
||||
<Flex align="center" justify="between" gap="4" py="1">
|
||||
<Text>Normalization</Text>
|
||||
<Text>
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic
|
||||
.custom_shell.normalization
|
||||
}
|
||||
</Text>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
@@ -178,7 +211,9 @@ export function CustomShellButton() {
|
||||
minWidth="8rem"
|
||||
>
|
||||
<Text color="gray">
|
||||
{Math.round(customShell.normalization ?? 0)}°
|
||||
{literals(strings.common.units.deg, [
|
||||
(customShell.normalization ?? 0).toFixed(0),
|
||||
])}
|
||||
</Text>
|
||||
<Slider
|
||||
min={0}
|
||||
@@ -197,11 +232,18 @@ export function CustomShellButton() {
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" justify="between" gap="4" py="1">
|
||||
<Text>Ricochet</Text>
|
||||
<Text>
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic
|
||||
.custom_shell.ricochet
|
||||
}
|
||||
</Text>
|
||||
|
||||
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
|
||||
<Text color="gray">
|
||||
{Math.round(customShell.ricochet ?? 90)}°
|
||||
{literals(strings.common.units.deg, [
|
||||
(customShell.ricochet ?? 90).toFixed(0),
|
||||
])}
|
||||
</Text>
|
||||
<Slider
|
||||
min={0}
|
||||
@@ -254,7 +296,10 @@ export function CustomShellButton() {
|
||||
<Flex justify="end" mt="6" gap="2">
|
||||
<Popover.Close>
|
||||
<Button color="red" variant="outline">
|
||||
Exit
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic
|
||||
.custom_shell.exit
|
||||
}
|
||||
</Button>
|
||||
</Popover.Close>
|
||||
<Button
|
||||
@@ -265,7 +310,10 @@ export function CustomShellButton() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.custom_shell
|
||||
.reset
|
||||
}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
+3
@@ -25,6 +25,7 @@ export function QuickInputs() {
|
||||
const initialGunPitch =
|
||||
tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
|
||||
useEffect(() => {
|
||||
if (!yawInput.current) return;
|
||||
@@ -92,6 +93,7 @@ export function QuickInputs() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
mutateDuel((state) => {
|
||||
@@ -133,6 +135,7 @@ export function QuickInputs() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
mutateDuel((state) => {
|
||||
|
||||
+125
-104
@@ -1,6 +1,8 @@
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Button, Checkbox, Flex, Text } from '@radix-ui/themes';
|
||||
import { Button, Checkbox, Flex, Text } from '@radix-ui/themes';
|
||||
import { Suspense } from 'react';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
|
||||
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
|
||||
@@ -24,6 +26,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -33,112 +36,144 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
mr="4"
|
||||
style={{ transform: 'translateY(-50%)', userSelect: 'none' }}
|
||||
direction="column"
|
||||
gap="2"
|
||||
gap="5"
|
||||
align="end"
|
||||
>
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{(thicknessRange.value * 1.5).toFixed(0)}
|
||||
</Text>
|
||||
<Flex gap="1">
|
||||
<Flex direction="column" align="end" gap="3">
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, [
|
||||
(thicknessRange.value * 1.5).toFixed(0),
|
||||
])}
|
||||
</Text>
|
||||
<Text size="1">
|
||||
{strings.website.tools.tankopedia.sandbox.static.primary}
|
||||
</Text>
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, ['0'])}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
width="1rem"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
|
||||
color: 'black',
|
||||
}}
|
||||
>
|
||||
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
<Text size="1">Primary</Text>
|
||||
</Flex>
|
||||
<Text color="gray" size="1">
|
||||
0
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="16px"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, [
|
||||
thicknessRange.value.toFixed(0),
|
||||
])}
|
||||
</Text>
|
||||
<Text size="1">
|
||||
{strings.website.tools.tankopedia.sandbox.static.spaced}
|
||||
</Text>
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, ['0'])}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{thicknessRange.value.toFixed(0)}
|
||||
</Text>
|
||||
<Flex gap="1">
|
||||
<Flex
|
||||
align="center"
|
||||
width="1rem"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
|
||||
}}
|
||||
>
|
||||
{showSpacedArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
<Text size="1">Spaced</Text>
|
||||
</Flex>
|
||||
<Text color="gray" size="1">
|
||||
0
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="16px"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showExternalModules = !draft.showExternalModules;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text size="1">
|
||||
{strings.website.tools.tankopedia.sandbox.static.external}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showExternalModules = !draft.showExternalModules;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Flex gap="1">
|
||||
<Flex
|
||||
justify="center"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#c0c0c0',
|
||||
color: 'black',
|
||||
}}
|
||||
>
|
||||
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
<Text size="1">External</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" align="end" gap="2">
|
||||
<Flex align="center" gap="2">
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editStatic = !draft.editStatic;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text size="1">
|
||||
{strings.website.tools.tankopedia.sandbox.static.edit.label}
|
||||
</Text>
|
||||
<Checkbox size="2" checked={editStatic} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="16px"
|
||||
height="16px"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#c0c0c0',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2" mt="2">
|
||||
<Button
|
||||
color="red"
|
||||
size="1"
|
||||
@@ -151,22 +186,8 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
}}
|
||||
>
|
||||
<ReloadIcon />
|
||||
Reset
|
||||
{strings.website.tools.tankopedia.sandbox.static.edit.reset}
|
||||
</Button>
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editStatic = !draft.editStatic;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text size="1">Edit</Text>
|
||||
<Checkbox size="2" checked={editStatic} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Suspense>
|
||||
|
||||
+57
-30
@@ -5,6 +5,7 @@ import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
uniqueGuns,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import {
|
||||
CameraIcon,
|
||||
CopyIcon,
|
||||
@@ -33,6 +34,7 @@ import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
||||
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { App } from '../../../../../stores/app';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import {
|
||||
@@ -87,6 +89,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -124,13 +127,14 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
>
|
||||
{!hasCustomShell && (
|
||||
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
||||
{(
|
||||
resolvePenetrationCoefficient(
|
||||
hasCalibratedShells,
|
||||
antagonistShell.type,
|
||||
) * antagonistShell.penetration.near
|
||||
).toFixed(0)}
|
||||
mm
|
||||
{literals(strings.common.units.mm, [
|
||||
(
|
||||
resolvePenetrationCoefficient(
|
||||
hasCalibratedShells,
|
||||
antagonistShell.type,
|
||||
) * antagonistShell.penetration.near
|
||||
).toFixed(0),
|
||||
])}
|
||||
</Text>
|
||||
)}
|
||||
{antagonistUniqueGuns.length > 1 && (
|
||||
@@ -198,10 +202,11 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
}
|
||||
secondaryDiscriminator={
|
||||
<Text style={{ fontSize: '0.75em' }}>
|
||||
{Math.round(
|
||||
gun.gun_type!.value.base.shells[0].caliber,
|
||||
)}
|
||||
mm
|
||||
{literals(strings.common.units.mm, [
|
||||
gun.gun_type!.value.base.shells[0].caliber.toFixed(
|
||||
0,
|
||||
),
|
||||
])}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
@@ -239,7 +244,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={thisShell.name}
|
||||
alt={unwrap(thisShell.name)}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
style={{
|
||||
width: '50%',
|
||||
@@ -341,13 +346,13 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
>
|
||||
<Checkbox checked={advancedHighlighting} />
|
||||
<Text color="gray" size="2">
|
||||
Advanced highlighting
|
||||
{strings.website.tools.tankopedia.sandbox.dynamic.advanced}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2">
|
||||
<Text color="gray" size="2">
|
||||
Shooter:
|
||||
{strings.website.tools.tankopedia.sandbox.dynamic.shooter}
|
||||
</Text>
|
||||
<Dialog.Root
|
||||
open={antagonistSelectorOpen}
|
||||
@@ -357,7 +362,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
<Button variant="ghost">
|
||||
<Flex gap="2" align="center">
|
||||
<SmallTankIcon id={antagonistTank.id} size={16} />
|
||||
{antagonistTank.name}
|
||||
{unwrap(antagonistTank.name)}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
@@ -403,21 +408,27 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
}}
|
||||
>
|
||||
<SegmentedControl.Item value={`${TankopediaDisplay.Model}`}>
|
||||
<Tooltip content="Model">
|
||||
<Tooltip
|
||||
content={strings.website.tools.tankopedia.sandbox.model.name}
|
||||
>
|
||||
<Flex height="100%" align="center">
|
||||
<img src={imgur('jAdYf0m')} style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaDisplay.DynamicArmor}`}>
|
||||
<Tooltip content="Dynamic armor">
|
||||
<Tooltip
|
||||
content={strings.website.tools.tankopedia.sandbox.dynamic.name}
|
||||
>
|
||||
<Flex height="100%" align="center">
|
||||
<img src={imgur('oe4Cq0g')} style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaDisplay.StaticArmor}`}>
|
||||
<Tooltip content="Static armor">
|
||||
<Tooltip
|
||||
content={strings.website.tools.tankopedia.sandbox.static.name}
|
||||
>
|
||||
<Flex height="100%" align="center">
|
||||
<img src={imgur('VQ4uDno')} style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
@@ -441,15 +452,15 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}>
|
||||
Hull down
|
||||
{strings.website.tools.tankopedia.sandbox.poses.hull_down}
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}>
|
||||
Face hug
|
||||
{strings.website.tools.tankopedia.sandbox.poses.face_hug}
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}>
|
||||
Default
|
||||
{strings.website.tools.tankopedia.sandbox.poses.default}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
@@ -464,7 +475,9 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
<DropdownMenu.Content>
|
||||
{display === TankopediaDisplay.DynamicArmor && (
|
||||
<>
|
||||
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>
|
||||
{strings.website.tools.tankopedia.sandbox.settings.armor}
|
||||
</DropdownMenu.Label>
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={greenPenetration}
|
||||
@@ -474,7 +487,10 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Green penetration
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.settings
|
||||
.green_penetration
|
||||
}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
@@ -485,7 +501,10 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Hide tank model under armor
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.settings
|
||||
.hide_model_under_armor
|
||||
}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
@@ -496,7 +515,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Opaque
|
||||
{strings.website.tools.tankopedia.sandbox.settings.opaque}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
|
||||
{developerMode && (
|
||||
@@ -508,13 +527,18 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<b>DEV:</b> Wireframe
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.settings
|
||||
.dev_wireframe
|
||||
}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenu.Label>Environment</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>
|
||||
{strings.website.tools.tankopedia.sandbox.settings.environment}
|
||||
</DropdownMenu.Label>
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={showGrid}
|
||||
@@ -524,7 +548,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show grid
|
||||
{strings.website.tools.tankopedia.sandbox.settings.show_grid}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
@@ -557,7 +581,10 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
{
|
||||
strings.website.tools.tankopedia.sandbox.screenshot
|
||||
.download
|
||||
}
|
||||
</Button>
|
||||
</Popover.Close>
|
||||
<Popover.Close>
|
||||
@@ -576,7 +603,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
}}
|
||||
>
|
||||
<CopyIcon />
|
||||
Copy
|
||||
{strings.website.tools.tankopedia.sandbox.screenshot.copy}
|
||||
</Button>
|
||||
</Popover.Close>
|
||||
</Flex>
|
||||
|
||||
+7
@@ -118,6 +118,11 @@ export function Controls({ naked }: ControlsProps) {
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
|
||||
switch (event) {
|
||||
case Pose.HullDown: {
|
||||
@@ -127,6 +132,7 @@ export function Controls({ naked }: ControlsProps) {
|
||||
protagonistGunModelDefinition.pitch,
|
||||
protagonistTurretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
camera.position
|
||||
@@ -159,6 +165,7 @@ export function Controls({ naked }: ControlsProps) {
|
||||
protagonistGunModelDefinition.pitch,
|
||||
protagonistTurretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
camera.position
|
||||
|
||||
+12
@@ -185,6 +185,11 @@ export function TankModel() {
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
@@ -196,6 +201,7 @@ export function TankModel() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
}
|
||||
@@ -287,6 +293,11 @@ export function TankModel() {
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
position.set(event.clientX, event.clientY);
|
||||
@@ -297,6 +308,7 @@ export function TankModel() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
}
|
||||
|
||||
+2
@@ -41,6 +41,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
const protagonistGun = Duel.use((state) => state.protagonist.gun);
|
||||
const protagonistTurret = Duel.use((state) => state.protagonist.turret);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
@@ -82,6 +83,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NAVBAR_HEIGHT } from '../../../constants/navbar';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import { useFullScreen } from '../../../hooks/useFullScreen';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
@@ -18,6 +19,7 @@ import { TankSandboxLoader } from './components/TankSandboxLoader';
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { unwrap } = useLocale();
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const isFullScreen = useFullScreen();
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
@@ -117,7 +119,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
wrap="nowrap"
|
||||
color={treeColor}
|
||||
>
|
||||
{protagonist.name}
|
||||
{unwrap(protagonist.name)}
|
||||
</Heading>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Heading, Text } from '@radix-ui/themes';
|
||||
import { Duel } from '../../stores/duel';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
|
||||
export function HistorySection() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
|
||||
if (!tank.description) return null;
|
||||
|
||||
return (
|
||||
<PageWrapper noFlex1 align="center" noMinHeight>
|
||||
<Heading>History of the {tank.name}</Heading>
|
||||
<Text>{tank.description}</Text>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+3
-1
@@ -9,6 +9,7 @@ import {
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { App } from '../../../../../../stores/app';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { Stars } from '../../Stars';
|
||||
@@ -24,6 +25,7 @@ export function VoteCaster() {
|
||||
const [postVote, setPostVote] = useState<boolean | null>(null);
|
||||
// true = open, false = closed, null = casting
|
||||
const [showDialogue, setShowDialogue] = useState<boolean | null>(false);
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -35,7 +37,7 @@ export function VoteCaster() {
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content maxWidth="23rem">
|
||||
<Dialog.Title>Voting for {tank.name}</Dialog.Title>
|
||||
<Dialog.Title>Voting for {unwrap(tank.name)}</Dialog.Title>
|
||||
|
||||
<Flex direction="column" mt="4">
|
||||
<Flex align="center" justify="between">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatCompact } from '@blitzkit/core';
|
||||
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { memo } from 'react';
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { useTankVotes } from '../../../../../hooks/useTankVotes';
|
||||
import { App } from '../../../../../stores/app';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
@@ -13,13 +14,14 @@ export const Votes = memo(
|
||||
const id = Duel.use((state) => state.protagonist.tank.id);
|
||||
const votes = useTankVotes(id);
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
const { locale } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="3">
|
||||
<Flex justify="between" align="center">
|
||||
<Heading size="4">
|
||||
{!votes && <Skeleton height="1em" width="4rem" />}
|
||||
{votes && formatCompact(votes.votes)}{' '}
|
||||
{votes && formatCompact(locale, votes.votes)}{' '}
|
||||
{votes && (votes.votes > 1 ? 'votes' : 'vote')}
|
||||
</Heading>
|
||||
|
||||
|
||||
@@ -5,17 +5,18 @@ import {
|
||||
TankType,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import { ChevronLeftIcon, MixIcon, UpdateIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Code, Dialog, Flex, Link } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../core/awaitables/provisionDefinitions';
|
||||
import { tankToDuelMember } from '../../../core/blitzkit/tankToDuelMember';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { App } from '../../../stores/app';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import { classIcons } from '../../ClassIcon';
|
||||
import { LinkI18n } from '../../LinkI18n';
|
||||
import { ScienceIcon } from '../../ScienceIcon';
|
||||
import { TankSearch } from '../../TankSearch';
|
||||
import { Listing } from './components/Listing';
|
||||
@@ -37,24 +38,28 @@ export function MetaSection() {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const [showSwapDialog, setShowSwapDialog] = useState(false);
|
||||
const { locale, strings, unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex justify="center" align="center">
|
||||
<Flex direction="column" align="center" gap="6">
|
||||
<Flex gap="2" wrap="wrap" justify="center" align="center">
|
||||
<Link href="/tools/tankopedia">
|
||||
<LinkI18n locale={locale} href="/tools/tankopedia">
|
||||
<Button variant="outline">
|
||||
<ChevronLeftIcon />
|
||||
Back
|
||||
{strings.website.tools.tankopedia.meta.back}
|
||||
</Button>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
|
||||
<Link href={`/tools/compare?tanks=${compareTanks.join('%2C')}`}>
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
href={`/tools/compare?tanks=${compareTanks.join('%2C')}`}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<MixIcon />
|
||||
Compare
|
||||
{strings.website.tools.tankopedia.meta.compare}
|
||||
</Button>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
|
||||
{assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) === 'true' && (
|
||||
<Link
|
||||
@@ -68,8 +73,8 @@ export function MetaSection() {
|
||||
<ScienceIcon height="1.25em" width="1.25em" />
|
||||
{assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH) ===
|
||||
'opentest'
|
||||
? 'Vanilla'
|
||||
: 'OpenTest'}
|
||||
? strings.website.tools.tankopedia.meta.vanilla
|
||||
: strings.website.tools.tankopedia.meta.opentest}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
@@ -78,12 +83,14 @@ export function MetaSection() {
|
||||
<Dialog.Trigger>
|
||||
<Button>
|
||||
<UpdateIcon />
|
||||
Swap
|
||||
{strings.website.tools.tankopedia.meta.swap.button}
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Swap tanks</Dialog.Title>
|
||||
<Dialog.Title>
|
||||
{strings.website.tools.tankopedia.meta.swap.title}
|
||||
</Dialog.Title>
|
||||
|
||||
<TankSearch
|
||||
compact
|
||||
@@ -116,9 +123,11 @@ export function MetaSection() {
|
||||
>
|
||||
<Flex direction="column" width="100%">
|
||||
{protagonist.name_full && (
|
||||
<Listing label="Full-name">{protagonist.name_full}</Listing>
|
||||
<Listing label={strings.website.tools.tankopedia.meta.full_name}>
|
||||
{unwrap(protagonist.name_full)}
|
||||
</Listing>
|
||||
)}
|
||||
<Listing label="Nation">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.nation}>
|
||||
<Flex align="center" gap="1">
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
@@ -132,17 +141,17 @@ export function MetaSection() {
|
||||
}
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Class">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.class}>
|
||||
<Flex align="center" gap="1">
|
||||
<ClassIcon width="1em" height="1em" />
|
||||
{strings.common.tank_class_short[protagonist.class]}
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Tier">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.tier}>
|
||||
{TIER_ROMAN_NUMERALS[protagonist.tier]}
|
||||
</Listing>
|
||||
<Listing
|
||||
label="Type"
|
||||
label={strings.website.tools.tankopedia.meta.type}
|
||||
color={
|
||||
protagonist.type === TankType.COLLECTOR
|
||||
? 'blue'
|
||||
@@ -157,12 +166,12 @@ export function MetaSection() {
|
||||
|
||||
<Flex direction="column" width="100%">
|
||||
{developerMode && (
|
||||
<Listing label="DEV: ID">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.dev_id}>
|
||||
<Code>{protagonist.id}</Code>
|
||||
</Listing>
|
||||
)}
|
||||
{protagonist.type === TankType.PREMIUM && (
|
||||
<Listing label="Purchase price">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.purchase}>
|
||||
<Flex align="center" gap="1">
|
||||
{protagonist.price.value / 400}
|
||||
<img
|
||||
@@ -174,14 +183,14 @@ export function MetaSection() {
|
||||
</Listing>
|
||||
)}
|
||||
<Listing
|
||||
label={`${
|
||||
label={
|
||||
protagonist.type === TankType.RESEARCHABLE
|
||||
? 'Purchase'
|
||||
: 'Restoration'
|
||||
} price`}
|
||||
? strings.website.tools.tankopedia.meta.purchase
|
||||
: strings.website.tools.tankopedia.meta.restoration
|
||||
}
|
||||
>
|
||||
<Flex align="center" gap="1">
|
||||
{protagonist.price.value.toLocaleString()}
|
||||
{protagonist.price.value.toLocaleString(locale)}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={TankPriceType[protagonist.price.type]}
|
||||
@@ -195,9 +204,9 @@ export function MetaSection() {
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Sale price">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.sale}>
|
||||
<Flex align="center" gap="1">
|
||||
{(protagonist.price.value / 2).toLocaleString()}
|
||||
{(protagonist.price.value / 2).toLocaleString(locale)}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={TankPriceType[protagonist.price.type]}
|
||||
@@ -212,12 +221,12 @@ export function MetaSection() {
|
||||
</Flex>
|
||||
</Listing>
|
||||
{protagonist.research_cost && (
|
||||
<Listing label="Research XP">
|
||||
<Listing label={strings.website.tools.tankopedia.meta.research}>
|
||||
<Flex align="center" gap="1">
|
||||
{(
|
||||
protagonist.research_cost.research_cost_type!
|
||||
.value as number
|
||||
).toLocaleString()}
|
||||
).toLocaleString(locale)}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt="xp"
|
||||
@@ -228,8 +237,6 @@ export function MetaSection() {
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* <Votes /> */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { asset, AverageDefinitions } from '@blitzkit/core';
|
||||
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { range } from 'lodash-es';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { Duel } from '../../stores/duel';
|
||||
import { FIRST_AVERAGES_ENTRY } from '../Charts/constants';
|
||||
import { ThemedLine } from '../Nivo/ThemedLine';
|
||||
|
||||
const currentEntry = Math.floor(Date.now() / 1000 / 60 / 60 / 24);
|
||||
const days = await Promise.all(
|
||||
range(FIRST_AVERAGES_ENTRY, currentEntry).map(async (day) => {
|
||||
const url = asset(`averages/${day}.pb`);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status !== 200) return null;
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const averageDefinitions = AverageDefinitions.decode(
|
||||
new Uint8Array(buffer),
|
||||
);
|
||||
|
||||
return { day, definitions: averageDefinitions };
|
||||
}),
|
||||
).then((days) => days.filter((day) => day !== null));
|
||||
|
||||
export function TankopediaCharts() {
|
||||
const id = Duel.use((state) => state.protagonist.tank.id);
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const data = days.map(({ day, definitions }) => {
|
||||
const entry = definitions.averages[id];
|
||||
const x = new Date(day * 24 * 60 * 60 * 1000);
|
||||
const y = entry ? Math.round((entry.samples.d_7 / 7) * ratio) : 0;
|
||||
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center">
|
||||
<Flex direction="column" width="50rem">
|
||||
<Heading>Players</Heading>
|
||||
<Text color="gray" mb="5">
|
||||
7-day rolling average
|
||||
</Text>
|
||||
|
||||
<Box height="25rem">
|
||||
<ThemedLine
|
||||
enablePoints={false}
|
||||
enableGridX={false}
|
||||
data={[{ id: 'value', data }]}
|
||||
xFormat={(date) => {
|
||||
return (date as Date).toLocaleDateString(undefined, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
}}
|
||||
xScale={{ type: 'time' }}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { asset, formatCompact, TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
||||
import { Box, Flex, Link, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Box, Flex, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||
import { LinkI18n } from '../../../LinkI18n';
|
||||
|
||||
type NodeProps = MaybeSkeletonComponentProps & {
|
||||
id: number;
|
||||
@@ -17,6 +20,7 @@ const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
const { locale, strings, unwrap } = useLocale();
|
||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const nextTanks = nextIds?.map((id) => tankDefinitions.tanks[id]);
|
||||
@@ -40,7 +44,8 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
averages && nextTanks ? Math.round(nextTanksXp! / averageXp!) : undefined;
|
||||
|
||||
return (
|
||||
<Link
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
href={`/tools/tankopedia/${id}`}
|
||||
key={id}
|
||||
style={{
|
||||
@@ -73,7 +78,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
alt={unwrap(tank.name)}
|
||||
src={asset(`icons/tanks/big/${id}.webp`)}
|
||||
width={64}
|
||||
height={64}
|
||||
@@ -87,7 +92,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
<Text color="gray" size="1">
|
||||
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||
</Text>
|
||||
<Text wrap="nowrap">{tank.name}</Text>
|
||||
<Text wrap="nowrap">{unwrap(tank.name)}</Text>
|
||||
</Flex>
|
||||
<Flex gap="2" align="center">
|
||||
<Text color="gray" size="1" wrap="nowrap">
|
||||
@@ -102,7 +107,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{formatCompact(thisTankXp)}
|
||||
{formatCompact(locale, thisTankXp)}
|
||||
</Flex>
|
||||
</Text>
|
||||
<Text color="gray" size="1" wrap="nowrap">
|
||||
@@ -117,7 +122,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{formatCompact(tank.price.value)}
|
||||
{formatCompact(locale, tank.price.value)}
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -125,16 +130,18 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
{averages && nextTanks && tank.tier !== 10 && (
|
||||
<Text color="gray" size="1" mt="2">
|
||||
{skeleton && <Skeleton height="1em" width="4em" />}
|
||||
{!skeleton && (
|
||||
<>
|
||||
{games!} {games === 1 ? 'battle' : 'battles'}
|
||||
</>
|
||||
)}
|
||||
{!skeleton &&
|
||||
(games === 1
|
||||
? strings.website.tools.tankopedia.tech_tree.battle
|
||||
: literals(
|
||||
strings.website.tools.tankopedia.tech_tree.battles,
|
||||
[`${games}`],
|
||||
))}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { asset, imgur, TankType } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
@@ -16,6 +18,7 @@ const tankDefinitions = await awaitableTankDefinitions;
|
||||
export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
|
||||
|
||||
export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { locale, strings } = useLocale();
|
||||
const master = Duel.use((state) => state.protagonist.tank);
|
||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
@@ -89,7 +92,9 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
py="6"
|
||||
>
|
||||
<Flex direction="column" align="center">
|
||||
<Heading size="6">Tech tree</Heading>
|
||||
<Heading size="6">
|
||||
{strings.website.tools.tankopedia.tech_tree.title}
|
||||
</Heading>
|
||||
|
||||
{master.tier > 1 && (
|
||||
<Flex gap="4" align="center">
|
||||
@@ -105,7 +110,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{Math.round(totalXp / xpMultiplier).toLocaleString()}
|
||||
{Math.round(totalXp / xpMultiplier).toLocaleString(locale)}
|
||||
</Flex>
|
||||
</Text>
|
||||
|
||||
@@ -121,7 +126,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{totalCredits.toLocaleString()}
|
||||
{totalCredits.toLocaleString(locale)}
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -154,7 +159,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
pl="1"
|
||||
>
|
||||
<Text size="2">
|
||||
x<b>{multiplier}</b>
|
||||
{literals(strings.common.units.x, [`${multiplier}`])}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
@@ -214,7 +219,10 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<CaretLeftIcon />
|
||||
</IconButton>
|
||||
<Text>
|
||||
Route {lineIndex + 1} of {lines.length}
|
||||
{literals(strings.website.tools.tankopedia.tech_tree.route, [
|
||||
`${lineIndex + 1}`,
|
||||
`${lines.length}`,
|
||||
])}
|
||||
</Text>
|
||||
<IconButton
|
||||
size="2"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { youtubers } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Box, Flex, Heading, Link, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { awaitableReviews } from '../../core/awaitables/reviews';
|
||||
import { Var } from '../../core/radix/var';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { Duel } from '../../stores/duel';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
@@ -12,6 +14,7 @@ const reviews = await awaitableReviews;
|
||||
export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const videos = reviews.reviews[tank.id]?.videos;
|
||||
const { strings } = useLocale();
|
||||
|
||||
if (!videos) return null;
|
||||
|
||||
@@ -19,9 +22,13 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<PageWrapper noFlex1 noMinHeight>
|
||||
<Flex direction="column" gap="6" align="center">
|
||||
<Flex direction="column" align="center">
|
||||
<Heading size="6">Review videos</Heading>
|
||||
<Heading size="6">
|
||||
{strings.website.tools.tankopedia.review.title}
|
||||
</Heading>
|
||||
|
||||
<Text color="gray">BlitzKit does not endorse any review videos.</Text>
|
||||
<Text color="gray">
|
||||
{strings.website.tools.tankopedia.review.disclaimer}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" wrap="wrap" justify="center">
|
||||
@@ -110,7 +117,9 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
width: '9rem',
|
||||
}}
|
||||
>
|
||||
Apply to BlitzKit YouTuber program
|
||||
{literals(strings.website.tools.tankopedia.review.join, [
|
||||
'BlitzKit',
|
||||
])}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
type CompositeStats,
|
||||
type CompositeStatsKey,
|
||||
} from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import strings from '@blitzkit/i18n/strings/en.json';
|
||||
import { amberDark, blueDark } from '@radix-ui/colors';
|
||||
import { Flex, Text, type FlexProps } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import type { ReactNode } from 'react';
|
||||
import { breakdownConfig } from '../constants/embeds';
|
||||
import { toRadiusVar } from '../core/radix/utils';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { useEmbedStateCurry } from '../stores/embedState/utilities';
|
||||
import { ClassHeavy } from './ClassIcon/components/ClassHeavy';
|
||||
import { ClassLight } from './ClassIcon/components/ClassLight';
|
||||
@@ -52,6 +53,7 @@ export function BreakdownEmbedCard({
|
||||
}: BreakdownEmbedCardProps) {
|
||||
const { useEmbedState, useRichText } =
|
||||
useEmbedStateCurry<typeof breakdownConfig>();
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -111,7 +113,7 @@ export function BreakdownEmbedCard({
|
||||
? 'Total'
|
||||
: tank === undefined
|
||||
? `Unknown tank`
|
||||
: tank.name}
|
||||
: unwrap(tank.name)}
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Share2Icon } from '@radix-ui/react-icons';
|
||||
import { generateTierListParams } from '../../../../core/blitzkit/generateTierListParams';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { TierList } from '../../../../stores/tierList';
|
||||
import { CopyButton } from '../../../CopyButton';
|
||||
|
||||
export function Share() {
|
||||
const rows = TierList.use((state) => state.rows);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<CopyButton
|
||||
@@ -13,7 +15,7 @@ export function Share() {
|
||||
`${location.origin}/tools/tier-list?${generateTierListParams(rows)}`
|
||||
}
|
||||
>
|
||||
<Share2Icon /> Share
|
||||
<Share2Icon /> {strings.website.tools.tier_list.buttons.share}
|
||||
</CopyButton>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { TierList, tierListInitialState } from '../../../stores/tierList';
|
||||
import { Share } from './components/Share';
|
||||
|
||||
export function TierListControls() {
|
||||
const mutateTierList = TierList.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex justify="center" gap="2">
|
||||
@@ -16,7 +18,7 @@ export function TierListControls() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ReloadIcon /> Reset
|
||||
<ReloadIcon /> {strings.website.tools.tier_list.buttons.reset}
|
||||
</Button>
|
||||
|
||||
<Share />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
import { assertSecret, imgur, ImgurSize } from '@blitzkit/core';
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Button, Code, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { Box, Button, Code, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import type { Tool } from '../constants/tools';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
import { LinkI18n } from './LinkI18n';
|
||||
|
||||
interface Props {
|
||||
tool: Tool;
|
||||
@@ -12,6 +14,12 @@ const { tool } = Astro.props;
|
||||
const disabled = tool.branches?.every(
|
||||
(branch) => branch !== assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH),
|
||||
);
|
||||
const { locale } = Astro.params;
|
||||
const strings = getStrings(locale);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.stringsId??tool.id ) as keyof typeof strings.website.tools
|
||||
];
|
||||
---
|
||||
|
||||
<style>
|
||||
@@ -54,7 +62,8 @@ const disabled = tool.branches?.every(
|
||||
flexGrow={tool.significant ? undefined : '1'}
|
||||
flexBasis={tool.significant ? undefined : { initial: undefined, sm: '0' }}
|
||||
>
|
||||
<Link
|
||||
<LinkI18n
|
||||
{locale}
|
||||
className="link"
|
||||
underline="none"
|
||||
href={disabled ? '#' : (tool.href ?? `/tools/${tool.id}`)}
|
||||
@@ -98,7 +107,7 @@ const disabled = tool.branches?.every(
|
||||
size={{ initial: '6', sm: tool.significant ? '7' : '5' }}
|
||||
weight="medium"
|
||||
>
|
||||
{tool.title}
|
||||
{toolStrings.name}
|
||||
</Heading>
|
||||
<Text
|
||||
align={{ initial: 'center', sm: 'left' }}
|
||||
@@ -112,7 +121,7 @@ const disabled = tool.branches?.every(
|
||||
<Code>{assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH)}</Code>
|
||||
</>
|
||||
) : (
|
||||
tool.description
|
||||
toolStrings.description
|
||||
)
|
||||
}
|
||||
</Text>
|
||||
@@ -129,11 +138,11 @@ const disabled = tool.branches?.every(
|
||||
color={tool.button.color}
|
||||
style={{ cursor: 'inherit' }}
|
||||
>
|
||||
{tool.button.text}
|
||||
{toolStrings.button}
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
</Flex>
|
||||
@@ -1,22 +1,22 @@
|
||||
---
|
||||
import { imgur } from '@blitzkit/core';
|
||||
import { type Tool } from '../constants/tools';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
|
||||
interface Props {
|
||||
tool: Tool;
|
||||
}
|
||||
|
||||
const { tool } = Astro.props;
|
||||
const { locale } = Astro.params;
|
||||
const strings = getStrings(locale);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.stringsId ?? tool.id) as keyof typeof strings.website.tools
|
||||
];
|
||||
---
|
||||
|
||||
<title>{tool.title}</title>
|
||||
<title>{toolStrings.name}</title>
|
||||
<meta property="og:image" content={imgur(tool.image)} />
|
||||
|
||||
{
|
||||
tool.pageDescription !== undefined && (
|
||||
<>
|
||||
<meta name="description" content={tool.pageDescription} />
|
||||
<meta property="og:description" content={tool.pageDescription} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<meta name="description" content={toolStrings.seo_description} />
|
||||
<meta property="og:description" content={toolStrings.seo_description} />
|
||||
@@ -1,9 +1,11 @@
|
||||
import { assertSecret, REGIONS } from '@blitzkit/core';
|
||||
import strings from '@blitzkit/core/lang/en.json';
|
||||
import { Button, type ButtonProps, Dialog, Flex, Link } from '@radix-ui/themes';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { WargamingIcon } from './WargamingIcon';
|
||||
|
||||
export function WargamingLoginButton({ children, ...props }: ButtonProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
|
||||
@@ -3,15 +3,12 @@ import type { ComponentProps } from 'react';
|
||||
|
||||
export interface Tool {
|
||||
id: string;
|
||||
stringsId?: string;
|
||||
image: string;
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
pageDescription?: string;
|
||||
button: {
|
||||
highContrast?: boolean;
|
||||
text: string;
|
||||
color: ComponentProps<typeof Button>['color'];
|
||||
};
|
||||
significant?: boolean;
|
||||
@@ -20,13 +17,8 @@ export interface Tool {
|
||||
|
||||
export const tankopediaTool: Tool = {
|
||||
id: 'tankopedia',
|
||||
title: 'Tankopedia',
|
||||
description: 'Blitz tank encyclopedia',
|
||||
pageDescription:
|
||||
'Statistics, armor, and more for all tanks in World of Tanks Blitz',
|
||||
image: 'D8j6GZx',
|
||||
button: {
|
||||
text: 'Find tanks',
|
||||
color: 'purple',
|
||||
},
|
||||
significant: true,
|
||||
@@ -35,13 +27,8 @@ export const tankopediaTool: Tool = {
|
||||
|
||||
// export const ratingTool: Tool = {
|
||||
// id: 'rating',
|
||||
// title: 'Rating',
|
||||
// description: 'Full rating leaderboard',
|
||||
// pageDescription:
|
||||
// 'Live and archived full rating leaderboard for World of Tanks Blitz',
|
||||
// image: 'WEyMZH3',
|
||||
// button: {
|
||||
// text: 'View leaderboard',
|
||||
// color: 'orange',
|
||||
// },
|
||||
// branches: ['main', 'dev'],
|
||||
@@ -49,13 +36,8 @@ export const tankopediaTool: Tool = {
|
||||
|
||||
export const compareTool: Tool = {
|
||||
id: 'compare',
|
||||
title: 'Compare',
|
||||
description: 'Compare tank statistics',
|
||||
pageDescription:
|
||||
'Compare tanks statistics and loadouts in World of Tanks Blitz',
|
||||
image: 'O6VNl6e',
|
||||
button: {
|
||||
text: 'Compare tanks',
|
||||
color: 'crimson',
|
||||
},
|
||||
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||
@@ -64,13 +46,8 @@ export const compareTool: Tool = {
|
||||
export const sessionTool: Tool = {
|
||||
// TODO: rename to tracker
|
||||
id: 'session',
|
||||
title: 'Session',
|
||||
description: 'Live session stats',
|
||||
pageDescription:
|
||||
'Track your stats in real time as you play World of Tanks Blitz',
|
||||
image: 'HdG9sTf',
|
||||
button: {
|
||||
text: 'Start tracking',
|
||||
color: 'blue',
|
||||
},
|
||||
branches: ['main', 'dev'],
|
||||
@@ -78,35 +55,25 @@ export const sessionTool: Tool = {
|
||||
|
||||
export const discordTool: Tool = {
|
||||
id: 'discord',
|
||||
title: 'Discord bot',
|
||||
description: 'Stats right in Discord',
|
||||
href: 'https://discord.com/application-directory/1097673957865443370',
|
||||
image: '0bWE5hC',
|
||||
button: {
|
||||
text: 'Install now',
|
||||
color: 'indigo',
|
||||
},
|
||||
};
|
||||
|
||||
export const moreTool: Tool = {
|
||||
id: 'more',
|
||||
title: 'More coming soon',
|
||||
description: "What we're making",
|
||||
href: 'https://discord.gg/nDt7AjGJQH',
|
||||
image: '1nPm6VI',
|
||||
button: {
|
||||
text: 'Join Discord',
|
||||
color: 'plum',
|
||||
},
|
||||
};
|
||||
|
||||
export const playerStatsTool: Tool = {
|
||||
id: 'player-stats',
|
||||
title: 'Player stats',
|
||||
description: 'Periodical player statistics',
|
||||
pageDescription: 'Periodical player statistics in World of Tanks Blitz',
|
||||
export const playersTool: Tool = {
|
||||
id: 'players',
|
||||
button: {
|
||||
text: 'Lookup player',
|
||||
color: 'blue',
|
||||
},
|
||||
image: 'YelSOfT',
|
||||
@@ -116,11 +83,7 @@ export const playerStatsTool: Tool = {
|
||||
|
||||
export const performanceTool: Tool = {
|
||||
id: 'performance',
|
||||
title: 'Performance',
|
||||
description: 'Tank performance statistics',
|
||||
pageDescription: 'Best tanks and their performances in World of Tanks Blitz',
|
||||
button: {
|
||||
text: 'View tanks',
|
||||
color: 'jade',
|
||||
},
|
||||
image: 'vOKFB03',
|
||||
@@ -129,12 +92,7 @@ export const performanceTool: Tool = {
|
||||
|
||||
export const embedTool: Tool = {
|
||||
id: 'embed',
|
||||
title: 'Embed',
|
||||
description: 'Embeds for streaming',
|
||||
pageDescription:
|
||||
'Create embeds for streaming World of Tanks Blitz with extreme customization',
|
||||
button: {
|
||||
text: 'Create now',
|
||||
color: 'red',
|
||||
},
|
||||
image: 'Q0faRYg',
|
||||
@@ -142,20 +100,14 @@ export const embedTool: Tool = {
|
||||
};
|
||||
|
||||
export const homeTool: Tool = {
|
||||
id: '',
|
||||
title: 'Home',
|
||||
button: { color: 'blue', text: '' },
|
||||
description: '',
|
||||
id: 'home',
|
||||
button: { color: 'blue' },
|
||||
image: 'rUPie9G',
|
||||
};
|
||||
|
||||
export const galleryTool: Tool = {
|
||||
id: 'gallery',
|
||||
title: 'Gallery',
|
||||
description: 'Blitz player avatars',
|
||||
pageDescription: 'All World of Tanks Blitz player avatars',
|
||||
button: {
|
||||
text: 'Browse all',
|
||||
color: 'gold',
|
||||
},
|
||||
image: 'VhKlknk',
|
||||
@@ -164,12 +116,8 @@ export const galleryTool: Tool = {
|
||||
|
||||
export const tierListTool: Tool = {
|
||||
id: 'tier-list',
|
||||
title: 'Tier list',
|
||||
description: 'Rank Blitz tanks',
|
||||
pageDescription:
|
||||
'Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz',
|
||||
stringsId: 'tier_list',
|
||||
button: {
|
||||
text: 'Rank tanks',
|
||||
color: 'orange',
|
||||
},
|
||||
image: 'oKKc8Xv',
|
||||
@@ -178,12 +126,7 @@ export const tierListTool: Tool = {
|
||||
|
||||
export const chartsTool: Tool = {
|
||||
id: 'charts',
|
||||
title: 'Charts',
|
||||
description: 'Lots of graphs',
|
||||
pageDescription:
|
||||
'Charts and graphs on anything and everything about tanks, players, and events in World of Tanks Blitz',
|
||||
button: {
|
||||
text: 'Nerd out',
|
||||
color: 'bronze',
|
||||
},
|
||||
image: '6fhkljA',
|
||||
|
||||
@@ -12,6 +12,7 @@ export function applyPitchYawLimits(
|
||||
pitchLimits: PitchLimits, // deg
|
||||
yawLimits?: YawLimits, // deg
|
||||
verticalStabilizer = false,
|
||||
downVerticalStabilizer = false,
|
||||
) {
|
||||
let newPitch = pitch;
|
||||
let newYaw = yaw;
|
||||
@@ -22,7 +23,8 @@ export function applyPitchYawLimits(
|
||||
);
|
||||
}
|
||||
|
||||
const depressionBuff = verticalStabilizer ? degToRad(3) : 0;
|
||||
const depressionBuff =
|
||||
verticalStabilizer || downVerticalStabilizer ? degToRad(3) : 0;
|
||||
const elevationBuff = verticalStabilizer ? degToRad(4) : 0;
|
||||
let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff;
|
||||
let upperPitch = -degToRad(pitchLimits.min) + elevationBuff;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { assertSecret, type BlitzGlossary } from '@blitzkit/core';
|
||||
|
||||
export async function fetchGlossary() {
|
||||
const response = await fetch(assertSecret(import.meta.env.WOTB_GLOSSARY));
|
||||
export async function fetchGlossary(locale: string) {
|
||||
const response = await fetch(
|
||||
`${assertSecret(
|
||||
import.meta.env.WOTB_GLOSSARY_DOMAIN,
|
||||
)}/${locale}${assertSecret(import.meta.env.WOTB_GLOSSARY_PATH)}`,
|
||||
);
|
||||
return (await response.json()) as BlitzGlossary;
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import { assertSecret } from '@blitzkit/core';
|
||||
import { DEFAULT_LOCALE, STRINGS, type SupportedLocale } from '@blitzkit/i18n';
|
||||
|
||||
const BRANCH_NAMES: Record<string, string> = {
|
||||
dev: 'beta',
|
||||
opentest: 'opentest',
|
||||
preview: 'preview',
|
||||
};
|
||||
export function resolveBranchName(locale: string = DEFAULT_LOCALE) {
|
||||
if (!(locale in STRINGS)) throw new Error(`Unsupported locale: ${locale}`);
|
||||
|
||||
export function resolveBranchName() {
|
||||
const strings = STRINGS[locale as SupportedLocale];
|
||||
const secret = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
||||
|
||||
if (
|
||||
assertSecret(import.meta.env.MODE) === 'development' &&
|
||||
secret === 'dev'
|
||||
) {
|
||||
return 'dev';
|
||||
return strings.common.branches.local;
|
||||
} else {
|
||||
return BRANCH_NAMES[secret] as string | undefined;
|
||||
return (strings.common.branches as Record<string, string>)[secret] as
|
||||
| string
|
||||
| undefined;
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ export function tankCharacteristics(
|
||||
const hasEngineAccelerator = equipment(117);
|
||||
const hasCamouflageNet = equipment(115);
|
||||
const hasImprovedVerticalStabilizer = equipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = equipment(124);
|
||||
const hasImprovedSuspension = equipment(123);
|
||||
|
||||
const hasTungsten = consumable(45);
|
||||
@@ -425,7 +426,8 @@ export function tankCharacteristics(
|
||||
const gunDepression =
|
||||
gunModelDefinition.pitch.max +
|
||||
gunDefaultPitch +
|
||||
(hasImprovedVerticalStabilizer ? 3 : 0);
|
||||
(hasImprovedVerticalStabilizer ? 3 : 0) +
|
||||
(hasDownImprovedVerticalStabilizer ? 3 : 0);
|
||||
const gunElevation =
|
||||
-gunModelDefinition.pitch.min -
|
||||
gunDefaultPitch +
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
||||
|
||||
export function getBestLocale() {
|
||||
const desiredLocale =
|
||||
localStorage.getItem('preferred-locale') ??
|
||||
navigator.language.split('-')[0];
|
||||
const isSupported = (SUPPORTED_LOCALES as unknown as string[]).includes(
|
||||
desiredLocale,
|
||||
);
|
||||
const isDefault = desiredLocale === DEFAULT_LOCALE;
|
||||
|
||||
if (!isSupported || isDefault) return undefined;
|
||||
|
||||
return desiredLocale;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DEFAULT_LOCALE, STRINGS, type SupportedLocale } from '@blitzkit/i18n';
|
||||
|
||||
export function getStrings(locale: string = DEFAULT_LOCALE) {
|
||||
if (locale in STRINGS) return STRINGS[locale as SupportedLocale];
|
||||
throw new Error(`Unsupported locale: ${locale}`);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DEFAULT_LOCALE, unwrapper } from '@blitzkit/i18n';
|
||||
import { createContext, useContext, type ReactNode } from 'react';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
|
||||
const LocaleContext = createContext<{
|
||||
locale: string;
|
||||
localeRaw: string | undefined;
|
||||
} | null>(null);
|
||||
|
||||
interface LocaleProviderProps extends LocaleAcceptorProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LocaleProvider({ locale, children }: LocaleProviderProps) {
|
||||
return (
|
||||
<LocaleContext.Provider
|
||||
value={{ locale: locale ?? DEFAULT_LOCALE, localeRaw: locale }}
|
||||
>
|
||||
{children}
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
const context = useContext(LocaleContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useLocale must be used within a LocaleProvider');
|
||||
}
|
||||
|
||||
const strings = getStrings(context.locale);
|
||||
const unwrap = unwrapper(context.locale);
|
||||
|
||||
return { locale: context.locale, strings, unwrap };
|
||||
}
|
||||
|
||||
export interface LocaleAcceptorProps {
|
||||
locale: string | undefined;
|
||||
}
|
||||
@@ -6,29 +6,18 @@ import Footer from '../components/Footer.astro';
|
||||
import Navbar from '../components/Navbar.astro';
|
||||
import Party3 from '../components/Party3.astro';
|
||||
import './base.css';
|
||||
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
||||
|
||||
export interface Props {
|
||||
noFooter?: boolean;
|
||||
}
|
||||
|
||||
const { noFooter } = Astro.props;
|
||||
const { pathname } = Astro.url;
|
||||
const isRoot = pathname === '/';
|
||||
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
{
|
||||
isRoot && (
|
||||
<>
|
||||
<title>BlitzKit</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="🎉 Tools for everything World of Tanks Blitz"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<meta property="og:site_name" content="BlitzKit" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.png" />
|
||||
@@ -36,6 +25,32 @@ const isRoot = pathname === '/';
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
|
||||
<Party3 />
|
||||
|
||||
<script>
|
||||
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
|
||||
import { getBestLocale } from '../core/i18n/getBestLocale';
|
||||
const isAPI = window.location.pathname.startsWith('/api');
|
||||
const isI18nPath = SUPPORTED_LOCALES.some((locale) =>
|
||||
window.location.pathname.startsWith(`/${locale}`),
|
||||
);
|
||||
const bestLocale = getBestLocale();
|
||||
const currentPathLocale = SUPPORTED_LOCALES.find((locale) =>
|
||||
window.location.pathname.startsWith(`/${locale}`),
|
||||
);
|
||||
|
||||
if (
|
||||
!isAPI && // not an api, duh!
|
||||
localStorage.getItem('preferred-locale') !== null && // no preference, so probably google's crawler
|
||||
bestLocale !== currentPathLocale // we need to redirect
|
||||
) {
|
||||
window.location.pathname = isI18nPath
|
||||
? window.location.pathname.replace(
|
||||
`/${currentPathLocale}`,
|
||||
bestLocale === undefined ? '' : `/${bestLocale}`,
|
||||
)
|
||||
: `/${bestLocale}${window.location.pathname}`;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -43,7 +58,7 @@ const isRoot = pathname === '/';
|
||||
<Flex direction="column" minHeight="100vh">
|
||||
<Navbar />
|
||||
|
||||
<Checks client:only="react" />
|
||||
<Checks {locale} client:only="react" />
|
||||
|
||||
<slot />
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { imgur, ImgurSize } from '@blitzkit/core';
|
||||
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import ToolOpenGraph from '../components/ToolOpenGraph.astro';
|
||||
import { type Tool } from '../constants/tools';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
|
||||
interface Props {
|
||||
@@ -10,6 +11,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const { tool } = Astro.props;
|
||||
const { locale } = Astro.params;
|
||||
const strings = getStrings(locale);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.stringsId ?? tool.id) as keyof typeof strings.website.tools
|
||||
];
|
||||
---
|
||||
|
||||
<ToolOpenGraph tool={tool} />
|
||||
@@ -33,8 +40,8 @@ const { tool } = Astro.props;
|
||||
WebkitBackdropFilter: 'blur(4rem)',
|
||||
}}
|
||||
>
|
||||
<Heading size="7">{tool?.title}</Heading>
|
||||
<Text>{tool?.description}</Text>
|
||||
<Heading size="7">{toolStrings.name}</Heading>
|
||||
<Text>{toolStrings.description}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
---
|
||||
import { DiscordLogoIcon, HomeIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { LinkI18n } from '../components/LinkI18n';
|
||||
import { PageWrapper } from '../components/PageWrapper';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
const { locale } = Astro.params;
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
@@ -12,11 +15,11 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
<Text color="gray">Where would you like to go?</Text>
|
||||
|
||||
<Flex mt="4" gap="2">
|
||||
<Link href="/">
|
||||
<LinkI18n {locale} href="/">
|
||||
<Button>
|
||||
<HomeIcon /> Home
|
||||
</Button>
|
||||
</Link>
|
||||
</LinkI18n>
|
||||
<Link href="https://discord.gg/nDt7AjGJQH">
|
||||
<Button variant="outline">
|
||||
<DiscordLogoIcon /> Discord
|
||||
|
||||
Loaded 100 of 300 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user