mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: optimized state handling
This commit is contained in:
1 parent
033d361cea
commit
230b2df5fd
6 files changed
+124
-165
No files matched your search
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { Card, Flex, Tabs, Theme } from '@radix-ui/themes';
|
||||||
EnterFullScreenIcon,
|
|
||||||
ExitFullScreenIcon,
|
|
||||||
GearIcon,
|
|
||||||
} from '@radix-ui/react-icons';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
DropdownMenu,
|
|
||||||
Flex,
|
|
||||||
Tabs,
|
|
||||||
Theme,
|
|
||||||
} from '@radix-ui/themes';
|
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from '@react-three/drei';
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
||||||
@@ -26,6 +14,7 @@ import { Controls } from '../Control';
|
|||||||
import { Lighting } from '../Lighting';
|
import { Lighting } from '../Lighting';
|
||||||
import { RotationInputs } from '../RotationInputs';
|
import { RotationInputs } from '../RotationInputs';
|
||||||
import { SceneProps } from '../SceneProps';
|
import { SceneProps } from '../SceneProps';
|
||||||
|
import { Options } from './components/Options';
|
||||||
import { TankArmor } from './components/TankArmor';
|
import { TankArmor } from './components/TankArmor';
|
||||||
import { TankModel } from './components/TankModel';
|
import { TankModel } from './components/TankModel';
|
||||||
|
|
||||||
@@ -35,8 +24,6 @@ export function TankDisplay() {
|
|||||||
const canvasWrapper = useRef<HTMLDivElement>(null);
|
const canvasWrapper = useRef<HTMLDivElement>(null);
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||||
const mode = useTankopedia((state) => state.mode);
|
const mode = useTankopedia((state) => state.mode);
|
||||||
const showGrid = useTankopedia((state) => state.model.showGrid);
|
|
||||||
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
|
|
||||||
const protagonist = useTankopedia((state) => {
|
const protagonist = useTankopedia((state) => {
|
||||||
if (!state.areTanksAssigned) return;
|
if (!state.areTanksAssigned) return;
|
||||||
return state.protagonist;
|
return state.protagonist;
|
||||||
@@ -143,68 +130,12 @@ export function TankDisplay() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TankModel />
|
<TankModel />
|
||||||
{mode === 'armor' && <TankArmor />}
|
<TankArmor />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Flex
|
<Options canvas={canvasWrapper} isFullScreen={isFullScreen} />
|
||||||
gap="4"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 18,
|
|
||||||
right: 18,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<Button color={showGrid ? 'purple' : 'gray'} variant="ghost">
|
|
||||||
<GearIcon />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={showGrid}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopedia((draft) => {
|
|
||||||
draft.model.showGrid = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Show grid
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={showSpacedArmor}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopedia((draft) => {
|
|
||||||
draft.model.showSpacedArmor = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Spaced armor
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
if (isFullScreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
} else {
|
|
||||||
canvasWrapper.current?.requestFullscreen();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isFullScreen ? (
|
|
||||||
<ExitFullScreenIcon />
|
|
||||||
) : (
|
|
||||||
<EnterFullScreenIcon />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<RotationInputs />
|
<RotationInputs />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
EnterFullScreenIcon,
|
||||||
|
ExitFullScreenIcon,
|
||||||
|
GearIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
||||||
|
import { RefObject } from 'react';
|
||||||
|
import mutateTankopedia, {
|
||||||
|
useTankopedia,
|
||||||
|
} from '../../../../../../../stores/tankopedia';
|
||||||
|
|
||||||
|
interface OptionsProps {
|
||||||
|
isFullScreen: boolean;
|
||||||
|
canvas: RefObject<HTMLElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Options({ isFullScreen, canvas }: OptionsProps) {
|
||||||
|
const showGrid = useTankopedia((state) => state.model.showGrid);
|
||||||
|
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap="4"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 18,
|
||||||
|
right: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button color={showGrid ? 'purple' : 'gray'} variant="ghost">
|
||||||
|
<GearIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={showGrid}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
draft.model.showGrid = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show grid
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={showSpacedArmor}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopedia((draft) => {
|
||||||
|
draft.model.showSpacedArmor = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Spaced armor
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
if (isFullScreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
canvas.current?.requestFullscreen();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import { GroupProps, useLoader } from '@react-three/fiber';
|
import { GroupProps, useLoader } from '@react-three/fiber';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Euler, Mesh, Vector3 } from 'three';
|
import { Euler, Group, Mesh, Vector3 } from 'three';
|
||||||
import { GLTFLoader } from 'three-stdlib';
|
import { GLTFLoader } from 'three-stdlib';
|
||||||
import { ArmorMesh } from '../../../../../../../components/ArmorMesh';
|
import { ArmorMesh } from '../../../../../../../components/ArmorMesh';
|
||||||
import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay';
|
import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay';
|
||||||
import { X_AXIS } from '../../../../../../../constants/axis';
|
import { X_AXIS } from '../../../../../../../constants/axis';
|
||||||
import { canSplash } from '../../../../../../../core/blitz/canSplash';
|
|
||||||
import { isExplosive } from '../../../../../../../core/blitz/isExplosive';
|
|
||||||
import { numericPenetration } from '../../../../../../../core/blitz/numericPenetration';
|
|
||||||
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
||||||
import {
|
import {
|
||||||
ModelDefinitions,
|
ModelDefinitions,
|
||||||
@@ -21,7 +18,7 @@ import { Lighting } from '../../Lighting';
|
|||||||
interface TankArmorProps extends GroupProps {}
|
interface TankArmorProps extends GroupProps {}
|
||||||
|
|
||||||
export function TankArmor({ ...props }: TankArmorProps) {
|
export function TankArmor({ ...props }: TankArmorProps) {
|
||||||
const controlsEnabled = useTankopedia((state) => state.model.controlsEnabled);
|
const wrapper = useRef<Group>(null);
|
||||||
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
|
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
|
||||||
ModelDefinitions | undefined
|
ModelDefinitions | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -29,10 +26,6 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
if (!state.areTanksAssigned) return;
|
if (!state.areTanksAssigned) return;
|
||||||
return state.protagonist;
|
return state.protagonist;
|
||||||
});
|
});
|
||||||
const antagonist = useTankopedia((state) => {
|
|
||||||
if (!state.areTanksAssigned) return;
|
|
||||||
return state.antagonist;
|
|
||||||
});
|
|
||||||
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
|
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
|
||||||
const model = useTankopedia((state) => state.model);
|
const model = useTankopedia((state) => state.model);
|
||||||
|
|
||||||
@@ -42,9 +35,20 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = useTankopedia.subscribe(
|
||||||
|
(state) => state.model.controlsEnabled,
|
||||||
|
(controlsEnabled) => {
|
||||||
|
if (wrapper.current) wrapper.current.visible = controlsEnabled;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
// it's ok to have hooks after this early termination because this will never be true
|
// it's ok to have hooks after this early termination because this will never be true
|
||||||
// it's more for typescript to stop throwing a fit
|
// it's more for typescript to stop throwing a fit
|
||||||
if (!protagonist || !antagonist) return null;
|
if (!protagonist) return null;
|
||||||
|
|
||||||
const armorGltf = useLoader(
|
const armorGltf = useLoader(
|
||||||
GLTFLoader,
|
GLTFLoader,
|
||||||
@@ -77,11 +81,6 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
.sub(turretOrigin)
|
.sub(turretOrigin)
|
||||||
.applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw);
|
.applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw);
|
||||||
const turretRotation = new Euler(0, 0, model.turretYaw);
|
const turretRotation = new Euler(0, 0, model.turretYaw);
|
||||||
const splashCapable = canSplash(antagonist.shell.type);
|
|
||||||
const explosiveCapable = isExplosive(antagonist.shell.type);
|
|
||||||
const penetration = numericPenetration(antagonist.shell.penetration);
|
|
||||||
const ricochet = antagonist.shell.ricochet ?? Math.PI / 2;
|
|
||||||
const normalization = antagonist.shell.normalization ?? 0;
|
|
||||||
|
|
||||||
if (tankModelDefinition.turretRotation) {
|
if (tankModelDefinition.turretRotation) {
|
||||||
const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180);
|
const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180);
|
||||||
@@ -99,25 +98,14 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
|
|
||||||
turretPosition.add(turretOrigin);
|
turretPosition.add(turretOrigin);
|
||||||
|
|
||||||
// const tracksGeometry = mergeBufferGeometries(
|
|
||||||
// modelNodes
|
|
||||||
// .map((node) => {
|
|
||||||
// const isWheel = node.name.startsWith('chassis_wheel_');
|
|
||||||
// const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
// const isVisible = isWheel || isTrack;
|
|
||||||
// if (isVisible) return (node as Mesh).geometry;
|
|
||||||
// })
|
|
||||||
// .filter(Boolean),
|
|
||||||
// );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeadsUpDisplay>
|
<HeadsUpDisplay>
|
||||||
<Lighting />
|
<Lighting />
|
||||||
|
|
||||||
<group
|
<group
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={wrapper}
|
||||||
rotation={[-Math.PI / 2, 0, model.hullYaw]}
|
rotation={[-Math.PI / 2, 0, model.hullYaw]}
|
||||||
visible={controlsEnabled}
|
|
||||||
>
|
>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isHull = node.name.startsWith('hull_');
|
||||||
@@ -139,31 +127,12 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
<ArmorMesh
|
<ArmorMesh
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
geometry={(node as Mesh).geometry}
|
||||||
caliber={antagonist.shell.caliber}
|
|
||||||
canSplash={splashCapable}
|
|
||||||
isExplosive={explosiveCapable}
|
|
||||||
isSpaced={spaced ?? false}
|
isSpaced={spaced ?? false}
|
||||||
normalization={normalization}
|
|
||||||
penetration={penetration}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
ricochet={ricochet}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* <ArmorMesh
|
|
||||||
geometry={tracksGeometry!}
|
|
||||||
caliber={antagonist.shell.caliber}
|
|
||||||
canSplash={splashCapable}
|
|
||||||
isExplosive={explosiveCapable}
|
|
||||||
isSpaced
|
|
||||||
normalization={normalization}
|
|
||||||
penetration={penetration}
|
|
||||||
thickness={50}
|
|
||||||
ricochet={ricochet}
|
|
||||||
isExternalModule
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
const isTrack = node.name.startsWith('chassis_track_');
|
||||||
@@ -176,14 +145,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
<ArmorMesh
|
<ArmorMesh
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
geometry={(node as Mesh).geometry}
|
||||||
caliber={antagonist.shell.caliber}
|
|
||||||
canSplash={splashCapable}
|
|
||||||
isExplosive={explosiveCapable}
|
|
||||||
isSpaced
|
isSpaced
|
||||||
normalization={normalization}
|
|
||||||
penetration={penetration}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
ricochet={ricochet}
|
|
||||||
isExternalModule
|
isExternalModule
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -215,14 +178,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
geometry={(node as Mesh).geometry}
|
||||||
position={turretOrigin}
|
position={turretOrigin}
|
||||||
caliber={antagonist.shell.caliber}
|
|
||||||
canSplash={splashCapable}
|
|
||||||
isExplosive={explosiveCapable}
|
|
||||||
isSpaced={spaced ?? false}
|
isSpaced={spaced ?? false}
|
||||||
normalization={normalization}
|
|
||||||
penetration={penetration}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
ricochet={ricochet}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -258,14 +215,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
geometry={(node as Mesh).geometry}
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
caliber={antagonist.shell.caliber}
|
|
||||||
canSplash={splashCapable}
|
|
||||||
isExplosive={explosiveCapable}
|
|
||||||
isSpaced={spaced ?? false}
|
isSpaced={spaced ?? false}
|
||||||
normalization={normalization}
|
|
||||||
penetration={penetration}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
ricochet={ricochet}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -283,14 +234,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
|
|||||||
<ArmorMesh
|
<ArmorMesh
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
geometry={(node as Mesh).geometry}
|
||||||
caliber={antagonist.shell.caliber}
|
|
||||||
canSplash={splashCapable}
|
|
||||||
isExplosive={explosiveCapable}
|
|
||||||
isSpaced
|
isSpaced
|
||||||
normalization={normalization}
|
|
||||||
penetration={penetration}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
ricochet={ricochet}
|
|
||||||
isExternalModule
|
isExternalModule
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { MeshProps } from '@react-three/fiber';
|
import { MeshProps } from '@react-three/fiber';
|
||||||
import { MeshStandardMaterial } from 'three';
|
import { MeshStandardMaterial } from 'three';
|
||||||
import CustomShaderMaterial from 'three-custom-shader-material';
|
import CustomShaderMaterial from 'three-custom-shader-material';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { canSplash } from '../../core/blitz/canSplash';
|
||||||
|
import { isExplosive } from '../../core/blitz/isExplosive';
|
||||||
|
import { resolveNearPenetration } from '../../core/blitz/resolveNearPenetration';
|
||||||
|
import { useTankopedia } from '../../stores/tankopedia';
|
||||||
import fragmentShader from './shaders/fragment.glsl';
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
import vertexShader from './shaders/vertex.glsl';
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
|
||||||
interface ArmorMeshProps extends MeshProps {
|
interface ArmorMeshProps extends MeshProps {
|
||||||
isExplosive: boolean;
|
|
||||||
canSplash: boolean;
|
|
||||||
isSpaced: boolean;
|
isSpaced: boolean;
|
||||||
isExternalModule?: boolean;
|
isExternalModule?: boolean;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
penetration: number;
|
|
||||||
caliber: number;
|
|
||||||
ricochet: number;
|
|
||||||
normalization: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ArmorMesh({
|
export function ArmorMesh({
|
||||||
isExplosive,
|
|
||||||
canSplash,
|
|
||||||
isSpaced,
|
isSpaced,
|
||||||
isExternalModule = false,
|
isExternalModule = false,
|
||||||
thickness,
|
thickness,
|
||||||
penetration,
|
|
||||||
caliber,
|
|
||||||
ricochet,
|
|
||||||
normalization,
|
|
||||||
...props
|
...props
|
||||||
}: ArmorMeshProps) {
|
}: ArmorMeshProps) {
|
||||||
|
const shell = useTankopedia((state) => {
|
||||||
|
if (!state.areTanksAssigned) return;
|
||||||
|
return state.antagonist.shell;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isSpaced && (
|
{!isSpaced && (
|
||||||
@@ -45,15 +45,19 @@ export function ArmorMesh({
|
|||||||
fragmentShader={fragmentShader}
|
fragmentShader={fragmentShader}
|
||||||
vertexShader={vertexShader}
|
vertexShader={vertexShader}
|
||||||
uniforms={{
|
uniforms={{
|
||||||
isExplosive: { value: isExplosive },
|
isExplosive: { value: isExplosive(shell.type) },
|
||||||
canSplash: { value: canSplash },
|
canSplash: { value: canSplash(shell.type) },
|
||||||
isSpaced: { value: isSpaced },
|
isSpaced: { value: isSpaced },
|
||||||
isExternalModule: { value: isExternalModule },
|
isExternalModule: { value: isExternalModule },
|
||||||
thickness: { value: thickness },
|
thickness: { value: thickness },
|
||||||
penetration: { value: penetration },
|
penetration: {
|
||||||
caliber: { value: caliber },
|
value: resolveNearPenetration(shell.penetration),
|
||||||
ricochet: { value: ricochet * (Math.PI / 180) },
|
},
|
||||||
normalization: { value: normalization * (Math.PI / 180) },
|
caliber: { value: shell.caliber },
|
||||||
|
ricochet: { value: degToRad(shell.ricochet ?? 90) },
|
||||||
|
normalization: {
|
||||||
|
value: degToRad(shell.normalization ?? 0),
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Vector2Tuple } from 'three';
|
import { Vector2Tuple } from 'three';
|
||||||
|
|
||||||
export function numericPenetration(penetration: number | Vector2Tuple) {
|
export function resolveNearPenetration(penetration: number | Vector2Tuple) {
|
||||||
return typeof penetration === 'number' ? penetration : penetration[0];
|
return typeof penetration === 'number' ? penetration : penetration[0];
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { subscribeWithSelector } from 'zustand/middleware';
|
||||||
import { TankType, TreeTypeString } from '../components/Tanks';
|
import { TankType, TreeTypeString } from '../components/Tanks';
|
||||||
import {
|
import {
|
||||||
GunDefinition,
|
GunDefinition,
|
||||||
@@ -54,7 +55,8 @@ type Tankopedia = {
|
|||||||
| { areTanksAssigned: false }
|
| { areTanksAssigned: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useTankopedia = create<Tankopedia>()(() => ({
|
export const useTankopedia = create<Tankopedia>()(
|
||||||
|
subscribeWithSelector<Tankopedia>(() => ({
|
||||||
sort: {
|
sort: {
|
||||||
by: 'tier',
|
by: 'tier',
|
||||||
direction: 'descending',
|
direction: 'descending',
|
||||||
@@ -76,7 +78,8 @@ export const useTankopedia = create<Tankopedia>()(() => ({
|
|||||||
},
|
},
|
||||||
areTanksAssigned: false,
|
areTanksAssigned: false,
|
||||||
mode: 'model',
|
mode: 'model',
|
||||||
}));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) {
|
export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) {
|
||||||
useTankopedia.setState(produce(recipe));
|
useTankopedia.setState(produce(recipe));
|
||||||
|
|||||||
Reference in new issue
Block a user