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' }}
>
Load model
-

-
+
)}
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"