mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: crew basics
This commit is contained in:
1 parent
b1e5502b04
commit
2c8c04387b
11 files changed
+183
-42
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Flex, Heading, Slider, TextField } from '@radix-ui/themes';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
|
||||
export function Crew() {
|
||||
return (
|
||||
<ConfigurationChildWrapper>
|
||||
<Heading size="4">Crew</Heading>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
gap="4"
|
||||
justify="end"
|
||||
style={{
|
||||
width: 240,
|
||||
}}
|
||||
>
|
||||
<Slider min={50} max={100} defaultValue={[100]} style={{ flex: 1 }} />
|
||||
<TextField.Root style={{ width: 64 }}>
|
||||
<TextField.Input defaultValue={100} />
|
||||
<TextField.Slot>%</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
</ConfigurationChildWrapper>
|
||||
);
|
||||
}
|
||||
+69
-2
@@ -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<HTMLInputElement>(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 (
|
||||
<ConfigurationChildWrapper>
|
||||
<Heading size="4">Miscellaneous</Heading>
|
||||
|
||||
<Flex gap="2">
|
||||
<Flex gap="2" align="center">
|
||||
<Flex
|
||||
align="center"
|
||||
direction="column"
|
||||
gap="1"
|
||||
justify="end"
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>Crew</TextField.Slot>
|
||||
<TextField.Input
|
||||
ref={crewInput}
|
||||
defaultValue={Math.round(crew * 100)}
|
||||
style={{ textAlign: 'right' }}
|
||||
onBlur={() => {
|
||||
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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField.Slot>%</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<Slider
|
||||
min={50}
|
||||
max={100}
|
||||
value={[crewDraft * 100]}
|
||||
style={{ width: '100%' }}
|
||||
onValueChange={([value]) => {
|
||||
setCrewDraft(value / 100);
|
||||
debouncedApplyDraft(value / 100);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<GenericTankComponentButton
|
||||
icon={asset('icons/camo.webp')}
|
||||
selected={camouflage}
|
||||
|
||||
@@ -7,7 +7,11 @@ import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNear
|
||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox';
|
||||
import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm';
|
||||
import { GUN_TYPE_NAMES } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||
import {
|
||||
CREW_MEMBER_NAMES,
|
||||
CREW_MEMBER_NAMES_PLURAL,
|
||||
GUN_TYPE_NAMES,
|
||||
} from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
|
||||
import { useConsumable } from '../../../../../../core/blitzkrieg/useConsumable';
|
||||
import { useEquipment } from '../../../../../../core/blitzkrieg/useEquipment';
|
||||
@@ -21,6 +25,7 @@ export function Characteristics() {
|
||||
const { tank, turret, gun, engine, track, shell } = useDuel(
|
||||
(state) => 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)}
|
||||
</Info>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">Crew</Heading>
|
||||
|
||||
{tank.crew.map((member) => {
|
||||
const count = member.count ?? 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoWithDelta
|
||||
name={`${count > 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}
|
||||
</InfoWithDelta>
|
||||
{member.substitute &&
|
||||
member.substitute.map((substitute) => (
|
||||
<Info indent name={`Substitutes ${substitute}`} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -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' }}
|
||||
>
|
||||
<Button
|
||||
variant="soft"
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
@@ -215,21 +217,21 @@ export function TankSandbox() {
|
||||
<LightningBoltIcon /> Load model
|
||||
</Button>
|
||||
|
||||
<div
|
||||
<Flex
|
||||
style={{
|
||||
filter: 'blur(16px)',
|
||||
width: 256,
|
||||
height: 128,
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transformOrigin: 'center center',
|
||||
transform: 'translate(25%, -25%) scale(300%)',
|
||||
}}
|
||||
>
|
||||
<img src={tankIcon(duel.antagonist!.tank.id, 'big')} />
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 } }) {
|
||||
<Modules />
|
||||
<Equipments />
|
||||
<Consumables />
|
||||
<Crew />
|
||||
<Provisions />
|
||||
<Miscellaneous />
|
||||
</Flex>
|
||||
|
||||
@@ -20,9 +20,28 @@ export type CrewMember =
|
||||
| '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 const CREW_MEMBER_NAMES_PLURAL: Record<CrewMember, string> = {
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function useWideFormat() {
|
||||
const wideFormat = typeof window !== 'undefined' && window.innerWidth > 880;
|
||||
return wideFormat;
|
||||
}
|
||||
@@ -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<TankopediaTemporary>()(
|
||||
[0, 0, 0],
|
||||
[0, 0, 0],
|
||||
],
|
||||
crew: 0.75,
|
||||
mode: 'model',
|
||||
consumables: [],
|
||||
provisions: [],
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user