diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx index b8a7015af..99a786684 100644 --- a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx @@ -3,6 +3,7 @@ import { use } from 'react'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox'; +import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm'; import { GUN_TYPE_NAMES, SHELL_NAMES, @@ -40,23 +41,6 @@ export function Characteristics() { stockTurret.weight + stockGun.weight; - let dpm: number; - - if (gun.type === 'regular') { - dpm = (shell.damage.armor / gun.reload) * 60; - } else if (gun.type === 'autoLoader') { - dpm = - ((shell.damage.armor * gun.count) / - (gun.reload + (gun.count - 1) * gun.interClip)) * - 60; - } else { - dpm = - ((shell.damage.armor * gun.count) / - (gun.reload.reduce((a, b) => a + b, 0) + - (gun.count - 1) * gun.interClip)) * - 60; - } - return ( @@ -95,7 +79,7 @@ export function Characteristics() { Fire {GUN_TYPE_NAMES[gun.type]} - {dpm.toFixed(0)} + {resolveDpm(gun, shell).toFixed(0)} {gun.type === 'autoReloader' && ( <> @@ -206,10 +190,10 @@ export function Characteristics() { - {gunModelDefinition.pitch.max} + {gunModelDefinition.pitch.max.toFixed(1)} - {-gunModelDefinition.pitch.min} + {(-gunModelDefinition.pitch.min).toFixed(1)} {gunModelDefinition.pitch.front && ( <> @@ -242,10 +226,13 @@ export function Characteristics() { Maneuverability - {tank.speed.forwards} + {tank.speed.forwards.toFixed(0)} - - {tank.speed.backwards} + + {tank.speed.backwards.toFixed(0)} + + + {engine.power} diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx index d31f86f75..ea71ef255 100644 --- a/src/app/tools/tankopedia/components/TankSearch/index.tsx +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -16,7 +16,12 @@ import { import { go } from 'fuzzysort'; import { use, useEffect, useMemo, useRef, useState } from 'react'; import { TankType, TreeType } from '../../../../../components/Tanks'; +import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration'; import { asset } from '../../../../../core/blitzkrieg/asset'; +import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; +import { normalizeBoundingBox } from '../../../../../core/blitzkrieg/normalizeBoundingBox'; +import { resolveDpm } from '../../../../../core/blitzkrieg/resolveDpm'; +import { resolveReload } from '../../../../../core/blitzkrieg/resolveReload'; import { NATIONS, TANK_ICONS, @@ -27,9 +32,9 @@ import { tanksDefinitionsArray, } from '../../../../../core/blitzkrieg/tankDefinitions'; import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon'; +import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox'; import { theme } from '../../../../../stitches.config'; import mutateTankopediaPersistent, { - TankopediaSortBy, TankopediaSortDirection, useTankopediaPersistent, } from '../../../../../stores/tankopedia'; @@ -52,6 +57,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) { const filters = useTankopediaPersistent((state) => state.filters); const sort = useTankopediaPersistent((state) => state.sort); const awaitedTanks = use(tanksDefinitionsArray); + const awaitedModelDefinitions = use(modelDefinitions); const defaultSortedTanks = useMemo( () => awaitedTanks @@ -68,43 +74,296 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) { [], ); const input = useRef(null); - const searchableTanks = useMemo( - () => - defaultSortedTanks - .filter( - (tank) => - (filters.tiers.length === 0 - ? true - : filters.tiers.includes(tank.tier)) && - (filters.types.length === 0 - ? true - : filters.types.includes(tank.type)) && - (filters.treeTypes.length === 0 - ? true - : filters.treeTypes.includes(tank.treeType)) && - (filters.nations.length === 0 - ? true - : filters.nations.includes(tank.nation)) && - (filters.test === 'include' - ? true - : filters.test === 'exclude' - ? !tank.testing - : tank.testing), - ) - .sort((a, b) => { - let diff = 0; + const searchableTanks = useMemo(() => { + const filtered = defaultSortedTanks.filter( + (tank) => + (filters.tiers.length === 0 + ? true + : filters.tiers.includes(tank.tier)) && + (filters.types.length === 0 + ? true + : filters.types.includes(tank.type)) && + (filters.treeTypes.length === 0 + ? true + : filters.treeTypes.includes(tank.treeType)) && + (filters.nations.length === 0 + ? true + : filters.nations.includes(tank.nation)) && + (filters.test === 'include' + ? true + : filters.test === 'exclude' + ? !tank.testing + : tank.testing), + ); + let sorted: TankDefinition[] = []; - if (sort.by === 'tier') { - diff = a.tier - b.tier; - } - if (sort.by === 'name') { - diff = a.name.localeCompare(b.name); - } + // .sort((a, b) => { + // let diff = 0; - return sort.direction === 'ascending' ? diff : -diff; - }), - [filters, sort], - ); + // if (sort.by === 'meta.tier') { + // diff = a.tier - b.tier; + // } + // if (sort.by === 'meta.name') { + // diff = a.name?.localeCompare(b.name); + // } + + // return sort.direction === 'ascending' ? diff : -diff; + // }); + + switch (sort.by) { + case 'meta.tier': + sorted = filtered.sort((a, b) => a.tier - b.tier); + break; + + 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.size': + 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 '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(); + }, [filters, sort]); const [searchResults, setSearchedList] = useState(searchableTanks); const page = useTankopediaPersistent((state) => state.filters.page); const chunkSize = @@ -118,6 +377,9 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) { const firstChunk = searchResultsPageSlice.slice(0, chunkSize); const secondChunk = searchResultsPageSlice.slice(chunkSize); + useEffect(() => { + setSearchedList(searchableTanks); + }, [searchableTanks]); useEffect(() => { mutateTankopediaPersistent((draft) => { draft.filters.page = Math.min( @@ -166,21 +428,292 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) { By - - mutateTankopediaPersistent((draft) => { - draft.sort.by = value as TankopediaSortBy; - }) - } - > - - Tier - - - Name - - + + + Meta + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'meta.tier'; + }); + }} + > + Tier + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'meta.name'; + }); + }} + > + Name + + + + + + Survivability + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.health'; + }); + }} + > + Health + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.viewRange'; + }); + }} + > + View range + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.camouflageStill'; + }); + }} + > + Camouflage + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.camouflageMoving'; + }); + }} + > + Camouflage moving + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.camouflageShooting'; + }); + }} + > + Camouflage shooting + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.size'; + }); + }} + > + Size + + + + + + Fire + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.dpm'; + }); + }} + > + DPM + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.reload'; + }); + }} + > + Reload + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.caliber'; + }); + }} + > + Caliber + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.standardPenetration'; + }); + }} + > + Standard penetration + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.premiumPenetration'; + }); + }} + > + Premium penetration + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.damage'; + }); + }} + > + Damage + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.shellVelocity'; + }); + }} + > + Shell velocity + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.aimTime'; + }); + }} + > + Aim time + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.dispersionStill'; + }); + }} + > + Dispersion still + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.dispersionMoving'; + }); + }} + > + Dispersion moving + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.gunDepression'; + }); + }} + > + Gun depression + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.gunElevation'; + }); + }} + > + Gun elevation + + + + + + + Maneuverability + + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.forwardsSpeed'; + }); + }} + > + Forwards speed + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.backwardsSpeed'; + }); + }} + > + Backwards speed + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.power'; + }); + }} + > + Power + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.powerToWeight'; + }); + }} + > + Power to weight + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.weight'; + }); + }} + > + Weight + + { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.traverseSpeed'; + }); + }} + > + Traverse speed + + + diff --git a/src/core/blitzkrieg/resolveDpm.ts b/src/core/blitzkrieg/resolveDpm.ts new file mode 100644 index 000000000..63739d39e --- /dev/null +++ b/src/core/blitzkrieg/resolveDpm.ts @@ -0,0 +1,20 @@ +import { GunDefinition, ShellDefinition } from './tankDefinitions'; + +export function resolveDpm(gun: GunDefinition, shell: ShellDefinition) { + if (gun.type === 'regular') { + return (shell.damage.armor / gun.reload) * 60; + } else if (gun.type === 'autoLoader') { + return ( + ((shell.damage.armor * gun.count) / + (gun.reload + (gun.count - 1) * gun.interClip)) * + 60 + ); + } else { + return ( + ((shell.damage.armor * gun.count) / + (gun.reload.reduce((a, b) => a + b, 0) + + (gun.count - 1) * gun.interClip)) * + 60 + ); + } +} diff --git a/src/core/blitzkrieg/resolveReload.ts b/src/core/blitzkrieg/resolveReload.ts new file mode 100644 index 000000000..358683428 --- /dev/null +++ b/src/core/blitzkrieg/resolveReload.ts @@ -0,0 +1,13 @@ +import { GunDefinition } from './tankDefinitions'; + +export function resolveReload(gun: GunDefinition) { + if (gun.type === 'regular') { + return gun.reload; + } else if (gun.type === 'autoLoader') { + return gun.reload + (gun.count - 1) * gun.interClip; + } else { + return ( + gun.reload.reduce((a, b) => a + b, 0) + (gun.count - 1) * gun.interClip + ); + } +} diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 02b39c6df..266edf075 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -15,7 +15,33 @@ import { TurretDefinition, } from '../core/blitzkrieg/tankDefinitions'; -export type TankopediaSortBy = 'tier' | 'name'; +export type TankopediaSortBy = + | 'meta.tier' + | 'meta.name' + | 'survivability.health' + | 'survivability.viewRange' + | 'survivability.camouflageStill' + | 'survivability.camouflageMoving' + | 'survivability.camouflageShooting' + | 'survivability.size' + | 'fire.dpm' + | 'fire.reload' + | 'fire.caliber' + | 'fire.standardPenetration' + | 'fire.premiumPenetration' + | 'fire.damage' + | 'fire.shellVelocity' + | 'fire.aimTime' + | 'fire.dispersionStill' + | 'fire.dispersionMoving' + | 'fire.gunDepression' + | 'fire.gunElevation' + | 'maneuverability.forwardsSpeed' + | 'maneuverability.backwardsSpeed' + | 'maneuverability.power' + | 'maneuverability.powerToWeight' + | 'maneuverability.weight' + | 'maneuverability.traverseSpeed'; export type TankopediaSortDirection = 'ascending' | 'descending'; export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only'; export type TankopediaMode = 'model' | 'armor'; @@ -116,7 +142,7 @@ export const useTankopediaPersistent = create()( }, }, sort: { - by: 'tier', + by: 'meta.tier', direction: 'descending', }, filters: {