diff --git a/packages/core/src/blitzkit/createDefaultEnvironment.ts b/packages/core/src/blitzkit/createDefaultEnvironment.ts new file mode 100644 index 000000000..f1408d0dd --- /dev/null +++ b/packages/core/src/blitzkit/createDefaultEnvironment.ts @@ -0,0 +1,11 @@ +import { SkillDefinitions } from "@blitzkit/protos"; +import { createDefaultSkills } from "./createDefaultSkills"; +import { TankEnvironment } from "./tankEnvironment"; + +export function createDefaultEnvironment(skills: SkillDefinitions) { + return { + equalize: false, + distance: 0, + skills: createDefaultSkills(skills), + } satisfies TankEnvironment; +} diff --git a/packages/core/src/blitzkit/index.ts b/packages/core/src/blitzkit/index.ts index 4c0660d83..1623326ad 100644 --- a/packages/core/src/blitzkit/index.ts +++ b/packages/core/src/blitzkit/index.ts @@ -3,6 +3,7 @@ export * from "./api"; export * from "./assertSecret"; export * from "./availableProvisions"; export * from "./coefficient"; +export * from "./createDefaultEnvironment"; export * from "./createDefaultProvisions"; export * from "./createDefaultSkills"; export * from "./degressiveStat"; @@ -24,6 +25,7 @@ export * from "./sluggify"; export * from "./staticAsset"; export * from "./sum"; export * from "./tankDefinitions"; +export * from "./tankEnvironment"; export * from "./tankIcon"; export * from "./tankNames"; export * from "./unionBoundingBox"; diff --git a/packages/core/src/blitzkit/tankEnvironment.ts b/packages/core/src/blitzkit/tankEnvironment.ts new file mode 100644 index 000000000..a0f2da159 --- /dev/null +++ b/packages/core/src/blitzkit/tankEnvironment.ts @@ -0,0 +1,5 @@ +export interface TankEnvironment { + equalize: boolean; + distance: number; + skills: Record; +} diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index e051cbd3b..c3b9c7624 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -484,53 +484,15 @@ }, "sort": { - "meta.none": "none", - "survivability.health": "health", - "survivability.viewRange": "view range", - "survivability.camouflageStill": "camouflage still", - "survivability.camouflageMoving": "camouflage moving", - "survivability.camouflageShooting": "camouflage shooting", - "survivability.volume": "volume", - "survivability.length": "length", - "fire.dpm": "standard DPM", - "fire.dpmPremium": "premium DPM", - "fire.reload": "reload", - "fire.caliber": "caliber", - "fire.standardPenetration": "standard penetration", - "fire.premiumPenetration": "premium penetration", - "fire.tertiaryPenetration": "tertiary penetration", - "fire.damage": "damage", - "fire.moduleDamage": "module damage", - "fire.shellVelocity": "shell velocity", - "fire.shellRange": "shell range", - "fire.shellCapacity": "shell capacity", - "fire.aimTime": "aim time", - "fire.dispersionStill": "dispersion still", - "fire.dispersionMoving": "dispersion moving", - "fire.gunDepression": "gun depression", - "fire.gunElevation": "gun elevation", - "maneuverability.forwardsSpeed": "forwards speed", - "maneuverability.backwardsSpeed": "backwards speed", - "maneuverability.power": "power", - "maneuverability.powerToWeight": "power to weight", - "maneuverability.weight": "weight", - "maneuverability.traverseSpeed": "traverse speed" - }, - "sort_dropdown": { "label": "Sort", - "groups": { - "sort": { - "no_sort": "No sort", - "label": "Sort by", - "fire": "Fire", - "maneuverability": "Maneuverability", - "survivability": "Survivability" - }, - "direction": { - "label": "Direction", - "ascending": "Ascending", - "descending": "Descending" - } + + "none": "None", + "by": "Sort by", + + "direction": { + "label": "Direction", + "ascending": "Ascending", + "descending": "Descending" } } }, diff --git a/packages/website/src/components/RecentlyViewedTanks/index.tsx b/packages/website/src/components/RecentlyViewedTanks/index.tsx index 28ce99d78..8aef642f9 100644 --- a/packages/website/src/components/RecentlyViewedTanks/index.tsx +++ b/packages/website/src/components/RecentlyViewedTanks/index.tsx @@ -33,7 +33,7 @@ export function RecentlyViewedTanks() { const strings = useStrings(); const by = TankSort.use((state) => state.by); - if (recentlyViewed.length === 0 || by !== "meta.none" || hasFilters) { + if (recentlyViewed.length === 0 || by !== null || hasFilters) { return null; } diff --git a/packages/website/src/components/TankCard/index.module.css b/packages/website/src/components/TankCard/index.module.css index bfac648ec..fd496b3f5 100644 --- a/packages/website/src/components/TankCard/index.module.css +++ b/packages/website/src/components/TankCard/index.module.css @@ -40,4 +40,10 @@ overflow: hidden; } } + + .discriminator { + display: flex; + justify-content: center; + gap: var(--space-2); + } } diff --git a/packages/website/src/components/TankCard/index.tsx b/packages/website/src/components/TankCard/index.tsx index b2f4b0f1e..5aaaac71a 100644 --- a/packages/website/src/components/TankCard/index.tsx +++ b/packages/website/src/components/TankCard/index.tsx @@ -82,7 +82,7 @@ export const TankCard = (props: MaybeSkeletonComponentProps) => { {props.discriminator && ( - {props.discriminator} +
{props.discriminator}
)} diff --git a/packages/website/src/components/TankSearch/components/Card.tsx b/packages/website/src/components/TankSearch/components/Card.tsx deleted file mode 100644 index 980275688..000000000 --- a/packages/website/src/components/TankSearch/components/Card.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - normalizeBoundingBox, - resolveDpm, - unionBoundingBox, -} from "@blitzkit/core"; -import type { TankDefinition } from "@blitzkit/protos"; -import { useMemo } from "react"; -import { api } from "../../../api/dynamic"; -import { resolveReload } from "../../../core/blitzkit/resolveReload"; -import { TankSort } from "../../../stores/tankopediaSort"; -import { TankCard } from "../../TankCard"; - -interface TankSearchCardProps { - tank: TankDefinition; - onSelect?: (tank: TankDefinition) => void; -} - -const modelDefinitions = await api.models(); - -export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) { - const by = TankSort.use((state) => state.by); - const discriminator = useMemo(() => { - if (by.startsWith("meta")) return undefined; - - const turret = tank.turrets.at(-1)!; - const gun = turret.guns.at(-1)!; - const shell0 = gun.shells[0]; - const shell1 = gun.shells[1]; - const shell2 = gun.shells[2]; - const tracks = tank.tracks.at(-1)!; - const engine = tank.engines.at(-1)!; - const tankModelDefinition = modelDefinitions.models[tank.id]; - const turretModelDefinition = tankModelDefinition.turrets[turret.id]; - const gunModelDefinition = turretModelDefinition.guns[gun.id]; - - switch (by) { - case "fire.aimTime": - return gun.aim_time.toFixed(2); - case "fire.caliber": - return shell0.caliber.toFixed(0); - case "fire.damage": - return shell0.armor_damage.toFixed(0); - case "fire.moduleDamage": - return shell0.module_damage.toFixed(0); - case "fire.dispersionMoving": - return tracks.dispersion_move.toFixed(3); - case "fire.dispersionStill": - return gun.dispersion_base.toFixed(3); - case "fire.dpm": - return resolveDpm(gun, shell0).toFixed(0); - case "fire.dpmPremium": - return shell1 ? resolveDpm(gun, shell1).toFixed(0) : "--"; - case "fire.reload": - return resolveReload(gun).toFixed(2); - case "fire.standardPenetration": - return shell0?.penetration!.near.toFixed(0); - case "fire.premiumPenetration": - return shell1 ? shell1?.penetration!.near.toFixed(0) : "--"; - case "fire.tertiaryPenetration": - return shell1 ? shell2?.penetration!.near.toFixed(0) : "--"; - case "fire.shellVelocity": - return shell0.velocity.toFixed(0); - case "fire.shellCapacity": - return gun.shell_capacity.toFixed(0); - case "fire.shellRange": - return shell0.range.toFixed(0); - case "fire.gunDepression": - return ( - gunModelDefinition.pitch!.max + - (tankModelDefinition.initial_turret_rotation?.pitch ?? 0) - ).toFixed(1); - case "fire.gunElevation": - return ( - -gunModelDefinition.pitch!.min - - (tankModelDefinition.initial_turret_rotation?.pitch ?? 0) - ).toFixed(1); - case "maneuverability.forwardsSpeed": - return tank.speed_forwards.toFixed(0); - case "maneuverability.backwardsSpeed": - return tank.speed_backwards.toFixed(0); - case "maneuverability.power": - return engine.power.toFixed(0); - case "maneuverability.powerToWeight": - return ( - (1000 * engine.power) / - (tank.weight + - engine.weight + - tracks.weight + - turret.weight + - gun.weight) - ).toFixed(1); - case "maneuverability.weight": - return ( - (tank.weight + - engine.weight + - tracks.weight + - turret.weight + - gun.weight) / - 1000 - ).toFixed(1); - case "maneuverability.traverseSpeed": - return tracks.traverse_speed.toFixed(1); - case "survivability.health": - return (tank.health + turret.health).toFixed(0); - case "survivability.viewRange": - return turret.view_range.toFixed(0); - case "survivability.camouflageStill": - return (tank.camouflage_still * 100).toFixed(0); - case "survivability.camouflageMoving": - return (tank.camouflage_moving * 100).toFixed(0); - case "survivability.camouflageShooting": - return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0); - case "survivability.volume": { - const dimensions = normalizeBoundingBox( - unionBoundingBox( - tankModelDefinition.bounding_box!, - turretModelDefinition.bounding_box!, - ), - ); - - return (dimensions.x * dimensions.y * dimensions.z).toFixed(0); - } - - case "survivability.length": { - const bounds = normalizeBoundingBox( - unionBoundingBox( - tankModelDefinition.bounding_box!, - turretModelDefinition.bounding_box!, - ), - ); - - return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0); - } - } - }, [by]); - - return ( - - ); -} diff --git a/packages/website/src/components/TankSearch/components/Sort.tsx b/packages/website/src/components/TankSearch/components/Sort.tsx index c598d8ee1..d48045ae6 100644 --- a/packages/website/src/components/TankSearch/components/Sort.tsx +++ b/packages/website/src/components/TankSearch/components/Sort.tsx @@ -1,40 +1,37 @@ import { CaretDownIcon } from "@radix-ui/react-icons"; +import { characteristicsGroups } from "../../../config/characteristicsGroups"; import { useStrings } from "../../../hooks/useStrings"; -import type { - TankopediaSortBy, - TankopediaSortDirection, -} from "../../../stores/tankopediaPersistent"; +import { TankopediaSortDirection } from "../../../stores/tankopediaPersistent"; import { TankSort } from "../../../stores/tankopediaSort"; +import { sortableCharacteristics } from "../../../tankopedia/sortableCharacteristics"; +import type { CharacteristicName } from "../../../types/characteristics"; import { Button } from "../../Button"; import { DropdownMenu } from "../../DropdownMenu"; interface ItemProps { - by: TankopediaSortBy; -} - -interface ItemProps { - by: TankopediaSortBy; + by: CharacteristicName; } function Item({ by }: ItemProps) { const strings = useStrings(); - const _by = TankSort.use((state) => state.by); + const selectedBy = TankSort.use((state) => state.by); return ( { TankSort.mutate((draft) => { draft.by = by; }); }} > - {strings.website.common.tank_search.sort[by]} + {strings.tanks.characteristics.items[by]} ); } export function Sort() { + const by = TankSort.use((state) => state.by); const direction = TankSort.use((state) => state.direction); const strings = useStrings(); @@ -42,103 +39,79 @@ export function Sort() { - {strings.website.common.tank_search.sort_dropdown.groups.sort.label} + {strings.website.common.tank_search.sort.direction.label} - + { + TankSort.mutate((draft) => { + draft.by = null; + }); + }} + > + {strings.website.common.tank_search.sort.none} + - - - {strings.website.common.tank_search.sort_dropdown.groups.sort.fire} - - - - - - - - - - - - - - - - - - - - - + {characteristicsGroups.map((group) => { + const items = group.order.filter( + (item) => "name" in item && sortableCharacteristics.has(item.name), + ); - - - { - strings.website.common.tank_search.sort_dropdown.groups.sort - .maneuverability - } - - - - - - - - - - + return ( + + + { + ( + strings.tanks.characteristics.titles as Record< + string, + string + > + )[group.name] + } + - - - { - strings.website.common.tank_search.sort_dropdown.groups.sort - .survivability - } - - - - - - - - - - - + + {items.map((item) => { + if (!("name" in item)) return null; + + return ; + })} + + + ); + })} - { - strings.website.common.tank_search.sort_dropdown.groups.direction - .label - } + {strings.website.common.tank_search.sort.direction.label} { + if ( + value !== TankopediaSortDirection.Ascending && + value !== TankopediaSortDirection.Descending + ) { + throw new Error(`Unknown tankopedia sort direction: ${value}`); + } + TankSort.mutate((draft) => { - draft.direction = value as TankopediaSortDirection; + draft.direction = value; }); }} > - - { - strings.website.common.tank_search.sort_dropdown.groups.direction - .ascending - } + + {strings.website.common.tank_search.sort.direction.ascending} - - { - strings.website.common.tank_search.sort_dropdown.groups.direction - .descending - } + + {strings.website.common.tank_search.sort.direction.descending} diff --git a/packages/website/src/components/TankSearch/components/TierFilter.tsx b/packages/website/src/components/TankSearch/components/TierFilter.tsx index 910608bfc..95238d501 100644 --- a/packages/website/src/components/TankSearch/components/TierFilter.tsx +++ b/packages/website/src/components/TankSearch/components/TierFilter.tsx @@ -12,7 +12,9 @@ export const TierFilter = memo(() => { const search = TankFilters.use((state) => state.search); const tiers = TankFilters.use((state) => state.tiers); - if (by !== "meta.none" || search) return null; + if (by !== null || search) { + return null; + } return ( diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index aac24e30d..471272081 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -1,9 +1,4 @@ -import { - metaSortTank, - normalizeBoundingBox, - SEARCH_KEYS, - unionBoundingBox, -} from "@blitzkit/core"; +import { metaSortTank, SEARCH_KEYS } from "@blitzkit/core"; import { literals } from "@blitzkit/i18n/src/literals"; import type { TankDefinition } from "@blitzkit/protos"; import fuzzysort from "fuzzysort"; @@ -11,16 +6,26 @@ import { uniq } from "lodash-es"; import { memo, useMemo } from "react"; import usePromise from "react-promise-suspense"; import { api } from "../../api/dynamic"; +import { characteristics } from "../../config/characteristics"; +import { characteristicRenderConfigs } from "../../config/characteristicsGroups"; import { awaitableTankNames } from "../../core/awaitables/tankNames"; import { filterTanks } from "../../core/blitzkit/filterTanks"; -import { resolveReload } from "../../core/blitzkit/resolveReload"; +import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer"; +import { computeDefaultCharacteristics } from "../../hooks/useCharacteristics"; import { useLocale } from "../../hooks/useLocale"; import { useStrings } from "../../hooks/useStrings"; import { App } from "../../stores/app"; import { TankFilters } from "../../stores/tankFilters"; -import { TankopediaPersistent } from "../../stores/tankopediaPersistent"; -import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants"; +import { + TankopediaPersistent, + TankopediaSortDirection, +} from "../../stores/tankopediaPersistent"; import { TankSort } from "../../stores/tankopediaSort"; +import { sortableCharacteristics } from "../../tankopedia/sortableCharacteristics"; +import { + CharacteristicType, + type ComputedCharacteristics, +} from "../../types/characteristics"; import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps"; import { Callout } from "../Callout"; import { ExperimentIcon } from "../ExperimentIcon"; @@ -43,390 +48,170 @@ export type TankSearchProps = MaybeSkeletonComponentProps & { onSelectAll?: (tanks: TankDefinition[]) => void; }; -function resolveDpm(a: any, b: any) { - return 0; -} - -const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] = - await Promise.all([ - api.game(), - api.models(), - api.tanks(), - awaitableTankNames, - ]); +const [gameDefinitions, tankDefinitions, tankNames] = await Promise.all([ + api.game(), + api.tanks(), + awaitableTankNames, +]); +const tankDefinitionsArray = Object.values(tankDefinitions.tanks); export const TankSearch = memo( ({ compact, onSelect, onSelectAll, skeleton }) => { const strings = useStrings(); const locale = useLocale(); + const wargaming = App.use((state) => state.logins.wargaming); - const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks); + const tankopediaSort = TankSort.use(); const tankFilters = TankFilters.use(); + const defaultCharacteristics = useMemo(() => { + const computed = new Map(); + + for (const tank of tankDefinitionsArray) { + computed.set(tank.id, computeDefaultCharacteristics(tank)); + } + + return computed; + }, []); + const filtered = usePromise( - () => { - if (tankFilters.search === null) { - return filterTanks( - tankFilters, - awaitedTanksDefinitionsArray, - wargaming?.id, - ); - } else { - return Promise.resolve( - fuzzysort - .go(tankFilters.search, tankNames, { keys: SEARCH_KEYS }) - .map((result) => tankDefinitions.tanks[result.obj.id]), + async () => { + let filtered = tankDefinitionsArray; + + console.log("A"); + + if (tankopediaSort.by !== null) { + console.log("B", filtered.length); + + filtered = filtered.filter((tank) => + defaultCharacteristics.get(tank.id)!.has(tankopediaSort.by!), ); + + console.log("C", filtered.length); } + + if (tankFilters.search === null) { + filtered = await filterTanks(tankFilters, filtered, wargaming?.id); + } else { + filtered = fuzzysort + .go(tankFilters.search, tankNames, { keys: SEARCH_KEYS }) + .map((result) => tankDefinitions.tanks[result.obj.id]); + } + + return filtered; }, // react-promise-suspense has awful type annotations - [tankFilters] as any, + [tankFilters, tankopediaSort.by] as any, ); + const sorted = useMemo(() => { - if (tankFilters.search === null) { - let sorted: TankDefinition[]; + if (tankFilters.search !== null) return filtered; - switch (tankopediaSort.by) { - case "meta.none": - sorted = metaSortTank(filtered, gameDefinitions); - break; + if ( + tankopediaSort.by === null || + !sortableCharacteristics.has(tankopediaSort.by) + ) { + const byMeta = metaSortTank([...filtered], gameDefinitions); - 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 tankopediaSort.direction === TankopediaSortDirection.Ascending + ? byMeta + : byMeta.reverse(); + } + + const values = new Map(); + const by = tankopediaSort.by; + const characteristic = characteristics[by]; + + for (const tank of filtered) { + const computedCharacteristics = defaultCharacteristics.get(tank.id)!; + if (!computedCharacteristics.has(by)) { + values.set(tank.id, undefined); + continue; + } + + const value = computedCharacteristics.get(by); + + let sortValue: number | undefined; + + switch (characteristic.type) { + case CharacteristicType.Number: { + sortValue = value as number; - return aHealth - bHealth; - }); break; } - case "survivability.viewRange": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range, + case CharacteristicType.NumberList: { + const values = value as number[]; + if (values.length === 0) { + sortValue = undefined; + break; + } + + let sum = 0; + + for (const item of values) { + sum += item; + } + + sortValue = sum / values.length; + + break; + } + + default: + throw new Error( + `Unsortable characteristic type ${characteristic.type}`, ); - 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)!.camouflage_loss - - b.camouflage_still * - b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss, - ); - break; - - case "survivability.volume": - sorted = filtered.sort((a, b) => { - const aTankModelDefinition = modelDefinitions.models[a.id]; - const bTankModelDefinition = modelDefinitions.models[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.bounding_box!, - aTurretModelDefinition.bounding_box!, - ), - ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.bounding_box!, - bTurretModelDefinition.bounding_box!, - ), - ); - const aVolume = aSize.x * aSize.y * aSize.z; - const bVolume = bSize.x * bSize.y * bSize.z; - - return aVolume - bVolume; - }); - break; - - case "survivability.length": - sorted = filtered.sort((a, b) => { - const aTankModelDefinition = modelDefinitions.models[a.id]; - const bTankModelDefinition = modelDefinitions.models[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.bounding_box!, - aTurretModelDefinition.bounding_box!, - ), - ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.bounding_box!, - bTurretModelDefinition.bounding_box!, - ), - ); - const aLength = Math.max(aSize.x, aSize.y, aSize.z); - const bLength = Math.max(bSize.x, bSize.y, bSize.z); - - 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.dpmPremium": - sorted = filtered.sort( - (a, b) => - (a.turrets.at(-1)!.guns.at(-1)!.shells[1] - ? resolveDpm( - a.turrets.at(-1)!.guns.at(-1)!, - a.turrets.at(-1)!.guns.at(-1)!.shells[1], - ) - : 0) - - (b.turrets.at(-1)!.guns.at(-1)!.shells[1] - ? resolveDpm( - b.turrets.at(-1)!.guns.at(-1)!, - b.turrets.at(-1)!.guns.at(-1)!.shells[1], - ) - : 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) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near, - ); - break; - - case "fire.premiumPenetration": - sorted = filtered.sort( - (a, b) => - (a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ?? - 0) - - (b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ?? - 0), - ); - break; - - case "fire.tertiaryPenetration": - sorted = filtered.sort( - (a, b) => - (a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ?? - 0) - - (b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ?? - 0), - ); - break; - - case "fire.damage": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage, - ); - break; - - case "fire.moduleDamage": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage, - ); - break; - - case "fire.shellVelocity": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity, - ); - break; - - case "fire.shellRange": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].range - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].range, - ); - break; - - case "fire.shellCapacity": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shell_capacity - - b.turrets.at(-1)!.guns.at(-1)!.shell_capacity, - ); - break; - - case "fire.aimTime": - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.aim_time - - b.turrets.at(-1)!.guns.at(-1)!.aim_time, - ); - 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) => - modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id] - .guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max + - (modelDefinitions.models[a.id].initial_turret_rotation?.pitch ?? - 0) - - modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id] - .guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max - - (modelDefinitions.models[b.id].initial_turret_rotation?.pitch ?? - 0), - ); - break; - - case "fire.gunElevation": - sorted = filtered.sort( - (a, b) => - modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id] - .guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min + - (modelDefinitions.models[b.id].initial_turret_rotation?.pitch ?? - 0) - - modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id] - .guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min - - (modelDefinitions.models[a.id].initial_turret_rotation?.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 / - (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)!.traverse_speed - - b.tracks.at(-1)!.traverse_speed, - ); - break; } - return tankopediaSort.direction === "ascending" - ? sorted - : sorted.reverse(); - } else { - return filtered; + values.set(tank.id, sortValue); } + + const direction = + tankopediaSort.direction === TankopediaSortDirection.Ascending ? 1 : -1; + + return [...filtered].sort((a, b) => { + const aValue = values.get(a.id); + const bValue = values.get(b.id); + + if (aValue === undefined) return bValue === undefined ? 0 : 1; + if (bValue === undefined) return -1; + + if (aValue === bValue) return 0; + + return (aValue < bValue ? -1 : 1) * direction; + }); }, [tankFilters, tankopediaSort]); - const data = sorted.map((tank) => ({ - key: `${tank.id}`, - tank, - onTankSelect: onSelect, - })); + console.log("V", sorted.length); + + const sortConfig = + tankopediaSort.by !== null && + sortableCharacteristics.has(tankopediaSort.by) + ? tankopediaSort.by + : undefined; + + const renderCharacteristic = useCharacteristicRenderer(); + + const data = sorted.map((tank) => { + const tankDefaultCharacteristics = defaultCharacteristics.get(tank.id)!; + const shouldRenderDiscriminator = + tankopediaSort.by !== null && + tankDefaultCharacteristics.has(tankopediaSort.by); + + return { + key: `${tank.id}`, + tank, + discriminator: shouldRenderDiscriminator + ? renderCharacteristic( + tankDefaultCharacteristics.get(tankopediaSort.by!)!, + characteristicRenderConfigs.get(tankopediaSort.by!)!, + ) + : null, + onTankSelect: onSelect, + }; + }); return ( @@ -473,16 +258,16 @@ export const TankSearch = memo( )} - {tankopediaSort.by !== "meta.none" && ( + {sortConfig && ( {literals(strings.website.common.tank_search.sorting_by, { - name: strings.website.common.tank_search.sort[ - tankopediaSort.by - ], + name: ( + strings.tanks.characteristics.items as Record< + string, + string + > + )[sortConfig], })} - {SORT_UNITS[tankopediaSort.by] === undefined - ? "" - : ` (${SORT_UNITS[tankopediaSort.by]})`} , {tankopediaSort.direction} )} diff --git a/packages/website/src/config/characteristicsGroups.ts b/packages/website/src/config/characteristicsGroups.ts index 40a11270e..1d4602efb 100644 --- a/packages/website/src/config/characteristicsGroups.ts +++ b/packages/website/src/config/characteristicsGroups.ts @@ -1,7 +1,11 @@ import type { CaseType } from "@blitzkit/core"; import type { GunDefinition } from "@blitzkit/protos"; -import { CharacteristicRanking } from "../types/characteristics"; -import type { CharacteristicsGroup } from "../types/characteristics"; +import { + CharacteristicRanking, + type CharacteristicName, + type CharacteristicRenderConfig, + type CharacteristicsGroup, +} from "../types/characteristics"; export const characteristicsGroups: CharacteristicsGroup[] = [ // { @@ -299,3 +303,15 @@ export const characteristicsGroups: CharacteristicsGroup[] = [ ], }, ]; + +export const characteristicRenderConfigs = new Map< + CharacteristicName, + CharacteristicRenderConfig +>(); + +for (const group of characteristicsGroups) { + for (const config of group.order) { + if (!("name" in config)) continue; + characteristicRenderConfigs.set(config.name, config); + } +} diff --git a/packages/website/src/hooks/useCharacteristics.ts b/packages/website/src/hooks/useCharacteristics.ts index 14f45a901..50d279eee 100644 --- a/packages/website/src/hooks/useCharacteristics.ts +++ b/packages/website/src/hooks/useCharacteristics.ts @@ -1,10 +1,16 @@ -import { degressiveStat, progressiveStat } from "@blitzkit/core"; +import { + createDefaultEnvironment, + degressiveStat, + progressiveStat, + type TankEnvironment, +} from "@blitzkit/core"; import type { BlitzEffectScript } from "@blitzkit/core/src/types/blitzEffectScript"; -import { CrewType } from "@blitzkit/protos"; +import { CrewType, type TankDefinition } from "@blitzkit/protos"; import { api } from "../api/dynamic"; import { characteristics } from "../config/characteristics"; import { defaultEqualizer } from "../config/equalizer"; -import { Tankopedia, type TankEnvironment } from "../stores/tankopedia"; +import { Tankopedia } from "../stores/tankopedia"; +import { createTankState } from "../tankopedia/createTankState"; import type { TankState } from "../tankopedia/tankState"; import type { CharacteristicContext, @@ -16,14 +22,7 @@ import { duelSides, type DuelSide } from "./useEquipment"; const tanks = await api.tanks(); const scripts = await api.scripts(); const equipment = await api.equipment(); - -export function useCharacteristics() { - const protagonist = Tankopedia.use((state) => state.protagonist); - const antagonist = Tankopedia.use((state) => state.antagonist); - const environment = Tankopedia.use((state) => state.environment); - - return computeCharacteristics({ protagonist, antagonist }, environment); -} +const skills = await api.skills(); export function computeCharacteristics( states: Record, @@ -191,3 +190,20 @@ export function computeCharacteristics( return computed; } + +export function useCharacteristics() { + const protagonist = Tankopedia.use((state) => state.protagonist); + const antagonist = Tankopedia.use((state) => state.antagonist); + const environment = Tankopedia.use((state) => state.environment); + + return computeCharacteristics({ protagonist, antagonist }, environment); +} + +export function computeDefaultCharacteristics(tank: TankDefinition) { + const state = createTankState(tank); + + return computeCharacteristics( + { protagonist: state, antagonist: state }, + createDefaultEnvironment(skills), + ); +} diff --git a/packages/website/src/stores/tankopedia.ts b/packages/website/src/stores/tankopedia.ts index b62261d05..343d03b3a 100644 --- a/packages/website/src/stores/tankopedia.ts +++ b/packages/website/src/stores/tankopedia.ts @@ -1,4 +1,4 @@ -import { createDefaultSkills } from "@blitzkit/core"; +import { createDefaultEnvironment, type TankEnvironment } from "@blitzkit/core"; import type { ShellDefinition, TankDefinition } from "@blitzkit/protos"; import { Soapstone } from "soapstone"; import type { Vector3 } from "three"; @@ -76,12 +76,6 @@ export enum TankopediaRelativeAgainst { All, } -export interface TankEnvironment { - equalize: boolean; - distance: number; - skills: Record; -} - interface Tankopedia { disturbed: boolean; revealed: boolean; @@ -128,11 +122,7 @@ export const Tankopedia = new Soapstone( antagonist: createTankState(tank), model: models.models[tank.id], - environment: { - equalize: false, - distance: 0, - skills: createDefaultSkills(skills), - }, + environment: createDefaultEnvironment(skills), relative_against: TankopediaRelativeAgainst.ClassAndTier, diff --git a/packages/website/src/stores/tankopediaPersistent/constants.ts b/packages/website/src/stores/tankopediaPersistent/constants.ts index 9ec03a40a..16f149cf2 100644 --- a/packages/website/src/stores/tankopediaPersistent/constants.ts +++ b/packages/website/src/stores/tankopediaPersistent/constants.ts @@ -1,39 +1,3 @@ -import type { TankopediaSortBy } from '.'; - -export const SORT_UNITS: Record = { - 'meta.none': undefined, - 'survivability.health': 'hp', - 'fire.aimTime': 's', - 'fire.caliber': 'mm', - 'fire.damage': 'hp', - 'fire.moduleDamage': 'hp', - 'fire.dispersionMoving': 'm', - 'fire.dispersionStill': 'm', - 'fire.dpm': undefined, - 'fire.dpmPremium': undefined, - 'fire.gunDepression': '°', - 'fire.gunElevation': '°', - 'fire.premiumPenetration': 'mm', - 'fire.tertiaryPenetration': 'mm', - 'fire.reload': 's', - 'fire.shellVelocity': 'm/s', - 'fire.shellRange': 'm', - 'fire.shellCapacity': undefined, - 'fire.standardPenetration': 'mm', - 'maneuverability.backwardsSpeed': 'kph', - 'maneuverability.forwardsSpeed': 'kph', - 'maneuverability.power': 'hp', - 'maneuverability.powerToWeight': 'hp/tn', - 'maneuverability.traverseSpeed': '°/s', - 'maneuverability.weight': 'tn', - 'survivability.camouflageMoving': '%', - 'survivability.camouflageShooting': '%', - 'survivability.camouflageStill': '%', - 'survivability.length': 'm', - 'survivability.viewRange': 'm', - 'survivability.volume': 'm^3', -}; - export enum TankopediaDisplay { Model, DynamicArmor, diff --git a/packages/website/src/stores/tankopediaPersistent/index.ts b/packages/website/src/stores/tankopediaPersistent/index.ts index 62ea18a73..24cb9b583 100644 --- a/packages/website/src/stores/tankopediaPersistent/index.ts +++ b/packages/website/src/stores/tankopediaPersistent/index.ts @@ -1,9 +1,9 @@ -import type en from "@blitzkit/i18n/strings/en.json"; import { Soapstone } from "soapstone"; -export type TankopediaSortBy = keyof typeof en.website.common.tank_search.sort; -export type TankopediaSortDirection = "ascending" | "descending"; -export type TankopediaTestTankDisplay = "include" | "exclude" | "only"; +export enum TankopediaSortDirection { + Ascending, + Descending, +} export interface TankopediaPersistent { wireframe: boolean; @@ -16,11 +16,6 @@ export interface TankopediaPersistent { recentlyViewed: number[]; hideTankModelUnderArmor: boolean; highGraphics: boolean; - - sort: { - by: TankopediaSortBy; - direction: TankopediaSortDirection; - }; } export const TankopediaPersistent = new Soapstone( @@ -35,10 +30,6 @@ export const TankopediaPersistent = new Soapstone( showPrimaryArmor: true, recentlyViewed: [], highGraphics: true, - sort: { - by: "meta.none", - direction: "ascending", - }, }, "tankopedia-3", ); diff --git a/packages/website/src/stores/tankopediaSort.ts b/packages/website/src/stores/tankopediaSort.ts index 112eff789..6a480eea9 100644 --- a/packages/website/src/stores/tankopediaSort.ts +++ b/packages/website/src/stores/tankopediaSort.ts @@ -1,15 +1,13 @@ -import { Soapstone } from 'soapstone'; -import { - type TankopediaSortBy, - type TankopediaSortDirection, -} from './tankopediaPersistent'; +import { Soapstone } from "soapstone"; +import type { CharacteristicName } from "../types/characteristics"; +import { TankopediaSortDirection } from "./tankopediaPersistent"; export interface TankSort { - by: TankopediaSortBy; + by: CharacteristicName | null; direction: TankopediaSortDirection; } export const TankSort = new Soapstone({ - by: 'meta.none', - direction: 'descending', + by: null, + direction: TankopediaSortDirection.Descending, }); diff --git a/packages/website/src/tankopedia/sortableCharacteristics.ts b/packages/website/src/tankopedia/sortableCharacteristics.ts new file mode 100644 index 000000000..ddc2b356e --- /dev/null +++ b/packages/website/src/tankopedia/sortableCharacteristics.ts @@ -0,0 +1,28 @@ +import { characteristics } from "../config/characteristics"; +import { characteristicsGroups } from "../config/characteristicsGroups"; +import { + CharacteristicType, + type CharacteristicName, +} from "../types/characteristics"; + +const sortableCharacteristicTypes = new Set([ + CharacteristicType.Number, + CharacteristicType.NumberList, +]); + +export const sortableCharacteristics = new Set(); + +for (const group of characteristicsGroups) { + for (const config of group.order) { + if (!("name" in config)) continue; + + // TODO: remove this when all characteristics are defined + if (!(config.name in characteristics)) continue; + + const item = characteristics[config.name]; + + if (!sortableCharacteristicTypes.has(item.type)) continue; + + sortableCharacteristics.add(config.name); + } +} diff --git a/packages/website/src/types/characteristics.ts b/packages/website/src/types/characteristics.ts index 46e155e15..9bf95fbc1 100644 --- a/packages/website/src/types/characteristics.ts +++ b/packages/website/src/types/characteristics.ts @@ -1,3 +1,4 @@ +import type { TankEnvironment } from "@blitzkit/core"; import type { BlitzEffectScript } from "@blitzkit/core/src/types/blitzEffectScript"; import type { Strings } from "@blitzkit/i18n"; import type { @@ -13,7 +14,6 @@ import type { ReactNode } from "react"; import type { characteristics } from "../config/characteristics"; import type { toys } from "../config/toys"; import type { DuelSide } from "../hooks/useEquipment"; -import type { TankEnvironment } from "../stores/tankopedia"; import type { TankState } from "../tankopedia/tankState"; export type Characteristic = {