diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx index be04fb43e..56a333d2e 100644 --- a/src/app/tools/tankopedia/components/TankSearch/index.tsx +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -1,4 +1,4 @@ -import { Callout, Flex, Text } from '@radix-ui/themes'; +import { Callout, Flex, Link, Text } from '@radix-ui/themes'; import { go } from 'fuzzysort'; import { times } from 'lodash'; import { memo, use, useMemo } from 'react'; @@ -31,381 +31,403 @@ import { treeTypeOrder } from './constants'; interface TankSearchProps { compact?: boolean; onSelect?: (tank: TankDefinition) => void; + onSelectAll?: (tanks: TankDefinition[]) => void; } -export const TankSearch = memo(({ compact, onSelect }) => { - const awaitedGameDefinitions = use(gameDefinitions); - const awaitedModelDefinitions = use(modelDefinitions); - const awaitedTankDefinitions = use(tankDefinitions); - const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); - const awaitedTankNames = use(tankNames); - const filters = useTankopediaFilters(); - const tanks = useMemo(() => { - if (filters.search === undefined) { - const filtered = awaitedTanksDefinitionsArray.filter((tank) => - tankopediaFilterTank(filters, tank), - ); - let sorted: TankDefinition[]; +export const TankSearch = memo( + ({ compact, onSelect, onSelectAll }) => { + const awaitedGameDefinitions = use(gameDefinitions); + const awaitedModelDefinitions = use(modelDefinitions); + const awaitedTankDefinitions = use(tankDefinitions); + const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); + const awaitedTankNames = use(tankNames); + const filters = useTankopediaFilters(); + const tanks = useMemo(() => { + if (filters.search === undefined) { + const filtered = awaitedTanksDefinitionsArray.filter((tank) => + tankopediaFilterTank(filters, tank), + ); + let sorted: TankDefinition[]; - switch (filters.sort.by) { - case 'meta.none': - sorted = filtered - .sort( + switch (filters.sort.by) { + case 'meta.none': + sorted = filtered + .sort( + (a, b) => + treeTypeOrder.indexOf(b.treeType) - + treeTypeOrder.indexOf(a.treeType), + ) + .sort( + (a, b) => + TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class), + ) + .sort( + (a, b) => + awaitedGameDefinitions.nations.indexOf(b.nation) - + awaitedGameDefinitions.nations.indexOf(a.nation), + ); + 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) => - treeTypeOrder.indexOf(b.treeType) - - treeTypeOrder.indexOf(a.treeType), - ) - .sort( + 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) => - TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class), - ) - .sort( + a.camouflage.still * + a.turrets.at(-1)!.guns.at(-1)!.camouflageLoss - + b.camouflage.still * + 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) => - awaitedGameDefinitions.nations.indexOf(b.nation) - - awaitedGameDefinitions.nations.indexOf(a.nation), + 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; + 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.camouflage.still * - a.turrets.at(-1)!.guns.at(-1)!.camouflageLoss - - b.camouflage.still * - 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, - ), + case 'fire.reload': + sorted = filtered.sort( + (a, b) => + resolveReload(a.turrets.at(-1)!.guns.at(-1)!) - + resolveReload(b.turrets.at(-1)!.guns.at(-1)!), ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.boundingBox, - bTurretModelDefinition.boundingBox, - ), + 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, ); - const aVolume = aSize[0] * aSize[1] * aSize[2]; - const bVolume = bSize[0] * bSize[1] * bSize[2]; + break; - 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, - ), + 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, + ), ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.boundingBox, - bTurretModelDefinition.boundingBox, - ), + break; + + case 'fire.premiumPenetration': + sorted = filtered.sort( + (a, b) => + resolveNearPenetration( + a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, + ) - + resolveNearPenetration( + b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, + ), ); - const aLength = Math.max(aSize[0], aSize[1], aSize[2]); - const bLength = Math.max(bSize[0], bSize[1], bSize[2]); + break; - return aLength - bLength; - }); - 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.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.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.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.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.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.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.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.dispersionMoving': + sorted = filtered.sort( + (a, b) => + a.tracks.at(-1)!.dispersion.move - + b.tracks.at(-1)!.dispersion.move, + ); + break; - case 'fire.premiumPenetration': - sorted = filtered.sort( - (a, b) => - resolveNearPenetration( - a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, - ) - - resolveNearPenetration( - b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, - ), - ); - 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[a.id].turretRotation?.pitch ?? 0) - + awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id] + .guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max - + (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0), + ); + 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.gunElevation': + sorted = filtered.sort( + (a, b) => + awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id] + .guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.min + + (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0) - + awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id] + .guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min - + (awaitedModelDefinitions[a.id].turretRotation?.pitch ?? 0), + ); + 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 'maneuverability.forwardsSpeed': + sorted = filtered.sort( + (a, b) => a.speed.forwards - b.speed.forwards, + ); + 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 'maneuverability.backwardsSpeed': + sorted = filtered.sort( + (a, b) => a.speed.backwards - b.speed.backwards, + ); + 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 'maneuverability.power': + sorted = filtered.sort( + (a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power, + ); + break; - case 'fire.dispersionMoving': - sorted = filtered.sort( - (a, b) => - a.tracks.at(-1)!.dispersion.move - - b.tracks.at(-1)!.dispersion.move, - ); - 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 '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[a.id].turretRotation?.pitch ?? 0) - - awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ - b.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.max - - (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0), - ); - break; - - case 'fire.gunElevation': - sorted = filtered.sort( - (a, b) => - awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ - b.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.min + - (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0) - - awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ - a.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.min - - (awaitedModelDefinitions[a.id].turretRotation?.pitch ?? 0), - ); - 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 / + 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; + ); + 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; + } - case 'maneuverability.traverseSpeed': - sorted = filtered.sort( - (a, b) => - a.tracks.at(-1)!.traverseSpeed - b.tracks.at(-1)!.traverseSpeed, - ); - break; + return filters.sort.direction === 'ascending' + ? sorted + : sorted.reverse(); + } else { + const searchedRaw = go(filters.search, awaitedTankNames, { + key: 'combined', + limit: 25, + }); + const searchedTanks = searchedRaw.map( + (result) => awaitedTankDefinitions[result.obj.id], + ); + return searchedTanks; } + }, [filters]); - return filters.sort.direction === 'ascending' ? sorted : sorted.reverse(); - } else { - const searchedRaw = go(filters.search, awaitedTankNames, { - key: 'combined', - limit: 25, - }); - const searchedTanks = searchedRaw.map( - (result) => awaitedTankDefinitions[result.obj.id], - ); - return searchedTanks; - } - }, [filters]); + return ( + + - return ( - - + {!filters.search && !filters.searching && ( + + )} - {!filters.search && !filters.searching && ( - - )} + {(filters.sort.by !== 'meta.none' || onSelectAll) && ( + + {filters.sort.by !== 'meta.none' && ( + + Sorting by {SORT_NAMES[filters.sort.by]} + {SORT_UNITS[filters.sort.by] === undefined + ? '' + : ` (${SORT_UNITS[filters.sort.by]})`} + , {filters.sort.direction} + + )} - {filters.sort.by !== 'meta.none' && ( - - - Sorting by {SORT_NAMES[filters.sort.by]} - {SORT_UNITS[filters.sort.by] === undefined - ? '' - : ` (${SORT_UNITS[filters.sort.by]})`} - , {filters.sort.direction} - - - )} + {onSelectAll && ( + onSelectAll(tanks)} + > + Select all {tanks.length} tanks + + )} + + )} - {filters.testing === 'only' && ( - - - - - - - Tanks in testing are subject to change and many not represent the - final product. - - - - )} + {filters.testing === 'only' && ( + + + + + + + Tanks in testing are subject to change and many not represent + the final product. + + + + )} - {!filters.searching && ( - <> - {tanks.length > 0 && ( - - {tanks.map((tank) => ( - - ))} - - )} + {!filters.searching && ( + <> + {tanks.length > 0 && ( + + {tanks.map((tank) => ( + + ))} + + )} - {tanks.length === 0 && } - - )} + {tanks.length === 0 && } + + )} - {filters.searching && ( - - {times(Math.round(10 + 10 * Math.random()), (index) => ( - - ))} - - )} - - ); -}); + {filters.searching && ( + + {times(Math.round(10 + 10 * Math.random()), (index) => ( + + ))} + + )} + + ); + }, +);