diff --git a/bun.lockb b/bun.lockb index 374b1c460..2e7334ffa 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 9197143a4..76c3e4176 100644 --- a/package.json +++ b/package.json @@ -29,32 +29,32 @@ }, "license": "MIT", "dependencies": { - "@gltf-transform/core": "^3.9.0", + "@gltf-transform/core": "^3.10.1", "@nivo/bar": "^0.87.0", - "@octokit/plugin-throttling": "^8.1.3", - "@octokit/rest": "^20.0.1", - "@prisma/client": "^5.4.1", + "@octokit/plugin-throttling": "^8.2.0", + "@octokit/rest": "^20.1.1", + "@prisma/client": "^5.16.1", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/themes": "^3.0.5", - "@react-three/drei": "^9.93.0", - "@react-three/fiber": "^8.15.14", - "@resvg/resvg-js": "^2.4.1", + "@radix-ui/themes": "^3.1.1", + "@react-three/drei": "^9.108.3", + "@react-three/fiber": "^8.16.8", + "@resvg/resvg-js": "^2.6.2", "@types/three": "^0.160.0", - "@vanilla-extract/css": "^1.13.0", + "@vanilla-extract/css": "^1.15.3", "@vanilla-extract/dynamic": "^2.1.1", - "adm-zip": "^0.5.12", + "adm-zip": "^0.5.14", "bepaint": "^0.1.0-beta.3", "csv-parse": "^5.5.6", "decompress": "^4.2.1", - "discord.js": "^14.13.0", - "dotenv": "^16.3.1", - "esbuild": "^0.19.3", - "express": "^4.18.2", + "discord.js": "^14.15.3", + "dotenv": "^16.4.5", + "esbuild": "^0.19.12", + "express": "^4.19.2", "false": "^0.0.4", "fuzzysort": "^2.0.4", "googleapis": "^134.0.0", - "immer": "^10.0.2", + "immer": "^10.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "lz4js": "^0.2.0", @@ -63,49 +63,49 @@ "next": "^14.2.4", "parse-dds": "^1.2.1", "pickleparser": "^0.2.1", - "postprocessing": "^6.34.1", + "postprocessing": "^6.35.6", "progress": "^2.0.3", - "protobufjs": "^7.2.6", + "protobufjs": "^7.3.2", "react": "^18.3.1", "react-device-detect": "^2.2.3", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-fuzzysort": "^1.0.1", "react-promise-suspense": "^0.3.4", - "satori": "^0.10.13", + "satori": "^0.10.14", "sharp": "^0.33.4", "three": "0.159.0", - "three-stdlib": "^2.29.4", - "vite-node": "^0.34.5", - "vitepress": "^1.0.0-rc.20", - "vitepress-plugin-auto-sidebar": "^1.1.0", - "yaml": "^2.3.4", - "zustand": "^4.4.1" + "three-stdlib": "^2.30.4", + "vite-node": "^0.34.7", + "vitepress": "^1.3.0", + "vitepress-plugin-auto-sidebar": "^1.2.0", + "yaml": "^2.4.5", + "zustand": "^4.5.4" }, "devDependencies": { "@total-typescript/ts-reset": "^0.5.1", "@types/adm-zip": "^0.5.5", - "@types/bun": "^1.1.5", + "@types/bun": "^1.1.6", "@types/decompress": "^4.2.7", - "@types/escape-html": "^1.0.2", - "@types/express": "^4.17.18", - "@types/js-yaml": "^4.0.8", - "@types/lodash": "^4.14.199", + "@types/escape-html": "^1.0.4", + "@types/express": "^4.17.21", + "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.6", "@types/lz4": "^0.6.4", "@types/lz4js": "^0.2.1", - "@types/markdown-escape": "^1.1.0", - "@types/node": "^20.6.5", + "@types/markdown-escape": "^1.1.3", + "@types/node": "^20.14.10", "@types/parse-dds": "^1.0.3", - "@types/pngjs": "^6.0.4", + "@types/pngjs": "^6.0.5", "@types/progress": "^2.0.7", - "@types/react": "^18.2.22", - "@vanilla-extract/next-plugin": "^2.3.1", + "@types/react": "^18.3.3", + "@vanilla-extract/next-plugin": "^2.4.3", "compare-versions": "^6.1.0", "crc": "^4.3.2", - "fast-xml-parser": "^4.3.2", - "knip": "^3.4.0", - "prettier": "^3.0.3", - "prisma": "^5.4.1", - "typescript": "^5.4.5" + "fast-xml-parser": "^4.4.0", + "knip": "^3.13.2", + "prettier": "^3.3.2", + "prisma": "^5.16.1", + "typescript": "^5.5.3" }, "packageManager": "yarn@1.22.1", "trustedDependencies": [ diff --git a/src/app/tools/tank-performance/components/Info.tsx b/src/app/tools/tank-performance/components/Info.tsx index 442d1386d..6aa6c82f0 100644 --- a/src/app/tools/tank-performance/components/Info.tsx +++ b/src/app/tools/tank-performance/components/Info.tsx @@ -10,7 +10,7 @@ export function Info() { const awaitedAverageDefinitions = use(averageDefinitions); const numberFormat = Intl.NumberFormat(undefined, { notation: 'compact' }); const ratio = useAveragesExclusionRatio(); - const samples = ratio * awaitedAverageDefinitions.sampled_players; + const samples = ratio * awaitedAverageDefinitions.samples.d_120; return ( diff --git a/src/app/tools/tank-performance/components/TankRow.tsx b/src/app/tools/tank-performance/components/TankRow.tsx index 52cb2e10b..0a6255854 100644 --- a/src/app/tools/tank-performance/components/TankRow.tsx +++ b/src/app/tools/tank-performance/components/TankRow.tsx @@ -24,11 +24,11 @@ export const TankRow = memo( {((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}% - {numberFormat.format(Math.round(ratio * averages.samples))} + {numberFormat.format(Math.round(ratio * averages.samples.d_30))} {numberFormat.format( - Math.round(ratio * averages.mu.battles * averages.samples), + Math.round(ratio * averages.mu.battles * averages.samples.total), )} diff --git a/src/app/tools/tank-performance/components/Tanks.tsx b/src/app/tools/tank-performance/components/Tanks.tsx index 15c08f094..25af192a6 100644 --- a/src/app/tools/tank-performance/components/Tanks.tsx +++ b/src/app/tools/tank-performance/components/Tanks.tsx @@ -6,7 +6,7 @@ import { Suspense, use, useEffect, useMemo, useState } from 'react'; import { averageDefinitionsArray } from '../../../../core/blitzkit/averageDefinitions'; import { filterTank } from '../../../../core/blitzkit/filterTank'; import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions'; -import { useTankFilters } from '../../../../stores/tankFilters'; +import * as TankFilters from '../../../../stores/tankFilters'; import { useTankPerformanceSort } from '../../../../stores/tankPerformanceSort'; import { RowLoader } from './RowLoader'; import { TankRow } from './TankRow'; @@ -18,7 +18,7 @@ const DEFAULT_LOADED_ROWS = 25; export function Tanks() { const awaitedTankDefinitions = use(tankDefinitions); const awaitedAverageDefinitionsArray = use(averageDefinitionsArray); - const filters = useTankFilters(); + const filters = TankFilters.use(); const sort = useTankPerformanceSort(); const tanksSorted = useMemo(() => { switch (sort.type) { @@ -31,7 +31,7 @@ export function Tanks() { return awaitedAverageDefinitionsArray.sort( (a, b) => sort.direction * - (a.samples * a.mu.battles - b.samples * b.mu.battles), + (a.samples.total * a.mu.battles - b.samples.total * b.mu.battles), ); case 'capturePoints': return awaitedAverageDefinitionsArray.sort( @@ -81,7 +81,7 @@ export function Tanks() { ); case 'players': return awaitedAverageDefinitionsArray.sort( - (a, b) => sort.direction * (a.samples - b.samples), + (a, b) => sort.direction * (a.samples.d_30 - b.samples.d_30), ); case 'spots': return awaitedAverageDefinitionsArray.sort( diff --git a/src/app/tools/tank-performance/components/TierBreakdown.tsx b/src/app/tools/tank-performance/components/TierBreakdown.tsx index 667a5d48d..ae851c26a 100644 --- a/src/app/tools/tank-performance/components/TierBreakdown.tsx +++ b/src/app/tools/tank-performance/components/TierBreakdown.tsx @@ -44,7 +44,8 @@ export function TierBreakdown() { tierTanks.forEach((stats) => { const tank = awaitedTankDefinitions[stats.id]; - populations[tank.class] += stats.mu.battles * stats.samples; + populations[tank.class] += + stats.mu.battles * stats.samples.d_30; }); TANK_CLASSES.forEach((tankClass) => { diff --git a/src/app/tools/tank-performance/components/Total.tsx b/src/app/tools/tank-performance/components/Total.tsx index e49d7bfc0..3ede675b3 100644 --- a/src/app/tools/tank-performance/components/Total.tsx +++ b/src/app/tools/tank-performance/components/Total.tsx @@ -18,7 +18,7 @@ export const Total = memo( const sum = useCallback( (slice: (tank: AverageDefinitionsAllStats) => number) => { return tanks.reduce( - (acc, tank) => acc + tank.samples * slice(tank.mu), + (acc, tank) => acc + tank.samples.total * slice(tank.mu), 0, ); }, diff --git a/src/app/tools/tank-performance/page.tsx b/src/app/tools/tank-performance/page.tsx index fc21cd5f5..1e4f6fdcc 100644 --- a/src/app/tools/tank-performance/page.tsx +++ b/src/app/tools/tank-performance/page.tsx @@ -1,5 +1,5 @@ import PageWrapper from '../../../components/PageWrapper'; -import { TankFiltersProvider } from '../../../stores/tankFilters'; +import * as TankFilters from '../../../stores/tankFilters'; import { FilterControl } from '../tankopedia/components/FilterControl'; import { Info } from './components/Info'; import { TankTable } from './components/Table'; @@ -7,13 +7,13 @@ import { TierBreakdown } from './components/TierBreakdown'; export default function Page() { return ( - + - + ); } diff --git a/src/app/tools/tankopedia/components/FilterControl.tsx b/src/app/tools/tankopedia/components/FilterControl.tsx index 67086750a..d2be97bf1 100644 --- a/src/app/tools/tankopedia/components/FilterControl.tsx +++ b/src/app/tools/tankopedia/components/FilterControl.tsx @@ -14,11 +14,7 @@ import { asset } from '../../../../core/blitzkit/asset'; import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions'; import { Tier } from '../../../../core/blitzkit/tankDefinitions'; import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants'; -import { - useTankFilters, - useTankFiltersContext, - useTankFiltersMutation, -} from '../../../../stores/tankFilters'; +import * as TankFilters from '../../../../stores/tankFilters'; interface FilterControlProps { compact?: boolean; @@ -26,9 +22,9 @@ interface FilterControlProps { export function FilterControl({ compact }: FilterControlProps) { const awaitedGameDefinitions = use(gameDefinitions); - const filters = useTankFilters(); - const mutateTankFilters = useTankFiltersMutation(); - const tankFiltersContext = useTankFiltersContext(); + const filters = TankFilters.use(); + const mutateTankFilters = TankFilters.useMutation(); + const tankFiltersStore = TankFilters.useStore(); return ( @@ -325,10 +321,7 @@ export function FilterControl({ compact }: FilterControlProps) { { - tankFiltersContext.setState( - tankFiltersContext.getInitialState(), - true, - ); + tankFiltersStore.setState(tankFiltersStore.getInitialState(), true); }} > diff --git a/src/app/tools/tankopedia/components/NoResults.tsx b/src/app/tools/tankopedia/components/NoResults.tsx index 5592c33a4..2082e60ac 100644 --- a/src/app/tools/tankopedia/components/NoResults.tsx +++ b/src/app/tools/tankopedia/components/NoResults.tsx @@ -1,12 +1,12 @@ import { Flex, Link, Text } from '@radix-ui/themes'; -import { useTankFiltersContext } from '../../../../stores/tankFilters'; +import * as TankFilters from '../../../../stores/tankFilters'; interface NoResultsProps { type?: 'filters' | 'search'; } export function NoResults({ type = 'filters' }: NoResultsProps) { - const tankFiltersContext = useTankFiltersContext(); + const tankFiltersStore = TankFilters.useStore(); return ( @@ -17,10 +17,7 @@ export function NoResults({ type = 'filters' }: NoResultsProps) { underline="always" color="red" onClick={() => - tankFiltersContext.setState( - tankFiltersContext.getInitialState(), - true, - ) + tankFiltersStore.setState(tankFiltersStore.getInitialState(), true) } > {type === 'filters' ? 'Try clearing filters' : 'Try searching again'} diff --git a/src/app/tools/tankopedia/components/QuickLink.tsx b/src/app/tools/tankopedia/components/QuickLink.tsx index d5471ba4f..694200d35 100644 --- a/src/app/tools/tankopedia/components/QuickLink.tsx +++ b/src/app/tools/tankopedia/components/QuickLink.tsx @@ -1,15 +1,15 @@ import { CaretRightIcon } from '@radix-ui/react-icons'; import { Button, Link, TextField } from '@radix-ui/themes'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; -import { useTankFilters } from '../../../../stores/tankFilters'; +import * as TankFilters from '../../../../stores/tankFilters'; interface QuickLinkProps { topResult?: TankDefinition; } export function QuickLink({ topResult }: QuickLinkProps) { - const search = useTankFilters((state) => state.search); - const searching = useTankFilters((state) => state.searching); + const search = TankFilters.use((state) => state.search); + const searching = TankFilters.use((state) => state.searching); if (!search || !topResult || searching) return null; diff --git a/src/app/tools/tankopedia/components/SearchBar.tsx b/src/app/tools/tankopedia/components/SearchBar.tsx index 54c9915d1..51f24611e 100644 --- a/src/app/tools/tankopedia/components/SearchBar.tsx +++ b/src/app/tools/tankopedia/components/SearchBar.tsx @@ -4,10 +4,7 @@ import { debounce } from 'lodash'; import { useRouter } from 'next/navigation'; import { KeyboardEventHandler, useCallback, useRef } from 'react'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; -import { - useTankFilters, - useTankFiltersContext, -} from '../../../../stores/tankFilters'; +import * as TankFilters from '../../../../stores/tankFilters'; import { QuickLink } from './QuickLink'; import { Sort } from './Sort'; @@ -17,27 +14,27 @@ interface SearchBarProps { } export function SearchBar({ topResult, onSelect }: SearchBarProps) { - const tankFiltersContext = useTankFiltersContext(); + const tankFiltersStore = TankFilters.useStore(); const router = useRouter(); const input = useRef(null); - const searching = useTankFilters((state) => state.searching); + const searching = TankFilters.use((state) => state.searching); const performSearch = useCallback( debounce(() => { - tankFiltersContext.setState({ searching: false }); + tankFiltersStore.setState({ searching: false }); if (!input.current) return; const sanitized = input.current.value.trim(); - tankFiltersContext.setState({ + tankFiltersStore.setState({ search: sanitized.length === 0 ? undefined : sanitized, }); }, 500), [], ); const handleChange = useCallback(() => { - if (!tankFiltersContext.getState().searching) { - tankFiltersContext.setState({ searching: true }); + if (!tankFiltersStore.getState().searching) { + tankFiltersStore.setState({ searching: true }); } performSearch(); diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx index 9f631646a..1106f8f92 100644 --- a/src/app/tools/tankopedia/components/TankSearch/index.tsx +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -20,7 +20,7 @@ import { } from '../../../../../core/blitzkit/tankDefinitions'; import { unionBoundingBox } from '../../../../../core/blitzkit/unionBoundingBox'; import { useAdExempt } from '../../../../../hooks/useAdExempt'; -import { useTankFilters } from '../../../../../stores/tankFilters'; +import * as TankFilters from '../../../../../stores/tankFilters'; import { SORT_NAMES, SORT_UNITS } from '../../../../../stores/tankopedia'; import { useTankopediaSort } from '../../../../../stores/tankopediaSort'; import { FilterControl } from '../FilterControl'; @@ -45,7 +45,7 @@ export const TankSearch = memo( const awaitedTankDefinitions = use(tankDefinitions); const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); const awaitedTankNames = use(tankNames); - const filters = useTankFilters(); + const filters = TankFilters.use(); const sort = useTankopediaSort(); const tanks = useMemo(() => { if (filters.search === undefined) { diff --git a/src/app/tools/tankopedia/components/TierFilter.tsx b/src/app/tools/tankopedia/components/TierFilter.tsx index 074826494..9a9ff062a 100644 --- a/src/app/tools/tankopedia/components/TierFilter.tsx +++ b/src/app/tools/tankopedia/components/TierFilter.tsx @@ -3,17 +3,14 @@ import { times } from 'lodash'; import { memo } from 'react'; import { Tier } from '../../../../core/blitzkit/tankDefinitions'; import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants'; -import { - useTankFilters, - useTankFiltersMutation, -} from '../../../../stores/tankFilters'; +import * as TankFilters from '../../../../stores/tankFilters'; import { useTankopediaSort } from '../../../../stores/tankopediaSort'; export const TierFilter = memo(() => { - const tierFilter = useTankFilters((state) => state.tiers); + const tierFilter = TankFilters.use((state) => state.tiers); const sort = useTankopediaSort(); - const search = useTankFilters((state) => state.search); - const mutateTankFilters = useTankFiltersMutation(); + const search = TankFilters.use((state) => state.search); + const mutateTankFilters = TankFilters.useMutation(); if (sort.by !== 'meta.none' || search) return null; diff --git a/src/app/tools/tankopedia/page.tsx b/src/app/tools/tankopedia/page.tsx index 5647d9edb..827fa169a 100644 --- a/src/app/tools/tankopedia/page.tsx +++ b/src/app/tools/tankopedia/page.tsx @@ -3,18 +3,18 @@ import { AdMidSectionResponsive } from '../../../components/AdMidSectionResponsive'; import PageWrapper from '../../../components/PageWrapper'; import { useAdExempt } from '../../../hooks/useAdExempt'; -import { TankFiltersProvider } from '../../../stores/tankFilters'; +import * as TankFilters from '../../../stores/tankFilters'; import { TankSearch } from './components/TankSearch'; export default function Page() { const exempt = useAdExempt(); return ( - + {!exempt && } - + ); } diff --git a/src/core/blitzkit/filterTank.ts b/src/core/blitzkit/filterTank.ts index e25e36fd1..5351b09ee 100644 --- a/src/core/blitzkit/filterTank.ts +++ b/src/core/blitzkit/filterTank.ts @@ -1,4 +1,3 @@ -import { TankFilters } from '../../stores/tankFilters'; import { TankDefinition } from './tankDefinitions'; export function filterTank(filters: TankFilters, tank: TankDefinition) { diff --git a/src/core/zustand/createNextSafeStore.tsx b/src/core/zustand/createNextSafeStore.tsx new file mode 100644 index 000000000..ec871f1d2 --- /dev/null +++ b/src/core/zustand/createNextSafeStore.tsx @@ -0,0 +1,71 @@ +import { Draft, produce } from 'immer'; +import { Context, createContext, ReactNode, useContext, useRef } from 'react'; +import { createStore, StoreApi, useStore as useStoreZustand } from 'zustand'; + +interface StoreProviderProps { + children: ReactNode; +} + +export function createStoreContext() { + return createContext | undefined>(undefined); +} + +export function createStoreProvider( + initialState: Type, + Context: Context | undefined>, +) { + return function Provider({ children }: StoreProviderProps) { + const storeRef = useRef>(); + if (!storeRef.current) { + storeRef.current = createStore()(() => ({ ...initialState })); + } + + return ( + {children} + ); + }; +} + +export function createNextSafeStore(initialState: Type) { + const Context = createContext | undefined>(undefined); + + function Provider({ children }: StoreProviderProps) { + const storeRef = useRef>(); + if (!storeRef.current) { + storeRef.current = createStore()(() => ({ ...initialState })); + } + + return ( + {children} + ); + } + + function useStore() { + const store = useContext(Context); + if (!store) { + throw new Error('useStoreContext must be used within Provider'); + } + return store; + } + + function use( + selector: (store: Type) => Slice = (state) => state as unknown as Slice, + ): Slice { + const store = useStore(); + return useStoreZustand(store, selector); + } + + function useMutation() { + const counterStoreContext = useStore(); + + return function (recipe: (draft: Draft) => void) { + counterStoreContext.setState( + produce((draft) => { + recipe(draft); + }), + ); + }; + } + + return { Provider, use, useMutation, useStore }; +} diff --git a/src/hooks/useAveragesExclusionRatio.ts b/src/hooks/useAveragesExclusionRatio.ts index 413c7b9df..46fa4a838 100644 --- a/src/hooks/useAveragesExclusionRatio.ts +++ b/src/hooks/useAveragesExclusionRatio.ts @@ -8,6 +8,6 @@ export function useAveragesExclusionRatio() { return ( awaitedDiscoveredIdsDefinitions.count / - awaitedAverageDefinitions.scanned_players + awaitedAverageDefinitions.samples.total ); } diff --git a/src/stores/tankFilters.tsx b/src/stores/tankFilters.tsx index c368391d7..6d87247b5 100644 --- a/src/stores/tankFilters.tsx +++ b/src/stores/tankFilters.tsx @@ -1,10 +1,8 @@ 'use client'; -import { produce } from 'immer'; -import { createContext, ReactNode, useContext, useRef } from 'react'; -import { createStore, StoreApi, useStore } from 'zustand'; import { TankClass, TreeType } from '../components/Tanks'; import { Tier } from '../core/blitzkit/tankDefinitions'; +import { createNextSafeStore } from '../core/zustand/createNextSafeStore'; export interface TankFilters { tiers: Tier[]; @@ -16,58 +14,13 @@ export interface TankFilters { searching: boolean; } -export const tankFiltersInitialState: TankFilters = { - testing: 'include', - searching: false, - classes: [], - nations: [], - tiers: [], - types: [], -}; - -const createTankFiltersStore = ( - initialState: TankFilters = tankFiltersInitialState, -) => createStore()(() => ({ ...initialState })); - -const TankFiltersContext = createContext | undefined>( - undefined, -); - -interface TankFiltersProviderProps { - children: ReactNode; -} - -export function TankFiltersProvider({ children }: TankFiltersProviderProps) { - const storeRef = useRef>(); - if (!storeRef.current) storeRef.current = createTankFiltersStore(); - - return ( - - {children} - - ); -} - -export function useTankFiltersContext() { - const context = useContext(TankFiltersContext); - if (!context) { - throw new Error('useTankFilters must be used within TankFiltersContext'); - } - return context; -} - -export function useTankFilters( - selector: (store: TankFilters) => Type = (store) => store as Type, -): Type { - const counterStoreContext = useTankFiltersContext(); - - return useStore(counterStoreContext, selector); -} - -export function useTankFiltersMutation() { - const counterStoreContext = useTankFiltersContext(); - - return function (recipe: (draft: TankFilters) => void) { - counterStoreContext.setState(produce(recipe)); - }; -} +export const { Provider, use, useMutation, useStore } = + createNextSafeStore({ + tiers: [], + nations: [], + classes: [], + types: [], + testing: 'include', + search: undefined, + searching: false, + });