mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: ricochet angle
This commit is contained in:
1 parent
44f00e042e
commit
3c63323814
1 file changed
+113
-54
+113
-54
@@ -1,7 +1,8 @@
|
|||||||
import { Card, Flex, Text } from '@radix-ui/themes';
|
import { Card, Flex, Text } from '@radix-ui/themes';
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from '@react-three/drei';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
import { ReactNode, useEffect, useRef } from 'react';
|
import { ReactNode, useEffect, useRef } from 'react';
|
||||||
import { Group, Vector3 } from 'three';
|
import { Group, Mesh, Vector3 } from 'three';
|
||||||
import { radToDeg } from 'three/src/math/MathUtils';
|
import { radToDeg } from 'three/src/math/MathUtils';
|
||||||
import { AngledPenetration } from '../icons/AngledPenetration';
|
import { AngledPenetration } from '../icons/AngledPenetration';
|
||||||
import { Block } from '../icons/Block';
|
import { Block } from '../icons/Block';
|
||||||
@@ -21,75 +22,133 @@ export const SHOT_ICONS: Record<Shot['type'], ReactNode> = {
|
|||||||
penetration: <NominalPenetration width={24} height={24} />,
|
penetration: <NominalPenetration width={24} height={24} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const THICKNESS = 0.05;
|
const THICKNESS = 0.025;
|
||||||
const LENGTH = 2;
|
const LENGTH = 2;
|
||||||
|
|
||||||
export function ShotDisplay() {
|
export function ShotDisplay() {
|
||||||
const shot = useTankopediaTemporary((state) => state.shot);
|
const shot = useTankopediaTemporary((state) => state.shot);
|
||||||
const wrapper = useRef<Group>(null);
|
const entryWrapper = useRef<Group>(null);
|
||||||
|
const entryLine = useRef<Mesh>(null);
|
||||||
|
const circle = useRef<Mesh>(null);
|
||||||
|
const exitWrapper = useRef<Group>(null);
|
||||||
|
const exitLine = useRef<Mesh>(null);
|
||||||
|
|
||||||
|
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 * LENGTH);
|
||||||
|
exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shot) return;
|
if (!shot) return;
|
||||||
|
|
||||||
const point = new Vector3(...shot.point);
|
const point = new Vector3(...shot.point);
|
||||||
const shotNormal = new Vector3(...shot.shellNormal);
|
const shotNormal = new Vector3(...shot.shellNormal);
|
||||||
|
const surfaceNormal = new Vector3(...shot.surfaceNormal);
|
||||||
|
const bounceNormal = shotNormal
|
||||||
|
.clone()
|
||||||
|
.multiplyScalar(-1)
|
||||||
|
.sub(
|
||||||
|
surfaceNormal
|
||||||
|
.clone()
|
||||||
|
.multiplyScalar(
|
||||||
|
2 * surfaceNormal.dot(shotNormal.clone().multiplyScalar(-1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
wrapper.current?.position.set(...shot.point);
|
entryWrapper.current?.position.set(...shot.point);
|
||||||
wrapper.current?.lookAt(point.clone().add(shotNormal));
|
entryWrapper.current?.lookAt(point.clone().add(shotNormal));
|
||||||
|
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]);
|
}, [shot]);
|
||||||
|
|
||||||
if (!shot) return null;
|
if (!shot) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={wrapper}>
|
<>
|
||||||
<Html position={[0, 0, LENGTH]}>
|
<mesh ref={circle}>
|
||||||
<Card
|
<torusGeometry args={[0.05, 0.0125, 4, 16]} />
|
||||||
style={{
|
<meshBasicMaterial
|
||||||
width: 300,
|
color={shot.type === 'penetration' ? 'green' : 'red'}
|
||||||
}}
|
depthTest={false}
|
||||||
>
|
/>
|
||||||
<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: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{shot.thicknesses.map(({ nominal, angled, type }, index) => (
|
|
||||||
<Flex gap="2">
|
|
||||||
<Text>
|
|
||||||
{index + 1}. {type[0].toUpperCase()}
|
|
||||||
{type.slice(1)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Flex gap="1">
|
|
||||||
<NominalPenetration width={24} height={24} />
|
|
||||||
<Text>{nominal}mm</Text>
|
|
||||||
</Flex>
|
|
||||||
{type !== 'external' && (
|
|
||||||
<Flex gap="1">
|
|
||||||
<AngledPenetration width={24} height={24} />
|
|
||||||
<Text>{Math.round(angled)}mm</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Html>
|
|
||||||
|
|
||||||
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
|
|
||||||
<cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} />
|
|
||||||
<meshBasicMaterial color="white" depthTest={false} />
|
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
|
||||||
|
<group ref={entryWrapper}>
|
||||||
|
<Html position={[0, 0, LENGTH]}>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shot.thicknesses.map(({ nominal, angled, type }, index) => (
|
||||||
|
<Flex gap="2">
|
||||||
|
<Text>
|
||||||
|
{index + 1}. {type[0].toUpperCase()}
|
||||||
|
{type.slice(1)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Flex gap="1">
|
||||||
|
<NominalPenetration width={24} height={24} />
|
||||||
|
<Text>{nominal}mm</Text>
|
||||||
|
</Flex>
|
||||||
|
{type !== 'external' && (
|
||||||
|
<Flex gap="1">
|
||||||
|
<AngledPenetration width={24} height={24} />
|
||||||
|
<Text>{Math.round(angled)}mm</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Html>
|
||||||
|
|
||||||
|
<mesh
|
||||||
|
position={[0, 0, LENGTH / 2]}
|
||||||
|
rotation={[Math.PI / 2, 0, 0]}
|
||||||
|
ref={entryLine}
|
||||||
|
>
|
||||||
|
<cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} />
|
||||||
|
<meshBasicMaterial color="white" depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{shot.type === 'ricochet' && (
|
||||||
|
<group ref={exitWrapper}>
|
||||||
|
<mesh
|
||||||
|
position={[0, 0, LENGTH / 2]}
|
||||||
|
rotation={[Math.PI / 2, 0, 0]}
|
||||||
|
ref={exitLine}
|
||||||
|
>
|
||||||
|
<cylinderGeometry
|
||||||
|
args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial color="white" depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user