feat: start of sorting

This commit is contained in:
TrèsAbhi committed 2024-06-22 16:39:31 -05:00
1 parent 479a165149
commit 38b2b6025f
8 files changed
+455 -75

No files matched your search

+312 -9
View File
@@ -2,12 +2,21 @@ import { Callout, Flex } from '@radix-ui/themes';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import { range } from 'lodash'; import { range } from 'lodash';
import { memo, use, useMemo } from 'react'; import { memo, use, useMemo } from 'react';
import { Vector2Tuple } from 'three';
import { ExperimentIcon } from '../../../../components/ExperimentIcon'; import { ExperimentIcon } from '../../../../components/ExperimentIcon';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { modelDefinitions } from '../../../../core/blitzkit/modelDefinitions';
import { normalizeBoundingBox } from '../../../../core/blitzkit/normalizeBoundingBox';
import { resolveDpm } from '../../../../core/blitzkit/resolveDpm';
import { resolveReload } from '../../../../core/blitzkit/resolveReload';
import { import {
TankDefinition,
Tier, Tier,
tankDefinitions, tankDefinitions,
tankNames, tankNames,
tanksDefinitionsArray,
} from '../../../../core/blitzkit/tankDefinitions'; } from '../../../../core/blitzkit/tankDefinitions';
import { unionBoundingBox } from '../../../../core/blitzkit/unionBoundingBox';
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
import { SearchBar } from './SearchBar'; import { SearchBar } from './SearchBar';
import { TankCard } from './TankCard'; import { TankCard } from './TankCard';
@@ -15,20 +24,309 @@ import { TankCardWrapper } from './TankCardWrapper';
import { TierCard } from './TierCard'; import { TierCard } from './TierCard';
export const Results = memo(() => { export const Results = memo(() => {
const awaitedModelDefinitions = use(modelDefinitions);
const awaitedTankDefinitions = use(tankDefinitions); const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
const awaitedTankNames = use(tankNames); const awaitedTankNames = use(tankNames);
const testing = useTankopediaFilters((state) => state.testing); const testing = useTankopediaFilters((state) => state.testing);
const search = useTankopediaFilters((state) => state.search); const search = useTankopediaFilters((state) => state.search);
const sort = useTankopediaFilters((state) => state.sort);
const searchedTanks = useMemo(() => { const searchedTanks = useMemo(() => {
if (search === undefined) return undefined; if (search !== undefined) {
const searchedRaw = go(search, awaitedTankNames, { key: 'combined' });
const searchedTanks = searchedRaw.map(
(result) => awaitedTankDefinitions[result.obj.id],
);
return searchedTanks;
}
const searchedRaw = go(search, awaitedTankNames, { key: 'combined' }); if (sort.by !== 'meta.tier') {
const searchedTanks = searchedRaw.map( const filtered = awaitedTanksDefinitionsArray;
(result) => awaitedTankDefinitions[result.obj.id], let sorted: TankDefinition[];
);
return searchedTanks; switch (sort.by) {
}, [search]); case 'meta.name':
sorted = filtered.sort((a, b) => a.name?.localeCompare(b.name));
break;
case 'survivability.health': {
sorted = filtered.sort((a, b) => {
const aHealth = a.health + a.turrets.at(-1)!.health;
const bHealth = b.health + b.turrets.at(-1)!.health;
return aHealth - bHealth;
});
break;
}
case 'survivability.viewRange':
sorted = filtered.sort(
(a, b) => a.turrets.at(-1)!.viewRange - b.turrets.at(-1)!.viewRange,
);
break;
case 'survivability.camouflageStill':
sorted = filtered.sort(
(a, b) => a.camouflage.still - b.camouflage.still,
);
break;
case 'survivability.camouflageMoving':
sorted = filtered.sort(
(a, b) => a.camouflage.moving - b.camouflage.moving,
);
break;
case 'survivability.camouflageShooting':
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.camouflageLoss -
b.turrets.at(-1)!.guns.at(-1)!.camouflageLoss,
);
break;
case 'survivability.volume':
sorted = filtered.sort((a, b) => {
const aTankModelDefinition = awaitedModelDefinitions[a.id];
const bTankModelDefinition = awaitedModelDefinitions[b.id];
const aTurretModelDefinition =
aTankModelDefinition.turrets[a.turrets.at(-1)!.id];
const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.boundingBox,
aTurretModelDefinition.boundingBox,
),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.boundingBox,
bTurretModelDefinition.boundingBox,
),
);
const aVolume = aSize[0] * aSize[1] * aSize[2];
const bVolume = bSize[0] * bSize[1] * bSize[2];
return aVolume - bVolume;
});
break;
case 'survivability.length':
sorted = filtered.sort((a, b) => {
const aTankModelDefinition = awaitedModelDefinitions[a.id];
const bTankModelDefinition = awaitedModelDefinitions[b.id];
const aTurretModelDefinition =
aTankModelDefinition.turrets[a.turrets.at(-1)!.id];
const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox(
unionBoundingBox(
aTankModelDefinition.boundingBox,
aTurretModelDefinition.boundingBox,
),
);
const bSize = normalizeBoundingBox(
unionBoundingBox(
bTankModelDefinition.boundingBox,
bTurretModelDefinition.boundingBox,
),
);
const aLength = Math.max(aSize[0], aSize[1], aSize[2]);
const bLength = Math.max(bSize[0], bSize[1], bSize[2]);
return aLength - bLength;
});
break;
case 'fire.dpm':
sorted = filtered.sort(
(a, b) =>
resolveDpm(
a.turrets.at(-1)!.guns.at(-1)!,
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
) -
resolveDpm(
b.turrets.at(-1)!.guns.at(-1)!,
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
),
);
break;
case 'fire.reload':
sorted = filtered.sort(
(a, b) =>
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
);
break;
case 'fire.caliber':
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
);
break;
case 'fire.standardPenetration':
sorted = filtered.sort(
(a, b) =>
resolveNearPenetration(
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration,
) -
resolveNearPenetration(
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration,
),
);
break;
case 'fire.premiumPenetration':
sorted = filtered.sort(
(a, b) =>
resolveNearPenetration(
(
a.turrets.at(-1)!.guns.at(-1)!.shells[1] ??
a.turrets.at(-1)!.guns.at(-1)!.shells[0]
).penetration,
) -
resolveNearPenetration(
(
b.turrets.at(-1)!.guns.at(-1)!.shells[1] ??
b.turrets.at(-1)!.guns.at(-1)!.shells[0]
).penetration,
),
);
break;
case 'fire.damage':
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor,
);
break;
case 'fire.shellVelocity':
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.shells[0].speed -
b.turrets.at(-1)!.guns.at(-1)!.shells[0].speed,
);
break;
case 'fire.aimTime':
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.aimTime -
b.turrets.at(-1)!.guns.at(-1)!.aimTime,
);
break;
case 'fire.dispersionStill':
sorted = filtered.sort(
(a, b) =>
a.turrets.at(-1)!.guns.at(-1)!.dispersion.base -
b.turrets.at(-1)!.guns.at(-1)!.dispersion.base,
);
break;
case 'fire.dispersionMoving':
sorted = filtered.sort(
(a, b) =>
a.tracks.at(-1)!.dispersion.move -
b.tracks.at(-1)!.dispersion.move,
);
break;
case 'fire.gunDepression':
sorted = filtered.sort(
(a, b) =>
awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[
a.turrets.at(-1)!.guns.at(-1)!.id
].pitch.max -
awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[
b.turrets.at(-1)!.guns.at(-1)!.id
].pitch.max,
);
break;
case 'fire.gunElevation':
sorted = filtered.sort(
(a, b) =>
// reversed because of negative values
awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[
b.turrets.at(-1)!.guns.at(-1)!.id
].pitch.min -
awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[
a.turrets.at(-1)!.guns.at(-1)!.id
].pitch.min,
);
break;
case 'maneuverability.forwardsSpeed':
sorted = filtered.sort((a, b) => a.speed.forwards - b.speed.forwards);
break;
case 'maneuverability.backwardsSpeed':
sorted = filtered.sort(
(a, b) => a.speed.backwards - b.speed.backwards,
);
break;
case 'maneuverability.power':
sorted = filtered.sort(
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
);
break;
case 'maneuverability.powerToWeight':
sorted = filtered.sort(
(a, b) =>
a.engines.at(-1)!.power /
(a.weight +
a.engines.at(-1)!.weight +
a.tracks.at(-1)!.weight +
a.turrets.at(-1)!.weight +
a.turrets.at(-1)!.guns.at(-1)!.weight) -
b.engines.at(-1)!.power /
(b.weight +
b.engines.at(-1)!.weight +
b.tracks.at(-1)!.weight +
b.turrets.at(-1)!.weight +
b.turrets.at(-1)!.guns.at(-1)!.weight),
);
break;
case 'maneuverability.weight':
sorted = filtered.sort(
(a, b) =>
a.weight +
a.engines.at(-1)!.weight +
a.tracks.at(-1)!.weight +
a.turrets.at(-1)!.weight +
a.turrets.at(-1)!.guns.at(-1)!.weight -
(b.weight +
b.engines.at(-1)!.weight +
b.tracks.at(-1)!.weight +
b.turrets.at(-1)!.weight +
b.turrets.at(-1)!.guns.at(-1)!.weight),
);
break;
case 'maneuverability.traverseSpeed':
sorted = filtered.sort(
(a, b) =>
a.tracks.at(-1)!.traverseSpeed - b.tracks.at(-1)!.traverseSpeed,
);
break;
}
return sort.direction === 'ascending' ? sorted : sorted.reverse();
}
return undefined;
}, [search, sort]);
return ( return (
<Flex direction="column" gap="4" flexGrow="1"> <Flex direction="column" gap="4" flexGrow="1">
@@ -47,9 +345,14 @@ export const Results = memo(() => {
)} )}
{!search && {!search &&
range(10, 0).map((tier) => <TierCard key={tier} tier={tier as Tier} />)} sort.by === 'meta.tier' &&
range(
...(sort.direction === 'descending'
? ([10, 0] as Vector2Tuple)
: ([1, 11] as Vector2Tuple)),
).map((tier) => <TierCard key={tier} tier={tier as Tier} />)}
{search && ( {(search || sort.by !== 'meta.tier') && (
<TankCardWrapper py="4"> <TankCardWrapper py="4">
{searchedTanks?.map((tank) => <TankCard key={tank.id} tank={tank} />)} {searchedTanks?.map((tank) => <TankCard key={tank.id} tank={tank} />)}
</TankCardWrapper> </TankCardWrapper>
@@ -1,11 +1,12 @@
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { Button, Spinner, TextField } from '@radix-ui/themes'; import { Button, Flex, Spinner, TextField } from '@radix-ui/themes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { KeyboardEventHandler, useCallback, useRef, useState } from 'react'; import { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
import { Link } from '../../../../components/Link'; import { Link } from '../../../../components/Link';
import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
import { Sort } from './Sort';
interface SearchBarProps { interface SearchBarProps {
topResult?: TankDefinition; topResult?: TankDefinition;
@@ -44,26 +45,31 @@ export function SearchBar({ topResult }: SearchBarProps) {
); );
return ( return (
<TextField.Root <Flex gap="2">
ref={input} <TextField.Root
placeholder="Search tanks..." style={{ flex: 1 }}
size="3" ref={input}
onChange={handleChange} placeholder="Search tanks..."
onKeyDown={handleKeyDown} size="3"
> onChange={handleChange}
<TextField.Slot> onKeyDown={handleKeyDown}
{searching ? <Spinner /> : <MagnifyingGlassIcon />} >
</TextField.Slot>
{topResult && (
<TextField.Slot> <TextField.Slot>
<Link href={`/tools/tankopedia/${topResult.id}`}> {searching ? <Spinner /> : <MagnifyingGlassIcon />}
<Button variant="ghost" size="3">
{topResult.name} <CaretRightIcon />
</Button>
</Link>
</TextField.Slot> </TextField.Slot>
)}
</TextField.Root> {topResult && (
<TextField.Slot>
<Link href={`/tools/tankopedia/${topResult.id}`}>
<Button variant="ghost" size="3">
{topResult.name} <CaretRightIcon />
</Button>
</Link>
</TextField.Slot>
)}
</TextField.Root>
<Sort />
</Flex>
); );
} }
@@ -1,23 +1,25 @@
import { CaretDownIcon } from '@radix-ui/react-icons'; import { CaretDownIcon } from '@radix-ui/react-icons';
import { Button, DropdownMenu } from '@radix-ui/themes'; import { Button, DropdownMenu } from '@radix-ui/themes';
import mutateTankopediaPersistent, { import { TankopediaSortDirection } from '../../../../stores/tankopedia';
TankopediaSortDirection, import {
useTankopediaPersistent, mutateTankopediaFilters,
} from '../../../../../../stores/tankopedia'; useTankopediaFilters,
} from '../../../../stores/tankopediaFilters';
export function Sort() { export function Sort() {
const sort = useTankopediaPersistent((state) => state.sort); const sort = useTankopediaFilters((state) => state.sort);
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<Button variant="soft" color="gray"> <Button variant="surface" size="3" color="gray">
Sort Sort
<CaretDownIcon /> <CaretDownIcon />
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>By</DropdownMenu.Label> <DropdownMenu.Content align="end">
<DropdownMenu.Label>Sort by</DropdownMenu.Label>
<DropdownMenu.Sub> <DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Meta</DropdownMenu.SubTrigger> <DropdownMenu.SubTrigger>Meta</DropdownMenu.SubTrigger>
@@ -25,7 +27,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'meta.tier'} checked={sort.by === 'meta.tier'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'meta.tier'; draft.sort.by = 'meta.tier';
}); });
}} }}
@@ -35,7 +37,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'meta.name'} checked={sort.by === 'meta.name'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'meta.name'; draft.sort.by = 'meta.name';
}); });
}} }}
@@ -51,7 +53,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.dpm'} checked={sort.by === 'fire.dpm'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.dpm'; draft.sort.by = 'fire.dpm';
}); });
}} }}
@@ -61,7 +63,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.reload'} checked={sort.by === 'fire.reload'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.reload'; draft.sort.by = 'fire.reload';
}); });
}} }}
@@ -71,7 +73,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.caliber'} checked={sort.by === 'fire.caliber'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.caliber'; draft.sort.by = 'fire.caliber';
}); });
}} }}
@@ -81,7 +83,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.standardPenetration'} checked={sort.by === 'fire.standardPenetration'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.standardPenetration'; draft.sort.by = 'fire.standardPenetration';
}); });
}} }}
@@ -91,7 +93,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.premiumPenetration'} checked={sort.by === 'fire.premiumPenetration'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.premiumPenetration'; draft.sort.by = 'fire.premiumPenetration';
}); });
}} }}
@@ -101,7 +103,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.damage'} checked={sort.by === 'fire.damage'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.damage'; draft.sort.by = 'fire.damage';
}); });
}} }}
@@ -111,7 +113,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.shellVelocity'} checked={sort.by === 'fire.shellVelocity'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.shellVelocity'; draft.sort.by = 'fire.shellVelocity';
}); });
}} }}
@@ -121,7 +123,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.aimTime'} checked={sort.by === 'fire.aimTime'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.aimTime'; draft.sort.by = 'fire.aimTime';
}); });
}} }}
@@ -131,7 +133,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.dispersionStill'} checked={sort.by === 'fire.dispersionStill'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.dispersionStill'; draft.sort.by = 'fire.dispersionStill';
}); });
}} }}
@@ -141,7 +143,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.dispersionMoving'} checked={sort.by === 'fire.dispersionMoving'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.dispersionMoving'; draft.sort.by = 'fire.dispersionMoving';
}); });
}} }}
@@ -151,7 +153,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.gunDepression'} checked={sort.by === 'fire.gunDepression'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.gunDepression'; draft.sort.by = 'fire.gunDepression';
}); });
}} }}
@@ -161,7 +163,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'fire.gunElevation'} checked={sort.by === 'fire.gunElevation'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.gunElevation'; draft.sort.by = 'fire.gunElevation';
}); });
}} }}
@@ -177,7 +179,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'maneuverability.forwardsSpeed'} checked={sort.by === 'maneuverability.forwardsSpeed'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'maneuverability.forwardsSpeed'; draft.sort.by = 'maneuverability.forwardsSpeed';
}); });
}} }}
@@ -187,7 +189,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'maneuverability.backwardsSpeed'} checked={sort.by === 'maneuverability.backwardsSpeed'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'maneuverability.backwardsSpeed'; draft.sort.by = 'maneuverability.backwardsSpeed';
}); });
}} }}
@@ -197,7 +199,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'maneuverability.power'} checked={sort.by === 'maneuverability.power'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'maneuverability.power'; draft.sort.by = 'maneuverability.power';
}); });
}} }}
@@ -207,7 +209,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'maneuverability.powerToWeight'} checked={sort.by === 'maneuverability.powerToWeight'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'maneuverability.powerToWeight'; draft.sort.by = 'maneuverability.powerToWeight';
}); });
}} }}
@@ -217,7 +219,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'maneuverability.weight'} checked={sort.by === 'maneuverability.weight'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'maneuverability.weight'; draft.sort.by = 'maneuverability.weight';
}); });
}} }}
@@ -227,7 +229,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'maneuverability.traverseSpeed'} checked={sort.by === 'maneuverability.traverseSpeed'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'maneuverability.traverseSpeed'; draft.sort.by = 'maneuverability.traverseSpeed';
}); });
}} }}
@@ -243,7 +245,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.health'} checked={sort.by === 'survivability.health'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.health'; draft.sort.by = 'survivability.health';
}); });
}} }}
@@ -253,7 +255,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.viewRange'} checked={sort.by === 'survivability.viewRange'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.viewRange'; draft.sort.by = 'survivability.viewRange';
}); });
}} }}
@@ -263,7 +265,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.camouflageStill'} checked={sort.by === 'survivability.camouflageStill'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.camouflageStill'; draft.sort.by = 'survivability.camouflageStill';
}); });
}} }}
@@ -273,7 +275,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.camouflageMoving'} checked={sort.by === 'survivability.camouflageMoving'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.camouflageMoving'; draft.sort.by = 'survivability.camouflageMoving';
}); });
}} }}
@@ -283,7 +285,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.camouflageShooting'} checked={sort.by === 'survivability.camouflageShooting'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.camouflageShooting'; draft.sort.by = 'survivability.camouflageShooting';
}); });
}} }}
@@ -293,7 +295,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.volume'} checked={sort.by === 'survivability.volume'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.volume'; draft.sort.by = 'survivability.volume';
}); });
}} }}
@@ -303,7 +305,7 @@ export function Sort() {
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={sort.by === 'survivability.length'} checked={sort.by === 'survivability.length'}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.by = 'survivability.length'; draft.sort.by = 'survivability.length';
}); });
}} }}
@@ -319,7 +321,7 @@ export function Sort() {
<DropdownMenu.RadioGroup <DropdownMenu.RadioGroup
value={sort.direction} value={sort.direction}
onValueChange={(value) => onValueChange={(value) =>
mutateTankopediaPersistent((draft) => { mutateTankopediaFilters((draft) => {
draft.sort.direction = value as TankopediaSortDirection; draft.sort.direction = value as TankopediaSortDirection;
}) })
} }
@@ -1,7 +1,9 @@
import { Flex, Link, Text } from '@radix-ui/themes'; import { Flex, Link, Text } from '@radix-ui/themes';
import { useEffect, useRef } from 'react'; import { use, useEffect, useMemo, useRef } from 'react';
import { classIcons } from '../../../../components/ClassIcon'; import { classIcons } from '../../../../components/ClassIcon';
import { asset } from '../../../../core/blitzkit/asset'; import { asset } from '../../../../core/blitzkit/asset';
import { modelDefinitions } from '../../../../core/blitzkit/modelDefinitions';
import { resolveDpm } from '../../../../core/blitzkit/resolveDpm';
import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
import { tankIcon } from '../../../../core/blitzkit/tankIcon'; import { tankIcon } from '../../../../core/blitzkit/tankIcon';
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank'; import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
@@ -12,8 +14,36 @@ interface TankCardProps {
} }
export function TankCard({ tank }: TankCardProps) { export function TankCard({ tank }: TankCardProps) {
const awaitedModelDefinitions = use(modelDefinitions);
const Icon = classIcons[tank.class]; const Icon = classIcons[tank.class];
const link = useRef<HTMLAnchorElement>(null); const link = useRef<HTMLAnchorElement>(null);
const sortBy = useTankopediaFilters((state) => state.sort.by);
const discriminator = useMemo(() => {
if (sortBy.startsWith('meta')) return undefined;
const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!;
const shell = gun.shells[0];
const tracks = tank.tracks.at(-1)!;
const tankModelDefinition = awaitedModelDefinitions[tank.id];
switch (sortBy) {
case 'fire.aimTime':
return `${gun.aimTime.toFixed(2)}s`;
case 'fire.caliber':
return `${shell.caliber.toFixed(0)}mm`;
case 'fire.damage':
return `${shell.damage.armor.toFixed(0)}hp`;
case 'fire.dispersionMoving':
return `+ ${tracks.dispersion.move.toFixed(3)}`;
case 'fire.dispersionStill':
return gun.dispersion.base.toFixed(3);
case 'fire.dpm':
return resolveDpm(gun, shell).toFixed(0);
case 'fire.gunDepression':
return awaitedModelDefinitions;
}
}, [sortBy]);
useEffect(() => { useEffect(() => {
const unsubscribe = useTankopediaFilters.subscribe((filters) => { const unsubscribe = useTankopediaFilters.subscribe((filters) => {
@@ -89,6 +119,21 @@ export function TankCard({ tank }: TankCardProps) {
{tank.name} {tank.name}
</Text> </Text>
</Flex> </Flex>
{discriminator && (
<Text
mt="-2"
color="gray"
align="center"
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{discriminator}
</Text>
)}
</Link> </Link>
); );
} }
@@ -31,9 +31,9 @@ import mutateTankopediaPersistent, {
useTankopediaPersistent, useTankopediaPersistent,
} from '../../../../../stores/tankopedia'; } from '../../../../../stores/tankopedia';
import { PageTurner } from '../PageTurner'; import { PageTurner } from '../PageTurner';
import { Sort } from '../Sort';
import { Options } from './components/Options'; import { Options } from './components/Options';
import { Results } from './components/Results'; import { Results } from './components/Results';
import { Sort } from './components/Sort';
import { treeTypeOrder } from './constants'; import { treeTypeOrder } from './constants';
interface TankSearchProps { interface TankSearchProps {
@@ -1,5 +1,7 @@
import { Flex, Heading, Separator } from '@radix-ui/themes'; import { Flex, Heading, Separator } from '@radix-ui/themes';
import { memo, use, useEffect, useMemo, useRef } from 'react'; import { memo, use, useEffect, useMemo, useRef } from 'react';
import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants';
import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions';
import { import {
Tier, Tier,
tanksDefinitionsArray, tanksDefinitionsArray,
@@ -9,6 +11,7 @@ import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilter
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
import { TankCard } from './TankCard'; import { TankCard } from './TankCard';
import { TankCardWrapper } from './TankCardWrapper'; import { TankCardWrapper } from './TankCardWrapper';
import { treeTypeOrder } from './TankSearch/constants';
interface TierCardProps { interface TierCardProps {
tier: Tier; tier: Tier;
@@ -16,10 +19,27 @@ interface TierCardProps {
export const TierCard = memo( export const TierCard = memo(
({ tier }: TierCardProps) => { ({ tier }: TierCardProps) => {
const awaitedGameDefinitions = use(gameDefinitions);
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const tanks = useMemo( const tanks = useMemo(
() => awaitedTanksDefinitionsArray.filter((tank) => tank.tier === tier), () =>
awaitedTanksDefinitionsArray
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
treeTypeOrder.indexOf(a.treeType) -
treeTypeOrder.indexOf(b.treeType),
)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation),
),
[tier], [tier],
); );
+1 -6
View File
@@ -8,12 +8,7 @@ import { Results } from './components/Results';
export default function Page() { export default function Page() {
return ( return (
<PageWrapper color="purple" size={1028 + 256}> <PageWrapper color="purple" size={1028 + 256}>
<Flex <Flex gap="8" direction={{ initial: 'column', sm: 'row' }} pb="6">
gap="8"
direction={{ initial: 'column', sm: 'row' }}
pt={{ initial: '0', sm: '6' }}
pb="6"
>
<FilterControl /> <FilterControl />
<Results /> <Results />
</Flex> </Flex>
+9
View File
@@ -2,6 +2,7 @@ import { produce } from 'immer';
import { create } from 'zustand'; import { create } from 'zustand';
import { TankClass, TreeType } from '../components/Tanks'; import { TankClass, TreeType } from '../components/Tanks';
import { Tier } from '../core/blitzkit/tankDefinitions'; import { Tier } from '../core/blitzkit/tankDefinitions';
import { TankopediaSortBy, TankopediaSortDirection } from './tankopedia';
export interface TankopediaFilters { export interface TankopediaFilters {
tier?: Tier; tier?: Tier;
@@ -10,10 +11,18 @@ export interface TankopediaFilters {
type?: TreeType; type?: TreeType;
testing: 'include' | 'exclude' | 'only'; testing: 'include' | 'exclude' | 'only';
search?: string; search?: string;
sort: {
by: TankopediaSortBy;
direction: TankopediaSortDirection;
};
} }
export const useTankopediaFilters = create<TankopediaFilters>()(() => ({ export const useTankopediaFilters = create<TankopediaFilters>()(() => ({
testing: 'include', testing: 'include',
sort: {
by: 'meta.tier',
direction: 'descending',
},
})); }));
export function mutateTankopediaFilters( export function mutateTankopediaFilters(