diff --git a/package.json b/package.json index a8cb9efcf..7ee6b09db 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "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", diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 5ad93e806..4c6ab6e3b 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -23,10 +23,10 @@ import { import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions'; import { superCompress } from '../../src/core/blitzkrieg/superCompress'; import { + Crew, CrewMember, GunDefinition, ShellType, - TankDefinition, TankDefinitionPrice, TankDefinitions, Tier, @@ -439,15 +439,40 @@ export async function definitions(production: boolean) { hullArmor.spaced.push(armorId); } }); - const crew: TankDefinition['crew'] = {}; + const crew: Crew[] = []; Object.entries(tankDefinition.root.crew).forEach(([key, value]) => { - if (!(key in crew)) crew[key as CrewMember] = 0; + 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') { - crew[key as CrewMember]!++; + if (entry.count === undefined) { + entry.count = 1; + } else { + entry.count++; + } + + if (value !== '') { + entry.substitute = value + .split('\n') + .map((member) => member.trim()) as CrewMember[]; + } } else { - crew[key as CrewMember]! += value.length; + if (entry.count === undefined) { + entry.count = value.length; + } else { + entry.count += value.length; + } + } + + if (entry.count === 1) { + delete entry.count; } }); diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Crew.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Crew.tsx deleted file mode 100644 index de4430f99..000000000 --- a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Crew.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Flex, Heading, Slider, TextField } from '@radix-ui/themes'; -import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; - -export function Crew() { - return ( - - Crew - - - - - - % - - - - ); -} diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx index 8b1d92c81..eb7e5f5b9 100644 --- a/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/components/Miscellaneous.tsx @@ -1,4 +1,6 @@ -import { Flex, Heading } from '@radix-ui/themes'; +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 { @@ -12,12 +14,77 @@ export function Miscellaneous() { const cooldownBooster = useTankopediaTemporary( (state) => state.cooldownBooster, ); + const crew = useTankopediaTemporary((state) => state.crew); + const crewInput = useRef(null); + const [crewDraft, setCrewDraft] = useState(crew); + + const debouncedApplyDraft = useMemo( + () => + debounce((value) => { + mutateTankopediaTemporary((draft) => { + draft.crew = value; + }); + }, 500), + [], + ); + + useEffect(() => { + setCrewDraft(crew); + if (!crewInput.current) return; + crewInput.current.value = `${Math.round(crew * 100)}`; + }, [crew]); return ( Miscellaneous - + + + + Crew + { + const newValue = Number(crewInput.current!.value) / 100; + + if (isNaN(newValue)) { + crewInput.current!.value = `${Math.round(crew * 100)}`; + return; + } + + mutateTankopediaTemporary((draft) => { + draft.crew = clamp(newValue / 100, 0.5, 1); + }); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + crewInput.current?.blur(); + } + }} + /> + % + + + { + setCrewDraft(value / 100); + debouncedApplyDraft(value / 100); + }} + /> + + state.protagonist!, ); + const crew = useTankopediaTemporary((state) => state.crew); const stockEngine = tank.engines[0]; const stockTrack = tank.tracks[0]; const stockTurret = tank.turrets[0]; @@ -497,6 +502,34 @@ export function Characteristics() { {size[0].toFixed(2)} x {size[2].toFixed(2)} x {size[1].toFixed(2)} + + + Crew + + {tank.crew.map((member) => { + const count = member.count ?? 1; + + return ( + <> + 1 ? `${member.count} ` : ''}${ + (count > 1 ? CREW_MEMBER_NAMES_PLURAL : CREW_MEMBER_NAMES)[ + member.type + ] + }`} + unit="%" + decimals={0} + > + {crew * (member.type === 'commander' ? 1 : 1.1) * 100} + + {member.substitute && + member.substitute.map((substitute) => ( + + ))} + + ); + })} + ); } diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index c8a231f33..04341c9a6 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -3,6 +3,7 @@ import { Badge, 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'; @@ -10,6 +11,7 @@ import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTran import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose'; import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon'; import { useModel } from '../../../../../../hooks/useModel'; +import { useWideFormat } from '../../../../../../hooks/useWideFormat'; import { useDuel } from '../../../../../../stores/duel'; import { TankopediaMode, @@ -38,7 +40,8 @@ export function TankSandbox() { const turretModelDefinition = tankModelDefinition.turrets[protagonist.turret.id]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const [loadModel, setLoadModel] = useState(true); // set to false someday? + const wideFormat = useWideFormat(); + const [loadModel, setLoadModel] = useState(wideFormat || !isMobile); const duel = useDuel(); const { hasPbr } = useModel(protagonist.tank.id); @@ -204,7 +207,6 @@ export function TankSandbox() { style={{ height: '100%', position: 'relative' }} > -
-
+ )} diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index 766a3ff00..75b836469 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -4,12 +4,12 @@ import { Flex } from '@radix-ui/themes'; import { use, useEffect } from 'react'; import PageWrapper from '../../../../components/PageWrapper'; import { tankDefinitions } from '../../../../core/blitzkrieg/tankDefinitions'; +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 { Crew } from './components/Characteristics/components/Crew'; import { Equipments } from './components/Characteristics/components/Equipments'; import { Miscellaneous } from './components/Characteristics/components/Miscellaneous'; import { Modules } from './components/Characteristics/components/Modules'; @@ -21,7 +21,7 @@ export default function Page({ params }: { params: { id: string } }) { const id = parseInt(params.id); const awaitedTankDefinitions = use(tankDefinitions); const assigned = useDuel((state) => state.assigned); - const wideFormat = typeof window !== 'undefined' && window.innerWidth > 880; + const wideFormat = useWideFormat(); useEffect(() => { mutateDuel((draft) => { @@ -121,7 +121,6 @@ export default function Page({ params }: { params: { id: string } }) { - diff --git a/src/core/blitzkrieg/tankDefinitions.ts b/src/core/blitzkrieg/tankDefinitions.ts index 155b503f8..1b63cbbd8 100644 --- a/src/core/blitzkrieg/tankDefinitions.ts +++ b/src/core/blitzkrieg/tankDefinitions.ts @@ -20,9 +20,28 @@ export type CrewMember = | 'driver' | 'loader'; export type TankDefinitions = Record; +export const CREW_MEMBER_NAMES: Record = { + commander: 'Commander', + radioman: 'Radioman', + gunner: 'Gunner', + driver: 'Driver', + loader: 'Loader', +}; +export const CREW_MEMBER_NAMES_PLURAL: Record = { + commander: 'Commanders', + radioman: 'Radiomen', + gunner: 'Gunners', + driver: 'Drivers', + loader: 'Loaders', +}; +export interface Crew { + type: CrewMember; + count?: number; + substitute?: CrewMember[]; +} export interface TankDefinition { id: number; - crew: { [crew in CrewMember]?: number }; + crew: Crew[]; health: number; nation: string; name: string; diff --git a/src/hooks/useWideFormat.ts b/src/hooks/useWideFormat.ts new file mode 100644 index 000000000..b18a94f48 --- /dev/null +++ b/src/hooks/useWideFormat.ts @@ -0,0 +1,4 @@ +export function useWideFormat() { + const wideFormat = typeof window !== 'undefined' && window.innerWidth > 880; + return wideFormat; +} diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 3695d7918..8d5312dea 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -67,6 +67,7 @@ interface TankopediaTemporary { equipmentMatrix: EquipmentMatrix; mode: TankopediaMode; consumables: number[]; + crew: number; provisions: number[]; camouflage: boolean; cooldownBooster: number; @@ -175,6 +176,7 @@ export const useTankopediaTemporary = create()( [0, 0, 0], [0, 0, 0], ], + crew: 0.75, mode: 'model', consumables: [], provisions: [], diff --git a/yarn.lock b/yarn.lock index bcb4939b8..09b485c8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4744,6 +4744,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" @@ -5202,6 +5209,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== @@ -5244,6 +5252,7 @@ string_decoder@~1.1.1: safe-buffer "~5.1.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== @@ -5463,6 +5472,11 @@ typescript@^5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +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"