feat: animation

This commit is contained in:
TrèsAbhi committed 2024-02-29 13:17:41 -06:00
1 parent 199e156e53
commit 7af1fdcf6b
2 files changed
+113 -102

No files matched your search

+99 -102
View File
@@ -1,10 +1,12 @@
import { Card, Flex, Inset, Text } from '@radix-ui/themes'; import { Card, Flex, Inset, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei'; import { Html } from '@react-three/drei';
import { Euler, Quaternion, Vector3 } from 'three'; import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import { Mesh } from 'three';
import { radToDeg } from 'three/src/math/MathUtils'; import { radToDeg } from 'three/src/math/MathUtils';
import { J_HAT } from '../../../constants/axis';
import { isExplosive } from '../../../core/blitz/isExplosive'; import { isExplosive } from '../../../core/blitz/isExplosive';
import { NaNFallback } from '../../../core/math/NaNFallback'; import { NaNFallback } from '../../../core/math/NaNFallback';
import { normalToEuler } from '../../../core/math/normalToEuler';
import { AngledPenetration } from '../../../icons/AngledPenetration'; import { AngledPenetration } from '../../../icons/AngledPenetration';
import { Block } from '../../../icons/Block'; import { Block } from '../../../icons/Block';
import { NominalPenetration } from '../../../icons/NominalPenetration'; import { NominalPenetration } from '../../../icons/NominalPenetration';
@@ -19,18 +21,6 @@ import {
} from '../../../stores/tankopedia'; } from '../../../stores/tankopedia';
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from './SpacedArmorScene';
// export const SHOT_NAMES: Record<Shot['type'], string> = {
// ricochet: 'Ricochet',
// penetration: 'Penetration',
// block: 'Blocked',
// };
// export const SHOT_ICONS: Record<ShotLayerBase[''], ReactNode> = {
// ricochet: <Ricochet width={24} height={24} />,
// block: <Block width={24} height={24} />,
// penetration: <NominalPenetration width={24} height={24} />,
// };
const layerTypeNames: Record<ArmorType | 'null', string> = { const layerTypeNames: Record<ArmorType | 'null', string> = {
[ArmorType.Core]: 'Core', [ArmorType.Core]: 'Core',
[ArmorType.Spaced]: 'Spaced', [ArmorType.Spaced]: 'Spaced',
@@ -38,30 +28,111 @@ const layerTypeNames: Record<ArmorType | 'null', string> = {
null: 'Gap', null: 'Gap',
}; };
// const THICKNESS = 0.05;
// const LENGTH = 4;
const LENGTH_INFINITY = 4; const LENGTH_INFINITY = 4;
const TRACER_THIN = 1 / 128;
const TRACER_THICK = 1 / 64;
export function ShotDisplay() { export function ShotDisplay() {
const shot = useTankopediaTemporary((state) => state.shot); const shot = useTankopediaTemporary((state) => state.shot);
const preMidPointTracer = useRef<Mesh>(null);
const postMidPointTracer = useRef<Mesh>(null);
const hasMultipleLayers = shot?.layers.some( const hasMultipleLayers = shot?.layers.some(
(layer, index) => index !== 0 && layer.type !== null, (layer, index) => index !== 0 && layer.type !== null,
); );
const { shell } = useDuel.getState().antagonist!; const { shell } = useDuel.getState().antagonist!;
const explosive = isExplosive(shell.type); const explosive = isExplosive(shell.type);
const midPointLayer = (shot?.layers.findLast(
(layer) => layer.type !== null && layer.status === 'ricochet',
) ?? shot?.layers.findLast((layer) => layer.type !== null)) as ShotLayerBase;
const firstLayer = shot?.layers[0] as Exclude<ShotLayer, ShotLayerGap>;
const lastLayer = shot?.layers.at(-1)!;
const distanceFromSpacedArmor = shot
? firstLayer.point.distanceTo(shot.point)
: undefined;
const preMidPointLayers = shot?.layers.slice(
0,
shot.layers.indexOf(midPointLayer as ShotLayer),
);
const preMidPointGap = shot
? preMidPointLayers!.reduce((accumulator, layer) => {
if (layer.type === null) return accumulator + layer.distance;
return accumulator;
}, 0) + LENGTH_INFINITY
: undefined;
useFrame(({ clock }) => {
if (!shot || !preMidPointTracer.current) return;
const t1 = clock.elapsedTime % 1;
const t2 = (clock.elapsedTime + 0.5) % 1;
preMidPointTracer.current.position.set(0, (1 - t1) * preMidPointGap!, 0);
preMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t1 - 0.5), 1);
if (!postMidPointTracer.current) return;
postMidPointTracer.current.position.set(0, t2 * preMidPointGap!, 0);
postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1);
});
if (!shot) return null;
return ( return (
<> <>
{shot?.layers.map((layer, index) => { <group
position={midPointLayer.point}
rotation={normalToEuler(midPointLayer.shellNormal)}
>
<mesh position={[0, preMidPointGap! / 2, 0]}>
<cylinderGeometry args={[TRACER_THIN, TRACER_THIN, preMidPointGap]} />
<meshBasicMaterial />
</mesh>
<mesh position={[0, preMidPointGap! / 2, 0]} ref={preMidPointTracer}>
<cylinderGeometry
args={[TRACER_THICK, TRACER_THICK, preMidPointGap]}
/>
<meshBasicMaterial
color={
lastLayer.status === 'blocked'
? 0xff4040
: lastLayer.status === 'penetration'
? 0x00ff00
: 0xffff00
}
/>
</mesh>
</group>
{midPointLayer.status === 'ricochet' && (
<group
position={midPointLayer.point}
rotation={normalToEuler(
midPointLayer.shellNormal
.clone()
.reflect(midPointLayer.surfaceNormal)
.multiplyScalar(-1),
)}
>
<mesh position={[0, preMidPointGap! / 2, 0]}>
<cylinderGeometry
args={[TRACER_THIN, TRACER_THIN, preMidPointGap]}
/>
</mesh>
<mesh position={[0, preMidPointGap! / 2, 0]} ref={postMidPointTracer}>
<cylinderGeometry
args={[TRACER_THICK, TRACER_THICK, preMidPointGap]}
/>
<meshBasicMaterial color={0xffff00} />
</mesh>
</group>
)}
{shot.layers.map((layer, index) => {
if (layer.type === null) return; if (layer.type === null) return;
const shellRotation = new Euler().setFromQuaternion( const shellRotation = normalToEuler(layer.shellNormal);
new Quaternion().setFromAxisAngle(
new Vector3().crossVectors(J_HAT, layer.shellNormal).normalize(),
J_HAT.angleTo(layer.shellNormal),
),
);
const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined; const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined;
const trueStatus = const trueStatus =
nextLayer?.status === 'blocked' ? 'blocked' : layer.status; nextLayer?.status === 'blocked' ? 'blocked' : layer.status;
@@ -107,75 +178,11 @@ export function ShotDisplay() {
</Inset> </Inset>
</Card> </Card>
</Html> </Html>
{(() => {
let length = LENGTH_INFINITY;
if (layer.index !== 0) {
const gap = shot.layers[index - 1] as ShotLayerGap;
length = gap.distance;
}
return (
<mesh position={[0, length / 2, 0]}>
<cylinderGeometry args={[1 / 64, 1 / 64, length]} />
<meshBasicMaterial color={0xffffff} depthTest={false} />
</mesh>
);
})()}
</group> </group>
{(() => {
if (
layer.status !== 'ricochet' ||
index !== shot.layers.length - 1
)
return null;
const ricochetShellNormal = layer.shellNormal
.clone()
.reflect(layer.surfaceNormal)
.multiplyScalar(-1);
const ricochetShellRotation = new Euler().setFromQuaternion(
new Quaternion().setFromAxisAngle(
new Vector3()
.crossVectors(J_HAT, ricochetShellNormal)
.normalize(),
J_HAT.angleTo(ricochetShellNormal),
),
);
return (
<group position={layer.point} rotation={ricochetShellRotation}>
<mesh position={[0, LENGTH_INFINITY / 2, 0]}>
<cylinderGeometry
args={[1 / 64, 1 / 64, LENGTH_INFINITY]}
/>
<meshBasicMaterial color={0xffffff} depthTest={false} />
</mesh>
</group>
);
})()}
</> </>
); );
})} })}
{shot &&
(() => {
const firstLayer = shot.layers[0] as Exclude<ShotLayer, ShotLayerGap>;
const lastLayer = shot.layers.at(-1)!;
const midPointLayer = (shot.layers.findLast(
(layer) => layer.type !== null && layer.status === 'ricochet',
) ??
shot.layers.findLast(
(layer) => layer.type !== null,
)) as ShotLayerBase;
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
shot.point,
);
console.log(distanceFromSpacedArmor);
return (
<Html <Html
position={midPointLayer.point} position={midPointLayer.point}
style={{ style={{
@@ -200,8 +207,7 @@ export function ShotDisplay() {
0.5 * 0.5 *
shell.damage.armor * shell.damage.armor *
(1 - (1 -
distanceFromSpacedArmor / distanceFromSpacedArmor! / shell.explosionRadius!) -
shell.explosionRadius!) -
1.1 * 1.1 *
(midPointLayer as ShotLayerNonExternal) (midPointLayer as ShotLayerNonExternal)
.thicknessAngled, .thicknessAngled,
@@ -219,23 +225,17 @@ export function ShotDisplay() {
<Flex> <Flex>
{hasMultipleLayers && ( {hasMultipleLayers && (
<Text size="2" style={{ width: 16 }}> <Text size="2" style={{ width: 16 }}>
{layer.type === null {layer.type === null ? null : `${layer.index + 1}.`}
? null
: `${layer.index + 1}.`}
</Text> </Text>
)} )}
<Text size="2"> <Text size="2">{layerTypeNames[`${layer.type}`]}</Text>
{layerTypeNames[`${layer.type}`]}
</Text>
</Flex> </Flex>
{layer.type !== null && ( {layer.type !== null && (
<> <>
<Flex align="center" style={{ width: 64 }}> <Flex align="center" style={{ width: 64 }}>
<NominalPenetration width={16} height={16} /> <NominalPenetration width={16} height={16} />
<Text size="2"> <Text size="2">{Math.round(layer.thickness)}mm</Text>
{Math.round(layer.thickness)}mm
</Text>
</Flex> </Flex>
{layer.type !== ArmorType.External && ( {layer.type !== ArmorType.External && (
@@ -258,7 +258,6 @@ export function ShotDisplay() {
{layer.type === null && ( {layer.type === null && (
<Text size="2"> <Text size="2">
{/* why is it x50? i can't pretend to understand this shit */}
{Math.round(layer.distance * 1000)}mm (- {Math.round(layer.distance * 1000)}mm (-
{Math.round(Math.min(100, layer.distance * 50))}% {Math.round(Math.min(100, layer.distance * 50))}%
penetration) penetration)
@@ -270,8 +269,6 @@ export function ShotDisplay() {
</Flex> </Flex>
</Card> </Card>
</Html> </Html>
);
})()}
</> </>
); );
} }
+14
View File
@@ -0,0 +1,14 @@
import { Euler, Quaternion, Vector3 } from 'three';
import { J_HAT } from '../../constants/axis';
const quaternion = new Quaternion();
const axis = new Vector3();
export function normalToEuler(normal: Vector3, up = J_HAT) {
return new Euler().setFromQuaternion(
quaternion.setFromAxisAngle(
axis.crossVectors(up, normal).normalize(),
up.angleTo(normal),
),
);
}