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
+185 -174

No files matched your search

+171 -174
View File
@@ -1,10 +1,12 @@
import { Card, Flex, Inset, Text } from '@radix-ui/themes';
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 { J_HAT } from '../../../constants/axis';
import { isExplosive } from '../../../core/blitz/isExplosive';
import { NaNFallback } from '../../../core/math/NaNFallback';
import { normalToEuler } from '../../../core/math/normalToEuler';
import { AngledPenetration } from '../../../icons/AngledPenetration';
import { Block } from '../../../icons/Block';
import { NominalPenetration } from '../../../icons/NominalPenetration';
@@ -19,18 +21,6 @@ import {
} from '../../../stores/tankopedia';
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> = {
[ArmorType.Core]: 'Core',
[ArmorType.Spaced]: 'Spaced',
@@ -38,30 +28,111 @@ const layerTypeNames: Record<ArmorType | 'null', string> = {
null: 'Gap',
};
// const THICKNESS = 0.05;
// const LENGTH = 4;
const LENGTH_INFINITY = 4;
const TRACER_THIN = 1 / 128;
const TRACER_THICK = 1 / 64;
export function ShotDisplay() {
const shot = useTankopediaTemporary((state) => state.shot);
const preMidPointTracer = useRef<Mesh>(null);
const postMidPointTracer = useRef<Mesh>(null);
const hasMultipleLayers = shot?.layers.some(
(layer, index) => index !== 0 && layer.type !== null,
);
const { shell } = useDuel.getState().antagonist!;
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 (
<>
{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;
const shellRotation = new Euler().setFromQuaternion(
new Quaternion().setFromAxisAngle(
new Vector3().crossVectors(J_HAT, layer.shellNormal).normalize(),
J_HAT.angleTo(layer.shellNormal),
),
);
const shellRotation = normalToEuler(layer.shellNormal);
const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined;
const trueStatus =
nextLayer?.status === 'blocked' ? 'blocked' : layer.status;
@@ -107,171 +178,97 @@ export function ShotDisplay() {
</Inset>
</Card>
</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>
{(() => {
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,
);
<Html
position={midPointLayer.point}
style={{
transform: 'translateY(-50%)',
marginLeft: 32,
pointerEvents: 'none',
}}
>
<Card style={{ width: 256 + 32 }}>
<Flex direction="column">
<Text>
{lastLayer.status[0].toUpperCase()}
{lastLayer.status.slice(1)}
{lastLayer.status === 'penetration' &&
`: ${Math.round(shell.damage.armor)}hp`}
{lastLayer.status === 'blocked' &&
shell.type === 'he' &&
`: ${NaNFallback(
Math.max(
0,
Math.round(
0.5 *
shell.damage.armor *
(1 -
distanceFromSpacedArmor! / shell.explosionRadius!) -
1.1 *
(midPointLayer as ShotLayerNonExternal)
.thicknessAngled,
),
),
0,
)}hp`}
</Text>
console.log(distanceFromSpacedArmor);
{shot.layers.map((layer) => {
if (layer.type === null && !explosive) return null;
return (
<Html
position={midPointLayer.point}
style={{
transform: 'translateY(-50%)',
marginLeft: 32,
pointerEvents: 'none',
}}
>
<Card style={{ width: 256 + 32 }}>
<Flex direction="column">
<Text>
{lastLayer.status[0].toUpperCase()}
{lastLayer.status.slice(1)}
{lastLayer.status === 'penetration' &&
`: ${Math.round(shell.damage.armor)}hp`}
{lastLayer.status === 'blocked' &&
shell.type === 'he' &&
`: ${NaNFallback(
Math.max(
0,
Math.round(
0.5 *
shell.damage.armor *
(1 -
distanceFromSpacedArmor /
shell.explosionRadius!) -
1.1 *
(midPointLayer as ShotLayerNonExternal)
.thicknessAngled,
),
),
0,
)}hp`}
</Text>
return (
<Flex align="center" gap="2">
<Flex>
{hasMultipleLayers && (
<Text size="2" style={{ width: 16 }}>
{layer.type === null ? null : `${layer.index + 1}.`}
</Text>
)}
<Text size="2">{layerTypeNames[`${layer.type}`]}</Text>
</Flex>
{shot.layers.map((layer) => {
if (layer.type === null && !explosive) return null;
{layer.type !== null && (
<>
<Flex align="center" style={{ width: 64 }}>
<NominalPenetration width={16} height={16} />
<Text size="2">{Math.round(layer.thickness)}mm</Text>
</Flex>
return (
<Flex align="center" gap="2">
<Flex>
{hasMultipleLayers && (
<Text size="2" style={{ width: 16 }}>
{layer.type === null
? null
: `${layer.index + 1}.`}
</Text>
{layer.type !== ArmorType.External && (
<Flex align="center">
{layer.status === 'ricochet' ? (
<Ricochet width={16} height={16} />
) : layer.status === 'blocked' ? (
<Block width={16} height={16} />
) : (
<AngledPenetration width={16} height={16} />
)}
<Text size="2">
{layerTypeNames[`${layer.type}`]}
{Math.round(layer.thicknessAngled)}mm (
{Math.round(radToDeg(layer.angle))}°)
</Text>
</Flex>
)}
</>
)}
{layer.type !== null && (
<>
<Flex align="center" style={{ width: 64 }}>
<NominalPenetration width={16} height={16} />
<Text size="2">
{Math.round(layer.thickness)}mm
</Text>
</Flex>
{layer.type !== ArmorType.External && (
<Flex align="center">
{layer.status === 'ricochet' ? (
<Ricochet width={16} height={16} />
) : layer.status === 'blocked' ? (
<Block width={16} height={16} />
) : (
<AngledPenetration width={16} height={16} />
)}
<Text size="2">
{Math.round(layer.thicknessAngled)}mm (
{Math.round(radToDeg(layer.angle))}°)
</Text>
</Flex>
)}
</>
)}
{layer.type === null && (
<Text size="2">
{/* why is it x50? i can't pretend to understand this shit */}
{Math.round(layer.distance * 1000)}mm (-
{Math.round(Math.min(100, layer.distance * 50))}%
penetration)
</Text>
)}
</Flex>
);
})}
{layer.type === null && (
<Text size="2">
{Math.round(layer.distance * 1000)}mm (-
{Math.round(Math.min(100, layer.distance * 50))}%
penetration)
</Text>
)}
</Flex>
</Card>
</Html>
);
})()}
);
})}
</Flex>
</Card>
</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),
),
);
}