diff --git a/.github/workflows/cd-assets.yml b/.github/workflows/cd-assets.yml new file mode 100644 index 000000000..46c0f6504 --- /dev/null +++ b/.github/workflows/cd-assets.yml @@ -0,0 +1,53 @@ +name: CD Build Assets + +on: + push: + branches: dev + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Use node + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'npm' + + - name: Install Yarn + run: npm install --location=global yarn + + - name: Use node_modules cache + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + + - name: Install dependencies + run: yarn + + - name: Add fixed sharp + run: yarn add sharp@0.33.1 --ignore-engines + + - name: Setup DepotDownloader + run: yarn setup:depotdownloader + + - name: Run DepotDownloader + run: ./temp/depotDownloader/DepotDownloader.exe -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}" + + - name: Build assets + run: | + $env:GH_TOKEN = "${{ secrets.GH_TOKEN }}" + $env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}" + yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }} diff --git a/.github/workflows/cd-host.yml b/.github/workflows/cd-host.yml index d9fd59e25..1dcd90e72 100644 --- a/.github/workflows/cd-host.yml +++ b/.github/workflows/cd-host.yml @@ -1,6 +1,8 @@ name: CD Host on: + push: + branches: main schedule: - cron: '0 */5 * * *' workflow_dispatch: diff --git a/.gitignore b/.gitignore index 11dac0cb3..3464b9cc4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ test.* servers.csv .next/ tsconfig.tsbuildinfo -temp/ \ No newline at end of file +temp/ +/.yarn +/.VSCodeCounter +/.yarn \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index eb6090c58..7ef122024 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,46 +3,98 @@ "AABBOX", "Abhi", "accountinfo", + "aeson", "alphatest", "apcr", + "artefacts", "atgm", + "AYUV", + "bbox", "bepaint", + "betakrieg", "BINORMAL", + "Binormals", + "bkwd", "blitzstars", + "Camo", + "CDON", + "classname", "cleandist", "cockroackdb", + "Coef", + "cooldown", "corsproxy", + "CUBEMAP", "CUBETEXCOORD", "customperiod", + "datasource", + "Ddpf", + "Ddsd", + "debugflags", "deburr", "decalmask", + "defence", + "degressive", + "devkrieg", "diacriticless", + "donotremove", + "drei", + "Droodles", "dvpl", "DVPLCRC", + "dxgi", "Everytime", "FASTNAME", + "FOURCC", + "fsformat", + "Fullscreen", "fullstats", "fuzzysort", "gamemode", + "glsl", + "GLTF", "haha", + "iife", "immer", + "Inclusivity", "JOINTINDEX", "JOINTWEIGHT", "kranvagn", "krieg", "Leaderboard", "leaderboards", + "lerp", + "LINEARSIZE", + "loddist", + "lods", + "metallicness", + "minitelrose", + "MIPMAPCOUNT", + "modeltransform", "MURICA", + "NEGATIVEX", + "NEGATIVEY", + "NEGATIVEZ", + "nmatname", + "noout", "normalmap", "Oopsy", + "OPTML", "outdir", "outfile", "ownedtanks", + "PIXELFORMAT", "playerachievements", "playerinfo", + "pngjs", + "POSITIVEX", + "POSITIVEY", + "POSITIVEZ", "Promisable", + "Pyogenics", "ratingsleaderboard", "ratingsplot", + "raycaster", "researchable", "Resvg", "roboto", @@ -50,22 +102,33 @@ "searchclans", "searchplayers", "searchtanks", + "SHAREDEXP", "SKLLD", "Smri", "Specularity", + "SSAO", "startcase", "statsfull", "steamapps", "Stringifying", + "struct", + "structs", "strvk", "subnation", + "Synx", "tankaverages", + "Tankerch", "tankopedia", "tankopedias", "tankstats", + "texconv", "TEXCOORD", "tresabhi", + "tungstentip", + "TYPELESS", + "ubjson", "Unicum", + "UNORM", "Unregistering", "vickers", "vitepress", @@ -73,7 +136,10 @@ "wargaming", "webp", "winrate", + "wireframe", "wotblitz", + "xffn", + "xkdkw", "XMLDVPL", "YAMLDVPL", "zustand" diff --git a/docs/changelogs/1.16.0.md b/docs/changelogs/1.16.0.md new file mode 100644 index 000000000..48f8e5a5c --- /dev/null +++ b/docs/changelogs/1.16.0.md @@ -0,0 +1,72 @@ +# Blitzkrieg 1.16.0 + +This version introduces the long awaited (and desperately needed) Blitzkrieg Tankopedia. + +## Credits + +- [Pyogenics](https://github.com/pyogenics): technical documentation on WoTB 3d models +- [aeson](https://discord.gg/WHdER7ZPAD): human encyclopedia on WoTB file layout +- [\[OPTML\]](https://discord.gg/Su8w9KtEVE), [\_34](https://www.twitch.tv/34wotb), [Droodles_YT](https://www.youtube.com/@DroodlesBlitz): Tankopedia beta-testing +- Minitelrose, ChickenMan7777: external module exceptions + +## Features + +- Added Tankopedia + - Check out the tool here: https://blitz-krieg.vercel.app/tools/tankopedia + - The root page features a search engine for efficiently finding tanks + - Search bar: lets you search tanks via it's name + - Sort: lets you sort tanks by their statistics and generic things like names and tiers + - Reset button (trash icon): resets all filters and sorts + - Test tanks: lets you control wether you want to include test tanks + - Tree type filter: filters tech tree, premium, and collector tanks + - Nations: filters tanks by nation + - Tank type filter: filters tanks by tank designation + - Tier filter: filters tanks by tier + - The search result itself displays the national flag, tier, type, image, and name of the tank + - Clicking on the tank redirects you to the tank's page + - Tankopedia page + - Tank sandbox: the first thing you'll see is a 3D model of the tank + - On slim mobile devices, click "Load model" to reveal it + - Model tab + - This model has all PBR features and is identical to the one in the game + - You can click and drag the turret to rotate it + - You can do the same with the gun but with the added control of elevating and depressing it + - Armor tab + - Shows red where the tank (the protagonist) cannot be penetrated + - You can switch the shells that the shooter (the antagonist) shoots + - By default, the antagonist tank is the same as the protagonist + - You can change antagonists using the antagonist bar + - Searching through antagonists is identical to the root search page + - You may also swap the protagonist and antagonist using the swap button + - Click on the tank's armor anywhere to get intimate details about it + - Shows spaced armor, gaps between layers of armor, ricochets, penetration losses over events, thickness, angled thicknesses, and angles + - View button (eye icon): allows you to pose the tank in a few preset configurations + - Sandbox settings + - Green penetration: shows the penetrable parts of the tank as green instead of using transparency + - Show grid: shows a grid under the tank + - View environment: shows the lighting environment around the tank + - Lighting: preset lighting environments + - Wireframe: displays armor as vertices instead of faces + - Fullscreen button: toggles fullscreen + - Note that some functionalities are disabled in fullscreen + - Modules: switch around modules to view it change live in the 3D model and affect stats + - Provisions, equipments, miscellaneous, and consumables also affect stats + - Stats + - As you switch around the configurations of the tank, the change in stats are displayed using green and red arrows with delta numbers + - Statistics also display their units + - Fire section + - This deals with statistics relating to the firepower of the tank + - You can switch shells that the stats are based on using the buttons to the right of the title + - You can also view the penetration loss over distance using the slider if available under the penetration section + - Statistics for maneuverability, survivability, and crew training are also displayed + +## Changes + +- Navigation bar + - Added credit to myself along with the logo and moved the whole thing to the left side + - Added a link to the discord icon to the right +- Removed polka dots from the background + +## Fixes + +- The `/ratings` command once again has the username field as an optional parameter diff --git a/docs/guide/cdon.md b/docs/guide/cdon.md new file mode 100644 index 000000000..025126b93 --- /dev/null +++ b/docs/guide/cdon.md @@ -0,0 +1,101 @@ +# Cedilla Object Notation + +This documentation uses an experimental binary notation syntax called "Buffer Up". + +1. JSON compressed as CDON, written directly to disk (a direct 50% reduction). +2. CDON further compressed with LZ4 (a clean 90% reduction). +3. The raw JSON file as fetched from a random data source. +4. CDON decompressed once again to JSON. Note that this file is a little larger than the original JSON file as it contains a lot more decimal places due to float precision (this shouldn't affect the size in memory). + +![an image of file sizes](https://i.imgur.com/AHJiFpq.png) + +FS stands for "Fast String". + +```cpp +primary CedillaObjectNotation { + utf8"CDON" magic; + Header header; + primary Body body; +} + +Header { + uint16 version; + FSFormat fsFormat; + StringTable stringTable; +} + +FSFormat enum { + Format8, + Format16, + Format32, +} + +FSPrimitive match(fsFormat) { + Format8: uint8; + Format16: uint16; + Format32: uint32; +} + +StringTable { + FSPrimitive count; + String[count] strings; +} + +Body { + primary Value object; +} + +ValueType enum { + Null, + Boolean, + Uint8, + Uint16, + Uint32, + Uint64, + Int8, + Int16, + Int32, + Int64, + Float32, + Float64, + String, + FS, + Array, + Object, +} + +String { + uint32 length; + primary utf8 value; +} + +StringValue Value & { (ValueType.String | ValueType.FS) type } + +Value { + ValueType type; + match { + Boolean: { uint8 value; } + Uint8: { uint8 value; } + Uint16: { uint16 value; } + Uint32: { uint32 value; } + Uint64: { uint64 value; } + Int8: { int8 value; } + Int16: { int16 value; } + Int32: { int32 value; } + Int64: { int64 value; } + Float32: { float32 value; } + Float64: { float64 value; } + String: { String value; } + FS: { FSPrimitive index; } + Array: { + uint32 count; + Value[count] values; + } + Object: { + uint32 count; + StringValue[count] keys; + Value[count] values; + } + } +} +``` diff --git a/docs/guide/creators.md b/docs/guide/creators.md index 757b2f262..18e4a7669 100644 --- a/docs/guide/creators.md +++ b/docs/guide/creators.md @@ -1,5 +1,15 @@ -# Creators +# Team -- **TrèsAbhi** `@tresabhi`: Creator and main programmer -- **Prince_NA** `@prince_na`: Game advisor & tester -- **Vovko**: Technical advisor +- TrèsAbhi: creator & programmer +- Prince_NA: game advisor & tester + +# Contributors + +- S0AP: bot verification process & initial advertizement platform +- Vovko: discord bot rendering pipeline +- Maddox: game file decompression +- Pyogenics: tank model decompression +- Aeson: game files layout +- Minitelrose, ChickenMan7777: 3-caliber rule edge cases +- \_34, HisRoyalFatness, Synx, xkdkw: embed UX +- [OPTML], Aeson, \_34, Droodles: beta testing tankopedia diff --git a/docs/guide/dpm.md b/docs/guide/dpm.md new file mode 100644 index 000000000..c288aa94f --- /dev/null +++ b/docs/guide/dpm.md @@ -0,0 +1,17 @@ +# Damage Per Minute (DPM) + +The damage per minute of a tank is the average damage that can be dealt in that tank with the first shell (usually AP) in 60 seconds. + +## Regular and Auto Loaders + +The damage per minute for these types of guns are calculated by allowing the gun to shoot as soon as the shell is loaded (and in the case of auto loaders, the clip is dumped as fast as the inter-clip time will allow). + +## Auto Reloaders + +These types of guns have 3 DPM values. + +- Damage per minute: calculated if you were to treat the shells like a normal auto loader where you dump all shells, wait for all shells to reload, and repeat. +- Maximum: some tanks gain DPM the deeper they go into the clip and others maintain a higher DPM the less shells used. The "maximum" DPM is calculated if you were to treat the gun as a single-shot regular gun with the shell with the lowest reload. The shell with the highest DPM is listed as "Optimal shell index." +- Effective: this is the maximum DPM tacked on with dumping the remaining shells at the beginning or the end of the 60 seconds. + - If "Optimal shell index" is 1, the clip must be dumped first then used as a single shot. + - If "Optimal shell index" is any other number, the gun should be treated as a single shot then dumped for the highest effective DPM. diff --git a/next.config.js b/next.config.js index 1bb819616..d923618ce 100644 --- a/next.config.js +++ b/next.config.js @@ -4,7 +4,15 @@ const withVanillaExtract = createVanillaExtractPlugin({ tests: /\.css$/ }); /** @type {import('next').NextConfig} */ const nextConfig = { - ...withVanillaExtract(), + ...withVanillaExtract({ + webpack: (config) => { + config.module.rules.push({ + test: /\.glsl$/, + type: 'asset/source', + }); + return config; + }, + }), distDir: 'dist/website', reactStrictMode: false, diff --git a/package.json b/package.json index 0368b0c47..729329ee7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.15.13", + "version": "1.16.0", "description": "🎉 Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", @@ -9,7 +9,7 @@ "build:website": "next build", "build:docs": "vitepress build docs", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard", - "build:assets": "vite-node --script scripts/buildAssets", + "build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index", "build:test": "vite-node --script scripts/buildTest", "build:ratingsplot": "vite-node --script scripts/buildRatingsPlot", "dev:bot": "yarn build:bot && yarn start:bot", @@ -19,26 +19,29 @@ "start:website": "next start", "host:docs": "vitepress preview docs", "publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish", - "zip": "zip -r dist.zip dist", "lint": "npx tsc", "lint:website": "next lint", "format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"", "lint:version": "vite-node --script scripts/checkVersionMissMatch", "knip": "knip", - "update": "yarn upgrade --latest" + "update": "yarn upgrade --latest", + "setup:depotdownloader": "vite-node --script scripts/setupDepotDownloader" }, "license": "MIT", "dependencies": { + "@gltf-transform/core": "^3.9.0", "@octokit/plugin-throttling": "^8.1.3", "@octokit/rest": "^20.0.1", "@prisma/client": "^5.4.1", "@radix-ui/colors": "^3.0.0-rc.4", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^2.0.1", - "@react-three/drei": "^9.90.1", - "@react-three/fiber": "^8.15.12", + "@react-three/drei": "^9.93.0", + "@react-three/fiber": "^8.15.14", "@resvg/resvg-js": "^2.4.1", + "@types/three": "^0.160.0", "@vanilla-extract/css": "^1.13.0", + "@vercel/analytics": "^1.2.2", "bepaint": "^0.1.0-beta.3", "decompress": "^4.2.1", "discord.js": "^14.13.0", @@ -50,14 +53,21 @@ "immer": "^10.0.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "lz4js": "^0.2.0", "markdown-escape": "^2.0.0", - "next": "^14.0.3", + "next": "^14.1.0", + "parse-dds": "^1.2.1", + "postprocessing": "^6.34.1", "react": "^18.2.0", + "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", "react-fuzzysort": "^1.0.1", + "react-promise-suspense": "^0.3.4", + "react-spinners": "^0.13.8", "satori": "^0.10.8", - "sharp": "^0.33.0", - "three": "^0.159.0", + "sharp": "0.33.1", + "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", @@ -66,7 +76,7 @@ }, "devDependencies": { "@total-typescript/ts-reset": "^0.5.1", - "@types/decompress": "^4.2.6", + "@types/decompress": "^4.2.7", "@types/escape-html": "^1.0.2", "@types/express": "^4.17.18", "@types/js-yaml": "^4.0.8", @@ -74,8 +84,9 @@ "@types/lz4": "^0.6.4", "@types/markdown-escape": "^1.1.0", "@types/node": "^20.6.5", + "@types/parse-dds": "^1.0.3", + "@types/pngjs": "^6.0.4", "@types/react": "^18.2.22", - "@types/three": "^0.159.0", "@vanilla-extract/next-plugin": "^2.3.1", "compare-versions": "^6.1.0", "crc": "^4.3.2", @@ -85,5 +96,6 @@ "prettier": "^3.0.3", "prisma": "^5.4.1", "typescript": "^5.2.2" - } + }, + "packageManager": "yarn@1.22.1" } diff --git a/public/assets/banners/discord.png b/public/assets/banners/discord.png new file mode 100644 index 000000000..f35024841 Binary files /dev/null and b/public/assets/banners/discord.png differ diff --git a/public/assets/banners/discord.webp b/public/assets/banners/discord.webp new file mode 100644 index 000000000..e91139b5b Binary files /dev/null and b/public/assets/banners/discord.webp differ diff --git a/public/google7348c0b85956d981.html b/public/google7348c0b85956d981.html deleted file mode 100644 index 4d4a98af4..000000000 --- a/public/google7348c0b85956d981.html +++ /dev/null @@ -1 +0,0 @@ -google-site-verification: google7348c0b85956d981.html \ No newline at end of file diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts deleted file mode 100644 index 28698f753..000000000 --- a/scripts/buildAssets.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { config } from 'dotenv'; -import { existsSync } from 'fs'; -import { mkdir, readdir, rm } from 'fs/promises'; -import { argv } from 'process'; -import sharp from 'sharp'; -import { TankType } from '../src/components/Tanks'; -import { NATION_IDS } from '../src/constants/nations'; -import { SCPGStream } from '../src/core/blitz/SCPGStream'; -import { readBase64DVPL } from '../src/core/blitz/readBase64DVPL'; -import { readDVPLFile } from '../src/core/blitz/readDVPLFile'; -import { readStringDVPL } from '../src/core/blitz/readStringDVPL'; -import { readXMLDVPL } from '../src/core/blitz/readXMLDVPL'; -import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL'; -import { toUniqueId } from '../src/core/blitz/toUniqueId'; -import commitMultipleFiles, { - FileChange, -} from '../src/core/blitzkrieg/commitMultipleFiles'; -import { BlitzkriegGunDefinitions } from '../src/core/blitzkrieg/definitions/guns'; -import { - BlitzkriegShellDefinitions, - ShellType, -} from '../src/core/blitzkrieg/definitions/shells'; -import { - BlitzkriegTankDefinitions, - Tier, -} from '../src/core/blitzkrieg/definitions/tanks'; -import { BlitzkriegTurretDefinitions } from '../src/core/blitzkrieg/definitions/turrets'; - -config(); - -// WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE! - -interface VehicleDefinitionList { - [key: string]: { - id: number; - userString: string; - shortUserString?: string; - description: string; - price: number | { gold: ''; '#text': number }; - sellPrice?: { gold: ''; '#text': number }; - enrichmentPermanentCost: number; - notInShop?: boolean; - tags: string; - level: number; - combatRole?: Record; - configurationModes: string; - }; -} -interface TurretDefinitionsList { - nextAvailableId: number; - ids: Record; -} -interface GunDefinitionsList { - nextAvailableId: number; - ids: Record; - shared: { - [key: string]: { - userString: string; - tags: string; - level: number; - pitchLimits: string; - burst?: { count: number; rate: number }; - clip?: { count: number; rate: number }; - shots: { - [key: string]: { - speed: number; - piercingPower: string; - }; - }; - }; - }; -} -type ShellKind = - | 'ARMOR_PIERCING' - | 'ARMOR_PIERCING_CR' - | 'HIGH_EXPLOSIVE' - | 'HOLLOW_CHARGE'; -type ShellDefinitionsList = Record< - string, - { - id: number; - userString: string; - icon: string; - kind: ShellKind; - caliber: number; - damage: { armor: number; devices: number }; - normalizationAngle: number; - ricochetAngle: number; - } -> & { - icons: Record; -}; -interface VehicleDefinitions { - turrets0: { - [key: string]: { - userString: number; - level: number; - yawLimits: string | string[]; - guns: { - [key: string]: { - maxAmmo: number; - pitchLimits?: string | string[]; - }; - }; - }; - }; -} -interface Strings { - [key: string]: string; -} -type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank'; -interface TankParameters { - resourcesPath: { - smallIconPath: string; - bigIconPath: string; - blitzModelPath: string; - }; -} - -const blitzTankTypeToBlitzkrieg: Record = { - 'AT-SPG': 'tank_destroyer', - lightTank: 'light', - mediumTank: 'medium', - heavyTank: 'heavy', -}; -const blitzShellKindToBLitzkrieg: Record = { - ARMOR_PIERCING: 'ap', - ARMOR_PIERCING_CR: 'ap_cr', - HIGH_EXPLOSIVE: 'he', - HOLLOW_CHARGE: 'hc', -}; -const DATA = - 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data'; -const LANGUAGE = 'en'; -const DOI = { - vehicleDefinitions: 'XML/item_defs/vehicles', - strings: 'Strings', - tankParameters: '3d/Tanks/Parameters', - smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons', - bigIcons: 'Gfx/UI/BigTankIcons', - flags: 'Gfx/Lobby/flags', - '3d': '3d', - bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big', -}; - -const allTargets = argv.includes('--all-targets'); -const targets = argv - .find((argument) => argument.startsWith('--target')) - ?.split('=')[1] - .split(','); - -if (!targets && !allTargets) throw new Error('No target(s) specified'); - -if (allTargets || targets?.includes('definitions')) { - console.log('Building tank definitions...'); - - const tankDefinitions: BlitzkriegTankDefinitions = {}; - const turretDefinitions: BlitzkriegTurretDefinitions = {}; - const gunDefinitions: BlitzkriegGunDefinitions = {}; - const shellDefinitions: BlitzkriegShellDefinitions = {}; - const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( - (nations) => nations.filter((nation) => nation !== 'common'), - ); - const strings = await readYAMLDVPL( - `${DATA}/${DOI.strings}/${LANGUAGE}.yaml.dvpl`, - ); - - await Promise.all( - nations.map(async (nation) => { - const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, - ); - const turretList = await readXMLDVPL<{ - root: TurretDefinitionsList; - }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/components/turrets.xml.dvpl`, - ); - const gunList = await readXMLDVPL<{ - root: GunDefinitionsList; - }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/components/guns.xml.dvpl`, - ); - const shellList = await readXMLDVPL<{ - root: ShellDefinitionsList; - }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/components/shells.xml.dvpl`, - ); - - for (const tankKey in tankList.root) { - const tank = tankList.root[tankKey]; - const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/${tankKey}.xml.dvpl`, - ); - const tankId = toUniqueId(nation, tank.id); - const tankTags = tank.tags.split(' '); - const tankTurrets = Object.keys(tankDefinition.root.turrets0).map( - (turretKey) => { - const turret = tankDefinition.root.turrets0[turretKey]; - const turretId = toUniqueId(nation, turretList.root.ids[turretKey]); - const turretGuns = Object.keys(turret.guns).map((gunKey) => { - const turretGunEntry = turret.guns[gunKey]; - const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); - const gun = gunList.root.shared[gunKey]; - const pitchLimitsRaw = - turretGunEntry.pitchLimits ?? gun.pitchLimits; - const gunShells = Object.keys(gun.shots).map((shellKey) => { - const turretShellEntry = gun.shots[shellKey]; - const shell = shellList.root[shellKey]; - const shellId = toUniqueId(nation, shell.id); - - shellDefinitions[shellId] = { - id: shellId, - name: - strings[shell.userString] ?? shellKey.replaceAll('_', ' '), - speed: turretShellEntry.speed, - damage: { - armor: shell.damage.armor, - devices: shell.damage.devices, - }, - caliber: shell.caliber, - normalization: shell.normalizationAngle, - ricochet: shell.ricochetAngle, - type: blitzShellKindToBLitzkrieg[shell.kind], - icon: shell.icon, - }; - - return shellId; - }); - - gunDefinitions[gunId] = { - id: gunId, - pitch: (typeof pitchLimitsRaw === 'string' - ? pitchLimitsRaw - : pitchLimitsRaw.at(-1)! - ) - .split(' ') - .map(Number) as [number, number], - name: strings[gun.userString] ?? gunKey.replaceAll('_', ' '), - tier: gun.level as Tier, - shells: gunShells, - }; - - return gunId; - }); - - turretDefinitions[turretId] = { - id: turretId, - name: - strings[turret.userString] ?? - turretKey - .replaceAll('_', ' ') - .replace(/^(Turret ([0-9] )?)+/, ''), - tier: turret.level as Tier, - yaw: (typeof turret.yawLimits === 'string' - ? turret.yawLimits - : turret.yawLimits.at(-1)! - ) - .split(' ') - .map(Number) as [number, number], - guns: turretGuns, - }; - - return turretId; - }, - ); - - tankDefinitions[tankId] = { - id: tankId, - name: - (tank.shortUserString - ? strings[tank.shortUserString] - : undefined) ?? - strings[tank.userString] ?? - tankKey.replaceAll('_', ' '), - nation, - tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false) - ? 'collector' - : (typeof tank.price === 'number' ? false : 'gold' in tank.price) - ? 'premium' - : 'researchable', - tier: tank.level as Tier, - type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType], - testing: tankTags.includes('testTank') ? true : undefined, - turrets: tankTurrets, - }; - } - }), - ); - - console.log('Committing tank definitions...'); - await commitMultipleFiles( - 'tresabhi', - 'blitzkrieg-assets', - 'main', - 'definitions', - [ - { - content: JSON.stringify(tankDefinitions), - encoding: 'utf-8', - path: 'definitions/tanks.json', - }, - { - content: JSON.stringify(turretDefinitions), - encoding: 'utf-8', - path: 'definitions/turrets.json', - }, - { - content: JSON.stringify(gunDefinitions), - encoding: 'utf-8', - path: 'definitions/guns.json', - }, - { - content: JSON.stringify(shellDefinitions), - encoding: 'utf-8', - path: 'definitions/shells.json', - }, - ], - - true, - ); -} - -if ( - allTargets || - targets?.includes('bigTankIcons') || - targets?.includes('smallTankIcons') -) { - console.log('Building tank icons...'); - - const changes: FileChange[] = []; - const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( - (nations) => nations.filter((nation) => nation !== 'common'), - ); - - await Promise.all( - nations.map(async (nation) => { - const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, - ); - - for (const tankIndex in tanks.root) { - const tank = tanks.root[tankIndex]; - const nationVehicleId = tank.id; - const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; - const parameters = await readYAMLDVPL( - `${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`, - ); - const small = `${DATA}/${parameters.resourcesPath.smallIconPath - .replace(/~res:\//, '') - .replace(/\..+/, '')}.packed.webp.dvpl`; - const big = `${DATA}/${parameters.resourcesPath.bigIconPath - .replace(/~res:\//, '') - .replace(/\..+/, '')}.packed.webp.dvpl`; - - if (allTargets || targets?.includes('bigTankIcons')) { - changes.push({ - content: await readBase64DVPL(big), - encoding: 'base64', - path: `icons/big/${id}.webp`, - }); - } - if (allTargets || targets?.includes('smallTankIcons')) { - changes.push({ - content: await readBase64DVPL(small), - encoding: 'base64', - path: `icons/small/${id}.webp`, - }); - } - } - }), - ); - - console.log('Committing tank icons...'); - await commitMultipleFiles( - 'tresabhi', - 'blitzkrieg-assets', - 'main', - 'tank icons', - changes, - true, - ); -} - -if (allTargets || targets?.includes('scratchedFlags')) { - console.log('Building scratched flags...'); - - const flags = await readdir(`${DATA}/${DOI.flags}`); - - console.log('Committing scratched flags...'); - commitMultipleFiles( - 'tresabhi', - 'blitzkrieg-assets', - 'main', - 'scratched flags', - await Promise.all( - flags - .filter( - (flag) => - flag.startsWith('flag_tutor-tank_') && - !flag.endsWith('@2x.packed.webp.dvpl'), - ) - .map(async (flag) => { - const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`); - const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1]; - - return { - content, - encoding: 'base64', - path: `flags/scratched/${name}.webp`, - } satisfies FileChange; - }), - ), - true, - ); -} - -if (allTargets || targets?.includes('circleFlags')) { - const flags = await readdir(`${DATA}/${DOI.flags}`); - - commitMultipleFiles( - 'tresabhi', - 'blitzkrieg-assets', - 'main', - 'circle flags', - await Promise.all( - flags - .filter( - (flag) => - flag.startsWith('flag_profile-stat_') && - !flag.endsWith('@2x.packed.webp.dvpl'), - ) - .map(async (flag) => { - const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`); - const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1]; - - return { - content, - encoding: 'base64', - path: `flags/circle/${name}.webp`, - } satisfies FileChange; - }), - ), - true, - ); -} - -if (allTargets || targets?.includes('tankModels')) { - console.log('Building tank models...'); - - if (existsSync('dist/assets/models')) { - await rm('dist/assets/models', { recursive: true }); - } - await mkdir('dist/assets/models', { recursive: true }); - - const changes: FileChange[] = []; - const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( - (nations) => nations.filter((nation) => nation !== 'common'), - ); - - await Promise.all( - nations.map(async (nation) => { - const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( - `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, - ); - - for (const tankIndex in tanks.root) { - const tank = tanks.root[tankIndex]; - const nationVehicleId = tank.id; - const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; - - // if (id !== 897) continue; - console.log(`Building model ${id} @ ${nation}/${tankIndex}`); - - const parameters = await readYAMLDVPL( - `${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`, - ); - const sc2Path = parameters.resourcesPath.blitzModelPath; - const scgPath = parameters.resourcesPath.blitzModelPath.replace( - /\.sc2$/, - '.scg', - ); - const sc2Stream = await SCPGStream.fromDVPLFile( - `${DATA}/${DOI['3d']}/${sc2Path}.dvpl`, - ); - const scgStream = await SCPGStream.fromDVPLFile( - `${DATA}/${DOI['3d']}/${scgPath}.dvpl`, - ); - - const sc2 = sc2Stream.consumeSC2(); - const scg = scgStream.consumeSCG(); - } - }), - ); -} - -if (allTargets || targets?.includes('shellIcons')) { - console.log('Building shell icons...'); - - const image = sharp( - await readDVPLFile( - `${DATA}/${DOI.bigShellIcons}/texture0.packed.webp.dvpl`, - ), - ); - - const changes = await Promise.all( - (await readdir(`${DATA}/${DOI.bigShellIcons}`)) - .filter((file) => file.endsWith('_l.txt.dvpl')) - .map(async (file) => { - const name = file.match(/(.+)_l\.txt\.dvpl/)![1]; - const sizes = ( - await readStringDVPL(`${DATA}/${DOI.bigShellIcons}/${file}`) - ) - .split('\n')[4] - .split(' ') - .map(Number); - - return { - content: ( - await image - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer() - ).toString('base64'), - encoding: 'base64', - path: `shells/${name}.webp`, - } satisfies FileChange; - }), - ); - - console.log('Committing shell icons...'); - commitMultipleFiles( - 'tresabhi', - 'blitzkrieg-assets', - 'main', - 'shell icons', - changes, - true, - ); -} diff --git a/scripts/buildAssets/boosterIcons.ts b/scripts/buildAssets/boosterIcons.ts new file mode 100644 index 000000000..2b1069ba8 --- /dev/null +++ b/scripts/buildAssets/boosterIcons.ts @@ -0,0 +1,48 @@ +import { readdir } from 'fs/promises'; +import sharp from 'sharp'; +import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; +import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; + +export async function boosterIcons(production: boolean) { + const boosterFiles = (await readdir(`${DATA}/${POI.boosterIcons}`)).filter( + (file) => !file.endsWith('@2x.txt.dvpl') && !file.startsWith('texture0'), + ); + const image = sharp( + await readDVPLFile(`${DATA}/${POI.boosterIcons}/texture0.packed.webp.dvpl`), + ); + const changes: FileChange[] = []; + + await Promise.all( + boosterFiles.map(async (file) => { + const name = file.match(/booster_(.+).txt.dvpl/)![1]; + const sizes = ( + await readStringDVPL(`${DATA}/${POI.boosterIcons}/${file}`) + ) + .split('\n')[4] + .split(' ') + .map(Number); + const content = ( + await image + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer() + ).toString('base64'); + + changes.push({ + path: `icons/boosters/${name}.webp`, + content, + encoding: 'base64', + }); + }), + ); + + await commitAssets('booster icons', changes, production); +} diff --git a/scripts/buildAssets/camouflageIcons.ts b/scripts/buildAssets/camouflageIcons.ts new file mode 100644 index 000000000..c9543456c --- /dev/null +++ b/scripts/buildAssets/camouflageIcons.ts @@ -0,0 +1,20 @@ +import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; + +export async function camouflageIcons(production: boolean) { + console.log('Building camouflage icons...'); + const content = await readDVPLFile( + `${DATA}/${POI.defaultCamoIcon.replace('.webp', '.packed.webp.dvpl')}`, + ).then((content) => content.toString('base64')); + const changes: FileChange[] = [ + { + content, + encoding: 'base64', + path: 'icons/camo.webp', + }, + ]; + + await commitAssets('camo icons', changes, production); +} diff --git a/scripts/buildAssets/circleFlags.ts b/scripts/buildAssets/circleFlags.ts new file mode 100644 index 000000000..d293f2bb7 --- /dev/null +++ b/scripts/buildAssets/circleFlags.ts @@ -0,0 +1,29 @@ +import { readdir } from 'fs/promises'; +import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; + +export async function circleFlags(production: boolean) { + const flags = await readdir(`${DATA}/${POI.flags}`); + const changes = await Promise.all( + flags + .filter( + (flag) => + flag.startsWith('flag_profile-stat_') && + !flag.endsWith('@2x.packed.webp.dvpl'), + ) + .map(async (flag) => { + const content = await readBase64DVPL(`${DATA}/${POI.flags}/${flag}`); + const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1]; + + return { + content, + encoding: 'base64', + path: `flags/circle/${name}.webp`, + } satisfies FileChange; + }), + ); + + await commitAssets('circle flags', changes, production); +} diff --git a/scripts/buildAssets/constants.ts b/scripts/buildAssets/constants.ts new file mode 100644 index 000000000..741fa1fb0 --- /dev/null +++ b/scripts/buildAssets/constants.ts @@ -0,0 +1,39 @@ +import { readdir } from 'fs/promises'; +import { argv } from 'process'; + +const isDepot = argv.includes('--depot'); +export const DATA = isDepot + ? await (async () => { + const [installationVersion] = await readdir('depots/444202'); + return `depots/444202/${installationVersion}/Data`; + })() + : 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data'; + +export const POI = { + vehicleDefinitions: 'XML/item_defs/vehicles', + strings: 'Strings', + cachedStrings: 'https://stufficons.wgcdn.co/localizations/en.yaml', + tankParameters: '3d/Tanks/Parameters', + smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons', + bigIcons: 'Gfx/UI/BigTankIcons', + flags: 'Gfx/Lobby/flags', + '3d': '3d', + bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big', + moduleIcons: 'Gfx/UI/ModulesTechTree', + collisionMeshes: '3d/Tanks/CollisionMeshes', + optionalDevices: 'XML/item_defs/vehicles/common/optional_devices.xml', + optionalDeviceSlots: + 'XML/item_defs/vehicles/common/optional_device_slots.xml', + optionalDeviceImageMappings: + 'UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml', + optionalDevicesImage: + 'Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp', + consumablesCommon: 'XML/item_defs/vehicles/common/consumables/common.xml', + consumableIcons: 'Gfx/UI/InventoryIcons/Big/Consumables', + provisionIcons: 'Gfx/UI/InventoryIcons/Big/Provisions', + equipmentItemImageMappings: + 'UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml', + provisionsCommon: 'XML/item_defs/vehicles/common/provisions/common.xml', + defaultCamoIcon: 'Gfx/UI/Hangar/IconCamouflage.webp', + boosterIcons: 'Gfx/Shared/boosters', +} as const; diff --git a/scripts/buildAssets/consumableProvisionIcons.ts b/scripts/buildAssets/consumableProvisionIcons.ts new file mode 100644 index 000000000..9f1f2ca0a --- /dev/null +++ b/scripts/buildAssets/consumableProvisionIcons.ts @@ -0,0 +1,164 @@ +import sharp from 'sharp'; +import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; +import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; +import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; +import { ConsumablesCommon, ProvisionsCommon } from './definitions'; + +interface Mappings { + Header: { version: number }; + StyleSheets: { + selectors: string[]; + properties: { 'bg-sprite': string }; + }[]; +} + +export async function consumableProvisionIcons(production: boolean) { + console.log('Building consumable and provision icons...'); + + const changes: FileChange[] = []; + const consumablesTexture = sharp( + await readDVPLFile( + `${DATA}/${POI.consumableIcons}/texture0.packed.webp.dvpl`, + ), + ); + const provisionsTexture = sharp( + await readDVPLFile( + `${DATA}/${POI.provisionIcons}/texture0.packed.webp.dvpl`, + ), + ); + + const equipmentItemImageMappings = await readYAMLDVPL( + `${DATA}/${POI.equipmentItemImageMappings}.dvpl`, + ); + const consumablesCommon = await readXMLDVPL<{ + root: ConsumablesCommon; + }>(`${DATA}/${POI.consumablesCommon}.dvpl`); + const provisionsCommon = await readXMLDVPL<{ + root: ProvisionsCommon; + }>(`${DATA}/${POI.provisionsCommon}.dvpl`); + const styleSheets = Object.values(equipmentItemImageMappings.StyleSheets); + + await Promise.all( + Object.values(consumablesCommon.root).map(async (consumable) => { + const styleSheet = styleSheets.find((styleSheet) => + styleSheet.selectors.includes( + `.equipment_item.${consumable.icon} * .equipment-img`, + ), + ); + + if (!styleSheet) { + console.warn( + `No style sheet found for consumable ${consumable.icon}; skipping...`, + ); + return; + } + + const configPath = styleSheet.properties['bg-sprite'] + .replace('~res:/', '') + .replace('.psd', '') + .replace('.txt', ''); + + if (configPath.startsWith('Gfx/Shared')) { + const image = sharp( + await readDVPLFile(`${DATA}/${configPath}.packed.webp.dvpl`), + ); + const content = ( + await image.trim({ threshold: 100 }).toBuffer() + ).toString('base64'); + + changes.push({ + path: `icons/consumables/${consumable.id}.webp`, + encoding: 'base64', + content, + }); + } else { + const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt.dvpl`)) + .split('\n')[4] + .split(' ') + .map(Number); + const content = ( + await consumablesTexture + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer() + ).toString('base64'); + + changes.push({ + path: `icons/consumables/${consumable.id}.webp`, + encoding: 'base64', + content, + }); + } + }), + ); + + await Promise.all( + Object.values(provisionsCommon.root).map(async (provision) => { + const styleSheet = styleSheets.find((styleSheet) => + styleSheet.selectors.includes( + `.equipment_item.${provision.icon} * .equipment-img`, + ), + ); + + if (!styleSheet) { + console.warn( + `No style sheet found for provision ${provision.icon}; skipping...`, + ); + return; + } + + const configPath = styleSheet.properties['bg-sprite'] + .replace('~res:/', '') + .replace('.psd', '') + .replace('.txt', ''); + + if (configPath.startsWith('Gfx/Shared')) { + const image = sharp( + await readDVPLFile(`${DATA}/${configPath}.packed.webp.dvpl`), + ); + const content = ( + await image.trim({ threshold: 100 }).toBuffer() + ).toString('base64'); + + changes.push({ + path: `icons/provisions/${provision.id}.webp`, + encoding: 'base64', + content, + }); + } else { + const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt.dvpl`)) + .split('\n')[4] + .split(' ') + .map(Number); + const content = ( + await provisionsTexture + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer() + ).toString('base64'); + + changes.push({ + path: `icons/provisions/${provision.id}.webp`, + encoding: 'base64', + content, + }); + } + }), + ); + + await commitAssets('consumable and provision icons', changes, production); +} diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts new file mode 100644 index 000000000..41a7c5f08 --- /dev/null +++ b/scripts/buildAssets/definitions.ts @@ -0,0 +1,1017 @@ +import { readdir } from 'fs/promises'; +import { parse as parsePath } from 'path'; +import { Vector3Tuple } from 'three'; +import { parse as parseYaml } from 'yaml'; +import { TankType } from '../../src/components/Tanks'; +import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; +import { toUniqueId } from '../../src/core/blitz/toUniqueId'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { + ConsumableDefinitions, + TankFilterDefinitionCategory, +} from '../../src/core/blitzkrieg/consumableDefinitions'; +import { + EquipmentDefinitions, + EquipmentPreset, + EquipmentRow, +} from '../../src/core/blitzkrieg/equipmentDefinitions'; +import { + ModelArmor, + ModelDefinitions, +} from '../../src/core/blitzkrieg/modelDefinitions'; +import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions'; +import { superCompress } from '../../src/core/blitzkrieg/superCompress'; +import { + Crew, + CrewMember, + GunDefinition, + ShellType, + TankDefinitionPrice, + TankDefinitions, + Tier, +} from '../../src/core/blitzkrieg/tankDefinitions'; +import { DATA, POI } from './constants'; +import { TankParameters } from './tankIcons'; + +type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank'; +interface Strings { + [key: string]: string; +} +type ShellKind = + | 'ARMOR_PIERCING' + | 'ARMOR_PIERCING_CR' + | 'HIGH_EXPLOSIVE' + | 'HOLLOW_CHARGE'; +type ShellDefinitionsList = Record< + string, + { + id: number; + userString: string; + icon: string; + kind: ShellKind; + caliber: number; + damage: { armor: number; devices: number }; + normalizationAngle: number; + ricochetAngle: number; + explosionRadius?: number; + } +> & { + icons: Record; +}; +interface EngineDefinitionsList { + nextAvailableId: number; + ids: Record; + shared: { + [key: string]: { + userString: string; + level: number; + fireStartingChance: number; + weight: number; + power: number; + }; + }; +} +export interface ChassisDefinitionsList { + nextAvailableId: number; + ids: Record; +} +type VehicleDefinitionArmor = Record< + string, + number | { vehicleDamageFactor: 0; '#text': number } +>; +interface VehicleDefinitions { + invisibility: { + moving: number; + still: number; + firePenalty: number; + }; + crew: Record; + speedLimits: { + forward: number; + backward: number; + }; + consumableSlots: number; + provisionSlots: number; + optDevicePreset: string; + hull: { + armor: VehicleDefinitionArmor; + turretPositions: { turret: string }; + turretInitialRotation?: { yaw: 0; pitch: 6.5; roll: 0 }; + maxHealth: number; + weight: number; + }; + chassis: { + [key: string]: { + weight: number; + terrainResistance: string; + rotationSpeed: number; + shotDispersionFactors: { + vehicleMovement: number; + vehicleRotation: number; + }; + level: number; + hullPosition: string; + armor: { + leftTrack: + | number + | { + chanceToHitByProjectile: number; + chanceToHitByExplosion: number; + '#text': number; + }; + rightTrack: + | number + | { + chanceToHitByProjectile: number; + chanceToHitByExplosion: number; + '#text': number; + }; + }; + }; + }; + engines: { + [key: string]: unknown; + }; + turrets0: { + [key: string]: { + rotationSpeed: number; + weight: number; + circularVisionRadius: number; + maxHealth: number; + armor: VehicleDefinitionArmor; + userString: number; + level: number; + yawLimits: string | string[]; + gunPosition: string | string[]; + models: { undamaged: string }; + hitTester: { + collisionModel: string; + }; + guns: { + [key: string]: { + armor: VehicleDefinitionArmor & { + gun?: + | number + | { + chanceToHitByProjectile: number; + chanceToHitByExplosion: number; + '#text': number; + }; + }; + invisibilityFactorAtShot: number | number[]; + reloadTime: number; + aimingTime?: number; + shotDispersionRadius: number; + shotDispersionFactors?: { + turretRotation: number; + afterShot: number; + whileGunDamaged: number; + }; + maxAmmo: number; + extraPitchLimits?: { + front?: string; + back?: string; + transition?: number | number[]; + }; + pitchLimits?: string | string[]; + pumpGunMode?: boolean; + pumpGunReloadTimes?: string; + clip?: { count: number; rate: number }; + models: { undamaged: string }; + }; + }; + }; + }; +} +export interface VehicleDefinitionList { + [key: string]: { + id: number; + userString: string; + shortUserString?: string; + description: string; + price: number | { gold: ''; '#text': number }; + sellPrice?: { gold: ''; '#text': number }; + enrichmentPermanentCost: number; + notInShop?: boolean; + tags: string; + level: number; + combatRole?: Record; + configurationModes: string; + }; +} +interface TurretDefinitionsList { + nextAvailableId: number; + ids: Record; +} +interface GunDefinitionsList { + nextAvailableId: number; + ids: Record; + shared: { + [key: string]: { + rotationSpeed: number; + weight: number; + shotDispersionFactors: { + turretRotation: number; + afterShot: number; + whileGunDamaged: number; + }; + aimingTime: number; + shotDispersionRadius: number; + userString: string; + tags: string; + level: number; + pitchLimits: string; + burst?: { count: number; rate: number }; + clip?: { count: number; rate: number }; + shots: { + [key: string]: { + speed: number; + piercingPower: string; + }; + }; + }; + }; +} + +export interface OptionalDevices { + [key: string]: { + id: number; + userString: string; + description: string; + icon: string; + script: unknown; + display_params: unknown; + }; +} + +interface OptionalDeviceSlots { + presets: { + [key: string]: { + level0: OptionalDeviceSlotRow; + level1: OptionalDeviceSlotRow; + level2: OptionalDeviceSlotRow; + }; + }; +} + +interface OptionalDeviceSlotRow { + [key: string]: { device0: string; device1: string }; +} +export interface ConsumablesCommon { + [key: string]: { + id: number; + userString: string; + description: string; + icon: string; + category: string; + tags: string; + vehicleFilter?: { + include: { vehicle: ConsumablesVehicleFilter; nations?: string }; + exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; + }; + script: { + '#text': string; + automatic?: boolean; + cooldown: number; + duration?: number; + shotEffect?: string; + bonusValues?: { [key: string]: number }; + } & Record; + }; +} +export interface ProvisionsCommon { + [key: string]: { + id: number; + userString: string; + description: string; + icon: string; + category: string; + tags: string; + vehicleFilter?: { + include: { vehicle: ConsumablesVehicleFilter; nations?: string }; + exclude?: { vehicle: ConsumablesVehicleFilter; nations?: string }; + }; + script: { + '#text': string; + automatic?: boolean; + shotEffect?: string; + bonusValues?: { [key: string]: number }; + } & Record; + }; +} + +type ConsumablesVehicleFilter = + | { minLevel: number; maxLevel: number } + | { name: string } + | { extendedTags: string }; + +const consumableProvisionEffectSuffixes = [ + 'Increase', + 'Factor', + 'Bias', + 'Coef', + 'Percent', +]; +const blitzTankTypeToBlitzkrieg: Record = { + 'AT-SPG': 'tankDestroyer', + lightTank: 'light', + mediumTank: 'medium', + heavyTank: 'heavy', +}; +const blitzShellKindToBLitzkrieg: Record = { + ARMOR_PIERCING: 'ap', + ARMOR_PIERCING_CR: 'ap_cr', + HIGH_EXPLOSIVE: 'he', + HOLLOW_CHARGE: 'hc', +}; +const missingStrings: Record = { + '#artefacts:tungstentip/name': 'Tungsten Shells', +}; + +export async function definitions(production: boolean) { + console.log('Building definitions...'); + + const tankDefinitions: TankDefinitions = {}; + const modelDefinitions: ModelDefinitions = {}; + const equipmentDefinitions: EquipmentDefinitions = { + presets: {}, + equipments: {}, + }; + const consumableDefinitions: ConsumableDefinitions = {}; + const provisionDefinitions: ProvisionDefinitions = {}; + const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( + (nations) => nations.filter((nation) => nation !== 'common'), + ); + const tankStringIdMap: Record = {}; + const stringsPreInstalled = await readYAMLDVPL( + `${DATA}/${POI.strings}/en.yaml.dvpl`, + ); + const stringsCache = await fetch(POI.cachedStrings) + .then((response) => response.text()) + .then((string) => parseYaml(string) as Strings); + const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( + `${DATA}/${POI.optionalDevices}.dvpl`, + ); + const strings = { + ...stringsPreInstalled, + ...stringsCache, + }; + const optionalDeviceSlots = await readXMLDVPL<{ + root: OptionalDeviceSlots; + }>(`${DATA}/${POI.optionalDeviceSlots}.dvpl`); + const consumables = await readXMLDVPL<{ root: ConsumablesCommon }>( + `${DATA}/${POI.consumablesCommon}.dvpl`, + ); + const provisions = await readXMLDVPL<{ root: ProvisionsCommon }>( + `${DATA}/${POI.provisionsCommon}.dvpl`, + ); + + await Promise.all( + nations.map(async (nation) => { + const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`, + ); + const turretList = await readXMLDVPL<{ + root: TurretDefinitionsList; + }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/components/turrets.xml.dvpl`, + ); + const gunList = await readXMLDVPL<{ + root: GunDefinitionsList; + }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/components/guns.xml.dvpl`, + ); + const shellList = await readXMLDVPL<{ + root: ShellDefinitionsList; + }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/components/shells.xml.dvpl`, + ); + const enginesList = await readXMLDVPL<{ + root: EngineDefinitionsList; + }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/components/engines.xml.dvpl`, + ); + const chassisList = await readXMLDVPL<{ + root: ChassisDefinitionsList; + }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/components/chassis.xml.dvpl`, + ); + + for (const tankKey in tankList.root) { + const tank = tankList.root[tankKey]; + const tankPrice: TankDefinitionPrice = + typeof tank.price === 'number' + ? { type: 'credits', value: tank.price } + : { type: 'gold', value: tank.price['#text'] }; + const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/${tankKey}.xml.dvpl`, + ); + const tankParameters = await readYAMLDVPL( + `${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`, + ); + const turretOrigin = tankDefinition.root.hull.turretPositions.turret + .split(' ') + .map(Number) as Vector3Tuple; + const tankId = toUniqueId(nation, tank.id); + const tankTags = tank.tags.split(' '); + const hullArmor: ModelArmor = { thickness: {} }; + const equipment = tankDefinition.root.optDevicePreset; + tankStringIdMap[`${nation}:${tankKey}`] = tankId; + + Object.keys(tankDefinition.root.hull.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + + const armorId = parseInt(armorIdString); + const armorRaw = tankDefinition.root.hull.armor[name]; + + if (typeof armorRaw === 'number') { + hullArmor.thickness[armorId] = armorRaw; + } else { + if (!hullArmor.spaced) hullArmor.spaced = []; + hullArmor.thickness[armorId] = armorRaw['#text']; + hullArmor.spaced.push(armorId); + } + }); + const crew: Crew[] = []; + const fixedCamouflage = tankTags.includes('eventCamouflage_user'); + + Object.entries(tankDefinition.root.crew).forEach(([key, value]) => { + let entry: Crew; + const index = crew.findIndex(({ type }) => type === key); + if (index === -1) { + entry = { type: key as CrewMember }; + crew.push(entry); + } else { + entry = crew[index]; + } + + if (typeof value === 'string') { + if (entry.count === undefined) { + entry.count = 1; + } else { + entry.count++; + } + + if (value !== '') { + entry.substitute = value + .split('\n') + .map((member) => member.trim()) as CrewMember[]; + } + } else { + if (entry.count === undefined) { + entry.count = value.length; + } else { + entry.count += value.length; + } + } + + if (entry.count === 1) { + delete entry.count; + } + }); + + tankDefinitions[tankId] = { + id: tankId, + fixedCamouflage: fixedCamouflage ? true : undefined, + crew, + weight: tankDefinition.root.hull.weight, + health: tankDefinition.root.hull.maxHealth, + speed: { + forwards: tankDefinition.root.speedLimits.forward, + backwards: tankDefinition.root.speedLimits.backward, + }, + equipment, + consumables: tankDefinition.root.consumableSlots, + provisions: tankDefinition.root.provisionSlots, + name: + (tank.shortUserString + ? strings[tank.shortUserString] + : undefined) ?? strings[tank.userString], + nameFull: strings[tank.userString], + nation, + treeType: tankTags.includes('collectible') + ? 'collector' + : (typeof tank.price === 'number' ? false : 'gold' in tank.price) + ? 'premium' + : 'researchable', + tier: tank.level as Tier, + type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType], + testing: tankTags.includes('testTank') ? true : undefined, + price: tankPrice, + camouflage: { + still: tankDefinition.root.invisibility.still, + moving: tankDefinition.root.invisibility.moving, + onFire: tankDefinition.root.invisibility.firePenalty, + }, + turrets: [], + engines: [], + tracks: [], + }; + + if (tankDefinitions[tankId].name === tankDefinitions[tankId].nameFull) { + delete tankDefinitions[tankId].nameFull; + } + + modelDefinitions[tankId] = { + armor: hullArmor, + turretOrigin, + turretRotation: tankDefinition.root.hull.turretInitialRotation + ? { + yaw: tankDefinition.root.hull.turretInitialRotation.yaw, + pitch: tankDefinition.root.hull.turretInitialRotation.pitch, + roll: tankDefinition.root.hull.turretInitialRotation.roll, + } + : undefined, + boundingBox: tankParameters.collision.hull.bbox, + turrets: {}, + tracks: {}, + }; + + Object.keys(tankDefinition.root.chassis).forEach((key) => { + const track = tankDefinition.root.chassis[key]; + const trackId = toUniqueId(nation, chassisList.root.ids[key]); + const terrainResistances = track.terrainResistance + .split(' ') + .map(Number); + const trackArmorRaw = track.armor.leftTrack; + const hullOrigin = track.hullPosition + .split(' ') + .map(Number) as Vector3Tuple; + + tankDefinitions[tankId].tracks.push({ + id: trackId, + weight: track.weight, + traverseSpeed: track.rotationSpeed, + dispersion: { + move: track.shotDispersionFactors.vehicleMovement, + traverse: track.shotDispersionFactors.vehicleRotation, + }, + resistance: { + hard: terrainResistances[0], + medium: terrainResistances[1], + soft: terrainResistances[2], + }, + tier: track.level as Tier, + }); + + modelDefinitions[tankId].tracks[trackId] = { + thickness: + typeof trackArmorRaw === 'number' + ? trackArmorRaw + : trackArmorRaw['#text'], + origin: hullOrigin, + }; + }); + + Object.keys(tankDefinition.root.engines).forEach((engineKey) => { + const engine = enginesList.root.shared[engineKey]; + const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]); + + tankDefinitions[tankId].engines.push({ + id: engineId, + name: strings[engine.userString], + fireChance: engine.fireStartingChance, + tier: engine.level as Tier, + weight: engine.weight, + power: engine.power, + }); + }); + + Object.keys(tankDefinition.root.turrets0).forEach( + (turretKey, turretIndex) => { + const turret = tankDefinition.root.turrets0[turretKey]; + const turretModel = Number( + parsePath(turret.models.undamaged).name.split('_')[1], + ); + const turretId = toUniqueId(nation, turretList.root.ids[turretKey]); + const turretYaw = ( + typeof turret.yawLimits === 'string' + ? turret.yawLimits + : turret.yawLimits.at(-1)! + ) + .split(' ') + .map(Number) as [number, number]; + const gunOrigin = ( + typeof turret.gunPosition === 'string' + ? turret.gunPosition + : turret.gunPosition[0] + ) + .split(' ') + .map(Number) as Vector3Tuple; + const turretArmor: ModelArmor = { thickness: {} }; + + Object.keys(turret.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + + const armorId = parseInt(armorIdString); + const armorRaw = turret.armor[name]; + + if (typeof armorRaw === 'number') { + turretArmor.thickness[armorId] = armorRaw; + } else { + if (!turretArmor.spaced) turretArmor.spaced = []; + turretArmor.thickness[armorId] = armorRaw['#text']; + turretArmor.spaced.push(armorId); + } + }); + + tankDefinitions[tankId].turrets.push({ + id: turretId, + traverseSpeed: turret.rotationSpeed, + name: + strings[turret.userString] ?? + turretKey + .replaceAll('_', ' ') + .replace(/^(Turret ([0-9] )?)+/, ''), + tier: turret.level as Tier, + guns: [], + health: turret.maxHealth, + viewRange: turret.circularVisionRadius, + weight: turret.weight, + }); + + modelDefinitions[tankId].turrets[turretId] = { + armor: turretArmor, + gunOrigin, + model: turretModel, + yaw: + -turretYaw[0] + turretYaw[1] === 360 + ? undefined + : { min: turretYaw[0], max: turretYaw[1] }, + guns: {}, + boundingBox: + tankParameters.collision[ + parsePath(turret.hitTester.collisionModel).name.toLowerCase() + ].bbox, + }; + + Object.keys(turret.guns).forEach((gunKey, gunIndex) => { + const turretGunEntry = turret.guns[gunKey]; + const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); + const gunListEntry = gunList.root.shared[gunKey]; + const pitchLimitsRaw = + turretGunEntry.pitchLimits ?? gunListEntry.pitchLimits; + const gunPitch = ( + typeof pitchLimitsRaw === 'string' + ? pitchLimitsRaw + : pitchLimitsRaw.at(-1)! + ) + .split(' ') + .map(Number) as [number, number]; + const gunModel = Number( + parsePath(turretGunEntry.models.undamaged).name.split('_')[1], + ); + const gunName = + strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' '); + const gunType = + 'clip' in turretGunEntry + ? turretGunEntry.pumpGunMode + ? 'autoReloader' + : 'autoLoader' + : 'regular'; + const gunReload = + gunType === 'autoReloader' + ? turretGunEntry.pumpGunReloadTimes!.split(' ').map(Number) + : turretGunEntry.reloadTime; + const gunClipCount = + gunType === 'regular' ? undefined : turretGunEntry.clip!.count; + const gunInterClip = + gunType === 'regular' + ? undefined + : 60 / turretGunEntry.clip!.rate; + const front = turretGunEntry.extraPitchLimits?.front + ? turretGunEntry.extraPitchLimits.front.split(' ').map(Number) + : undefined; + const back = turretGunEntry.extraPitchLimits?.back + ? turretGunEntry.extraPitchLimits.back.split(' ').map(Number) + : undefined; + const transition = turretGunEntry.extraPitchLimits?.transition + ? typeof turretGunEntry.extraPitchLimits.transition === 'number' + ? turretGunEntry.extraPitchLimits.transition + : turretGunEntry.extraPitchLimits.transition.at(-1)! + : undefined; + const gunArmor: ModelArmor = { thickness: {} }; + const shotDispersionFactors = + turretGunEntry.shotDispersionFactors ?? + gunListEntry.shotDispersionFactors; + + Object.keys(turretGunEntry.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + const armorId = parseInt(armorIdString); + const armorRaw = turretGunEntry.armor[name]; + if (typeof armorRaw === 'number') { + gunArmor.thickness[armorId] = armorRaw; + } else { + if (!gunArmor.spaced) gunArmor.spaced = []; + gunArmor.thickness[armorId] = armorRaw['#text']; + gunArmor.spaced.push(armorId); + } + }); + + tankDefinitions[tankId].turrets[turretIndex].guns.push({ + id: gunId, + weight: gunListEntry.weight, + rotationSpeed: gunListEntry.rotationSpeed, + name: gunName, + tier: gunListEntry.level as Tier, + shells: [], + type: gunType, + reload: gunReload, + count: gunClipCount, + interClip: gunInterClip, + camouflageLoss: + typeof turretGunEntry.invisibilityFactorAtShot === 'number' + ? turretGunEntry.invisibilityFactorAtShot + : turretGunEntry.invisibilityFactorAtShot.at(-1)!, + aimTime: turretGunEntry.aimingTime ?? gunListEntry.aimingTime, + dispersion: { + base: + turretGunEntry.shotDispersionRadius ?? + gunListEntry.shotDispersionRadius, + damaged: shotDispersionFactors.whileGunDamaged, + shot: shotDispersionFactors.afterShot, + traverse: shotDispersionFactors.turretRotation, + }, + } as GunDefinition); + + modelDefinitions[tankId].turrets[turretId].guns[gunId] = { + armor: gunArmor, + model: gunModel, + thickness: + turretGunEntry.armor.gun === undefined + ? 0 + : typeof turretGunEntry.armor.gun === 'number' + ? turretGunEntry.armor.gun + : turretGunEntry.armor.gun['#text'], + pitch: { + min: gunPitch[0], + max: gunPitch[1], + + front: front + ? { + min: front[0], + max: front[1], + range: front[2], + } + : undefined, + back: back + ? { + min: back[0], + max: back[1], + range: back[2], + } + : undefined, + transition, + }, + }; + + Object.keys(gunListEntry.shots).forEach((shellKey) => { + const gunShellEntry = gunListEntry.shots[shellKey]; + const shell = shellList.root[shellKey]; + const shellId = toUniqueId(nation, shell.id); + const shellName = + strings[shell.userString] ?? shellKey.replaceAll('_', ' '); + const penetrationRaw = gunShellEntry.piercingPower + .split(' ') + .filter((penetrationString) => penetrationString !== '') + .map(Number); + + tankDefinitions[tankId].turrets[turretIndex].guns[ + gunIndex + ].shells.push({ + id: shellId, + name: shellName, + speed: gunShellEntry.speed, + damage: { + armor: shell.damage.armor, + module: shell.damage.devices, + }, + caliber: shell.caliber, + normalization: shell.normalizationAngle, + ricochet: shell.ricochetAngle, + type: blitzShellKindToBLitzkrieg[shell.kind], + explosionRadius: + shell.kind === 'HIGH_EXPLOSIVE' + ? shell.explosionRadius ?? 0 + : undefined, + icon: shell.icon, + penetration: + penetrationRaw[0] === penetrationRaw[1] + ? penetrationRaw[0] + : [penetrationRaw[0], penetrationRaw[1]], + }); + }); + }); + }, + ); + } + }), + ); + + Object.entries(optionalDevices.root).forEach( + ([optionalDeviceKey, optionalDeviceEntry]) => { + if (optionalDeviceKey === 'nextAvailableId') return; + + equipmentDefinitions.equipments[optionalDeviceEntry.id] = { + name: strings[optionalDeviceEntry.userString], + }; + }, + ); + + Object.entries(optionalDeviceSlots.root.presets).forEach( + ([optionalDeviceSlotKey, optionalDeviceSlotEntry]) => { + if (optionalDeviceSlotKey === 'emptyPreset') return; + + equipmentDefinitions.presets[optionalDeviceSlotKey] = Object.values( + optionalDeviceSlotEntry, + ).map( + (level) => + Object.values(level).map((options) => { + return [ + optionalDevices.root[options.device0].id, + optionalDevices.root[options.device1].id, + ]; + }) as EquipmentRow, + ) as EquipmentPreset; + }, + ); + + Object.values(consumables.root).forEach((consumable) => { + consumableDefinitions[consumable.id] = { + id: consumable.id, + cooldown: consumable.script.cooldown, + duration: consumable.script.duration, + name: + strings[consumable.userString] ?? + missingStrings[consumable.userString] ?? + `Unknown ${consumable.id}`, + include: [], + }; + + const includeRaw = consumable.vehicleFilter?.include.vehicle; + const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle; + + if (includeRaw) { + if ('minLevel' in includeRaw) { + consumableDefinitions[consumable.id].include.push({ + type: 'tier', + min: includeRaw.minLevel, + max: includeRaw.maxLevel, + }); + } else if ('name' in includeRaw) { + consumableDefinitions[consumable.id].include.push({ + type: 'ids', + ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else throw new SyntaxError('Unhandled include type'); + + if (consumable.vehicleFilter?.include.nations) { + consumableDefinitions[consumable.id].include.push({ + type: 'nation', + nations: consumable.vehicleFilter.include.nations.split(' '), + }); + } + } + + if (excludeRaw) { + consumableDefinitions[consumable.id].exclude = []; + + if ('name' in excludeRaw) { + consumableDefinitions[consumable.id].exclude!.push({ + type: 'ids', + ids: excludeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else if ('extendedTags' in excludeRaw) { + consumableDefinitions[consumable.id].exclude!.push({ + type: 'category', + categories: excludeRaw.extendedTags.split( + ' ', + ) as TankFilterDefinitionCategory[], + }); + } else throw new SyntaxError('Unhandled exclude type'); + + if (consumable.vehicleFilter?.exclude?.nations) { + consumableDefinitions[consumable.id].exclude!.push({ + type: 'nation', + nations: consumable.vehicleFilter.exclude.nations.split(' '), + }); + } + } + }); + + Object.values(provisions.root).forEach((provision) => { + provisionDefinitions[provision.id] = { + id: provision.id, + name: + strings[provision.userString] ?? + missingStrings[provision.userString] ?? + `Unknown ${provision.id}`, + include: [], + }; + + const includeRaw = provision.vehicleFilter?.include.vehicle; + const excludeRaw = provision.vehicleFilter?.exclude?.vehicle; + + if (includeRaw) { + if ('minLevel' in includeRaw) { + provisionDefinitions[provision.id].include.push({ + type: 'tier', + min: includeRaw.minLevel, + max: includeRaw.maxLevel, + }); + } else if ('name' in includeRaw) { + provisionDefinitions[provision.id].include.push({ + type: 'ids', + ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else throw new SyntaxError('Unhandled include type'); + + if (provision.vehicleFilter?.include.nations) { + provisionDefinitions[provision.id].include.push({ + type: 'nation', + nations: provision.vehicleFilter.include.nations.split(' '), + }); + } + } + + if (excludeRaw) { + provisionDefinitions[provision.id].exclude = []; + + if ('name' in excludeRaw) { + provisionDefinitions[provision.id].exclude!.push({ + type: 'ids', + ids: excludeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else if ('extendedTags' in excludeRaw) { + provisionDefinitions[provision.id].exclude!.push({ + type: 'category', + categories: excludeRaw.extendedTags.split( + ' ', + ) as TankFilterDefinitionCategory[], + }); + } else throw new SyntaxError('Unhandled exclude type'); + + if (provision.vehicleFilter?.exclude?.nations) { + provisionDefinitions[provision.id].exclude!.push({ + type: 'nation', + nations: provision.vehicleFilter.exclude.nations.split(' '), + }); + } + } + + if (provision.script.bonusValues?.crewLevelIncrease !== undefined) { + provisionDefinitions[provision.id].crew = + provision.script.bonusValues?.crewLevelIncrease; + } + }); + + await commitAssets( + 'definitions', + [ + { + content: superCompress(tankDefinitions), + encoding: 'base64', + path: 'definitions/tanks.cdon.lz4', + }, + { + content: superCompress(modelDefinitions), + encoding: 'base64', + path: 'definitions/models.cdon.lz4', + }, + { + content: superCompress(equipmentDefinitions), + encoding: 'base64', + path: 'definitions/equipment.cdon.lz4', + }, + { + content: superCompress(consumableDefinitions), + encoding: 'base64', + path: 'definitions/consumables.cdon.lz4', + }, + { + content: superCompress(provisionDefinitions), + encoding: 'base64', + path: 'definitions/provisions.cdon.lz4', + }, + ], + production, + ); +} diff --git a/scripts/buildAssets/equipmentIcons.ts b/scripts/buildAssets/equipmentIcons.ts new file mode 100644 index 000000000..31e178b8c --- /dev/null +++ b/scripts/buildAssets/equipmentIcons.ts @@ -0,0 +1,93 @@ +import sharp from 'sharp'; +import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; +import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; +import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; +import { OptionalDevices } from './definitions'; + +interface Mappings { + Header: { version: number }; + + StyleSheets: { + selectors: string[]; + properties: { 'bg-sprite': string }; + }[]; +} + +export async function equipmentIcons(production: boolean) { + console.log('Building equipment icons...'); + + const changes: FileChange[] = []; + const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( + `${DATA}/${POI.optionalDevices}.dvpl`, + ); + const mappings = await readYAMLDVPL( + `${DATA}/${POI.optionalDeviceImageMappings}.dvpl`, + ); + const image = sharp( + await readDVPLFile(`${DATA}/${POI.optionalDevicesImage}.dvpl`), + ); + + await Promise.all( + Object.entries(optionalDevices.root).map( + async ([optionalDeviceKey, optionalDevice]) => { + if (optionalDeviceKey === 'nextAvailableId') return; + + const mapping = mappings.StyleSheets.find((mapping) => + mapping.selectors.includes( + `.optional_device_item.${optionalDevice.icon} #Img`, + ), + ); + + if (!mapping) { + console.warn( + `No mapping found for ${optionalDevice.icon}; skipping...`, + ); + return; + } + + const configPathRaw = mapping.properties['bg-sprite']; + const configPath = configPathRaw.replace('~res:/', ''); + + if (configPath.startsWith('Gfx/Lobby')) { + const configPathWebp = configPath.replace('.txt', ''); + const image = sharp( + await readDVPLFile(`${DATA}/${configPathWebp}.packed.webp.dvpl`), + ); + const content = (await image.trim().toBuffer()).toString('base64'); + + changes.push({ + path: `icons/equipment/${optionalDevice.id}.webp`, + encoding: 'base64', + content, + }); + } else { + const config = await readStringDVPL(`${DATA}/${configPath}.dvpl`); + const sizes = config.split('\n')[4].split(' ').map(Number); + const content = ( + await image + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer() + ).toString('base64'); + + changes.push({ + path: `icons/equipment/${optionalDevice.id}.webp`, + encoding: 'base64', + content, + }); + } + }, + ), + ); + + await commitAssets('equipment icons', changes, production); +} diff --git a/scripts/buildAssets/index.ts b/scripts/buildAssets/index.ts new file mode 100644 index 000000000..b864c754f --- /dev/null +++ b/scripts/buildAssets/index.ts @@ -0,0 +1,45 @@ +import { config } from 'dotenv'; +import { argv } from 'process'; + +config(); + +const allTargets = argv.includes('--all-targets'); +const targets = argv + .find((argument) => argument.startsWith('--target')) + ?.split('=')[1] + .split(','); +const production = argv.includes('--production'); + +import { boosterIcons } from './boosterIcons'; +import { camouflageIcons } from './camouflageIcons'; +import { circleFlags } from './circleFlags'; +import { consumableProvisionIcons } from './consumableProvisionIcons'; +import { definitions } from './definitions'; +import { equipmentIcons } from './equipmentIcons'; +import { moduleIcons } from './moduleIcons'; +import { scratchedFlags } from './scratchedFlags'; +import { shellIcons } from './shellIcons'; +import { tankArmors } from './tankArmors'; +import { tankIcons } from './tankIcons'; +import { tankModels } from './tankModels'; + +if (!targets && !allTargets) throw new Error('No target(s) specified'); + +const methods = [ + definitions, + tankIcons, + scratchedFlags, + circleFlags, + shellIcons, + moduleIcons, + equipmentIcons, + consumableProvisionIcons, + camouflageIcons, + boosterIcons, + tankArmors, + tankModels, +]; + +for (const method of methods) { + if (allTargets || targets?.includes(method.name)) await method(production); +} diff --git a/scripts/buildAssets/moduleIcons.ts b/scripts/buildAssets/moduleIcons.ts new file mode 100644 index 000000000..1866e9aa4 --- /dev/null +++ b/scripts/buildAssets/moduleIcons.ts @@ -0,0 +1,32 @@ +import { readdir } from 'fs/promises'; +import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; + +export async function moduleIcons(production: boolean) { + console.log('Building module icons...'); + + const changes = await Promise.all( + (await readdir(`${DATA}/${POI.moduleIcons}`)) + .filter( + (file) => + !file.endsWith('@2x.packed.webp.dvpl') && file.startsWith('vehicle'), + ) + .map(async (file) => { + const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1]; + const name = nameRaw[0].toLowerCase() + nameRaw.slice(1); + const content = await readBase64DVPL( + `${DATA}/${POI.moduleIcons}/${file}`, + ); + + return { + content, + path: `icons/modules/${name}.webp`, + encoding: 'base64', + } satisfies FileChange; + }), + ); + + await commitAssets('module icons', changes, production); +} diff --git a/scripts/buildAssets/scratchedFlags.ts b/scripts/buildAssets/scratchedFlags.ts new file mode 100644 index 000000000..8bde6509b --- /dev/null +++ b/scripts/buildAssets/scratchedFlags.ts @@ -0,0 +1,31 @@ +import { readdir } from 'fs/promises'; +import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; + +export async function scratchedFlags(production: boolean) { + console.log('Building scratched flags...'); + + const flags = await readdir(`${DATA}/${POI.flags}`); + const changes = await Promise.all( + flags + .filter( + (flag) => + flag.startsWith('flag_tutor-tank_') && + !flag.endsWith('@2x.packed.webp.dvpl'), + ) + .map(async (flag) => { + const content = await readBase64DVPL(`${DATA}/${POI.flags}/${flag}`); + const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1]; + + return { + content, + encoding: 'base64', + path: `flags/scratched/${name}.webp`, + } satisfies FileChange; + }), + ); + + await commitAssets('scratched flags', changes, production); +} diff --git a/scripts/buildAssets/shellIcons.ts b/scripts/buildAssets/shellIcons.ts new file mode 100644 index 000000000..350bfd72e --- /dev/null +++ b/scripts/buildAssets/shellIcons.ts @@ -0,0 +1,49 @@ +import { readdir } from 'fs/promises'; +import sharp from 'sharp'; +import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; +import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; + +export async function shellIcons(production: boolean) { + console.log('Building shell icons...'); + + const image = sharp( + await readDVPLFile( + `${DATA}/${POI.bigShellIcons}/texture0.packed.webp.dvpl`, + ), + ); + + const changes = await Promise.all( + (await readdir(`${DATA}/${POI.bigShellIcons}`)) + .filter((file) => file.endsWith('_l.txt.dvpl')) + .map(async (file) => { + const name = file.match(/(.+)_l\.txt\.dvpl/)![1]; + const sizes = ( + await readStringDVPL(`${DATA}/${POI.bigShellIcons}/${file}`) + ) + .split('\n')[4] + .split(' ') + .map(Number); + + return { + content: ( + await image + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer() + ).toString('base64'), + encoding: 'base64', + path: `icons/shells/${name}.webp`, + } satisfies FileChange; + }), + ); + + await commitAssets('shell icons', changes, production); +} diff --git a/scripts/buildAssets/tankArmors.ts b/scripts/buildAssets/tankArmors.ts new file mode 100644 index 000000000..9e67677e2 --- /dev/null +++ b/scripts/buildAssets/tankArmors.ts @@ -0,0 +1,49 @@ +import { NodeIO } from '@gltf-transform/core'; +import { readdir } from 'fs/promises'; +import { extractArmor } from '../../src/core/blitz/extractArmor'; +import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { toUniqueId } from '../../src/core/blitz/toUniqueId'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; +import { VehicleDefinitionList } from './definitions'; + +export async function tankArmors(production: boolean) { + console.log('Building tank armors...'); + + const changes: FileChange[] = []; + const nodeIO = new NodeIO(); + const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( + (nations) => nations.filter((nation) => nation !== 'common'), + ); + + await Promise.all( + nations.map(async (nation) => { + const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`, + ); + + await Promise.all( + Object.entries(tanks.root).map(async ([tankKey, tank]) => { + if (tankKey.includes('tutorial_bot')) return; + + const id = toUniqueId(nation, tank.id); + + console.log(`Building armor ${id} @ ${nation}/${tankKey}`); + + const model = await extractArmor(DATA, `${nation}-${tankKey}`); + + changes.push({ + path: `3d/tanks/armor/${id}.glb`, + encoding: 'base64', + content: Buffer.from(await nodeIO.writeBinary(model)).toString( + 'base64', + ), + }); + }), + ); + }), + ); + + await commitAssets('tank armor', changes, production); +} diff --git a/scripts/buildAssets/tankIcons.ts b/scripts/buildAssets/tankIcons.ts new file mode 100644 index 000000000..2bef7b303 --- /dev/null +++ b/scripts/buildAssets/tankIcons.ts @@ -0,0 +1,81 @@ +import { readdir } from 'fs/promises'; +import { Vector3Tuple } from 'three'; +import { NATION_IDS } from '../../src/constants/nations'; +import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; +import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; +import { VehicleDefinitionList } from './definitions'; + +export interface TankParameters { + resourcesPath: { + smallIconPath: string; + bigIconPath: string; + blitzModelPath: string; + }; + collision: { + [key: string]: { + points: Vector3Tuple; + bbox: { + min: Vector3Tuple; + max: Vector3Tuple; + }; + averageThickness?: Record; + }; + }; +} + +export async function tankIcons(production: boolean) { + console.log('Building tank icons...'); + + const changes: FileChange[] = []; + const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( + (nations) => nations.filter((nation) => nation !== 'common'), + ); + + await Promise.all( + nations.map(async (nation) => { + const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`, + ); + + await Promise.all( + Object.entries(tanks.root).map(async ([tankKey, tank]) => { + if (tankKey.includes('tutorial_bot')) return; + + const nationVehicleId = tank.id; + const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; + if (12913 !== id) return; + const parameters = await readYAMLDVPL( + `${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`, + ); + const small = `${DATA}/${parameters.resourcesPath.smallIconPath + .replace(/~res:\//, '') + .replace(/\..+/, '')}.packed.webp.dvpl`; + const big = `${DATA}/${parameters.resourcesPath.bigIconPath + .replace(/~res:\//, '') + .replace(/\..+/, '')}.packed.webp.dvpl`; + + if (big) { + changes.push({ + content: await readBase64DVPL(big), + encoding: 'base64', + path: `icons/tanks/big/${id}.webp`, + }); + } + if (small) { + changes.push({ + content: await readBase64DVPL(small), + encoding: 'base64', + path: `icons/tanks/small/${id}.webp`, + }); + } + }), + ); + }), + ); + + await commitAssets('tank icons', changes, production); +} diff --git a/scripts/buildAssets/tankModels.ts b/scripts/buildAssets/tankModels.ts new file mode 100644 index 000000000..edf276fe9 --- /dev/null +++ b/scripts/buildAssets/tankModels.ts @@ -0,0 +1,77 @@ +import { NodeIO } from '@gltf-transform/core'; +import { readdir } from 'fs/promises'; +import { Vector3Tuple } from 'three'; +import { extractModel } from '../../src/core/blitz/extractModel'; +import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; +import { toUniqueId } from '../../src/core/blitz/toUniqueId'; +// import { AssetCommit } from '../../src/core/github/assetCommit'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, POI } from './constants'; +import { VehicleDefinitionList } from './definitions'; +import { TankParameters } from './tankIcons'; + +interface VehicleCustomization { + armorColor: string; +} + +export async function tankModels(production: boolean) { + console.log('Building tank models...'); + + const nodeIO = new NodeIO(); + const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( + (nations) => nations.filter((nation) => nation !== 'common'), + ); + + for (const nationIndex in nations) { + const nation = nations[nationIndex]; + const changes: FileChange[] = []; + // const assetCommit = new AssetCommit(`tank models ${nation}`, production); + const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`, + ); + const customization = await readXMLDVPL<{ root: VehicleCustomization }>( + `${DATA}/${POI.vehicleDefinitions}/${nation}/customization.xml.dvpl`, + ); + const baseColor = customization.root.armorColor + .split(' ') + .slice(0, 3) + .map(Number) + .map((channel) => channel / 255) as Vector3Tuple; + + console.log(`Building models for ${nation}`); + + await Promise.all( + Object.entries(tanks.root).map(async ([tankKey, tank]) => { + if (tankKey.includes('tutorial_bot')) return; + + const id = toUniqueId(nation, tank.id); + + console.log(`Building model ${id} @ ${nation}/${tankKey}`); + + const parameters = await readYAMLDVPL( + `${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`, + ); + const model = await extractModel( + DATA, + parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''), + baseColor, + ); + + // writeFile(`test.${tankKey}.glb`, await nodeIO.writeBinary(model)); + + changes.push({ + path: `3d/tanks/models/${id}.glb`, + content: Buffer.from(await nodeIO.writeBinary(model)).toString( + 'base64', + ), + encoding: 'base64', + }); + }), + ); + + // assetCommit.push(); + await commitAssets(`tank models ${nation}`, changes, production); + } +} diff --git a/scripts/buildRatingsLeaderboard.ts b/scripts/buildRatingsLeaderboard.ts index 64d00a244..891ea6edf 100644 --- a/scripts/buildRatingsLeaderboard.ts +++ b/scripts/buildRatingsLeaderboard.ts @@ -1,6 +1,5 @@ -import { Octokit } from '@octokit/rest'; import { rm, writeFile } from 'fs/promises'; -import { argv, env } from 'process'; +import { argv } from 'process'; import { RatingsInfo, RatingsNeighbors, @@ -115,7 +114,6 @@ for (let index = 0; index < PLAYERS; index++) { const leaderboardJSON = JSON.stringify(leaderboard); if (publish) { - const octokit = new Octokit({ auth: env.GH_TOKEN }); const info = await patientFetchJSON( `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, ); diff --git a/scripts/setupDepotDownloader.ts b/scripts/setupDepotDownloader.ts new file mode 100644 index 000000000..37aa6858c --- /dev/null +++ b/scripts/setupDepotDownloader.ts @@ -0,0 +1,33 @@ +import decompress from 'decompress'; +import { existsSync } from 'fs'; +import { mkdir, readdir, writeFile } from 'fs/promises'; + +if (!existsSync('temp')) { + console.log('Creating temp directory...'); + await mkdir('temp'); +} + +console.log('Downloading depot downloader...'); +await fetch( + 'https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest', +) + .then( + (response) => + response.json() as Promise<{ + assets: { browser_download_url: string; name: string }[]; + }>, + ) + .then(({ assets }) => { + const asset = assets.find(({ name }) => name.includes('windows-x64')); + if (!asset) throw new Error('No asset found'); + return fetch(asset.browser_download_url); + }) + .then((response) => response.arrayBuffer()) + .then((arrayBuffer) => + writeFile('temp/depotDownloader.zip', Buffer.from(arrayBuffer)), + ); + +console.log('Unzipping...'); +await decompress('temp/depotDownloader.zip', 'temp/depotDownloader'); + +console.log('Download complete; files:', await readdir('temp/depotDownloader')); diff --git a/src/app/embeds/session/page.tsx b/src/app/embeds/session/page.tsx index 160de7ac3..c4b81a6f9 100644 --- a/src/app/embeds/session/page.tsx +++ b/src/app/embeds/session/page.tsx @@ -3,13 +3,12 @@ import { ContextMenu } from '@radix-ui/themes'; import { useEffect, useState } from 'react'; import * as Breakdown from '../../../components/Breakdown'; -import { TreeTypeEnum } from '../../../components/Tanks'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; import fetchBlitz from '../../../core/blitz/fetchBlitz'; import { - BlitzkriegTankDefinition, + TankDefinition, tankDefinitions, -} from '../../../core/blitzkrieg/definitions/tanks'; +} from '../../../core/blitzkrieg/tankDefinitions'; import { tankAverages } from '../../../core/blitzstars/tankAverages'; import calculateWN8 from '../../../core/statistics/calculateWN8'; import { deltaTankStats } from '../../../core/statistics/deltaTankStats'; @@ -37,7 +36,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) { list: { stats: IndividualTankStats; - tankDefinitions?: BlitzkriegTankDefinition; + tankDefinitions?: TankDefinition; career: IndividualTankStats; currentWN8?: number; careerWN8?: number; @@ -313,12 +312,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) { } type="tank" tankType={tankDefinitions?.type} - treeType={(() => { - if (tankDefinitions?.tree_type === 'collector') - return TreeTypeEnum.Collector; - if (tankDefinitions?.tree_type === 'premium') - return TreeTypeEnum.Premium; - })()} + treeType={tankDefinitions?.treeType} stats={session.customColumns.map((customColumn) => { const rowStat = rowStats[customColumn.display]; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fcc77e566..f408b1185 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,15 +1,25 @@ 'use client'; -import { Flex, Theme } from '@radix-ui/themes'; +import { CaretRightIcon } from '@radix-ui/react-icons'; +import { AlertDialog, Button, Flex, Text, Theme } from '@radix-ui/themes'; import '@radix-ui/themes/styles.css'; +import { Analytics } from '@vercel/analytics/react'; import { config } from 'dotenv'; import { Roboto_Flex } from 'next/font/google'; import { usePathname } from 'next/navigation'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; +import { SkyLine } from '../assets/art/SkyLine'; import Navbar from '../components/Navbar'; +import PageWrapper from '../components/PageWrapper'; +import isDev from '../core/blitzkrieg/isDev'; +import { isLocalhost } from '../core/blitzkrieg/isLocalhost'; +import { useApp } from '../stores/app'; +import { mintTheme } from '../themes/mint'; config(); +const DEV_BUILD_AGREEMENT_COOLDOWN = 8 * 24 * 60 * 60 * 1000; + interface RootLayoutProps { children: ReactNode; } @@ -22,6 +32,16 @@ const robotoFlex = Roboto_Flex({ export default function RootLayout({ children }: RootLayoutProps) { const pathname = usePathname(); const isEmbed = pathname.split('/')[1] === 'embeds'; + const [showDevBuildAlert, setShowDevBuildAlert] = useState(false); + + useEffect(() => { + setShowDevBuildAlert( + isDev() && + !isLocalhost() && + Date.now() - useApp.getState().devBuildAgreementTime >= + DEV_BUILD_AGREEMENT_COOLDOWN, + ); + }, []); return ( @@ -37,11 +57,10 @@ export default function RootLayout({ children }: RootLayoutProps) { style={{ margin: 0, paddingTop: isEmbed ? 0 : '3.25rem', - background: isEmbed - ? 'transparent' - : 'url(https://i.imgur.com/PhS06NJ.png)', }} > + + {!isEmbed && } + + + Experimental version! + + This version may have a lot of issues. Report issues to{' '} + + the official Discord server + + . Also consider using{' '} + + the more stable release version + + . You will be asked again in 8 days. + + + + + + + {children} + + + + + + + Blitzkrieg. Free forever. For everyone. + + + Consider supporting today 🍀 + + + + + + + + + + + + + diff --git a/src/app/page.css.ts b/src/app/page.css.ts index a4c4ebbc6..ff2585631 100644 --- a/src/app/page.css.ts +++ b/src/app/page.css.ts @@ -3,7 +3,7 @@ import { theme } from '../stitches.config'; export const tool = styleVariants({ enabled: { - boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`, + boxShadow: `inset 0 -256px 512px -128px ${theme.colors.appBackground1}`, ':hover': { boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`, diff --git a/src/app/page.tsx b/src/app/page.tsx index 5437eacd1..9b9ed47c1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -30,7 +30,8 @@ export default function Page() { ({ node: ( + {progress !== undefined && ( + + {Math.round(progress * 100)}% + + )} + + + ); + + if (naked) return animation; + + return ( + + {animation} + + ); +} diff --git a/src/app/tools/layout.tsx b/src/app/tools/layout.tsx index ebaae8f62..dc25ac5b0 100644 --- a/src/app/tools/layout.tsx +++ b/src/app/tools/layout.tsx @@ -1,9 +1,11 @@ 'use client'; +import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { ReactNode } from 'react'; +import { ReactNode, Suspense } from 'react'; import { TOOLS } from '../../constants/tools'; import { theme } from '../../stitches.config'; +import { Loader } from './components/Loader'; interface ToolsLayoutProps { children: ReactNode; @@ -11,59 +13,67 @@ interface ToolsLayoutProps { export default function ToolsLayout({ children }: ToolsLayoutProps) { const pathname = usePathname(); - const toolId = pathname.split('/').at(2); + const pathSplit = pathname.split('/'); + const toolId = pathSplit.at(2); const tool = TOOLS.find((tool) => tool.id === toolId); return ( <> -
+ {tool && {tool.title}} + + {pathSplit.length === 3 && (
- - - {tool?.title} - - - {tool?.description} - -
+
- {children} + + {tool?.title} + + + {tool?.description} + +
+ )} + + }>{children} ); } diff --git a/src/app/tools/tankopedia/[id]/components/AntagonistBar.tsx b/src/app/tools/tankopedia/[id]/components/AntagonistBar.tsx new file mode 100644 index 000000000..ffe8033f1 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/AntagonistBar.tsx @@ -0,0 +1,274 @@ +import { CaretRightIcon, Cross1Icon, ShuffleIcon } from '@radix-ui/react-icons'; +import { + Button, + Card, + Dialog, + Flex, + Heading, + Tabs, + Text, + Tooltip, +} from '@radix-ui/themes'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { ModuleButton } from '../../../../../components/ModuleButtons/ModuleButton'; +import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton'; +import { SmallTankIcon } from '../../../../../components/SmallTankIcon'; +import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration'; +import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath'; +import { + SHELL_NAMES, + TIER_ROMAN_NUMERALS, +} from '../../../../../core/blitzkrieg/tankDefinitions'; +import { mutateDuel, useDuel } from '../../../../../stores/duel'; +import { + mutateTankopediaTemporary, + useTankopediaPersistent, +} from '../../../../../stores/tankopedia'; +import { TankSearch } from '../../components/TankSearch'; + +interface AntagonistBarProps { + floating?: boolean; +} + +export function AntagonistBar({ floating }: AntagonistBarProps) { + const antagonist = useDuel((state) => state.antagonist!); + const mode = useTankopediaPersistent((state) => state.mode); + const [tab, setTab] = useState('search'); + const [antagonistSelectorOpen, setAntagonistSelectorVisible] = + useState(false); + const router = useRouter(); + + if (mode !== 'armor') return null; + + return ( + + + + {antagonist.gun.shells.map((shell, index) => { + return ( + { + mutateDuel((draft) => { + draft.antagonist!.shell = shell; + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + }); + }} + /> + ); + })} + + + + {!floating && Versus} + + {floating && ( + + Versus {antagonist.tank.name} + + + )} + + {!floating && ( + + + + + + + + + + + + + + Search + Configure + + + + + { + mutateDuel((draft) => { + draft.antagonist!.tank = tank; + draft.antagonist!.engine = tank.engines.at(-1)!; + draft.antagonist!.track = tank.tracks.at(-1)!; + draft.antagonist!.turret = tank.turrets.at(-1)!; + draft.antagonist!.gun = + draft.antagonist!.turret.guns.at(-1)!; + draft.antagonist!.shell = + draft.antagonist!.gun.shells[0]; + }); + setAntagonistSelectorVisible(false); + }} + /> + + + + + + + + {antagonist.tank.name} modules + + + + + {antagonist.tank.turrets.map((turret, index) => ( + + { + mutateDuel((draft) => { + draft.antagonist!.turret = turret; + draft.antagonist!.gun = + turret.guns.at(-1)!; + draft.antagonist!.shell = + draft.antagonist!.gun.shells[0]; + }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + }} + selected={ + antagonist.turret.id === turret.id + } + discriminator={ + TIER_ROMAN_NUMERALS[turret.tier] + } + module="turret" + /> + + ))} + + + {antagonist.turret.guns.map((gun, index) => ( + + { + mutateDuel((draft) => { + draft.antagonist!.gun = gun; + draft.antagonist!.shell = gun.shells[0]; + }); + }} + selected={antagonist.gun.id === gun.id} + discriminator={ + TIER_ROMAN_NUMERALS[gun.tier] + } + module="gun" + /> + + ))} + + + + + + Properties + +
    +
  • + Turret: {antagonist.turret.name} +
  • +
  • + Gun: {antagonist.gun.name} +
  • +
  • Shells:
  • +
      + {antagonist.gun.shells.map((shell) => ( +
    • + {SHELL_NAMES[shell.type]}:{' '} + + {resolveNearPenetration(shell.penetration)} + mm + + , {shell.damage.armor}HP +
    • + ))} +
    +
+
+
+
+
+
+
+
+ )} +
+ + +
+
+ ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/ConfigurationChildWrapper.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/ConfigurationChildWrapper.tsx new file mode 100644 index 000000000..1276c1a02 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/ConfigurationChildWrapper.tsx @@ -0,0 +1,12 @@ +import { ReactNode } from 'react'; +import * as styles from '../../../../page.css'; + +interface ConfigurationChildWrapperProps { + children: ReactNode; +} + +export function ConfigurationChildWrapper({ + children, +}: ConfigurationChildWrapperProps) { + return
{children}
; +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Consumables.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Consumables.tsx new file mode 100644 index 000000000..4de56ac48 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Consumables.tsx @@ -0,0 +1,75 @@ +import { Flex, Heading } from '@radix-ui/themes'; +import { use } from 'react'; +import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton'; +import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity'; +import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions'; +import { useEquipment } from '../../../../../../../hooks/useEquipment'; +import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; +import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; + +export function Consumables() { + const protagonist = useDuel((state) => state.protagonist!); + const awaitedConsumableDefinitions = use(consumableDefinitions); + const consumables = useDuel((state) => state.protagonist!.consumables); + const consumablesList = Object.values(awaitedConsumableDefinitions).filter( + (consumable) => + checkConsumableProvisionInclusivity( + consumable, + protagonist.tank, + protagonist.gun, + ), + ); + const cooldownBooster = useDuel( + (state) => state.protagonist!.cooldownBooster, + ); + const hasConsumableDeliverySystem = useEquipment(118); + const hasHighEndConsumables = useEquipment(101); + + return ( + + Consumables + + + {consumablesList.map((consumable, index) => { + const selected = consumables.includes(consumable.id); + + return ( + { + mutateDuel((draft) => { + if (selected) { + draft.protagonist!.consumables = + draft.protagonist!.consumables.filter( + (id) => id !== consumable.id, + ); + } else { + draft.protagonist!.consumables.push(consumable.id); + } + }); + }} + /> + ); + })} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Equipments.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Equipments.tsx new file mode 100644 index 000000000..7bf344a3e --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Equipments.tsx @@ -0,0 +1,62 @@ +import { Flex, Heading } from '@radix-ui/themes'; +import { use } from 'react'; +import { EquipmentButton } from '../../../../../../../components/ModuleButtons/EquipmentButton'; +import { equipmentDefinitions } from '../../../../../../../core/blitzkrieg/equipmentDefinitions'; +import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; +import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; + +export function Equipments() { + const protagonist = useDuel((state) => state.protagonist!); + const awaitedEquipmentDefinitions = use(equipmentDefinitions); + const equipmentRows = + awaitedEquipmentDefinitions.presets[protagonist.tank.equipment]; + const equipmentMatrix = useDuel((state) => state.protagonist!.equipment); + + return ( + + Equipments + + + {equipmentRows.map((equipmentRow, rowIndex) => ( + + {equipmentRow.map((equipment, columnIndex) => ( + + { + mutateDuel((draft) => { + if (equipmentMatrix[rowIndex][columnIndex] === -1) { + draft.protagonist!.equipment[rowIndex][columnIndex] = 0; + } else { + draft.protagonist!.equipment[rowIndex][columnIndex] = + -1; + } + }); + }} + /> + { + mutateDuel((draft) => { + if (equipmentMatrix[rowIndex][columnIndex] === 1) { + draft.protagonist!.equipment[rowIndex][columnIndex] = 0; + } else { + draft.protagonist!.equipment[rowIndex][columnIndex] = 1; + } + }); + }} + /> + + ))} + + ))} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Info.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Info.tsx new file mode 100644 index 000000000..10e294607 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Info.tsx @@ -0,0 +1,95 @@ +import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons'; +import { Flex, Text } from '@radix-ui/themes'; +import { ReactNode } from 'react'; +import { theme } from '../../../../../../../stitches.config'; + +export interface InfoProps { + name: ReactNode; + children?: ReactNode; + unit?: string; + indent?: boolean; + highlight?: boolean; + delta?: number; + decimals?: number; + prefix?: string; + deltaType?: 'higherIsBetter' | 'lowerIsBetter'; +} + +export function Info({ + name, + children, + unit, + indent = false, + highlight, + delta, + decimals, + prefix, + deltaType = 'higherIsBetter', +}: InfoProps) { + return ( + + + {name} + {unit != undefined && ( + <> + {' '} + + {unit} + + + )} + + + {children !== undefined && ( +
+ )} + + {children !== undefined && ( + + {delta !== undefined && delta !== 0 && ( + <> + 0 + ? 'green' + : 'tomato' + } + > + {decimals !== undefined + ? Math.abs(delta).toFixed(decimals) + : Math.abs(delta)} + + 0 + ? 'green' + : 'tomato' + } + > + {delta > 0 ? : } + + + )} + + {prefix} + {decimals !== undefined && typeof children === 'number' + ? children.toFixed(decimals) + : children} + + + )} + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/InfoWithDelta.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/InfoWithDelta.tsx new file mode 100644 index 000000000..9ac8fe560 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/InfoWithDelta.tsx @@ -0,0 +1,16 @@ +import { useDelta } from '../../../../../../../hooks/useDelta'; +import { Info, InfoProps } from './Info'; + +interface InfoWithDeltaProps extends InfoProps { + children: number; +} + +export function InfoWithDelta({ children, ...props }: InfoWithDeltaProps) { + const delta = useDelta(children); + + return ( + + {children} + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx new file mode 100644 index 000000000..39e86d24e --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx @@ -0,0 +1,138 @@ +import { Flex, Heading, Slider, TextField } from '@radix-ui/themes'; +import { clamp, debounce } from 'lodash'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { GenericTankComponentButton } from '../../../../../../../components/ModuleButtons/GenericTankComponentButton'; +import { asset } from '../../../../../../../core/blitzkrieg/asset'; +import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; +import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; + +export function Miscellaneous() { + const camouflage = useDuel((state) => state.protagonist!.camouflage); + const cooldownBooster = useDuel( + (state) => state.protagonist!.cooldownBooster, + ); + const crewMastery = useDuel((state) => state.protagonist!.crewMastery); + const crewInput = useRef(null); + const [crewMasteryDraft, setCrewMasteryDraft] = useState(crewMastery); + const { tank } = useDuel((state) => state.protagonist!); + + const debouncedApplyDraft = useMemo( + () => + debounce((value) => { + mutateDuel((draft) => { + draft.protagonist!.crewMastery = value; + }); + }, 500), + [], + ); + + useEffect(() => { + setCrewMasteryDraft(crewMastery); + if (!crewInput.current) return; + crewInput.current.value = `${Math.round(crewMastery * 100)}`; + }, [crewMastery]); + + return ( + + Miscellaneous + + + {!tank.fixedCamouflage && ( + { + mutateDuel((draft) => { + draft.protagonist!.camouflage = !camouflage; + }); + }} + /> + )} + 0} + first + last + banner={ + cooldownBooster === 0 + ? undefined + : cooldownBooster === 1 + ? '#afb0abc0' + : cooldownBooster === 2 + ? '#5f72cbc0' + : '#9b3cc0c0' + } + iconStyles={{ + transform: 'translate(-50%, -50%) scale(0.8)', + }} + onClick={() => { + mutateDuel((draft) => { + draft.protagonist!.cooldownBooster++; + if (draft.protagonist!.cooldownBooster === 4) + draft.protagonist!.cooldownBooster = 0; + }); + }} + /> + + {tank.treeType === 'researchable' && ( + + + Crew + { + const newValue = Number(crewInput.current!.value) / 100; + + if (isNaN(newValue)) { + crewInput.current!.value = `${Math.round(crewMastery * 100)}`; + return; + } + + mutateDuel((draft) => { + draft.protagonist!.crewMastery = clamp( + newValue / 100, + 0.5, + 1, + ); + }); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + crewInput.current?.blur(); + } + }} + /> + % + + + { + setCrewMasteryDraft(value / 100); + debouncedApplyDraft(value / 100); + }} + /> + + )} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Modules.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Modules.tsx new file mode 100644 index 000000000..fc258968c --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Modules.tsx @@ -0,0 +1,108 @@ +import { Flex, Heading } from '@radix-ui/themes'; +import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton'; +import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions'; +import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; +import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia'; +import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; + +export function Modules() { + const protagonist = useDuel((state) => state.protagonist!); + + return ( + + Modules + + + + {protagonist.tank.turrets.map((turret, index) => { + return ( + { + mutateDuel((draft) => { + draft.protagonist!.turret = turret; + draft.protagonist!.gun = turret.guns.at(-1)!; + draft.protagonist!.shell = turret.guns.at(-1)!.shells[0]; + }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + }} + /> + ); + })} + + + + {protagonist.turret.guns.map((gun, index) => { + return ( + { + mutateDuel((draft) => { + draft.protagonist!.gun = gun; + draft.protagonist!.shell = gun.shells[0]; + }); + }} + /> + ); + })} + + + + {protagonist.tank.engines.map((engine, index) => { + return ( + { + mutateDuel((draft) => { + draft.protagonist!.engine = engine; + }); + }} + /> + ); + })} + + + + {protagonist.tank.tracks.map((track, index) => { + return ( + { + mutateDuel((draft) => { + draft.protagonist!.track = track; + }); + }} + /> + ); + })} + + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Provisions.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Provisions.tsx new file mode 100644 index 000000000..bca0d16c8 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Provisions.tsx @@ -0,0 +1,54 @@ +import { Flex, Heading } from '@radix-ui/themes'; +import { use } from 'react'; +import { ProvisionButton } from '../../../../../../../components/ModuleButtons/ProvisionButton'; +import { availableProvisions } from '../../../../../../../core/blitzkrieg/availableProvisions'; +import { provisionDefinitions } from '../../../../../../../core/blitzkrieg/provisionDefinitions'; +import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; +import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; + +export function Provisions() { + const { tank, gun } = useDuel((state) => state.protagonist!); + const provisions = useDuel((state) => state.protagonist!.provisions); + const awaitedProvisionDefinitions = use(provisionDefinitions); + const provisionsList = availableProvisions( + tank, + gun, + awaitedProvisionDefinitions, + ); + + return ( + + Provisions + + + {provisionsList.map((provision, index) => { + const selected = provisions.includes(provision.id); + + return ( + { + mutateDuel((draft) => { + if (selected) { + draft.protagonist!.provisions = + draft.protagonist!.provisions.filter( + (id) => id !== provision.id, + ); + } else { + draft.protagonist!.provisions.push(provision.id); + } + }); + }} + /> + ); + })} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx new file mode 100644 index 000000000..b7d4d3e17 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx @@ -0,0 +1,653 @@ +import { AccessibilityIcon } from '@radix-ui/react-icons'; +import { Flex, Heading, Slider, Text, TextField } from '@radix-ui/themes'; +import { debounce } from 'lodash'; +import { use, useEffect, useRef, useState } from 'react'; +import { lerp } from 'three/src/math/MathUtils'; +import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton'; +import { isExplosive } from '../../../../../../core/blitz/isExplosive'; +import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; +import { coefficient } from '../../../../../../core/blitzkrieg/coefficient'; +import { degressiveStat } from '../../../../../../core/blitzkrieg/degressiveStat'; +import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; +import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox'; +import { progressiveStat } from '../../../../../../core/blitzkrieg/progressiveStat'; +import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm'; +import { sum } from '../../../../../../core/blitzkrieg/sum'; +import { + CREW_MEMBER_NAMES, + GUN_TYPE_NAMES, + SHELL_NAMES, +} from '../../../../../../core/blitzkrieg/tankDefinitions'; +import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox'; +import { useConsumable } from '../../../../../../hooks/useConsumable'; +import { useEquipment } from '../../../../../../hooks/useEquipment'; +import { useProvision } from '../../../../../../hooks/useProvision'; +import { useProvisions } from '../../../../../../hooks/useProvisions'; +import { mutateDuel, useDuel } from '../../../../../../stores/duel'; +import { Info } from './components/Info'; +import { InfoWithDelta } from './components/InfoWithDelta'; + +export function Characteristics() { + const awaitedModelDefinitions = use(modelDefinitions); + const { tank, turret, gun, engine, track, shell } = useDuel( + (state) => state.protagonist!, + ); + const hasImprovedVentilation = useEquipment(102); + const provisions = useProvisions(); + const crewMastery = useDuel((state) => state.protagonist!.crewMastery); + const provisionCrewBonus = + provisions.reduce( + (total, provision) => + provision.crew ? total + provision.crew / 100 : total, + 0, + ) + (hasImprovedVentilation ? 0.05 : 0); + const commanderMastery = crewMastery + provisionCrewBonus; + const crewMemberMastery = commanderMastery * 1.1; + const stockEngine = tank.engines[0]; + const stockTrack = tank.tracks[0]; + const stockTurret = tank.turrets[0]; + const stockGun = stockTurret.guns[0]; + const tankModelDefinition = awaitedModelDefinitions[tank.id]; + const turretModelDefinition = tankModelDefinition.turrets[turret.id]; + const gunModelDefinition = turretModelDefinition.guns[gun.id]; + const size = normalizeBoundingBox( + unionBoundingBox( + tankModelDefinition.boundingBox, + turretModelDefinition.boundingBox, + ), + ); + const weight = + tank.weight + engine.weight + track.weight + turret.weight + gun.weight; + const weightTons = weight / 1000; + const stockWeight = + tank.weight + + stockEngine.weight + + stockTrack.weight + + stockTurret.weight + + stockGun.weight; + const [penetrationDistance, setPenetrationDistance] = useState(250); + const camouflage = useDuel((state) => state.protagonist!.camouflage); + const hasRammer = useEquipment(100); + const hasCalibratedShells = useEquipment(103); + const hasEnhancedGunLayingDrive = useEquipment(104); + const hasSupercharger = useEquipment(107); + const hasVerticalStabilizer = useEquipment(105); + const hasRefinedGun = useEquipment(106); + const hasImprovedAssembly = useEquipment(111); + const hasImprovedOptics = useEquipment(114); + const hasImprovedControl = useEquipment(116); + const hasEngineAccelerator = useEquipment(117); + const hasCamouflageNet = useEquipment(115); + const hasImprovedVerticalStabilizer = useEquipment(122); + const hasImprovedSuspension = useEquipment(123); + const penetrationDistanceInput = useRef(null); + const hasEnginePowerBoost = useConsumable(14); + const hasImprovedEnginePowerBoost = useConsumable(27); + const hasAdrenaline = useConsumable(18); + const hasTungsten = useConsumable(45); + const hasReticleCalibration = useConsumable(28); + const hasShellReloadBoost = useConsumable(29); + const hasStandardFuel = useProvision(18); + const hasImprovedFuel = useProvision(19); + const hasProtectiveKit = useProvision(22); + const hasSandbagArmor = useProvision(28); + const hasEnhancedSandbagArmor = useProvision(29); + const hasImprovedGunPowder = useProvision(46); + const hasGearOil = useProvision(44); + const hasImprovedGearOil = useProvision(45); + + const healthCoefficient = coefficient( + [hasSandbagArmor, 0.03], + [hasEnhancedSandbagArmor, 0.06], + [hasImprovedAssembly, 0.06], + ); + const reloadCoefficient = + (coefficient([hasRammer, -0.07]) * + coefficient([true, degressiveStat(crewMemberMastery)])) / + coefficient([hasAdrenaline && gun.type === 'regular', 0.2]); + const penetrationCoefficient = coefficient([ + hasCalibratedShells, + isExplosive(shell.type) ? 0.1 : 0.05, + ]); + const damageCoefficient = coefficient( + // TODO: add max rolls + [hasTungsten, 0.15], + ); + const interClipCoefficient = coefficient([hasShellReloadBoost, -0.3]); + const shellVelocityCoefficient = coefficient( + [hasSupercharger, 0.3], + [hasImprovedGunPowder, 0.3], + ); + const penetrationLossOverDistanceCoefficient = coefficient([ + hasSupercharger, + -0.5, + ]); + const aimTimeCoefficient = + coefficient( + [hasEnhancedGunLayingDrive, -0.1], + [hasReticleCalibration, -0.4], + ) * coefficient([true, degressiveStat(crewMemberMastery)]); + const dispersionStillCoefficient = + coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) * + coefficient([true, degressiveStat(crewMemberMastery)]); + const dispersionMovingCoefficient = coefficient([ + hasVerticalStabilizer, + -0.15, + ]); + const enginePowerCoefficient = coefficient( + [ + hasEngineAccelerator && (tank.type === 'light' || tank.type === 'medium'), + 0.05, + ], + [ + hasEngineAccelerator && + (tank.type === 'heavy' || tank.type === 'tankDestroyer'), + 0.07, + ], + [hasEnginePowerBoost, 0.2], + [hasImprovedEnginePowerBoost, 0.4], + [hasStandardFuel, 0.03], + [hasImprovedFuel, 0.1], + [hasGearOil, 0.03], + [hasImprovedGearOil, 0.06], + ); + const turretTraverseCoefficient = + coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) * + coefficient([true, progressiveStat(crewMemberMastery)]); + const hullTraverseCoefficient = + coefficient( + [hasImprovedControl, 0.1], + [hasImprovedEnginePowerBoost, 0.05], + ) * coefficient([true, progressiveStat(crewMemberMastery)]); + const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]); + const viewRangeCoefficient = + coefficient([ + hasImprovedOptics, + tank.type === 'tankDestroyer' ? 0.05 : tank.type === 'heavy' ? 0.07 : 0.1, + ]) * coefficient([true, progressiveStat(commanderMastery)]); + const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); + + const speedForwardsSum = sum( + [hasImprovedEnginePowerBoost, 5], + [hasGearOil, 2], + [hasImprovedGearOil, 4], + ); + const speedBackwardsSum = sum( + [hasImprovedEnginePowerBoost, 10], + [hasGearOil, 2], + [hasImprovedGearOil, 4], + ); + const camouflageSumMoving = sum( + [ + hasCamouflageNet, + tank.type === 'heavy' + ? 0.03 + : tank.type === 'tankDestroyer' + ? 0.07 + : 0.05, + ], + [ + camouflage, + tank.type === 'tankDestroyer' + ? 0.04 + : tank.type === 'heavy' + ? 0.03 + : 0.02, + ], + ); + const camouflageSumStill = sum( + [ + hasCamouflageNet, + 2 * + (tank.type === 'heavy' + ? 0.03 + : tank.type === 'tankDestroyer' + ? 0.07 + : 0.05), + ], + [ + camouflage, + tank.type === 'tankDestroyer' + ? 0.04 + : tank.type === 'heavy' + ? 0.03 + : 0.02, + ], + ); + + const resolvedEnginePower = engine.power * enginePowerCoefficient; + const dpm = resolveDpm( + gun, + shell, + damageCoefficient, + reloadCoefficient, + interClipCoefficient, + ); + + useEffect(() => { + if (penetrationDistanceInput.current) { + penetrationDistanceInput.current.value = `${penetrationDistance}`; + } + }, [penetrationDistance]); + + return ( + + + + Fire + + + {gun.shells.map((thisShell, index) => { + return ( + { + mutateDuel((draft) => { + draft.protagonist!.shell = thisShell; + }); + }} + /> + ); + })} + + + + {GUN_TYPE_NAMES[gun.type]} + + {dpm} + + {gun.type === 'autoReloader' && ( + <> + + {gun.reload.at(-1)! < gun.reload.at(-2)! + ? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) * + 60 + : (shell.damage.armor / gun.reload[0]) * 60} + + + {gun.reload.at(-1)! < gun.reload.at(-2)! + ? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) * + (60 - + (gun.reload.slice(0, -1).length - 1) * gun.interClip) + + shell.damage.armor * gun.reload.slice(0, -1).length + : (shell.damage.armor / (gun.reload[0] + gun.interClip)) * + (60 - (gun.reload.slice(1).length - 1) * gun.interClip) + + shell.damage.armor * gun.reload.slice(1).length} + + + What's the difference? + + } + /> + + {gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1} + + + )} + {gun.type === 'autoReloader' ? ( + <> + + {gun.reload.map((reload, index) => ( + + {reload} + + ))} + + ) : ( + + {gun.reload * reloadCoefficient} + + )} + {gun.type === 'autoLoader' && ( + + {gun.interClip * interClipCoefficient} + + )} + + {shell.caliber} + + + {resolveNearPenetration(shell.penetration) * penetrationCoefficient} + + {typeof shell.penetration !== 'number' && ( + <> + + {lerp( + shell.penetration[0], + shell.penetration[1], + (penetrationDistance * penetrationLossOverDistanceCoefficient) / + 500, + ) * penetrationCoefficient} + + + Distance + { + setPenetrationDistance(value); + }, 500)} + /> + + { + setPenetrationDistance( + Math.min( + parseInt(penetrationDistanceInput.current!.value), + 500, + ), + ); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.currentTarget.blur(); + } + }} + /> + m + + + + )} + + {shell.damage.armor * damageCoefficient} + + + {shell.damage.module * damageCoefficient} + + + {shell.speed * shellVelocityCoefficient} + + + {gun.aimTime * aimTimeCoefficient} + + + + {gun.dispersion.base * dispersionStillCoefficient} + + + {track.dispersion.move * dispersionMovingCoefficient} + + + {track.dispersion.traverse * dispersionMovingCoefficient} + + + {gun.dispersion.traverse * dispersionMovingCoefficient} + + + {gun.dispersion.shot * dispersionMovingCoefficient} + + + {gun.dispersion.damaged * dispersionMovingCoefficient} + + + + {gunModelDefinition.pitch.max + + (hasImprovedVerticalStabilizer ? 3 : 0)} + + + {-gunModelDefinition.pitch.min + + (hasImprovedVerticalStabilizer ? 3 : 0)} + + {gunModelDefinition.pitch.front && ( + <> + + {gunModelDefinition.pitch.front.max} + + + {-gunModelDefinition.pitch.front.min} + + + )} + {gunModelDefinition.pitch.back && ( + <> + + {gunModelDefinition.pitch.back.max} + + + {-gunModelDefinition.pitch.back.min} + + + )} + {turretModelDefinition.yaw && ( + <> + + {-turretModelDefinition.yaw.min} + + + {turretModelDefinition.yaw.max} + + + )} + + + + Maneuverability + + + {tank.speed.forwards + speedForwardsSum} + + + {tank.speed.backwards + speedBackwardsSum} + + + {resolvedEnginePower} + + + + {resolvedEnginePower / + weightTons / + (track.resistance.hard * resistanceCoefficient)} + + + {resolvedEnginePower / + weightTons / + (track.resistance.medium * resistanceCoefficient)} + + + {resolvedEnginePower / + weightTons / + (track.resistance.soft * resistanceCoefficient)} + + + {weightTons} + + + {turret.traverseSpeed * turretTraverseCoefficient} + + + + {(resolvedEnginePower / stockEngine.power) * + track.traverseSpeed * + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.hard * resistanceCoefficient)) * + (stockWeight / weight)} + + + {(resolvedEnginePower / stockEngine.power) * + track.traverseSpeed * + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.medium * resistanceCoefficient)) * + (stockWeight / weight)} + + + {(resolvedEnginePower / stockEngine.power) * + track.traverseSpeed * + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.soft * resistanceCoefficient)) * + (stockWeight / weight)} + + + + + Survivability + + {(tank.health + turret.health) * healthCoefficient} + + + {Math.round(engine.fireChance * fireChanceCoefficient * 100)} + + + {turret.viewRange * viewRangeCoefficient} + + + + {(tank.camouflage.still + camouflageSumStill) * 100} + + + {(tank.camouflage.moving + camouflageSumMoving) * 100} + + + {(tank.camouflage.still * gun.camouflageLoss + camouflageSumStill) * + 100} + + + {(tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving) * + 100} + + + {(tank.camouflage.onFire * tank.camouflage.still + + camouflageSumStill) * + 100} + + + {size[0].toFixed(2)} x {size[2].toFixed(2)} x {size[1].toFixed(2)} + + + + + Crew training + + {tank.crew.map((member) => { + const count = member.count ?? 1; + + return ( + <> + 1 ? ` x ${count}` : ''}`} + unit="%" + decimals={0} + > + {(member.type === 'commander' + ? commanderMastery + : crewMemberMastery) * 100} + + {member.substitute && ( + + + + {member.substitute + .map((sub, index) => + index === 0 ? CREW_MEMBER_NAMES[sub] : sub, + ) + .join(', ')} + + + } + /> + )} + + ); + })} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Control.tsx b/src/app/tools/tankopedia/[id]/components/Control.tsx new file mode 100644 index 000000000..8cef95e33 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Control.tsx @@ -0,0 +1,165 @@ +import { OrbitControls } from '@react-three/drei'; +import { useThree } from '@react-three/fiber'; +import { useEffect, useRef } from 'react'; +import { Vector3 } from 'three'; +import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; +import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; +import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; +import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose'; +import { useAwait } from '../../../../../hooks/useAwait'; +import { useDuel } from '../../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; + +const poseDistances: Record = { + [Pose.HullDown]: 15, + [Pose.FaceHug]: 5, + [Pose.Default]: -1, +}; + +export function Controls() { + const camera = useThree((state) => state.camera); + const orbitControls = useRef(null); + const awaitedModelDefinitions = useAwait(modelDefinitions); + const protagonist = useDuel((state) => state.protagonist!); + const antagonist = useDuel((state) => state.antagonist!); + const protagonistModelDefinition = + awaitedModelDefinitions[protagonist.tank.id]; + const protagonistTrackModelDefinition = + awaitedModelDefinitions[protagonist.tank.id].tracks[protagonist.track.id]; + const antagonistModelDefinition = awaitedModelDefinitions[antagonist.tank.id]; + const protagonistTurretModelDefinition = + protagonistModelDefinition.turrets[protagonist.turret.id]; + const antagonistTurretModelDefinition = + antagonistModelDefinition.turrets[antagonist.turret.id]; + const protagonistGunModelDefinition = + protagonistTurretModelDefinition.guns[protagonist.gun.id]; + const protagonistHullOrigin = new Vector3( + protagonistTrackModelDefinition.origin[0], + protagonistTrackModelDefinition.origin[1], + -protagonistTrackModelDefinition.origin[2], + ); + const protagonistTurretOrigin = new Vector3( + protagonistModelDefinition.turretOrigin[0], + protagonistModelDefinition.turretOrigin[1], + -protagonistModelDefinition.turretOrigin[2], + ); + const protagonistGunOrigin = new Vector3( + protagonistTurretModelDefinition.gunOrigin[0], + protagonistTurretModelDefinition.gunOrigin[1], + -protagonistTurretModelDefinition.gunOrigin[2], + ); + const antagonistGunHeight = + protagonistTrackModelDefinition.origin[1] + + antagonistModelDefinition.turretOrigin[1] + + antagonistTurretModelDefinition.gunOrigin[1]; + // const hasSetInitialPose = useRef(false); + + useEffect(() => { + camera.position.set(-4, 4, -16); + orbitControls.current?.target.set(0, 1.25, 0); + }, [camera]); + + useEffect(() => { + const unsubscribeTankopedia = useTankopediaPersistent.subscribe( + (state) => state.model.visual.controlsEnabled, + (enabled) => { + if (orbitControls.current) orbitControls.current.enabled = enabled; + }, + ); + + function handlePoseEvent(event: Pose) { + switch (event) { + case Pose.HullDown: { + const [pitch] = applyPitchYawLimits( + -Infinity, + 0, + protagonistGunModelDefinition.pitch, + protagonistTurretModelDefinition.yaw, + ); + + camera.position + .set(0, 0, 0) + .add(protagonistHullOrigin) + .add(protagonistTurretOrigin) + .add(protagonistGunOrigin) + .add( + new Vector3( + 0, + poseDistances[event] * Math.sin(pitch), + poseDistances[event] * -Math.cos(pitch), + ), + ); + camera.lookAt( + protagonistHullOrigin + .clone() + .add(protagonistTurretOrigin) + .add(protagonistGunOrigin), + ); + orbitControls.current?.target.set(0, antagonistGunHeight, 0); + + break; + } + + case Pose.FaceHug: { + const [pitch] = applyPitchYawLimits( + 0, + 0, + protagonistGunModelDefinition.pitch, + protagonistTurretModelDefinition.yaw, + ); + + camera.position + .set(0, 0, 0) + .add(protagonistHullOrigin) + .add(protagonistTurretOrigin) + .add(protagonistGunOrigin) + .add( + new Vector3( + 0, + poseDistances[event] * Math.sin(pitch), + poseDistances[event] * -Math.cos(pitch), + ), + ); + orbitControls.current?.target + .set(0, 0, 0) + .add(protagonistHullOrigin) + .add(protagonistTurretOrigin) + .add(protagonistGunOrigin) + .add( + new Vector3( + 0, + 0.5 * poseDistances[event] * Math.sin(pitch), + 0.5 * poseDistances[event] * -Math.cos(pitch), + ), + ); + + break; + } + + case Pose.Default: { + camera.position.set(-4, 4, -16); + orbitControls.current?.target.set(0, 1.25, 0); + break; + } + } + } + + poseEvent.on(handlePoseEvent); + + return () => { + unsubscribeTankopedia(); + poseEvent.off(handlePoseEvent); + }; + }, [camera, protagonist.tank.id, antagonist.tank.id]); + + return ( + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Lighting.tsx b/src/app/tools/tankopedia/[id]/components/Lighting.tsx new file mode 100644 index 000000000..1cf7f6393 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Lighting.tsx @@ -0,0 +1,41 @@ +import { Environment } from '@react-three/drei'; +import { useModel } from '../../../../../hooks/useModel'; +import { useDuel } from '../../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; + +export const ENVIRONMENTS = [ + 'lobby', + 'apartment', + 'warehouse', + 'dawn', + 'sunset', +] as const; + +export function Lighting() { + const protagonist = useDuel((state) => state.protagonist!); + const { hasPbr } = useModel(protagonist.tank.id); + const showEnvironment = useTankopediaPersistent( + (state) => state.model.visual.showEnvironment, + ); + const environment = useTankopediaPersistent( + (state) => state.model.visual.environment, + ); + + return ( + <> + + + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx new file mode 100644 index 000000000..a8a087792 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -0,0 +1,259 @@ +import { LightningBoltIcon } from '@radix-ui/react-icons'; +import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes'; +import { PerspectiveCamera } from '@react-three/drei'; +import { Canvas } from '@react-three/fiber'; +import { Suspense, use, useEffect, useRef, useState } from 'react'; +import { isMobile } from 'react-device-detect'; +import { ShotDisplay } from '../../../../../../components/ShotDisplay'; +import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; +import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; +import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; +import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose'; +import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon'; +import { useWideFormat } from '../../../../../../hooks/useWideFormat'; +import { mutateDuel, useDuel } from '../../../../../../stores/duel'; +import mutateTankopediaPersistent, { + TankopediaMode, + mutateTankopediaTemporary, + useTankopediaPersistent, +} from '../../../../../../stores/tankopedia'; +import { AntagonistBar } from '../AntagonistBar'; +import { Controls } from '../Control'; +import { Lighting } from '../Lighting'; +import { RotationInputs } from '../QuickInputs'; +import { SceneProps } from '../SceneProps'; +import { ModelLoader } from './components/ModelLoader'; +import { Options } from './components/Options'; +import { TankArmor } from './components/TankArmor'; +import { ExternalModuleMask } from './components/TankArmor/components/ExternalModuleMask'; +import { SpacedArmorDepth } from './components/TankArmor/components/SpacedArmorDepth'; +import { TankModel } from './components/TankModel'; + +export function TankSandbox() { + const canvas = useRef(null); + const awaitedModelDefinitions = use(modelDefinitions); + const canvasWrapper = useRef(null); + const [isFullScreen, setIsFullScreen] = useState(false); + const mode = useTankopediaPersistent((state) => state.mode); + const protagonist = useDuel((state) => state.protagonist!); + const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + const wideFormat = useWideFormat(); + const [loadModel, setLoadModel] = useState(wideFormat || !isMobile); + const duel = useDuel(); + + function handlePointerDown() { + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + function handlePointerMove(event: PointerEvent) { + event.preventDefault(); + } + function handlePointerUp(event: PointerEvent) { + event.preventDefault(); + + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + } + + useEffect(() => { + function handleFullScreenChange() { + setIsFullScreen(document.fullscreenElement !== null); + } + + function handlePoseEvent(pose: Pose) { + switch (pose) { + case Pose.HullDown: { + mutateDuel((draft) => { + const [pitch, yaw] = applyPitchYawLimits( + -Infinity, + 0, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + + modelTransformEvent.emit({ pitch, yaw }); + draft.protagonist!.pitch = pitch; + draft.protagonist!.yaw = yaw; + }); + + break; + } + + case Pose.FaceHug: { + mutateDuel((draft) => { + const [pitch, yaw] = applyPitchYawLimits( + Infinity, + 0, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + + modelTransformEvent.emit({ pitch, yaw }); + draft.protagonist!.pitch = pitch; + draft.protagonist!.yaw = yaw; + }); + + break; + } + + case Pose.Default: + mutateDuel((draft) => { + const [pitch, yaw] = applyPitchYawLimits( + 0, + 0, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + + modelTransformEvent.emit({ pitch, yaw }); + draft.protagonist!.pitch = pitch; + draft.protagonist!.yaw = yaw; + }); + + break; + } + } + + poseEvent.on(handlePoseEvent); + document.addEventListener('fullscreenchange', handleFullScreenChange); + + return () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange); + poseEvent.off(handlePoseEvent); + }; + }); + + useEffect(() => { + mutateDuel((draft) => { + [draft.protagonist!.pitch, draft.protagonist!.yaw] = applyPitchYawLimits( + draft.protagonist!.pitch, + draft.protagonist!.yaw, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + }); + }, [protagonist.gun, protagonist.turret]); + + return ( + + + + + { + mutateTankopediaPersistent((draft) => { + draft.mode = mode as TankopediaMode; + }); + }} + > + + + + Model + + + Armor + + + +
+ {loadModel ? ( +
+ { + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + }} + > + + + + + }> + + + + + + + + +
+ ) : ( + + + + + + + + )} +
+ + + + {isFullScreen && } + + {loadModel && } +
+
+
+
+ ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/ModelLoader.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/ModelLoader.tsx new file mode 100644 index 000000000..1e93ac2ef --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/ModelLoader.tsx @@ -0,0 +1,17 @@ +import { Html, useProgress } from '@react-three/drei'; +import { theme } from '../../../../../../../stitches.config'; +import { Loader } from '../../../../../components/Loader'; + +export function ModelLoader() { + const { progress } = useProgress(); + + return ( + + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx new file mode 100644 index 000000000..9896894d8 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx @@ -0,0 +1,164 @@ +import { + EnterFullScreenIcon, + ExitFullScreenIcon, + EyeOpenIcon, + GearIcon, +} from '@radix-ui/react-icons'; +import { Button, DropdownMenu, Flex } from '@radix-ui/themes'; +import { RefObject } from 'react'; +import { Pose, poseEvent } from '../../../../../../../core/blitzkrieg/pose'; +import mutateTankopediaPersistent, { + useTankopediaPersistent, +} from '../../../../../../../stores/tankopedia'; +import { ENVIRONMENTS } from '../../Lighting'; + +interface OptionsProps { + isFullScreen: boolean; + canvasWrapper: RefObject; +} + +export function Options({ isFullScreen, canvasWrapper }: OptionsProps) { + const showGrid = useTankopediaPersistent( + (state) => state.model.visual.showGrid, + ); + const showEnvironment = useTankopediaPersistent( + (state) => state.model.visual.showEnvironment, + ); + const greenPenetration = useTankopediaPersistent( + (state) => state.model.visual.greenPenetration, + ); + const wireframe = useTankopediaPersistent( + (state) => state.model.visual.wireframe, + ); + const fullScreenAvailable = + typeof document !== 'undefined' && document.fullscreenEnabled; + const environment = useTankopediaPersistent( + (state) => state.model.visual.environment, + ); + + return ( + + {!isFullScreen && ( + + + + + + + poseEvent.emit(Pose.HullDown)}> + Hull down + + + poseEvent.emit(Pose.FaceHug)}> + Face hug + + + poseEvent.emit(Pose.Default)}> + Default + + + + )} + + {!isFullScreen && ( + + + + + + + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.greenPenetration = checked; + }); + }} + > + Green penetration + + + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.showGrid = checked; + }); + }} + > + Show grid + + + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.showEnvironment = checked; + }); + }} + > + View environment + + + + Lighting + + + + {ENVIRONMENTS.map((environment) => ( + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.environment = environment; + }); + }} + > + {environment[0].toUpperCase() + environment.slice(1)} + + ))} + + + + + { + mutateTankopediaPersistent((draft) => { + draft.model.visual.wireframe = checked; + }); + }} + > + Wireframe + + + + )} + + {fullScreenAvailable && ( + + )} + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx new file mode 100644 index 000000000..e547a01fe --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx @@ -0,0 +1,213 @@ +import { useEffect, useRef } from 'react'; +import { Euler, Group, Mesh, Vector3 } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { ArmorMesh } from '../../../../../../../../../components/ArmorMesh'; +import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; +import { + ModelTransformEventData, + modelTransformEvent, +} from '../../../../../../../../../core/blitzkrieg/modelTransform'; +import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId'; +import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; +import { useArmor } from '../../../../../../../../../hooks/useArmor'; +import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; +import { useDuel } from '../../../../../../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; + +export function ArmorHighlighting() { + const protagonist = useDuel((state) => state.protagonist!); + const wrapper = useRef(null); + const modelDefinitions = useModelDefinitions(); + const gunContainer = useRef(null); + const turretContainer = useRef(null); + const initialTankopediaState = useTankopediaPersistent.getState(); + + useEffect(() => { + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretPosition = new Vector3(); + const turretRotation = new Euler(); + const gunPosition = new Vector3(); + const gunRotation = new Euler(); + + function handleModelTransform({ pitch, yaw }: ModelTransformEventData) { + if (yaw === undefined) return; + + gunPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + gunRotation.set(pitch, 0, 0); + gunContainer.current?.position.copy(gunPosition); + gunContainer.current?.rotation.copy(gunRotation); + turretPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + turretRotation.set(0, 0, yaw); + + if (tankModelDefinition.turretRotation) { + const initialPitch = -degToRad( + tankModelDefinition.turretRotation.pitch, + ); + const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw); + const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll); + + turretPosition + .applyAxisAngle(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + const unsubscribe = useTankopediaPersistent.subscribe( + (state) => state.mode, + (mode) => { + if (wrapper.current) wrapper.current.visible = mode === 'armor'; + }, + ); + + handleModelTransform(protagonist); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + unsubscribe(); + }; + }); + + const armorGltf = useArmor(protagonist.tank.id); + const armorNodes = Object.values(armorGltf.nodes); + const tankModelDefinition = modelDefinitions[protagonist.tank.id]; + const trackModelDefinition = tankModelDefinition.tracks[protagonist.track.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const maxThickness = Math.max( + trackModelDefinition.thickness, + gunModelDefinition.thickness, + ...armorNodes + .map((node) => { + const armorId = nameToArmorId(node.name); + return resolveArmor(tankModelDefinition.armor, armorId).thickness; + }) + .filter(Boolean), + ); + + return ( + + {armorNodes.map((node) => { + const isHull = node.name.startsWith('hull_'); + const isVisible = isHull; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + tankModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined || spaced) return null; + + return ( + + ); + })} + + + {armorNodes.map((node) => { + const isCurrentTurret = node.name.startsWith( + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentTurret; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + turretModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined || spaced) return null; + + return ( + + ); + })} + + + {armorNodes.map((node) => { + const isCurrentGun = node.name.startsWith( + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentGun; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + gunModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined || spaced) return null; + + return ( + + ); + })} + + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx new file mode 100644 index 000000000..66858e5bd --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx @@ -0,0 +1,286 @@ +import { memo, useEffect, useRef } from 'react'; +import { Euler, Group, Vector3 } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { ArmorMeshExternalModuleMask } from '../../../../../../../../../components/ArmorMesh'; +import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; +import { + ModelTransformEventData, + modelTransformEvent, +} from '../../../../../../../../../core/blitzkrieg/modelTransform'; +import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId'; +import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; +import { useArmor } from '../../../../../../../../../hooks/useArmor'; +import { useModel } from '../../../../../../../../../hooks/useModel'; +import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; +import { useDuel } from '../../../../../../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; + +interface ExternalModuleMaskProps { + ornamental?: boolean; +} + +/** + * When rendered, generates a mask and thickness buffer for external modules. + * - R: 1 means external module + * - G: normalized thickness; multiply with max thickness to get nominal thickness + * - B: no data + * - A: 1 means is armor; 0 is background + */ +export const ExternalModuleMask = memo( + ({ ornamental = false }) => { + const protagonist = useDuel((state) => state.protagonist!); + const wrapper = useRef(null); + const modelDefinitions = useModelDefinitions(); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const initialTankopediaState = useTankopediaPersistent.getState(); + + useEffect(() => { + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretPosition = new Vector3(); + const turretRotation = new Euler(); + const gunPosition = new Vector3(); + const gunRotation = new Euler(); + + function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { + gunPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + gunRotation.set(pitch, 0, 0); + gunContainer.current?.position.copy(gunPosition); + gunContainer.current?.rotation.copy(gunRotation); + + if (yaw === undefined) return; + + turretPosition + .set(0, 0, 0) + .sub(turretOrigin) + .sub(hullOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + turretRotation.set(0, 0, yaw); + + if (tankModelDefinition.turretRotation) { + const initialPitch = -degToRad( + tankModelDefinition.turretRotation.pitch, + ); + const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw); + const initialRoll = -degToRad( + tankModelDefinition.turretRotation.roll, + ); + + turretPosition + .applyAxisAngle(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + + handleModelTransform(protagonist); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + }; + }); + + useEffect(() => { + const unsubscribe = useTankopediaPersistent.subscribe( + (state) => state.mode, + (mode) => { + if (wrapper.current) wrapper.current.visible = mode === 'armor'; + }, + ); + + return unsubscribe; + }); + + const armorGltf = useArmor(protagonist.tank.id); + const { gltf: modelGltf } = useModel(protagonist.tank.id); + + const armorNodes = Object.values(armorGltf.nodes); + const modelNodes = Object.values(modelGltf.nodes); + const tankModelDefinition = modelDefinitions[protagonist.tank.id]; + const trackModelDefinition = + tankModelDefinition.tracks[protagonist.track.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + const maxThickness = Math.max( + trackModelDefinition.thickness, + gunModelDefinition.thickness, + ...armorNodes + .map((node) => { + const armorId = nameToArmorId(node.name); + return resolveArmor(tankModelDefinition.armor, armorId).thickness; + }) + .filter(Boolean), + ); + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + + return ( + + + {armorNodes.map((node) => { + const isHull = node.name.startsWith('hull_'); + const isVisible = isHull; + const armorId = nameToArmorId(node.name); + const { thickness, spaced } = resolveArmor( + tankModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined || spaced) return null; + + return ( + + ); + })} + + + {modelNodes.map((node) => { + const isWheel = node.name.startsWith('chassis_wheel_'); + const isTrack = node.name.startsWith('chassis_track_'); + const isVisible = isWheel || isTrack; + + if (!isVisible) return null; + + return ( + + ); + })} + + + + {armorNodes.map((node) => { + const isCurrentTurret = node.name.startsWith( + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentTurret; + const armorId = nameToArmorId(node.name); + const { thickness, spaced } = resolveArmor( + turretModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined || spaced) return null; + + return ( + + + + ); + })} + + + + {armorNodes.map((node) => { + const isCurrentGun = node.name.startsWith( + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentGun; + const armorId = nameToArmorId(node.name); + const { thickness, spaced } = resolveArmor( + gunModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined || spaced) return null; + + return ( + + + + ); + })} + + {modelNodes.map((node) => { + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentGun; + + if (!isVisible) return null; + + return ( + + ); + })} + + + + ); + }, +); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx new file mode 100644 index 000000000..a856743ae --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx @@ -0,0 +1,299 @@ +import { memo, useEffect, useRef } from 'react'; +import { Euler, Group, Vector3 } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { ArmorMeshSpacedArmorDepth } from '../../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth'; +import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis'; +import { + ModelTransformEventData, + modelTransformEvent, +} from '../../../../../../../../../core/blitzkrieg/modelTransform'; +import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId'; +import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; +import { useArmor } from '../../../../../../../../../hooks/useArmor'; +import { useModel } from '../../../../../../../../../hooks/useModel'; +import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; +import { useDuel } from '../../../../../../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia'; + +interface SpacedArmorDepthProps { + ornamental?: boolean; +} + +/** + * When rendered, this component provides the depth buffer for the armor which + * when compared to the depth of the core armor will give you the distance + * between the first point of contact and the core armor. The color channels + * hold information about the thickness of the spaced armor (external modules + * not included). + * - R: angle of the armor where 0 is 0 and 1 is 90 + * - G: normalized thickness; multiply with max thickness to get nominal thickness + * - B: no data + * - A: 1 means is armor; 0 is background + */ +export const SpacedArmorDepth = memo( + ({ ornamental = false }) => { + const protagonist = useDuel((state) => state.protagonist!); + const wrapper = useRef(null); + const modelDefinitions = useModelDefinitions(); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const initialTankopediaState = useTankopediaPersistent.getState(); + + useEffect(() => { + if (!modelDefinitions) return; + + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretPosition = new Vector3(); + const turretRotation = new Euler(); + const gunPosition = new Vector3(); + const gunRotation = new Euler(); + + function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { + gunPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + gunRotation.set(pitch, 0, 0); + gunContainer.current?.position.copy(gunPosition); + gunContainer.current?.rotation.copy(gunRotation); + + if (yaw === undefined) return; + + turretPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + turretRotation.set(0, 0, yaw); + + if (tankModelDefinition.turretRotation) { + const initialPitch = -degToRad( + tankModelDefinition.turretRotation.pitch, + ); + const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw); + const initialRoll = -degToRad( + tankModelDefinition.turretRotation.roll, + ); + + turretPosition + .applyAxisAngle(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + + handleModelTransform(protagonist); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + }; + }); + + useEffect(() => { + const unsubscribe = useTankopediaPersistent.subscribe( + (state) => state.mode, + (mode) => { + if (wrapper.current) wrapper.current.visible = mode === 'armor'; + }, + ); + + return unsubscribe; + }); + + const armorGltf = useArmor(protagonist.tank.id); + const { gltf: modelGltf } = useModel(protagonist.tank.id); + + const armorNodes = Object.values(armorGltf.nodes); + const modelNodes = Object.values(modelGltf.nodes); + const tankModelDefinition = modelDefinitions[protagonist.tank.id]; + const trackModelDefinition = + tankModelDefinition.tracks[protagonist.track.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + const maxThickness = Math.max( + trackModelDefinition.thickness, + gunModelDefinition.thickness, + ...armorNodes + .map((node) => { + const armorId = nameToArmorId(node.name); + return resolveArmor(tankModelDefinition.armor, armorId).thickness; + }) + .filter(Boolean), + ); + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + + return ( + + + {armorNodes.map((node) => { + const isHull = node.name.startsWith('hull_'); + const isVisible = isHull; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + tankModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined) return null; + + return ( + + ); + })} + + {modelNodes.map((node) => { + const isWheel = node.name.startsWith('chassis_wheel_'); + const isTrack = node.name.startsWith('chassis_track_'); + const isVisible = isWheel || isTrack; + + if (!isVisible) return null; + + return ( + + ); + })} + + + + {armorNodes.map((node) => { + const isCurrentTurret = node.name.startsWith( + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentTurret; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + turretModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined) return null; + + return ( + + + + ); + })} + + + + {armorNodes.map((node) => { + const isCurrentGun = node.name.startsWith( + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentGun; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + gunModelDefinition.armor, + armorId, + ); + + if (!isVisible || thickness === undefined) return null; + + return ( + + + + ); + })} + + + {modelNodes.map((node) => { + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentGun; + + if (!isVisible) return null; + + return ( + + ); + })} + + + + ); + }, +); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx new file mode 100644 index 000000000..df4cc78d5 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/index.tsx @@ -0,0 +1,67 @@ +import { createPortal, useFrame } from '@react-three/fiber'; +import { memo } from 'react'; +import { DepthTexture, Scene, Vector2 } from 'three'; +import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh'; +import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth'; +import { ArmorHighlighting } from './components/ArmorHighlighting'; +import { ExternalModuleMask } from './components/ExternalModuleMask'; +import { SpacedArmorDepth } from './components/SpacedArmorDepth'; + +export const TankArmor = memo(() => { + const externalModuleMaskScene = new Scene(); + const spacedArmorDepthScene = new Scene(); + const armorHighlightingScene = new Scene(); + const externalModuleMaskPortal = createPortal( + , + externalModuleMaskScene, + ); + const spacedArmorDepthPortal = createPortal( + , + spacedArmorDepthScene, + ); + const armorHighlightingPortal = createPortal( + , + armorHighlightingScene, + ); + const renderSize = new Vector2(); + const newRenderSize = new Vector2(); + + useFrame(({ gl, scene, camera }) => { + const pixelRatio = gl.getPixelRatio(); + gl.autoClear = true; + + gl.getSize(newRenderSize).multiplyScalar(pixelRatio); + if (!newRenderSize.equals(renderSize)) { + renderSize.copy(newRenderSize); + spacedArmorDepthRenderTarget.depthTexture = new DepthTexture( + renderSize.x, + renderSize.y, + ); + } + + externalModuleMaskRenderTarget.setSize(renderSize.x, renderSize.y); + gl.setRenderTarget(externalModuleMaskRenderTarget); + gl.render(externalModuleMaskScene, camera); + + gl.clearDepth(); + spacedArmorDepthRenderTarget.setSize(renderSize.x, renderSize.y * 0.5); + gl.setRenderTarget(spacedArmorDepthRenderTarget); + gl.render(spacedArmorDepthScene, camera); + + gl.autoClear = false; + + gl.setRenderTarget(null); + gl.render(scene, camera); + + gl.clearDepth(); + gl.render(armorHighlightingScene, camera); + }, 1); + + return ( + <> + {externalModuleMaskPortal} + {spacedArmorDepthPortal} + {armorHighlightingPortal} + + ); +}); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx new file mode 100644 index 000000000..472675451 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -0,0 +1,254 @@ +import { ThreeEvent, useThree } from '@react-three/fiber'; +import { memo, useEffect, useRef } from 'react'; +import { Euler, Group, Vector3 } from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis'; +import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits'; +import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree'; +import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions'; +import { + ModelTransformEventData, + modelTransformEvent, +} from '../../../../../../../core/blitzkrieg/modelTransform'; +import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180'; +import { useAwait } from '../../../../../../../hooks/useAwait'; +import { useModel } from '../../../../../../../hooks/useModel'; +import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; +import mutateTankopediaPersistent, { + mutateTankopediaTemporary, +} from '../../../../../../../stores/tankopedia'; + +export const TankModel = memo(() => { + const awaitedModelDefinitions = useAwait(modelDefinitions); + const protagonist = useDuel((draft) => draft.protagonist!); + const canvas = useThree((state) => state.gl.domElement); + const hullContainer = useRef(null); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + + useEffect(() => { + const hullOrigin = new Vector3( + trackModelDefinition.origin[0], + trackModelDefinition.origin[1], + -trackModelDefinition.origin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(I_HAT, Math.PI / 2); + const turretPosition = new Vector3(); + const turretRotation = new Euler(); + const gunPosition = new Vector3(); + const gunRotation = new Euler(); + + function handleModelTransform({ yaw, pitch }: ModelTransformEventData) { + gunPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(I_HAT, pitch) + .add(gunOrigin) + .add(turretOrigin) + .add(hullOrigin); + gunRotation.set(pitch, 0, 0); + gunContainer.current?.position.copy(gunPosition); + gunContainer.current?.rotation.copy(gunRotation); + + if (yaw === undefined) return; + + turretPosition + .set(0, 0, 0) + .sub(hullOrigin) + .sub(turretOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + turretRotation.set(0, 0, yaw); + + if (tankModelDefinition.turretRotation) { + const initialPitch = -degToRad( + tankModelDefinition.turretRotation.pitch, + ); + const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw); + const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll); + + turretPosition + .applyAxisAngle(I_HAT, initialPitch) + .applyAxisAngle(J_HAT, initialRoll) + .applyAxisAngle(K_HAT, initialYaw); + turretRotation.x += initialPitch; + turretRotation.y += initialRoll; + turretRotation.z += initialYaw; + } + + turretPosition.add(turretOrigin).add(hullOrigin); + turretContainer.current?.position.copy(turretPosition); + turretContainer.current?.rotation.copy(turretRotation); + } + + handleModelTransform(protagonist); + modelTransformEvent.on(handleModelTransform); + + return () => { + modelTransformEvent.off(handleModelTransform); + }; + }); + + if (!awaitedModelDefinitions) return; + + const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; + const trackModelDefinition = tankModelDefinition.tracks[protagonist.track.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + + const { gltf } = useModel(protagonist.tank.id); + const nodes = Object.values(gltf.nodes); + + return ( + + {nodes.map((node) => { + const isHull = node.name === 'hull'; + const isWheel = node.name.startsWith('chassis_wheel_'); + const isTrack = node.name.startsWith('chassis_track_'); + const isVisible = isHull || isWheel || isTrack; + + if (!isVisible) return null; + + function onPointerDown(event: ThreeEvent) { + event.stopPropagation(); + } + + return jsxTree(node, { + castShadow: true, + receiveShadow: true, + onPointerDown, + }); + })} + + + {nodes.map((node) => { + const isTurret = node.name.startsWith('turret_'); + const isCurrentTurret = + node.name === + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentTurret; + + if (!isVisible) return null; + + let pitch = 0; + let yaw = 0; + + function onPointerDown(event: ThreeEvent) { + event.stopPropagation(); + + if (isTurret) { + yaw = protagonist.yaw; + pitch = protagonist.pitch; + + mutateTankopediaPersistent((draft) => { + draft.model.visual.controlsEnabled = false; + }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + } + function handlePointerMove(event: PointerEvent) { + [pitch, yaw] = applyPitchYawLimits( + pitch, + yaw + event.movementX * (Math.PI / canvas.width), + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + modelTransformEvent.emit({ pitch, yaw }); + } + function handlePointerUp() { + mutateDuel((draft) => { + draft.protagonist!.pitch = normalizeAnglePI(pitch); + draft.protagonist!.yaw = normalizeAnglePI(yaw); + }); + mutateTankopediaPersistent((draft) => { + draft.model.visual.controlsEnabled = true; + }); + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + } + + return jsxTree(node, { + castShadow: true, + receiveShadow: true, + onPointerDown, + }); + })} + + + {nodes.map((node) => { + const isCurrentMantlet = + node.name === + `gun_${gunModelDefinition.model + .toString() + .padStart(2, '0')}_mask`; + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentGun || isCurrentMantlet; + + if (!isVisible) return null; + + let pitch = 0; + let yaw = 0; + + function onPointerDown(event: ThreeEvent) { + event.stopPropagation(); + + mutateTankopediaPersistent((draft) => { + draft.model.visual.controlsEnabled = false; + }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + pitch = protagonist.pitch; + yaw = protagonist.yaw; + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + function handlePointerMove(event: PointerEvent) { + [pitch, yaw] = applyPitchYawLimits( + pitch - event.movementY * (Math.PI / canvas.height), + yaw + event.movementX * (Math.PI / canvas.width), + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + modelTransformEvent.emit({ pitch, yaw }); + } + function handlePointerUp() { + mutateTankopediaPersistent((draft) => { + draft.model.visual.controlsEnabled = true; + }); + mutateDuel((draft) => { + draft.protagonist!.pitch = normalizeAnglePI(pitch); + draft.protagonist!.yaw = normalizeAnglePI(yaw); + }); + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + } + + return jsxTree(node, { + castShadow: true, + receiveShadow: true, + onPointerDown, + }); + })} + + + + ); +}); diff --git a/src/app/tools/tankopedia/[id]/components/QuickInputs/components/QuickEquipmentButton.tsx b/src/app/tools/tankopedia/[id]/components/QuickInputs/components/QuickEquipmentButton.tsx new file mode 100644 index 000000000..7c6e1596b --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/QuickInputs/components/QuickEquipmentButton.tsx @@ -0,0 +1,25 @@ +import { Button } from '@radix-ui/themes'; +import { ComponentProps } from 'react'; +import { asset } from '../../../../../../../core/blitzkrieg/asset'; + +interface QuickEquipmentButtonProps extends ComponentProps { + equipment: number; + active: boolean; +} + +export function QuickEquipmentButton({ + equipment, + active, + ...props +}: QuickEquipmentButtonProps) { + return ( + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/QuickInputs/index.tsx b/src/app/tools/tankopedia/[id]/components/QuickInputs/index.tsx new file mode 100644 index 000000000..9001f3a6f --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/QuickInputs/index.tsx @@ -0,0 +1,179 @@ +import { Flex, TextField } from '@radix-ui/themes'; +import { use, useEffect, useRef } from 'react'; +import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; +import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; +import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; +import { useEquipment } from '../../../../../../hooks/useEquipment'; +import { mutateDuel, useDuel } from '../../../../../../stores/duel'; +import { useTankopediaPersistent } from '../../../../../../stores/tankopedia'; +import { QuickEquipmentButton } from './components/QuickEquipmentButton'; + +interface RotationInputsProps { + isFullScreen?: boolean; +} + +export function RotationInputs({ isFullScreen }: RotationInputsProps) { + const protagonist = useDuel((state) => state.protagonist!); + const awaitedModelDefinitions = use(modelDefinitions); + const turretYawInput = useRef(null); + const gunPitchInput = useRef(null); + const mode = useTankopediaPersistent((state) => state.mode); + const hasCalibratedShells = useEquipment(103, true); + const hasEnhancedArmor = useEquipment(110); + + useEffect(() => { + turretYawInput.current!.value = `${-Math.round( + protagonist.yaw * (180 / Math.PI), + )}`; + }, [protagonist.yaw]); + useEffect(() => { + gunPitchInput.current!.value = `${-Math.round( + protagonist.pitch * (180 / Math.PI), + )}`; + }, [protagonist.pitch]); + + const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + + return ( + + + + + Yaw + + { + const [pitch, yaw] = applyPitchYawLimits( + protagonist.pitch, + -Number(turretYawInput.current?.value) * (Math.PI / 180), + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + modelTransformEvent.emit({ pitch, yaw }); + mutateDuel((state) => { + state.protagonist!.pitch = pitch; + state.protagonist!.yaw = yaw; + }); + turretYawInput.current!.value = `${Math.round( + yaw * (180 / Math.PI), + )}`; + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + turretYawInput.current?.blur(); + } + }} + onFocus={() => turretYawInput.current?.focus()} + ref={turretYawInput} + style={{ textAlign: 'right' }} + /> + + ° + + + + + + Pitch + + { + const [pitch, yaw] = applyPitchYawLimits( + -Number(gunPitchInput.current?.value) * (Math.PI / 180), + protagonist.yaw, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + modelTransformEvent.emit({ pitch, yaw }); + mutateDuel((state) => { + state.protagonist!.pitch = pitch; + state.protagonist!.yaw = yaw; + }); + gunPitchInput.current!.value = `${Math.round( + pitch * (180 / Math.PI), + )}`; + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + gunPitchInput.current?.blur(); + } + }} + onFocus={() => gunPitchInput.current?.focus()} + ref={gunPitchInput} + style={{ textAlign: 'right' }} + /> + + ° + + + + + {mode === 'armor' && ( + + { + mutateDuel((draft) => { + draft.antagonist!.equipment[0][0] = hasCalibratedShells ? 0 : 1; + }); + }} + /> + { + mutateDuel((draft) => { + draft.protagonist!.equipment[1][1] = hasEnhancedArmor ? 0 : -1; + }); + }} + /> + + )} + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/SceneProps.tsx b/src/app/tools/tankopedia/[id]/components/SceneProps.tsx new file mode 100644 index 000000000..2be3155b5 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/SceneProps.tsx @@ -0,0 +1,32 @@ +import { Color } from 'three'; +import InfiniteGridHelper from '../../../../../components/InfiniteGridHelper'; +import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; + +export function SceneProps() { + const showGrid = useTankopediaPersistent( + (state) => state.model.visual.showGrid, + ); + const showEnvironment = useTankopediaPersistent( + (state) => state.model.visual.showEnvironment, + ); + + if (!showGrid || showEnvironment) return null; + + return ( + <> + + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Title.tsx b/src/app/tools/tankopedia/[id]/components/Title.tsx new file mode 100644 index 000000000..24a8febde --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Title.tsx @@ -0,0 +1,79 @@ +import { UpdateIcon } from '@radix-ui/react-icons'; +import { Button, Dialog, Flex, Heading } from '@radix-ui/themes'; +import { useState } from 'react'; +import { TREE_TYPE_ICONS } from '../../../../../components/Tanks'; +import { assignDuelMember } from '../../../../../core/blitzkrieg/assignDuelMember'; +import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath'; +import { useDuel } from '../../../../../stores/duel'; +import { TankSearch } from '../../components/TankSearch'; + +export function Title() { + const protagonist = useDuel((state) => state.protagonist!); + const [changeTankDialogOpen, setChangeTankDialogOpen] = useState(false); + + return ( + <> + {protagonist.tank.name} + + + + + + {' '} + {protagonist.tank.name} + + + + + + + + + + + + + { + assignDuelMember('protagonist', tank.id); + setChangeTankDialogOpen(false); + pushTankopediaPath(tank.id); + }} + /> + + + + + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index ebbedfb6a..0251a97c6 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -1,250 +1,109 @@ 'use client'; -import { ArrowLeftIcon, CaretRightIcon } from '@radix-ui/react-icons'; -import { - Button, - Card, - Checkbox, - Dialog, - Flex, - Heading, - Slider, - Tabs, - Text, - Theme, - Tooltip, -} from '@radix-ui/themes'; -import { OrbitControls } from '@react-three/drei'; -import { Canvas } from '@react-three/fiber'; -import Link from 'next/link'; -import { use, useEffect, useState } from 'react'; -import { Flag } from '../../../../components/Flag'; +import { Flex } from '@radix-ui/themes'; +import { useEffect } from 'react'; import PageWrapper from '../../../../components/PageWrapper'; -import { asset } from '../../../../core/blitzkrieg/asset'; -import { gunDefinitions } from '../../../../core/blitzkrieg/definitions/guns'; -import { shellDefinitions } from '../../../../core/blitzkrieg/definitions/shells'; -import { - TIER_ROMAN_NUMERALS, - tankDefinitions, - tanksDefinitionsArray, -} from '../../../../core/blitzkrieg/definitions/tanks'; -import { turretDefinitions } from '../../../../core/blitzkrieg/definitions/turrets'; -import * as styles from '../page.css'; - -type Mode = 'model' | 'armor'; +import { assignDuelMember } from '../../../../core/blitzkrieg/assignDuelMember'; +import { useWideFormat } from '../../../../hooks/useWideFormat'; +import { mutateDuel, useDuel } from '../../../../stores/duel'; +import { mutateTankopediaTemporary } from '../../../../stores/tankopedia'; +import { AntagonistBar } from './components/AntagonistBar'; +import { Characteristics } from './components/Characteristics'; +import { Consumables } from './components/Characteristics/components/Consumables'; +import { Equipments } from './components/Characteristics/components/Equipments'; +import { Miscellaneous } from './components/Characteristics/components/Miscellaneous'; +import { Modules } from './components/Characteristics/components/Modules'; +import { Provisions } from './components/Characteristics/components/Provisions'; +import { TankSandbox } from './components/Model/TankSandbox'; +import { Title } from './components/Title'; export default function Page({ params }: { params: { id: string } }) { - const id = parseInt(params.id); - const awaitedTankDefinitions = use(tankDefinitions); - const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); - const awaitedTurretDefinitions = use(turretDefinitions); - const awaitedGunDefinitions = use(gunDefinitions); - const awaitedShellDefinitions = use(shellDefinitions); - const tank = awaitedTankDefinitions[id]; - const [turret, setTurret] = useState( - awaitedTurretDefinitions[tank.turrets[0]], - ); - const [gun, setGun] = useState(awaitedGunDefinitions[turret.guns[0]]); - const [crew, setCrew] = useState(100); - const [versusTank, setVersusTank] = useState( - awaitedTanksDefinitionsArray[0].id, - ); - const [mode, setMode] = useState('model'); + const initialId = parseInt(params.id); + const assigned = useDuel((state) => state.assigned); + const wideFormat = useWideFormat(); - console.log(versusTank); + useEffect(() => { + assignDuelMember('both', initialId); - useEffect(() => setGun(awaitedGunDefinitions[turret.guns[0]]), [turret]); + function handleKeyDown(event: KeyboardEvent) { + if (document.activeElement instanceof HTMLInputElement) return; + + function wipeShot() { + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + } + + if (event.key === '1') { + wipeShot(); + mutateDuel((draft) => { + draft.antagonist!.shell = draft.antagonist!.gun.shells[0]; + }); + } else if (event.key === '2') { + wipeShot(); + mutateDuel((draft) => { + if (draft.antagonist!.gun.shells[1]) { + draft.antagonist!.shell = draft.antagonist!.gun.shells[1]; + } + }); + } else if (event.key === '3') { + wipeShot(); + mutateDuel((draft) => { + if (draft.antagonist!.gun.shells[2]) { + draft.antagonist!.shell = draft.antagonist!.gun.shells[2]; + } + }); + } + } + + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [initialId, assigned]); return ( - - - - - + {assigned && ( + <> + + + <Flex + style={{ width: '100%' }} + gap="8" + direction={wideFormat ? 'row' : 'column'} + align="start" + justify="center" + > + <Flex + gap="4" + direction="column" + style={{ + flex: 1, + width: '100%', + top: 64, + position: wideFormat ? 'sticky' : undefined, + }} > - <Flex gap="1" align="center"> - <ArrowLeftIcon /> - <Text>Back to tankopedia</Text> - </Flex> - </Link> + <TankSandbox /> + <AntagonistBar /> - <Flex gap="2" align="center"> - <Flag nation={tank.nation} /> - <Heading>{tank.name}</Heading> + <Flex direction="column" gap="4"> + <Modules /> + <Provisions /> + <Equipments /> + <Miscellaneous /> + <Consumables /> + </Flex> + </Flex> + + <Flex style={{ width: wideFormat ? 320 : '100%' }}> + <Characteristics /> </Flex> </Flex> - - <Card style={{ position: 'relative' }}> - <Flex direction="column" gap="2"> - <Tabs.Root - value={mode} - onValueChange={(value) => setMode(value as Mode)} - > - <Tabs.List> - <Tabs.Trigger value="model">Model</Tabs.Trigger> - <Tabs.Trigger value="armor">Armor</Tabs.Trigger> - </Tabs.List> - </Tabs.Root> - - <div style={{ height: '50vh', maxHeight: 576 }}> - <Canvas onPointerDown={(event) => event.preventDefault()}> - <OrbitControls /> - - <ambientLight /> - <directionalLight /> - - <mesh> - <torusKnotGeometry args={[undefined, undefined, 128, 16]} /> - <meshStandardMaterial color="cyan" /> - </mesh> - </Canvas> - </div> - - {mode === 'armor' && ( - <Flex gap="2" align="center" className={styles.enhancedArmor}> - <Checkbox defaultChecked /> - <Text>Enhanced armor</Text> - </Flex> - )} - </Flex> - </Card> - - {mode === 'armor' && ( - <Card> - <Flex align="center" justify="between" gap="2"> - <Flex align="center" gap="4"> - <Text>Versus</Text> - - <Dialog.Root> - <Dialog.Trigger> - <Button variant="ghost"> - {awaitedTankDefinitions[versusTank].name} - <CaretRightIcon /> - </Button> - </Dialog.Trigger> - - <Dialog.Content>Do something lol</Dialog.Content> - </Dialog.Root> - </Flex> - - <Flex gap="2" align="center"> - <Checkbox defaultChecked /> - <Text>Calibrated shells</Text> - </Flex> - - <Flex gap="1"> - <Button variant="solid" radius="small"> - <img src={asset('shells/ap.webp')} width={24} height={24} /> - </Button> - <Button variant="soft" radius="small" color="gray"> - <img - src={asset('shells/hc_premium.webp')} - width={24} - height={24} - /> - </Button> - <Button variant="soft" radius="small" color="gray"> - <img src={asset('shells/he.webp')} width={24} height={24} /> - </Button> - </Flex> - </Flex> - </Card> - )} - </Flex> - - <Flex gap="6" direction="column"> - <Heading>Build</Heading> - - <Flex gap="2" direction="column"> - <Heading size="5" weight="regular"> - Modules - </Heading> - <Theme radius="small"> - <Flex gap="4"> - <Flex gap="1"> - {tank.turrets.map((turretId) => { - const thisTurret = awaitedTurretDefinitions[turretId]; - - return ( - <Tooltip content={thisTurret.name}> - <Button - onClick={() => setTurret(thisTurret)} - variant={ - turret.id === thisTurret.id ? 'solid' : 'soft' - } - > - <img - src="/images/modules/turret.png" - width={32} - height={32} - /> - {TIER_ROMAN_NUMERALS[thisTurret.tier]} - </Button> - </Tooltip> - ); - })} - </Flex> - - <Flex gap="1"> - {turret.guns.map((gunId, index) => { - const thisGun = awaitedGunDefinitions[gunId]; - - return ( - <Tooltip content={thisGun.name}> - <Button - onClick={() => setGun(thisGun)} - variant={gun.id === thisGun.id ? 'solid' : 'soft'} - > - <img - src="/images/modules/gun.png" - width={32} - height={32} - /> - {TIER_ROMAN_NUMERALS[thisGun.tier]} - </Button> - </Tooltip> - ); - })} - </Flex> - </Flex> - </Theme> - </Flex> - - <Flex gap="2" direction="column"> - <Heading size="5" weight="regular"> - Crew - </Heading> - - <Flex gap="4" align="center"> - <Text>{crew}%</Text> - - <Slider - style={{ flex: 1 }} - value={[crew]} - min={50} - max={100} - onValueChange={([crew]) => setCrew(crew)} - /> - </Flex> - </Flex> - </Flex> - - <Flex gap="2" direction="column"> - <Heading size="5">Shells</Heading> - - {gun.shells.map((shellId) => { - const shell = awaitedShellDefinitions[shellId]; - - return ( - <Text> - {shell.name} ({shell.type}): {shell.damage.armor} - </Text> - ); - })} - </Flex> - </Flex> + </> + )} </PageWrapper> ); } diff --git a/src/app/tools/tankopedia/components/Options/index.tsx b/src/app/tools/tankopedia/components/Options/index.tsx deleted file mode 100644 index c67417100..000000000 --- a/src/app/tools/tankopedia/components/Options/index.tsx +++ /dev/null @@ -1,388 +0,0 @@ -'use client'; - -import { CaretDownIcon } from '@radix-ui/react-icons'; -import { Button, DropdownMenu, Flex } from '@radix-ui/themes'; -import { use } from 'react'; -import { Flag } from '../../../../../components/Flag'; -import { - TANK_TYPES, - TREE_TYPES, - TREE_TYPE_ICONS, - TREE_TYPE_IMAGES, - TreeTypeEnum, -} from '../../../../../components/Tanks'; -import { - NATIONS, - TIERS, - TIER_ROMAN_NUMERALS, -} from '../../../../../core/blitzkrieg/definitions/tanks'; -import mutateTankopedia, { - TankopediaTestTankDisplay, - useTankopedia, -} from '../../../../../stores/tankopedia'; -import * as styles from './index.css'; - -export function Options() { - const nations = use(NATIONS); - const tankopediaState = useTankopedia(); - - return ( - <Flex justify="center" direction="column" gap="2"> - <Flex gap="2" style={{ flex: 1 }} wrap="wrap"> - <Button - color="red" - onClick={() => - useTankopedia.setState({ - filters: { - nations: [], - tiers: [], - treeTypes: [], - types: [], - test: 'include', - }, - }) - } - > - Clear - </Button> - <DropdownMenu.Root> - <DropdownMenu.Trigger> - <Button variant="soft"> - Test tanks - <CaretDownIcon /> - </Button> - </DropdownMenu.Trigger> - <DropdownMenu.Content> - <DropdownMenu.RadioGroup - value={tankopediaState.filters.test} - onValueChange={(value) => - mutateTankopedia((draft) => { - draft.filters.test = value as TankopediaTestTankDisplay; - }) - } - > - <DropdownMenu.RadioItem value="include"> - Include - </DropdownMenu.RadioItem> - <DropdownMenu.RadioItem value="exclude"> - Exclude - </DropdownMenu.RadioItem> - <DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem> - </DropdownMenu.RadioGroup> - </DropdownMenu.Content> - </DropdownMenu.Root> - <Flex style={{ flex: 1 }}> - {TREE_TYPES.map((type, index) => ( - <Button - key={type} - variant={ - tankopediaState.filters.treeTypes.includes(type) - ? 'solid' - : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: index === 0 ? undefined : 0, - borderBottomLeftRadius: index === 0 ? undefined : 0, - borderTopRightRadius: - index === TREE_TYPES.length - 1 ? undefined : 0, - borderBottomRightRadius: - index === TREE_TYPES.length - 1 ? undefined : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.treeTypes.includes(type)) { - draft.filters.treeTypes = draft.filters.treeTypes.filter( - (preexistingType) => preexistingType !== type, - ); - } else { - draft.filters.treeTypes.push(type); - } - }) - } - > - <img - src={TREE_TYPE_IMAGES[type]} - style={{ width: '1em', height: '1em' }} - /> - </Button> - ))} - </Flex> - <Flex style={{ flex: 1 }}> - {[...TANK_TYPES].reverse().map((type, index) => ( - <Button - key={type} - variant={ - tankopediaState.filters.types.includes(type) ? 'solid' : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: index === 0 ? undefined : 0, - borderBottomLeftRadius: index === 0 ? undefined : 0, - borderTopRightRadius: - index === TANK_TYPES.length - 1 ? undefined : 0, - borderBottomRightRadius: - index === TANK_TYPES.length - 1 ? undefined : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.types.includes(type)) { - draft.filters.types = draft.filters.types.filter( - (preexistingType) => preexistingType !== type, - ); - } else { - draft.filters.types.push(type); - } - }) - } - > - <img - src={TREE_TYPE_ICONS[TreeTypeEnum.TechTree][type]} - style={{ width: '1em', height: '1em' }} - /> - </Button> - ))} - </Flex> - <Flex - direction="column" - className={styles.splitFilters} - style={{ width: '100%' }} - > - <Flex> - {[...TIERS] - .reverse() - .slice(0, 5) - .map((tier, index) => ( - <Button - key={tier} - variant={ - tankopediaState.filters.tiers.includes(tier) - ? 'solid' - : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: index === 0 ? undefined : 0, - borderBottomLeftRadius: 0, - borderTopRightRadius: - index === TIERS.length - Math.round(TIERS.length / 2) - 1 - ? undefined - : 0, - borderBottomRightRadius: 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.tiers.includes(tier)) { - draft.filters.tiers = draft.filters.tiers.filter( - (preexistingTier) => preexistingTier !== tier, - ); - } else { - draft.filters.tiers.push(tier); - } - }) - } - > - {TIER_ROMAN_NUMERALS[tier]} - </Button> - ))} - </Flex> - <Flex> - {[...TIERS] - .reverse() - .slice(5, 10) - .map((tier, index) => ( - <Button - key={tier} - variant={ - tankopediaState.filters.tiers.includes(tier) - ? 'solid' - : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: 0, - borderBottomLeftRadius: index === 0 ? undefined : 0, - borderTopRightRadius: 0, - borderBottomRightRadius: - index === TIERS.length - Math.round(TIERS.length / 2) - 1 - ? undefined - : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.tiers.includes(tier)) { - draft.filters.tiers = draft.filters.tiers.filter( - (preexistingTier) => preexistingTier !== tier, - ); - } else { - draft.filters.tiers.push(tier); - } - }) - } - > - {TIER_ROMAN_NUMERALS[tier]} - </Button> - ))} - </Flex> - </Flex> - <Flex - direction="column" - className={styles.splitFilters} - style={{ width: '100%' }} - > - <Flex> - {nations.slice(0, 5).map((nation, index) => ( - <Button - key={nation} - variant={ - tankopediaState.filters.nations.includes(nation) - ? 'solid' - : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: index === 0 ? undefined : 0, - borderBottomLeftRadius: 0, - borderTopRightRadius: - index === nations.length - Math.round(nations.length / 2) - ? undefined - : 0, - borderBottomRightRadius: - index === nations.length - 1 ? undefined : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.nations.includes(nation)) { - draft.filters.nations = draft.filters.nations.filter( - (preexistingType) => preexistingType !== nation, - ); - } else { - draft.filters.nations.push(nation); - } - }) - } - > - <Flag nation={nation} /> - </Button> - ))} - </Flex> - - <Flex> - {nations.slice(5, 10).map((nation, index) => ( - <Button - key={nation} - variant={ - tankopediaState.filters.nations.includes(nation) - ? 'solid' - : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: 0, - borderBottomLeftRadius: index === 0 ? undefined : 0, - borderTopRightRadius: - index === nations.length - 1 ? undefined : 0, - borderBottomRightRadius: - index === - nations.length - Math.round(nations.length / 2) - 1 - ? undefined - : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.nations.includes(nation)) { - draft.filters.nations = draft.filters.nations.filter( - (preexistingType) => preexistingType !== nation, - ); - } else { - draft.filters.nations.push(nation); - } - }) - } - > - <Flag nation={nation} /> - </Button> - ))} - </Flex> - </Flex> - </Flex> - - <Flex gap="2" style={{ flex: 1 }} wrap="wrap"> - <Flex className={styles.oneLineFilters} style={{ flex: 1 }}> - {[...TIERS].reverse().map((tier, index) => ( - <Button - key={tier} - variant={ - tankopediaState.filters.tiers.includes(tier) ? 'solid' : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: index === 0 ? undefined : 0, - borderBottomLeftRadius: index === 0 ? undefined : 0, - borderTopRightRadius: - index === TIERS.length - 1 ? undefined : 0, - borderBottomRightRadius: - index === TIERS.length - 1 ? undefined : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.tiers.includes(tier)) { - draft.filters.tiers = draft.filters.tiers.filter( - (preexistingTier) => preexistingTier !== tier, - ); - } else { - draft.filters.tiers.push(tier); - } - }) - } - > - {TIER_ROMAN_NUMERALS[tier]} - </Button> - ))} - </Flex> - <Flex className={styles.oneLineFilters} style={{ flex: 1 }}> - {nations.map((nation, index) => ( - <Button - key={nation} - variant={ - tankopediaState.filters.nations.includes(nation) - ? 'solid' - : 'soft' - } - style={{ - flex: 1, - margin: -0.5, - borderTopLeftRadius: index === 0 ? undefined : 0, - borderBottomLeftRadius: index === 0 ? undefined : 0, - borderTopRightRadius: - index === nations.length - 1 ? undefined : 0, - borderBottomRightRadius: - index === nations.length - 1 ? undefined : 0, - }} - onClick={() => - mutateTankopedia((draft) => { - if (draft.filters.nations.includes(nation)) { - draft.filters.nations = draft.filters.nations.filter( - (preexistingType) => preexistingType !== nation, - ); - } else { - draft.filters.nations.push(nation); - } - }) - } - > - <Flag nation={nation} /> - </Button> - ))} - </Flex> - </Flex> - </Flex> - ); -} diff --git a/src/app/tools/tankopedia/components/PageTurner.tsx b/src/app/tools/tankopedia/components/PageTurner.tsx index 231a6c690..131a6b423 100644 --- a/src/app/tools/tankopedia/components/PageTurner.tsx +++ b/src/app/tools/tankopedia/components/PageTurner.tsx @@ -1,21 +1,18 @@ import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons'; import { Button, Flex, TextField } from '@radix-ui/themes'; import { useEffect, useRef } from 'react'; -import { BlitzkriegTankDefinition } from '../../../../core/blitzkrieg/definitions/tanks'; +import { TankDefinition } from '../../../../core/blitzkrieg/tankDefinitions'; +import mutateTankopediaPersistent, { + useTankopediaPersistent, +} from '../../../../stores/tankopedia'; interface PageTurnerProps { - page: number; - setPage: (page: number) => void; - searchedList: BlitzkriegTankDefinition[]; + searchedList: TankDefinition[]; tanksPerPage: number; } -export function PageTurner({ - page, - setPage, - searchedList, - tanksPerPage, -}: PageTurnerProps) { +export function PageTurner({ searchedList, tanksPerPage }: PageTurnerProps) { + const page = useTankopediaPersistent((state) => state.filters.page); const pageInput = useRef<HTMLInputElement>(null); useEffect(() => { @@ -27,7 +24,11 @@ export function PageTurner({ <Button variant="soft" disabled={page === 0} - onClick={() => setPage(Math.max(0, page - 1))} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.filters.page = Math.max(0, page - 1); + }); + }} > <CaretLeftIcon /> </Button> @@ -41,15 +42,15 @@ export function PageTurner({ max={Math.floor(searchedList.length / tanksPerPage) + 1} style={{ width: 64, textAlign: 'center' }} onBlur={(event) => { - setPage( - Math.max( + mutateTankopediaPersistent((draft) => { + draft.filters.page = Math.max( 0, Math.min( Math.floor(searchedList.length / tanksPerPage), event.target.valueAsNumber - 1, ), - ), - ); + ); + }); }} onKeyDown={(event) => { if (event.key === 'Enter') { @@ -64,11 +65,14 @@ export function PageTurner({ <Button variant="soft" disabled={Math.floor(searchedList.length / tanksPerPage) === page} - onClick={() => - setPage( - Math.min(Math.floor(searchedList.length / tanksPerPage), page + 1), - ) - } + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.filters.page = Math.min( + Math.floor(searchedList.length / tanksPerPage), + page + 1, + ); + }); + }} > <CaretRightIcon /> </Button> diff --git a/src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx b/src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx new file mode 100644 index 000000000..dec1e62cf --- /dev/null +++ b/src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx @@ -0,0 +1,53 @@ +import { Button, Flex, Text } from '@radix-ui/themes'; +import { SmallTankIcon } from '../../../../../../components/SmallTankIcon'; +import { + TIER_ROMAN_NUMERALS, + TankDefinition, +} from '../../../../../../core/blitzkrieg/tankDefinitions'; + +interface CompactSearchResultRowProps { + tanks: TankDefinition[]; + onSelect: (tank: TankDefinition) => void; +} + +export function CompactSearchResultRow({ + tanks, + onSelect = () => {}, +}: CompactSearchResultRowProps) { + return ( + <Flex justify="center"> + <Flex direction="column" gap="2" align="start"> + {tanks.map((tank) => ( + <Button + key={tank.id} + variant="ghost" + style={{ width: '100%' }} + onClick={() => onSelect(tank)} + > + <Flex gap="2" align="start" style={{ width: '100%' }}> + <Text align="right" color="gray" style={{ width: 24 }}> + {TIER_ROMAN_NUMERALS[tank.tier]} + </Text> + + <Text color={tank.treeType === 'collector' ? 'blue' : 'amber'}> + <Flex gap="2" align="center"> + <SmallTankIcon id={tank.id} size={16} /> + + <div + style={{ + display: 'block', + color: + tank.treeType === 'researchable' ? 'white' : undefined, + }} + > + {tank.name} + </div> + </Flex> + </Text> + </Flex> + </Button> + ))} + </Flex> + </Flex> + ); +} diff --git a/src/app/tools/tankopedia/components/Options/index.css.ts b/src/app/tools/tankopedia/components/TankSearch/components/Options/index.css.ts similarity index 100% rename from src/app/tools/tankopedia/components/Options/index.css.ts rename to src/app/tools/tankopedia/components/TankSearch/components/Options/index.css.ts diff --git a/src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx b/src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx new file mode 100644 index 000000000..0b512243f --- /dev/null +++ b/src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx @@ -0,0 +1,338 @@ +'use client'; + +import { CaretDownIcon } from '@radix-ui/react-icons'; +import { Button, DropdownMenu, Flex } from '@radix-ui/themes'; +import { use } from 'react'; +import { Flag } from '../../../../../../../components/Flag'; +import { + TANK_TYPES, + TREE_TYPES, + TREE_TYPE_ICONS, + TREE_TYPE_IMAGES, +} from '../../../../../../../components/Tanks'; +import { + NATIONS, + TIERS, + TIER_ROMAN_NUMERALS, +} from '../../../../../../../core/blitzkrieg/tankDefinitions'; +import mutateTankopediaPersistent, { + TankopediaTestTankDisplay, + useTankopediaPersistent, +} from '../../../../../../../stores/tankopedia'; +import * as styles from './index.css'; + +export function Options() { + const nations = use(NATIONS); + const filters = useTankopediaPersistent((state) => state.filters); + + return ( + <Flex gap="2" wrap="wrap" justify="center"> + <DropdownMenu.Root> + <DropdownMenu.Trigger> + <Button variant="soft" color="gray"> + Test tanks + <CaretDownIcon /> + </Button> + </DropdownMenu.Trigger> + <DropdownMenu.Content> + <DropdownMenu.RadioGroup + value={filters.test} + onValueChange={(value) => + mutateTankopediaPersistent((draft) => { + draft.filters.test = value as TankopediaTestTankDisplay; + }) + } + > + <DropdownMenu.RadioItem value="include"> + Include + </DropdownMenu.RadioItem> + <DropdownMenu.RadioItem value="exclude"> + Exclude + </DropdownMenu.RadioItem> + <DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem> + </DropdownMenu.RadioGroup> + </DropdownMenu.Content> + </DropdownMenu.Root> + <Flex> + {TREE_TYPES.map((type, index) => ( + <Button + key={type} + color={filters.treeTypes.includes(type) ? undefined : 'gray'} + variant={filters.treeTypes.includes(type) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: index === 0 ? undefined : 0, + borderBottomLeftRadius: index === 0 ? undefined : 0, + borderTopRightRadius: + index === TREE_TYPES.length - 1 ? undefined : 0, + borderBottomRightRadius: + index === TREE_TYPES.length - 1 ? undefined : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.treeTypes.includes(type)) { + draft.filters.treeTypes = draft.filters.treeTypes.filter( + (preexistingType) => preexistingType !== type, + ); + } else { + draft.filters.treeTypes.push(type); + } + }) + } + > + <img + src={TREE_TYPE_IMAGES[type]} + style={{ width: '1em', height: '1em' }} + /> + </Button> + ))} + </Flex> + <Flex className={styles.oneLineFilters}> + {nations.map((nation, index) => ( + <Button + key={nation} + color={filters.nations.includes(nation) ? undefined : 'gray'} + variant={filters.nations.includes(nation) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: index === 0 ? undefined : 0, + borderBottomLeftRadius: index === 0 ? undefined : 0, + borderTopRightRadius: + index === nations.length - 1 ? undefined : 0, + borderBottomRightRadius: + index === nations.length - 1 ? undefined : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.nations.includes(nation)) { + draft.filters.nations = draft.filters.nations.filter( + (preexistingType) => preexistingType !== nation, + ); + } else { + draft.filters.nations.push(nation); + } + }) + } + > + <Flag nation={nation} /> + </Button> + ))} + </Flex> + <Flex> + {[...TANK_TYPES].map((type, index) => ( + <Button + key={type} + color={filters.types.includes(type) ? undefined : 'gray'} + variant={filters.types.includes(type) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: index === 0 ? undefined : 0, + borderBottomLeftRadius: index === 0 ? undefined : 0, + borderTopRightRadius: + index === TANK_TYPES.length - 1 ? undefined : 0, + borderBottomRightRadius: + index === TANK_TYPES.length - 1 ? undefined : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.types.includes(type)) { + draft.filters.types = draft.filters.types.filter( + (preexistingType) => preexistingType !== type, + ); + } else { + draft.filters.types.push(type); + } + }) + } + > + <img + src={TREE_TYPE_ICONS.researchable[type]} + style={{ width: '1em', height: '1em' }} + /> + </Button> + ))} + </Flex> + <Flex className={styles.oneLineFilters}> + {[...TIERS].map((tier, index) => ( + <Button + key={tier} + color={filters.tiers.includes(tier) ? undefined : 'gray'} + variant={filters.tiers.includes(tier) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: index === 0 ? undefined : 0, + borderBottomLeftRadius: index === 0 ? undefined : 0, + borderTopRightRadius: index === TIERS.length - 1 ? undefined : 0, + borderBottomRightRadius: + index === TIERS.length - 1 ? undefined : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.tiers.includes(tier)) { + draft.filters.tiers = draft.filters.tiers.filter( + (preexistingTier) => preexistingTier !== tier, + ); + } else { + draft.filters.tiers.push(tier); + } + }) + } + > + {TIER_ROMAN_NUMERALS[tier]} + </Button> + ))} + </Flex>{' '} + <Flex + direction="column" + className={styles.splitFilters} + style={{ width: '100%' }} + > + <Flex> + {[...TIERS].slice(0, 5).map((tier, index) => ( + <Button + key={tier} + color={filters.tiers.includes(tier) ? undefined : 'gray'} + variant={filters.tiers.includes(tier) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: index === 0 ? 16 : 0, + borderBottomLeftRadius: 0, + borderTopRightRadius: + index === TIERS.length - Math.round(TIERS.length / 2) - 1 + ? 16 + : 0, + borderBottomRightRadius: 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.tiers.includes(tier)) { + draft.filters.tiers = draft.filters.tiers.filter( + (preexistingTier) => preexistingTier !== tier, + ); + } else { + draft.filters.tiers.push(tier); + } + }) + } + > + {TIER_ROMAN_NUMERALS[tier]} + </Button> + ))} + </Flex> + <Flex> + {[...TIERS].slice(5).map((tier, index) => ( + <Button + key={tier} + color={filters.tiers.includes(tier) ? undefined : 'gray'} + variant={filters.tiers.includes(tier) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: 0, + borderBottomLeftRadius: index === 0 ? 16 : 0, + borderTopRightRadius: 0, + borderBottomRightRadius: + index === TIERS.length - Math.round(TIERS.length / 2) - 1 + ? 16 + : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.tiers.includes(tier)) { + draft.filters.tiers = draft.filters.tiers.filter( + (preexistingTier) => preexistingTier !== tier, + ); + } else { + draft.filters.tiers.push(tier); + } + }) + } + > + {TIER_ROMAN_NUMERALS[tier]} + </Button> + ))} + </Flex> + </Flex> + <Flex + direction="column" + className={styles.splitFilters} + style={{ width: '100%' }} + > + <Flex> + {nations.slice(0, 5).map((nation, index) => ( + <Button + key={nation} + color={filters.nations.includes(nation) ? undefined : 'gray'} + variant={filters.nations.includes(nation) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: index === 0 ? 16 : 0, + borderBottomLeftRadius: 0, + borderTopRightRadius: + index === nations.length - Math.round(nations.length / 2) + ? 16 + : 0, + borderBottomRightRadius: + index === nations.length - 1 ? undefined : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.nations.includes(nation)) { + draft.filters.nations = draft.filters.nations.filter( + (preexistingType) => preexistingType !== nation, + ); + } else { + draft.filters.nations.push(nation); + } + }) + } + > + <Flag nation={nation} /> + </Button> + ))} + </Flex> + + <Flex> + {nations.slice(5).map((nation, index) => ( + <Button + key={nation} + color={filters.nations.includes(nation) ? undefined : 'gray'} + variant={filters.nations.includes(nation) ? 'surface' : 'soft'} + style={{ + flex: 1, + margin: -0.5, + borderTopLeftRadius: 0, + borderBottomLeftRadius: index === 0 ? 16 : 0, + borderTopRightRadius: + index === nations.length - 1 ? undefined : 0, + borderBottomRightRadius: + index === nations.length - Math.round(nations.length / 2) - 1 + ? 16 + : 0, + }} + onClick={() => + mutateTankopediaPersistent((draft) => { + if (draft.filters.nations.includes(nation)) { + draft.filters.nations = draft.filters.nations.filter( + (preexistingType) => preexistingType !== nation, + ); + } else { + draft.filters.nations.push(nation); + } + }) + } + > + <Flag nation={nation} /> + </Button> + ))} + </Flex> + </Flex> + </Flex> + ); +} diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx new file mode 100644 index 000000000..41a6f3a7d --- /dev/null +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -0,0 +1,896 @@ +import { slateDark } from '@radix-ui/colors'; +import { + CaretDownIcon, + MagnifyingGlassIcon, + TrashIcon, +} from '@radix-ui/react-icons'; +import { + Button, + Card, + DropdownMenu, + Flex, + Inset, + Text, + TextField, +} from '@radix-ui/themes'; +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, + TANK_ICONS_COLLECTOR, + TANK_ICONS_PREMIUM, + TIER_ROMAN_NUMERALS, + TankDefinition, + tanksDefinitionsArray, +} from '../../../../../core/blitzkrieg/tankDefinitions'; +import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon'; +import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox'; +import { theme } from '../../../../../stitches.config'; +import mutateTankopediaPersistent, { + SORT_NAMES, + TankopediaSortDirection, + useTankopediaPersistent, +} from '../../../../../stores/tankopedia'; +import * as styles from '../../page.css'; +import { PageTurner } from '../PageTurner'; +import { CompactSearchResultRow } from './components/CompactSearchResultRow'; +import { Options } from './components/Options'; + +interface TankSearchProps { + compact?: boolean; + onSelect?: (tank: TankDefinition) => void; +} + +const tankTypeOrder: TankType[] = ['light', 'medium', 'heavy', 'tankDestroyer']; +const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector']; + +export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) { + const tanksPerPage = compact ? 16 : 96; + const nations = use(NATIONS); + const filters = useTankopediaPersistent((state) => state.filters); + const sort = useTankopediaPersistent((state) => state.sort); + const awaitedTanks = use(tanksDefinitionsArray); + const awaitedModelDefinitions = use(modelDefinitions); + const defaultSortedTanks = useMemo( + () => + awaitedTanks + .sort( + (a, b) => + (treeTypeOrder.indexOf(a.treeType) - + treeTypeOrder.indexOf(b.treeType)) * + (sort.direction === 'descending' ? -1 : 1), + ) + .sort( + (a, b) => + (tankTypeOrder.indexOf(a.type) - tankTypeOrder.indexOf(b.type)) * + (sort.direction === 'descending' ? -1 : 1), + ) + .sort( + (a, b) => + (nations.indexOf(a.nation) - nations.indexOf(b.nation)) * + (sort.direction === 'descending' ? -1 : 1), + ), + [], + ); + const input = useRef<HTMLInputElement>(null); + 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[] = []; + + 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(); + }, [ + sort, + ...Object.entries(filters) + .filter(([name]) => name !== 'page') + .map(([, value]) => value), + ]); + const [searchResults, setSearchedList] = useState(searchableTanks); + const page = useTankopediaPersistent((state) => state.filters.page); + const wideFormat = typeof window !== 'undefined' && window.innerWidth > 512; + const chunkSize = wideFormat ? tanksPerPage / 2 : Infinity; + const searchResultsPageSlice = searchResults.slice( + page * tanksPerPage, + (page + 1) * tanksPerPage, + ); + const firstChunk = searchResultsPageSlice.slice(0, chunkSize); + const secondChunk = searchResultsPageSlice.slice(chunkSize); + + useEffect(() => { + setSearchedList(searchableTanks); + }, [searchableTanks]); + useEffect(() => { + mutateTankopediaPersistent((draft) => { + draft.filters.page = Math.min( + Math.max(0, page), + Math.ceil(searchResults.length / tanksPerPage) - 1, + ); + }); + }, [searchResults]); + + return ( + <> + <Flex direction="column" gap="4"> + <Flex gap="2"> + <TextField.Root style={{ flex: 1 }}> + <TextField.Slot> + <MagnifyingGlassIcon height="16" width="16" /> + </TextField.Slot> + + <TextField.Input + ref={input} + placeholder="Search tanks..." + onChange={(event) => { + const results = go(event.target.value, searchableTanks, { + keys: [ + 'name', + 'nameFull', + 'id', + ] satisfies (keyof TankDefinition)[], + all: true, + }).map(({ obj }) => obj); + + setSearchedList(results); + }} + /> + </TextField.Root> + + <DropdownMenu.Root> + <DropdownMenu.Trigger> + <Button variant="soft" color="gray"> + Sort + <CaretDownIcon /> + </Button> + </DropdownMenu.Trigger> + <DropdownMenu.Content> + <DropdownMenu.Label>By</DropdownMenu.Label> + + <DropdownMenu.Sub> + <DropdownMenu.SubTrigger>Meta</DropdownMenu.SubTrigger> + <DropdownMenu.SubContent> + <DropdownMenu.CheckboxItem + checked={sort.by === 'meta.tier'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'meta.tier'; + }); + }} + > + Tier + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'meta.name'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'meta.name'; + }); + }} + > + Name + </DropdownMenu.CheckboxItem> + </DropdownMenu.SubContent> + </DropdownMenu.Sub> + + <DropdownMenu.Sub> + <DropdownMenu.SubTrigger>Fire</DropdownMenu.SubTrigger> + <DropdownMenu.SubContent> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.dpm'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.dpm'; + }); + }} + > + DPM + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.reload'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.reload'; + }); + }} + > + Reload + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.caliber'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.caliber'; + }); + }} + > + Caliber + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.standardPenetration'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.standardPenetration'; + }); + }} + > + Standard penetration + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.premiumPenetration'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.premiumPenetration'; + }); + }} + > + Premium penetration + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.damage'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.damage'; + }); + }} + > + Damage + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.shellVelocity'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.shellVelocity'; + }); + }} + > + Shell velocity + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.aimTime'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.aimTime'; + }); + }} + > + Aim time + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.dispersionStill'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.dispersionStill'; + }); + }} + > + Dispersion still + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.dispersionMoving'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.dispersionMoving'; + }); + }} + > + Dispersion moving + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.gunDepression'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.gunDepression'; + }); + }} + > + Gun depression + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'fire.gunElevation'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'fire.gunElevation'; + }); + }} + > + Gun elevation + </DropdownMenu.CheckboxItem> + </DropdownMenu.SubContent> + </DropdownMenu.Sub> + + <DropdownMenu.Sub> + <DropdownMenu.SubTrigger> + Maneuverability + </DropdownMenu.SubTrigger> + <DropdownMenu.SubContent> + <DropdownMenu.CheckboxItem + checked={sort.by === 'maneuverability.forwardsSpeed'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.forwardsSpeed'; + }); + }} + > + Forwards speed + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'maneuverability.backwardsSpeed'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.backwardsSpeed'; + }); + }} + > + Backwards speed + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'maneuverability.power'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.power'; + }); + }} + > + Power + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'maneuverability.powerToWeight'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.powerToWeight'; + }); + }} + > + Power to weight + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'maneuverability.weight'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.weight'; + }); + }} + > + Weight + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'maneuverability.traverseSpeed'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'maneuverability.traverseSpeed'; + }); + }} + > + Traverse speed + </DropdownMenu.CheckboxItem> + </DropdownMenu.SubContent> + </DropdownMenu.Sub> + + <DropdownMenu.Sub> + <DropdownMenu.SubTrigger>Survivability</DropdownMenu.SubTrigger> + <DropdownMenu.SubContent> + <DropdownMenu.CheckboxItem + checked={sort.by === 'survivability.health'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.health'; + }); + }} + > + Health + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'survivability.viewRange'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.viewRange'; + }); + }} + > + View range + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'survivability.camouflageStill'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.camouflageStill'; + }); + }} + > + Camouflage + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'survivability.camouflageMoving'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.camouflageMoving'; + }); + }} + > + Camouflage moving + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'survivability.camouflageShooting'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.camouflageShooting'; + }); + }} + > + Camouflage shooting + </DropdownMenu.CheckboxItem> + <DropdownMenu.CheckboxItem + checked={sort.by === 'survivability.size'} + onClick={() => { + mutateTankopediaPersistent((draft) => { + draft.sort.by = 'survivability.size'; + }); + }} + > + Size + </DropdownMenu.CheckboxItem> + </DropdownMenu.SubContent> + </DropdownMenu.Sub> + + <DropdownMenu.Separator /> + + <DropdownMenu.Label>Direction</DropdownMenu.Label> + <DropdownMenu.RadioGroup + value={sort.direction} + onValueChange={(value) => + mutateTankopediaPersistent((draft) => { + draft.sort.direction = value as TankopediaSortDirection; + }) + } + > + <DropdownMenu.RadioItem value="ascending"> + Ascending + </DropdownMenu.RadioItem> + <DropdownMenu.RadioItem value="descending"> + Descending + </DropdownMenu.RadioItem> + </DropdownMenu.RadioGroup> + </DropdownMenu.Content> + </DropdownMenu.Root> + + <Button + color="red" + variant="soft" + onClick={() => + useTankopediaPersistent.setState({ + filters: { + nations: [], + tiers: [], + treeTypes: [], + types: [], + test: 'include', + page: 0, + }, + sort: { + by: 'meta.tier', + direction: 'descending', + }, + }) + } + > + <TrashIcon /> + </Button> + </Flex> + + <Options /> + </Flex> + + <Flex justify="center"> + <Text color="gray"> + Sorting: {SORT_NAMES[sort.by]}, {sort.direction} + </Text> + </Flex> + + {!compact && ( + <PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} /> + )} + <Flex wrap="wrap" gap="2" justify={compact ? 'between' : 'center'}> + {!compact && + searchResultsPageSlice.map((tank) => ( + <Card + key={tank.id} + onClick={() => onSelect(tank)} + className={styles.listing} + style={{ + flex: 1, + cursor: 'pointer', + height: 64, + }} + > + <Inset + key={tank.id} + style={{ + minWidth: 256, + minHeight: 64, + position: 'relative', + display: 'flex', + }} + > + <img + className={styles.flag} + src={asset(`flags/scratched/${tank.nation}.webp`)} + style={{ + width: '100%', + height: '100%', + objectFit: 'contain', + position: 'absolute', + }} + /> + + <div + className={styles.listingShadow} + style={{ + width: '100%', + height: '100%', + position: 'absolute', + transition: `box-shadow ${theme.durations.regular}`, + }} + /> + + <div + style={{ + width: 256, + height: 128, + }} + className={styles.listingImage} + > + <img + src={tankIcon(tank.id)} + style={{ + objectFit: 'contain', + objectPosition: 'left center', + }} + /> + </div> + + <Flex + align="center" + justify="between" + className={styles.listingLabel} + style={{ + position: 'absolute', + bottom: 8, + padding: '0 12px', + width: '100%', + boxSizing: 'border-box', + }} + > + <Flex align="center" justify="center" gap="1"> + <img + src={ + (tank.treeType === 'collector' + ? TANK_ICONS_COLLECTOR + : tank.treeType === 'premium' + ? TANK_ICONS_PREMIUM + : TANK_ICONS)[tank.type] + } + style={{ width: '1em', height: '1em' }} + /> + <Text + size="4" + color={ + tank.treeType === 'collector' + ? 'blue' + : tank.treeType === 'premium' + ? 'amber' + : undefined + } + weight="medium" + style={{ + color: + tank.treeType === 'researchable' + ? slateDark.slate12 + : undefined, + whiteSpace: 'nowrap', + maxWidth: 160, + display: 'block', + textOverflow: 'ellipsis', + overflow: 'hidden', + }} + > + {tank.name} + </Text> + </Flex> + + <Text size="4" color="gray" highContrast={false}> + {TIER_ROMAN_NUMERALS[tank.tier]} + </Text> + </Flex> + </Inset> + </Card> + ))} + + {compact && ( + <CompactSearchResultRow tanks={firstChunk} onSelect={onSelect} /> + )} + + {compact && secondChunk.length > 0 && ( + <CompactSearchResultRow tanks={secondChunk} onSelect={onSelect} /> + )} + </Flex> + <PageTurner tanksPerPage={tanksPerPage} searchedList={searchResults} /> + </> + ); +} diff --git a/src/app/tools/tankopedia/page.css.ts b/src/app/tools/tankopedia/page.css.ts index c5cc972f4..891a7fbb4 100644 --- a/src/app/tools/tankopedia/page.css.ts +++ b/src/app/tools/tankopedia/page.css.ts @@ -7,18 +7,24 @@ export const flag = style({ transitionDuration: theme.durations.regular, left: '0', top: '0', + scale: 1.5, + opacity: 0.75, [`.${listing}:hover &, .${listing}:active &`]: { - transform: 'translateX(-25%)', - filter: 'blur(3rem)', + opacity: 1, }, + + // [`.${listing}:hover &, .${listing}:active &`]: { + // transform: 'translateX(-25%)', + // filter: 'blur(3rem)', + // }, }); export const listingShadow = style({ - boxShadow: `inset 128px -128px 128px -128px ${theme.colors.appBackground1}`, + boxShadow: `inset 64px -80px 64px -64px ${theme.colors.appBackground1}`, [`.${listing}:hover &, .${listing}:active &`]: { - boxShadow: `inset 0 0 0 -128px ${theme.colors.appBackground1}`, + boxShadow: `inset 0 0 0 -64px ${theme.colors.appBackground1}`, }, }); @@ -26,22 +32,23 @@ export const listingLabel = style({ transition: `transform ${theme.durations.regular}`, transform: 'translateY(0)', - [`.${listing}:hover &, .${listing}:active &`]: { - transform: 'translateY(200%)', - }, + // [`.${listing}:hover &, .${listing}:active &`]: { + // transform: 'translateY(200%)', + // }, }); export const listingImage = style({ transition: `transform ${theme.durations.regular}`, position: 'absolute', left: '50%', + top: '-25%', - [`.${listing}:hover &, .${listing}:active &`]: { - transform: 'translate(-25%, 10%)', - }, + // [`.${listing}:hover &, .${listing}:active &`]: { + // transform: 'translate(-25%, 10%)', + // }, }); -export const enhancedArmor = style({ +export const alignTopRight = style({ '@media': { 'screen and (min-width: 400px)': { position: 'absolute', @@ -55,3 +62,27 @@ export const enhancedArmor = style({ }, }, }); + +export const characteristics = style({ + display: 'flex', + gap: 32, + + '@media': { + 'screen and (max-width: 800px)': { + flexDirection: 'column', + }, + }, +}); + +export const configurationChild = style({ + display: 'flex', + gap: 8, + flexDirection: 'column', +}); + +export const configuration = style({ + display: 'flex', + flexDirection: 'column', + gap: 16, + flex: 1, +}); diff --git a/src/app/tools/tankopedia/page.tsx b/src/app/tools/tankopedia/page.tsx index e99ec392b..8dd966b7a 100644 --- a/src/app/tools/tankopedia/page.tsx +++ b/src/app/tools/tankopedia/page.tsx @@ -1,329 +1,19 @@ 'use client'; -import { slateDark } from '@radix-ui/colors'; -import { - CaretDownIcon, - CaretUpIcon, - MagnifyingGlassIcon, -} from '@radix-ui/react-icons'; -import { - Button, - Card, - DropdownMenu, - Flex, - Inset, - Text, - TextField, -} from '@radix-ui/themes'; -import { go } from 'fuzzysort'; -import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react'; +import { useRouter } from 'next/navigation'; import PageWrapper from '../../../components/PageWrapper'; -import { asset } from '../../../core/blitzkrieg/asset'; -import { - BlitzkriegTankDefinition, - TANK_ICONS, - TANK_ICONS_COLLECTOR, - TANK_ICONS_PREMIUM, - TIER_ROMAN_NUMERALS, - tanksDefinitionsArray, -} from '../../../core/blitzkrieg/definitions/tanks'; -import { tankIcon } from '../../../core/blitzkrieg/tankIcon'; -import { theme } from '../../../stitches.config'; -import mutateTankopedia, { - TankopediaSortBy, - TankopediaSortDirection, - useTankopedia, -} from '../../../stores/tankopedia'; -import { Options } from './components/Options'; -import { PageTurner } from './components/PageTurner'; -import * as styles from './page.css'; - -const TANKS_PER_PAGE = 3 * 2 ** 3; +import { TankSearch } from './components/TankSearch'; export default function Page() { - const tankopediaState = useTankopedia(); - const awaitedTanks = use(tanksDefinitionsArray); - const input = useRef<HTMLInputElement>(null); - const searchableTanks = useMemo( - () => - awaitedTanks - .filter( - (tank) => - (tankopediaState.filters.tiers.length === 0 - ? true - : tankopediaState.filters.tiers.includes(tank.tier)) && - (tankopediaState.filters.types.length === 0 - ? true - : tankopediaState.filters.types.includes(tank.type)) && - (tankopediaState.filters.treeTypes.length === 0 - ? true - : tankopediaState.filters.treeTypes.includes(tank.tree_type)) && - (tankopediaState.filters.nations.length === 0 - ? true - : tankopediaState.filters.nations.includes(tank.nation)) && - (tankopediaState.filters.test === 'include' - ? true - : tankopediaState.filters.test === 'exclude' - ? !tank.testing - : tank.testing), - ) - .sort((a, b) => { - let diff = 0; - - if (tankopediaState.sort.by === 'tier') { - diff = a.tier - b.tier; - } - if (tankopediaState.sort.by === 'name') { - diff = a.name.localeCompare(b.name); - } - - return tankopediaState.sort.direction === 'ascending' ? diff : -diff; - }), - [tankopediaState.filters, tankopediaState.sort], - ); - const [searchedList, setSearchedList] = useState(searchableTanks); - const [page, setPage] = useState(0); - const [showFilters, setShowFilters] = useState(false); - - useEffect(() => { - setSearchedList(searchableTanks); - if (input.current) input.current.value = ''; - }, [searchableTanks]); - - useEffect(() => { - setPage( - Math.min( - Math.max(0, page), - Math.ceil(searchedList.length / TANKS_PER_PAGE) - 1, - ), - ); - }, [searchedList]); + const router = useRouter(); return ( - <PageWrapper size="wide" color="purple"> - <Suspense fallback={<Text>Loading...</Text>}> - <Flex direction="column" gap="4"> - <Flex gap="2"> - <TextField.Root style={{ flex: 1 }}> - <TextField.Slot> - <MagnifyingGlassIcon height="16" width="16" /> - </TextField.Slot> - <TextField.Input - ref={input} - placeholder="Search tanks..." - onChange={(event) => { - if (event.target.value.length === 0) { - setSearchedList(searchableTanks); - } else { - setSearchedList( - go(event.target.value, searchableTanks, { - keys: [ - 'name', - 'id', - ] satisfies (keyof BlitzkriegTankDefinition)[], - }).map(({ obj }) => obj), - ); - } - }} - /> - </TextField.Root> - - <DropdownMenu.Root> - <DropdownMenu.Trigger> - <Button variant="soft"> - Sort - <CaretDownIcon /> - </Button> - </DropdownMenu.Trigger> - <DropdownMenu.Content> - <DropdownMenu.Label>By</DropdownMenu.Label> - <DropdownMenu.RadioGroup - value={tankopediaState.sort.by} - onValueChange={(value) => - mutateTankopedia((draft) => { - draft.sort.by = value as TankopediaSortBy; - }) - } - > - <DropdownMenu.RadioItem value="tier"> - Tier - </DropdownMenu.RadioItem> - <DropdownMenu.RadioItem value="name"> - Name - </DropdownMenu.RadioItem> - </DropdownMenu.RadioGroup> - - <DropdownMenu.Separator /> - - <DropdownMenu.Label>Direction</DropdownMenu.Label> - <DropdownMenu.RadioGroup - value={tankopediaState.sort.direction} - onValueChange={(value) => - mutateTankopedia((draft) => { - draft.sort.direction = value as TankopediaSortDirection; - }) - } - > - <DropdownMenu.RadioItem value="ascending"> - Ascending - </DropdownMenu.RadioItem> - <DropdownMenu.RadioItem value="descending"> - Descending - </DropdownMenu.RadioItem> - </DropdownMenu.RadioGroup> - </DropdownMenu.Content> - </DropdownMenu.Root> - - <Button - variant="soft" - onClick={() => setShowFilters((state) => !state)} - > - Filters {showFilters ? <CaretUpIcon /> : <CaretDownIcon />} - </Button> - </Flex> - - {showFilters && ( - <Card> - <Suspense fallback={<Text>Loading...</Text>}> - <Options /> - </Suspense> - </Card> - )} - </Flex> - - <PageTurner - page={page} - setPage={setPage} - tanksPerPage={TANKS_PER_PAGE} - searchedList={searchedList} - /> - - <Flex wrap="wrap" gap="3" justify="center"> - {searchedList - .map((tank) => ( - <a - key={tank.id} - href={`/tools/tankopedia/${tank.id}`} - style={{ - flex: 1, - textDecoration: 'none', - color: 'unset', - }} - > - <Card className={styles.listing}> - <Inset - key={tank.id} - style={{ - minWidth: 256, - minHeight: 128, - position: 'relative', - display: 'flex', - }} - > - <img - className={styles.flag} - src={asset(`flags/scratched/${tank.nation}.webp`)} - style={{ - width: '100%', - height: '100%', - objectFit: 'contain', - position: 'absolute', - }} - /> - - <div - style={{ - width: 256, - height: 128, - }} - className={styles.listingImage} - > - <img - src={tankIcon(tank.id)} - style={{ - objectFit: 'contain', - objectPosition: 'left center', - }} - /> - </div> - - <div - className={styles.listingShadow} - style={{ - width: '100%', - height: '100%', - position: 'absolute', - transition: `box-shadow ${theme.durations.regular}`, - }} - /> - - <Flex - align="center" - justify="between" - className={styles.listingLabel} - style={{ - position: 'absolute', - bottom: 8, - padding: '0 12px', - width: '100%', - boxSizing: 'border-box', - }} - > - <Flex align="center" justify="center" gap="1"> - <img - src={ - (tank.tree_type === 'collector' - ? TANK_ICONS_COLLECTOR - : tank.tree_type === 'premium' - ? TANK_ICONS_PREMIUM - : TANK_ICONS)[tank.type] - } - style={{ width: '1em', height: '1em' }} - /> - <Text - size="4" - color={ - tank.tree_type === 'collector' - ? 'blue' - : tank.tree_type === 'premium' - ? 'amber' - : undefined - } - weight="medium" - style={{ - color: - tank.tree_type === 'researchable' - ? slateDark.slate12 - : undefined, - whiteSpace: 'nowrap', - maxWidth: 160, - display: 'block', - textOverflow: 'ellipsis', - overflow: 'hidden', - }} - > - {tank.name} - </Text> - </Flex> - - <Text size="4" color="gray" highContrast={false}> - {TIER_ROMAN_NUMERALS[tank.tier]} - </Text> - </Flex> - </Inset> - </Card> - </a> - )) - .slice(page * TANKS_PER_PAGE, (page + 1) * TANKS_PER_PAGE)} - </Flex> - - <PageTurner - page={page} - setPage={setPage} - tanksPerPage={TANKS_PER_PAGE} - searchedList={searchedList} - /> - </Suspense> + <PageWrapper size="double" color="purple"> + <TankSearch + onSelect={(tank) => { + router.push(`./tankopedia/${tank.id}`); + }} + /> </PageWrapper> ); } diff --git a/src/assets/art/SkyLine.tsx b/src/assets/art/SkyLine.tsx new file mode 100644 index 000000000..3560aac4c --- /dev/null +++ b/src/assets/art/SkyLine.tsx @@ -0,0 +1,61 @@ +import { ComponentProps } from 'react'; + +export function SkyLine(props: ComponentProps<'svg'>) { + return ( + <svg + width="294" + height="99" + viewBox="0 0 294 99" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path + d="M80.2779 23.3701C80.2779 23.3701 73.8679 13.2501 67.5679 18.3701C67.5679 18.3701 65.5679 9.69007 59.1579 12.9301C52.7479 16.1701 55.4779 21.4901 55.4779 21.4901C55.4779 21.4901 47.2779 20.6101 46.4279 24.8101C46.4279 24.8101 39.2879 24.8101 38.5579 30.8101H81.9579L80.2779 23.3701Z" + fill="#082424" + /> + <path + d="M89.8678 90.33C92.4159 88.2998 95.5978 87.2321 98.8547 87.3144C102.112 87.3967 105.236 88.6237 107.678 90.78C107.448 90.56 110.008 88.53 110.258 88.38C111.377 87.6722 112.624 87.1899 113.928 86.96C116.408 86.52 119.358 87.04 121.088 89.02C121.116 88.7441 121.226 88.483 121.405 88.2708C121.583 88.0585 121.821 87.9049 122.088 87.83C122.359 87.7706 122.641 87.7932 122.9 87.8946C123.158 87.9961 123.38 88.172 123.538 88.4C122.718 87.14 125.278 85.35 126.068 84.91C127.406 84.2635 128.837 83.8283 130.308 83.62C133.736 82.9624 137.235 82.7473 140.718 82.98C147.715 83.4087 154.471 85.6975 160.288 89.61C160.918 90.05 161.538 90.5 162.138 90.97C165.328 87.55 168.738 85.66 173.558 86.17C174.583 86.2315 175.58 86.5254 176.474 87.0296C177.368 87.5338 178.135 88.235 178.718 89.08C178.968 88.4817 179.403 87.9788 179.959 87.6443C180.514 87.3098 181.162 87.1612 181.808 87.22C182.452 87.2962 183.074 87.5053 183.633 87.8341C184.193 88.1628 184.678 88.6041 185.058 89.13C183.768 87.46 189.648 84.44 190.618 84.26C193.668 83.71 196.618 85.71 199.008 87.43C201.189 85.0982 204.063 83.5308 207.204 82.9599C210.345 82.389 213.586 82.8448 216.448 84.26C218.627 82.7527 221.302 82.1396 223.92 82.5471C226.538 82.9546 228.9 84.3517 230.518 86.45C231.368 85.7343 232.356 85.2011 233.421 84.8836C234.485 84.566 235.604 84.4709 236.707 84.6041C237.81 84.7372 238.874 85.0959 239.832 85.6578C240.791 86.2196 241.623 86.9726 242.278 87.87C242.088 87.58 245.968 86.11 246.428 86.14C247.21 86.2189 247.955 86.5104 248.583 86.9829C249.21 87.4553 249.696 88.0907 249.988 88.82C250.566 88.33 251.291 88.0483 252.048 88.02C252.494 88.0752 252.901 88.3007 253.184 88.6492C253.468 88.9978 253.605 89.4423 253.568 89.89C254.07 89.4425 254.67 89.1189 255.319 88.9453C255.969 88.7716 256.65 88.7527 257.308 88.89C260.708 89.63 261.658 95.18 261.648 98.09H68.3578C68.0578 98.07 79.9878 83.84 89.8678 90.33Z" + fill="#082424" + /> + <path d="M249.299 63.98H187.048V70.17H249.299V63.98Z" fill="#082424" /> + <path + d="M246.628 70.1699H189.708V97.5099H246.628V70.1699Z" + fill="#082424" + /> + <path d="M283.819 47.53H254.069V52.35H283.819V47.53Z" fill="#082424" /> + <path + d="M281.669 52.3401H256.219V98.0801H281.669V52.3401Z" + fill="#082424" + /> + <path + d="M164.078 73.6901V68.8501H142.238V73.6901H122.458V79.7901H125.808V98.0901H180.498V79.7901H183.848V73.6901H164.078Z" + fill="#082424" + /> + <path + d="M73.1879 59.22V52H55.9079V59.22H47.9079V52H30.6279V59.22H22.8879V98.09H81.9579V59.22H73.1879Z" + fill="#082424" + /> + <path + d="M115.578 31.3701V25.3601H111.968V19.3401H109.638V13.3301H95.9577V19.3401H93.6277V25.3601H90.0277V31.3701H86.4177V98.0901H119.178V31.3701H115.578Z" + fill="#082424" + /> + <path + d="M102.798 14.33C102.533 14.33 102.279 14.2247 102.091 14.0371C101.903 13.8496 101.798 13.5952 101.798 13.33V1.91003C101.798 1.64482 101.903 1.39046 102.091 1.20293C102.279 1.01539 102.533 0.910034 102.798 0.910034C103.063 0.910034 103.318 1.01539 103.505 1.20293C103.693 1.39046 103.798 1.64482 103.798 1.91003V13.33C103.798 13.5952 103.693 13.8496 103.505 14.0371C103.318 14.2247 103.063 14.33 102.798 14.33Z" + fill="#082424" + /> + <path + d="M179.959 48.8401C179.959 48.8401 180.779 41.3601 186.509 40.9201C190.559 40.6101 191.729 43.6201 191.729 43.6201C191.598 42.1914 191.866 40.7543 192.503 39.469C193.14 38.1836 194.122 37.1005 195.339 36.3401C199.609 33.5901 203.919 34.4601 205.339 38.0901C207.263 37.0072 209.491 36.5916 211.677 36.9082C213.862 37.2248 215.881 38.2556 217.419 39.8401C223.119 45.4601 223.069 48.8401 223.069 48.8401H179.959Z" + fill="#082424" + /> + <path + d="M26.6637 84.6301V73.6901H4.82372V84.6301H0.499809V90.7301L0.499512 98.09H43.0838L43.0837 90.7301H46.4337V84.6301H26.6637Z" + fill="#082424" + /> + <path + d="M293.627 73.6901H236.707V98.09H293.627V73.6901Z" + fill="#082424" + /> + </svg> + ); +} diff --git a/src/commands/breakdown.tsx b/src/commands/breakdown.tsx index fc6308cc9..f69921b73 100644 --- a/src/commands/breakdown.tsx +++ b/src/commands/breakdown.tsx @@ -7,9 +7,11 @@ import Wrapper from '../components/Wrapper'; import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getTankStats from '../core/blitz/getTankStats'; -import getTreeType from '../core/blitz/getTreeType'; -import { tankDefinitions } from '../core/blitzkrieg/definitions/tanks'; import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; +import { + TankDefinition, + tankDefinitions, +} from '../core/blitzkrieg/tankDefinitions'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; import { tankAverages } from '../core/blitzstars/tankAverages'; @@ -36,7 +38,7 @@ import getWN8Percentile from '../core/statistics/getWN8Percentile'; import { CommandRegistryPromisable } from '../events/interactionCreate'; const ROWS_PER_PAGE = 8; -const MAX_PAGES = 10; +const MAX_PAGES = 9; export async function renderBreakdown( { region, id }: ResolvedPlayer, @@ -180,68 +182,68 @@ export async function renderBreakdown( />, ); - await Promise.all( - filteredOrder.map(async (id, index) => { - const tankDefinition = awaitedTankDefinitions[id]; - const current = orderedCurrentStats[index]; - const career = orderedCareerStats[index]; - const currentWN8 = orderedCurrentWN8[index]; - const careerWN8 = orderedCareerWN8[index]; + filteredOrder.forEach((id, index) => { + const tankDefinition = awaitedTankDefinitions[id] as + | TankDefinition + | undefined; + const current = orderedCurrentStats[index]; + const career = orderedCareerStats[index]; + const currentWN8 = orderedCurrentWN8[index]; + const careerWN8 = orderedCareerWN8[index]; - children.push( - <Breakdown.Row - key={id} - type="tank" - tankType={tankDefinition?.type} - treeType={tankDefinition ? await getTreeType(id) : undefined} - title={awaitedTankDefinitions[id].name} - stats={[ - { - title: 'Battles', - current: current.battles.toLocaleString(), - career: career.battles.toLocaleString(), - }, - { - title: 'Winrate', - current: `${(100 * (current.wins / current.battles)).toFixed( - 2, - )}%`, - career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`, - delta: - current.wins / current.battles - career.wins / career.battles, - }, - { - title: 'WN8', - current: - currentWN8 === undefined - ? undefined - : Math.round(currentWN8).toLocaleString(), - career: - careerWN8 === undefined - ? undefined - : Math.round(careerWN8).toLocaleString(), - percentile: - currentWN8 === undefined - ? undefined - : getWN8Percentile(currentWN8), - }, - { - title: 'Damage', - current: Math.round( - current.damage_dealt / current.battles, - ).toLocaleString(), - career: Math.round( - career.damage_dealt / career.battles, - ).toLocaleString(), - delta: - current.damage_dealt / current.battles - - career.damage_dealt / career.battles, - }, - ]} - />, - ); - }), - ); + children.push( + <Breakdown.Row + key={id} + type="tank" + tankType={tankDefinition?.type} + treeType={tankDefinition?.treeType} + title={tankDefinition?.name ?? `Unknown tank ${id}`} + stats={[ + { + title: 'Battles', + current: current.battles.toLocaleString(), + career: career.battles.toLocaleString(), + }, + { + title: 'Winrate', + current: `${(100 * (current.wins / current.battles)).toFixed( + 2, + )}%`, + career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`, + delta: + current.wins / current.battles - career.wins / career.battles, + }, + { + title: 'WN8', + current: + currentWN8 === undefined + ? undefined + : Math.round(currentWN8).toLocaleString(), + career: + careerWN8 === undefined + ? undefined + : Math.round(careerWN8).toLocaleString(), + percentile: + currentWN8 === undefined + ? undefined + : getWN8Percentile(currentWN8), + }, + { + title: 'Damage', + current: Math.round( + current.damage_dealt / current.battles, + ).toLocaleString(), + career: Math.round( + career.damage_dealt / career.battles, + ).toLocaleString(), + delta: + current.damage_dealt / current.battles - + career.damage_dealt / career.battles, + }, + ]} + />, + ); + }); } const pages = chunk( @@ -282,9 +284,7 @@ export async function renderBreakdown( description={`${name} • ${filterDescriptions}`} /> - {filteredOrder.length === 0 && ( - <NoData type={NoDataType.BattlesInPeriod} /> - )} + <NoData type={NoDataType.BattlesInPeriod} /> </Wrapper>, ]; } diff --git a/src/commands/eligible.ts b/src/commands/eligible.ts index 1158b6910..8225885e6 100644 --- a/src/commands/eligible.ts +++ b/src/commands/eligible.ts @@ -1,8 +1,8 @@ import { SlashCommandBuilder } from 'discord.js'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; -import { tankDefinitions } from '../core/blitzkrieg/definitions/tanks'; import getPeriodNow from '../core/blitzkrieg/getPeriodNow'; import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo'; +import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions'; import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; import { tankAverages } from '../core/blitzstars/tankAverages'; import addUsernameChoices from '../core/discord/addUsernameChoices'; diff --git a/src/commands/ownedTanks.tsx b/src/commands/ownedTanks.tsx index e92e628bf..8842c9ff0 100644 --- a/src/commands/ownedTanks.tsx +++ b/src/commands/ownedTanks.tsx @@ -7,14 +7,13 @@ import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getTankStats from '../core/blitz/getTankStats'; -import getTreeType from '../core/blitz/getTreeType'; +import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { - BlitzkriegTankDefinition, TIER_ROMAN_NUMERALS, + TankDefinition, Tier, tankDefinitions, -} from '../core/blitzkrieg/definitions/tanks'; -import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; +} from '../core/blitzkrieg/tankDefinitions'; import { tankIcon } from '../core/blitzkrieg/tankIcon'; import addTierChoices from '../core/discord/addTierChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices'; @@ -47,7 +46,7 @@ export const ownedTanksCommand: CommandRegistry = { ) ).filter((tank) => tank.tankDefinitions?.tier === tier); const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']); - const groupedTanks: Record<string, BlitzkriegTankDefinition[]> = {}; + const groupedTanks: Record<string, TankDefinition[]> = {}; const nations: string[] = []; filteredTanks.forEach((tank) => { @@ -98,7 +97,7 @@ export const ownedTanksCommand: CommandRegistry = { name={tank.name} tankType={tank.type} image={tankIcon(tank.id)} - treeType={await getTreeType(tank.id)} + treeType={tank.treeType} /> )), )} @@ -111,7 +110,7 @@ export const ownedTanksCommand: CommandRegistry = { name={tank.name} tankType={tank.type} image={tankIcon(tank.id)} - treeType={await getTreeType(tank.id)} + treeType={tank.treeType} /> )), )} diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index ecbdc1453..479583f0d 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -22,12 +22,13 @@ import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLat import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard'; import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard'; -import { UserError } from '../core/blitzkrieg/userError'; import addRegionChoices from '../core/discord/addRegionChoices'; +import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; import embedNegative from '../core/discord/embedNegative'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { UserError } from '../hooks/userError'; export interface RatingsPlayer { spa_id: number; @@ -132,6 +133,7 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>( function addSubcommands(option: SlashCommandSubcommandBuilder) { return option + .addStringOption(addUsernameChoices) .addIntegerOption((option) => option .setName('limit') diff --git a/src/commands/searchTanks.ts b/src/commands/searchTanks.ts index cb2f1e089..e06414c46 100644 --- a/src/commands/searchTanks.ts +++ b/src/commands/searchTanks.ts @@ -1,7 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; import { go } from 'fuzzysort'; import markdownEscape from 'markdown-escape'; -import { tankNamesDiacritics } from '../core/blitzkrieg/definitions/tanks'; +import { tankNames } from '../core/blitzkrieg/tankDefinitions'; import addTankChoices from '../core/discord/addTankChoices'; import embedInfo from '../core/discord/embedInfo'; import { CommandRegistry } from '../events/interactionCreate'; @@ -25,7 +25,7 @@ export const searchTanksCommand: CommandRegistry = { async handler(interaction) { const tank = interaction.options.getString('tank')!; const limit = interaction.options.getInteger('limit') ?? 25; - const results = go(tank, await tankNamesDiacritics, { + const results = go(tank, await tankNames, { limit, keys: ['combined'], }).map((result) => result.obj.original); diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx new file mode 100644 index 000000000..816799dfc --- /dev/null +++ b/src/components/ArmorMesh/components/Armor/index.tsx @@ -0,0 +1,459 @@ +import { MeshProps, useFrame, useThree } from '@react-three/fiber'; +import { memo, useEffect, useRef } from 'react'; +import { + IUniform, + Mesh, + Quaternion, + Raycaster, + ShaderMaterial, + Vector2, + Vector3, +} from 'three'; +import { degToRad } from 'three/src/math/MathUtils'; +import { canRicochet } from '../../../../core/blitz/canRicochet'; +import { canSplash } from '../../../../core/blitz/canSplash'; +import { isExplosive } from '../../../../core/blitz/isExplosive'; +import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; +import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment'; +import { useAwait } from '../../../../hooks/useAwait'; +import { useDuel } from '../../../../stores/duel'; +import { + ArmorPiercingLayer, + Shot, + mutateTankopediaTemporary, + useTankopediaPersistent, +} from '../../../../stores/tankopedia'; +import { externalModuleMaskRenderTarget } from '../ExternalModuleMask'; +import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; + +interface ArmorMeshProps extends MeshProps { + thickness: number; + maxThickness: number; +} + +export type ArmorMeshUserData = { + type: 'coreArmor' | 'spacedArmor' | 'externalModule'; + thickness: number; +}; + +export const ArmorMesh = memo<ArmorMeshProps>( + ({ thickness, maxThickness, ...props }) => { + const camera = useThree((state) => state.camera); + const initialShell = useDuel.getState().antagonist!.shell; + const initialTankopedia = useTankopediaPersistent.getState(); + const material = useRef<ShaderMaterial>(null); + const explosionCapable = isExplosive(initialShell.type); + const ricochetCapable = canRicochet(initialShell.type); + const cameraNormal = new Vector3(); + const shellNormal = new Vector3(); + const surfaceNormal = new Vector3(); + const mesh = useRef<Mesh>(null); + const scene = useThree((state) => state.scene); + const hasCalibratedShells = useAwait(hasEquipment(103, true)); + const hasEnhancedArmor = useAwait(hasEquipment(110)); + const wireframe = initialTankopedia.model.visual.wireframe; + + useEffect(() => { + async function updateQuickEquipments() { + const shell = useDuel.getState().antagonist!.shell; + const nearPenetration = resolveNearPenetration(shell.penetration); + const hasCalibratedShells = await hasEquipment(103, true); + const hasEnhancedArmor = await hasEquipment(110); + + if (material.current) { + material.current.uniforms.thickness.value = + thickness * (hasEnhancedArmor ? 1.04 : 1); + material.current.uniforms.penetration.value = + nearPenetration * + (hasCalibratedShells ? (isExplosive(shell.type) ? 1.1 : 1.05) : 1); + } + } + + function updateShellProperties() { + const shell = useDuel.getState().antagonist!.shell; + + if (material.current) { + material.current.uniforms.canRicochet.value = canRicochet(shell.type); + material.current.uniforms.isExplosive.value = isExplosive(shell.type); + material.current.uniforms.canSplash.value = canSplash(shell.type); + material.current.uniforms.caliber.value = shell.caliber; + material.current.uniforms.ricochetAngle.value = degToRad( + shell.ricochet ?? 90, + ); + material.current.uniforms.damage.value = shell.damage.armor; + material.current.uniforms.explosionRadius.value = + shell.explosionRadius ?? 0; + } + + updateQuickEquipments(); + } + + const unsubscribes = [ + useDuel.subscribe( + (state) => state.protagonist!.equipment, + updateQuickEquipments, + ), + useDuel.subscribe( + (state) => state.antagonist!.equipment, + updateQuickEquipments, + ), + useDuel.subscribe( + (state) => state.antagonist!.shell, + updateShellProperties, + ), + useTankopediaPersistent.subscribe( + (state) => state.model.visual.greenPenetration, + (greenPenetration) => { + if (material.current) { + material.current.uniforms.greenPenetration.value = + greenPenetration; + } + }, + ), + useTankopediaPersistent.subscribe( + (state) => state.model.visual.wireframe, + (wireframe) => { + if (material.current) { + material.current.wireframe = wireframe; + material.current.uniforms.wireframe.value = wireframe; + } + }, + ), + ]; + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; + }, []); + + useFrame(({ gl, camera }) => { + if (material.current) { + gl.getSize(material.current.uniforms.resolution.value); + + ( + material.current.uniforms.resolution as IUniform<Vector2> + ).value.multiplyScalar(gl.getPixelRatio()); + + material.current.uniforms.externalModuleMask.value = + externalModuleMaskRenderTarget.texture; + material.current.uniforms.spacedArmorDepth.value = + spacedArmorDepthRenderTarget.depthTexture; + material.current.uniforms.spacedArmorMask.value = + spacedArmorDepthRenderTarget.texture; + material.current.uniforms.projectionMatrixInverse.value = + camera.projectionMatrixInverse; + } + }); + + return ( + <> + <mesh {...props} renderOrder={0}> + <meshBasicMaterial colorWrite={false} /> + </mesh> + + <mesh + {...props} + renderOrder={1} + userData={ + { + type: 'coreArmor', + thickness, + } satisfies ArmorMeshUserData + } + ref={mesh} + onClick={async (event) => { + // if (useTankopediaTemporary.getState().mode !== 'armor') return; + + shellNormal.copy( + cameraNormal.copy(camera.position).sub(event.point).normalize(), + ); + surfaceNormal + .copy(event.normal!) + .applyQuaternion( + event.object.getWorldQuaternion(new Quaternion()), + ); + const angle = Math.acos(surfaceNormal.dot(shellNormal)); + const shell = useDuel.getState().antagonist!.shell; + const hasCalibratedShells = await hasEquipment(103, true); + const resolvedPenetration = + resolveNearPenetration(shell.penetration) * + (hasCalibratedShells + ? isExplosive(shell.type) + ? 1.1 + : 1.05 + : 1); + const indexedObjects = new Set<ArmorMeshUserData['type']>(); + const intersections = event.intersections + .filter(({ object }) => Boolean(object.userData.type)) + .map((event) => { + const typedEvent = event as typeof event & { + object: (typeof event)['object'] & { + userData: ArmorMeshUserData; + }; + }; + + return typedEvent; + }) + .filter(({ object }) => { + if (indexedObjects.has(object.userData.type)) return false; + indexedObjects.add(object.userData.type); + return true; + }); + const intersectionsTillFirstCoreArmor = intersections.slice( + 0, + intersections.findIndex( + ({ object }) => object.userData.type === 'coreArmor', + ) + 1, + ); + const coreArmorMesh = + intersectionsTillFirstCoreArmor.at(-1)!.object; + const hasEnhancedArmor = await hasEquipment(110); + const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1; + + if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return; + + let hasRicochet = false; + const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor + .map((event, index) => { + if (hasRicochet) return null; + + switch (event.object.userData.type) { + case 'externalModule': { + return { + nominal: + event.object.userData.thickness * thicknessCoefficient, + angled: + event.object.userData.thickness * thicknessCoefficient, + type: 'external', + distance: event.distance, + ricochet: false, + } satisfies ArmorPiercingLayer; + } + + case 'spacedArmor': + case 'coreArmor': { + const threeCalibersRule = + shell.caliber > + 3 * + (event.object.userData.thickness * + thicknessCoefficient); + + if ( + index === 0 && + canRicochet(shell.type) && + !threeCalibersRule && + angle >= degToRad(shell.ricochet!) + ) { + hasRicochet = true; + } + + const twoCalibersRule = + shell.caliber > + 2 * + (event.object.userData.thickness * + thicknessCoefficient); + const normalization = + twoCalibersRule && event.object.userData.thickness > 0 + ? ((shell.normalization ?? 0) * 1.4 * shell.caliber) / + (2 * + event.object.userData.thickness * + thicknessCoefficient) + : shell.normalization ?? 0; + const angled = + (event.object.userData.thickness * thicknessCoefficient) / + Math.cos(angle - degToRad(normalization)); + + return { + ricochet: hasRicochet, + nominal: + event.object.userData.thickness * thicknessCoefficient, + angled, + type: + event.object.userData.type === 'coreArmor' + ? 'core' + : 'spaced', + distance: event.distance, + } satisfies ArmorPiercingLayer; + } + } + }) + .filter(Boolean); + const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => { + if (aIndex === armorPiercingLayersRaw.length - 1) return null; + const layerB = armorPiercingLayersRaw[aIndex + 1]; + + return { + type: 'gap', + gap: Math.abs(layerB.distance - layerA.distance), + } satisfies ArmorPiercingLayer; + }); + const armorPiercingLayers: ArmorPiercingLayer[] = []; + + armorPiercingLayersRaw.forEach((layer, index) => { + const gap = gaps[index]; + + armorPiercingLayers.push(layer); + if (gap) armorPiercingLayers.push(gap); + }); + + let remainingPenetration = resolvedPenetration; + const explosiveCapable = isExplosive(shell.type); + + armorPiercingLayers.forEach((layer) => { + if (layer.type !== 'gap') { + if (layer.ricochet) { + remainingPenetration *= 0.75; + } else { + remainingPenetration -= layer.angled; + } + } else if (explosiveCapable) { + // there is a 50% penetration loss per meter for HE based shells + remainingPenetration -= 0.5 * remainingPenetration * layer.gap; + } + + remainingPenetration = Math.max(0, remainingPenetration); + }); + + const hasPenetrated = remainingPenetration > 0; + let ricochet: Shot['ricochet'] = undefined; + + if (hasRicochet) { + const raycaster = new Raycaster(); + const bounceNormal = shellNormal + .clone() + .multiplyScalar(-1) + .sub( + surfaceNormal + .clone() + .multiplyScalar( + 2 * + surfaceNormal.dot( + shellNormal.clone().multiplyScalar(-1), + ), + ), + ); + raycaster.set(event.point, bounceNormal); + const intersections = raycaster + .intersectObjects(scene.children, true) + .filter( + (intersection) => + typeof intersection.object.userData?.type === 'string', + ); + const indexOfCoreArmor = intersections.findIndex( + ({ object }) => object.userData.type === 'coreArmor', + ); + const intersectionsTillFirstCoreArmor = intersections.slice( + 0, + indexOfCoreArmor + 1, + ); + + if (intersectionsTillFirstCoreArmor.length > 0) { + intersectionsTillFirstCoreArmor.forEach((intersection) => { + switch (intersection.object.userData.type) { + case 'externalModule': { + remainingPenetration -= + intersection.object.userData.thickness; + break; + } + + case 'spacedArmor': + case 'coreArmor': { + const angle = bounceNormal.angleTo( + intersection.object.position.clone().sub(event.point), + ); + const twoCalibersRule = + shell.caliber > + 2 * + (intersection.object.userData.thickness * + thicknessCoefficient); + const normalization = twoCalibersRule + ? ((shell.normalization ?? 0) * 1.4 * shell.caliber) / + (2 * + intersection.object.userData.thickness * + thicknessCoefficient) + : shell.normalization ?? 0; + const angled = + (intersection.object.userData.thickness * + thicknessCoefficient) / + Math.cos(angle - degToRad(normalization)); + + remainingPenetration -= angled; + } + } + }); + + ricochet = { + point: intersectionsTillFirstCoreArmor + .at(-1)! + .point.toArray(), + distance: intersectionsTillFirstCoreArmor.at(-1)!.distance, + penetration: remainingPenetration > 0, + }; + } + } + + mutateTankopediaTemporary((draft) => { + draft.shot = { + type: hasRicochet + ? 'ricochet' + : hasPenetrated + ? 'penetration' + : 'block', + point: event.point.toArray(), + thicknesses: armorPiercingLayers, + shellNormal: shellNormal.toArray(), + surfaceNormal: surfaceNormal.toArray(), + angle, + ricochet, + }; + }); + }} + > + <shaderMaterial + wireframe={wireframe} + ref={material} + fragmentShader={fragmentShader} + vertexShader={vertexShader} + transparent + depthWrite={false} + uniforms={{ + resolution: { value: new Vector2() }, + externalModuleMask: { value: null }, + spacedArmorDepth: { value: null }, + spacedArmorMask: { value: null }, + projectionMatrixInverse: { value: null }, + zNear: { value: camera.near }, + zFar: { value: camera.far }, + greenPenetration: { + value: initialTankopedia.model.visual.greenPenetration, + }, + maxThickness: { value: maxThickness }, + isExplosive: { value: explosionCapable }, + canRicochet: { value: ricochetCapable }, + canSplash: { value: canSplash(initialShell.type) }, + thickness: { + value: thickness * (hasEnhancedArmor ? 1.04 : 1), + }, + penetration: { + value: + resolveNearPenetration(initialShell.penetration) * + (hasCalibratedShells ? (explosionCapable ? 1.1 : 1.05) : 1), + }, + caliber: { value: initialShell.caliber }, + ricochetAngle: { value: degToRad(initialShell.ricochet ?? 90) }, + normalization: { + value: degToRad(initialShell.normalization ?? 0), + }, + damage: { value: initialShell.damage.armor }, + explosionRadius: { value: initialShell.explosionRadius ?? 0 }, + wireframe: { value: wireframe }, + }} + /> + </mesh> + </> + ); + }, + () => true, +); diff --git a/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl new file mode 100644 index 000000000..1bb58ff26 --- /dev/null +++ b/src/components/ArmorMesh/components/Armor/shaders/fragment.glsl @@ -0,0 +1,120 @@ +#define PI 3.1415926535897932384626433832795 + +varying vec3 vViewPosition; +varying vec3 vNormal; +uniform bool isExplosive; +uniform bool canRicochet; +uniform bool canSplash; +uniform float thickness; +uniform float penetration; +uniform float caliber; +uniform float ricochetAngle; +uniform float normalization; +uniform sampler2D externalModuleMask; +uniform highp sampler2D spacedArmorDepth; +uniform sampler2D spacedArmorMask; +uniform vec2 resolution; +varying mat4 vProjectionMatrix; +uniform float maxThickness; +uniform bool greenPenetration; +uniform float damage; +uniform float explosionRadius; +uniform bool wireframe; + +float depthToDistance(float depth) { + mat4 projectionMatrixInverse = inverse(vProjectionMatrix); + vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); + vec4 eyePosition = projectionMatrixInverse * clipPosition; + vec3 NDCPosition = eyePosition.xyz / eyePosition.w; + float worldDistance = length(NDCPosition); + + return worldDistance; +} + +float angleTo(vec3 a, vec3 b) { + return acos(dot(a, b) / (length(a) * length(b))); +} + +void main() { + float penetrationChance = -1.0; + float splashChance = -1.0; + + vec2 screenCoordinates = gl_FragCoord.xy / resolution; + vec4 externalModuleMaskColor = texture2D(externalModuleMask, screenCoordinates); + vec4 spacedArmorMaskColor = texture2D(spacedArmorMask, screenCoordinates); + bool isUnderExternalModule = externalModuleMaskColor.r == 1.0; + bool isUnderSpacedArmor = spacedArmorMaskColor.a == 1.0; + float spacedArmorAngle = spacedArmorMaskColor.r * (PI / 2.0); + + float coreArmorAngle = angleTo(vNormal, -vViewPosition); + bool coreArmorThreeCalibersRule = caliber > 3.0 * thickness; + float spacedArmorNominalThickness = spacedArmorMaskColor.g * maxThickness; + bool spacedArmorThreeCalibersRule = isUnderSpacedArmor && caliber > 3.0 * spacedArmorNominalThickness; + bool hasRicochetedSpacedArmor = isUnderSpacedArmor && !spacedArmorThreeCalibersRule && spacedArmorAngle >= ricochetAngle; + bool hasRicochetedCoreArmor = !hasRicochetedSpacedArmor && !isUnderExternalModule && canRicochet && !coreArmorThreeCalibersRule && coreArmorAngle >= ricochetAngle; + + if (hasRicochetedCoreArmor || hasRicochetedSpacedArmor) { + penetrationChance = 0.0; + splashChance = 0.0; + } else { + // time to chip away at the penetration, one case at a time + float remainingPenetration = penetration; + + if (isUnderExternalModule) { + // external modules don't care about angle, they'll reduce penetration by their thickness + float externalModuleThickness = externalModuleMaskColor.g * maxThickness; + remainingPenetration -= externalModuleThickness; + } + + if (isUnderSpacedArmor) { + // spaced armor on the other hand, does care about angle + bool spacedArmorTwoCalibersRule = caliber > 2.0 * spacedArmorNominalThickness && spacedArmorNominalThickness > 0.0; + float finalSpacedArmorNormalization = spacedArmorTwoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * spacedArmorNominalThickness) : normalization; + float finalSpacedArmorThickness = spacedArmorNominalThickness / cos(spacedArmorAngle - finalSpacedArmorNormalization); + remainingPenetration -= finalSpacedArmorThickness; + } + remainingPenetration = max(remainingPenetration, 0.0); + + float spacedArmorDepth = texture2D(spacedArmorDepth, screenCoordinates).r; + float spacedArmorDistance = depthToDistance(spacedArmorDepth); + float coreArmorDistance = depthToDistance(gl_FragCoord.z); + float distanceFromSpacedArmor = abs(coreArmorDistance - spacedArmorDistance); + if (isExplosive) { + // there is a 50% penetration loss per meter for HE based shells + remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor; + } + + bool coreArmorTwoCalibersRule = caliber > 2.0 * thickness && thickness > 0.0; + float finalNormalization = coreArmorTwoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * thickness) : normalization; + float finalCoreArmorThickness = thickness / cos(coreArmorAngle - finalNormalization); + float deltaPenetration = finalCoreArmorThickness - remainingPenetration; + float randomRadius = remainingPenetration * 0.05; + + if (deltaPenetration > randomRadius) { + // even with a 5% buff to the penetration, can't penetrate + penetrationChance = 0.0; + } else if (deltaPenetration < -randomRadius) { + // even with a 5% nerf, can penetrate + penetrationChance = 1.0; + } else { + // transition area + penetrationChance = max(1.0 - (deltaPenetration + randomRadius) / (2.0 * randomRadius), 0.0); + } + + if (canSplash) { + // only allow splasing if the damage equation deals more than 0 damage + float nominalArmorThickness = finalCoreArmorThickness - remainingPenetration; + float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness; + splashChance = finalDamage > 0.0 ? 1.0 : 0.0; + } else { + splashChance = 0.0; + } + } + + vec3 color = vec3(1.0, splashChance * 0.392, 0.0); + if (greenPenetration || wireframe) { + gl_FragColor = vec4(mix(color, vec3(0.0, 1.0, 0.0), penetrationChance), wireframe ? 1.0 : 0.5); + } else { + gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5); + } +} \ No newline at end of file diff --git a/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl new file mode 100644 index 000000000..0e4d1407f --- /dev/null +++ b/src/components/ArmorMesh/components/Armor/shaders/vertex.glsl @@ -0,0 +1,13 @@ +varying vec3 vNormal; +varying vec3 vViewPosition; +varying mat4 vProjectionMatrix; + +void main() { + vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); + + vViewPosition = vViewPosition4.xyz; + vProjectionMatrix = projectionMatrix; + vNormal = normalMatrix * normal; + + gl_Position = projectionMatrix * vViewPosition4; +} \ No newline at end of file diff --git a/src/components/ArmorMesh/components/ExternalModuleMask.tsx b/src/components/ArmorMesh/components/ExternalModuleMask.tsx new file mode 100644 index 000000000..1d5aed6bb --- /dev/null +++ b/src/components/ArmorMesh/components/ExternalModuleMask.tsx @@ -0,0 +1,52 @@ +import { Color, MeshBasicMaterial, Object3D, WebGLRenderTarget } from 'three'; +import { jsxTree } from '../../../core/blitzkrieg/jsxTree'; +import { ArmorMeshUserData } from './Armor'; + +export const externalModuleMaskRenderTarget = new WebGLRenderTarget(); + +type ArmorMeshExternalModuleMaskProps = { + ornamental?: boolean; + node: Object3D; +} & ( + | { + exclude?: false; + maxThickness: number; + thickness: number; + } + | { + exclude: true; + } +); + +const excludeMaterial = new MeshBasicMaterial({ + colorWrite: false, +}); + +export function ArmorMeshExternalModuleMask({ + ornamental = false, + node, + ...props +}: ArmorMeshExternalModuleMaskProps) { + return ( + <> + {props.exclude && + !ornamental && + jsxTree(node, { renderOrder: 0, material: excludeMaterial })} + + {!props.exclude && + jsxTree(node, { + renderOrder: 1, + onClick: ornamental ? () => {} : undefined, + userData: { + type: 'externalModule', + thickness: props.thickness, + } satisfies ArmorMeshUserData, + material: new MeshBasicMaterial({ + color: new Color(1, props.thickness / props.maxThickness, 0), + depthWrite: false, + colorWrite: !ornamental, + }), + })} + </> + ); +} diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx b/src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx new file mode 100644 index 000000000..9d61e0796 --- /dev/null +++ b/src/components/ArmorMesh/components/SpacedArmorDepth/index.tsx @@ -0,0 +1,72 @@ +import { + MeshBasicMaterial, + Object3D, + ShaderMaterial, + WebGLRenderTarget, +} from 'three'; +import { jsxTree } from '../../../../core/blitzkrieg/jsxTree'; +import { ArmorMeshUserData } from '../Armor'; +import fragmentShader from './shaders/fragment.glsl'; +import vertexShader from './shaders/vertex.glsl'; + +export const spacedArmorDepthRenderTarget = new WebGLRenderTarget(); + +type ArmorMeshSpacedArmorDepthProps = { + ornamental?: boolean; + node: Object3D; +} & ( + | { + include?: false; + isExternalModule: boolean; + } + | { + include: true; + thickness: number; + maxThickness: number; + } +); + +const excludeMaterial = new MeshBasicMaterial({ + colorWrite: false, +}); +const includeMaterial = new MeshBasicMaterial({ + colorWrite: false, + depthWrite: false, +}); + +export function ArmorMeshSpacedArmorDepth({ + ornamental = false, + node, + ...props +}: ArmorMeshSpacedArmorDepthProps) { + return ( + <> + {!props.include && + !ornamental && + jsxTree(node, { + renderOrder: props.isExternalModule ? 2 : 0, + material: excludeMaterial, + })} + + {props.include && + jsxTree(node, { + renderOrder: 1, + onClick: ornamental ? () => {} : undefined, + userData: { + type: 'spacedArmor', + thickness: props.thickness, + } satisfies ArmorMeshUserData, + material: ornamental + ? includeMaterial + : new ShaderMaterial({ + fragmentShader, + vertexShader, + uniforms: { + thickness: { value: props.thickness }, + maxThickness: { value: props.maxThickness }, + }, + }), + })} + </> + ); +} diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl b/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl new file mode 100644 index 000000000..6fe90ce36 --- /dev/null +++ b/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/fragment.glsl @@ -0,0 +1,11 @@ +#define PI 3.1415926535897932384626433832795 + +varying vec3 vViewPosition; +varying vec3 vNormal; +uniform float thickness; +uniform float maxThickness; + +void main() { + float angle = acos(dot(vNormal, -normalize(vViewPosition))); + gl_FragColor = vec4(angle / (PI / 2.0), thickness / maxThickness, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl b/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl new file mode 100644 index 000000000..d57a82f32 --- /dev/null +++ b/src/components/ArmorMesh/components/SpacedArmorDepth/shaders/vertex.glsl @@ -0,0 +1,10 @@ +varying vec3 vViewPosition; +varying vec3 vNormal; + +void main() { + vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0); + vViewPosition = vViewPosition4.xyz; + vNormal = normalMatrix * normal; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); +} \ No newline at end of file diff --git a/src/components/ArmorMesh/index.tsx b/src/components/ArmorMesh/index.tsx new file mode 100644 index 000000000..b64eb57f2 --- /dev/null +++ b/src/components/ArmorMesh/index.tsx @@ -0,0 +1,2 @@ +export * from './components/Armor'; +export * from './components/ExternalModuleMask'; diff --git a/src/components/Breakdown/components/Row.tsx b/src/components/Breakdown/components/Row.tsx index 246609a47..b22f1cb86 100644 --- a/src/components/Breakdown/components/Row.tsx +++ b/src/components/Breakdown/components/Row.tsx @@ -5,7 +5,7 @@ import { GrayColor, PALETTES, } from '../../../constants/radixColors'; -import { TREE_TYPE_ICONS, TankType, TreeTypeEnum } from '../../Tanks'; +import { TREE_TYPE_ICONS, TankType, TreeType } from '../../Tanks'; import { RowStat } from './RowStat'; export interface RowStatItem { @@ -21,7 +21,7 @@ interface RowProps { naked?: boolean; title: string; minimized?: boolean; - treeType?: TreeTypeEnum; + treeType?: TreeType; tankType?: TankType; color?: AccentColor | GrayColor; stats: (RowStatItem | undefined)[]; @@ -44,7 +44,7 @@ export function Row({ <div className="session-tracker-row" style={{ - maxWidth: naked ? 600 : undefined, + maxWidth: naked ? 600 : Infinity, display: 'flex', flexDirection: 'column', borderRadius: 4, @@ -68,7 +68,7 @@ export function Row({ > {type === 'tank' && tankType !== undefined && ( <img - src={TREE_TYPE_ICONS[treeType ?? TreeTypeEnum.TechTree][tankType]} + src={TREE_TYPE_ICONS[treeType ?? 'researchable'][tankType]} style={{ width: 16, height: 16 }} /> )} @@ -77,9 +77,9 @@ export function Row({ style={{ color: type === 'tank' - ? treeType === TreeTypeEnum.Collector + ? treeType === 'collector' ? theme.textLowContrast_blue - : treeType === TreeTypeEnum.Premium + : treeType === 'premium' ? theme.textLowContrast_amber : theme.textHighContrast : theme.textLowContrast, diff --git a/src/components/HeadsUpDisplay.tsx b/src/components/HeadsUpDisplay.tsx new file mode 100644 index 000000000..b64759a1f --- /dev/null +++ b/src/components/HeadsUpDisplay.tsx @@ -0,0 +1,53 @@ +import { createPortal, useFrame, useThree } from '@react-three/fiber'; +import { isInteger } from 'lodash'; +import { ReactNode } from 'react'; +import { Scene } from 'three'; + +interface HeadsUpDisplayProps { + priority?: number; + children: ReactNode; +} +/** + * Renders `children` above the default scene, even if the priority is a + * negative number. + */ +export function HeadsUpDisplay({ + priority = 1, + children, +}: HeadsUpDisplayProps) { + const { gl: renderer, scene: defaultScene, camera } = useThree(); + const scene = new Scene(); + const portal = createPortal(children, scene, { + events: { priority: priority + 1 }, + }); + + useFrame(() => { + if (priority === 1) { + renderer.clear(); + renderer.render(defaultScene, camera); + } + + renderer.clearDepth(); + renderer.render(scene, camera); + }, priority); + + renderer.autoClear = false; + if (!isInteger(priority)) { + throw new TypeError('Priority property must be an integer'); + } + + return portal; +} + +/** + * Use this component if the default scene disappears on usage of the + * HeadsUpDisplay; caused because there are not HeadsUpDisplay components + * with `1` priority. + */ +export function RenderDefault() { + const { gl: renderer, scene, camera } = useThree(); + + useFrame(() => renderer.render(scene, camera)); + + return null; +} diff --git a/src/components/InfiniteGridHelper.tsx b/src/components/InfiniteGridHelper.tsx new file mode 100644 index 000000000..f6b4dc298 --- /dev/null +++ b/src/components/InfiniteGridHelper.tsx @@ -0,0 +1,76 @@ +import { MeshProps } from '@react-three/fiber'; +import { forwardRef } from 'react'; +import { DoubleSide, Mesh } from 'three'; + +interface InfiniteGridHelperProps extends MeshProps { + size1?: number; + size2?: number; + color?: THREE.Color; + distance?: number; +} +/** + * Thanks, Fyrestar, for [THREE.InfiniteGridHelper](https://github.com/Fyrestar/THREE.InfiniteGridHelper/)! + */ +const InfiniteGridHelper = forwardRef<Mesh, InfiniteGridHelperProps>( + ({ size1, size2, color, distance, ...props }, ref) => { + return ( + <mesh {...props} ref={ref} frustumCulled={false}> + <shaderMaterial + args={[ + { + side: DoubleSide, + uniforms: { + uSize1: { value: size1 }, + uSize2: { value: size2 }, + uColor: { value: color }, + uDistance: { value: distance }, + }, + extensions: { derivatives: true }, + transparent: true, + + vertexShader: ` + varying vec3 worldPosition; + uniform float uDistance; + + void main() { + vec3 pos = position.xzy * uDistance; + worldPosition = pos; + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } + `, + + fragmentShader: ` + varying vec3 worldPosition; + uniform float uSize1; + uniform float uSize2; + uniform vec3 uColor; + uniform float uDistance; + vec2 origin = vec2(0.0, 0.0); + + float getGrid(float size) { + vec2 r = worldPosition.xz / size; + vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r); + float line = min(grid.x, grid.y); + + return (1.0 - min(line, 1.0)) * max(0.0, min(1.0, cameraPosition.y / 5.0)); + } + + void main() { + float d = 1.0 - min(distance(origin, worldPosition.xz) / uDistance, 1.0); + float g1 = getGrid(uSize1); + float g2 = getGrid(uSize2); + gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 10.0)); + gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2); + + if ( gl_FragColor.a <= 0.0 ) discard; + } + `, + }, + ]} + /> + <planeGeometry /> + </mesh> + ); + }, +); +export default InfiniteGridHelper; diff --git a/src/components/ModuleButtons/ConsumableButton.tsx b/src/components/ModuleButtons/ConsumableButton.tsx new file mode 100644 index 000000000..3658fa0e1 --- /dev/null +++ b/src/components/ModuleButtons/ConsumableButton.tsx @@ -0,0 +1,62 @@ +import { ClockIcon, ReloadIcon } from '@radix-ui/react-icons'; +import { Flex, Text } from '@radix-ui/themes'; +import { asset } from '../../core/blitzkrieg/asset'; +import { useDelta } from '../../hooks/useDelta'; +import { GenericTankComponentButton } from './GenericTankComponentButton'; +import { TankComponentButtonProps } from './TankComponentButton'; + +interface ConsumableButtonProps extends TankComponentButtonProps { + consumable: number; + duration?: number; + cooldown: number; +} + +export function ConsumableButton({ + consumable, + cooldown, + duration, + ...props +}: ConsumableButtonProps) { + const cooldownDelta = useDelta(cooldown); + const durationDelta = useDelta(duration ?? 0); + + return ( + <GenericTankComponentButton + icon={asset(`icons/consumables/${consumable}.webp`)} + {...props} + > + <Flex direction="column" style={{ transform: 'translateX(-10px)' }}> + {duration && ( + <Text + color={ + durationDelta < 0 + ? 'green' + : durationDelta > 0 + ? 'tomato' + : undefined + } + > + <Flex align="center" gap="1" style={{ marginBottom: -4 }}> + <ClockIcon width={12} height={12} /> + {Math.round(duration)}s + </Flex> + </Text> + )} + <Text + color={ + cooldownDelta < 0 + ? 'green' + : cooldownDelta > 0 + ? 'tomato' + : undefined + } + > + <Flex align="center" gap="1"> + <ReloadIcon width={12} height={12} /> + {Math.round(cooldown)}s + </Flex> + </Text> + </Flex> + </GenericTankComponentButton> + ); +} diff --git a/src/components/ModuleButtons/EquipmentButton.tsx b/src/components/ModuleButtons/EquipmentButton.tsx new file mode 100644 index 000000000..d9da23176 --- /dev/null +++ b/src/components/ModuleButtons/EquipmentButton.tsx @@ -0,0 +1,16 @@ +import { asset } from '../../core/blitzkrieg/asset'; +import { GenericTankComponentButton } from './GenericTankComponentButton'; +import { TankComponentButtonProps } from './TankComponentButton'; + +interface EquipmentButtonProps extends TankComponentButtonProps { + equipment: number; +} + +export function EquipmentButton({ equipment, ...props }: EquipmentButtonProps) { + return ( + <GenericTankComponentButton + icon={asset(`icons/equipment/${equipment}.webp`)} + {...props} + /> + ); +} diff --git a/src/components/ModuleButtons/GenericTankComponentButton.tsx b/src/components/ModuleButtons/GenericTankComponentButton.tsx new file mode 100644 index 000000000..a8057673c --- /dev/null +++ b/src/components/ModuleButtons/GenericTankComponentButton.tsx @@ -0,0 +1,47 @@ +import { HTMLAttributes } from 'react'; +import { + TankComponentButton, + TankComponentButtonProps, +} from './TankComponentButton'; + +interface GenericTankComponentButtonProps extends TankComponentButtonProps { + icon: string; + iconStyles?: HTMLAttributes<HTMLImageElement>['style']; +} + +export function GenericTankComponentButton({ + icon, + iconStyles, + children, + ...props +}: GenericTankComponentButtonProps) { + return ( + <TankComponentButton {...props}> + <div + style={{ + width: 48, + height: 40, + position: 'relative', + }} + > + <img + draggable={false} + src={icon} + style={{ + objectFit: 'contain', + width: 32, + height: 32, + position: 'absolute', + opacity: props.disabled ? 0.5 : 1, + transform: 'translate(-50%, -50%) scale(0.75)', + top: '50%', + left: '50%', + ...iconStyles, + }} + /> + </div> + + {children} + </TankComponentButton> + ); +} diff --git a/src/components/ModuleButtons/ModuleButton.tsx b/src/components/ModuleButtons/ModuleButton.tsx new file mode 100644 index 000000000..a1223c597 --- /dev/null +++ b/src/components/ModuleButtons/ModuleButton.tsx @@ -0,0 +1,21 @@ +import { asset } from '../../core/blitzkrieg/asset'; +import { GenericTankComponentButton } from './GenericTankComponentButton'; +import { TankComponentButtonProps } from './TankComponentButton'; + +interface ModuleProps extends TankComponentButtonProps { + module: string; +} + +export function ModuleButton({ module, ...props }: ModuleProps) { + return ( + <GenericTankComponentButton + icon={asset(`icons/modules/${module}.webp`)} + iconStyles={{ + top: '50%', + left: '50%', + transform: 'translate(calc(-50% + 2px), calc(-50% + 2px))', + }} + {...props} + /> + ); +} diff --git a/src/components/ModuleButtons/ProvisionButton.tsx b/src/components/ModuleButtons/ProvisionButton.tsx new file mode 100644 index 000000000..a8cf07eb6 --- /dev/null +++ b/src/components/ModuleButtons/ProvisionButton.tsx @@ -0,0 +1,16 @@ +import { asset } from '../../core/blitzkrieg/asset'; +import { GenericTankComponentButton } from './GenericTankComponentButton'; +import { TankComponentButtonProps } from './TankComponentButton'; + +interface ProvisionButtonProps extends TankComponentButtonProps { + provision: number; +} + +export function ProvisionButton({ provision, ...props }: ProvisionButtonProps) { + return ( + <GenericTankComponentButton + icon={asset(`icons/provisions/${provision}.webp`)} + {...props} + /> + ); +} diff --git a/src/components/ModuleButtons/ShellButton.tsx b/src/components/ModuleButtons/ShellButton.tsx new file mode 100644 index 000000000..7646c0783 --- /dev/null +++ b/src/components/ModuleButtons/ShellButton.tsx @@ -0,0 +1,16 @@ +import { asset } from '../../core/blitzkrieg/asset'; +import { GenericTankComponentButton } from './GenericTankComponentButton'; +import { TankComponentButtonProps } from './TankComponentButton'; + +interface ShellButtonProps extends TankComponentButtonProps { + shell: string; +} + +export function ShellButton({ shell, ...props }: ShellButtonProps) { + return ( + <GenericTankComponentButton + icon={asset(`icons/shells/${shell}.webp`)} + {...props} + /> + ); +} diff --git a/src/components/ModuleButtons/TankComponentButton.tsx b/src/components/ModuleButtons/TankComponentButton.tsx new file mode 100644 index 000000000..3062096cf --- /dev/null +++ b/src/components/ModuleButtons/TankComponentButton.tsx @@ -0,0 +1,78 @@ +import { Button, Text } from '@radix-ui/themes'; +import { ComponentProps, ReactNode } from 'react'; + +export interface TankComponentButtonProps + extends ComponentProps<typeof Button> { + selected?: boolean; + first?: boolean; + last?: boolean; + rowChild?: boolean; + children?: ReactNode; + discriminator?: ReactNode; + disabled?: boolean; + banner?: string; +} + +export function TankComponentButton({ + selected, + first = false, + last = false, + rowChild, + discriminator, + children, + banner, + ...props +}: TankComponentButtonProps) { + return ( + <Button + radius="medium" + color={selected ? undefined : 'gray'} + variant={selected ? 'surface' : 'soft'} + style={{ + padding: 0, + minWidth: 48, + height: 40, + position: 'relative', + borderTopLeftRadius: first ? undefined : 0, + borderTopRightRadius: last ? undefined : 0, + borderBottomLeftRadius: first ? undefined : 0, + borderBottomRightRadius: last ? undefined : 0, + margin: rowChild ? -0.5 : 'unset', + overflow: 'hidden', + }} + {...props} + > + {banner !== undefined && ( + <div + style={{ + position: 'absolute', + width: '200%', + height: 12, + backgroundColor: banner, + top: '50%', + left: '-50%', + transform: 'translateY(-50%) rotate(-22.5deg)', + }} + /> + )} + + {discriminator !== undefined && ( + <Text + size="1" + style={{ + textShadow: '0 0 4px black', + zIndex: 1, + position: 'absolute', + top: '50%', + right: 8, + textAlign: 'right', + }} + > + {discriminator} + </Text> + )} + + {children} + </Button> + ); +} diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 762afa985..0d437c4a6 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -2,7 +2,7 @@ import { DiscordLogoIcon } from '@radix-ui/react-icons'; import { Flex, Text, Theme } from '@radix-ui/themes'; import Link from 'next/link'; import { theme } from '../../stitches.config'; -import { BlitzkriegWormWide } from '../BlitzkriegWormWide'; +import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide'; export default function Navbar() { return ( diff --git a/src/components/PageWrapper.tsx b/src/components/PageWrapper.tsx index 09ce1bfb6..98ba0a074 100644 --- a/src/components/PageWrapper.tsx +++ b/src/components/PageWrapper.tsx @@ -3,7 +3,7 @@ import { ComponentProps } from 'react'; interface PageWrapperProps extends ComponentProps<typeof Flex> { color?: ComponentProps<typeof Theme>['accentColor']; - size?: 'regular' | 'wide'; + size?: 'regular' | 'wide' | 'double'; } export default function PageWrapper({ @@ -19,7 +19,7 @@ export default function PageWrapper({ gap="4" style={{ width: '100%', - maxWidth: size === 'regular' ? 800 : 960, + maxWidth: size === 'regular' ? 800 : size === 'double' ? 1200 : 960, margin: 'auto', padding: 16, boxSizing: 'border-box', diff --git a/src/components/ShotDisplay.tsx b/src/components/ShotDisplay.tsx new file mode 100644 index 000000000..2e2c29a05 --- /dev/null +++ b/src/components/ShotDisplay.tsx @@ -0,0 +1,226 @@ +import { Card, Flex, Text } from '@radix-ui/themes'; +import { Html } from '@react-three/drei'; +import { useFrame } from '@react-three/fiber'; +import { ReactNode, useEffect, useRef } from 'react'; +import { Group, Mesh, Vector3 } from 'three'; +import { radToDeg } from 'three/src/math/MathUtils'; +import { isExplosive } from '../core/blitz/isExplosive'; +import { AngledPenetration } from '../icons/AngledPenetration'; +import { Block } from '../icons/Block'; +import { NominalPenetration } from '../icons/NominalPenetration'; +import { Ricochet } from '../icons/Ricochet'; +import { useDuel } from '../stores/duel'; +import { Shot, useTankopediaTemporary } from '../stores/tankopedia'; + +export const SHOT_NAMES: Record<Shot['type'], string> = { + ricochet: 'Ricochet', + penetration: 'Penetration', + block: 'Blocked', +}; + +export const SHOT_ICONS: Record<Shot['type'], ReactNode> = { + ricochet: <Ricochet width={24} height={24} />, + block: <Block width={24} height={24} />, + penetration: <NominalPenetration width={24} height={24} />, +}; + +const THICKNESS = 0.05; +const LENGTH = 4; + +export function ShotDisplay() { + const shot = useTankopediaTemporary((state) => state.shot); + const entryWrapper = useRef<Group>(null); + const entryLine = useRef<Mesh>(null); + const circle = useRef<Mesh>(null); + const ricochetCircle = useRef<Mesh>(null); + const exitWrapper = useRef<Group>(null); + const exitLine = useRef<Mesh>(null); + const secondLineLength = shot?.ricochet ? shot?.ricochet.distance : LENGTH; + const number = shot && (shot.thicknesses.length > 1 || shot.ricochet); + const shell = useDuel((state) => state.antagonist!.shell); + + useFrame(({ clock }) => { + const t1 = clock.elapsedTime % 1; + const t2 = (clock.elapsedTime + 0.5) % 1; + + entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH); + entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1); + exitLine.current?.position.set(0, 0, t2 * secondLineLength); + exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1); + }); + + useEffect(() => { + if (!shot) return; + + const point = new Vector3(...shot.point); + const shellNormal = new Vector3(...shot.shellNormal); + const surfaceNormal = new Vector3(...shot.surfaceNormal); + const bounceNormal = shellNormal + .clone() + .multiplyScalar(-1) + .sub( + surfaceNormal + .clone() + .multiplyScalar( + 2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)), + ), + ); + + entryWrapper.current?.position.set(...shot.point); + entryWrapper.current?.lookAt(point.clone().add(shellNormal)); + exitWrapper.current?.position.set(...shot.point); + exitWrapper.current?.lookAt(point.clone().add(bounceNormal)); + circle.current?.position.set(...shot.point); + circle.current?.lookAt(point.clone().add(surfaceNormal)); + }, [shot]); + + if (!shot) return null; + + return ( + <> + <mesh ref={circle}> + <torusGeometry args={[0.05, 0.0125, 4, 16]} /> + <meshBasicMaterial + color={shot.type === 'penetration' ? 'green' : 'red'} + depthTest={false} + /> + </mesh> + + <group ref={entryWrapper}> + <Html + position={[0, 0, 0]} + style={{ + paddingLeft: 16, + }} + > + <Card + style={{ + width: number ? 300 : 200, + userSelect: 'none', + }} + > + <Flex direction="column" gap="1"> + <Flex align="center" gap="2"> + {SHOT_ICONS[shot.type]} + <Text> + {SHOT_NAMES[shot.type]} at {Math.round(radToDeg(shot.angle))}° + </Text> + </Flex> + + <Flex + direction="column" + style={{ + paddingLeft: number ? 8 : undefined, + }} + > + {shot.thicknesses.map((layer, index) => ( + <Flex gap="2" key={index}> + {number && ( + <Text> + {index + 1}. {layer.type[0].toUpperCase()} + {layer.type.slice(1)} + </Text> + )} + + <Flex gap="1"> + {layer.type !== 'gap' && ( + <NominalPenetration width={24} height={24} /> + )} + <Text> + {Math.round( + layer.type === 'gap' + ? layer.gap * 1000 + : layer.nominal, + )} + mm + {layer.type === 'gap' && isExplosive(shell.type) + ? ` (-${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% penetration)` + : ''} + </Text> + </Flex> + {layer.type !== 'external' && layer.type !== 'gap' && ( + <Flex gap="1"> + <AngledPenetration width={24} height={24} /> + <Text>{Math.round(layer.angled)}mm</Text> + </Flex> + )} + </Flex> + ))} + + {shot.ricochet && ( + <> + <Flex gap="1"> + <Text>{shot.thicknesses.length + 1}. Ricochet</Text> + <Ricochet width={24} height={24} /> + <Text>(-25% penetration)</Text> + </Flex> + {shot.ricochet.penetration ? ( + <Flex gap="1"> + <Text>{shot.thicknesses.length + 2}. Penetration</Text> + <NominalPenetration width={24} height={24} /> + </Flex> + ) : ( + <Flex gap="1"> + <Text>{shot.thicknesses.length + 2}. Blocked</Text> + <Block width={24} height={24} /> + </Flex> + )} + </> + )} + </Flex> + </Flex> + </Card> + </Html> + + <mesh + renderOrder={1} + position={[0, 0, LENGTH / 2]} + rotation={[Math.PI / 2, 0, 0]} + ref={entryLine} + > + <cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} /> + <meshBasicMaterial color="#ffff80" /> + </mesh> + <mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}> + <cylinderGeometry args={[THICKNESS / 4, THICKNESS / 4, LENGTH, 8]} /> + <meshBasicMaterial color="#ffffff" depthTest={false} /> + </mesh> + </group> + + {shot.type === 'ricochet' && ( + <group ref={exitWrapper}> + <mesh + renderOrder={1} + position={[0, 0, secondLineLength / 2]} + rotation={[Math.PI / 2, 0, 0]} + ref={exitLine} + > + <cylinderGeometry + args={[THICKNESS / 2, THICKNESS / 2, secondLineLength, 8]} + /> + <meshBasicMaterial color="#ff8080" /> + </mesh> + <mesh + position={[0, 0, secondLineLength / 2]} + rotation={[Math.PI / 2, 0, 0]} + > + <cylinderGeometry + args={[THICKNESS / 4, THICKNESS / 4, secondLineLength, 8]} + /> + <meshBasicMaterial color="#ffffff" depthTest={false} /> + </mesh> + + {shot.ricochet && ( + <mesh ref={ricochetCircle} position={[0, 0, secondLineLength]}> + <icosahedronGeometry args={[0.05, 1]} /> + <meshBasicMaterial + color={shot.ricochet.penetration ? 'green' : 'red'} + depthTest={false} + /> + </mesh> + )} + </group> + )} + </> + ); +} diff --git a/src/components/SmallTankIcon.tsx b/src/components/SmallTankIcon.tsx new file mode 100644 index 000000000..a324c818d --- /dev/null +++ b/src/components/SmallTankIcon.tsx @@ -0,0 +1,29 @@ +import { asset } from '../core/blitzkrieg/asset'; + +interface SmallTankIconProps { + id: number; + size?: number; +} + +const WIDTH = 70; +const HEIGHT = 24; + +export function SmallTankIcon({ id, size = HEIGHT }: SmallTankIconProps) { + return ( + <div + style={{ + height: size, + overflow: 'hidden', + width: WIDTH * (size / HEIGHT), + }} + > + <img + src={asset(`icons/tanks/small/${id}.webp`)} + style={{ + transform: `scale(${(size / HEIGHT) * 100}%)`, + transformOrigin: 'top left', + }} + /> + </div> + ); +} diff --git a/src/components/Tanks/components/Item.tsx b/src/components/Tanks/components/Item.tsx index 4bf8766d1..da22e336f 100644 --- a/src/components/Tanks/components/Item.tsx +++ b/src/components/Tanks/components/Item.tsx @@ -2,34 +2,28 @@ import { TANK_ICONS, TANK_ICONS_COLLECTOR, TANK_ICONS_PREMIUM, -} from '../../../core/blitzkrieg/definitions/tanks'; +} from '../../../core/blitzkrieg/tankDefinitions'; import { theme } from '../../../stitches.config'; -export enum TreeTypeEnum { - TechTree, - Premium, - Collector, -} - export const TANK_TYPES = [ - 'tank_destroyer', 'light', 'medium', 'heavy', + 'tankDestroyer', ] as const; export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; export type TankType = (typeof TANK_TYPES)[number]; -export type TreeTypeString = (typeof TREE_TYPES)[number]; +export type TreeType = (typeof TREE_TYPES)[number]; -export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = { +export const TREE_TYPE_NAMES: Record<TreeType, string> = { researchable: 'Tech tree', premium: 'Premium', collector: 'Collector', }; -export const TREE_TYPE_IMAGES: Record<TreeTypeString, string> = { +export const TREE_TYPE_IMAGES: Record<TreeType, string> = { researchable: 'https://i.imgur.com/pJxO2XY.png', premium: 'https://i.imgur.com/mZzSwOU.png', collector: 'https://i.imgur.com/7A0RsG5.png', @@ -39,19 +33,19 @@ export interface ItemProps { image?: string; tankType?: TankType; name: string; - treeType: TreeTypeEnum; + treeType: TreeType; } const TREE_TYPE_COLOR = { - [TreeTypeEnum.TechTree]: '', - [TreeTypeEnum.Premium]: '_amber', - [TreeTypeEnum.Collector]: '_blue', + researchable: '', + premium: '_amber', + collector: '_blue', } as const; -export const TREE_TYPE_ICONS = { - [TreeTypeEnum.TechTree]: TANK_ICONS, - [TreeTypeEnum.Premium]: TANK_ICONS_PREMIUM, - [TreeTypeEnum.Collector]: TANK_ICONS_COLLECTOR, +export const TREE_TYPE_ICONS: Record<TreeType, Record<TankType, string>> = { + researchable: TANK_ICONS, + premium: TANK_ICONS_PREMIUM, + collector: TANK_ICONS_COLLECTOR, }; export function Item({ image, tankType, name, treeType }: ItemProps) { diff --git a/src/components/TierWeights.tsx b/src/components/TierWeights.tsx index 3fe2149b6..f95dbabd2 100644 --- a/src/components/TierWeights.tsx +++ b/src/components/TierWeights.tsx @@ -1,4 +1,4 @@ -import { Tier } from '../core/blitzkrieg/definitions/tanks'; +import { Tier } from '../core/blitzkrieg/tankDefinitions'; import { theme } from '../stitches.config'; export type TierWeightsRecord = Partial<Record<Tier, number>>; diff --git a/src/constants/axis.ts b/src/constants/axis.ts new file mode 100644 index 000000000..9d600d2be --- /dev/null +++ b/src/constants/axis.ts @@ -0,0 +1,5 @@ +import { Vector3 } from 'three'; + +export const I_HAT = new Vector3(1, 0, 0); +export const J_HAT = new Vector3(0, 1, 0); +export const K_HAT = new Vector3(0, 0, 1); diff --git a/src/constants/tools.ts b/src/constants/tools.ts index a531edd8e..b311ca9e9 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -3,6 +3,7 @@ interface Tool { title: string; description: string; disabled?: boolean; + href?: string; } export const TOOLS: Tool[] = [ @@ -11,33 +12,38 @@ export const TOOLS: Tool[] = [ title: 'Session tracker', description: 'Live session performance tracker', }, + { + id: 'tankopedia', + title: 'Tankopedia', + description: 'Blitz tank encyclopedia', + }, { id: 'ratings', title: 'Ratings', description: 'Ratings gamemode leaderboards', }, { - id: 'tankopedia', - title: 'Tankopedia', - description: 'Blitz tank encyclopedia', - disabled: true, - }, - { - id: 'inactive', - title: 'Inactivity tracker', - description: 'Find inactive members of a clan', - disabled: true, - }, - { - id: 'profile', - title: 'Player profile', - description: "A player's basic non-statistical info", - disabled: true, + id: 'discord', + title: 'Discord bot', + description: 'Stats and tools right in Discord', + href: 'https://discord.com/application-directory/1097673957865443370', }, // { - // title: 'More coming soon', - // description: 'New tools are added often', - // banner: '/assets/banners/more.png', - // id: 'https://discord.gg/nDt7AjGJQH', + // id: 'inactive', + // title: 'Inactivity tracker', + // description: 'Find inactive members of a clan', + // disabled: true, // }, + // { + // id: 'profile', + // title: 'Player profile', + // description: "A player's basic non-statistical info", + // disabled: true, + // }, + { + id: 'more', + title: 'More coming soon', + description: 'This site is being actively developed', + href: 'https://discord.gg/nDt7AjGJQH', + }, ]; diff --git a/src/core/blitz/SCPGStream.ts b/src/core/blitz/SCPGStream.ts deleted file mode 100644 index 5cb39fe60..000000000 --- a/src/core/blitz/SCPGStream.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { times } from 'lodash'; -import { readDVPL } from './readDVPL'; -import { readDVPLFile } from './readDVPLFile'; - -enum KAType { - NONE = 0, - BOOLEAN = 1, - INT32 = 2, - FLOAT = 3, - STRING = 4, - WIDE_STRING = 5, - BYTE_ARRAY = 6, - UINT32 = 7, - KEYED_ARCHIVE = 8, - INT64 = 9, - UINT64 = 10, - VECTOR2 = 11, - VECTOR3 = 12, - VECTOR4 = 13, - MATRIX2 = 14, - MATRIX3 = 15, - MATRIX4 = 16, - COLOR = 17, - FASTNAME = 18, - AABBOX3 = 19, - FILEPATH = 20, - FLOAT64 = 21, - INT8 = 22, - UINT8 = 23, - INT16 = 24, - UINT16 = 25, - ARRAY = 27, - UNCONFIRMED_TRANSFORM = 29, -} - -enum VertexType { - VERTEX, - NORMAL, - COLOR, - TEXCOORD0, - TEXCOORD1, - TEXCOORD2, - TEXCOORD3, - TANGENT, - BINORMAL, - HARD_JOINTINDEX, - PIVOT4, - FLEXIBILITY, - ANGLE_SIN_COS, - JOINTINDEX, - JOINTWEIGHT, - CUBETEXCOORD0, - CUBETEXCOORD1, - CUBETEXCOORD2, - CUBETEXCOORD3, -} - -interface PolygonGroupRaw { - '##name': 'PolygonGroup'; - '#id': Buffer; - cubeTextureCoordCount: number; - indexCount: number; - indexFormat: 0 | 1; - indices: Buffer; - packing: 0 | 1; - primitiveCount: number; - rhi_primitiveType: 1 | 2 | 10; - textureCoordCount: number; - vertexCount: number; - vertexFormat: number; - vertices: Buffer; -} - -const VECTOR_SIZES = [ - [VertexType.COLOR, VertexType.FLEXIBILITY, VertexType.HARD_JOINTINDEX], - [ - VertexType.TEXCOORD0, - VertexType.TEXCOORD1, - VertexType.TEXCOORD2, - VertexType.TEXCOORD3, - VertexType.ANGLE_SIN_COS, - ], - [ - VertexType.VERTEX, - VertexType.NORMAL, - VertexType.TANGENT, - VertexType.BINORMAL, - VertexType.CUBETEXCOORD0, - VertexType.CUBETEXCOORD1, - VertexType.CUBETEXCOORD2, - VertexType.CUBETEXCOORD3, - ], - [VertexType.PIVOT4, VertexType.JOINTINDEX, VertexType.JOINTWEIGHT], -]; - -export class SCPGStream { - public index = 0; - constructor(public buffer: Buffer) {} - - skip(size: number) { - this.index += size; - } - back(size: number) { - this.skip(-size); - } - read(size: number) { - return this.buffer.subarray(this.index, this.index + size); - } - consume(size: number) { - const subarray = this.read(size); - this.skip(size); - - return subarray; - } - - readRemaining() { - return this.read(Number.POSITIVE_INFINITY); - } - consumeRemaining() { - return this.consume(Number.POSITIVE_INFINITY); - } - - consumeByteArray(size: number) { - return this.consume(size); - } - - consumeAscii(size: number) { - return this.consume(size).toString('ascii'); - } - - consumeInt8() { - return this.consume(1).readInt8(); - } - consumeInt16() { - return this.consume(2).readInt16LE(); - } - consumeInt32() { - return this.consume(4).readInt32LE(); - } - consumeInt64() { - return this.consume(8).readBigInt64LE(); - } - consumeUInt8() { - return this.consume(1).readUInt8(); - } - consumeUInt16() { - return this.consume(2).readUInt16LE(); - } - consumeUInt32() { - return this.consume(4).readUInt32LE(); - } - consumeUInt64() { - return this.consume(8).readBigUInt64LE(); - } - consumeFloat() { - return this.consume(4).readFloatLE(); - } - - consumeBoolean() { - return this.consume(1).readUInt8() === 1; - } - - consumeVectorN(size: number) { - return times(size, () => this.consumeFloat()); - } - consumeVector2() { - return this.consumeVectorN(2); - } - consumeVector3() { - return this.consumeVectorN(3); - } - consumeVector4() { - return this.consumeVectorN(4); - } - - consumeMatrixN(size: number) { - return times(size, () => this.consumeVectorN(size)); - } - consumeMatrix2() { - return this.consumeMatrixN(2); - } - consumeMatrix3() { - return this.consumeMatrixN(3); - } - consumeMatrix4() { - return this.consumeMatrixN(4); - } - - consumeSCGHeader() { - return { - name: this.consumeAscii(4), - version: this.consumeUInt32(), - nodeCount: this.consumeUInt32(), - nodeCount2: this.consumeUInt32(), - }; - } - consumeSCG() { - const header = this.consumeSCGHeader(); - const polygonGroupsRaw: PolygonGroupRaw[] = []; - - for (let index = 0; index < header.nodeCount; index++) { - const polygonGroupRaw = this.consumeKA() as PolygonGroupRaw; - polygonGroupsRaw.push(polygonGroupRaw); - } - - const polygonGroups = polygonGroupsRaw.map((polygonGroupRaw) => { - const indicesStream = new SCPGStream(polygonGroupRaw.indices); - const indices: number[] = []; - - for (let index = 0; index < polygonGroupRaw.indexCount; index++) { - indices.push( - polygonGroupRaw.indexFormat === 0 - ? indicesStream.consumeUInt16() - : indicesStream.consumeUInt32(), - ); - } - - const verticesStream = new SCPGStream(polygonGroupRaw.vertices); - const vertices: Partial<Record<VertexType, number[]>>[] = []; - const vertexFormatBuffer = Buffer.alloc(4); - vertexFormatBuffer.writeUInt32LE(polygonGroupRaw.vertexFormat, 0); - const vertexFormat = [...vertexFormatBuffer] - .map((number) => number.toString(2)) - .join('') - .split('') - .map((bitString, index) => (bitString === '1' ? index : null)) - .filter((type) => type !== null) as VertexType[]; - - for (let index = 0; index < polygonGroupRaw.vertexCount; index++) { - vertexFormat.forEach((type) => { - const resolved = VECTOR_SIZES.some((types, size) => { - if (types.includes(type)) { - if (!(index in vertices)) vertices[index] = {}; - vertices[index][type] = verticesStream.consumeVectorN(size + 1); - - return true; - } - }); - - if (!resolved) { - throw new TypeError(`Unknown vertex type: ${type}`); - } - }); - } - - return { vertices, indices }; - }); - - return polygonGroups; - } - - consumeSC2Header() { - const header = { - name: this.consumeAscii(4), - version: this.consumeUInt32(), - nodeCount: this.consumeUInt32(), - }; - const versionTags = this.consumeKA(); - const descriptor = this.consumeSC2Descriptor(); - - return { header, versionTags, descriptor }; - } - consumeSC2Descriptor() { - const size = this.consumeUInt32(); - const fileType = this.consumeUInt32(); - - // other felids that we don't care about for some reason lol - this.skip(size - 4); - - return { size, fileType }; - } - consumeSC2() { - this.consumeSC2Header(); - return this.consumeKA(); - } - - consumeKAValue( - type: KAType = this.consumeUInt8(), - stringTable?: Record<number, string>, - ): unknown { - switch (type) { - case KAType.NONE: - return undefined; - - case KAType.FILEPATH: - case KAType.STRING: { - if (stringTable) { - const id = this.consumeUInt32(); - return stringTable[id]; - } else { - const length = this.consumeUInt32(); - return this.consumeAscii(length); - } - } - - case KAType.BYTE_ARRAY: { - const length = this.consumeUInt32(); - return this.consumeByteArray(length); - } - - case KAType.FLOAT: - return this.consumeFloat(); - - case KAType.ARRAY: { - const length = this.consumeUInt32(); - const value = []; - - for (let index = 0; index < length; index++) { - value.push(this.consumeKAValue(undefined, stringTable)); - } - - return value; - } - - case KAType.KEYED_ARCHIVE: { - this.skip(4); // UInt32 length of the nested archive in bytes - - return this.consumeKA(stringTable); - } - - case KAType.INT8: - return this.consumeInt8(); - case KAType.INT16: - return this.consumeInt16(); - case KAType.INT32: - return this.consumeInt32(); - case KAType.INT64: - return this.consumeInt64(); - case KAType.UINT8: - return this.consumeUInt8(); - case KAType.UINT16: - return this.consumeUInt16(); - case KAType.UINT32: - return this.consumeUInt32(); - case KAType.UINT64: - return this.consumeUInt64(); - - case KAType.BOOLEAN: - return this.consumeBoolean(); - - case KAType.VECTOR2: - return this.consumeVector2(); - case KAType.VECTOR3: - return this.consumeVector3(); - case KAType.VECTOR4: - return this.consumeVector4(); - - case KAType.COLOR: - return this.consumeVector4(); // as RGBA - - case KAType.MATRIX2: - return this.consumeMatrix2(); - case KAType.MATRIX3: - return this.consumeMatrix3(); - case KAType.MATRIX4: - return this.consumeMatrix4(); - - case KAType.AABBOX3: { - const minimum = this.consumeVector3(); - const maximum = this.consumeVector3(); - - return { minimum, maximum }; - } - - case KAType.FASTNAME: { - if (!stringTable) throw new Error('No string table provided'); - const index = this.consumeUInt32(); - return stringTable[index]; - } - - case KAType.UNCONFIRMED_TRANSFORM: { - const position = this.consumeVector3(); - const scale = this.consumeVector3(); - const rotation = this.consumeVector3(); - const UNCONFIRMED_scalar = this.consumeFloat(); - - return { - position, - scale, - rotation, - UNCONFIRMED_scalar, - }; - } - - default: - throw new TypeError( - `Unhandled KA type: ${type} (${KAType[type] ?? 'unknown type'})`, - ); - } - } - consumeKA(stringTable?: Record<number, string>) { - this.skip(2); // "KA" - const version = this.consumeUInt16(); - const pairs: Record<string, any> = {}; - - if (version === 0x0001) { - const count = this.consumeUInt32(); - - for (let index = 0; index < count; index++) { - const name = this.consumeKAValue(undefined, stringTable) as string; - const value = this.consumeKAValue(undefined, stringTable); - - pairs[name] = value; - } - } else if (version === 0x0002) { - const count = this.consumeUInt32(); - const strings: string[] = []; - const stringTable: Record<number, string> = {}; - - for (let index = 0; index < count; index++) { - const length = this.consumeUInt16(); - const string = this.consumeAscii(length); - - strings.push(string); - } - - for (let index = 0; index < count; index++) { - const id = this.consumeUInt32(); - stringTable[id] = strings[index]; - } - - const nodeCount = this.consumeUInt32(); - - for (let index = 0; index < nodeCount; index++) { - const keyId = this.consumeUInt32(); - const value = this.consumeKAValue(undefined, stringTable); - const key = stringTable[keyId]; - - pairs[key] = value; - } - } else if (version === 0x0102) { - if (!stringTable) throw new Error('No string table provided'); - - const count = this.consumeUInt32(); - - for (let index = 0; index < count; index++) { - const keyIndex = this.consumeUInt32(); - const key = stringTable[keyIndex]; - const valueType = this.consumeUInt8(); - - if (valueType === KAType.STRING) { - const valueIndex = this.consumeUInt32(); - const value = stringTable[valueIndex]; - pairs[key] = value; - } else { - const value = this.consumeKAValue(valueType, stringTable); - pairs[key] = value; - } - } - } else if (version === 0xff02) { - // same as 0x0102 but empty and with no count - return {}; - } else throw new RangeError(`Unhandled KA version: ${version}`); - - return pairs; - } - - static fromDVPL(buffer: Buffer) { - return new SCPGStream(readDVPL(buffer)); - } - - static async fromDVPLFile(file: string) { - return new SCPGStream(await readDVPLFile(file)); - } -} diff --git a/src/core/blitz/applyPitchYawLimits.ts b/src/core/blitz/applyPitchYawLimits.ts new file mode 100644 index 000000000..06994d837 --- /dev/null +++ b/src/core/blitz/applyPitchYawLimits.ts @@ -0,0 +1,81 @@ +import { clamp } from 'lodash'; +import { PitchLimits, YawLimits } from '../blitzkrieg/modelDefinitions'; +import { normalizeAnglePI } from '../math/normalizeAngle180'; + +export function applyPitchYawLimits( + pitch: number, // rad + yaw: number, // rad + pitchLimits: PitchLimits, // deg + yawLimits?: YawLimits, // deg +) { + let newPitch = pitch; + let newYaw = yaw; + + if (yawLimits) { + newYaw = normalizeAnglePI( + clamp( + newYaw, + -yawLimits.max * (Math.PI / 180), + -yawLimits.min * (Math.PI / 180), + ), + ); + } + + let lowerPitch = -pitchLimits.max * (Math.PI / 180); + let upperPitch = -pitchLimits.min * (Math.PI / 180); + const transition = (pitchLimits.transition ?? 10) * (Math.PI / 180); + + if (pitchLimits.back) { + const range = pitchLimits.back.range * (Math.PI / 180); + const yawRotatedAbs = Math.abs(normalizeAnglePI(newYaw - Math.PI)); + + if (yawRotatedAbs <= range + transition) { + if (yawRotatedAbs <= range) { + // inside range + lowerPitch = -pitchLimits.back.max * (Math.PI / 180); + upperPitch = -pitchLimits.back.min * (Math.PI / 180); + } else { + // inside transition + const transitionProgress = (yawRotatedAbs - range) / transition; + const maxInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.back.max + + transitionProgress * pitchLimits.max; + const minInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.back.min + + transitionProgress * pitchLimits.min; + + lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); + upperPitch = -minInterpolatedPitch * (Math.PI / 180); + } + } + } + + if (pitchLimits.front) { + const range = pitchLimits.front.range * (Math.PI / 180); + const yawAbs = Math.abs(normalizeAnglePI(newYaw)); + + if (yawAbs <= range + transition) { + if (yawAbs <= range) { + // inside range + lowerPitch = -pitchLimits.front.max * (Math.PI / 180); + upperPitch = -pitchLimits.front.min * (Math.PI / 180); + } else { + // inside transition + const transitionProgress = (yawAbs - range) / transition; + const maxInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.front.max + + transitionProgress * pitchLimits.max; + const minInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.front.min + + transitionProgress * pitchLimits.min; + + lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); + upperPitch = -minInterpolatedPitch * (Math.PI / 180); + } + } + } + + newPitch = clamp(newPitch, lowerPitch, upperPitch); + + return [newPitch, newYaw]; +} diff --git a/src/core/blitz/canRicochet.ts b/src/core/blitz/canRicochet.ts new file mode 100644 index 000000000..fc03a9a99 --- /dev/null +++ b/src/core/blitz/canRicochet.ts @@ -0,0 +1,7 @@ +import { ShellDefinition } from '../blitzkrieg/tankDefinitions'; + +export function canRicochet( + type: ShellDefinition['type'], +): type is Exclude<ShellDefinition['type'], 'he'> { + return type !== 'he'; +} diff --git a/src/core/blitz/canSplash.ts b/src/core/blitz/canSplash.ts new file mode 100644 index 000000000..85e3d82eb --- /dev/null +++ b/src/core/blitz/canSplash.ts @@ -0,0 +1,5 @@ +import { ShellDefinition } from '../blitzkrieg/tankDefinitions'; + +export function canSplash(type: ShellDefinition['type']): type is 'he' { + return type === 'he'; +} diff --git a/src/core/blitz/extractArmor.ts b/src/core/blitz/extractArmor.ts new file mode 100644 index 000000000..b2819ff05 --- /dev/null +++ b/src/core/blitz/extractArmor.ts @@ -0,0 +1,141 @@ +import { Accessor, Document, Node, Scene } from '@gltf-transform/core'; +import { times } from 'lodash'; +import { Hierarchy, Sc2ReadStream } from '../streams/sc2'; +import { ScgReadStream } from '../streams/scg'; +import { VertexAttribute } from '../streams/scpg'; +import { + vertexAttributeGLTFName, + vertexAttributeGltfVectorSizes, +} from './extractModel'; +import { readDVPLFile } from './readDVPLFile'; + +Buffer.prototype.toJSON = function () { + return [...(this as Buffer)].map((int) => int.toString(16)).join(''); +}; + +export async function extractArmor(data: string, fileName: string) { + const sc2Path = `${data}/3d/Tanks/CollisionMeshes/${fileName}.sc2.dvpl`; + const scgPath = `${data}/3d/Tanks/CollisionMeshes/${fileName}.scg.dvpl`; + const sc2 = new Sc2ReadStream((await readDVPLFile(sc2Path)).buffer).sc2(); + const scg = new ScgReadStream((await readDVPLFile(scgPath)).buffer).scg(); + const document = new Document(); + const scene = document.createScene(); + const buffer = document.createBuffer(); + + function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) { + hierarchies.forEach((hierarchy) => { + const components = times( + hierarchy.components.count, + (index) => hierarchy.components[index.toString().padStart(4, '0')], + ); + + components.forEach((component) => { + switch (component['comp.typename']) { + case 'TransformComponent': + break; + + case 'RenderComponent': { + const batch = component['rc.renderObj']['ro.batches']['0000']; + const polygonGroup = scg.get(batch['rb.datasource']); + + if (!polygonGroup) { + console.warn( + `Missing polygon group ${batch['rb.datasource']} (${hierarchy.name}); skipping...`, + ); + + break; + } + + const hardJointIndices = new Set<number>(); + const vertexHardJointIndices = new Map<number, number>(); + const attributes = new Map<VertexAttribute, number[][]>(); + + polygonGroup.vertices.forEach((vertex, index) => { + vertex.forEach(({ attribute, value }) => { + if (!attributes.has(attribute)) { + attributes.set(attribute, []); + } + + attributes.get(attribute)!.push(value); + + if (attribute === VertexAttribute.HARD_JOINTINDEX) { + hardJointIndices.add(value[0]); + vertexHardJointIndices.set(index, value[0]); + } + }); + }); + + if (attributes.has(VertexAttribute.TANGENT)) { + attributes.set( + VertexAttribute.TANGENT, + attributes + .get(VertexAttribute.TANGENT)! + .map((tangent) => [...tangent, 1]), + ); + } + + const accessors = new Map<string, Accessor>(); + + attributes.forEach((value, attribute) => { + const name = vertexAttributeGLTFName[attribute]; + if (!name || accessors.has(name)) return; + const vertexSize = vertexAttributeGltfVectorSizes[attribute]; + const accessor = document + .createAccessor(name) + .setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`) + .setArray(new Float32Array(value.flat())) + .setBuffer(buffer); + + accessors.set(name, accessor); + }); + + hardJointIndices.forEach((hardJointIndex) => { + const node = document.createNode( + `${hierarchy.name}_armor_${hardJointIndex}`, + ); + const mesh = document.createMesh(batch['##name']); + const indicesAccessor = document + .createAccessor() + .setType('SCALAR') + .setArray( + new Uint16Array( + polygonGroup.indices.filter( + (index) => + vertexHardJointIndices.get(index) === hardJointIndex, + ), + ), + ) + .setBuffer(buffer); + const primitive = document + .createPrimitive() + .setIndices(indicesAccessor); + + accessors.forEach((accessor, name) => { + primitive.setAttribute(name, accessor); + }); + + mesh.addPrimitive(primitive); + node.setMesh(mesh); + parent.addChild(node); + }); + + break; + } + + default: + throw new TypeError( + `Unhandled component type: ${component['comp.typename']}`, + ); + } + }); + + if (hierarchy['#hierarchy']) { + parseHierarchies(hierarchy['#hierarchy'], parent); + } + }); + } + + parseHierarchies(sc2['#hierarchy'], scene); + + return document; +} diff --git a/src/core/blitz/extractModel.ts b/src/core/blitz/extractModel.ts new file mode 100644 index 000000000..427672617 --- /dev/null +++ b/src/core/blitz/extractModel.ts @@ -0,0 +1,320 @@ +import { Document, Material, Node, Scene } from '@gltf-transform/core'; +import { times } from 'lodash'; +import { dirname } from 'path'; +import { + Matrix4, + Quaternion, + Vector3, + Vector3Tuple, + Vector4Tuple, +} from 'three'; +import { TextureMutation, readTexture } from '../blitzkrieg/readTexture'; +import { Hierarchy, Sc2ReadStream, Textures } from '../streams/sc2'; +import { ScgReadStream, vertexAttributeVectorSizes } from '../streams/scg'; +import { VertexAttribute } from '../streams/scpg'; +import { readDVPLFile } from './readDVPLFile'; + +const ERROR_ON_UNKNOWN_COMPONENT = false; +const MAX_FLOAT32 = 2 ** 127 * (2 - 2 ** -23); + +export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> = + { + [VertexAttribute.VERTEX]: 'POSITION', + [VertexAttribute.NORMAL]: 'NORMAL', + [VertexAttribute.TEXCOORD0]: 'TEXCOORD_0', + [VertexAttribute.TEXCOORD1]: 'TEXCOORD_0', + [VertexAttribute.TEXCOORD2]: 'TEXCOORD_0', + [VertexAttribute.TEXCOORD3]: 'TEXCOORD_0', + [VertexAttribute.TANGENT]: 'TANGENT', + [VertexAttribute.JOINTINDEX]: 'JOINT_0', + [VertexAttribute.JOINTWEIGHT]: 'WEIGHT_0', + }; + +export const vertexAttributeGltfVectorSizes = { + ...vertexAttributeVectorSizes, + + [VertexAttribute.TANGENT]: 4, +} as const; + +const omitMeshNames = { + start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'], + end: ['_POINT'], +}; + +export async function extractModel( + data: string, + path: string, + baseColor?: Vector3Tuple, +) { + const sc2Path = `${data}/3d/${path}.sc2.dvpl`; + const scgPath = `${data}/3d/${path}.scg.dvpl`; + const sc2 = new Sc2ReadStream((await readDVPLFile(sc2Path)).buffer).sc2(); + const scg = new ScgReadStream((await readDVPLFile(scgPath)).buffer).scg(); + const document = new Document(); + const scene = document.createScene(); + const buffer = document.createBuffer(); + const materials = new Map<bigint, Material | bigint | undefined>(); + + // create materials + await Promise.all( + sc2['#dataNodes'].map(async (node) => { + const id = new DataView(node['#id']).getBigUint64(0, true); + + if (node.parentMaterialKey !== undefined) { + /** + * material depends on a parent and doesn't seem to have any + * properties so we just point it to the parent + */ + materials.set(id, node.parentMaterialKey); + return; + } + + const material = document.createMaterial(node.materialName); + let textures: Textures | undefined = undefined; + + if (node.textures) { + textures = node.textures; + } else if (typeof node.configCount === 'number') { + textures = node.configArchive_0.textures; + } + + if (textures) { + material.setBaseColorTexture( + document + .createTexture(node.materialName) + .setMimeType('image/png') + .setImage( + await readTexture( + `${data}/3d/${dirname(path)}/${textures.albedo}`, + baseColor + ? { mutation: TextureMutation.BaseColor, baseColor } + : undefined, + ), + ), + ); + + if (textures.baseRMMap) { + material.setMetallicRoughnessTexture( + document + .createTexture(node.materialName) + .setMimeType('image/png') + .setImage( + await readTexture( + `${data}/3d/${dirname(path)}/${textures.baseRMMap}`, + { mutation: TextureMutation.RoughnessMetallicness }, + ), + ), + ); + } + + if (textures.baseNormalMap ?? textures.normalmap) { + const isBase = textures.baseNormalMap !== undefined; + + material.setNormalTexture( + document + .createTexture(node.materialName) + .setMimeType('image/png') + .setImage( + await readTexture( + `${data}/3d/${dirname(path)}/${ + textures.baseNormalMap ?? textures.normalmap + }`, + isBase ? { mutation: TextureMutation.Normal } : undefined, + ), + ), + ); + } + + if (textures.miscMap) { + material.setOcclusionTexture( + document + .createTexture(node.materialName) + .setMimeType('image/png') + .setImage( + await readTexture( + `${data}/3d/${dirname(path)}/${textures.miscMap}`, + { mutation: TextureMutation.Miscellaneous }, + ), + ), + ); + } + + materials.set( + new DataView(node['#id']).getBigUint64(0, true), + material, + ); + } + }), + ); + + // replace children materials with parents + materials.forEach((material, id) => { + if (typeof material !== 'bigint') return; + + let resolvedMaterial: Material | bigint | undefined = material; + + while (typeof resolvedMaterial === 'bigint') { + const linkedParentMaterial = materials.get(resolvedMaterial); + + resolvedMaterial = linkedParentMaterial; + } + + materials.set(id, resolvedMaterial); + }); + + function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) { + hierarchies.forEach((hierarchy) => { + if ( + omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) || + omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit)) + ) + return; + + const node = document.createNode(hierarchy.name); + const components = times( + hierarchy.components.count, + (index) => hierarchy.components[index.toString().padStart(4, '0')], + ); + + components.forEach((component) => { + switch (component['comp.typename']) { + case 'LodComponent': + // found and used later by transform component + // TODO: lod component always shows up before transform component; cache it before parsing transform + break; + + case 'TransformComponent': { + const translation = new Vector3(); + const rotation = new Quaternion(); + const scale = new Vector3(); + + if ( + !component['tc.worldTranslation'] || + !component['tc.worldScale'] || + !component['tc.localTranslation'] || + !component['tc.localScale'] + ) { + console.log(component); + } + + new Matrix4() + .multiplyMatrices( + new Matrix4().compose( + new Vector3().fromArray(component['tc.worldTranslation']), + new Quaternion().fromArray(component['tc.worldRotation']), + new Vector3().fromArray(component['tc.worldScale']), + ), + new Matrix4().compose( + new Vector3().fromArray(component['tc.localTranslation']), + new Quaternion().fromArray(component['tc.localRotation']), + new Vector3().fromArray(component['tc.localScale']), + ), + ) + .decompose(translation, rotation, scale); + + node.setTranslation(translation.toArray()); + node.setRotation(rotation.toArray() as Vector4Tuple); + node.setScale(scale.toArray()); + + break; + } + + case 'RenderComponent': { + const renderObject = component['rc.renderObj']; + + times(renderObject['ro.batchCount'], (batchIndex): void => { + const lodIndex = renderObject[`rb${batchIndex}.lodIndex`]; + + if (lodIndex !== 0) return; + + const batchKey = batchIndex.toString().padStart(4, '0'); + const batch = renderObject['ro.batches'][batchKey]; + const material = materials.get(batch['rb.nmatname']); + const polygonGroup = scg.get(batch['rb.datasource']); + + if (!(material instanceof Material)) { + // probably shadow material + return; + } + if (polygonGroup === undefined) { + throw new Error( + `Missing polygon group ${batch['rb.datasource']}`, + ); + } + + const lodNode = document.createNode(batchKey); + const indicesAccessor = document + .createAccessor() + .setType('SCALAR') + .setArray(new Uint16Array(polygonGroup.indices)) + .setBuffer(buffer); + const primitive = document + .createPrimitive() + .setIndices(indicesAccessor) + .setMaterial(material); + const attributes = new Map<VertexAttribute, number[][]>(); + + polygonGroup.vertices.forEach((vertex) => { + vertex.forEach(({ attribute, value }) => { + if (!attributes.has(attribute)) { + attributes.set(attribute, []); + } + + attributes.get(attribute)!.push(value); + }); + }); + + if (attributes.has(VertexAttribute.TANGENT)) { + attributes.set( + VertexAttribute.TANGENT, + attributes + .get(VertexAttribute.TANGENT)! + .map((tangent) => [...tangent, 1]), + ); + } + + attributes.forEach((value, attribute) => { + const name = vertexAttributeGLTFName[attribute]; + if (!name || primitive.getAttribute(name)) return; + const vertexSize = vertexAttributeGltfVectorSizes[attribute]; + + const attributeAccessor = document + .createAccessor(name) + .setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`) + .setArray(new Float32Array(value.flat())) + .setBuffer(buffer); + primitive.setAttribute(name, attributeAccessor); + }); + + const mesh = document + .createMesh(batch['##name']) + .addPrimitive(primitive); + lodNode.setMesh(mesh); + node.addChild(lodNode); + }); + + break; + } + + default: { + if (ERROR_ON_UNKNOWN_COMPONENT) { + throw new Error( + `Unhandled component type: ${component['comp.typename']}`, + ); + } + } + } + }); + + if (hierarchy['#hierarchy']) { + parseHierarchies(hierarchy['#hierarchy'], node); + } + + parent.addChild(node); + }); + } + + parseHierarchies(sc2['#hierarchy'], scene); + + return document; +} diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts index c35df14b5..be11ffe10 100644 --- a/src/core/blitz/getTankStats.ts +++ b/src/core/blitz/getTankStats.ts @@ -1,7 +1,7 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; +import { UserError } from '../../hooks/userError'; import { TanksStats } from '../../types/tanksStats'; -import { UserError } from '../blitzkrieg/userError'; import fetchBlitz from './fetchBlitz'; export default async function getTankStats(region: Region, id: number) { diff --git a/src/core/blitz/getTreeType.ts b/src/core/blitz/getTreeType.ts deleted file mode 100644 index b8aedd82d..000000000 --- a/src/core/blitz/getTreeType.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TreeTypeEnum } from '../../components/Tanks'; -import { tankDefinitions } from '../blitzkrieg/definitions/tanks'; - -export default async function getTreeType(id: number) { - const entry = (await tankDefinitions)[id]; - - if (entry.tree_type === 'collector') return TreeTypeEnum.Collector; - if (entry.tree_type === 'premium') return TreeTypeEnum.Premium; - - return TreeTypeEnum.TechTree; -} diff --git a/src/core/blitz/isExplosive.ts b/src/core/blitz/isExplosive.ts new file mode 100644 index 000000000..cd0e0984f --- /dev/null +++ b/src/core/blitz/isExplosive.ts @@ -0,0 +1,7 @@ +import { ShellDefinition } from '../blitzkrieg/tankDefinitions'; + +export function isExplosive( + type: ShellDefinition['type'], +): type is 'he' | 'hc' { + return type === 'he' || type === 'hc'; +} diff --git a/src/core/blitz/readYAML.ts b/src/core/blitz/readYAML.ts new file mode 100644 index 000000000..e459e5426 --- /dev/null +++ b/src/core/blitz/readYAML.ts @@ -0,0 +1,7 @@ +import { readFile } from 'fs/promises'; +import { parse } from 'yaml'; + +export async function readYAML<Type>(file: string) { + const yaml = (await readFile(file)).toString(); + return parse(yaml) as Type; +} diff --git a/src/core/blitz/resolveNearPenetration.ts b/src/core/blitz/resolveNearPenetration.ts new file mode 100644 index 000000000..c6680dffe --- /dev/null +++ b/src/core/blitz/resolveNearPenetration.ts @@ -0,0 +1,5 @@ +import { Vector2Tuple } from 'three'; + +export function resolveNearPenetration(penetration: number | Vector2Tuple) { + return typeof penetration === 'number' ? penetration : penetration[0]; +} diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index d6720847c..c9d555d98 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -1,10 +1,10 @@ import { go } from 'fuzzysort'; +import { UserError } from '../../hooks/userError'; import markdownEscape from 'markdown-escape'; import { tankDefinitions, tanksDefinitionsArray, -} from '../blitzkrieg/definitions/tanks'; -import { UserError } from '../blitzkrieg/userError'; +} from '../blitzkrieg/tankDefinitions'; export default async function resolveTankId(tank: string | number) { const number = typeof tank === 'string' ? parseInt(tank) : tank; diff --git a/src/core/blitzkrieg/asset.ts b/src/core/blitzkrieg/asset.ts index b659a17e4..b8c31f25d 100644 --- a/src/core/blitzkrieg/asset.ts +++ b/src/core/blitzkrieg/asset.ts @@ -1,3 +1,5 @@ +import isDev from './isDev'; + export function asset(path: string) { - return `https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/${path}`; + return `https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/${isDev() ? 'dev' : 'main'}/${path}`; } diff --git a/src/core/blitzkrieg/assignDuelMember.ts b/src/core/blitzkrieg/assignDuelMember.ts new file mode 100644 index 000000000..aef3eab48 --- /dev/null +++ b/src/core/blitzkrieg/assignDuelMember.ts @@ -0,0 +1,57 @@ +import { + DuelMember, + genericDefaultEquipmentMatrix, + mutateDuel, +} from '../../stores/duel'; +import { availableProvisions } from './availableProvisions'; +import { provisionDefinitions } from './provisionDefinitions'; +import { tankDefinitions } from './tankDefinitions'; + +export async function assignDuelMember( + type: 'antagonist' | 'protagonist' | 'both', + id: number, +) { + const awaitedTankDefinitions = await tankDefinitions; + const awaitedProvisionDefinitions = await provisionDefinitions; + const tank = awaitedTankDefinitions[id]; + const turret = tank.turrets.at(-1)!; + const gun = turret.guns.at(-1)!; + const shell = gun.shells[0]; + const engine = tank.engines.at(-1)!; + const track = tank.tracks.at(-1)!; + const provisionsList = availableProvisions( + tank, + gun, + awaitedProvisionDefinitions, + ); + const member: DuelMember = { + tank, + engine, + turret, + gun, + shell, + track, + equipment: genericDefaultEquipmentMatrix, + pitch: 0, + yaw: 0, + camouflage: true, + consumables: [], + cooldownBooster: 0, + crewMastery: 1, + provisions: provisionsList + .filter((provision) => provision.crew !== undefined) + .map((provision) => provision.id), + }; + + mutateDuel((draft) => { + draft.assigned = true; + if (type === 'antagonist') { + draft.antagonist = member; + } else if (type === 'protagonist') { + draft.protagonist = member; + } else { + draft.protagonist = member; + draft.antagonist = { ...member }; + } + }); +} diff --git a/src/core/blitzkrieg/availableProvisions.ts b/src/core/blitzkrieg/availableProvisions.ts new file mode 100644 index 000000000..143ad7158 --- /dev/null +++ b/src/core/blitzkrieg/availableProvisions.ts @@ -0,0 +1,13 @@ +import { checkConsumableProvisionInclusivity } from './checkConsumableProvisionInclusivity'; +import { ProvisionDefinitions } from './provisionDefinitions'; +import { GunDefinition, TankDefinition } from './tankDefinitions'; + +export function availableProvisions( + tank: TankDefinition, + gun: GunDefinition, + provisionDefinitions: ProvisionDefinitions, +) { + return Object.values(provisionDefinitions).filter((provision) => + checkConsumableProvisionInclusivity(provision, tank, gun), + ); +} diff --git a/src/core/blitzkrieg/checkConsumableProvisionInclusivity.ts b/src/core/blitzkrieg/checkConsumableProvisionInclusivity.ts new file mode 100644 index 000000000..27083a6f0 --- /dev/null +++ b/src/core/blitzkrieg/checkConsumableProvisionInclusivity.ts @@ -0,0 +1,47 @@ +import { ConsumableEntry } from './consumableDefinitions'; +import { ProvisionEntry } from './provisionDefinitions'; +import { GunDefinition, TankDefinition } from './tankDefinitions'; + +export function checkConsumableProvisionInclusivity( + consumableProvision: ConsumableEntry | ProvisionEntry, + tank: TankDefinition, + gun: GunDefinition, +) { + const included = consumableProvision.include.every((rule) => { + switch (rule.type) { + case 'tier': + return rule.min <= tank.tier && tank.tier <= rule.max; + + case 'ids': + return rule.ids.includes(tank.id); + + case 'nation': + return rule.nations.includes(tank.nation); + + case 'category': + throw new SyntaxError('Category filtering found in include rule'); + } + }); + const excluded = consumableProvision.exclude?.some((rule) => { + switch (rule.type) { + case 'tier': + return rule.min <= tank.tier && tank.tier <= rule.max; + + case 'ids': + return rule.ids.includes(tank.id); + + case 'nation': + return rule.nations.includes(tank.nation); + + case 'category': + return rule.categories.some((category) => { + switch (category) { + case 'clip': + return gun.type !== 'regular'; + } + }); + } + }); + + return consumableProvision.include.length > 0 && included && !excluded; +} diff --git a/src/core/blitzkrieg/coefficient.ts b/src/core/blitzkrieg/coefficient.ts new file mode 100644 index 000000000..b6ce2df34 --- /dev/null +++ b/src/core/blitzkrieg/coefficient.ts @@ -0,0 +1,3 @@ +export function coefficient(...effects: [boolean, number][]) { + return effects.reduce((accumulator, b) => accumulator + (b[0] ? b[1] : 0), 1); +} diff --git a/src/core/blitzkrieg/commitAssets.ts b/src/core/blitzkrieg/commitAssets.ts new file mode 100644 index 000000000..2c53e8814 --- /dev/null +++ b/src/core/blitzkrieg/commitAssets.ts @@ -0,0 +1,20 @@ +import commitMultipleFiles, { FileChange } from './commitMultipleFiles'; + +export async function commitAssets( + message: string, + changes: FileChange[], + production: boolean, +) { + console.log(`Committing ${message}...`); + + if (changes.length === 0) return; + + await commitMultipleFiles( + 'tresabhi', + 'blitzkrieg-assets', + production ? 'main' : 'dev', + `${message} - ${new Date().toDateString()}`, + changes, + true, + ); +} diff --git a/src/core/blitzkrieg/commitMultipleFiles.ts b/src/core/blitzkrieg/commitMultipleFiles.ts index c8cdfeace..237a72bdd 100644 --- a/src/core/blitzkrieg/commitMultipleFiles.ts +++ b/src/core/blitzkrieg/commitMultipleFiles.ts @@ -1,7 +1,6 @@ +import { GithubChangeBlob, createBlob } from './createBlob'; import { octokit } from './octokit'; -const TIME_PER_BLOB = 2 ** 4 * 1000; - export interface FileChange { path: string; content: string; @@ -30,41 +29,13 @@ export default async function commitMultipleFiles( commit_sha: latestCommitSha, }) ).data.tree.sha; - const blobs: { - sha: string; - path: string; - mode: '100644'; - type: 'blob'; - }[] = []; + const blobs: GithubChangeBlob[] = []; for (const changeIndexString in changes) { const changeIndex = parseInt(changeIndexString); const change = changes[changeIndex]; - let done = false; - - while (!done) { - try { - const { sha } = ( - await octokit.git.createBlob({ - owner, - repo, - content: change.content, - encoding: change.encoding, - }) - ).data; - - blobs.push({ sha, path: change.path, mode: '100644', type: 'blob' }); - done = true; - } catch (error) { - console.warn( - `Failed blob ${changeIndex + 1} / ${ - changes.length - }; retrying in ${TIME_PER_BLOB}ms...`, - ); - - await new Promise((resolve) => setTimeout(resolve, TIME_PER_BLOB)); - } - } + const createdBlob = await createBlob(owner, repo, change); + blobs.push(createdBlob!); if (verbose) { console.log( diff --git a/src/core/blitzkrieg/consumableDefinitions.ts b/src/core/blitzkrieg/consumableDefinitions.ts new file mode 100644 index 000000000..76a417488 --- /dev/null +++ b/src/core/blitzkrieg/consumableDefinitions.ts @@ -0,0 +1,40 @@ +import { asset } from './asset'; +import { fetchCdonLz4 } from './fetchCdonLz4'; + +export type TankFilterDefinitionCategory = 'clip'; + +export type TankFilterDefinition = + | { + type: 'tier'; + min: number; + max: number; + } + | { + type: 'ids'; + ids: number[]; + } + | { + type: 'category'; + categories: TankFilterDefinitionCategory[]; + } + | { + type: 'nation'; + nations: string[]; + }; + +export interface ConsumableEntry { + id: number; + name: string; + cooldown: number; + duration?: number; + include: TankFilterDefinition[]; + exclude?: TankFilterDefinition[]; +} + +export interface ConsumableDefinitions { + [key: string]: ConsumableEntry; +} + +export const consumableDefinitions = fetchCdonLz4<ConsumableDefinitions>( + asset('definitions/consumables.cdon.lz4'), +); diff --git a/src/core/blitzkrieg/createBlob.ts b/src/core/blitzkrieg/createBlob.ts new file mode 100644 index 000000000..2677521da --- /dev/null +++ b/src/core/blitzkrieg/createBlob.ts @@ -0,0 +1,46 @@ +import { throttle } from 'lodash'; +import { FileChange } from './commitMultipleFiles'; +import { octokit } from './octokit'; + +export interface GithubChangeBlob { + sha: string; + path: string; + mode: '100644'; + type: 'blob'; +} + +const TIME_PER_BLOB = 2 ** 4 * 1000; + +// github rate limit: 5000 requests per hour +export const createBlob = throttle( + async (owner: string, repo: string, change: FileChange) => { + while (true) { + try { + const { sha } = ( + await octokit.git.createBlob({ + owner, + repo, + content: change.content, + encoding: change.encoding, + }) + ).data; + + return { + sha, + path: change.path, + mode: '100644', + type: 'blob', + } satisfies GithubChangeBlob; + } catch (error) { + console.warn( + `Failed blob ${change.path}; retrying in ${TIME_PER_BLOB}ms...`, + // error, + ); + + await new Promise((resolve) => setTimeout(resolve, TIME_PER_BLOB)); + } + } + }, + (5000 / (60 * 60 * 1000)) * 0.9, + { trailing: true }, +); diff --git a/src/core/blitzkrieg/definitions/guns.ts b/src/core/blitzkrieg/definitions/guns.ts deleted file mode 100644 index 4de669219..000000000 --- a/src/core/blitzkrieg/definitions/guns.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { asset } from '../asset'; -import { Tier } from './tanks'; - -export interface BlitzkriegGunDefinition { - id: number; - name: string; - tier: Tier; - pitch: [number, number]; - shells: number[]; -} - -export type BlitzkriegGunDefinitions = Record<number, BlitzkriegGunDefinition>; - -export const gunDefinitions = fetch(asset('definitions/guns.json'), { - cache: 'no-cache', -}).then( - async (response) => response.json() as Promise<BlitzkriegGunDefinitions>, -); diff --git a/src/core/blitzkrieg/definitions/shells.ts b/src/core/blitzkrieg/definitions/shells.ts deleted file mode 100644 index 77f008855..000000000 --- a/src/core/blitzkrieg/definitions/shells.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { asset } from '../asset'; - -export type ShellType = 'ap' | 'ap_cr' | 'hc' | 'he'; - -export interface BlitzkriegShellDefinition { - id: number; - name: string; - speed: number; - damage: { armor: number; devices: number }; - caliber: number; - normalization: number; - ricochet: number; - type: ShellType; - icon: string; -} - -export type BlitzkriegShellDefinitions = Record< - number, - BlitzkriegShellDefinition ->; - -export const shellDefinitions = fetch(asset('definitions/shells.json'), { - cache: 'no-cache', -}).then( - async (response) => response.json() as Promise<BlitzkriegShellDefinitions>, -); diff --git a/src/core/blitzkrieg/definitions/tanks.ts b/src/core/blitzkrieg/definitions/tanks.ts deleted file mode 100644 index c9ca0f57c..000000000 --- a/src/core/blitzkrieg/definitions/tanks.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { deburr } from 'lodash'; -import { TankType, TreeTypeString } from '../../../components/Tanks'; -import { asset } from '../asset'; - -export interface BlitzkriegTankDefinition { - id: number; - nation: string; - name: string; - tree_type: TreeTypeString; - tier: Tier; - type: TankType; - testing?: boolean; - turrets: number[]; -} -export type BlitzkriegTankDefinitions = Record< - number, - BlitzkriegTankDefinition ->; - -export const tankDefinitions = fetch(asset('definitions/tanks.json'), { - cache: 'no-cache', -}).then( - async (response) => response.json() as Promise<BlitzkriegTankDefinitions>, -); - -const entries = new Promise<BlitzkriegTankDefinition[]>(async (resolve) => { - resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry)); -}); -export const tanksDefinitionsArray = new Promise<BlitzkriegTankDefinition[]>( - async (resolve) => { - resolve((await entries).map((entry) => entry)); - }, -); -export const tankNames = new Promise<string[]>(async (resolve) => { - resolve((await entries).map(({ name }) => name)); -}); -export const tankNamesDiacritics = tankNames.then((tankNames) => - Promise.all( - tankNames.map(async (tankName, index) => { - const { id } = (await tanksDefinitionsArray)[index]; - const name = tankName ?? `Unknown Tank ${id}`; - const diacriticless = deburr(name); - - return { - original: name, - diacriticless, - combined: `${name}${diacriticless}`, - id, - }; - }), - ), -); - -export const TANK_ICONS: Record<TankType, string> = { - tank_destroyer: 'https://i.imgur.com/BIHSEH0.png', - light: 'https://i.imgur.com/CSNha5V.png', - medium: 'https://i.imgur.com/wvf3ltm.png', - heavy: 'https://i.imgur.com/ECeqlZa.png', -}; - -export const TANK_ICONS_PREMIUM: Record<TankType, string> = { - tank_destroyer: 'https://i.imgur.com/TCu3EdR.png', - light: 'https://i.imgur.com/zdkpTRb.png', - medium: 'https://i.imgur.com/3z7eHX6.png', - heavy: 'https://i.imgur.com/P3vbmyA.png', -}; - -export const TANK_ICONS_COLLECTOR: Record<TankType, string> = { - tank_destroyer: 'https://i.imgur.com/WTjeirB.png', - light: 'https://i.imgur.com/EwhtKkU.png', - medium: 'https://i.imgur.com/u8YDMBh.png', - heavy: 'https://i.imgur.com/8xRf3nc.png', -}; - -export const TIERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const; - -export type Tier = (typeof TIERS)[number]; - -export const TIER_ROMAN_NUMERALS: Record<Tier, string> = { - 1: 'I', - 2: 'II', - 3: 'III', - 4: 'IV', - 5: 'V', - 6: 'VI', - 7: 'VII', - 8: 'VIII', - 9: 'IX', - 10: 'X', -}; - -export const NATIONS = tanksDefinitionsArray.then((tanks) => { - const nationsObject: Record<string, true> = {}; - - tanks.forEach(({ nation }) => { - nationsObject[nation] = true; - }); - - return Object.keys(nationsObject); -}); diff --git a/src/core/blitzkrieg/definitions/turrets.ts b/src/core/blitzkrieg/definitions/turrets.ts deleted file mode 100644 index 7399a09c3..000000000 --- a/src/core/blitzkrieg/definitions/turrets.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { asset } from '../asset'; -import { Tier } from './tanks'; - -export interface BlitzkriegTurretDefinition { - id: number; - name: string; - tier: Tier; - yaw: [number, number]; - guns: number[]; -} - -export type BlitzkriegTurretDefinitions = Record< - number, - BlitzkriegTurretDefinition ->; - -export const turretDefinitions = fetch(asset('definitions/turrets.json'), { - cache: 'no-cache', -}).then( - async (response) => response.json() as Promise<BlitzkriegTurretDefinitions>, -); diff --git a/src/core/blitzkrieg/degressiveStat.ts b/src/core/blitzkrieg/degressiveStat.ts new file mode 100644 index 000000000..7fdcac47a --- /dev/null +++ b/src/core/blitzkrieg/degressiveStat.ts @@ -0,0 +1,3 @@ +export function degressiveStat(crew: number) { + return 0.875 / (0.00375 * 100 * crew + 0.5) - 1; +} diff --git a/src/core/blitzkrieg/equipmentDefinitions.ts b/src/core/blitzkrieg/equipmentDefinitions.ts new file mode 100644 index 000000000..65b032f40 --- /dev/null +++ b/src/core/blitzkrieg/equipmentDefinitions.ts @@ -0,0 +1,27 @@ +import { asset } from './asset'; +import { fetchCdonLz4 } from './fetchCdonLz4'; + +type EquipmentOptions = [number, number]; + +export type EquipmentRow = [ + EquipmentOptions, + EquipmentOptions, + EquipmentOptions, +]; + +export type EquipmentPreset = [EquipmentRow, EquipmentRow, EquipmentRow]; + +export interface EquipmentDefinitions { + presets: { + [key: string]: EquipmentPreset; + }; + equipments: { + [key: number]: { + name: string; + }; + }; +} + +export const equipmentDefinitions = fetchCdonLz4<EquipmentDefinitions>( + asset('definitions/equipment.cdon.lz4'), +); diff --git a/src/core/blitzkrieg/eventManager.ts b/src/core/blitzkrieg/eventManager.ts new file mode 100644 index 000000000..80eb1de76 --- /dev/null +++ b/src/core/blitzkrieg/eventManager.ts @@ -0,0 +1,14 @@ +export class EventManager<Type> { + private callbacks: Set<(data: Type) => void> = new Set(); + + on(callback: (data: Type) => void) { + this.callbacks.add(callback); + } + off(callback: (data: Type) => void) { + return this.callbacks.delete(callback); + } + + emit(data: Type) { + this.callbacks.forEach((callback) => callback(data)); + } +} diff --git a/src/core/blitzkrieg/fetchCdonLz4.ts b/src/core/blitzkrieg/fetchCdonLz4.ts new file mode 100644 index 000000000..f7d40ac8d --- /dev/null +++ b/src/core/blitzkrieg/fetchCdonLz4.ts @@ -0,0 +1,7 @@ +import { superDecompress } from './superDecompress'; + +export async function fetchCdonLz4<Type>(url: string) { + return await fetch(url, { cache: 'no-cache' }) + .then((response) => response.arrayBuffer()) + .then((buffer) => superDecompress<Type>(new Uint8Array(buffer))); +} diff --git a/src/core/blitzkrieg/filtersToDescription.ts b/src/core/blitzkrieg/filtersToDescription.ts index c7963b5e4..ce5b5a8b0 100644 --- a/src/core/blitzkrieg/filtersToDescription.ts +++ b/src/core/blitzkrieg/filtersToDescription.ts @@ -1,11 +1,7 @@ import { TREE_TYPE_NAMES } from '../../components/Tanks'; import { encyclopediaInfo } from '../blitz/encyclopediaInfo'; import { StatFilters } from '../statistics/filterStats'; -import { - TIER_ROMAN_NUMERALS, - Tier, - tankDefinitions, -} from './definitions/tanks'; +import { TIER_ROMAN_NUMERALS, Tier, tankDefinitions } from './tankDefinitions'; export async function filtersToDescription({ nation, diff --git a/src/core/blitzkrieg/hasEquipment.ts b/src/core/blitzkrieg/hasEquipment.ts new file mode 100644 index 000000000..396153b31 --- /dev/null +++ b/src/core/blitzkrieg/hasEquipment.ts @@ -0,0 +1,20 @@ +import { useDuel } from '../../stores/duel'; +import { equipmentDefinitions } from './equipmentDefinitions'; + +export async function hasEquipment(id: number, antagonist = false) { + const awaitedEquipmentDefinitions = await equipmentDefinitions; + const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!; + const equipmentRows = + awaitedEquipmentDefinitions.presets[member.tank.equipment]; + const equipmentMatrix = + useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!.equipment; + + return equipmentRows.some((equipmentRow, rowIndex) => { + return equipmentRow.some((equipment, columnIndex) => { + const choice = equipmentMatrix[rowIndex][columnIndex]; + if (choice === 0) return false; + const equipped = equipment[choice === -1 ? 0 : 1]; + return equipped === id; + }); + }); +} diff --git a/src/core/blitzkrieg/isDev.ts b/src/core/blitzkrieg/isDev.ts index 9167667ba..4d7e00762 100644 --- a/src/core/blitzkrieg/isDev.ts +++ b/src/core/blitzkrieg/isDev.ts @@ -1,3 +1,10 @@ +const devHostStartNames = ['blitzkrieg-', 'localhost', 'betakrieg', 'devkrieg']; + export default function isDev() { - return process.env.NODE_ENV === 'development'; + return ( + process.env.NODE_ENV === 'development' || + process.env.VERCEL_ENV === 'preview' || + (typeof window !== 'undefined' && + devHostStartNames.some((name) => window.location.host.startsWith(name))) + ); } diff --git a/src/core/blitzkrieg/isLocalhost.ts b/src/core/blitzkrieg/isLocalhost.ts new file mode 100644 index 000000000..0042dcfd9 --- /dev/null +++ b/src/core/blitzkrieg/isLocalhost.ts @@ -0,0 +1,5 @@ +export function isLocalhost() { + return ( + typeof window !== 'undefined' && window.location.hostname === 'localhost' + ); +} diff --git a/src/core/blitzkrieg/jsxToSvg.ts b/src/core/blitzkrieg/jsxToSvg.ts index e0acc1621..a79416be6 100644 --- a/src/core/blitzkrieg/jsxToSvg.ts +++ b/src/core/blitzkrieg/jsxToSvg.ts @@ -1,40 +1,26 @@ -import { readFile } from 'fs'; -import { join } from 'path'; +import { readFile } from 'fs/promises'; import satori from 'satori'; -import robotoBlackFile from '../../assets/fonts/Roboto-Black.ttf'; -import robotoBoldFile from '../../assets/fonts/Roboto-Bold.ttf'; -import robotoFile from '../../assets/fonts/Roboto.ttf'; const FONT_NAME = 'Roboto'; -const FONT_FILES = [robotoFile, robotoBoldFile, robotoBlackFile]; +const FONT_FILES = [ + { weight: 400 as const, path: 'src/assets/fonts/Roboto.ttf' }, + { weight: 700 as const, path: 'src/assets/fonts/Roboto-Bold.ttf' }, + { weight: 900 as const, path: 'src/assets/fonts/Roboto-Black.ttf' }, +]; -let roboto: Buffer; -let robotoBold: Buffer; -let robotoBlack: Buffer; - -Promise.all( - FONT_FILES.map( - (file) => - new Promise<Buffer>((resolve, reject) => { - const path = join(__dirname, file); - - readFile(path, (error, data) => { - if (error) reject(error); - resolve(data); - }); - }), +const fonts = Promise.all( + FONT_FILES.map(({ path, weight }) => + readFile(path).then((data) => ({ + data: Buffer.from(data.buffer), + name: FONT_NAME, + weight, + })), ), -).then((data) => { - [roboto, robotoBold, robotoBlack] = data; -}); +); export default async function jsxToSvg(jsx: JSX.Element) { return await satori(jsx, { width: 480, - fonts: [ - { data: roboto, name: FONT_NAME, weight: 400 }, - { data: robotoBold, name: FONT_NAME, weight: 700 }, - { data: robotoBlack, name: FONT_NAME, weight: 900 }, - ], + fonts: await fonts, }); } diff --git a/src/core/blitzkrieg/jsxTree.tsx b/src/core/blitzkrieg/jsxTree.tsx new file mode 100644 index 000000000..044c4a771 --- /dev/null +++ b/src/core/blitzkrieg/jsxTree.tsx @@ -0,0 +1,31 @@ +import { MeshProps } from '@react-three/fiber'; +import { Mesh, Object3D } from 'three'; + +export function jsxTree(node: Object3D, mesh?: MeshProps, key?: string) { + if (node instanceof Mesh) { + return ( + <mesh + key={key} + geometry={node.geometry} + material={node.material} + position={node.position} + rotation={node.rotation} + scale={node.scale} + {...mesh} + /> + ); + } else if (node instanceof Object3D) { + return ( + <group + key={key} + position={node.position} + rotation={node.rotation} + scale={node.scale} + > + {node.children.map((child) => jsxTree(child, mesh, child.uuid))} + </group> + ); + } + + return null; +} diff --git a/src/core/blitzkrieg/modelDefinitions.ts b/src/core/blitzkrieg/modelDefinitions.ts new file mode 100644 index 000000000..1f49f358f --- /dev/null +++ b/src/core/blitzkrieg/modelDefinitions.ts @@ -0,0 +1,62 @@ +import { Vector3Tuple } from 'three'; +import { asset } from './asset'; +import { fetchCdonLz4 } from './fetchCdonLz4'; + +export type ModelDefinitions = Record<number, ModelDefinition>; + +export interface ModelArmor { + thickness: Record<number, number>; + spaced?: number[]; +} + +export interface BoundingBox { + min: Vector3Tuple; + max: Vector3Tuple; +} +interface ModelDefinition { + armor: ModelArmor; + turretOrigin: Vector3Tuple; + turretRotation?: InitialTurretRotation; + turrets: Record<number, TurretModelDefinition>; + tracks: Record<number, TrackDefinition>; + boundingBox: BoundingBox; +} +interface TrackDefinition { + thickness: number; + origin: Vector3Tuple; +} +export interface InitialTurretRotation { + yaw: number; + pitch: number; + roll: number; +} +interface TurretModelDefinition { + boundingBox: BoundingBox; + armor: ModelArmor; + model: number; + gunOrigin: Vector3Tuple; + guns: Record<number, GunModelDefinition>; + yaw?: YawLimits; +} +export interface YawLimits { + min: number; + max: number; +} +interface GunModelDefinition { + armor: ModelArmor; + thickness: number; + model: number; + pitch: PitchLimits; +} +export interface PitchLimits { + min: number; + max: number; + + front?: { min: number; max: number; range: number }; + back?: { min: number; max: number; range: number }; + transition?: number; +} + +export const modelDefinitions = fetchCdonLz4<ModelDefinitions>( + asset('definitions/models.cdon.lz4'), +); diff --git a/src/core/blitzkrieg/modelTransform.ts b/src/core/blitzkrieg/modelTransform.ts new file mode 100644 index 000000000..1b5487d7f --- /dev/null +++ b/src/core/blitzkrieg/modelTransform.ts @@ -0,0 +1,8 @@ +import { EventManager } from './eventManager'; + +export interface ModelTransformEventData { + yaw?: number; + pitch: number; +} + +export const modelTransformEvent = new EventManager<ModelTransformEventData>(); diff --git a/src/core/blitzkrieg/nameToArmorId.ts b/src/core/blitzkrieg/nameToArmorId.ts new file mode 100644 index 000000000..6ebb6bb15 --- /dev/null +++ b/src/core/blitzkrieg/nameToArmorId.ts @@ -0,0 +1,6 @@ +export function nameToArmorId(name: string) { + const match = name.match(/.+_armor_(-?\d+)/); + const group1 = match![1]; + + return parseInt(group1); +} diff --git a/src/core/blitzkrieg/normalizeBoundingBox.ts b/src/core/blitzkrieg/normalizeBoundingBox.ts new file mode 100644 index 000000000..c29c75056 --- /dev/null +++ b/src/core/blitzkrieg/normalizeBoundingBox.ts @@ -0,0 +1,8 @@ +import { Vector3Tuple } from 'three'; +import { BoundingBox } from './modelDefinitions'; + +export function normalizeBoundingBox(boundingBox: BoundingBox) { + return boundingBox.max.map( + (max, index) => max - boundingBox.min[index], + ) as Vector3Tuple; +} diff --git a/src/core/blitzkrieg/pose.ts b/src/core/blitzkrieg/pose.ts new file mode 100644 index 000000000..00f4e93a0 --- /dev/null +++ b/src/core/blitzkrieg/pose.ts @@ -0,0 +1,9 @@ +import { EventManager } from './eventManager'; + +export enum Pose { + HullDown, + FaceHug, + Default, +} + +export const poseEvent = new EventManager<Pose>(); diff --git a/src/core/blitzkrieg/progressiveStat.ts b/src/core/blitzkrieg/progressiveStat.ts new file mode 100644 index 000000000..59fb0b420 --- /dev/null +++ b/src/core/blitzkrieg/progressiveStat.ts @@ -0,0 +1,3 @@ +export function progressiveStat(crew: number) { + return (0.00375 * 100 * crew + 0.5) / 0.875 - 1; +} diff --git a/src/core/blitzkrieg/provisionDefinitions.ts b/src/core/blitzkrieg/provisionDefinitions.ts new file mode 100644 index 000000000..9f70f90af --- /dev/null +++ b/src/core/blitzkrieg/provisionDefinitions.ts @@ -0,0 +1,19 @@ +import { asset } from './asset'; +import { TankFilterDefinition } from './consumableDefinitions'; +import { fetchCdonLz4 } from './fetchCdonLz4'; + +export interface ProvisionEntry { + id: number; + name: string; + include: TankFilterDefinition[]; + exclude?: TankFilterDefinition[]; + crew?: number; +} + +export interface ProvisionDefinitions { + [key: string]: ProvisionEntry; +} + +export const provisionDefinitions = fetchCdonLz4<ProvisionDefinitions>( + asset('definitions/provisions.cdon.lz4'), +); diff --git a/src/core/blitzkrieg/pushTankopediaPath.ts b/src/core/blitzkrieg/pushTankopediaPath.ts new file mode 100644 index 000000000..dc5e17f15 --- /dev/null +++ b/src/core/blitzkrieg/pushTankopediaPath.ts @@ -0,0 +1,3 @@ +export function pushTankopediaPath(id: number) { + window.history.pushState(null, '', `/tools/tankopedia/${id}`); +} diff --git a/src/core/blitzkrieg/readTexture.ts b/src/core/blitzkrieg/readTexture.ts new file mode 100644 index 000000000..81fe709fb --- /dev/null +++ b/src/core/blitzkrieg/readTexture.ts @@ -0,0 +1,110 @@ +import { existsSync } from 'fs'; +import sharp from 'sharp'; +import { Vector3Tuple } from 'three'; +import { readDVPLFile } from '../blitz/readDVPLFile'; +import { DdsReadStream } from '../streams/dds'; +import { PvrReadStream } from '../streams/pvr'; + +export enum TextureMutation { + Normal, + RoughnessMetallicness, + BaseColor, + Miscellaneous, +} + +type ReadTextureOptions = + | { + mutation: + | TextureMutation.Normal + | TextureMutation.RoughnessMetallicness + | TextureMutation.Miscellaneous; + } + | { + mutation: TextureMutation.BaseColor; + /** + * Normalized between 0 and 1 + */ + baseColor: Vector3Tuple; + }; + +export async function readTexture(path: string, options?: ReadTextureOptions) { + const ddsTexturePath = path.replace('.tex', '.dx11.dds.dvpl'); + const isDds = existsSync(ddsTexturePath); + const resolvedTexturePath = isDds + ? ddsTexturePath + : ddsTexturePath.replace('.dds', '.pvr'); + const decompressedDvpl = await readDVPLFile(resolvedTexturePath); + + const raw = isDds + ? await new DdsReadStream(decompressedDvpl.buffer).dds() + : new PvrReadStream(decompressedDvpl.buffer).pvr(); + const channels = 4 * raw.width * raw.height; + + switch (options?.mutation) { + case TextureMutation.Normal: { + for (let index = 0; index < channels; index += 4) { + /** + * Red is always 255 and blue is always 0. Only alpha and green contain any + * sort of information. + */ + let x = raw.data[index + 3] * (2 / 255) - 1; + let y = raw.data[index + 1] * (2 / 255) - 1; + const underSqrt = 1 - x ** 2 - y ** 2; + let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt); + + raw.data[index] = Math.round((x + 1) * (255 / 2)); + raw.data[index + 1] = Math.round((y + 1) * (255 / 2)); + raw.data[index + 2] = Math.round((z + 1) * (255 / 2)); + raw.data[index + 3] = 255; + } + + break; + } + + case TextureMutation.RoughnessMetallicness: { + for (let index = 0; index < channels; index += 4) { + /** + * Same channel situation as normal maps. + */ + const metallicness = raw.data[index + 1]; + const roughness = raw.data[index + 3]; + + raw.data[index] = 0; + raw.data[index + 1] = roughness; + raw.data[index + 2] = metallicness; + raw.data[index + 3] = 255; + } + + break; + } + + case TextureMutation.Miscellaneous: { + for (let index = 0; index < channels; index += 4) { + /** + * Alpha is ambient occlusion and green is emissive. But very few tanks + * use emissive so I am ignoring it for now. + */ + // const emissive = raw.data[index + 1]; + const occlusion = raw.data[index + 3]; + + raw.data[index] = occlusion; + raw.data[index + 1] = 0; + raw.data[index + 2] = 0; + raw.data[index + 3] = 255; + } + + break; + } + + case TextureMutation.BaseColor: { + for (let index = 0; index < channels; index += 4) { + // alpha map is specularity but I am ignoring it for now + raw.data[index + 3] = 255; + } + + break; + } + } + + return await sharp(raw.data, { raw: raw }).png().toBuffer(); +} diff --git a/src/core/blitzkrieg/resolveDpm.ts b/src/core/blitzkrieg/resolveDpm.ts new file mode 100644 index 000000000..440f610ba --- /dev/null +++ b/src/core/blitzkrieg/resolveDpm.ts @@ -0,0 +1,28 @@ +import { GunDefinition, ShellDefinition } from './tankDefinitions'; + +export function resolveDpm( + gun: GunDefinition, + shell: ShellDefinition, + damageCoefficient = 1, + reloadCoefficient = 1, + interClipCoefficient = 1, +) { + const alpha = shell.damage.armor * damageCoefficient; + + if (gun.type === 'regular') { + return (alpha / (reloadCoefficient * gun.reload)) * 60; + } else if (gun.type === 'autoLoader') { + return ( + ((alpha * gun.count) / + (gun.reload + (gun.count - 1) * gun.interClip * interClipCoefficient)) * + 60 + ); + } else { + return ( + ((alpha * 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/core/blitzkrieg/resolveThickness.ts b/src/core/blitzkrieg/resolveThickness.ts new file mode 100644 index 000000000..a4727c1e1 --- /dev/null +++ b/src/core/blitzkrieg/resolveThickness.ts @@ -0,0 +1,8 @@ +import { ModelArmor } from './modelDefinitions'; + +export function resolveArmor(armor: ModelArmor, index: number) { + const spaced = armor.spaced?.includes(index) ?? false; + const thickness = index === -1 ? 0 : armor.thickness[index] ?? 0; + + return { spaced, thickness }; +} diff --git a/src/core/blitzkrieg/sum.ts b/src/core/blitzkrieg/sum.ts new file mode 100644 index 000000000..30dd6a572 --- /dev/null +++ b/src/core/blitzkrieg/sum.ts @@ -0,0 +1,3 @@ +export function sum(...effects: [boolean, number][]) { + return effects.reduce((accumulator, b) => accumulator + (b[0] ? b[1] : 0), 0); +} diff --git a/src/core/blitzkrieg/superCompress.ts b/src/core/blitzkrieg/superCompress.ts new file mode 100644 index 000000000..5236447e3 --- /dev/null +++ b/src/core/blitzkrieg/superCompress.ts @@ -0,0 +1,8 @@ +import { compress } from 'lz4js'; +import { CdonWriteStream } from '../streams/cdon'; + +export function superCompress(object: any) { + const write = new CdonWriteStream().cdon(object); + const compressed = compress(write.uint8Array); + return Buffer.from(compressed).toString('base64'); +} diff --git a/src/core/blitzkrieg/superDecompress.ts b/src/core/blitzkrieg/superDecompress.ts new file mode 100644 index 000000000..697bab1de --- /dev/null +++ b/src/core/blitzkrieg/superDecompress.ts @@ -0,0 +1,8 @@ +import { decompress } from 'lz4js'; +import { CdonReadStream } from '../streams/cdon'; + +export function superDecompress<Type>(buffer: ArrayBuffer) { + const decompressed = decompress(new Uint8Array(buffer)).buffer; + const read = new CdonReadStream(decompressed); + return read.cdon() as Type; +} diff --git a/src/core/blitzkrieg/svgToPngThreaded.ts b/src/core/blitzkrieg/svgToPngThreaded.ts index 96afc52b5..6340861c3 100644 --- a/src/core/blitzkrieg/svgToPngThreaded.ts +++ b/src/core/blitzkrieg/svgToPngThreaded.ts @@ -3,6 +3,7 @@ import { Worker } from 'worker_threads'; const MAX_WORKERS = 2; const workers = [...Array(MAX_WORKERS)].map(() => { + // const worker = new Worker('src/workers/render.ts'); const worker = new Worker(`${__dirname}/workers/render.cjs`); console.log(`🟡 Launching render worker ${worker.threadId}`); return worker; diff --git a/src/core/blitzkrieg/tankDefinitions.ts b/src/core/blitzkrieg/tankDefinitions.ts new file mode 100644 index 000000000..3cadb52a7 --- /dev/null +++ b/src/core/blitzkrieg/tankDefinitions.ts @@ -0,0 +1,230 @@ +import { deburr } from 'lodash'; +import { Vector2Tuple } from 'three'; +import { TankType, TreeType } from '../../components/Tanks'; +import { asset } from './asset'; +import { fetchCdonLz4 } from './fetchCdonLz4'; + +export type ShellType = 'ap' | 'ap_cr' | 'hc' | 'he'; + +export const SHELL_NAMES: Record<ShellType, string> = { + ap: 'AP', + ap_cr: 'APCR', + hc: 'HEAT', + he: 'HE', +}; + +export type CrewMember = + | 'commander' + | 'radioman' + | 'gunner' + | 'driver' + | 'loader'; +export type TankDefinitions = Record<number, TankDefinition>; +export const CREW_MEMBER_NAMES: Record<CrewMember, string> = { + commander: 'Commander', + radioman: 'Radioman', + gunner: 'Gunner', + driver: 'Driver', + loader: 'Loader', +}; +export interface Crew { + type: CrewMember; + count?: number; + substitute?: CrewMember[]; +} +export interface TankDefinition { + id: number; + fixedCamouflage?: boolean; + crew: Crew[]; + health: number; + nation: string; + name: string; + nameFull?: string; + treeType: TreeType; + consumables: number; + provisions: number; + tier: Tier; + type: TankType; + testing?: boolean; + turrets: TurretDefinition[]; + engines: EngineDefinition[]; + tracks: TrackDefinition[]; + price: TankDefinitionPrice; + speed: { + forwards: number; + backwards: number; + }; + camouflage: { + still: number; + moving: number; + onFire: number; + }; + equipment: string; + weight: number; +} +export interface TrackDefinition { + id: number; + tier: Tier; + weight: number; + traverseSpeed: number; + dispersion: { + move: number; + traverse: number; + }; + resistance: { + hard: number; + medium: number; + soft: number; + }; +} +export interface EngineDefinition { + id: number; + name: string; + tier: Tier; + fireChance: number; + power: number; + weight: number; +} +export type TankDefinitionPrice = + | { type: 'credits'; value: number } + | { type: 'gold'; value: number }; +export interface TurretDefinition { + health: number; + viewRange: number; + traverseSpeed: number; + id: number; + name: string; + tier: Tier; + guns: GunDefinition[]; + weight: number; +} +export type GunDefinition = + | GunDefinitionRegular + | GunDefinitionAutoLoader + | GunDefinitionAutoReloader; +interface GunDefinitionBase { + rotationSpeed: number; + weight: number; + id: number; + name: string; + tier: Tier; + shells: ShellDefinition[]; + camouflageLoss: number; + aimTime: number; + dispersion: { + base: number; + traverse: number; + shot: number; + damaged: number; + }; +} +interface GunDefinitionRegular extends GunDefinitionBase { + type: 'regular'; + reload: number; +} +interface GunDefinitionAutoLoader extends GunDefinitionBase { + type: 'autoLoader'; + reload: number; + interClip: number; + count: number; +} +interface GunDefinitionAutoReloader extends GunDefinitionBase { + type: 'autoReloader'; + reload: number[]; + interClip: number; + count: number; +} +export type ShellDefinition = { + id: number; + name: string; + speed: number; + damage: { armor: number; module: number }; + caliber: number; + icon: string; + penetration: number | Vector2Tuple; + type: ShellType; + normalization?: number; + ricochet?: number; + explosionRadius?: number; +}; + +export const GUN_TYPE_NAMES: Record<GunDefinition['type'], string> = { + regular: 'Regular', + autoLoader: 'Auto loader', + autoReloader: 'Auto reloader', +}; + +export const tankDefinitions = fetchCdonLz4<TankDefinitions>( + asset('definitions/tanks.cdon.lz4'), +); + +const entries = new Promise<TankDefinition[]>(async (resolve) => { + resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry)); +}); +export const tanksDefinitionsArray = new Promise<TankDefinition[]>( + async (resolve) => { + resolve((await entries).map((entry) => entry)); + }, +); +export const tankNames = tanksDefinitionsArray.then((tanks) => + Promise.all( + tanks.map(async (tank, index) => { + const { id } = (await tanksDefinitionsArray)[index]; + + return { + id, + original: tank.name, + combined: `${tank.name}${deburr(tank.name)}${tank.nameFull ? `${tank.nameFull}${deburr(tank.nameFull)}` : ''}`, + }; + }), + ), +); + +export const TANK_ICONS: Record<TankType, string> = { + tankDestroyer: 'https://i.imgur.com/BIHSEH0.png', + light: 'https://i.imgur.com/CSNha5V.png', + medium: 'https://i.imgur.com/wvf3ltm.png', + heavy: 'https://i.imgur.com/ECeqlZa.png', +}; + +export const TANK_ICONS_PREMIUM: Record<TankType, string> = { + tankDestroyer: 'https://i.imgur.com/TCu3EdR.png', + light: 'https://i.imgur.com/zdkpTRb.png', + medium: 'https://i.imgur.com/3z7eHX6.png', + heavy: 'https://i.imgur.com/P3vbmyA.png', +}; + +export const TANK_ICONS_COLLECTOR: Record<TankType, string> = { + tankDestroyer: 'https://i.imgur.com/WTjeirB.png', + light: 'https://i.imgur.com/EwhtKkU.png', + medium: 'https://i.imgur.com/u8YDMBh.png', + heavy: 'https://i.imgur.com/8xRf3nc.png', +}; + +export const TIERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const; + +export type Tier = (typeof TIERS)[number]; + +export const TIER_ROMAN_NUMERALS: Record<Tier, string> = { + 1: 'I', + 2: 'II', + 3: 'III', + 4: 'IV', + 5: 'V', + 6: 'VI', + 7: 'VII', + 8: 'VIII', + 9: 'IX', + 10: 'X', +}; + +export const NATIONS = tanksDefinitionsArray.then((tanks) => { + const nations = Array.from( + new Set<string>(Object.values(tanks).map((tank) => tank.nation)), + ); + + nations.splice(nations.indexOf('other'), 1); + nations.push('other'); + + return nations; +}); diff --git a/src/core/blitzkrieg/tankIcon.ts b/src/core/blitzkrieg/tankIcon.ts index 8ca97d74f..1ebe0aef3 100644 --- a/src/core/blitzkrieg/tankIcon.ts +++ b/src/core/blitzkrieg/tankIcon.ts @@ -1,5 +1,5 @@ import { asset } from './asset'; export function tankIcon(id: number, size: 'big' | 'small' = 'big') { - return asset(`icons/${size}/${id}.webp`); + return asset(`icons/tanks/${size}/${id}.webp`); } diff --git a/src/core/blitzkrieg/unionBoundingBox.ts b/src/core/blitzkrieg/unionBoundingBox.ts new file mode 100644 index 000000000..86d026a0d --- /dev/null +++ b/src/core/blitzkrieg/unionBoundingBox.ts @@ -0,0 +1,9 @@ +import { Vector3Tuple } from 'three'; +import { BoundingBox } from './modelDefinitions'; + +export function unionBoundingBox(a: BoundingBox, b: BoundingBox) { + return { + min: a.min.map((a, index) => Math.min(a, b.min[index])) as Vector3Tuple, + max: a.max.map((a, index) => Math.max(a, b.max[index])) as Vector3Tuple, + } satisfies BoundingBox; +} diff --git a/src/core/blitzstars/tankAverages.ts b/src/core/blitzstars/tankAverages.ts index 1ed7d87a3..7ce28099f 100644 --- a/src/core/blitzstars/tankAverages.ts +++ b/src/core/blitzstars/tankAverages.ts @@ -1,6 +1,6 @@ import { AllStats } from '../blitz/getAccountInfo'; import { context } from '../blitzkrieg/context'; -import { tankDefinitions } from '../blitzkrieg/definitions/tanks'; +import { tankDefinitions } from '../blitzkrieg/tankDefinitions'; export interface SpecialStats { winrate: number; diff --git a/src/core/discord/addFilterOptions.ts b/src/core/discord/addFilterOptions.ts index 1f00e9942..5523016c9 100644 --- a/src/core/discord/addFilterOptions.ts +++ b/src/core/discord/addFilterOptions.ts @@ -1,5 +1,5 @@ import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js'; -import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks'; +import { TREE_TYPE_NAMES, TreeType } from '../../components/Tanks'; import { EncyclopediaInfo } from '../blitz/encyclopediaInfo'; import addTankChoices from './addTankChoices'; import addTierChoices from './addTierChoices'; @@ -41,15 +41,15 @@ export function addFilterOptions< .addChoices( { name: TREE_TYPE_NAMES['researchable'], - value: 'researchable' satisfies TreeTypeString, + value: 'researchable' satisfies TreeType, }, { name: TREE_TYPE_NAMES.premium, - value: 'premium' satisfies TreeTypeString, + value: 'premium' satisfies TreeType, }, { name: TREE_TYPE_NAMES.collector, - value: 'collector' satisfies TreeTypeString, + value: 'collector' satisfies TreeType, }, ) .setRequired(false), diff --git a/src/core/discord/autocompleteTanks.ts b/src/core/discord/autocompleteTanks.ts index 78f1b17ab..479850e26 100644 --- a/src/core/discord/autocompleteTanks.ts +++ b/src/core/discord/autocompleteTanks.ts @@ -4,7 +4,7 @@ import { CacheType, } from 'discord.js'; import { go } from 'fuzzysort'; -import { tankNamesDiacritics } from '../blitzkrieg/definitions/tanks'; +import { tankNames } from '../blitzkrieg/tankDefinitions'; import { DISCORD_CHOICES_MAX_NAME_SIZE, OVERFLOW_SUFFIX, @@ -19,7 +19,7 @@ export default async function autocompleteTanks( await interaction.respond( focusedOption.value ? await Promise.all( - go(focusedOption.value, await tankNamesDiacritics, { + go(focusedOption.value, await tankNames, { keys: ['combined'], limit: 10, }).map(async (item) => { diff --git a/src/core/discord/getFiltersFromButton.ts b/src/core/discord/getFiltersFromButton.ts index 3d906e35b..f1b24829e 100644 --- a/src/core/discord/getFiltersFromButton.ts +++ b/src/core/discord/getFiltersFromButton.ts @@ -1,5 +1,5 @@ import { ButtonInteraction } from 'discord.js'; -import { TreeTypeString } from '../../components/Tanks'; +import { TreeType } from '../../components/Tanks'; import { StatFilters } from '../statistics/filterStats'; export function getFiltersFromButton(interaction: ButtonInteraction) { @@ -10,8 +10,7 @@ export function getFiltersFromButton(interaction: ButtonInteraction) { tank: parseInt(url.searchParams.get('tank') ?? '0') || undefined, tankType: url.searchParams.get('tank-type') ?? undefined, treeType: - (url.searchParams.get('tree-type') as TreeTypeString | undefined) ?? - undefined, + (url.searchParams.get('tree-type') as TreeType | undefined) ?? undefined, tier: parseInt(url.searchParams.get('tier') ?? '0') || undefined, } satisfies StatFilters; } diff --git a/src/core/discord/getFiltersFromCommand.ts b/src/core/discord/getFiltersFromCommand.ts index 3a08f1bad..c958dbc35 100644 --- a/src/core/discord/getFiltersFromCommand.ts +++ b/src/core/discord/getFiltersFromCommand.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction } from 'discord.js'; -import { TreeTypeString } from '../../components/Tanks'; +import { TreeType } from '../../components/Tanks'; import resolveTankId from '../blitz/resolveTankId'; import { StatFilters } from '../statistics/filterStats'; @@ -13,9 +13,8 @@ export async function getFiltersFromCommand( tier: parseInt(interaction.options.getString('tier') ?? '0') || undefined, tankType: interaction.options.getString('tank-type') ?? undefined, treeType: - (interaction.options.getString('tree-type') as - | TreeTypeString - | undefined) ?? undefined, + (interaction.options.getString('tree-type') as TreeType | undefined) ?? + undefined, tank: tankRaw === null ? undefined : await resolveTankId(tankRaw), } satisfies StatFilters; } diff --git a/src/core/discord/resolveClanFromCommand.ts b/src/core/discord/resolveClanFromCommand.ts index 773723449..1efe29028 100644 --- a/src/core/discord/resolveClanFromCommand.ts +++ b/src/core/discord/resolveClanFromCommand.ts @@ -1,8 +1,8 @@ import { ChatInputCommandInteraction } from 'discord.js'; import markdownEscape from 'markdown-escape'; import { Region } from '../../constants/regions'; +import { UserError } from '../../hooks/userError'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; -import { UserError } from '../blitzkrieg/userError'; import { serverAndIdPattern } from './resolvePlayerFromCommand'; export default async function resolveClanFromCommand( diff --git a/src/core/discord/resolvePlayerFromCommand.ts b/src/core/discord/resolvePlayerFromCommand.ts index 132994f31..93d6bc3b9 100644 --- a/src/core/discord/resolvePlayerFromCommand.ts +++ b/src/core/discord/resolvePlayerFromCommand.ts @@ -1,9 +1,9 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import markdownEscape from 'markdown-escape'; import { Region } from '../../constants/regions'; +import { UserError } from '../../hooks/userError'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz'; -import { UserError } from '../blitzkrieg/userError'; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; diff --git a/src/core/github/assetCommit.ts b/src/core/github/assetCommit.ts new file mode 100644 index 000000000..ad5af7ce2 --- /dev/null +++ b/src/core/github/assetCommit.ts @@ -0,0 +1,62 @@ +import { GithubChangeBlob, createBlob } from '../blitzkrieg/createBlob'; +import { octokit } from '../blitzkrieg/octokit'; + +export class AssetCommit { + constructor( + public message: string, + public production: boolean, + ) {} + private blobs: GithubChangeBlob[] = []; + + async add(path: string, content: string, encoding: 'utf-8' | 'base64') { + console.log(`Blob: adding ${path}...`); + + const blob = await createBlob('tresabhi', 'blitzkrieg-assets', { + content, + encoding, + path, + }); + + this.blobs.push(blob!); + } + + async push() { + if (this.blobs.length === 0) return; + + const latestCommitSha = ( + await octokit.git.getRef({ + owner: 'tresabhi', + repo: 'blitzkrieg-assets', + ref: `heads/${this.production ? 'main' : 'dev'}`, + }) + ).data.object.sha; + const treeSha = ( + await octokit.git.getCommit({ + owner: 'tresabhi', + repo: 'blitzkrieg-assets', + commit_sha: latestCommitSha, + }) + ).data.tree.sha; + const blobs: GithubChangeBlob[] = []; + + const { data: treeData } = await octokit.git.createTree({ + owner: 'tresabhi', + repo: 'blitzkrieg-assets', + base_tree: treeSha, + tree: blobs, + }); + const { data: newCommitData } = await octokit.git.createCommit({ + owner: 'tresabhi', + repo: 'blitzkrieg-assets', + message: this.message, + tree: treeData.sha, + parents: [latestCommitSha], + }); + await octokit.git.updateRef({ + owner: 'tresabhi', + repo: 'blitzkrieg-assets', + ref: `heads/${this.production ? 'main' : 'dev'}`, + sha: newCommitData.sha, + }); + } +} diff --git a/src/core/math/normalizeAngle180.ts b/src/core/math/normalizeAngle180.ts new file mode 100644 index 000000000..264995e05 --- /dev/null +++ b/src/core/math/normalizeAngle180.ts @@ -0,0 +1,6 @@ +export function normalizeAnglePI(angle: number) { + const newAngle = angle % (2 * Math.PI); + if (newAngle > Math.PI) return newAngle - 2 * Math.PI; + if (newAngle < -Math.PI) return newAngle + 2 * Math.PI; + return newAngle; +} diff --git a/src/core/statistics/filterStats.ts b/src/core/statistics/filterStats.ts index c48ce83c5..c13845ffc 100644 --- a/src/core/statistics/filterStats.ts +++ b/src/core/statistics/filterStats.ts @@ -1,5 +1,5 @@ -import { TreeTypeString } from '../../components/Tanks'; -import { tankDefinitions } from '../blitzkrieg/definitions/tanks'; +import { TreeType } from '../../components/Tanks'; +import { tankDefinitions } from '../blitzkrieg/tankDefinitions'; import { DiffedTankStats } from '../blitzstars/getStatsInPeriod'; import { tankAverages } from '../blitzstars/tankAverages'; import calculateWN8 from './calculateWN8'; @@ -9,7 +9,7 @@ export interface StatFilters { nation?: string; tier?: number; tankType?: string; - treeType?: TreeTypeString; + treeType?: TreeType; tank?: number; } @@ -31,10 +31,10 @@ export async function filterStats( (filters.tier === undefined || entry.tier === filters.tier) && (filters.tankType === undefined || entry.type === filters.tankType) && (filters.treeType === undefined || - (filters.treeType === 'collector' && entry.tree_type === 'collector') || - (filters.treeType === 'premium' && entry.tree_type === 'premium') || + (filters.treeType === 'collector' && entry.treeType === 'collector') || + (filters.treeType === 'premium' && entry.treeType === 'premium') || (filters.treeType === 'researchable' && - entry.tree_type === 'researchable')) && + entry.treeType === 'researchable')) && (filters.tank === undefined || entry.id === filters.tank) ); }); diff --git a/src/core/statistics/getTierWeights.ts b/src/core/statistics/getTierWeights.ts index 933bd30ee..532b65911 100644 --- a/src/core/statistics/getTierWeights.ts +++ b/src/core/statistics/getTierWeights.ts @@ -1,6 +1,6 @@ import { TierWeightsRecord } from '../../components/TierWeights'; import { AllStats } from '../blitz/getAccountInfo'; -import { tankDefinitions } from '../blitzkrieg/definitions/tanks'; +import { tankDefinitions } from '../blitzkrieg/tankDefinitions'; export async function getTierWeights( stats: Record<number, AllStats>, diff --git a/src/core/streams/buffer.ts b/src/core/streams/buffer.ts new file mode 100644 index 000000000..872c11acb --- /dev/null +++ b/src/core/streams/buffer.ts @@ -0,0 +1,165 @@ +export class ReadStream { + public index = 0; + private dataView: DataView; + private textDecoderAscii = new TextDecoder('ascii'); + private textDecoderUtf8 = new TextDecoder('utf8'); + + constructor(public buffer: ArrayBuffer) { + this.dataView = new DataView(buffer); + } + + seek(size: number) { + this.index += size; + } + read(size: number) { + return this.buffer.slice(this.index, this.index + size); + } + consume(size: number) { + const subarray = this.read(size); + this.seek(size); + return subarray; + } + consumeUint8Array(size: number) { + return new Uint8Array(this.consume(size)); + } + increment(size: number) { + return (this.index += size) - size; + } + + readRemaining() { + return this.read(Number.POSITIVE_INFINITY); + } + consumeRemaining() { + return this.consume(Number.POSITIVE_INFINITY); + } + + byte(size: number) { + return this.consume(size); + } + + ascii(size: number) { + return this.textDecoderAscii.decode(this.consume(size)); + } + utf8(size: number) { + return this.textDecoderUtf8.decode(this.consume(size)); + } + + int8() { + return this.dataView.getInt8(this.increment(1)); + } + int16() { + return this.dataView.getInt16(this.increment(2), true); + } + int32() { + return this.dataView.getInt32(this.increment(4), true); + } + int64() { + return this.dataView.getBigInt64(this.increment(8), true); + } + uint8() { + return this.dataView.getUint8(this.increment(1)); + } + uint16() { + return this.dataView.getUint16(this.increment(2), true); + } + uint32() { + return this.dataView.getUint32(this.increment(4), true); + } + uint64() { + return this.dataView.getBigUint64(this.increment(8), true); + } + float32() { + return this.dataView.getFloat32(this.increment(4), true); + } + float64() { + return this.dataView.getFloat64(this.increment(8), true); + } + + boolean() { + return this.dataView.getUint8(this.increment(1)) === 1; + } +} + +export class WriteStream { + private array: number[] = []; + private textEncoder = new TextEncoder(); + + get uint8Array() { + return new Uint8Array(this.array); + } + + byte(value: Uint8Array) { + this.array.push(...value); + return this; + } + + ascii(value: string) { + for (let index = 0; index < value.length; index++) { + this.array.push(value.charCodeAt(index)); + } + return this; + } + utf8(value: string) { + this.array.push(...this.textEncoder.encode(value)); + return this; + } + + private uintN(bytes: number, value: number) { + for (let index = 0; index < bytes; index++) { + this.array.push((value >>> (8 * index)) & 0xff); + } + return this; + } + uint8(value: number) { + this.uintN(1, value); + return this; + } + uint16(value: number) { + this.uintN(2, value); + return this; + } + uint32(value: number) { + this.uintN(4, value); + return this; + } + uint64(value: bigint) { + for (let index = 0n; index < 8n; index++) { + this.uint8(Number((value >> (8n * index)) & 0xffn)); + } + return this; + } + private intN(bytes: number, value: number) { + for (let index = 0; index < bytes; index++) { + this.array.push((value >> (8 * index)) & 0xff); + } + return this; + } + int8(value: number) { + this.intN(1, value); + return this; + } + int16(value: number) { + this.intN(2, value); + return this; + } + int32(value: number) { + this.intN(4, value); + return this; + } + int64(value: bigint) { + for (let index = 0n; index < 8n; index++) { + this.int8(Number((value >> (8n * index)) & 0xffn)); + } + return this; + } + float32(value: number) { + const array = new Float32Array([value]); + this.array.push(...new Uint8Array(array.buffer)); + return this; + } + float64(value: number) { + const array = new Float64Array([value]); + this.array.push(...new Uint8Array(array.buffer)); + return this; + } +} diff --git a/src/core/streams/cdon.ts b/src/core/streams/cdon.ts new file mode 100644 index 000000000..478b72638 --- /dev/null +++ b/src/core/streams/cdon.ts @@ -0,0 +1,375 @@ +import { times } from 'lodash'; +import { ReadStream, WriteStream } from './buffer'; + +enum ValueType { + Null, + Boolean, + Uint8, + Uint16, + Uint32, + Uint64, + Int8, + Int16, + Int32, + Int64, + Float32, + Float64, + String, + FastString, + Array, + Object, +} + +enum FastStringFormat { + Format8, + Format16, + Format32, +} + +export type CdonValue = + | undefined + | null + | boolean + | number + | bigint + | string + | CdonValue[] + | { [key: string]: CdonValue }; + +export class CdonReadStream extends ReadStream { + cdon() { + this.magic(); + const header = this.header(); + const body = this.body(header.fastStringFormat, header.stringTable); + return body; + } + + magic() { + const magic = this.utf8(4); + + if (magic !== 'CDON') { + throw new Error(`Invalid Cdon magic number: "${magic}"`); + } + } + + header() { + const version = this.uint16(); + const fastStringFormat = this.uint8() as FastStringFormat; + + return { + version, + fastStringFormat, + stringTable: this.stringTable(fastStringFormat), + }; + } + + body( + fastStringFormat: FastStringFormat, + stringTable: Record<number, string>, + ) { + return this.value(fastStringFormat, stringTable); + } + + stringTable(fastStringFormat: FastStringFormat) { + let count: number; + + switch (fastStringFormat) { + case FastStringFormat.Format8: + count = this.uint8(); + break; + + case FastStringFormat.Format16: + count = this.uint16(); + break; + + case FastStringFormat.Format32: + count = this.uint32(); + break; + } + + const strings: string[] = []; + + for (let index = 0; index < count; index++) { + strings.push(this.string()); + } + + const table: Record<number, string> = {}; + times(count, (index) => (table[index] = strings[index])); + + return table; + } + + string() { + const length = this.uint32(); + const string = this.utf8(length); + return string; + } + + value( + fastStringFormat: FastStringFormat, + stringTable: Record<number, string>, + ): CdonValue { + const type = this.uint8() as ValueType; + + switch (type) { + case ValueType.Null: + return null; + + case ValueType.Boolean: + return this.boolean(); + + case ValueType.Uint8: + return this.uint8(); + + case ValueType.Uint16: + return this.uint16(); + + case ValueType.Uint32: + return this.uint32(); + + case ValueType.Uint64: + return this.uint64(); + + case ValueType.Int8: + return this.int8(); + + case ValueType.Int16: + return this.int16(); + + case ValueType.Int32: + return this.int32(); + + case ValueType.Int64: + return this.int64(); + + case ValueType.Float32: + return this.float32(); + + case ValueType.Float64: + return this.float64(); + + case ValueType.String: + return this.string(); + + case ValueType.FastString: { + switch (fastStringFormat) { + case FastStringFormat.Format8: + return stringTable[this.uint8()]; + + case FastStringFormat.Format16: + return stringTable[this.uint16()]; + + case FastStringFormat.Format32: + return stringTable[this.uint32()]; + } + } + + case ValueType.Array: + return times(this.uint32(), () => + this.value(fastStringFormat, stringTable), + ); + + case ValueType.Object: { + const count = this.uint32(); + const keys = times( + count, + () => this.value(fastStringFormat, stringTable) as string, + ); + const values = times(count, () => + this.value(fastStringFormat, stringTable), + ); + const object: Record<string, CdonValue> = {}; + + keys.forEach((string, index) => (object[string] = values[index])); + + return object; + } + + default: + throw new Error(`Invalid value type: ${type}`); + } + } +} + +export class CdonWriteStream extends WriteStream { + cdon(object: CdonValue) { + this.magic(); + const header = this.header(object); + this.body(header.fastStringFormat, object, header.stringTable); + return this; + } + + body( + fastStringFormat: FastStringFormat, + object: CdonValue, + stringTable: Map<string, number>, + ) { + this.value(fastStringFormat, object, stringTable); + return this; + } + + value( + fastStringFormat: FastStringFormat, + object: CdonValue, + stringTable: Map<string, number>, + ) { + if (object === null || object === undefined) { + this.uint8(ValueType.Null); + } else if (typeof object === 'boolean') { + this.uint8(ValueType.Boolean); + this.uint8(object ? 1 : 0); + } else if (typeof object === 'number') { + if (object % 1 === 0) { + if (object < 0) { + if (object < -0x80000000) { + this.uint8(ValueType.Int64); + this.int64(BigInt(object)); + } else if (object < -0x8000) { + this.uint8(ValueType.Int32); + this.int32(object); + } else if (object < -0x80) { + this.uint8(ValueType.Int16); + this.int16(object); + } else { + this.uint8(ValueType.Int8); + this.int8(object); + } + } else { + if (object > 0x7fffffff) { + this.uint8(ValueType.Uint64); + this.uint64(BigInt(object)); + } else if (object > 0x7fff) { + this.uint8(ValueType.Uint32); + this.uint32(object); + } else if (object > 0x7f) { + this.uint8(ValueType.Uint16); + this.uint16(object); + } else { + this.uint8(ValueType.Uint8); + this.uint8(object); + } + } + } else { + if (object < -0x80000000 || object > 0x7fffffff) { + this.uint8(ValueType.Float64); + this.float64(object); + } else { + this.uint8(ValueType.Float32); + this.float32(object); + } + } + } else if (typeof object === 'bigint') { + if (object < 0) { + this.int64(object); + } else { + this.uint64(object); + } + } else if (typeof object === 'string') { + if (stringTable.has(object)) { + this.uint8(ValueType.FastString); + const id = stringTable.get(object)!; + switch (fastStringFormat) { + case FastStringFormat.Format8: + this.uint8(id); + break; + + case FastStringFormat.Format16: + this.uint16(id); + break; + + case FastStringFormat.Format32: + this.uint32(id); + } + } else { + this.uint8(ValueType.String); + this.string(object); + } + } else if (Array.isArray(object)) { + this.uint8(ValueType.Array); + this.uint32(object.length); + object.forEach((value) => + this.value(fastStringFormat, value, stringTable), + ); + } else { + const entries = Object.entries(object).filter( + ([, value]) => value !== undefined, + ); + + this.uint8(ValueType.Object); + this.uint32(entries.length); + entries.forEach(([key]) => + this.value(fastStringFormat, key, stringTable), + ); + entries.forEach(([, value]) => + this.value(fastStringFormat, value, stringTable), + ); + } + } + + header(object: CdonValue) { + this.uint16(1); + return this.stringTable(object); + } + + stringTable(object: CdonValue) { + const stringTable = this.buildStringTable( + this.collectStringCounts(object, new Map()), + ); + let fastStringFormat: FastStringFormat; + + if (stringTable.size <= 0xff) { + fastStringFormat = FastStringFormat.Format8; + this.uint8(FastStringFormat.Format8); + this.uint8(stringTable.size); + } else if (stringTable.size <= 0xffff) { + fastStringFormat = FastStringFormat.Format16; + this.uint8(FastStringFormat.Format16); + this.uint16(stringTable.size); + } else { + fastStringFormat = FastStringFormat.Format32; + this.uint8(FastStringFormat.Format32); + this.uint32(stringTable.size); + } + + stringTable.forEach((_index, string) => this.string(string)); + + return { fastStringFormat, stringTable }; + } + + buildStringTable(stringCounts: Map<string, number>) { + const stringTable = new Map<string, number>(); + + stringCounts.forEach((count, string) => { + if (count > 1) stringTable.set(string, stringTable.size); + }); + + return stringTable; + } + + collectStringCounts(object: CdonValue, stringCounts: Map<string, number>) { + if (typeof object === 'string') { + if (stringCounts.has(object)) { + stringCounts.set(object, stringCounts.get(object)! + 1); + } else { + stringCounts.set(object, 1); + } + } else if (object instanceof Array) { + object.forEach((value) => this.collectStringCounts(value, stringCounts)); + } else if (object instanceof Object) { + Object.entries(object).forEach((strings) => + this.collectStringCounts(strings, stringCounts), + ); + } + + return stringCounts; + } + + string(value: string) { + this.uint32(Buffer.byteLength(value)); + this.utf8(value); + } + + magic() { + this.utf8('CDON'); + } +} diff --git a/src/core/streams/dds.ts b/src/core/streams/dds.ts new file mode 100644 index 000000000..a57bf589a --- /dev/null +++ b/src/core/streams/dds.ts @@ -0,0 +1,487 @@ +import { times } from 'lodash'; +import { Vector3Tuple, Vector4Tuple } from 'three'; +import { WindowsReadStream } from './windows'; + +enum DxgiFormat { + UNKNOWN = 0, + R32G32B32A32_TYPELESS = 1, + R32G32B32A32_FLOAT = 2, + R32G32B32A32_UINT = 3, + R32G32B32A32_SINT = 4, + R32G32B32_TYPELESS = 5, + R32G32B32_FLOAT = 6, + R32G32B32_UINT = 7, + R32G32B32_SINT = 8, + R16G16B16A16_TYPELESS = 9, + R16G16B16A16_FLOAT = 10, + R16G16B16A16_UNORM = 11, + R16G16B16A16_UINT = 12, + R16G16B16A16_SNORM = 13, + R16G16B16A16_SINT = 14, + R32G32_TYPELESS = 15, + R32G32_FLOAT = 16, + R32G32_UINT = 17, + R32G32_SINT = 18, + R32G8X24_TYPELESS = 19, + D32_FLOAT_S8X24_UINT = 20, + R32_FLOAT_X8X24_TYPELESS = 21, + X32_TYPELESS_G8X24_UINT = 22, + R10G10B10A2_TYPELESS = 23, + R10G10B10A2_UNORM = 24, + R10G10B10A2_UINT = 25, + R11G11B10_FLOAT = 26, + R8G8B8A8_TYPELESS = 27, + R8G8B8A8_UNORM = 28, + R8G8B8A8_UNORM_SRGB = 29, + R8G8B8A8_UINT = 30, + R8G8B8A8_SNORM = 31, + R8G8B8A8_SINT = 32, + R16G16_TYPELESS = 33, + R16G16_FLOAT = 34, + R16G16_UNORM = 35, + R16G16_UINT = 36, + R16G16_SNORM = 37, + R16G16_SINT = 38, + R32_TYPELESS = 39, + D32_FLOAT = 40, + R32_FLOAT = 41, + R32_UINT = 42, + R32_SINT = 43, + R24G8_TYPELESS = 44, + D24_UNORM_S8_UINT = 45, + R24_UNORM_X8_TYPELESS = 46, + X24_TYPELESS_G8_UINT = 47, + R8G8_TYPELESS = 48, + R8G8_UNORM = 49, + R8G8_UINT = 50, + R8G8_SNORM = 51, + R8G8_SINT = 52, + R16_TYPELESS = 53, + R16_FLOAT = 54, + D16_UNORM = 55, + R16_UNORM = 56, + R16_UINT = 57, + R16_SNORM = 58, + R16_SINT = 59, + R8_TYPELESS = 60, + R8_UNORM = 61, + R8_UINT = 62, + R8_SNORM = 63, + R8_SINT = 64, + A8_UNORM = 65, + R1_UNORM = 66, + R9G9B9E5_SHAREDEXP = 67, + R8G8_B8G8_UNORM = 68, + G8R8_G8B8_UNORM = 69, + BC1_TYPELESS = 70, + BC1_UNORM = 71, + BC1_UNORM_SRGB = 72, + BC2_TYPELESS = 73, + BC2_UNORM = 74, + BC2_UNORM_SRGB = 75, + BC3_TYPELESS = 76, + BC3_UNORM = 77, + BC3_UNORM_SRGB = 78, + BC4_TYPELESS = 79, + BC4_UNORM = 80, + BC4_SNORM = 81, + BC5_TYPELESS = 82, + BC5_UNORM = 83, + BC5_SNORM = 84, + B5G6R5_UNORM = 85, + B5G5R5A1_UNORM = 86, + B8G8R8A8_UNORM = 87, + B8G8R8X8_UNORM = 88, + R10G10B10_XR_BIAS_A2_UNORM = 89, + B8G8R8A8_TYPELESS = 90, + B8G8R8A8_UNORM_SRGB = 91, + B8G8R8X8_TYPELESS = 92, + B8G8R8X8_UNORM_SRGB = 93, + BC6H_TYPELESS = 94, + BC6H_UF16 = 95, + BC6H_SF16 = 96, + BC7_TYPELESS = 97, + BC7_UNORM = 98, + BC7_UNORM_SRGB = 99, + AYUV = 100, + Y410 = 101, + Y416 = 102, + NV12 = 103, + P010 = 104, + P016 = 105, + '420_OPAQUE' = 106, + YUY2 = 107, + Y210 = 108, + Y216 = 109, + NV11 = 110, + AI44 = 111, + IA44 = 112, + P8 = 113, + A8P8 = 114, + B4G4R4A4_UNORM = 115, + P208 = 130, + V208 = 131, + V408 = 132, + SAMPLER_FEEDBACK_MIN_MIP_OPAQUE, + SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE, + FORCE_UINT = 0xffffffff, +} + +enum DdsDimension { + UNKNOWN = 0, + BUFFER = 1, + TEXTURE1D = 2, + TEXTURE2D = 3, + TEXTURE3D = 4, +} + +enum DdsdFlags { + CAPS = 0x1, + HEIGHT = 0x2, + WIDTH = 0x4, + PITCH = 0x8, + PIXELFORMAT = 0x1000, + MIPMAPCOUNT = 0x20000, + LINEARSIZE = 0x80000, + DEPTH = 0x800000, +} + +enum DdpfFlags { + ALPHA = 0x1, + FOURCC = 0x4, + RGB = 0x40, + YUV = 0x200, + LUMINANCE = 0x20000, +} + +enum DdsCaps { + COMPLEX = 0x8, + MIPMAP = 0x400000, + TEXTURE = 0x1000, +} + +enum DdsCaps2 { + CUBEMAP = 0x200, + CUBEMAP_POSITIVEX = 0x400, + CUBEMAP_NEGATIVEX = 0x800, + CUBEMAP_POSITIVEY = 0x1000, + CUBEMAP_NEGATIVEY = 0x2000, + CUBEMAP_POSITIVEZ = 0x4000, + CUBEMAP_NEGATIVEZ = 0x8000, + VOLUME = 0x200000, +} + +export class DdsReadStream extends WindowsReadStream { + async dds() { + this.magicNumber(); + const header = this.header(); + const dxgiFormat = this.resolveDxgiFormat(header); + + switch (dxgiFormat) { + case DxgiFormat.BC1_TYPELESS: + case DxgiFormat.BC1_UNORM: + case DxgiFormat.BC1_UNORM_SRGB: { + return this.iterateBlocks(header.width, header.height, () => { + const colorInterpolations = this.bc1ColorInterpolations(); + const colorKeys = this.bc1ColorKeys(); + return (index) => colorInterpolations[colorKeys[index]]; + }); + } + + case DxgiFormat.BC2_TYPELESS: + case DxgiFormat.BC2_UNORM: + case DxgiFormat.BC2_UNORM_SRGB: { + return this.iterateBlocks(header.width, header.height, () => { + const alphaInterpolations = this.bc2AlphaInterpolations(); + const colorInterpolations = this.bc2ColorInterpolations(); + const colorKeys = this.bc1ColorKeys(); + return (index) => { + return [ + ...colorInterpolations[colorKeys[index]], + alphaInterpolations[index], + ]; + }; + }); + } + + case DxgiFormat.BC3_TYPELESS: + case DxgiFormat.BC3_UNORM: + case DxgiFormat.BC3_UNORM_SRGB: { + return this.iterateBlocks(header.width, header.height, () => { + const alphaInterpolations = this.bc3AlphaInterpolations(); + const alphaKeys = this.bc3AlphaKeys(); + const colorInterpolations = this.bc2ColorInterpolations(); + const colorKeys = this.bc1ColorKeys(); + return (index) => [ + ...colorInterpolations[colorKeys[index]], + alphaInterpolations[alphaKeys[index]], + ]; + }); + } + + default: + throw new TypeError( + `Unsupported DXGI format: ${DxgiFormat[dxgiFormat]} (${dxgiFormat})`, + ); + } + } + + magicNumber() { + const magic = this.dword(); + + if (magic !== 0x20534444) { + throw new Error(`Invalid DDS magic number: 0x${magic.toString(16)}`); + } + + return magic; + } + pixelFormat() { + return { + size: this.dword(), + flags: this.dword() as DdpfFlags, + fourCC: this.ascii(4), + rgbBitCount: this.dword(), + rBitMask: this.dword(), + gBitMask: this.dword(), + bBitMask: this.dword(), + aBitMask: this.dword(), + }; + } + header() { + return { + size: this.dword(), + flags: this.dword() as DdsdFlags, + height: this.dword(), + width: this.dword(), + pitchOrLinearSize: this.dword(), + depth: this.dword(), + mipMapCount: this.dword(), + reserved1: times(11, () => this.dword()), + pf: this.pixelFormat(), + caps: this.dword() as DdsCaps, + caps2: this.dword() as DdsCaps2, + caps3: this.dword(), + caps4: this.dword(), + reserved2: times(1, () => this.dword()), + }; + } + headerDxt10() { + return { + dxgiFormat: this.dword() as DxgiFormat, + resourceDimension: this.dword() as DdsDimension, + miscFlag: this.dword(), + arraySize: this.dword(), + miscFlags2: this.dword(), + }; + } + + resolveDxgiFormat(header: ReturnType<typeof this.header>) { + switch (header.pf.fourCC) { + case 'DXT1': + return DxgiFormat.BC1_UNORM; + + case 'DXT3': + return DxgiFormat.BC2_UNORM; + + case 'DXT5': + return DxgiFormat.BC3_UNORM; + + case 'DX10': + return this.headerDxt10().dxgiFormat; + + default: + throw new TypeError(`Unsupported FourCC: ${header.pf.fourCC}`); + } + } + + assetFactor4(width: number, height: number) { + if (width % 4 !== 0 || height % 4 !== 0) { + throw new RangeError('Width and height must be divisible by 4'); + } + } + iterateBlocks( + width: number, + height: number, + blockIterator: () => (index: number) => Vector4Tuple, + ) { + this.assetFactor4(width, height); + const data = Buffer.alloc(width * height * 4); + const blocksX = width / 4; + const blocksY = height / 4; + + times(blocksX * blocksY, (index) => { + const blockPosX = (index % blocksX) * 4; + const blockPosY = Math.floor(index / blocksX) * 4; + const pixelIterator = blockIterator(); + + times(16, (pixelIndex) => { + const pixelPosX = pixelIndex % 4; + const pixelPosY = Math.floor(pixelIndex / 4); + const x = blockPosX + pixelPosX; + const y = blockPosY + pixelPosY; + const bufferIndex = 4 * (y * width + x); + + pixelIterator(pixelIndex).forEach((channel, index) => { + data[bufferIndex + index] = Math.round(channel * 255); + }); + }); + }); + + return { data, width, height, channels: 4 as const }; + } + + readR5G6B5Int() { + return new DataView(this.read(2)).getUint16(0, true); + } + R5G6B5() { + const buffer = this.consumeUint8Array(2); + const r = (buffer[1] & 0b11111000) >>> 3; + const g = ((buffer[1] & 0b111) << 3) | ((buffer[0] & 0b11100000) >>> 5); + const b = buffer[0] & 0b11111; + + return [r / 31, g / 63, b / 31] as Vector3Tuple; + } + R5G6B5A0() { + return [...this.R5G6B5(), 1] as Vector4Tuple; + } + A8() { + return this.uint8() / 255; + } + + bc1ColorInterpolations() { + const int0 = this.readR5G6B5Int(); + const color0 = this.R5G6B5A0(); + const int1 = this.readR5G6B5Int(); + const color1 = this.R5G6B5A0(); + const alpha = int0 < int1; + const color2 = alpha + ? (color0.map( + (channel0, index) => (channel0 + color1[index]) / 2, + ) as Vector4Tuple) + : (color0.map( + (channel0, index) => (2 * channel0 + color1[index]) / 3, + ) as Vector4Tuple); + const color3 = alpha + ? ([0, 0, 0, 0] as Vector4Tuple) + : (color0.map( + (channel0, index) => (channel0 + 2 * color1[index]) / 3, + ) as Vector4Tuple); + + return { + [0b00]: color0, + [0b01]: color1, + [0b10]: color2, + [0b11]: color3, + } as Record<number, Vector4Tuple>; + } + bc1ColorKeys() { + const buffer = this.consumeUint8Array(4); + const d = (buffer[0] & 0b11000000) >>> 6; + const c = (buffer[0] & 0b110000) >>> 4; + const b = (buffer[0] & 0b1100) >>> 2; + const a = buffer[0] & 0b11; + const h = (buffer[1] & 0b11000000) >>> 6; + const g = (buffer[1] & 0b110000) >>> 4; + const f = (buffer[1] & 0b1100) >>> 2; + const e = buffer[1] & 0b11; + const l = (buffer[2] & 0b11000000) >>> 6; + const k = (buffer[2] & 0b110000) >>> 4; + const j = (buffer[2] & 0b1100) >>> 2; + const i = buffer[2] & 0b11; + const p = (buffer[3] & 0b11000000) >>> 6; + const o = (buffer[3] & 0b110000) >>> 4; + const n = (buffer[3] & 0b1100) >>> 2; + const m = buffer[3] & 0b11; + + return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]; + } + bc2AlphaInterpolations() { + const buffer = this.consumeUint8Array(8); + const d = (buffer[1] & 0b11110000) >>> 4; + const c = buffer[1] & 0b1111; + const b = (buffer[0] & 0b11110000) >>> 4; + const a = buffer[0] & 0b1111; + const h = (buffer[3] & 0b11110000) >>> 4; + const g = buffer[3] & 0b1111; + const f = (buffer[2] & 0b11110000) >>> 4; + const e = buffer[2] & 0b1111; + const l = (buffer[5] & 0b11110000) >>> 4; + const k = buffer[5] & 0b1111; + const j = (buffer[4] & 0b11110000) >>> 4; + const i = buffer[4] & 0b1111; + const p = (buffer[7] & 0b11110000) >>> 4; + const o = buffer[7] & 0b1111; + const n = (buffer[6] & 0b11110000) >>> 4; + const m = buffer[6] & 0b1111; + + return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p].map( + (alpha) => alpha / 15, + ); + } + bc2ColorInterpolations() { + const color0 = this.R5G6B5(); + const color1 = this.R5G6B5(); + const color2 = color0.map( + (channel0, index) => (2 * channel0 + color1[index]) / 3, + ) as Vector3Tuple; + const color3 = color0.map( + (channel0, index) => (channel0 + 2 * color1[index]) / 3, + ) as Vector3Tuple; + + return { + [0b00]: color0, + [0b01]: color1, + [0b10]: color2, + [0b11]: color3, + } as Record<number, Vector3Tuple>; + } + bc3AlphaInterpolations() { + const alpha0 = this.A8(); + const alpha1 = this.A8(); + const full = alpha0 > alpha1; + + return { + [0b000]: alpha0, + [0b001]: alpha1, + + ...(full + ? { + [0b010]: (6 * alpha0 + 1 * alpha1) / 7, + [0b011]: (5 * alpha0 + 2 * alpha1) / 7, + [0b100]: (4 * alpha0 + 3 * alpha1) / 7, + [0b101]: (3 * alpha0 + 4 * alpha1) / 7, + [0b110]: (2 * alpha0 + 5 * alpha1) / 7, + [0b111]: (1 * alpha0 + 6 * alpha1) / 7, + } + : { + [0b010]: (4 * alpha0 + 1 * alpha1) / 5, + [0b011]: (3 * alpha0 + 2 * alpha1) / 5, + [0b100]: (2 * alpha0 + 3 * alpha1) / 5, + [0b101]: (1 * alpha0 + 4 * alpha1) / 5, + [0b110]: 0, + [0b111]: 255, + }), + } as Record<number, number>; + } + bc3AlphaKeys() { + const buffer = this.consumeUint8Array(6); + const h = (buffer[2] & 0b11100000) >>> 5; + const g = (buffer[2] & 0b11100) >>> 2; + const f = ((buffer[2] & 0b11) << 1) | ((buffer[1] & 0b10000000) >>> 7); + const e = (buffer[1] & 0b1110000) >>> 4; + const d = (buffer[1] & 0b1110) >>> 1; + const c = ((buffer[1] & 0b1) << 2) | ((buffer[0] & 0b11000000) >>> 6); + const b = (buffer[0] & 0b111000) >>> 3; + const a = buffer[0] & 0b111; + const p = (buffer[5] & 0b11100000) >>> 5; + const o = (buffer[5] & 0b11100) >>> 2; + const n = ((buffer[5] & 0b11) << 1) | ((buffer[4] & 0b10000000) >>> 7); + const m = (buffer[4] & 0b1110000) >>> 4; + const l = (buffer[4] & 0b1110) >>> 1; + const k = ((buffer[4] & 0b1) << 2) | ((buffer[3] & 0b11000000) >>> 6); + const j = (buffer[3] & 0b111000) >>> 3; + const i = buffer[3] & 0b111; + + return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]; + } +} diff --git a/src/core/streams/pvr.ts b/src/core/streams/pvr.ts new file mode 100644 index 000000000..d23360ddf --- /dev/null +++ b/src/core/streams/pvr.ts @@ -0,0 +1,239 @@ +import { clamp, isEqual, times } from 'lodash'; +import { ReadStream } from './buffer'; + +export enum PvrFlags { + NoFlag = 0, + PreMultiplied = 0x02, +} + +export enum PvrPixelFormat { + PVRTC_2bpp_RGB = 0, + PVRTC_2bpp_RGBA = 1, + PVRTC_4bpp_RGB = 2, + PVRTC_4bpp_RGBA = 3, + PVRTC_II_2bpp = 4, + PVRTC_II_4bpp = 5, + ETC1 = 6, + DXT1_BC1 = 7, + DXT2 = 8, + DXT3_BC2 = 9, + DXT4 = 10, + DXT5_BC3 = 11, + BC4 = 12, + BC5 = 13, + BC6 = 14, + BC7 = 15, + UYVY = 16, + YUY2 = 17, + BW1bpp = 18, + R9G9B9E5_SharedExponent = 19, + RGBG8888 = 20, + GRGB8888 = 21, + ETC2_RGB = 22, + ETC2_RGBA = 23, + ETC2_RGB_A1 = 24, + EAC_R11 = 25, + EAC_RG11 = 26, + ASTC_4x4 = 27, + ASTC_5x4 = 28, + ASTC_5x5 = 29, + ASTC_6x5 = 30, + ASTC_6x6 = 31, + ASTC_8x5 = 32, + ASTC_8x6 = 33, + ASTC_8x8 = 34, + ASTC_10x5 = 35, + ASTC_10x6 = 36, + ASTC_10x8 = 37, + ASTC_10x10 = 38, + ASTC_12x10 = 39, + ASTC_12x12 = 40, + ASTC_3x3x3 = 41, + ASTC_4x3x3 = 42, + ASTC_4x4x3 = 43, + ASTC_4x4x4 = 44, + ASTC_5x4x4 = 45, + ASTC_5x5x4 = 46, + ASTC_5x5x5 = 47, + ASTC_6x5x5 = 48, + ASTC_6x6x5 = 49, + ASTC_6x6x6 = 50, + BASISU_ETC1S = 51, + BASISU_UASTC = 52, + RGBM = 53, + RGBD = 54, +} + +enum PvrColorSpace { + LinearRGB = 0, + sRGB = 1, +} + +enum PvrChannelType { + UnsignedByteNormalised = 0, + SignedByteNormalised = 1, + UnsignedByte = 2, + SignedByte = 3, + UnsignedShortNormalised = 4, + SignedShortNormalised = 5, + UnsignedShort = 6, + SignedShort = 7, + UnsignedIntegerNormalised = 8, + SignedIntegerNormalised = 9, + UnsignedInteger = 10, + SignedInteger = 11, + SignedFloat = 12, + UnsignedFloat = 13, +} + +enum ResolvedBitRatePixelFormat { + R4G4B4A4, + R5G6B5A0, +} + +export class PvrReadStream extends ReadStream { + pvr() { + const header = this.header(); + this.metadata(); + + if (typeof header.pixelFormat === 'number') { + switch (header.pixelFormat) { + default: + throw new TypeError( + `Unhandled pixel format ${PvrPixelFormat[header.pixelFormat]} (${ + header.pixelFormat + })`, + ); + } + } else { + const resolvedPixelFormat = this.resolveBitRatePixelFormat( + header.pixelFormat, + ); + const pixelCount = header.width * header.height; + + switch (resolvedPixelFormat) { + case ResolvedBitRatePixelFormat.R4G4B4A4: { + const data = Buffer.alloc(pixelCount * 4); + + times(pixelCount, (index) => { + const buffer = this.consumeUint8Array(2); + const bufferIndex = index * 4; + const r = ((buffer[1] & 0b11110000) >>> 4) / 15; + const g = (buffer[1] & 0b1111) / 15; + const b = ((buffer[0] & 0b11110000) >>> 4) / 15; + const a = (buffer[0] & 0b1111) / 15; + + data[bufferIndex] = Math.round(clamp(r * 255, 0, 255)); + data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255)); + data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255)); + data[bufferIndex + 3] = Math.round(clamp(a * 255, 0, 255)); + }); + + return { + data, + width: header.width, + height: header.height, + channels: 4 as const, + }; + } + + case ResolvedBitRatePixelFormat.R5G6B5A0: { + const data = Buffer.alloc(pixelCount * 3); + + times(pixelCount, (index) => { + const buffer = this.consumeUint8Array(2); + const bufferIndex = index * 4; + const r = ((buffer[1] & 0b11111000) >>> 3) / 31; + const g = + ((buffer[1] & 0b111) << 3) | + (((buffer[0] & 0b11100000) >>> 5) / 31); + const b = buffer[0] & 0b11111; + + data[bufferIndex] = Math.round(clamp(r * 255, 0, 255)); + data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255)); + data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255)); + }); + + return { + data, + width: header.width, + height: header.height, + channels: 3 as const, + }; + } + + default: + throw new TypeError( + `Unhandled resolved pixel format ${resolvedPixelFormat} (${header.pixelFormat})`, + ); + } + } + } + + header() { + const header = { + version: this.ascii(4), + flags: this.uint32() as PvrFlags, + pixelFormat: this.pixelFormat(), + colorSpace: this.uint32() as PvrColorSpace, + channelType: this.uint32() as PvrChannelType, + height: this.uint32(), + width: this.uint32(), + depth: this.uint32(), + surfaces: this.uint32(), + faces: this.uint32(), + mipMaps: this.uint32(), + metadataSize: this.uint32(), + }; + + if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch'); + + return header; + } + pixelFormat() { + this.seek(4); + const usesPresetFormat = this.uint32() === 0; + this.seek(-8); + + return usesPresetFormat + ? (Number(this.uint64()) as PvrPixelFormat) + : { + order: this.ascii(4), + bitRates: times(4, () => this.uint8()), + }; + } + resolveBitRatePixelFormat( + pixelFormat: Exclude<ReturnType<typeof this.pixelFormat>, number>, + ) { + if (pixelFormat.order === 'rgba') { + if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) { + return ResolvedBitRatePixelFormat.R4G4B4A4; + } else + throw new TypeError( + `Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`, + ); + } else if (pixelFormat.order === 'rgb\0') { + if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) { + return ResolvedBitRatePixelFormat.R5G6B5A0; + } else + throw new TypeError( + `Unhandled rgb bit rate ${pixelFormat.bitRates.join(', ')}`, + ); + } else { + throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`); + } + } + metadata() { + const fourCC = this.ascii(4); + const key = this.uint32(); + const dataSize = this.uint32(); + const data = this.consume(dataSize); + + return { + fourCC, + key, + dataSize, + data, + }; + } +} diff --git a/src/core/streams/sc2.ts b/src/core/streams/sc2.ts new file mode 100644 index 000000000..506aee783 --- /dev/null +++ b/src/core/streams/sc2.ts @@ -0,0 +1,321 @@ +import { Vector3Tuple, Vector4Tuple } from 'three'; +import { ScpgReadStream } from './scpg'; + +export interface SC2 { + '#dataNodes': DataNode[]; + '#hierarchy': Hierarchy[]; + '#sceneComponents': SceneComponents; +} + +type DataNode = { + '##name': 'NMaterial'; + '#id': ArrayBuffer; + materialName: string; + parentMaterialKey?: bigint; + qualityGroup?: string; + fxName?: string; + + textures?: Textures; +} & ( + | ({ + configCount: number; + } & Record<`configArchive_${number}`, ConfigArchive>) + | { + configCount: undefined; + } +); + +interface ConfigArchive { + configName: string; + fxName: string; + properties: Properties; + textureSampleStates: TextureSampleStates; + textures: Textures; + customCullMode?: number; + enabledPresets?: EnabledPresets; + flags?: Flags; +} + +interface EnabledPresets { + AlphaTest: boolean; +} + +interface Flags { + AMBIENT_ATTENUATION_BOX: number; + BLENDING: number; +} + +interface Properties { + alphatestThreshold: ArrayBuffer; + attenuationBoxHalfSize: ArrayBuffer; + attenuationBoxSmoothness: ArrayBuffer; + baseColorFactor?: ArrayBuffer; + decalTileCoordScale?: ArrayBuffer; + inGlossiness: ArrayBuffer; + inSpecularity: ArrayBuffer; + metalFresnelReflectance: ArrayBuffer; + metallicRoughnessFactor?: ArrayBuffer; +} + +interface TextureSampleStates { + albedo: number; + baseColorMap: number; + baseNormalMap: number; + baseRMMap: number; + decalmask?: number; + maskMap: number; + miscMap: number; + normalmap: number; +} + +export interface Textures { + albedo: string; + baseColorMap?: string; + baseNormalMap?: string; + baseRMMap?: string; + decalmask?: string; + maskMap?: string; + miscMap?: string; + normalmap?: string; +} + +export interface Hierarchy { + '##name': 'Entity'; + '#hierarchy'?: Hierarchy[]; + components: Components; + flags: number; + id: bigint; + name: string; +} + +// 0001, 0002, 0003...: Component +type Components = { + count: number; +} & Record<string, Component>; + +// 0001, 0002, 0003... +type RoBatches = Record< + string, + { + '##name': 'RenderBatch'; + 'rb.aabbox': RbAabbox; + 'rb.classname': 'RenderBatch'; + 'rb.datasource': bigint; + 'rb.nmatname': bigint; + 'rb.sortingKey': number; + } +>; + +interface RbAabbox { + minimum: number[]; + maximum: number[]; +} + +export type Component = + | { + 'comp.typename': 'TransformComponent'; + 'tc.localRotation': Vector4Tuple; + 'tc.localScale': Vector3Tuple; + 'tc.localTranslation': Vector3Tuple; + 'tc.worldRotation': Vector4Tuple; + 'tc.worldScale': Vector3Tuple; + 'tc.worldTranslation': Vector3Tuple; + } + | { + 'comp.typename': 'RenderComponent'; + 'rc.renderObj': { + '##name': 'Mesh'; + 'ro.batchCount': number; + 'ro.batches': RoBatches; + 'ro.debugflags': number; + 'ro.flags': number; + 'ro.notShadowOnly': boolean; + 'ro.sOclCull': boolean; + 'ro.sOclIndex': number; + } & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>; + } + | { + 'comp.typename': 'SlotComponent'; + 'sc.attachmentRotation': number[]; + 'sc.attachmentScale': number[]; + 'sc.attachmentTranslation': number[]; + 'sc.configFilePath': string; + 'sc.slotName': string; + 'sc.template': string; + 'sc.typeFiltersCount': number; + } + | { + 'comp.typename': 'LodComponent'; + 'lc.loddist': Record<`distance${number}`, number>; + } + | { + 'comp.typename': 'DecorItemComponent'; + drawOrder: number; + shouldApplyCamo: boolean; + vanishTargetAlpha: number; + vanishWhenArmorShown: boolean; + } + | { + 'comp.typename': 'NewSlotComponent'; + slotItemsCount: number; + slots: { + [key: string]: { + configFilePath: string; + name: string; + transform: { + position: Vector3Tuple; + scale: Vector3Tuple; + rotation: Vector4Tuple; + }; + }; + }; + } + | { + 'comp.typename': 'ScenarioComponent'; + 'scenario.scriptId': string; + } + | ({ + 'comp.typename': 'StateSwitcherComponent'; + 'ssc.activeState': number; + 'ssc.statesCount': number; + } & Record<`ssc.state${number}`, string>) + | { + 'comp.typename': 'CustomPropertiesComponent'; + 'cpc.properties.archive': { + 'editor.donotremove': number; + }; + } + | ({ + 'comp.typename': 'ActionComponent'; + 'ac.actionCount': number; + } & Record< + string, // 0000, 0001, ... + { + 'act.delay': number; + 'act.delayVariation': number; + 'act.entityName': string; + 'act.event': number; + 'act.eventToTrigger': string; + 'act.motionFrameTime': number; + 'act.motionSpeed': number; + 'act.stateActivated': number; + 'act.stopAfterNRepeats': number; + 'act.stopWhenEmpty': boolean; + 'act.switchIndex': number; + 'act.type': number; + 'act.userEventId': string; + } + >) + | { + 'comp.typename': 'ParticleEffectComponent'; + 'pe.clearOnRestart': boolean; + 'pe.effectDuration': number; + 'pe.emitters': { + '##name': string; + 'emitter.data': { + 'emitter.id': `${number}`; + 'emitter.quality': string; + }[]; + 'emitter.position': Vector3Tuple; + 'emitter.rotation': Vector4Tuple; + 'emitter.scale': Vector3Tuple; + }[]; + 'pe.inheritScale': boolean; + 'pe.nestedEmitters': boolean; + 'pe.repeatsCount': number; + 'pe.startFromTime': number; + 'pe.stopWhenEmpty': boolean; + 'pe.version': number; + 'ro.flags': number; + } + | { + 'comp.typename': 'TankElementComponent'; + isDestructible: boolean; + shouldCastShadow: boolean; + shouldGenerateSilhouette: boolean; + } + | { + 'comp.typename': 'MotionComponent'; + 'motion.filepath': string; + 'motion.playbackRate': number; + 'simpleMotion.repeatsCount': number; + } + | { + 'comp.typename': 'SkeletonComponent'; + joints: Record< + string, // 0000, 0001, ... + { + 'joint.bbox.max': Vector3Tuple; + 'joint.bbox.min': Vector3Tuple; + 'joint.bindPose': [ + Vector4Tuple, + Vector4Tuple, + Vector4Tuple, + Vector4Tuple, + ]; + 'joint.invBindPose': [ + Vector4Tuple, + Vector4Tuple, + Vector4Tuple, + Vector4Tuple, + ]; + 'joint.name': string; + 'joint.parentIndex': number; + 'joint.uid': string; + } + >; + jointsCount: number; + } + | { + 'comp.typename': 'AnimationComponent'; + animation: bigint; + animationTimeScale: number; + repeatsCount: number; + } + | { + 'comp.typename': 'SkeletonJointBindingComponent'; + count: number; + map: Record< + string, // 0000, 0001, ... + { + Uid: string; + jointUid: string; + } + >; + }; + +interface SceneComponents { + count: number; +} + +export class Sc2ReadStream extends ScpgReadStream { + header() { + return { + name: this.ascii(4), + version: this.uint32(), + nodeCount: this.uint32(), + }; + } + + versionTags() { + return this.ka(); + } + + descriptor() { + const size = this.uint32(); + const fileType = this.uint32(); + + // other felids that we don't have any info about + this.seek(size - 4); + + return { size, fileType }; + } + + sc2() { + this.header(); + this.versionTags(); + this.descriptor(); + + return this.ka() as SC2; + } +} diff --git a/src/core/streams/scg.ts b/src/core/streams/scg.ts new file mode 100644 index 000000000..2297a799d --- /dev/null +++ b/src/core/streams/scg.ts @@ -0,0 +1,135 @@ +import { ScpgReadStream, VertexAttribute } from './scpg'; + +interface PolygonGroupRaw { + '##name': 'PolygonGroup'; + '#id': ArrayBuffer; + cubeTextureCoordCount: number; + indexCount: number; + indexFormat: 0 | 1; + indices: ArrayBuffer; + packing: 0 | 1; + primitiveCount: number; + rhi_primitiveType: 1 | 2 | 10; + textureCoordCount: number; + vertexCount: number; + vertexFormat: number; + vertices: ArrayBuffer; +} + +type BlitzkriegVertex = { attribute: VertexAttribute; value: number[] }[]; + +interface BlitzkriegPolygonGroup { + vertices: BlitzkriegVertex[]; + indices: number[]; +} + +const vertexAttributesArray = Object.values(VertexAttribute).filter( + (value) => typeof value === 'number', +); + +export const vertexAttributeVectorSizes = { + [VertexAttribute.VERTEX]: 3, + [VertexAttribute.NORMAL]: 3, + [VertexAttribute.COLOR]: 1, + [VertexAttribute.TEXCOORD0]: 2, + [VertexAttribute.TEXCOORD1]: 2, + [VertexAttribute.TEXCOORD2]: 2, + [VertexAttribute.TEXCOORD3]: 2, + [VertexAttribute.TANGENT]: 3, + [VertexAttribute.BINORMAL]: 3, + [VertexAttribute.HARD_JOINTINDEX]: 1, + [VertexAttribute.CUBETEXCOORD0]: 3, + [VertexAttribute.CUBETEXCOORD1]: 3, + [VertexAttribute.CUBETEXCOORD2]: 3, + [VertexAttribute.CUBETEXCOORD3]: 3, + [VertexAttribute.PIVOT4]: 4, + [VertexAttribute.PIVOT_DEPRECATED]: 3, + [VertexAttribute.FLEXIBILITY]: 1, + [VertexAttribute.ANGLE_SIN_COS]: 2, + [VertexAttribute.JOINTINDEX]: 4, + [VertexAttribute.JOINTWEIGHT]: 4, +} as const; + +export class ScgReadStream extends ScpgReadStream { + header() { + return { + name: this.ascii(4), + version: this.uint32(), + nodeCount: this.uint32(), + nodeCount2: this.uint32(), + }; + } + + scg() { + const header = this.header(); + const polygonGroupsRaw: PolygonGroupRaw[] = []; + const polygonGroups = new Map<bigint, BlitzkriegPolygonGroup>(); + + for (let index = 0; index < header.nodeCount; index++) { + const polygonGroupRaw = this.ka() as PolygonGroupRaw; + polygonGroupsRaw.push(polygonGroupRaw); + } + + polygonGroupsRaw.forEach((polygonGroupRaw) => { + const indicesStream = new ScpgReadStream(polygonGroupRaw.indices); + const indices: number[] = []; + + for (let index = 0; index < polygonGroupRaw.indexCount; index++) { + indices.push( + polygonGroupRaw.indexFormat === 0 + ? indicesStream.uint16() + : indicesStream.uint32(), + ); + } + + const verticesStream = new ScpgReadStream(polygonGroupRaw.vertices); + const vertices: BlitzkriegVertex[] = []; + const { format, stride } = ScgReadStream.parseVertexFormat( + polygonGroupRaw.vertexFormat, + ); + const strideBasedSize = stride * polygonGroupRaw.vertexCount; + + if (verticesStream.buffer.byteLength !== strideBasedSize) { + console.warn( + `Vertex stride mismatch; expected ${strideBasedSize}, got ${verticesStream.buffer.byteLength}; skipping...`, + ); + + return; + } + + for (let index = 0; index < polygonGroupRaw.vertexCount; index++) { + vertices[index] = format.map((attribute) => ({ + attribute, + value: verticesStream.vectorN(vertexAttributeVectorSizes[attribute]), + })); + } + + polygonGroups.set( + new DataView(polygonGroupRaw['#id']).getBigUint64(0, true), + { + vertices, + indices, + }, + ); + }); + + return polygonGroups; + } + + static parseVertexFormat(formatInt: number) { + const format: VertexAttribute[] = []; + let stride = 0; + + vertexAttributesArray.forEach((attributeUntyped) => { + const attribute = attributeUntyped as VertexAttribute; + const mask = 1 << attribute; + + if (formatInt & mask) { + format.push(attribute); + stride += vertexAttributeVectorSizes[attribute] * 4; + } + }); + + return { format, stride }; + } +} diff --git a/src/core/streams/scpg.ts b/src/core/streams/scpg.ts new file mode 100644 index 000000000..8d8401b71 --- /dev/null +++ b/src/core/streams/scpg.ts @@ -0,0 +1,263 @@ +import { times } from 'lodash'; +import { ReadStream } from './buffer'; + +enum KAType { + NONE = 0, + BOOLEAN = 1, + INT32 = 2, + FLOAT = 3, + STRING = 4, + WIDE_STRING = 5, + BYTE_ARRAY = 6, + UINT32 = 7, + KEYED_ARCHIVE = 8, + INT64 = 9, + UINT64 = 10, + VECTOR2 = 11, + VECTOR3 = 12, + VECTOR4 = 13, + MATRIX2 = 14, + MATRIX3 = 15, + MATRIX4 = 16, + COLOR = 17, + FASTNAME = 18, + AABBOX3 = 19, + FILEPATH = 20, + FLOAT64 = 21, + INT8 = 22, + UINT8 = 23, + INT16 = 24, + UINT16 = 25, + ARRAY = 27, + TRANSFORM = 29, +} + +export enum VertexAttribute { + VERTEX = 0, + NORMAL = 1, + COLOR = 2, + TEXCOORD0 = 3, + TEXCOORD1 = 4, + TEXCOORD2 = 5, + TEXCOORD3 = 6, + TANGENT = 7, + BINORMAL = 8, + HARD_JOINTINDEX = 9, + PIVOT4 = 10, + PIVOT_DEPRECATED = 11, + FLEXIBILITY = 12, + ANGLE_SIN_COS = 13, + JOINTINDEX = 14, + JOINTWEIGHT = 15, + CUBETEXCOORD0 = 16, + CUBETEXCOORD1 = 17, + CUBETEXCOORD2 = 18, + CUBETEXCOORD3 = 19, +} + +export class ScpgReadStream extends ReadStream { + vectorN(size: number) { + return times(size, () => this.float32()); + } + vector2() { + return this.vectorN(2); + } + vector3() { + return this.vectorN(3); + } + vector4() { + return this.vectorN(4); + } + + matrixN(size: number) { + return times(size, () => this.vectorN(size)); + } + matrix2() { + return this.matrixN(2); + } + matrix3() { + return this.matrixN(3); + } + matrix4() { + return this.matrixN(4); + } + + kaValue( + type: KAType = this.uint8(), + stringTable?: Record<number, string>, + ): unknown { + switch (type) { + case KAType.NONE: + return undefined; + + case KAType.FILEPATH: + case KAType.STRING: { + if (stringTable) { + const id = this.uint32(); + return stringTable[id]; + } else { + const length = this.uint32(); + return this.ascii(length); + } + } + + case KAType.BYTE_ARRAY: { + const length = this.uint32(); + return this.byte(length); + } + + case KAType.FLOAT: + return this.float32(); + + case KAType.ARRAY: { + const length = this.uint32(); + const value = []; + + for (let index = 0; index < length; index++) { + value.push(this.kaValue(undefined, stringTable)); + } + + return value; + } + + case KAType.KEYED_ARCHIVE: { + this.seek(4); // UInt32 length of the nested archive in bytes + + return this.ka(stringTable); + } + + case KAType.INT8: + return this.int8(); + case KAType.INT16: + return this.int16(); + case KAType.INT32: + return this.int32(); + case KAType.INT64: + return this.int64(); + case KAType.UINT8: + return this.uint8(); + case KAType.UINT16: + return this.uint16(); + case KAType.UINT32: + return this.uint32(); + case KAType.UINT64: + return this.uint64(); + + case KAType.BOOLEAN: + return this.boolean(); + + case KAType.VECTOR2: + return this.vector2(); + case KAType.VECTOR3: + return this.vector3(); + case KAType.VECTOR4: + return this.vector4(); + + case KAType.COLOR: + return this.vector4(); // as RGBA + + case KAType.MATRIX2: + return this.matrix2(); + case KAType.MATRIX3: + return this.matrix3(); + case KAType.MATRIX4: + return this.matrix4(); + + case KAType.AABBOX3: { + const minimum = this.vector3(); + const maximum = this.vector3(); + + return { minimum, maximum }; + } + + case KAType.FASTNAME: { + if (!stringTable) throw new Error('No string table provided'); + const index = this.uint32(); + return stringTable[index]; + } + + case KAType.TRANSFORM: { + const position = this.vector3(); + const scale = this.vector3(); + const rotation = this.vector4(); + + return { + position, + scale, + rotation, + }; + } + + default: + throw new TypeError( + `Unhandled KA type: ${type} (${KAType[type] ?? 'unknown type'})`, + ); + } + } + ka(stringTable?: Record<number, string>) { + this.seek(2); // "KA" + const version = this.uint16(); + const pairs: Record<string, any> = {}; + + if (version === 0x0001) { + const count = this.uint32(); + + for (let index = 0; index < count; index++) { + const name = this.kaValue(undefined, stringTable) as string; + const value = this.kaValue(undefined, stringTable); + + pairs[name] = value; + } + } else if (version === 0x0002) { + const count = this.uint32(); + const strings: string[] = []; + const stringTable: Record<number, string> = {}; + + for (let index = 0; index < count; index++) { + const length = this.uint16(); + const string = this.ascii(length); + + strings.push(string); + } + + for (let index = 0; index < count; index++) { + const id = this.uint32(); + stringTable[id] = strings[index]; + } + + const nodeCount = this.uint32(); + + for (let index = 0; index < nodeCount; index++) { + const keyId = this.uint32(); + const value = this.kaValue(undefined, stringTable); + const key = stringTable[keyId]; + + pairs[key] = value; + } + } else if (version === 0x0102) { + if (!stringTable) throw new Error('No string table provided'); + + const count = this.uint32(); + + for (let index = 0; index < count; index++) { + const keyIndex = this.uint32(); + const key = stringTable[keyIndex]; + const valueType = this.uint8(); + + if (valueType === KAType.STRING) { + const valueIndex = this.uint32(); + const value = stringTable[valueIndex]; + pairs[key] = value; + } else { + const value = this.kaValue(valueType, stringTable); + pairs[key] = value; + } + } + } else if (version === 0xff02) { + // same as 0x0102 but empty and with no count + return {}; + } else throw new RangeError(`Unhandled KA version: ${version}`); + + return pairs; + } +} diff --git a/src/core/streams/windows.ts b/src/core/streams/windows.ts new file mode 100644 index 000000000..7d0cb056d --- /dev/null +++ b/src/core/streams/windows.ts @@ -0,0 +1,7 @@ +import { ReadStream } from './buffer'; + +export class WindowsReadStream extends ReadStream { + dword() { + return this.uint32(); + } +} diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index de02a84cf..93e93ea4c 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -9,11 +9,11 @@ import { } from 'discord.js'; import { commands } from '..'; import discord from '../../../../discord.json' assert { type: 'json' }; -import { UserError } from '../../../core/blitzkrieg/userError'; import buttonLink from '../../../core/discord/buttonLink'; import embedWarning from '../../../core/discord/embedWarning'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import { psa } from '../../../core/discord/psa'; +import { UserError } from '../../../hooks/userError'; export default async function handleChatInputCommand( interaction: ChatInputCommandInteraction<CacheType>, diff --git a/src/hooks/useAppliedEquipments.ts b/src/hooks/useAppliedEquipments.ts new file mode 100644 index 000000000..1c2663951 --- /dev/null +++ b/src/hooks/useAppliedEquipments.ts @@ -0,0 +1,12 @@ +import { EquipmentPreset } from '../core/blitzkrieg/equipmentDefinitions'; +import { EquipmentMatrix } from '../stores/duel'; + +export function useAppliedEquipments( + matrix: EquipmentMatrix, + preset: EquipmentPreset, +) { + const flatMatrix = matrix.flat(); + return preset + .flat() + .map((options, index) => options[flatMatrix[index] == 1 ? 1 : 0]); +} diff --git a/src/hooks/useArmor.ts b/src/hooks/useArmor.ts new file mode 100644 index 000000000..420e6089e --- /dev/null +++ b/src/hooks/useArmor.ts @@ -0,0 +1,11 @@ +import { ObjectMap, useLoader } from '@react-three/fiber'; +import { GLTF, GLTFLoader } from 'three-stdlib'; +import { asset } from '../core/blitzkrieg/asset'; + +const cache: Record<number, GLTF & ObjectMap> = {}; + +export function useArmor(id: number) { + const gltf = useLoader(GLTFLoader, asset(`3d/tanks/armor/${id}.glb`)); + if (!cache[id]) cache[id] = gltf; + return cache[id]; +} diff --git a/src/hooks/useAwait.ts b/src/hooks/useAwait.ts new file mode 100644 index 000000000..f0d9b2145 --- /dev/null +++ b/src/hooks/useAwait.ts @@ -0,0 +1,8 @@ +import usePromise from 'react-promise-suspense'; + +/** + * A more stable alternative to `use` using `usePromise`. + */ +export function useAwait<Type>(promise: Promise<Type>) { + return usePromise(async () => await promise, []); +} diff --git a/src/hooks/useConsumable.ts b/src/hooks/useConsumable.ts new file mode 100644 index 000000000..758cb60ec --- /dev/null +++ b/src/hooks/useConsumable.ts @@ -0,0 +1,6 @@ +import { useDuel } from '../stores/duel'; + +export function useConsumable(id: number) { + const consumables = useDuel((state) => state.protagonist!.consumables); + return consumables.includes(id); +} diff --git a/src/hooks/useDelta.ts b/src/hooks/useDelta.ts new file mode 100644 index 000000000..5dc606824 --- /dev/null +++ b/src/hooks/useDelta.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react'; + +export function useDelta(value: number) { + const lastValue = useRef(value); + + useEffect(() => { + lastValue.current = value; + }); + + return value - lastValue.current; +} diff --git a/src/hooks/useEquipment.ts b/src/hooks/useEquipment.ts new file mode 100644 index 000000000..6c568c18b --- /dev/null +++ b/src/hooks/useEquipment.ts @@ -0,0 +1,29 @@ +import { use, useMemo } from 'react'; +import { equipmentDefinitions } from '../core/blitzkrieg/equipmentDefinitions'; +import { useDuel } from '../stores/duel'; + +export function useEquipment(id: number, antagonist = false) { + const awaitedEquipmentDefinitions = use(equipmentDefinitions); + const member = useDuel( + (state) => state[antagonist ? 'antagonist' : 'protagonist']!, + ); + const equipmentRows = + awaitedEquipmentDefinitions.presets[member.tank.equipment]; + const equipmentMatrix = useDuel( + (state) => state[antagonist ? 'antagonist' : 'protagonist']!.equipment, + ); + const value = useMemo( + () => + equipmentRows.some((equipmentRow, rowIndex) => { + return equipmentRow.some((equipment, columnIndex) => { + const choice = equipmentMatrix[rowIndex][columnIndex]; + if (choice === 0) return false; + const equipped = equipment[choice === -1 ? 0 : 1]; + return equipped === id; + }); + }), + [equipmentMatrix, member.tank], + ); + + return value; +} diff --git a/src/hooks/useEquipmentPreset.ts b/src/hooks/useEquipmentPreset.ts new file mode 100644 index 000000000..73ef7d6c2 --- /dev/null +++ b/src/hooks/useEquipmentPreset.ts @@ -0,0 +1,8 @@ +import { equipmentDefinitions } from '../core/blitzkrieg/equipmentDefinitions'; +import { useAwait } from './useAwait'; + +export function useEquipmentPreset(preset: string) { + const awaitedEquipmentDefinitions = useAwait(equipmentDefinitions); + console.log(awaitedEquipmentDefinitions); + return awaitedEquipmentDefinitions.presets[preset]; +} diff --git a/src/hooks/useModel.ts b/src/hooks/useModel.ts new file mode 100644 index 000000000..df2433686 --- /dev/null +++ b/src/hooks/useModel.ts @@ -0,0 +1,28 @@ +import { ObjectMap, useLoader } from '@react-three/fiber'; +import { Mesh, MeshStandardMaterial } from 'three'; +import { GLTF, GLTFLoader } from 'three-stdlib'; +import { asset } from '../core/blitzkrieg/asset'; + +const cache: Record< + number, + { + gltf: GLTF & ObjectMap; + hasPbr: boolean; + } +> = {}; + +export function useModel(id: number) { + const gltf = useLoader(GLTFLoader, asset(`3d/tanks/models/${id}.glb`)); + if (!cache[id]) { + cache[id] = { + gltf, + hasPbr: Object.values(gltf.nodes).some( + (node) => + node instanceof Mesh && + node.material instanceof MeshStandardMaterial && + node.material.roughnessMap !== null, + ), + }; + } + return cache[id]; +} diff --git a/src/hooks/useModelDefinitions.ts b/src/hooks/useModelDefinitions.ts new file mode 100644 index 000000000..8f6b8530f --- /dev/null +++ b/src/hooks/useModelDefinitions.ts @@ -0,0 +1,6 @@ +import { modelDefinitions } from '../core/blitzkrieg/modelDefinitions'; +import { useAwait } from './useAwait'; + +export function useModelDefinitions() { + return useAwait(modelDefinitions); +} diff --git a/src/hooks/useProvision.ts b/src/hooks/useProvision.ts new file mode 100644 index 000000000..217af4cf4 --- /dev/null +++ b/src/hooks/useProvision.ts @@ -0,0 +1,6 @@ +import { useDuel } from '../stores/duel'; + +export function useProvision(id: number) { + const provisions = useDuel((state) => state.protagonist!.provisions); + return provisions.includes(id); +} diff --git a/src/hooks/useProvisions.ts b/src/hooks/useProvisions.ts new file mode 100644 index 000000000..3ebcb4c97 --- /dev/null +++ b/src/hooks/useProvisions.ts @@ -0,0 +1,9 @@ +import { use } from 'react'; +import { provisionDefinitions } from '../core/blitzkrieg/provisionDefinitions'; +import { useDuel } from '../stores/duel'; + +export function useProvisions() { + const provisions = useDuel((state) => state.protagonist!.provisions); + const awaitedProvisionDefinitions = use(provisionDefinitions); + return provisions.map((id) => awaitedProvisionDefinitions[id]); +} diff --git a/src/hooks/useWideFormat.ts b/src/hooks/useWideFormat.ts new file mode 100644 index 000000000..022d8561d --- /dev/null +++ b/src/hooks/useWideFormat.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react'; + +function hasWideFormat() { + return typeof window !== 'undefined' && window.innerWidth > 880; +} + +export function useWideFormat() { + const [wideFormat, setWideFormat] = useState(hasWideFormat()); + + useEffect(() => { + function handleResize() { + setWideFormat(hasWideFormat()); + } + + window.addEventListener('resize', handleResize); + }, []); + + return wideFormat; +} diff --git a/src/core/blitzkrieg/userError.ts b/src/hooks/userError.ts similarity index 100% rename from src/core/blitzkrieg/userError.ts rename to src/hooks/userError.ts diff --git a/src/icons/AngledPenetration.tsx b/src/icons/AngledPenetration.tsx new file mode 100644 index 000000000..58df8fee6 --- /dev/null +++ b/src/icons/AngledPenetration.tsx @@ -0,0 +1,25 @@ +import { ComponentProps } from 'react'; + +export function AngledPenetration(props: ComponentProps<'svg'>) { + return ( + <svg + width="64" + height="64" + viewBox="0 0 64 64" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path + d="M56 33C56.5523 33 57 32.5523 57 32C57 31.4477 56.5523 31 56 31V33ZM8 32L18 37.7735V26.2265L8 32ZM56 31H17V33H56V31Z" + fill="currentColor" + /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M28 8H12L22 28H38L28 8ZM42 36H26L36 56H52L42 36Z" + fill="currentColor" + /> + </svg> + ); +} diff --git a/src/components/BlitzkriegWormWide.tsx b/src/icons/BlitzkriegWormWide.tsx similarity index 97% rename from src/components/BlitzkriegWormWide.tsx rename to src/icons/BlitzkriegWormWide.tsx index c5bd33b5d..8c2e46784 100644 --- a/src/components/BlitzkriegWormWide.tsx +++ b/src/icons/BlitzkriegWormWide.tsx @@ -1,4 +1,6 @@ -export function BlitzkriegWormWide() { +import { ComponentProps } from 'react'; + +export function BlitzkriegWormWide(props: ComponentProps<'svg'>) { return ( <svg width="74" @@ -6,6 +8,7 @@ export function BlitzkriegWormWide() { viewBox="0 0 74 17" fill="none" xmlns="http://www.w3.org/2000/svg" + {...props} > <path d="M33.945 12.9858V0H36.875V7.50095L39.4608 4.63967H42.7879L36.875 10.7408V12.9858H33.945ZM39.7873 12.9858L37.0515 9.04164L38.8695 7.04314L43.2292 12.9858H39.7873Z" diff --git a/src/icons/Block.tsx b/src/icons/Block.tsx new file mode 100644 index 000000000..a9494489d --- /dev/null +++ b/src/icons/Block.tsx @@ -0,0 +1,25 @@ +import { ComponentProps } from 'react'; + +export function Block(props: ComponentProps<'svg'>) { + return ( + <svg + width="64" + height="64" + viewBox="0 0 64 64" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path d="M40 8H24V26H40V8Z" fill="currentColor" /> + <path d="M40 38H24V56H40V38Z" fill="currentColor" /> + <path + d="M40 26H24C21 26 21.0302 30 21 32C20.9698 34 21 38 24 38H40C37 38 36.9698 34 37 32C37.0301 30 37 26 40 26Z" + fill="currentColor" + /> + <path + d="M56 33C56.5523 33 57 32.5523 57 32C57 31.4477 56.5523 31 56 31L56 33ZM37 32L47 37.7735L47 26.2265L37 32ZM56 31L46 31L46 33L56 33L56 31Z" + fill="currentColor" + /> + </svg> + ); +} diff --git a/src/icons/NominalPenetration.tsx b/src/icons/NominalPenetration.tsx new file mode 100644 index 000000000..7b6ed0758 --- /dev/null +++ b/src/icons/NominalPenetration.tsx @@ -0,0 +1,21 @@ +import { ComponentProps } from 'react'; + +export function NominalPenetration(props: ComponentProps<'svg'>) { + return ( + <svg + width="64" + height="64" + viewBox="0 0 64 64" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path d="M40 8H24V28H40V8Z" fill="currentColor" /> + <path d="M40 36H24V56H40V36Z" fill="currentColor" /> + <path + d="M56 33C56.5523 33 57 32.5523 57 32C57 31.4477 56.5523 31 56 31V33ZM8 32L18 37.7735V26.2265L8 32ZM56 31H17V33H56V31Z" + fill="currentColor" + /> + </svg> + ); +} diff --git a/src/icons/Ricochet.tsx b/src/icons/Ricochet.tsx new file mode 100644 index 000000000..f79672ec6 --- /dev/null +++ b/src/icons/Ricochet.tsx @@ -0,0 +1,20 @@ +import { ComponentProps } from 'react'; + +export function Ricochet(props: ComponentProps<'svg'>) { + return ( + <svg + width="64" + height="64" + viewBox="0 0 64 64" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path d="M12 8H28L52 56H36L12 8Z" fill="currentColor" /> + <path + d="M55.4 44.8C55.8418 45.1314 56.4686 45.0418 56.8 44.6C57.1314 44.1582 57.0418 43.5314 56.6 43.2L55.4 44.8ZM40 8L34.2265 18H45.7735L40 8ZM40 32H39V32.5L39.4 32.8L40 32ZM56.6 43.2L40.6 31.2L39.4 32.8L55.4 44.8L56.6 43.2ZM41 32V17H39V32H41Z" + fill="currentColor" + /> + </svg> + ); +} diff --git a/src/stitches.config.ts b/src/stitches.config.ts index 307f317ab..413f706be 100644 --- a/src/stitches.config.ts +++ b/src/stitches.config.ts @@ -1,7 +1,9 @@ import { amberDark, + amberDarkA, blueDark, orangeDark, + purpleDark, slate, slateDark, slateDarkA, @@ -19,7 +21,9 @@ export const theme = { ...createColors(slate, 'light'), ...createColors(amberDark, 'amber'), + ...createColors(amberDarkA, 'amberAlpha'), ...createColors(orangeDark, 'orange'), + ...createColors(purpleDark, 'purple'), ...createColors(blueDark, 'blue'), ...createColors(tealDark, 'teal'), }, diff --git a/src/stores/app.ts b/src/stores/app.ts new file mode 100644 index 000000000..1c1366937 --- /dev/null +++ b/src/stores/app.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface App { + devBuildAgreementTime: number; +} + +export const useApp = create<App>()( + persist( + (set) => ({ + devBuildAgreementTime: 0, + }), + { name: 'app' }, + ), +); diff --git a/src/stores/duel.ts b/src/stores/duel.ts new file mode 100644 index 000000000..e629f2921 --- /dev/null +++ b/src/stores/duel.ts @@ -0,0 +1,62 @@ +import { produce } from 'immer'; +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; +import { + EngineDefinition, + GunDefinition, + ShellDefinition, + TankDefinition, + TrackDefinition, + TurretDefinition, +} from '../core/blitzkrieg/tankDefinitions'; + +type EquipmentMatrixItem = -1 | 0 | 1; +type EquipmentMatrixRow = [ + EquipmentMatrixItem, + EquipmentMatrixItem, + EquipmentMatrixItem, +]; +export type EquipmentMatrix = [ + EquipmentMatrixRow, + EquipmentMatrixRow, + EquipmentMatrixRow, +]; + +export interface DuelMember { + tank: TankDefinition; + engine: EngineDefinition; + turret: TurretDefinition; + gun: GunDefinition; + shell: ShellDefinition; + track: TrackDefinition; + equipment: EquipmentMatrix; + pitch: number; + yaw: number; + consumables: number[]; + crewMastery: number; + provisions: number[]; + camouflage: boolean; + cooldownBooster: number; +} + +export interface Duel { + assigned: boolean; + protagonist?: DuelMember; + antagonist?: DuelMember; +} + +export const useDuel = create<Duel>()( + subscribeWithSelector<Duel>(() => ({ + assigned: false, + })), +); + +export function mutateDuel(recipe: (draft: Duel) => void) { + useDuel.setState(produce(recipe)); +} + +export const genericDefaultEquipmentMatrix: EquipmentMatrix = [ + [-1, 1, -1], + [0, 0, 0], + [0, 0, 0], +]; diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 447edf4bd..f2f8b5d26 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -1,13 +1,28 @@ import { produce } from 'immer'; +import { merge } from 'lodash'; +import { Vector3Tuple } from 'three'; import { create } from 'zustand'; -import { TankType, TreeTypeString } from '../components/Tanks'; -import { Tier } from '../core/blitzkrieg/definitions/tanks'; +import { persist, subscribeWithSelector } from 'zustand/middleware'; +import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting'; +import { TankType, TreeType } from '../components/Tanks'; +import { Tier } from '../core/blitzkrieg/tankDefinitions'; -export type TankopediaSortBy = 'tier' | 'name'; +export type TankopediaSortBy = keyof typeof SORT_NAMES; export type TankopediaSortDirection = 'ascending' | 'descending'; export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only'; +export type TankopediaMode = 'model' | 'armor'; -type Tankopedia = { +interface TankopediaPersistent { + model: { + visual: { + wireframe: boolean; + environment: (typeof ENVIRONMENTS)[number]; + controlsEnabled: boolean; + showGrid: boolean; + greenPenetration: boolean; + showEnvironment: boolean; + }; + }; sort: { by: TankopediaSortBy; direction: TankopediaSortDirection; @@ -15,26 +30,117 @@ type Tankopedia = { filters: { tiers: Tier[]; types: TankType[]; - treeTypes: TreeTypeString[]; + treeTypes: TreeType[]; nations: string[]; test: TankopediaTestTankDisplay; + page: number; }; -}; - -export const useTankopedia = create<Tankopedia>()(() => ({ - sort: { - by: 'tier', - direction: 'descending', - }, - filters: { - tiers: [], - types: [], - treeTypes: [], - nations: [], - test: 'include', - }, -})); - -export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) { - useTankopedia.setState(produce(recipe)); + mode: TankopediaMode; +} + +interface TankopediaTemporary { + shot?: Shot; +} + +export interface Shot { + point: Vector3Tuple; + surfaceNormal: Vector3Tuple; + shellNormal: Vector3Tuple; + thicknesses: ArmorPiercingLayer[]; + type: 'ricochet' | 'penetration' | 'block'; + angle: number; + ricochet?: { + distance: number; + point: Vector3Tuple; + penetration: boolean; + }; +} + +export type ArmorPiercingLayer = + | { + distance: number; + nominal: number; + angled: number; + ricochet: boolean; + type: 'core' | 'spaced' | 'external'; + } + | { + type: 'gap'; + gap: number; + }; + +export const SORT_NAMES = { + 'meta.tier': 'tier', + 'meta.name': 'name', + 'survivability.health': 'health', + 'survivability.viewRange': 'view range', + 'survivability.camouflageStill': 'camouflage still', + 'survivability.camouflageMoving': 'camouflage moving', + 'survivability.camouflageShooting': 'camouflage shooting', + 'survivability.size': 'size', + 'fire.dpm': 'DPM', + 'fire.reload': 'reload', + 'fire.caliber': 'caliber', + 'fire.standardPenetration': 'standard penetration', + 'fire.premiumPenetration': 'premium penetration', + 'fire.damage': 'damage', + 'fire.shellVelocity': 'shell velocity', + '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', +} as const; + +export const useTankopediaPersistent = create<TankopediaPersistent>()( + persist( + subscribeWithSelector<TankopediaPersistent>(() => ({ + model: { + visual: { + wireframe: false, + environment: ENVIRONMENTS[0], + controlsEnabled: true, + showGrid: true, + greenPenetration: false, + showEnvironment: false, + }, + }, + sort: { + by: 'meta.tier', + direction: 'descending', + }, + filters: { + tiers: [], + types: [], + treeTypes: [], + nations: [], + test: 'include', + page: 0, + }, + mode: 'model', + })), + { name: 'tankopedia', merge }, + ), +); + +export const useTankopediaTemporary = create<TankopediaTemporary>()( + subscribeWithSelector<TankopediaTemporary>(() => ({})), +); + +export default function mutateTankopediaPersistent( + recipe: (draft: TankopediaPersistent) => void, +) { + useTankopediaPersistent.setState(produce(recipe)); +} + +export function mutateTankopediaTemporary( + recipe: (draft: TankopediaTemporary) => void, +) { + useTankopediaTemporary.setState(produce(recipe)); } diff --git a/src/themes/mint.ts b/src/themes/mint.ts new file mode 100644 index 000000000..9c63ae9be --- /dev/null +++ b/src/themes/mint.ts @@ -0,0 +1,6 @@ +import { mintDark } from '@radix-ui/colors'; +import { createColors } from 'bepaint'; + +export const mintTheme = { + colors: createColors(mintDark), +}; diff --git a/src/types/glsl.d.ts b/src/types/glsl.d.ts new file mode 100644 index 000000000..77ffce2fc --- /dev/null +++ b/src/types/glsl.d.ts @@ -0,0 +1,4 @@ +declare module '*.glsl' { + const value: string; + export default value; +} diff --git a/src/types/lz4js.d.ts b/src/types/lz4js.d.ts new file mode 100644 index 000000000..a290cf0e6 --- /dev/null +++ b/src/types/lz4js.d.ts @@ -0,0 +1,4 @@ +declare module 'lz4js' { + export function compress(buffer: Uint8Array, maxSize?: number): Uint8Array; + export function decompress(buffer: Uint8Array, maxSize?: number): Uint8Array; +} diff --git a/yarn.lock b/yarn.lock index 15dffb0cd..7029eaa43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,109 +29,109 @@ resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== -"@algolia/cache-browser-local-storage@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.21.1.tgz#ecd8e6fe9a6ecbb63c9c29a9574e47315846347e" - integrity sha512-vUkac/vgj8inyGR/IgunRjTOQ6IlBwl7afFkIfUZRqbqKKXBs+A/g5wgH+UnAlCSW8wjFRAIfCzuvSRb1/qjsQ== +"@algolia/cache-browser-local-storage@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz#14b6dc9abc9e3a304a5fffb063d15f30af1032d1" + integrity sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g== dependencies: - "@algolia/cache-common" "4.21.1" + "@algolia/cache-common" "4.22.1" -"@algolia/cache-common@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.21.1.tgz#86b9f5c8b5c21b7a6479d388e04678408a449e65" - integrity sha512-HUo4fRk8KXFMyCASW0k+Kl8iXBoRPdqAjV9OVaFibTNg1dbwnpe6eIxbSTM6AJ2X82ic/8x3GuAO8zF/E515PA== +"@algolia/cache-common@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.22.1.tgz#c625dff4bc2a74e79f9aed67b4e053b0ef1b3ec1" + integrity sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA== -"@algolia/cache-in-memory@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.21.1.tgz#dfd3249c4887250fdceb76191b05ba95b94821b3" - integrity sha512-+l2pLg6yIwRaGNtv41pGF/f/e9Qk80FeYE41f4OXS9lb5vpyrxzqM5nUaffWk/ZSFrPDuw5J2E226c//tIIffA== +"@algolia/cache-in-memory@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz#858a3d887f521362e87d04f3943e2810226a0d71" + integrity sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw== dependencies: - "@algolia/cache-common" "4.21.1" + "@algolia/cache-common" "4.22.1" -"@algolia/client-account@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.21.1.tgz#60e5225ea4b4440219030775dcb1b9bd3ad92e54" - integrity sha512-AC6SjA9n38th73gAUqcjsuxNUChpwaflaAhPL0qO9cUICN67njpQrnYaoSVZ/yx0opG5zQFRKbpEcuPGj0XjhQ== +"@algolia/client-account@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.22.1.tgz#a7fb8b66b9a4f0a428e1426b2561144267d76d43" + integrity sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw== dependencies: - "@algolia/client-common" "4.21.1" - "@algolia/client-search" "4.21.1" - "@algolia/transporter" "4.21.1" + "@algolia/client-common" "4.22.1" + "@algolia/client-search" "4.22.1" + "@algolia/transporter" "4.22.1" -"@algolia/client-analytics@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.21.1.tgz#400d7defd32e8312ccdf8cd41533055f5ab4f52a" - integrity sha512-q6AxvAcBl4fNZXZsMwRRQXcsxUv0PK5eUAz/lHDvgkMWAg6cP7Fl+WIq0fHcG7cJA4EHf2sT5fV6Z+yUlf7NfA== +"@algolia/client-analytics@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.22.1.tgz#506558740b4d49b1b1e3393861f729a8ce921851" + integrity sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg== dependencies: - "@algolia/client-common" "4.21.1" - "@algolia/client-search" "4.21.1" - "@algolia/requester-common" "4.21.1" - "@algolia/transporter" "4.21.1" + "@algolia/client-common" "4.22.1" + "@algolia/client-search" "4.22.1" + "@algolia/requester-common" "4.22.1" + "@algolia/transporter" "4.22.1" -"@algolia/client-common@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.21.1.tgz#20798c96c1d45078648bf28dfb84e50cd13a5d94" - integrity sha512-LOH7ncYwY/x7epOgxc/MIuV7m3qzl00wIjDG5/9rgImFpkV0X+D/ndJI9DmPsIx7yaTLd5xv/XYuKLcvrUR0eQ== +"@algolia/client-common@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.22.1.tgz#042b19c1b6157c485fa1b551349ab313944d2b05" + integrity sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ== dependencies: - "@algolia/requester-common" "4.21.1" - "@algolia/transporter" "4.21.1" + "@algolia/requester-common" "4.22.1" + "@algolia/transporter" "4.22.1" -"@algolia/client-personalization@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.21.1.tgz#55ed8edb8258b2f4b05bfc37d454dca9209bb106" - integrity sha512-u2CyQjHbyVwPqM5eSXd/o+rh1Pk949P/MO6s+OxyEGg6/R2YpYvmsafVZl9Q+xqT8pFaf5QygfcqlSdMUDHV5Q== +"@algolia/client-personalization@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.22.1.tgz#ff088d797648224fb582e9fe5828f8087835fa3d" + integrity sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ== dependencies: - "@algolia/client-common" "4.21.1" - "@algolia/requester-common" "4.21.1" - "@algolia/transporter" "4.21.1" + "@algolia/client-common" "4.22.1" + "@algolia/requester-common" "4.22.1" + "@algolia/transporter" "4.22.1" -"@algolia/client-search@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.21.1.tgz#b08f6ccfaf404530e3e5a38e8492a635ff15153f" - integrity sha512-3KqSmMkQmF+ACY/Ms5TdcvrcK8iqgQP/N0EPnNUUP4LMUzAACpLLTdzA+AtCuc6oaz5ITtGJBVdPUljj5Jf/Lg== +"@algolia/client-search@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.22.1.tgz#508cc6ab3d1f4e9c02735a630d4dff6fbb8514a2" + integrity sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA== dependencies: - "@algolia/client-common" "4.21.1" - "@algolia/requester-common" "4.21.1" - "@algolia/transporter" "4.21.1" + "@algolia/client-common" "4.22.1" + "@algolia/requester-common" "4.22.1" + "@algolia/transporter" "4.22.1" -"@algolia/logger-common@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.21.1.tgz#b0979321592af12b986aea2b7ac4fc368920860f" - integrity sha512-9AyYpR2OO9vPkkDlpTtW2/6nX+RmMd7LUwzJiAF3uN+BYUiQqgXEp+oGaH8UC0dgetmK7wJO6hw4b39cnTdEpw== +"@algolia/logger-common@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.22.1.tgz#79cf4cd295de0377a94582c6aaac59b1ded731d9" + integrity sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg== -"@algolia/logger-console@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.21.1.tgz#59bdceab3d93ed478e4cb61cfe8f951cb9ef1487" - integrity sha512-9wizQiQ8kL4DiBmT82i403UwacNuv+0hpfsfaWYZQrGjpzG+yvXETWM4AgwFZLj007esuKQiGfOPUoYFZNkGGA== +"@algolia/logger-console@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.22.1.tgz#0355345f6940f67aaa78ae9b81c06e44e49f2336" + integrity sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA== dependencies: - "@algolia/logger-common" "4.21.1" + "@algolia/logger-common" "4.22.1" -"@algolia/requester-browser-xhr@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.21.1.tgz#c841a76f64171d3b892aea16e23d819b7f6a8e0a" - integrity sha512-9NudesJLuXtRHV+JD8fTkrsdVj/oAPQbtLnxBbSQeMduzV6+a7W+G9VuWo5fwFymCdXR8/Hb6jy8D1owQIq5Gw== +"@algolia/requester-browser-xhr@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz#f04df6fe9690a071b267c77d26b83a3be9280361" + integrity sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw== dependencies: - "@algolia/requester-common" "4.21.1" + "@algolia/requester-common" "4.22.1" -"@algolia/requester-common@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.21.1.tgz#5fd9acce9faa8b931f91b0e86e384956874c3c43" - integrity sha512-KtX2Ep3C43XxoN3xKw755cdf9enE6gPgzh6ufZQRJBl4rYCOoXbiREU6noDYX/Nq+Q+sl03V37WAp0YgtIlh9g== +"@algolia/requester-common@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.22.1.tgz#27be35f3718aafcb6b388ff9c3aa2defabd559ff" + integrity sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg== -"@algolia/requester-node-http@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.21.1.tgz#a39a0003e7697009da032238d2b3134a65ec9fae" - integrity sha512-EcD8cY6Bh2iMySpqXglTKU9+pt+km1ws3xF0V7CGMIUzW1HmN/ZVhi4apCBY4tEMytbyARv0XRTPsolSC4gSSw== +"@algolia/requester-node-http@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz#589a6fa828ad0f325e727a6fcaf4e1a2343cc62b" + integrity sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA== dependencies: - "@algolia/requester-common" "4.21.1" + "@algolia/requester-common" "4.22.1" -"@algolia/transporter@4.21.1": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.21.1.tgz#ffe43fb9d03c042aed89cec793687a41278fd35e" - integrity sha512-KGLFKz8krzOWRwcbR4FT49Grh1dES/mG8dHABEojbvrfUb6kUFxkAee/aezp2GIxuNx+gpQjRn1IzOsqbUZL0A== +"@algolia/transporter@4.22.1": + version "4.22.1" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.22.1.tgz#8843841b857dc021668f31647aa557ff19cd9cb1" + integrity sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ== dependencies: - "@algolia/cache-common" "4.21.1" - "@algolia/logger-common" "4.21.1" - "@algolia/requester-common" "4.21.1" + "@algolia/cache-common" "4.22.1" + "@algolia/logger-common" "4.22.1" + "@algolia/requester-common" "4.22.1" "@ampproject/remapping@^2.2.0": version "2.2.1" @@ -141,7 +141,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== @@ -154,21 +154,21 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== -"@babel/core@^7.20.7": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.6.tgz#8be77cd77c55baadcc1eae1c33df90ab6d2151d4" - integrity sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw== +"@babel/core@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" + integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-module-transforms" "^7.23.3" - "@babel/helpers" "^7.23.6" - "@babel/parser" "^7.23.6" - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.6" - "@babel/types" "^7.23.6" + "@babel/helpers" "^7.23.9" + "@babel/parser" "^7.23.9" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -268,14 +268,14 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== -"@babel/helpers@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.6.tgz#d03af2ee5fb34691eec0cda90f5ecbb4d4da145a" - integrity sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA== +"@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== dependencies: - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.6" - "@babel/types" "^7.23.6" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" "@babel/highlight@^7.23.4": version "7.23.4" @@ -286,12 +286,12 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.22.15", "@babel/parser@^7.23.5", "@babel/parser@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" - integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== -"@babel/plugin-syntax-typescript@^7.20.0": +"@babel/plugin-syntax-typescript@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f" integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== @@ -299,25 +299,25 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" - integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" + integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== +"@babel/template@^7.22.15", "@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" -"@babel/traverse@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.6.tgz#b53526a2367a0dd6edc423637f3d2d0f2521abc5" - integrity sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ== +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -325,15 +325,15 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.6" - "@babel/types" "^7.23.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" - integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== dependencies: "@babel/helper-string-parser" "^7.23.4" "@babel/helper-validator-identifier" "^7.22.20" @@ -464,379 +464,171 @@ to-pascal-case "^1.0.0" unescape-js "^1.0.5" -"@esbuild/android-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz#b11bd4e4d031bb320c93c83c137797b2be5b403b" - integrity sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg== - -"@esbuild/android-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" - integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== - -"@esbuild/android-arm64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.9.tgz#683794bdc3d27222d3eced7b74cad15979548031" - integrity sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ== - -"@esbuild/android-arm@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.6.tgz#ac6b5674da2149997f6306b3314dae59bbe0ac26" - integrity sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g== - -"@esbuild/android-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" - integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== - -"@esbuild/android-arm@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.9.tgz#21a4de41f07b2af47401c601d64dfdefd056c595" - integrity sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA== - -"@esbuild/android-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.6.tgz#18c48bf949046638fc209409ff684c6bb35a5462" - integrity sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ== - -"@esbuild/android-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" - integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== - -"@esbuild/android-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.9.tgz#e2d7674bc025ddc8699f0cc76cb97823bb63c252" - integrity sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA== - -"@esbuild/darwin-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz#b3fe19af1e4afc849a07c06318124e9c041e0646" - integrity sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA== - -"@esbuild/darwin-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" - integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== - -"@esbuild/darwin-arm64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.9.tgz#ae7a582289cc5c0bac15d4b9020a90cb7288f1e9" - integrity sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw== - -"@esbuild/darwin-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz#f4dacd1ab21e17b355635c2bba6a31eba26ba569" - integrity sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg== - -"@esbuild/darwin-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" - integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== - -"@esbuild/darwin-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.9.tgz#8a216c66dcf51addeeb843d8cfaeff712821d12b" - integrity sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ== - -"@esbuild/freebsd-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz#ea4531aeda70b17cbe0e77b0c5c36298053855b4" - integrity sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg== - -"@esbuild/freebsd-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" - integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== - -"@esbuild/freebsd-arm64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.9.tgz#63d4f603e421252c3cd836b18d01545be7c6c440" - integrity sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g== - -"@esbuild/freebsd-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz#1896170b3c9f63c5e08efdc1f8abc8b1ed7af29f" - integrity sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q== - -"@esbuild/freebsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" - integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== - -"@esbuild/freebsd-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.9.tgz#a3db52595be65360eae4de1d1fa3c1afd942e1e4" - integrity sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA== - -"@esbuild/linux-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz#967dfb951c6b2de6f2af82e96e25d63747f75079" - integrity sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w== - -"@esbuild/linux-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" - integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== - -"@esbuild/linux-arm64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.9.tgz#4ae5811ce9f8d7df5eb9edd9765ea9401a534f13" - integrity sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ== - -"@esbuild/linux-arm@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz#097a0ee2be39fed3f37ea0e587052961e3bcc110" - integrity sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw== - -"@esbuild/linux-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" - integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== - -"@esbuild/linux-arm@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.9.tgz#9807e92cfd335f46326394805ad488e646e506f2" - integrity sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw== - -"@esbuild/linux-ia32@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz#a38a789d0ed157495a6b5b4469ec7868b59e5278" - integrity sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ== - -"@esbuild/linux-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" - integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== - -"@esbuild/linux-ia32@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.9.tgz#18892c10f3106652b16f9da88a0362dc95ed46c7" - integrity sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q== - -"@esbuild/linux-loong64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz#ae3983d0fb4057883c8246f57d2518c2af7cf2ad" - integrity sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ== - -"@esbuild/linux-loong64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" - integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== - -"@esbuild/linux-loong64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.9.tgz#dc2ebf9a125db0a1bba18c2bbfd4fbdcbcaf61c2" - integrity sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA== - -"@esbuild/linux-mips64el@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz#15fbbe04648d944ec660ee5797febdf09a9bd6af" - integrity sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA== - -"@esbuild/linux-mips64el@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" - integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== - -"@esbuild/linux-mips64el@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.9.tgz#4c2f7c5d901015e3faf1563c4a89a50776cb07fd" - integrity sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw== - -"@esbuild/linux-ppc64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz#38210094e8e1a971f2d1fd8e48462cc65f15ef19" - integrity sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg== - -"@esbuild/linux-ppc64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" - integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== - -"@esbuild/linux-ppc64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.9.tgz#8385332713b4e7812869622163784a5633f76fc4" - integrity sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ== - -"@esbuild/linux-riscv64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz#bc3c66d5578c3b9951a6ed68763f2a6856827e4a" - integrity sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ== - -"@esbuild/linux-riscv64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" - integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== - -"@esbuild/linux-riscv64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.9.tgz#23f1db24fa761be311874f32036c06249aa20cba" - integrity sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg== - -"@esbuild/linux-s390x@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz#d7ba7af59285f63cfce6e5b7f82a946f3e6d67fc" - integrity sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q== - -"@esbuild/linux-s390x@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" - integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== - -"@esbuild/linux-s390x@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.9.tgz#2dffe497726b897c9f0109e774006e25b33b4fd0" - integrity sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw== - -"@esbuild/linux-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz#ba51f8760a9b9370a2530f98964be5f09d90fed0" - integrity sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw== - -"@esbuild/linux-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" - integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== - -"@esbuild/linux-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.9.tgz#ceb1d62cd830724ff5b218e5d3172a8bad59420e" - integrity sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A== - -"@esbuild/netbsd-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz#e84d6b6fdde0261602c1e56edbb9e2cb07c211b9" - integrity sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A== - -"@esbuild/netbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" - integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== - -"@esbuild/netbsd-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz#0cbca65e9ef4d3fc41502d3e055e6f49479a8f18" - integrity sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug== - -"@esbuild/openbsd-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz#cf4b9fb80ce6d280a673d54a731d9c661f88b083" - integrity sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw== - -"@esbuild/openbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" - integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== - -"@esbuild/openbsd-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz#1f57adfbee09c743292c6758a3642e875bcad1cf" - integrity sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw== - -"@esbuild/sunos-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz#a6838e246079b24d962b9dcb8d208a3785210a73" - integrity sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw== - -"@esbuild/sunos-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" - integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== - -"@esbuild/sunos-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz#116be6adbd2c7479edeeb5f6ea0441002ab4cb9c" - integrity sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw== - -"@esbuild/win32-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz#ace0186e904d109ea4123317a3ba35befe83ac21" - integrity sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg== - -"@esbuild/win32-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" - integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== - -"@esbuild/win32-arm64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.9.tgz#2be22131ab18af4693fd737b161d1ef34de8ca9d" - integrity sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg== - -"@esbuild/win32-ia32@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz#7fb3f6d4143e283a7f7dffc98a6baf31bb365c7e" - integrity sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg== - -"@esbuild/win32-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" - integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== - -"@esbuild/win32-ia32@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.9.tgz#e10ead5a55789b167b4225d2469324538768af7c" - integrity sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg== - -"@esbuild/win32-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz#563ff4277f1230a006472664fa9278a83dd124da" - integrity sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA== - -"@esbuild/win32-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" - integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== - -"@esbuild/win32-x64@0.19.9": - version "0.19.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz#b2da6219b603e3fa371a78f53f5361260d0c5585" - integrity sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ== +"@esbuild/aix-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" + integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== + +"@esbuild/android-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" + integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== + +"@esbuild/android-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" + integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== + +"@esbuild/android-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" + integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== + +"@esbuild/darwin-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" + integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== + +"@esbuild/darwin-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" + integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== + +"@esbuild/freebsd-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" + integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== + +"@esbuild/freebsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" + integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== + +"@esbuild/linux-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" + integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== + +"@esbuild/linux-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" + integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== + +"@esbuild/linux-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" + integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== + +"@esbuild/linux-loong64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" + integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== + +"@esbuild/linux-mips64el@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" + integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== + +"@esbuild/linux-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" + integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== + +"@esbuild/linux-riscv64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" + integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== + +"@esbuild/linux-s390x@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" + integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== + +"@esbuild/linux-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" + integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== + +"@esbuild/netbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" + integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== + +"@esbuild/openbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" + integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== + +"@esbuild/sunos-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" + integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== + +"@esbuild/win32-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" + integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== + +"@esbuild/win32-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" + integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== + +"@esbuild/win32-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" + integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== "@fastify/busboy@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== -"@floating-ui/core@^1.4.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071" - integrity sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A== +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== dependencies: - "@floating-ui/utils" "^0.1.3" + "@floating-ui/utils" "^0.2.1" -"@floating-ui/dom@^1.5.1": - version "1.5.3" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa" - integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA== +"@floating-ui/dom@^1.6.1": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== dependencies: - "@floating-ui/core" "^1.4.2" - "@floating-ui/utils" "^0.1.3" + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" "@floating-ui/react-dom@^2.0.0": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.4.tgz#b076fafbdfeb881e1d86ae748b7ff95150e9f3ec" - integrity sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ== + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== dependencies: - "@floating-ui/dom" "^1.5.1" + "@floating-ui/dom" "^1.6.1" -"@floating-ui/utils@^0.1.3": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" - integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== -"@img/sharp-darwin-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.0.tgz#b3e4ed1887f53faca5e95e3fd9f25181432353ed" - integrity sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug== +"@gltf-transform/core@^3.9.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.0.tgz#854e7345f23971e4e7367a29183a2d1b62d45e46" + integrity sha512-NxVKhSWvH0j1tjZE8Yl461HUMyZLmYmqcbqHw0TOcQd5Q1SV7Y5w6W68XMt9/amRfMAiJLLNREE7kbr+Z0Ydbw== + dependencies: + property-graph "^1.3.1" + +"@img/sharp-darwin-arm64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz#9d3cb0e4899b10b003608a018877b45b6db02861" + integrity sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw== optionalDependencies: "@img/sharp-libvips-darwin-arm64" "1.0.0" -"@img/sharp-darwin-x64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz#277714bff6e6d468162146019933a416467f3082" - integrity sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw== +"@img/sharp-darwin-x64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz#3ad6b275dba0ed9995ce4403fc2c59b0475a0162" + integrity sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw== optionalDependencies: "@img/sharp-libvips-darwin-x64" "1.0.0" @@ -880,64 +672,64 @@ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz#af33a04e75f192c5396c6a41df8b7b7bf15a8006" integrity sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg== -"@img/sharp-linux-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz#d9a361c1d5344754f6038cc9d74e1dd4d217f52f" - integrity sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw== +"@img/sharp-linux-arm64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz#3f419eb6f9ace924c511903a4d9034fb847de06b" + integrity sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw== optionalDependencies: "@img/sharp-libvips-linux-arm64" "1.0.0" -"@img/sharp-linux-arm@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz#e0f804033d99ce2af73c78d47eeb118e28b4af41" - integrity sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg== +"@img/sharp-linux-arm@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz#87e157716c55fa274dd652e71bf9a5b7c260f8ab" + integrity sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA== optionalDependencies: "@img/sharp-libvips-linux-arm" "1.0.0" -"@img/sharp-linux-s390x@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz#4b6d49f87364c60ae015825746f365385f1c3b54" - integrity sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ== +"@img/sharp-linux-s390x@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz#c554567bb211cb1e916562752e70ef65df0cdcb7" + integrity sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A== optionalDependencies: "@img/sharp-libvips-linux-s390x" "1.0.0" -"@img/sharp-linux-x64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz#a4fb262f143f468aec974aad6e53d1859a75ec90" - integrity sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA== +"@img/sharp-linux-x64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz#c8623c743e2a68b460b63bd2e225b1b5a485d144" + integrity sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg== optionalDependencies: "@img/sharp-libvips-linux-x64" "1.0.0" -"@img/sharp-linuxmusl-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz#423a9e5e3ec1e6476dbdfd78dda91c6f062bfd67" - integrity sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg== +"@img/sharp-linuxmusl-arm64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz#1b17f9950246108cb22cf98f1822fbe4a5b0dc9e" + integrity sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA== optionalDependencies: "@img/sharp-libvips-linuxmusl-arm64" "1.0.0" -"@img/sharp-linuxmusl-x64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz#9d0beac9a71cd7d3bc41eef63521e60edc3820f5" - integrity sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw== +"@img/sharp-linuxmusl-x64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz#69e2ab197c6d6e7a09748e0f2d03244d2c2afed7" + integrity sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ== optionalDependencies: "@img/sharp-libvips-linuxmusl-x64" "1.0.0" -"@img/sharp-wasm32@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz#5b085509f04256c43f201552fce5144fe0bd66b9" - integrity sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw== +"@img/sharp-wasm32@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz#aa6f33a8535e6bd4a66c59aeb569499db9d30043" + integrity sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA== dependencies: "@emnapi/runtime" "^0.44.0" -"@img/sharp-win32-ia32@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz#a89bc502a70e5547cbff9451d06f9962425e7b39" - integrity sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg== +"@img/sharp-win32-ia32@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz#8df522d79b3c08f59e65fbe773849daa353c4f12" + integrity sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ== -"@img/sharp-win32-x64@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz#bfae1b82608a0d420e4b0b836ddb7dd58eaffa8e" - integrity sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q== +"@img/sharp-win32-x64@0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz#8b56e97dc9d987b070d7530a357161d0f057c5af" + integrity sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -961,9 +753,9 @@ "@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/set-array@^1.0.1": version "1.1.2" @@ -976,9 +768,9 @@ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.20" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" - integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + version "0.3.22" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -988,55 +780,55 @@ resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz#a78e137018a19933b7a1d0e887d553d4ab833d10" integrity sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q== -"@next/env@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" - integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== +"@next/env@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341" + integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw== -"@next/swc-darwin-arm64@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" - integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== +"@next/swc-darwin-arm64@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39" + integrity sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ== -"@next/swc-darwin-x64@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" - integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== +"@next/swc-darwin-x64@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz#0863a22feae1540e83c249384b539069fef054e9" + integrity sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g== -"@next/swc-linux-arm64-gnu@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" - integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== +"@next/swc-linux-arm64-gnu@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz#893da533d3fce4aec7116fe772d4f9b95232423c" + integrity sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ== -"@next/swc-linux-arm64-musl@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" - integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== +"@next/swc-linux-arm64-musl@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz#d81ddcf95916310b8b0e4ad32b637406564244c0" + integrity sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g== -"@next/swc-linux-x64-gnu@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" - integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== +"@next/swc-linux-x64-gnu@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz#18967f100ec19938354332dcb0268393cbacf581" + integrity sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ== -"@next/swc-linux-x64-musl@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" - integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== +"@next/swc-linux-x64-musl@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz#77077cd4ba8dda8f349dc7ceb6230e68ee3293cf" + integrity sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg== -"@next/swc-win32-arm64-msvc@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" - integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== +"@next/swc-win32-arm64-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz#5f0b8cf955644104621e6d7cc923cad3a4c5365a" + integrity sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ== -"@next/swc-win32-ia32-msvc@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" - integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== +"@next/swc-win32-ia32-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz#21f4de1293ac5e5a168a412b139db5d3420a89d0" + integrity sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw== -"@next/swc-win32-x64-msvc@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" - integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== +"@next/swc-win32-x64-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz#e561fb330466d41807123d932b365cf3d33ceba2" + integrity sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1060,9 +852,9 @@ fastq "^1.6.0" "@npmcli/git@^5.0.0": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-5.0.3.tgz#ad3ede0994bcf716ddb63d361f3ea16cb72d878c" - integrity sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw== + version "5.0.4" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-5.0.4.tgz#d18c50f99649e6e89e8b427318134f582498700c" + integrity sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ== dependencies: "@npmcli/promise-spawn" "^7.0.0" lru-cache "^10.0.1" @@ -1102,9 +894,9 @@ semver "^7.5.3" "@npmcli/promise-spawn@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz#fd1c64ed4ff2341e503e1f390c62640a6540df09" - integrity sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ== + version "7.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz#a836de2f42a2245d629cf6fbb8dd6c74c74c55af" + integrity sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg== dependencies: which "^4.0.0" @@ -1114,9 +906,9 @@ integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== "@octokit/core@^5.0.0": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.0.2.tgz#ae7c5d61fdd98ba348a27c3cc510879a130b1234" - integrity sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg== + version "5.1.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.1.0.tgz#81dacf0197ed7855e6413f128bd6dd9e121e7d2f" + integrity sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g== dependencies: "@octokit/auth-token" "^4.0.0" "@octokit/graphql" "^7.0.0" @@ -1161,11 +953,11 @@ integrity sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA== "@octokit/plugin-rest-endpoint-methods@^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz#eeaa4de97a2ae26404dea30ce3e17b11928e027c" - integrity sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q== + version "10.3.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.3.0.tgz#cba45a365ebc5dbeeca2db95466a514e69c5aa75" + integrity sha512-c/fjpoHispRvBZuRoTVt/uALg7pXa9RQbXWJiDMk6NDkGNomuAZG7YuYYpZoxeoXv+kVRjIDTsO0e1z0pei+PQ== dependencies: - "@octokit/types" "^12.3.0" + "@octokit/types" "^12.4.0" "@octokit/plugin-throttling@^8.1.3": version "8.1.3" @@ -1185,9 +977,9 @@ once "^1.4.0" "@octokit/request@^8.0.1", "@octokit/request@^8.0.2": - version "8.1.6" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.1.6.tgz#a76a859c30421737a3918b40973c2ff369009571" - integrity sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ== + version "8.2.0" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.2.0.tgz#125c547bc3f4c0e2dfa38c6829a1cf00027fbd98" + integrity sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ== dependencies: "@octokit/endpoint" "^9.0.0" "@octokit/request-error" "^5.0.0" @@ -1204,10 +996,10 @@ "@octokit/plugin-request-log" "^4.0.0" "@octokit/plugin-rest-endpoint-methods" "^10.0.0" -"@octokit/types@^12.0.0", "@octokit/types@^12.2.0", "@octokit/types@^12.3.0", "@octokit/types@^12.4.0": - version "12.4.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.4.0.tgz#8f97b601e91ce6b9776ed8152217e77a71be7aac" - integrity sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ== +"@octokit/types@^12.0.0", "@octokit/types@^12.2.0", "@octokit/types@^12.4.0": + version "12.5.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.5.0.tgz#a60003ad6bb073e5cac936f6ecf47c834f87c058" + integrity sha512-YJEKcb0KkJlIUNU/zjnZwHEP8AoVh/OoIcP/1IyR4UHxExz7fzpe/a8IG4wBtQi7QDEqiomVLX88S6FpxxAJtg== dependencies: "@octokit/openapi-types" "^19.1.0" @@ -1221,12 +1013,12 @@ resolved "https://registry.yarnpkg.com/@pnpm/constants/-/constants-7.1.1.tgz#3db261425fe15425aa213a2b003f4f60c9378b43" integrity sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw== -"@pnpm/core-loggers@9.0.4": - version "9.0.4" - resolved "https://registry.yarnpkg.com/@pnpm/core-loggers/-/core-loggers-9.0.4.tgz#019bd12bb0ca5f824e89eaae4a57470d62177271" - integrity sha512-P5IiCwLbYy/vlCDTxEMReB67NVs1Y4ip6iIEM3Y1fbxm0JbPMWTUMKGf2fy7eqGDF3/Vuxk5H7o/A4II6SWzMA== +"@pnpm/core-loggers@9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@pnpm/core-loggers/-/core-loggers-9.0.6.tgz#59a65822cc5ef901dad5aca5b8f1f9562cf91e2a" + integrity sha512-iK67SGbp+06bA/elpg51wygPFjNA7JKHtKkpLxqXXHw+AjFFBC3f2OznJsCIuDK6HdGi5UhHLYqo5QxJ2gMqJQ== dependencies: - "@pnpm/types" "9.4.0" + "@pnpm/types" "9.4.2" "@pnpm/error@5.0.2": version "5.0.2" @@ -1267,29 +1059,29 @@ semver "^7.3.5" validate-npm-package-name "^4.0.0" -"@pnpm/npm-resolver@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@pnpm/npm-resolver/-/npm-resolver-18.0.0.tgz#e7023b07b2e12836a2e3d354fc9fed3e1cccc4a3" - integrity sha512-FGHmnRjSf7tQHagk6bMrUtHvZbz3ROUoGRDrpMyqJo///+S7SZt/hSDS77PhZ7T6PRXipkFyUtRkqtHmGKFCAg== +"@pnpm/npm-resolver@18.1.0": + version "18.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/npm-resolver/-/npm-resolver-18.1.0.tgz#bbafcdf88391ee84e2dc78c2b599b54dbbc8f8e3" + integrity sha512-fUYKX/iHiHldL0VRVvkQI35YK2jWhZEkPO6rrGke8309+LKAo12v833nBttMDpQrtHefmqhB4mhCzQq6L2Xqmg== dependencies: - "@pnpm/core-loggers" "9.0.4" + "@pnpm/core-loggers" "9.0.6" "@pnpm/error" "5.0.2" "@pnpm/fetching-types" "5.0.0" "@pnpm/graceful-fs" "3.2.0" "@pnpm/resolve-workspace-range" "5.0.1" - "@pnpm/resolver-base" "11.0.0" - "@pnpm/types" "9.4.0" + "@pnpm/resolver-base" "11.1.0" + "@pnpm/types" "9.4.2" "@zkochan/retry" "^0.2.0" encode-registry "^3.0.1" load-json-file "^6.2.0" - lru-cache "^10.0.1" + lru-cache "^10.0.2" normalize-path "^3.0.0" p-limit "^3.1.0" p-memoize "4.0.1" parse-npm-tarball-url "^3.0.0" path-temp "^2.1.0" ramda "npm:@pnpm/ramda@0.28.1" - rename-overwrite "^4.0.4" + rename-overwrite "^5.0.0" semver "^7.5.4" ssri "10.0.5" version-selector-type "^3.0.0" @@ -1301,68 +1093,68 @@ dependencies: semver "^7.4.0" -"@pnpm/resolver-base@11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@pnpm/resolver-base/-/resolver-base-11.0.0.tgz#f18d65d805766008ee948e939bb7dffad84b8083" - integrity sha512-oxfjO8Ie6aBQPXSqOWGJP9s0xj9Z4cbRI7fK63WKhjwmNH4CTrSfikRL2o4FoXo2APAbJEUp2lCxx+86dq2tUg== +"@pnpm/resolver-base@11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/resolver-base/-/resolver-base-11.1.0.tgz#e640ba9ae096bf05a0b905496a63509556322618" + integrity sha512-y2qKaj18pwe1VWc3YXEitdYFo+WqOOt60aqTUuOVkJAirUzz0DzuYh3Ifct4znYWPdgUXHaN5DMphNF5iL85rA== dependencies: - "@pnpm/types" "9.4.0" + "@pnpm/types" "9.4.2" -"@pnpm/types@9.4.0": - version "9.4.0" - resolved "https://registry.yarnpkg.com/@pnpm/types/-/types-9.4.0.tgz#243b62eea4beab0bb65f1ecec1dfb6f739bba9bd" - integrity sha512-IRDuIuNobLRQe0UyY2gbrrTzYS46tTNvOEfL6fOf0Qa8NyxUzeXz946v7fQuQE3LSBf8ENBC5SXhRmDl+mBEqA== +"@pnpm/types@9.4.2": + version "9.4.2" + resolved "https://registry.yarnpkg.com/@pnpm/types/-/types-9.4.2.tgz#0a34c3c41d5452461d8d8958374a727f9c46cfb2" + integrity sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA== -"@pnpm/workspace.pkgs-graph@^2.0.11": - version "2.0.11" - resolved "https://registry.yarnpkg.com/@pnpm/workspace.pkgs-graph/-/workspace.pkgs-graph-2.0.11.tgz#37ae4e0e470c3df8252e9d91076bf7ec62e7b250" - integrity sha512-VRX7E7pX92C0akCMYGzsTqJoOwQS7/8R40pAPK7smgaEpKeEgVThqnIXt+wPdseD5CzS7OzMaIWlT3WXr3O5rQ== +"@pnpm/workspace.pkgs-graph@^2.0.13": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@pnpm/workspace.pkgs-graph/-/workspace.pkgs-graph-2.0.14.tgz#3ea12b8d95987e64bc98876422ea7ad578cd022a" + integrity sha512-SBXXyWDkPEoaLTjLRyQzRHoBYH+P0NLcIjX1yPUxuJiMTvGOMzjpLWTuxYNVe/P0V0VQMrjpJFaJPjlViNLhzg== dependencies: "@pnpm/npm-package-arg" "^1.0.0" - "@pnpm/npm-resolver" "18.0.0" + "@pnpm/npm-resolver" "18.1.0" "@pnpm/resolve-workspace-range" "5.0.1" ramda "npm:@pnpm/ramda@0.28.1" "@prisma/client@^5.4.1": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.7.0.tgz#c29dd9a16e100902eb2d2443d90fee2482d2aeac" - integrity sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg== + version "5.9.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.9.1.tgz#d92bd2f7f006e0316cb4fda9d73f235965cf2c64" + integrity sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ== -"@prisma/debug@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.7.0.tgz#abdb2060be4fe819e73e2683cf1b039841566198" - integrity sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA== +"@prisma/debug@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.9.1.tgz#906274e73d3267f88b69459199fa3c51cd9511a3" + integrity sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA== -"@prisma/engines-version@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9": - version "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz#777827898f1bfe6a76b17fbe7d9600cf543c4cc1" - integrity sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg== +"@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64": + version "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz#54d2164f28d23e09d41cf9eb0bddbbe7f3aaa660" + integrity sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ== -"@prisma/engines@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.7.0.tgz#a32e232819b66bd9dee7500b455781742dc54b2f" - integrity sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA== +"@prisma/engines@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.9.1.tgz#767539afc6f193a182d0495b30b027f61f279073" + integrity sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ== dependencies: - "@prisma/debug" "5.7.0" - "@prisma/engines-version" "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9" - "@prisma/fetch-engine" "5.7.0" - "@prisma/get-platform" "5.7.0" + "@prisma/debug" "5.9.1" + "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" + "@prisma/fetch-engine" "5.9.1" + "@prisma/get-platform" "5.9.1" -"@prisma/fetch-engine@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz#7d2795828b692b02707e7ab6876f6227a68fc309" - integrity sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w== +"@prisma/fetch-engine@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz#5d3b2c9af54a242e37b3f9561b59ab72f8e92818" + integrity sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA== dependencies: - "@prisma/debug" "5.7.0" - "@prisma/engines-version" "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9" - "@prisma/get-platform" "5.7.0" + "@prisma/debug" "5.9.1" + "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" + "@prisma/get-platform" "5.9.1" -"@prisma/get-platform@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.7.0.tgz#eb81011f537c2d10c0225278cd5165a82d0b57c8" - integrity sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA== +"@prisma/get-platform@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.9.1.tgz#a66bb46ab4d30db786c84150ef074ab0aad4549e" + integrity sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg== dependencies: - "@prisma/debug" "5.7.0" + "@prisma/debug" "5.9.1" "@radix-ui/colors@^3.0.0", "@radix-ui/colors@^3.0.0-rc.4": version "3.0.0" @@ -1922,9 +1714,9 @@ "@babel/runtime" "^7.13.10" "@radix-ui/themes@^2.0.1": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/themes/-/themes-2.0.2.tgz#39514b346e4a5cef0324d1e4bbe7ab09d7419bd2" - integrity sha512-9NaIp3HkoSkyCGAnJ21PL8yuazcV2q/q3wVNJ7jU00qZfzeybYREpoik11tMRA0+DWttQhSUy1ZqD8u6YqsbkQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/themes/-/themes-2.0.3.tgz#c9e8f4029d73769f356dbe29c97ce3cbade601a8" + integrity sha512-yaXQ8aWT2P1CQ0Xe6YCRD9HXsfMTvKkrIYkrc4aitCzhGTLS0sjtTqKmrxIWMVA+3DIbEuG9K/8aAMRJBhep8g== dependencies: "@radix-ui/colors" "^3.0.0" "@radix-ui/primitive" "^1.0.1" @@ -2000,10 +1792,10 @@ resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.6.1.tgz#913d3a68c5cbc1124fdb18eff919432f7b6abdde" integrity sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q== -"@react-three/drei@^9.90.1": - version "9.92.1" - resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-9.92.1.tgz#004c8d042b24e1a790de8982f93f38269e3d9652" - integrity sha512-XgC6Y0er7WDn1UtjZ8MwXt1UZJj84IVEcZNwkaZ1BTia21IAJsbFo6G8A6XAkNFfivY8oCslljwQnqz623RWIQ== +"@react-three/drei@^9.93.0": + version "9.99.0" + resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-9.99.0.tgz#70cdbd6fd6ab58279bc67f4ea6ce8cb822e67d26" + integrity sha512-F4Oyxn+t+7R+kA+NGuK2izog1KkwH7KmEBd6aTYTex43YDRY5AFXfCDoiD9SOU6jliQRW2Vx2qw/YHfPBYMUwA== dependencies: "@babel/runtime" "^7.11.2" "@mediapipe/tasks-vision" "0.10.8" @@ -2013,27 +1805,25 @@ cross-env "^7.0.3" detect-gpu "^5.0.28" glsl-noise "^0.0.0" - lodash.clamp "^4.0.3" - lodash.omit "^4.5.0" - lodash.pick "^4.4.0" - maath "^0.9.0" + maath "^0.10.7" meshline "^3.1.6" react-composer "^5.0.3" react-merge-refs "^1.1.0" stats-gl "^2.0.0" stats.js "^0.17.0" suspend-react "^0.1.3" - three-mesh-bvh "^0.6.7" - three-stdlib "^2.28.0" - troika-three-text "^0.47.2" + three-mesh-bvh "^0.7.0" + three-stdlib "^2.29.4" + troika-three-text "^0.49.0" + tunnel-rat "^0.1.2" utility-types "^3.10.0" uuid "^9.0.1" - zustand "^3.5.13" + zustand "^3.7.1" -"@react-three/fiber@^8.15.12": - version "8.15.12" - resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.15.12.tgz#0bc08730eec32e53373482fbb518d116d3aa3df3" - integrity sha512-yg0CyXVHIdSbNjM/GAgDrGJnKLTsfTlaR5FoJGEh9IgVKptOoudnFZhBt/Cau4rzx2X6eLmB1+aWOm1dEHSUpg== +"@react-three/fiber@^8.15.14": + version "8.15.16" + resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.15.16.tgz#eadaa9432dd55a7d7334c42880470c7ac3c30e83" + integrity sha512-4f47K9e2mP8W/guNtu3e2J/Nt6GwKTsX/YP2dktPZRcpHYEsqfXCO8kSfvVMb+lQ8wR0HoFzggqdnGuhZaui0g== dependencies: "@babel/runtime" "^7.17.8" "@types/react-reconciler" "^0.26.7" @@ -2125,80 +1915,80 @@ "@resvg/resvg-js-win32-ia32-msvc" "2.6.0" "@resvg/resvg-js-win32-x64-msvc" "2.6.0" -"@rollup/rollup-android-arm-eabi@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.8.0.tgz#0e42b155630adaaec0f659f979ece4b7d3391329" - integrity sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q== +"@rollup/rollup-android-arm-eabi@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6" + integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w== -"@rollup/rollup-android-arm64@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.8.0.tgz#6c9fe8f9eb0cd9029be93b822b1a1c2d6b31c275" - integrity sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ== +"@rollup/rollup-android-arm64@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2" + integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ== -"@rollup/rollup-darwin-arm64@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.8.0.tgz#7dcb1317a8089762c1f7e437c1e1d695b787b70f" - integrity sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g== +"@rollup/rollup-darwin-arm64@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69" + integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ== -"@rollup/rollup-darwin-x64@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.8.0.tgz#91d7d31d22607c4fcccce9126457d6785c57f7c7" - integrity sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA== +"@rollup/rollup-darwin-x64@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8" + integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg== -"@rollup/rollup-linux-arm-gnueabihf@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.8.0.tgz#f2015d6e4ff41417f2e2c55b3d9625346e355c57" - integrity sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A== +"@rollup/rollup-linux-arm-gnueabihf@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d" + integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA== -"@rollup/rollup-linux-arm64-gnu@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.8.0.tgz#95207444b78f235c9de62797ec2a3dcd18daf473" - integrity sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g== +"@rollup/rollup-linux-arm64-gnu@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68" + integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA== -"@rollup/rollup-linux-arm64-musl@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.8.0.tgz#bc994c676fd3aae14aaa905040fdcde461e41ce5" - integrity sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA== +"@rollup/rollup-linux-arm64-musl@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7" + integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ== -"@rollup/rollup-linux-riscv64-gnu@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.8.0.tgz#67984f1d1f663610f4e1f6e638a2b07169562448" - integrity sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw== +"@rollup/rollup-linux-riscv64-gnu@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7" + integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw== -"@rollup/rollup-linux-x64-gnu@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.8.0.tgz#48e09a455d543be986003c7c2ea37c16ff4a53d5" - integrity sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q== +"@rollup/rollup-linux-x64-gnu@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3" + integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA== -"@rollup/rollup-linux-x64-musl@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.8.0.tgz#df8d0966b02d1bdc6447b5eb58fa18666da1f3e8" - integrity sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg== +"@rollup/rollup-linux-x64-musl@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05" + integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw== -"@rollup/rollup-win32-arm64-msvc@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.8.0.tgz#7cce8efc5c9239a1bafe7ac2a52743bc5734471f" - integrity sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA== +"@rollup/rollup-win32-arm64-msvc@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e" + integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw== -"@rollup/rollup-win32-ia32-msvc@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.8.0.tgz#794ef4058d04f97447e4434c083b1309b2be73c2" - integrity sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw== +"@rollup/rollup-win32-ia32-msvc@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40" + integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA== -"@rollup/rollup-win32-x64-msvc@4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.8.0.tgz#05057436705f0be9203c30612a48225ec70af741" - integrity sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg== +"@rollup/rollup-win32-x64-msvc@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235" + integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg== "@sapphire/async-queue@^1.5.0": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.1.tgz#f72f8f0dea05e83ba830387567dd6ef8a996537a" - integrity sha512-1RdpsmDQR/aWfp8oJzPtn4dNQrbpqSL5PIA0uAB/XwerPXUf994Ug1au1e7uGcD7ei8/F63UDjr5GWps1g/HxQ== + version "1.5.2" + resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.2.tgz#2982dce16e5b8b1ea792604d20c23c0585877b97" + integrity sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg== "@sapphire/shapeshift@^3.9.3": - version "3.9.4" - resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-3.9.4.tgz#427f9f19c9d32ae94c56cd08529db04e9ea626d0" - integrity sha512-SiOoCBmm8O7QuadLJnX4V0tAkhC54NIOZJtmvw+5zwnHaiulGkjY02wxCuK8Gf4V540ILmGz+UulC0U8mrOZjg== + version "3.9.6" + resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-3.9.6.tgz#bd9629c08641f5b94ae094e23f092187a3ed9a7d" + integrity sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g== dependencies: fast-deep-equal "^3.1.3" lodash "^4.17.21" @@ -2209,9 +1999,21 @@ integrity sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA== "@sapphire/snowflake@^3.5.1": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.2.tgz#ca3d11a0011563c90b103f8ef398f2dea3e7beb4" - integrity sha512-FTm9RdyELF21PQN5dS/HLRs90XqWclHa+p0gkonc+BA2X2QKfFySHSjUbO65rmArd/ghR9Ahj2fMfedTZEqzOw== + version "3.5.3" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.3.tgz#0c102aa2ec5b34f806e9bc8625fc6a5e1d0a0c6a" + integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== + +"@shikijs/core@1.1.5", "@shikijs/core@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.1.5.tgz#ed92e3b7c89ba150564127062c29ae715c5da875" + integrity sha512-cKc5vGQ4p/4sjx48BHIO7CvLaN32vqpz5Wh7v2n+U1EezGdfX4Wms7khBctKz3iCg9yYq4sfGUc2t+JWj6EUsw== + +"@shikijs/transformers@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-1.1.5.tgz#13608be740c75a46fb3b7371822a48f1013bbe79" + integrity sha512-ot6KWPmLuSN9nA9FAhttOXZIjKIy7cnwpNtI9aWmYN72RUaDz8eojRfMGUXsXXUxW/buvcvdZQAQldk7/pFpdw== + dependencies: + shiki "1.1.5" "@shuding/opentype.js@1.4.0-beta.0": version "1.4.0-beta.0" @@ -2221,10 +2023,10 @@ fflate "^0.7.3" string.prototype.codepointat "^0.2.1" -"@sindresorhus/merge-streams@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz#9cd84cc15bc865a5ca35fcaae198eb899f7b5c90" - integrity sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw== +"@sindresorhus/merge-streams@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" + integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== "@snyk/github-codeowners@1.1.0": version "1.1.0" @@ -2262,7 +2064,7 @@ dependencies: "@types/node" "*" -"@types/decompress@^4.2.6": +"@types/decompress@^4.2.7": version "4.2.7" resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.7.tgz#604f69b69d519ecb74dea1ea0829f159b85e1332" integrity sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g== @@ -2279,10 +2081,15 @@ resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.4.tgz#dc7c166b76c7b03b27e32f80edf01d91eb5d9af2" integrity sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg== +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/express-serve-static-core@^4.17.33": - version "4.17.41" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6" - integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA== + version "4.17.43" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" + integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== dependencies: "@types/node" "*" "@types/qs" "*" @@ -2299,13 +2106,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/hast@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.3.tgz#7f75e6b43bc3f90316046a287d9ad3888309f7e1" - integrity sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ== - dependencies: - "@types/unist" "*" - "@types/http-errors@*": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" @@ -2346,13 +2146,6 @@ "@types/linkify-it" "*" "@types/mdurl" "*" -"@types/mdast@^4.0.0": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" - integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== - dependencies: - "@types/unist" "*" - "@types/mdurl@*": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.5.tgz#3e0d2db570e9fb6ccb2dc8fde0be1d79ac810d39" @@ -2369,9 +2162,9 @@ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/node@*", "@types/node@^20.6.5": - version "20.10.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.4.tgz#b246fd84d55d5b1b71bf51f964bd514409347198" - integrity sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg== + version "20.11.19" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" + integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== dependencies: undici-types "~5.26.4" @@ -2380,15 +2173,27 @@ resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz#90267db13f64d6e9ccb5ae3eac92786a7c77a516" integrity sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A== +"@types/parse-dds@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/parse-dds/-/parse-dds-1.0.3.tgz#a1bb00421906094510d88c64af978564d3c35c9b" + integrity sha512-LD9BruIpfP6Z9aAJ488h+PwtSwLzM0NO+Rs1l/zjcPLBTqR31OnIGmAvJYhGiHhbOn7R0YXw1gL5bImNiK6HNw== + +"@types/pngjs@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.4.tgz#9a457aebabd944efde1a773a0fa1d74933e8021b" + integrity sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ== + dependencies: + "@types/node" "*" + "@types/prop-types@*": version "15.7.11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== "@types/qs@*": - version "6.9.10" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.10.tgz#0af26845b5067e1c9a622658a51f60a3934d51e8" - integrity sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw== + version "6.9.11" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" + integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== "@types/range-parser@*": version "1.2.7" @@ -2410,9 +2215,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^18.2.22": - version "18.2.45" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.45.tgz#253f4fac288e7e751ab3dc542000fb687422c15c" - integrity sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg== + version "18.2.57" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.57.tgz#147b516d8bdb2900219acbfc6f939bdeecca7691" + integrity sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2445,30 +2250,25 @@ resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.3.tgz#705446e12ce0fad618557dd88236f51148b7a935" integrity sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ== -"@types/three@^0.159.0": - version "0.159.0" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.159.0.tgz#e1c9d2a84543c2febee5244cc5cf7d657d13aa45" - integrity sha512-2gybdh7HtX+rGUgslzK7QEJfzD2I0qrbUGzKk+dK0FDx49UHkNX0rqZVRzIgeFjBd1HzzhNNgwNoMacm3Wyc7w== +"@types/three@^0.160.0": + version "0.160.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.160.0.tgz#7915a97e0a14ccaa9ccbb9f190c5730b04a23075" + integrity sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w== dependencies: "@types/stats.js" "*" "@types/webxr" "*" fflate "~0.6.10" meshoptimizer "~0.18.1" -"@types/unist@*", "@types/unist@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" - integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== - "@types/web-bluetooth@^0.0.20": version "0.0.20" resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== "@types/webxr@*", "@types/webxr@^0.5.2": - version "0.5.10" - resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.10.tgz#19c76208ec015ca3f139505e14d94d6b740f518a" - integrity sha512-n3u5sqXQJhf1CS68mw3Wf16FQ4cRPNBBwdYLFzq3UddiADOim1Pn3Y6PBdDilz1vOJF3ybLxJ8ZEDlLIzrOQZg== + version "0.5.14" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.14.tgz#9a03121a4912ea113b31e5c9c17f164d4fff8a1f" + integrity sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA== "@types/ws@8.5.9": version "8.5.9" @@ -2484,11 +2284,6 @@ dependencies: "@types/node" "*" -"@ungap/structured-clone@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - "@use-gesture/core@10.3.0": version "10.3.0" resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.3.0.tgz#9afd3777a45b2a08990a5dcfcf8d9ddd55b00db9" @@ -2501,17 +2296,17 @@ dependencies: "@use-gesture/core" "10.3.0" -"@vanilla-extract/babel-plugin-debug-ids@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.3.tgz#ce07190343b51ed658b385bdce1e79952a4e8526" - integrity sha512-vm4jYu1xhSa6ofQ9AhIpR3DkAp4c+eoR1Rpm8/TQI4DmWbmGbOjYRcqV0aWsfaIlNhN4kFuxFMKBNN9oG6iRzA== +"@vanilla-extract/babel-plugin-debug-ids@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.5.tgz#e24424f46dd7737764a4bb5ac6dcdf19240f88bc" + integrity sha512-Rc9A6ylsw7EBErmpgqCMvc/Z/eEZxI5k1xfLQHw7f5HHh3oc5YfzsAsYU/PdmSNjF1dp3sGEViBdDltvwnfVaA== dependencies: - "@babel/core" "^7.20.7" + "@babel/core" "^7.23.9" "@vanilla-extract/css@^1.13.0", "@vanilla-extract/css@^1.14.0": - version "1.14.0" - resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.14.0.tgz#45fab9c04d893e3e363cf2cde7559d21233b7f63" - integrity sha512-rYfm7JciWZ8PFzBM/HDiE2GLnKI3xJ6/vdmVJ5BSgcCZ5CxRlM9Cjqclni9lGzF3eMOijnUhCd/KV8TOzyzbMA== + version "1.14.1" + resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.14.1.tgz#ae24b2d2ff6abd452f8e72440073b05a0372d62e" + integrity sha512-V4JUuHNjZgl64NGfkDJePqizkNgiSpphODtZEs4cCPuxLAzwOUJYATGpejwimJr1n529kq4DEKWexW22LMBokw== dependencies: "@emotion/hash" "^0.9.0" "@vanilla-extract/private" "^1.0.3" @@ -2525,180 +2320,196 @@ modern-ahocorasick "^1.0.0" outdent "^0.8.0" -"@vanilla-extract/integration@^6.0.0": - version "6.2.4" - resolved "https://registry.yarnpkg.com/@vanilla-extract/integration/-/integration-6.2.4.tgz#bd8a5ec0916051c1ef5fb66d8484a5cad8d8c58d" - integrity sha512-+AfymNMVq9sEUe0OJpdCokmPZg4Zi6CqKaW/PnUOfDwEn53ighHOMOBl5hAgxYR8Kiz9NG43Bn00mkjWlFi+ng== +"@vanilla-extract/integration@^7.0.0": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@vanilla-extract/integration/-/integration-7.1.1.tgz#fd7781f859efea5698a2f1bb0b3d1107096ea496" + integrity sha512-2JjDKL2HDTazis4oTkUFsQFUyw61k8oym9r0NOLSJC0JB0W25W1ryVZvDx74d3deIyB5AWbCselyzl3gja30kQ== dependencies: - "@babel/core" "^7.20.7" - "@babel/plugin-syntax-typescript" "^7.20.0" - "@vanilla-extract/babel-plugin-debug-ids" "^1.0.2" + "@babel/core" "^7.23.9" + "@babel/plugin-syntax-typescript" "^7.23.3" + "@vanilla-extract/babel-plugin-debug-ids" "^1.0.5" "@vanilla-extract/css" "^1.14.0" - esbuild "0.17.6" + esbuild "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0" eval "0.1.8" find-up "^5.0.0" javascript-stringify "^2.0.1" lodash "^4.17.21" - mlly "^1.1.0" + mlly "^1.4.2" outdent "^0.8.0" - vite "^4.1.4" - vite-node "^0.28.5" + vite "^5.0.11" + vite-node "^1.2.0" "@vanilla-extract/next-plugin@^2.3.1": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@vanilla-extract/next-plugin/-/next-plugin-2.3.2.tgz#e82bf1c343f01e2ba5971ea7ab5a6a4f7b8d8668" - integrity sha512-wBulJAhmr8guYo5YxrfsA1T/TQ3tbMJjNtE6QrVZvs1NTnT9/WAmf4IVrbUvQbH+LBj5LXO8dRzueGPAvepSng== + version "2.3.6" + resolved "https://registry.yarnpkg.com/@vanilla-extract/next-plugin/-/next-plugin-2.3.6.tgz#fb685f2347bef4c29584aefb64eb9caeace8bc63" + integrity sha512-EzVJU5mUgMnERW4qVxIlivzjioFbgY+Q/hrrZxEsZBRpbcV35vf7oksCJc5seG2nzWTBRGJGoodJaXSm74pz3g== dependencies: - "@vanilla-extract/webpack-plugin" "^2.3.1" + "@vanilla-extract/webpack-plugin" "^2.3.5" "@vanilla-extract/private@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@vanilla-extract/private/-/private-1.0.3.tgz#7ec72bc2ff6fe51f9d650f962e8d1989b073690f" integrity sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ== -"@vanilla-extract/webpack-plugin@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@vanilla-extract/webpack-plugin/-/webpack-plugin-2.3.1.tgz#359d93b8b579560c1dadc6ae8eb655126332e02e" - integrity sha512-S767iP+S67Uh+BsTe+u60BMbtRqZAxzjK7gYd6a+1hcbMKznYxdQMeP2qs9wr+NhRqZFrNxewo3/ztMnN85TOg== +"@vanilla-extract/webpack-plugin@^2.3.5": + version "2.3.6" + resolved "https://registry.yarnpkg.com/@vanilla-extract/webpack-plugin/-/webpack-plugin-2.3.6.tgz#d07ebf3769f12d8430cf2f7664c6c2b6496ba5e5" + integrity sha512-GnWdVzBFDfCzNRyBoYzuwPTEWFOKFtq3Td9gla6ng+dCeJFRGW+6jT5hXvqJojw8Rk2fWzc6XA2FHPrVa7W9wA== dependencies: - "@vanilla-extract/integration" "^6.0.0" + "@vanilla-extract/integration" "^7.0.0" chalk "^4.1.1" debug "^4.3.1" loader-utils "^2.0.0" -"@vitejs/plugin-vue@^4.5.0": - version "4.5.2" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz#1212d81bc83680e14448fefe55abd9fe1ed49ed1" - integrity sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ== +"@vercel/analytics@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.2.2.tgz#715d8f203a170c06ba36b363e03b048c03060d5d" + integrity sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A== + dependencies: + server-only "^0.0.1" + +"@vitejs/plugin-vue@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" + integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== "@vladfrangu/async_event_emitter@^2.2.2": version "2.2.4" resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.4.tgz#d3537432c6db6444680a596271dff8ea407343b3" integrity sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug== -"@vue/compiler-core@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.11.tgz#9fa26f8c81b9b34365f94ce1ed4d0e6e6f94a2ac" - integrity sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w== +"@vue/compiler-core@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.19.tgz#3161b1ede69da00f3ce8155dfab907a3eaa0515e" + integrity sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w== dependencies: - "@babel/parser" "^7.23.5" - "@vue/shared" "3.3.11" + "@babel/parser" "^7.23.9" + "@vue/shared" "3.4.19" + entities "^4.5.0" estree-walker "^2.0.2" source-map-js "^1.0.2" -"@vue/compiler-dom@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.11.tgz#36a76ea3a296d41bad133a6912cb0a847d969e4f" - integrity sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw== +"@vue/compiler-dom@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz#2457e57e978f431e3b5fd11fc50a3e92d5816f9a" + integrity sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA== dependencies: - "@vue/compiler-core" "3.3.11" - "@vue/shared" "3.3.11" + "@vue/compiler-core" "3.4.19" + "@vue/shared" "3.4.19" -"@vue/compiler-sfc@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.11.tgz#acfae240c875d067e0e2c9a4e2d910074408c73b" - integrity sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA== +"@vue/compiler-sfc@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz#33b238ded6d63e51f6a7048b742626f6007df129" + integrity sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg== dependencies: - "@babel/parser" "^7.23.5" - "@vue/compiler-core" "3.3.11" - "@vue/compiler-dom" "3.3.11" - "@vue/compiler-ssr" "3.3.11" - "@vue/reactivity-transform" "3.3.11" - "@vue/shared" "3.3.11" + "@babel/parser" "^7.23.9" + "@vue/compiler-core" "3.4.19" + "@vue/compiler-dom" "3.4.19" + "@vue/compiler-ssr" "3.4.19" + "@vue/shared" "3.4.19" estree-walker "^2.0.2" - magic-string "^0.30.5" - postcss "^8.4.32" + magic-string "^0.30.6" + postcss "^8.4.33" source-map-js "^1.0.2" -"@vue/compiler-ssr@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.11.tgz#598942a73b64f2bd3f95908b104a7fbb55fc41a2" - integrity sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg== +"@vue/compiler-ssr@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz#1f8ee06005ebbaa354f8783fad84e9f7ea4a69c2" + integrity sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw== dependencies: - "@vue/compiler-dom" "3.3.11" - "@vue/shared" "3.3.11" + "@vue/compiler-dom" "3.4.19" + "@vue/shared" "3.4.19" -"@vue/devtools-api@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.1.tgz#7f71f31e40973eeee65b9a64382b13593fdbd697" - integrity sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA== - -"@vue/reactivity-transform@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.11.tgz#2bd486f4eff60c8724309925618891e722fcfadc" - integrity sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg== +"@vue/devtools-api@^7.0.14": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.0.15.tgz#875a228fb5154fa4a6678e8cbc4c894089d87cde" + integrity sha512-kgEYWosDyWpS1vFSuJNNWUnHkP+VkL3Y+9mw+rf7ex41SwbYL/WdC3KXqAtjiSrEs7r/FrHmUTh0BkINJPFkbA== dependencies: - "@babel/parser" "^7.23.5" - "@vue/compiler-core" "3.3.11" - "@vue/shared" "3.3.11" - estree-walker "^2.0.2" - magic-string "^0.30.5" + "@vue/devtools-kit" "^7.0.15" -"@vue/reactivity@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.3.11.tgz#91f8e6c9ac60a595a5278c836b197628fd947a0d" - integrity sha512-D5tcw091f0nuu+hXq5XANofD0OXnBmaRqMYl5B3fCR+mX+cXJIGNw/VNawBqkjLNWETrFW0i+xH9NvDbTPVh7g== +"@vue/devtools-kit@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.0.15.tgz#d6f5d527358ad6c707cb13ccbbc4260a9c6dcae5" + integrity sha512-dT7OeCe1LUCIhHIb/yRR6Hn+XHh73r1o78onqCrxEKHdoZwBItiIeVnmJZPEUDFstIxfs+tJL231mySk3laTow== dependencies: - "@vue/shared" "3.3.11" + "@vue/devtools-shared" "^7.0.15" + hookable "^5.5.3" + mitt "^3.0.1" + perfect-debounce "^1.0.0" + speakingurl "^14.0.1" -"@vue/runtime-core@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.11.tgz#63defba57bc54c1dac68a95b56c2633b1419193d" - integrity sha512-g9ztHGwEbS5RyWaOpXuyIVFTschclnwhqEbdy5AwGhYOgc7m/q3NFwr50MirZwTTzX55JY8pSkeib9BX04NIpw== +"@vue/devtools-shared@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.0.15.tgz#a179552dab23b7c7e4bb22726a82a268eef7239b" + integrity sha512-fpfvMVvS7aDgO7x2JPFiTQ1MHcCc63/bE7yTgs278gMBybuO9b3hdiZ/k0Pw1rN+RefaU9yQiFA+5CCFc1D+6w== dependencies: - "@vue/reactivity" "3.3.11" - "@vue/shared" "3.3.11" + rfdc "^1.3.1" -"@vue/runtime-dom@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.11.tgz#1146d8d280b0fec4d2e18c4a4c8f8121d0cecc09" - integrity sha512-OlhtV1PVpbgk+I2zl+Y5rQtDNcCDs12rsRg71XwaA2/Rbllw6mBLMi57VOn8G0AjOJ4Mdb4k56V37+g8ukShpQ== +"@vue/reactivity@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.19.tgz#8cf335d97d07881d8184cb23289289dc18b03f60" + integrity sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA== dependencies: - "@vue/runtime-core" "3.3.11" - "@vue/shared" "3.3.11" - csstype "^3.1.2" + "@vue/shared" "3.4.19" -"@vue/server-renderer@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.11.tgz#409aed8031a125791e2143552975ecd1958ad601" - integrity sha512-AIWk0VwwxCAm4wqtJyxBylRTXSy1wCLOKbWxHaHiu14wjsNYtiRCSgVuqEPVuDpErOlRdNnuRgipQfXRLjLN5A== +"@vue/runtime-core@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.19.tgz#ef10357fdf3afdf68523b55424541000105e2aeb" + integrity sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw== dependencies: - "@vue/compiler-ssr" "3.3.11" - "@vue/shared" "3.3.11" + "@vue/reactivity" "3.4.19" + "@vue/shared" "3.4.19" -"@vue/shared@3.3.11": - version "3.3.11" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.11.tgz#f6a038e15237edefcc90dbfe7edb806dd355c7bd" - integrity sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw== +"@vue/runtime-dom@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz#079141e31d9f47515b9595f29843d51011f88739" + integrity sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g== + dependencies: + "@vue/runtime-core" "3.4.19" + "@vue/shared" "3.4.19" + csstype "^3.1.3" -"@vueuse/core@10.7.0", "@vueuse/core@^10.6.1": - version "10.7.0" - resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.7.0.tgz#34f2f02f179dc0dcffc2be70d6b1233e011404b9" - integrity sha512-4EUDESCHtwu44ZWK3Gc/hZUVhVo/ysvdtwocB5vcauSV4B7NiGY5972WnsojB3vRNdxvAt7kzJWE2h9h7C9d5w== +"@vue/server-renderer@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.19.tgz#e6f8ff5268d0758766ca9835375218924d5f0eb6" + integrity sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw== + dependencies: + "@vue/compiler-ssr" "3.4.19" + "@vue/shared" "3.4.19" + +"@vue/shared@3.4.19": + version "3.4.19" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.19.tgz#28105147811bcf1e6612bf1c9ab0c6d91ada019c" + integrity sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw== + +"@vueuse/core@10.7.2", "@vueuse/core@^10.7.2": + version "10.7.2" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.7.2.tgz#78917803a29a0bca1803a6521fdf7ff873f6e72c" + integrity sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ== dependencies: "@types/web-bluetooth" "^0.0.20" - "@vueuse/metadata" "10.7.0" - "@vueuse/shared" "10.7.0" + "@vueuse/metadata" "10.7.2" + "@vueuse/shared" "10.7.2" vue-demi ">=0.14.6" -"@vueuse/integrations@^10.6.1": - version "10.7.0" - resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-10.7.0.tgz#80d9608f697bd836a2231b6318700f02504d399c" - integrity sha512-rxiMYgS+91n93qXpHZF9NbHhppWY6IJyVTDxt4acyChL0zZVx7P8FAAfpF1qVK8e4wfjerhpEiMJ0IZ1GWUZ2A== +"@vueuse/integrations@^10.7.2": + version "10.7.2" + resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-10.7.2.tgz#763de1337d3925be9097aac9a725eec39d4c8e69" + integrity sha512-+u3RLPFedjASs5EKPc69Ge49WNgqeMfSxFn+qrQTzblPXZg6+EFzhjarS5edj2qAf6xQ93f95TUxRwKStXj/sQ== dependencies: - "@vueuse/core" "10.7.0" - "@vueuse/shared" "10.7.0" + "@vueuse/core" "10.7.2" + "@vueuse/shared" "10.7.2" vue-demi ">=0.14.6" -"@vueuse/metadata@10.7.0": - version "10.7.0" - resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.7.0.tgz#7b05e6cfd376aa9bb339a81e16a89c12f3e88c03" - integrity sha512-GlaH7tKP2iBCZ3bHNZ6b0cl9g0CJK8lttkBNUX156gWvNYhTKEtbweWLm9rxCPIiwzYcr/5xML6T8ZUEt+DkvA== +"@vueuse/metadata@10.7.2": + version "10.7.2" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.7.2.tgz#ba0187ce138c287fd80301afc5b0d6a97e563633" + integrity sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ== -"@vueuse/shared@10.7.0": - version "10.7.0" - resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.7.0.tgz#21e425cc5ede421e0cda38ac59a0beee6da86b1b" - integrity sha512-kc00uV6CiaTdc3i1CDC4a3lBxzaBE9AgYNtFN87B5OOscqeWElj/uza8qVDmk7/U8JbqoONLbtqiLJ5LGRuqlw== +"@vueuse/shared@10.7.2": + version "10.7.2" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.7.2.tgz#746441fbc08072371dd600a55883422c83fd0cab" + integrity sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA== dependencies: vue-demi ">=0.14.6" @@ -2722,10 +2533,10 @@ accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn@^8.10.0: - version "8.11.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" - integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== +acorn@^8.11.3: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== aggregate-error@^3.0.0: version "3.1.0" @@ -2736,24 +2547,24 @@ aggregate-error@^3.0.0: indent-string "^4.0.0" algoliasearch@^4.19.1: - version "4.21.1" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.21.1.tgz#61fd5f9d4480fca263d9c22c2cdf24ef6d37631d" - integrity sha512-Ym0MGwOcjQhZ+s1N/j0o94g3vQD0MzNpWsfJLyPVCt0zHflbi0DwYX+9GPmTJ4BzegoxWMyCPgcmpd3R+VlOzQ== + version "4.22.1" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.22.1.tgz#f10fbecdc7654639ec20d62f109c1b3a46bc6afc" + integrity sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg== dependencies: - "@algolia/cache-browser-local-storage" "4.21.1" - "@algolia/cache-common" "4.21.1" - "@algolia/cache-in-memory" "4.21.1" - "@algolia/client-account" "4.21.1" - "@algolia/client-analytics" "4.21.1" - "@algolia/client-common" "4.21.1" - "@algolia/client-personalization" "4.21.1" - "@algolia/client-search" "4.21.1" - "@algolia/logger-common" "4.21.1" - "@algolia/logger-console" "4.21.1" - "@algolia/requester-browser-xhr" "4.21.1" - "@algolia/requester-common" "4.21.1" - "@algolia/requester-node-http" "4.21.1" - "@algolia/transporter" "4.21.1" + "@algolia/cache-browser-local-storage" "4.22.1" + "@algolia/cache-common" "4.22.1" + "@algolia/cache-in-memory" "4.22.1" + "@algolia/client-account" "4.22.1" + "@algolia/client-analytics" "4.22.1" + "@algolia/client-common" "4.22.1" + "@algolia/client-personalization" "4.22.1" + "@algolia/client-search" "4.22.1" + "@algolia/logger-common" "4.22.1" + "@algolia/logger-console" "4.22.1" + "@algolia/requester-browser-xhr" "4.22.1" + "@algolia/requester-common" "4.22.1" + "@algolia/requester-node-http" "4.22.1" + "@algolia/transporter" "4.22.1" ansi-regex@^5.0.1: version "5.0.1" @@ -2882,9 +2693,9 @@ body-parser@1.20.1: unpipe "1.0.0" bole@^5.0.0: - version "5.0.9" - resolved "https://registry.yarnpkg.com/bole/-/bole-5.0.9.tgz#ecd3b25d2f4b49941cd12028df89a71bdb142ae6" - integrity sha512-35GeBG6T7GW9VmLDF2IoKAtFCqMjmmq1uICbsICI0pID7ZAyUKlf7dg1wpXmn9GcMKHtg0S19CPMU5yfY3tv+g== + version "5.0.11" + resolved "https://registry.yarnpkg.com/bole/-/bole-5.0.11.tgz#c4a165975422daee6f576360e882a425c8e40617" + integrity sha512-KB0Ye0iMAW5BnNbnLfMSQcnI186hKUzE2fpkZWqcxsoTR7eqzlTidSOMYPHJOn/yR7VGH7uSZp37qH9q2Et0zQ== dependencies: fast-safe-stringify "^2.0.7" individual "^3.0.0" @@ -2917,12 +2728,12 @@ braces@^3.0.2: fill-range "^7.0.1" browserslist@^4.22.2: - version "4.22.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" - integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== dependencies: - caniuse-lite "^1.0.30001565" - electron-to-chromium "^1.4.601" + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" node-releases "^2.0.14" update-browserslist-db "^1.0.13" @@ -2949,11 +2760,6 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - buffer@^5.2.1: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -2994,14 +2800,16 @@ cac@^6.7.14: resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== -call-bind@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" - integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== +call-bind@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.1" - set-function-length "^1.1.1" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" camelize@^1.0.0: version "1.0.1" @@ -3009,19 +2817,14 @@ camelize@^1.0.0: integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== camera-controls@^2.4.2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.7.3.tgz#99e0449f68d203bf5f98f6c4ac0021c10b5c13a8" - integrity sha512-L4mxjBd3u8qiOLozdWrH2P8ZybSsDXBF7iyNyqNEFJhPUkovmuARWR8JTc1B/qlclOIg6FvZZA/0uAZMMim0mw== + version "2.8.3" + resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.8.3.tgz#54c296d21564dede84e56ffd6b75b3daa3c76e90" + integrity sha512-zFjqUR6onLkG+z1A6vAWfzovxZxWVSvp6e5t3lfZgfgPZtX3n74aykNAUaoRbq8Y3tOxadHkDjbfGDOP9hFf2w== -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001565: - version "1.0.30001568" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz#53fa9297273c9a977a560663f48cbea1767518b7" - integrity sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A== - -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== +caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587: + version "1.0.30001588" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz#07f16b65a7f95dba82377096923947fb25bce6e3" + integrity sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ== chalk@^2.4.2: version "2.4.2" @@ -3040,25 +2843,10 @@ chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - classnames@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-stack@^2.0.0: version "2.2.0" @@ -3115,11 +2903,6 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -3239,7 +3022,7 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^3.0.2, csstype@^3.0.7, csstype@^3.1.2: +csstype@^3.0.2, csstype@^3.0.7, csstype@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -3348,14 +3131,14 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-data-property@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" - integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== +define-data-property@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - get-intrinsic "^1.2.1" + es-define-property "^1.0.0" + es-errors "^1.3.0" gopd "^1.0.1" - has-property-descriptors "^1.0.0" depd@2.0.0: version "2.0.0" @@ -3367,20 +3150,15 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-gpu@^5.0.28: - version "5.0.37" - resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.37.tgz#27febe44d478ef4d35cd38007355da795ba075d5" - integrity sha512-EraWs84faI4iskB4qvE39bevMIazEvd1RpoyGLOBesRLbiz6eMeJqqRPHjEFClfRByYZzi9IzU35rBXIO76oDw== + version "5.0.38" + resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.38.tgz#1c05ce728ea1229d16db15b865631609bf0d6952" + integrity sha512-36QeGHSXYcJ/RfrnPEScR8GDprbXFG4ZhXsfVNVHztZr38+fRxgHnJl3CjYXXjbeRUhu3ZZBJh6Lg0A9v0Qd8A== dependencies: webgl-constants "^1.1.1" @@ -3394,13 +3172,6 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== -devlop@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" - integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== - dependencies: - dequal "^2.0.0" - discord-api-types@0.37.61: version "0.37.61" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.61.tgz#9dd8e58c624237e6f1b23be2d29579af268b8c5b" @@ -3427,14 +3198,14 @@ discord.js@^14.13.0: ws "8.14.2" dotenv@^16.3.1: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + version "16.4.4" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.4.tgz#a26e7bb95ebd36272ebb56edb80b826aecf224c1" + integrity sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg== draco3d@^1.4.1: - version "1.5.6" - resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.5.6.tgz#0d570a9792e3a3a9fafbfea065b692940441c626" - integrity sha512-+3NaRjWktb5r61ZFoDejlykPEFKT5N/LkbXsaddlw6xNSXBanUYpFc2AXXpbJDilPHazcSreU/DpQIaxfX0NfQ== + version "1.5.7" + resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.5.7.tgz#94f9bce293eb8920c159dc91a4ce9124a9e899e0" + integrity sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ== eastasianwidth@^0.2.0: version "0.2.0" @@ -3455,10 +3226,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.601: - version "1.4.611" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.611.tgz#92d3a8f03110fbf5f99b054da97825f994fa480a" - integrity sha512-ZtRpDxrjHapOwxtv+nuth5ByB8clyn8crVynmRNGO3wG3LOp8RTcyZDqwaI6Ng6y8FCK2hVZmJoqwCskKbNMaw== +electron-to-chromium@^1.4.668: + version "1.4.675" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.675.tgz#9e612a5339e3c9f884982478a8aaf2da3ab6ec7d" + integrity sha512-+1u3F/XPNIdUwv8i1lDxHAxCvNNU0QIqgb1Ycn+Jnng8ITzWSvUqixRSM7NOazJuwhf65IV17f/VbKj8DmL26A== emoji-regex@^10.2.1: version "10.3.0" @@ -3499,7 +3270,7 @@ end-of-stream@^1.0.0: dependencies: once "^1.4.0" -entities@^4.4.0: +entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -3516,94 +3287,51 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -esbuild@0.17.6: - version "0.17.6" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.6.tgz#bbccd4433629deb6e0a83860b3b61da120ba4e01" - integrity sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q== - optionalDependencies: - "@esbuild/android-arm" "0.17.6" - "@esbuild/android-arm64" "0.17.6" - "@esbuild/android-x64" "0.17.6" - "@esbuild/darwin-arm64" "0.17.6" - "@esbuild/darwin-x64" "0.17.6" - "@esbuild/freebsd-arm64" "0.17.6" - "@esbuild/freebsd-x64" "0.17.6" - "@esbuild/linux-arm" "0.17.6" - "@esbuild/linux-arm64" "0.17.6" - "@esbuild/linux-ia32" "0.17.6" - "@esbuild/linux-loong64" "0.17.6" - "@esbuild/linux-mips64el" "0.17.6" - "@esbuild/linux-ppc64" "0.17.6" - "@esbuild/linux-riscv64" "0.17.6" - "@esbuild/linux-s390x" "0.17.6" - "@esbuild/linux-x64" "0.17.6" - "@esbuild/netbsd-x64" "0.17.6" - "@esbuild/openbsd-x64" "0.17.6" - "@esbuild/sunos-x64" "0.17.6" - "@esbuild/win32-arm64" "0.17.6" - "@esbuild/win32-ia32" "0.17.6" - "@esbuild/win32-x64" "0.17.6" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" -esbuild@^0.18.10: - version "0.18.20" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" - integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== - optionalDependencies: - "@esbuild/android-arm" "0.18.20" - "@esbuild/android-arm64" "0.18.20" - "@esbuild/android-x64" "0.18.20" - "@esbuild/darwin-arm64" "0.18.20" - "@esbuild/darwin-x64" "0.18.20" - "@esbuild/freebsd-arm64" "0.18.20" - "@esbuild/freebsd-x64" "0.18.20" - "@esbuild/linux-arm" "0.18.20" - "@esbuild/linux-arm64" "0.18.20" - "@esbuild/linux-ia32" "0.18.20" - "@esbuild/linux-loong64" "0.18.20" - "@esbuild/linux-mips64el" "0.18.20" - "@esbuild/linux-ppc64" "0.18.20" - "@esbuild/linux-riscv64" "0.18.20" - "@esbuild/linux-s390x" "0.18.20" - "@esbuild/linux-x64" "0.18.20" - "@esbuild/netbsd-x64" "0.18.20" - "@esbuild/openbsd-x64" "0.18.20" - "@esbuild/sunos-x64" "0.18.20" - "@esbuild/win32-arm64" "0.18.20" - "@esbuild/win32-ia32" "0.18.20" - "@esbuild/win32-x64" "0.18.20" +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -esbuild@^0.19.3: - version "0.19.9" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.9.tgz#423a8f35153beb22c0b695da1cd1e6c0c8cdd490" - integrity sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg== +esbuild@^0.19.3, "esbuild@npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0": + version "0.19.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" + integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== optionalDependencies: - "@esbuild/android-arm" "0.19.9" - "@esbuild/android-arm64" "0.19.9" - "@esbuild/android-x64" "0.19.9" - "@esbuild/darwin-arm64" "0.19.9" - "@esbuild/darwin-x64" "0.19.9" - "@esbuild/freebsd-arm64" "0.19.9" - "@esbuild/freebsd-x64" "0.19.9" - "@esbuild/linux-arm" "0.19.9" - "@esbuild/linux-arm64" "0.19.9" - "@esbuild/linux-ia32" "0.19.9" - "@esbuild/linux-loong64" "0.19.9" - "@esbuild/linux-mips64el" "0.19.9" - "@esbuild/linux-ppc64" "0.19.9" - "@esbuild/linux-riscv64" "0.19.9" - "@esbuild/linux-s390x" "0.19.9" - "@esbuild/linux-x64" "0.19.9" - "@esbuild/netbsd-x64" "0.19.9" - "@esbuild/openbsd-x64" "0.19.9" - "@esbuild/sunos-x64" "0.19.9" - "@esbuild/win32-arm64" "0.19.9" - "@esbuild/win32-ia32" "0.19.9" - "@esbuild/win32-x64" "0.19.9" + "@esbuild/aix-ppc64" "0.19.12" + "@esbuild/android-arm" "0.19.12" + "@esbuild/android-arm64" "0.19.12" + "@esbuild/android-x64" "0.19.12" + "@esbuild/darwin-arm64" "0.19.12" + "@esbuild/darwin-x64" "0.19.12" + "@esbuild/freebsd-arm64" "0.19.12" + "@esbuild/freebsd-x64" "0.19.12" + "@esbuild/linux-arm" "0.19.12" + "@esbuild/linux-arm64" "0.19.12" + "@esbuild/linux-ia32" "0.19.12" + "@esbuild/linux-loong64" "0.19.12" + "@esbuild/linux-mips64el" "0.19.12" + "@esbuild/linux-ppc64" "0.19.12" + "@esbuild/linux-riscv64" "0.19.12" + "@esbuild/linux-s390x" "0.19.12" + "@esbuild/linux-x64" "0.19.12" + "@esbuild/netbsd-x64" "0.19.12" + "@esbuild/openbsd-x64" "0.19.12" + "@esbuild/sunos-x64" "0.19.12" + "@esbuild/win32-arm64" "0.19.12" + "@esbuild/win32-ia32" "0.19.12" + "@esbuild/win32-x64" "0.19.12" escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" @@ -3680,6 +3408,11 @@ fast-deep-equal@3.1.3, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== + fast-glob@3.3.2, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -3697,16 +3430,16 @@ fast-safe-stringify@^2.0.7: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fast-xml-parser@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz#761e641260706d6e13251c4ef8e3f5694d4b0d79" - integrity sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg== + version "4.3.4" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.4.tgz#385cc256ad7bbc57b91515a38a22502a9e1fca0d" + integrity sha512-utnwm92SyozgA3hhH2I8qldf2lBqm6qHOICawRNRFu1qMe3+oqr+GcXjGqTmXTMGE5T4eC03kr/rlh5C1IRdZA== dependencies: strnum "^1.0.5" fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -3849,11 +3582,12 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" - integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: + es-errors "^1.3.0" function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" @@ -3879,11 +3613,6 @@ glob-parent@^5.1.2: dependencies: is-glob "^4.0.1" -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - glob@^10.2.2: version "10.3.10" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" @@ -3913,11 +3642,11 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globby@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.0.tgz#ea9c062a3614e33f516804e778590fcf055256b9" - integrity sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ== + version "14.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.1.tgz#a1b44841aa7f4c6d8af2bc39951109d77301959b" + integrity sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ== dependencies: - "@sindresorhus/merge-streams" "^1.0.0" + "@sindresorhus/merge-streams" "^2.1.0" fast-glob "^3.3.2" ignore "^5.2.4" path-type "^5.0.0" @@ -3936,7 +3665,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.10, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11: +graceful-fs@^4.1.10, graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3956,17 +3685,17 @@ has-own-property@^0.1.0: resolved "https://registry.yarnpkg.com/has-own-property/-/has-own-property-0.1.0.tgz#992b0f5bb3a25416f8d4d0cde53f497b9d7b1ea5" integrity sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw== -has-property-descriptors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" - integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== +has-property-descriptors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.2.2" + es-define-property "^1.0.0" has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.3: version "1.0.3" @@ -3974,106 +3703,22 @@ has-symbols@^1.0.3: integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + version "2.0.1" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" + integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== dependencies: function-bind "^1.1.2" -hast-util-from-parse5@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" - integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - hastscript "^8.0.0" - property-information "^6.0.0" - vfile "^6.0.0" - vfile-location "^5.0.0" - web-namespaces "^2.0.0" - -hast-util-parse-selector@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" - integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== - dependencies: - "@types/hast" "^3.0.0" - -hast-util-raw@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.1.tgz#2ba8510e4ed2a1e541cde2a4ebb5c38ab4c82c2d" - integrity sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - "@ungap/structured-clone" "^1.0.0" - hast-util-from-parse5 "^8.0.0" - hast-util-to-parse5 "^8.0.0" - html-void-elements "^3.0.0" - mdast-util-to-hast "^13.0.0" - parse5 "^7.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-to-html@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz#51c0ae2a3550b9aa988c094c4fc4e327af0dddd1" - integrity sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - ccount "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-raw "^9.0.0" - hast-util-whitespace "^3.0.0" - html-void-elements "^3.0.0" - mdast-util-to-hast "^13.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - stringify-entities "^4.0.0" - zwitch "^2.0.4" - -hast-util-to-parse5@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" - integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-whitespace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" - integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== - dependencies: - "@types/hast" "^3.0.0" - -hastscript@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" - integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - hast-util-parse-selector "^4.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - hex-rgb@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776" integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== +hookable@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" + integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + hosted-git-info@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" @@ -4088,11 +3733,6 @@ hosted-git-info@^7.0.0: dependencies: lru-cache "^10.0.1" -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -4122,9 +3762,9 @@ ieee754@^1.1.13, ieee754@^1.2.1: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.1.8, ignore@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" - integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== immer@^10.0.2: version "10.0.3" @@ -4309,9 +3949,9 @@ json5@^2.1.2, json5@^2.2.3: integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-parser@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== jsonfile@^6.0.1: version "6.1.0" @@ -4323,18 +3963,17 @@ jsonfile@^6.0.1: graceful-fs "^4.1.6" knip@^3.4.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/knip/-/knip-3.7.1.tgz#2386b1f06e2d1d54b50f81fbc7cc116b783c693f" - integrity sha512-e/LyOHFE1p35DqRMI4Wz1vyLFrMP6uMlP3i2S/yaqUMt3+p6Mmn0USnOK0JNkuFUGIG9jcw63tZgBwMrY7j2IA== + version "3.13.2" + resolved "https://registry.yarnpkg.com/knip/-/knip-3.13.2.tgz#34f347a9b3ff1d45f3be7fed6bd3bb1ee5fbbaaa" + integrity sha512-izf5dvW+7fG0OfeZKyJTdhmrgQE1ltoxhPnNxYUKnPEBUMDEb61N2LD6SESKEpt4b6Mmbj4h9Tr4/14zcb7PSA== dependencies: "@ericcornelissen/bash-parser" "0.5.2" "@npmcli/map-workspaces" "3.0.4" "@npmcli/package-json" "5.0.0" "@pkgjs/parseargs" "0.11.0" "@pnpm/logger" "5.0.0" - "@pnpm/workspace.pkgs-graph" "^2.0.11" + "@pnpm/workspace.pkgs-graph" "^2.0.13" "@snyk/github-codeowners" "1.1.0" - chalk "^5.3.0" easy-table "1.2.0" fast-glob "3.3.2" globby "^14.0.0" @@ -4342,6 +3981,7 @@ knip@^3.4.0: js-yaml "4.1.0" micromatch "4.0.5" minimist "1.2.8" + picocolors "1.0.0" pretty-ms "8.0.0" strip-json-comments "5.0.1" summary "2.1.0" @@ -4387,11 +4027,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.clamp@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa" - integrity sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg== - lodash.curry@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" @@ -4402,16 +4037,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.omit@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" - integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg== - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== - lodash.snakecase@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" @@ -4429,10 +4054,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" - integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== +lru-cache@^10.0.1, lru-cache@^10.0.2, "lru-cache@^9.1.1 || ^10.0.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== lru-cache@^5.1.1: version "5.1.1" @@ -4458,15 +4083,20 @@ lz4@^0.6.5: nan "^2.13.2" xxhashjs "^0.2.2" -maath@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/maath/-/maath-0.9.0.tgz#425a9600dfc5d0aecfa48029a2e3eea852531b06" - integrity sha512-aAR8hoUqPxlsU8VOxkS9y37jhUzdUxM017NpCuxFU1Gk+nMaZASZxymZrV8LRSHzRk/watlbfyNKu6XPUhCFrQ== +lz4js@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/lz4js/-/lz4js-0.2.0.tgz#09f1a397cb2158f675146c3351dde85058cb322f" + integrity sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg== + +maath@^0.10.7: + version "0.10.7" + resolved "https://registry.yarnpkg.com/maath/-/maath-0.10.7.tgz#9289b42a5db8ac5b26407b3bfca4e3bebefe50b4" + integrity sha512-zQ2xd7dNOIVTjAS+hj22fyj1EFYmOJX6tzKjZ92r6WDoq8hyFxjuGA2q950tmR4iC/EKXoMQdSipkaJVuUHDTg== magic-bytes.js@^1.5.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.7.0.tgz#29ca1a137b508fa656ca35fe79683bf10246e0d5" - integrity sha512-YzVU2+/hrjwx8xcgAw+ffNq3jkactpj+f1iSL4LonrFKhvnwDzHSqtFdk/MMRP53y9ScouJ7cKEnqYsJwsHoYA== + version "1.8.0" + resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz#8362793c60cd77c2dd77db6420be727192df68e2" + integrity sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q== magic-string@^0.16.0: version "0.16.0" @@ -4475,10 +4105,10 @@ magic-string@^0.16.0: dependencies: vlq "^0.2.1" -magic-string@^0.30.5: - version "0.30.5" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" - integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== +magic-string@^0.30.6: + version "0.30.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" + integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -4511,20 +4141,6 @@ markdown-escape@^2.0.0: resolved "https://registry.yarnpkg.com/markdown-escape/-/markdown-escape-2.0.0.tgz#44484d30d7845b91575c77fd8898f23a0a6f890c" integrity sha512-Trz4v0+XWlwy68LJIyw3bLbsJiC8XAbRCKF9DbEtZjyndKOGVx6n+wNB0VfoRmY2LKboQLeniap3xrb6LGSJ8A== -mdast-util-to-hast@^13.0.0: - version "13.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz#74c0a9f014bb2340cae6118f6fccd75467792be7" - integrity sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@ungap/structured-clone" "^1.0.0" - devlop "^1.0.0" - micromark-util-sanitize-uri "^2.0.0" - trim-lines "^3.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - media-query-parser@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29" @@ -4564,9 +4180,9 @@ merge2@^1.3.0: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== meshline@^3.1.6: - version "3.1.7" - resolved "https://registry.yarnpkg.com/meshline/-/meshline-3.1.7.tgz#f60fef8c5b859740078a60c701b4c95bcc6f794d" - integrity sha512-uf9fPI9wy0Ie0kZjvKuIkf2n7gi3ih0wdTeb/kmSvmzpPyEL5d9lFohg9+JV9VC4sQUBOZDgxu6fnjn57goSHg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/meshline/-/meshline-3.2.0.tgz#0fcffd1fcae780b0e6bf0db991c8d7384154b075" + integrity sha512-ZaJkC967GTuef7UBdO0rGPX544oIWaNo7tYedVHSoR2lje6RR16fX8IsgMxgxoYYERtjqsRWIYBSPBxG4QR84Q== meshoptimizer@~0.18.1: version "0.18.1" @@ -4578,38 +4194,6 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromark-util-character@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.0.1.tgz#52b824c2e2633b6fb33399d2ec78ee2a90d6b298" - integrity sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw== - dependencies: - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" - integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== - -micromark-util-sanitize-uri@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" - integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-symbol@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" - integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== - -micromark-util-types@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" - integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== - micromatch@4.0.5, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -4669,26 +4253,26 @@ minisearch@^6.3.0: resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-6.3.0.tgz#985a2f1ca3c73c2d65af94f0616bfe57164b0b6b" integrity sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ== -mlly@^1.1.0, mlly@^1.2.0, mlly@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" - integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +mlly@^1.2.0, mlly@^1.4.0, mlly@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.5.0.tgz#8428a4617d54cc083d3009030ac79739a0e5447a" + integrity sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ== dependencies: - acorn "^8.10.0" - pathe "^1.1.1" + acorn "^8.11.3" + pathe "^1.1.2" pkg-types "^1.0.3" - ufo "^1.3.0" + ufo "^1.3.2" modern-ahocorasick@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz#dec373444f51b5458ac05216a8ec376e126dd283" integrity sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA== -mrmime@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4730,29 +4314,28 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -next@^14.0.3: - version "14.0.4" - resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" - integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== +next@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" + integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q== dependencies: - "@next/env" "14.0.4" + "@next/env" "14.1.0" "@swc/helpers" "0.5.2" busboy "1.6.0" - caniuse-lite "^1.0.30001406" + caniuse-lite "^1.0.30001579" graceful-fs "^4.2.11" postcss "8.4.31" styled-jsx "5.1.1" - watchpack "2.4.0" optionalDependencies: - "@next/swc-darwin-arm64" "14.0.4" - "@next/swc-darwin-x64" "14.0.4" - "@next/swc-linux-arm64-gnu" "14.0.4" - "@next/swc-linux-arm64-musl" "14.0.4" - "@next/swc-linux-x64-gnu" "14.0.4" - "@next/swc-linux-x64-musl" "14.0.4" - "@next/swc-win32-arm64-msvc" "14.0.4" - "@next/swc-win32-ia32-msvc" "14.0.4" - "@next/swc-win32-x64-msvc" "14.0.4" + "@next/swc-darwin-arm64" "14.1.0" + "@next/swc-darwin-x64" "14.1.0" + "@next/swc-linux-arm64-gnu" "14.1.0" + "@next/swc-linux-arm64-musl" "14.1.0" + "@next/swc-linux-x64-gnu" "14.1.0" + "@next/swc-linux-x64-musl" "14.1.0" + "@next/swc-win32-arm64-msvc" "14.1.0" + "@next/swc-win32-ia32-msvc" "14.1.0" + "@next/swc-win32-x64-msvc" "14.1.0" node-fetch@3.0.0-beta.9: version "3.0.0-beta.9" @@ -4819,7 +4402,7 @@ object-assign@^4.0.1, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.9.0: +object-inspect@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== @@ -4900,6 +4483,11 @@ parse-css-color@^0.2.1: color-name "^1.1.4" hex-rgb "^4.1.0" +parse-dds@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/parse-dds/-/parse-dds-1.2.1.tgz#b16533e00c4db91178b3460271361df4cb328164" + integrity sha512-oE88N8mzUi9d26tAaKVc0clxi6AjeTSWpLwgtcEVjXTYY1MW58w8FctmM9gGgvFd6YGu1/eFFYVeDVnlMsKBtA== + parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -4922,13 +4510,6 @@ parse-npm-tarball-url@^3.0.0: dependencies: semver "^6.1.0" -parse5@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -4974,17 +4555,22 @@ path-type@^5.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== -pathe@^1.1.0, pathe@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" - integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== +pathe@^1.1.0, pathe@^1.1.1, pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -picocolors@^1.0.0: +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@1.0.0, picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== @@ -5039,29 +4625,34 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.27, postcss@^8.4.32: - version "8.4.32" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9" - integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw== +postcss@^8.4.33, postcss@^8.4.35: + version "8.4.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" + integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== dependencies: nanoid "^3.3.7" picocolors "^1.0.0" source-map-js "^1.0.2" +postprocessing@^6.34.1: + version "6.34.3" + resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.34.3.tgz#3d51bace743ddfe12ab5befac0f63f8a66be9a7c" + integrity sha512-AjsxAGWeHMKlCTuWLeahNnPyFwR0c/pEmveq5mBz767lFBc1+HHYzk0aRGBCE9PZIjiA27oRL7lATd+fVOscRA== + potpack@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14" integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ== preact@^10.0.0: - version "10.19.3" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899" - integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ== + version "10.19.5" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.5.tgz#ed220be0d3273102b5c97dd0163468164064d9f1" + integrity sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ== prettier@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" - integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw== + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== pretty-ms@8.0.0: version "8.0.0" @@ -5071,11 +4662,11 @@ pretty-ms@8.0.0: parse-ms "^3.0.0" prisma@^5.4.1: - version "5.7.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.7.0.tgz#3c1c56d392b5d1137de954edefa4533fa092663e" - integrity sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q== + version "5.9.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.9.1.tgz#baa3dd635fbf71504980978f10f55ea11068f6aa" + integrity sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ== dependencies: - "@prisma/engines" "5.7.0" + "@prisma/engines" "5.9.1" proc-log@^3.0.0: version "3.0.0" @@ -5109,10 +4700,10 @@ prop-types@^15.6.0: object-assign "^4.1.1" react-is "^16.13.1" -property-information@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.0.tgz#6bc4c618b0c2d68b3bb8b552cbb97f8e300a0f82" - integrity sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ== +property-graph@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/property-graph/-/property-graph-1.3.1.tgz#25fb2c8040ba9b15f04cda90a443f3f844505293" + integrity sha512-gei3N/bHWJdCItJ4blnlGWd9iauEZI+JZYj/A0D177XSI01+QhiJGAVscYBhe3Yywow3A2QJzVtsO2P+UgrRRQ== proxy-addr@~2.0.7: version "2.0.7" @@ -5161,6 +4752,13 @@ react-composer@^5.0.3: dependencies: prop-types "^15.6.0" +react-device-detect@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.3.tgz#97a7ae767cdd004e7c3578260f48cf70c036e7ca" + integrity sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw== + dependencies: + ua-parser-js "^1.0.33" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -5187,6 +4785,13 @@ react-merge-refs@^1.1.0: resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06" integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ== +react-promise-suspense@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz#05d19a75703d71374674840056cfef2fcd38809d" + integrity sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ== + dependencies: + fast-deep-equal "^2.0.1" + react-reconciler@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.27.0.tgz#360124fdf2d76447c7491ee5f0e04503ed9acf5b" @@ -5196,9 +4801,9 @@ react-reconciler@^0.27.0: scheduler "^0.21.0" react-remove-scroll-bar@^2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" - integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== + version "2.3.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c" + integrity sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw== dependencies: react-style-singleton "^2.2.1" tslib "^2.0.0" @@ -5214,6 +4819,11 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-spinners@^0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc" + integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA== + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -5268,14 +4878,14 @@ readable-stream@^2.3.0, readable-stream@^2.3.5: util-deprecate "~1.0.1" regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -rename-overwrite@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/rename-overwrite/-/rename-overwrite-4.0.4.tgz#254f0fa91610fa467c233482109ae1dc2f365736" - integrity sha512-5MC+p5npnyaJlFkwTHb0pqU2mkUkkW65ZWH8qwxcDlv+5nchtalcdzG+gaaianbWWwvwxi7vu7WSg6jdCweKug== +rename-overwrite@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rename-overwrite/-/rename-overwrite-5.0.0.tgz#ccd1f85aef47d41647ccc1c681d9b6d054cf5fc8" + integrity sha512-vSxE5Ww7Jnyotvaxi3Dj0vOMoojH8KMkBfs9xYeW/qNfJiLTcC1fmwTjrbGUq3mQSOCxkG0DbdcvwTUrpvBN4w== dependencies: "@zkochan/rimraf" "^2.1.2" fs-extra "10.1.0" @@ -5305,6 +4915,11 @@ reverse-arguments@^1.0.0: resolved "https://registry.yarnpkg.com/reverse-arguments/-/reverse-arguments-1.0.0.tgz#c28095a3a921ac715d61834ddece9027992667cd" integrity sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ== +rfdc@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -5312,31 +4927,26 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@^3.27.1: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== - optionalDependencies: - fsevents "~2.3.2" - rollup@^4.2.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.8.0.tgz#365c34e85f1ed034de974dab934c1663cc69b754" - integrity sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA== + version "4.12.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5" + integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q== + dependencies: + "@types/estree" "1.0.5" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.8.0" - "@rollup/rollup-android-arm64" "4.8.0" - "@rollup/rollup-darwin-arm64" "4.8.0" - "@rollup/rollup-darwin-x64" "4.8.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.8.0" - "@rollup/rollup-linux-arm64-gnu" "4.8.0" - "@rollup/rollup-linux-arm64-musl" "4.8.0" - "@rollup/rollup-linux-riscv64-gnu" "4.8.0" - "@rollup/rollup-linux-x64-gnu" "4.8.0" - "@rollup/rollup-linux-x64-musl" "4.8.0" - "@rollup/rollup-win32-arm64-msvc" "4.8.0" - "@rollup/rollup-win32-ia32-msvc" "4.8.0" - "@rollup/rollup-win32-x64-msvc" "4.8.0" + "@rollup/rollup-android-arm-eabi" "4.12.0" + "@rollup/rollup-android-arm64" "4.12.0" + "@rollup/rollup-darwin-arm64" "4.12.0" + "@rollup/rollup-darwin-x64" "4.12.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.12.0" + "@rollup/rollup-linux-arm64-gnu" "4.12.0" + "@rollup/rollup-linux-arm64-musl" "4.12.0" + "@rollup/rollup-linux-riscv64-gnu" "4.12.0" + "@rollup/rollup-linux-x64-gnu" "4.12.0" + "@rollup/rollup-linux-x64-musl" "4.12.0" + "@rollup/rollup-win32-arm64-msvc" "4.12.0" + "@rollup/rollup-win32-ia32-msvc" "4.12.0" + "@rollup/rollup-win32-x64-msvc" "4.12.0" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -5362,9 +4972,9 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== satori@^0.10.8: - version "0.10.11" - resolved "https://registry.yarnpkg.com/satori/-/satori-0.10.11.tgz#4d198beef405668120566d29a554411c534d8a5d" - integrity sha512-yLm1xPRPZUaKcBZJ6nmezoJjHB4MqV8x7Mu0PyZUJodRWRDD27UbeMwzuY9LEGG57WYLO4CQsGPlbHWV1Ex9TQ== + version "0.10.13" + resolved "https://registry.yarnpkg.com/satori/-/satori-0.10.13.tgz#658a9920f55268d2002819387a80a0b6d4bdc262" + integrity sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ== dependencies: "@shuding/opentype.js" "1.4.0-beta.0" css-background-parser "^0.1.0" @@ -5404,9 +5014,9 @@ semver@^6.1.0, semver@^6.3.1: integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.5, semver@^7.4.0, semver@^7.5.3, semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" @@ -5439,32 +5049,39 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" -set-function-length@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" - integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== +server-only@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" + integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== + +set-function-length@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" + integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== dependencies: - define-data-property "^1.1.1" - get-intrinsic "^1.2.1" + define-data-property "^1.1.2" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.3" gopd "^1.0.1" - has-property-descriptors "^1.0.0" + has-property-descriptors "^1.0.1" setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sharp@^0.33.0: - version "0.33.0" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.0.tgz#95f77cda3684522e210ab55a167dad6cb5fbfe4f" - integrity sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q== +sharp@0.33.1: + version "0.33.1" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.1.tgz#81e8778b9f5e2b195666cf56b2e5a110c2399d70" + integrity sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ== dependencies: color "^4.2.3" detect-libc "^2.0.2" semver "^7.5.4" optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.0" - "@img/sharp-darwin-x64" "0.33.0" + "@img/sharp-darwin-arm64" "0.33.1" + "@img/sharp-darwin-x64" "0.33.1" "@img/sharp-libvips-darwin-arm64" "1.0.0" "@img/sharp-libvips-darwin-x64" "1.0.0" "@img/sharp-libvips-linux-arm" "1.0.0" @@ -5473,15 +5090,15 @@ sharp@^0.33.0: "@img/sharp-libvips-linux-x64" "1.0.0" "@img/sharp-libvips-linuxmusl-arm64" "1.0.0" "@img/sharp-libvips-linuxmusl-x64" "1.0.0" - "@img/sharp-linux-arm" "0.33.0" - "@img/sharp-linux-arm64" "0.33.0" - "@img/sharp-linux-s390x" "0.33.0" - "@img/sharp-linux-x64" "0.33.0" - "@img/sharp-linuxmusl-arm64" "0.33.0" - "@img/sharp-linuxmusl-x64" "0.33.0" - "@img/sharp-wasm32" "0.33.0" - "@img/sharp-win32-ia32" "0.33.0" - "@img/sharp-win32-x64" "0.33.0" + "@img/sharp-linux-arm" "0.33.1" + "@img/sharp-linux-arm64" "0.33.1" + "@img/sharp-linux-s390x" "0.33.1" + "@img/sharp-linux-x64" "0.33.1" + "@img/sharp-linuxmusl-arm64" "0.33.1" + "@img/sharp-linuxmusl-x64" "0.33.1" + "@img/sharp-wasm32" "0.33.1" + "@img/sharp-win32-ia32" "0.33.1" + "@img/sharp-win32-x64" "0.33.1" shebang-command@^2.0.0: version "2.0.0" @@ -5500,28 +5117,22 @@ shell-quote-word@^1.0.1: resolved "https://registry.yarnpkg.com/shell-quote-word/-/shell-quote-word-1.0.1.tgz#e2bdfd22d599fd68886491677e38f560f9d469c9" integrity sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg== -shikiji-transformers@^0.7.4: - version "0.7.6" - resolved "https://registry.yarnpkg.com/shikiji-transformers/-/shikiji-transformers-0.7.6.tgz#56e9317f87aabd8f18f8899d94121532d720b69f" - integrity sha512-yTp+7JMD/aXbV9ndn14eo9IK/UNt8iDsLNyqlOmCtcldlkqWE9T2YKAlOHOTVaeDfYWUWZa2EgSXb/CBfepBrw== +shiki@1.1.5, shiki@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.1.5.tgz#9d48cd8f835706dd38ccb879c87351271583d6ba" + integrity sha512-754GuKIwkUdT810Xm8btuyNQPL+q3PqOkwGW/VlmAWyMYp+HbvvDt69sWXO1sm5aeczBJQjmQTTMR4GkKQNQPw== dependencies: - shikiji "0.7.6" - -shikiji@0.7.6, shikiji@^0.7.4: - version "0.7.6" - resolved "https://registry.yarnpkg.com/shikiji/-/shikiji-0.7.6.tgz#ae65517af8dfbb4f861288cd888e8ecaf9aceec9" - integrity sha512-KzEtvSGQtBvfwVIB70kOmIfl/5rz1LC8j+tvlHXsJKAIdONNQvG1at7ivUUq3xUctqgO6fsO3AGomUSh0F+wsQ== - dependencies: - hast-util-to-html "^9.0.0" + "@shikijs/core" "1.1.5" side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + version "1.0.5" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.5.tgz#9a84546599b48909fb6af1211708d23b1946221b" + integrity sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.6" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^4.0.1: version "4.1.0" @@ -5545,24 +5156,6 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-support@^0.5.21: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -5572,9 +5165,9 @@ spdx-correct@^3.0.0: spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -5585,9 +5178,14 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.16" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" - integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== + version "3.0.17" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" + integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== + +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== split2@^3.0.0: version "3.2.2" @@ -5624,6 +5222,7 @@ streamsearch@^1.1.0: integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5665,15 +5264,8 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -stringify-entities@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" - integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5758,15 +5350,15 @@ tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -three-mesh-bvh@^0.6.7: - version "0.6.8" - resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.6.8.tgz#f27d18ca75bdc59316dff0f561af8fb316621a54" - integrity sha512-EGebF9DZx1S8+7OZYNNTT80GXJZVf+UYXD/HyTg/e2kR/ApofIFfUS4ZzIHNnUVIadpnLSzM4n96wX+l7GMbnQ== +three-mesh-bvh@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.7.2.tgz#165a7b36c6640edb7af2639081bb6adbc1318188" + integrity sha512-HKRGZDKIUwwjL2mApYIAN8AYA5WAfFvc50PY8TL/9aGWzrRN1WLfPKAvTjv35F8PdPZzTmzHotCvIMFEN9FUSw== -three-stdlib@^2.28.0: - version "2.28.7" - resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.28.7.tgz#677ad2ce4d57375f7582e5fd62eadfd07ef6059a" - integrity sha512-E7NuztilCswBKnEoyqydvA7N4dy0cf/gLA0bKrrg6+Q6j4WtusGa/+t9oK2HVq47S1AHRH2CvFHpdIGNjPKo/A== +three-stdlib@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.29.4.tgz#6e8741f6a2d435d15ed73f3a14dd149660d4ce51" + integrity sha512-XNzGCrz/uAk9XoLwd35eN7dQyI4ggXZTeqjcN034YdYBpBlNO9kmLHehl/0Nw9jCelblB7jla+unHAOIyLyV6Q== dependencies: "@types/draco3d" "^1.4.0" "@types/offscreencanvas" "^2019.6.4" @@ -5775,7 +5367,7 @@ three-stdlib@^2.28.0: fflate "^0.6.9" potpack "^1.0.1" -three@^0.159.0: +three@0.159.0: version "0.159.0" resolved "https://registry.yarnpkg.com/three/-/three-0.159.0.tgz#6576b1210805b14f0765bac41fd0e4ec18e43b2e" integrity sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA== @@ -5838,30 +5430,25 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - -troika-three-text@^0.47.2: - version "0.47.2" - resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.47.2.tgz#fdf89059c010563bb829262b20c41f69ca79b712" - integrity sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng== +troika-three-text@^0.49.0: + version "0.49.0" + resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.49.0.tgz#bc2dc1924250c477cd39316cd83585dee12550e0" + integrity sha512-sn9BNC6eIX8OO3iAkPwjecJ7Pn21Ve8P1UNFMNeQzXx759rrqS4i4pSZs7FLMYdWyCKVXBFGimBySFwRKLjq/Q== dependencies: bidi-js "^1.0.2" - troika-three-utils "^0.47.2" - troika-worker-utils "^0.47.2" + troika-three-utils "^0.49.0" + troika-worker-utils "^0.49.0" webgl-sdf-generator "1.1.1" -troika-three-utils@^0.47.2: - version "0.47.2" - resolved "https://registry.yarnpkg.com/troika-three-utils/-/troika-three-utils-0.47.2.tgz#af49ca694245dce631963d5fefe4e8e1b8af9044" - integrity sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg== +troika-three-utils@^0.49.0: + version "0.49.0" + resolved "https://registry.yarnpkg.com/troika-three-utils/-/troika-three-utils-0.49.0.tgz#3fbdbf8783740ce3c1f7acac642e7e957ea4f090" + integrity sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA== -troika-worker-utils@^0.47.2: - version "0.47.2" - resolved "https://registry.yarnpkg.com/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz#e7c5de5f37d56c072b13fa8112bb844e048ff46c" - integrity sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA== +troika-worker-utils@^0.49.0: + version "0.49.0" + resolved "https://registry.yarnpkg.com/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz#e5e200a09d2e0e4eb9fe818a83effa912e2ec4b4" + integrity sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg== ts-mixer@^6.0.3: version "6.0.3" @@ -5873,6 +5460,13 @@ tslib@2.6.2, tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tunnel-rat@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.2.tgz#1717efbc474ea2d8aa05a91622457a6e201c0aeb" + integrity sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ== + dependencies: + zustand "^4.3.2" + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -5891,10 +5485,15 @@ typescript@^5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -ufo@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.2.tgz#c7d719d0628a1c80c006d2240e0d169f6e3c0496" - integrity sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA== +ua-parser-js@^1.0.33: + version "1.0.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" + integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== + +ufo@^1.3.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32" + integrity sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ== unbzip2-stream@^1.0.9: version "1.4.3" @@ -5943,44 +5542,6 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" - integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-stringify-position@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" - integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - -unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - universal-user-agent@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" @@ -6005,9 +5566,9 @@ update-browserslist-db@^1.0.13: picocolors "^1.0.0" use-callback-ref@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" - integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + version "1.3.1" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0" + integrity sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ== dependencies: tslib "^2.0.0" @@ -6030,9 +5591,9 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== utility-types@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" - integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + version "3.11.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" + integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== utils-merge@1.0.1: version "1.0.1" @@ -6078,45 +5639,6 @@ version-selector-type@^3.0.0: dependencies: semver "^7.3.2" -vfile-location@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" - integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== - dependencies: - "@types/unist" "^3.0.0" - vfile "^6.0.0" - -vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - -vfile@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" - integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - vfile-message "^4.0.0" - -vite-node@^0.28.5: - version "0.28.5" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.28.5.tgz#56d0f78846ea40fddf2e28390899df52a4738006" - integrity sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA== - dependencies: - cac "^6.7.14" - debug "^4.3.4" - mlly "^1.1.0" - pathe "^1.1.0" - picocolors "^1.0.0" - source-map "^0.6.1" - source-map-support "^0.5.21" - vite "^3.0.0 || ^4.0.0" - vite-node@^0.34.5: version "0.34.7" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.7.tgz#9fbcaf5597826d224e6301a486027faa22c2b09c" @@ -6129,24 +5651,24 @@ vite-node@^0.34.5: picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" -"vite@^3.0.0 || ^4.0.0", vite@^4.1.4: - version "4.5.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a" - integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA== +vite-node@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.3.0.tgz#618cc26d83545cfbd4e2c3014678257496d2bd00" + integrity sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA== dependencies: - esbuild "^0.18.10" - postcss "^8.4.27" - rollup "^3.27.1" - optionalDependencies: - fsevents "~2.3.2" + cac "^6.7.14" + debug "^4.3.4" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^5.0.0" -"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", vite@^5.0.2: - version "5.0.8" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.8.tgz#070383dcb4d4f12adde9c119b8cca0f40bf906e9" - integrity sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ== +"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", vite@^5.0.0, vite@^5.0.11, vite@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.3.tgz#dd072653a80225702265550a4700561740dfde55" + integrity sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew== dependencies: esbuild "^0.19.3" - postcss "^8.4.32" + postcss "^8.4.35" rollup "^4.2.0" optionalDependencies: fsevents "~2.3.3" @@ -6157,25 +5679,25 @@ vitepress-plugin-auto-sidebar@^1.1.0: integrity sha512-LgkpjKVlNlQS54PFv5R/Y4+CHtHoXsJ3xgyVTKNi0sG1fXUmViKCg3Hnl9eo/SfNn894qsNRXoNneQmRBP2Lmw== vitepress@^1.0.0-rc.20: - version "1.0.0-rc.31" - resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-rc.31.tgz#8defda3d5705a219e5bf3565efe57c7d52315b01" - integrity sha512-ikH9pIjOOAbyoYAGBVfTz8TzuXp+UoWaIRMU4bw/oiTg8R65SbAaGKY84xx6TuL+f4VqUJ8lhzW82YyxSLvstA== + version "1.0.0-rc.44" + resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-rc.44.tgz#01bce883761c22de42b9869a95f04bd02cbb8cdb" + integrity sha512-tO5taxGI7fSpBK1D8zrZTyJJERlyU9nnt0jHSt3fywfq3VKn977Hg0wUuTkEmwXlFYwuW26+6+3xorf4nD3XvA== dependencies: "@docsearch/css" "^3.5.2" "@docsearch/js" "^3.5.2" + "@shikijs/core" "^1.1.5" + "@shikijs/transformers" "^1.1.5" "@types/markdown-it" "^13.0.7" - "@vitejs/plugin-vue" "^4.5.0" - "@vue/devtools-api" "^6.5.1" - "@vueuse/core" "^10.6.1" - "@vueuse/integrations" "^10.6.1" + "@vitejs/plugin-vue" "^5.0.4" + "@vue/devtools-api" "^7.0.14" + "@vueuse/core" "^10.7.2" + "@vueuse/integrations" "^10.7.2" focus-trap "^7.5.4" mark.js "8.11.1" minisearch "^6.3.0" - mrmime "^1.0.1" - shikiji "^0.7.4" - shikiji-transformers "^0.7.4" - vite "^5.0.2" - vue "^3.3.8" + shiki "^1.1.5" + vite "^5.1.3" + vue "^3.4.19" vlq@^0.2.1: version "0.2.3" @@ -6183,28 +5705,20 @@ vlq@^0.2.1: integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== vue-demi@>=0.14.6: - version "0.14.6" - resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92" - integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w== + version "0.14.7" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2" + integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA== -vue@^3.3.8: - version "3.3.11" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.11.tgz#898d97025f73cdb5fc4e3ae3fd07a54615232140" - integrity sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w== +vue@^3.4.19: + version "3.4.19" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.19.tgz#f9ae0a44db86628548736ff04152830726a97263" + integrity sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw== dependencies: - "@vue/compiler-dom" "3.3.11" - "@vue/compiler-sfc" "3.3.11" - "@vue/runtime-dom" "3.3.11" - "@vue/server-renderer" "3.3.11" - "@vue/shared" "3.3.11" - -watchpack@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" + "@vue/compiler-dom" "3.4.19" + "@vue/compiler-sfc" "3.4.19" + "@vue/runtime-dom" "3.4.19" + "@vue/server-renderer" "3.4.19" + "@vue/shared" "3.4.19" wcwidth@^1.0.1: version "1.0.1" @@ -6213,11 +5727,6 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web-namespaces@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" - integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== - webgl-constants@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855" @@ -6271,9 +5780,9 @@ ws@8.14.2: integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== ws@^8.14.2: - version "8.15.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.15.1.tgz#271ba33a45ca0cc477940f7f200cd7fba7ee1997" - integrity sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== xtend@^4.0.0: version "4.0.2" @@ -6330,19 +5839,14 @@ zod@3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== -zustand@^3.5.13, zustand@^3.7.1: +zustand@^3.7.1: version "3.7.2" resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA== -zustand@^4.4.1: - version "4.4.7" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c" - integrity sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw== +zustand@^4.3.2, zustand@^4.4.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.1.tgz#2088956ee454759fb8b866ca335a2373e76736c5" + integrity sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg== dependencies: use-sync-external-store "1.2.0" - -zwitch@^2.0.0, zwitch@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==