feat: shot display

This commit is contained in:
TrèsAbhi committed 2024-05-26 13:42:51 -05:00
1 parent 3438e51ef1
commit 7f8797a449
3 files changed
+113 -35

No files matched your search

+77 -11
View File
@@ -1,7 +1,10 @@
import { Card, Flex, Text } from '@radix-ui/themes'; import { Card, Flex, Kbd, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei'; import { Html } from '@react-three/drei';
import { ComponentProps } from 'react'; import { useFrame } from '@react-three/fiber';
import { Shot, useTankopediaTemporary } from '../../../stores/tankopedia'; import { ComponentProps, useRef } from 'react';
import { Euler, Mesh, Quaternion } from 'three';
import { J_HAT } from '../../../constants/axis';
import { ShotStatus, useTankopediaTemporary } from '../../../stores/tankopedia';
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from './SpacedArmorScene';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent'; import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
@@ -17,7 +20,7 @@ const externalLayerNames: Record<ExternalModuleVariant, string> = {
track: 'Tracks', track: 'Tracks',
}; };
const shotStatusNames: Record<Shot['status'], string> = { const shotStatusNames: Record<ShotStatus, string> = {
blocked: 'Blocked', blocked: 'Blocked',
penetration: 'penetration', penetration: 'penetration',
ricochet: 'ricochet', ricochet: 'ricochet',
@@ -25,7 +28,7 @@ const shotStatusNames: Record<Shot['status'], string> = {
}; };
const shotStatusColors: Record< const shotStatusColors: Record<
Shot['status'], ShotStatus,
ComponentProps<typeof Text>['color'] ComponentProps<typeof Text>['color']
> = { > = {
blocked: 'red', blocked: 'red',
@@ -40,13 +43,31 @@ const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() { export function ShotDisplay() {
const shot = useTankopediaTemporary((state) => state.shot); const shot = useTankopediaTemporary((state) => state.shot);
const inTracer = useRef<Mesh>(null);
useFrame(({ clock }) => {
if (!shot || !inTracer.current) return;
const t = clock.elapsedTime % 1;
const scale1 = -Math.abs(2 * t - 1) + 1;
inTracer.current.scale.set(1, scale1, 1);
inTracer.current.position.set(0, (1 - t) * inLength, 0);
});
if (!shot) return null; if (!shot) return null;
const inLength =
shot.in.layers.reduce((accumulator, layer) => {
if (layer.type === null) return accumulator + layer.distance;
return accumulator;
}, 0) + LENGTH_INFINITY;
const inLast = shot.in.layers.findLast((layer) => layer.type !== null)!;
return ( return (
<group> <group>
<Html <Html
position={shot.point} position={inLast.point}
style={{ style={{
pointerEvents: 'none', pointerEvents: 'none',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
@@ -54,18 +75,18 @@ export function ShotDisplay() {
> >
<Card ml="9" style={{ width: 300 }}> <Card ml="9" style={{ width: 300 }}>
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">
<Text color={shotStatusColors[shot.status]} weight="bold"> <Text color={shotStatusColors[shot.in.status]} weight="bold">
{shot.status !== 'blocked' && ( {shot.in.status !== 'blocked' && (
<> <>
{Math.round(shot.damage).toLocaleString()} {Math.round(shot.damage).toLocaleString()}
<Text size="1">hp</Text> <Text size="1">hp</Text>
</> </>
)} )}
{` ${shotStatusNames[shot.status]} `} {` ${shotStatusNames[shot.in.status]} `}
</Text> </Text>
<Flex direction="column"> <Flex direction="column">
{shot.layersIn.map((layer, index) => { {shot.in.layers.map((layer, index) => {
const layerName = const layerName =
layer.type === ArmorType.External layer.type === ArmorType.External
? externalLayerNames[layer.variant] ? externalLayerNames[layer.variant]
@@ -74,7 +95,10 @@ export function ShotDisplay() {
layer.type === null layer.type === null
? null ? null
: `${(shot.containsGaps ? index / 2 : index) + 1}.`; : `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor = shotStatusColors[layer.status]; const shotStatusColor =
shot.in.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return ( return (
<Flex align="center" gap="2"> <Flex align="center" gap="2">
@@ -155,6 +179,48 @@ export function ShotDisplay() {
</Flex> </Flex>
</Card> </Card>
</Html> </Html>
{shot.in.layers.map((layer, index) => {
if (layer.type === null) return null;
const layerIndex =
layer.type === null
? null
: (shot.containsGaps ? index / 2 : index) + 1;
const shotStatusColor =
shot.in.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<Html position={layer.point} center>
<Kbd>
<Text color={shotStatusColor}>{layerIndex}</Text>
</Kbd>
</Html>
);
})}
<group
position={inLast.point}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(J_HAT, shot.in.cameraNormal),
)}
>
<mesh position={[0, inLength / 2, 0]}>
<cylinderGeometry
args={[TRACER_THIN / 2, TRACER_THIN / 2, inLength]}
/>
<meshBasicMaterial depthTest={false} />
</mesh>
<mesh ref={inTracer} position={[0, inLength / 2, 0]}>
<cylinderGeometry
args={[TRACER_THICK / 2, TRACER_THICK / 2, inLength]}
/>
<meshBasicMaterial color="red" />
</mesh>
</group>
</group> </group>
); );
} }
@@ -95,17 +95,20 @@ export function SpacedArmorSceneComponent({
const { antagonist, protagonist } = useDuel.getState(); const { antagonist, protagonist } = useDuel.getState();
const shell = antagonist!.shell; const shell = antagonist!.shell;
const shot: Shot = {
status: 'blocked',
damage: -1,
containsGaps: shell.type === ShellType.HEAT,
layersIn: [],
point: event.point,
};
const cameraNormal = camera.position const cameraNormal = camera.position
.clone() .clone()
.sub(event.point) .sub(event.point)
.normalize(); .normalize();
const shot: Shot = {
damage: -1,
containsGaps: shell.type === ShellType.HEAT,
in: {
cameraNormal,
layers: [],
status: 'blocked',
},
point: event.point,
};
const hasCalibratedShells = await hasEquipment( const hasCalibratedShells = await hasEquipment(
103, 103,
@@ -157,14 +160,14 @@ export function SpacedArmorSceneComponent({
const layer = intersection.object.userData; const layer = intersection.object.userData;
if (shell.type === ShellType.HEAT && index > 0) { if (shell.type === ShellType.HEAT && index > 0) {
const lastLayer = shot.layersIn.at(-1) as ShotLayerBase; const lastLayer = shot.in.layers.at(-1) as ShotLayerBase;
const distance = lastLayer.point.distanceTo( const distance = lastLayer.point.distanceTo(
intersection.point, intersection.point,
); );
remainingPenetration -= 0.5 * remainingPenetration * distance; remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration <= 0; const blocked = remainingPenetration <= 0;
shot.layersIn.push({ shot.in.layers.push({
type: null, type: null,
distance, distance,
status: blocked ? 'blocked' : 'penetration', status: blocked ? 'blocked' : 'penetration',
@@ -179,7 +182,7 @@ export function SpacedArmorSceneComponent({
const blocked = const blocked =
remainingPenetration <= 0 || shell.type === ShellType.HE; remainingPenetration <= 0 || shell.type === ShellType.HE;
shot.layersIn.push({ shot.in.layers.push({
type: ArmorType.External, type: ArmorType.External,
index, index,
thickness, thickness,
@@ -213,7 +216,7 @@ export function SpacedArmorSceneComponent({
if (!threeCalibersRule && angle >= ricochet) { if (!threeCalibersRule && angle >= ricochet) {
remainingPenetration *= 0.75; remainingPenetration *= 0.75;
shot.layersIn.push({ shot.in.layers.push({
type: layer.type, type: layer.type,
index, index,
thickness, thickness,
@@ -229,7 +232,7 @@ export function SpacedArmorSceneComponent({
remainingPenetration -= finalThickness; remainingPenetration -= finalThickness;
const blocked = remainingPenetration <= 0; const blocked = remainingPenetration <= 0;
shot.layersIn.push({ shot.in.layers.push({
type: layer.type, type: layer.type,
index, index,
thickness, thickness,
@@ -249,11 +252,11 @@ export function SpacedArmorSceneComponent({
index++; index++;
} }
const lastLayer = shot.layersIn.at(-1) as ShotLayerNonExternal; const lastLayer = shot.in.layers.at(-1) as ShotLayerNonExternal;
const firstLayer = shot.layersIn[0] as ShotLayerBase; const firstLayer = shot.in.layers[0] as ShotLayerBase;
if (shell.type === ShellType.HE) { if (shell.type === ShellType.HE) {
const totalSpacedArmorThickness = shot.layersIn.reduce( const totalSpacedArmorThickness = shot.in.layers.reduce(
(acc, layer) => { (acc, layer) => {
if (layer.type === null || layer.type === ArmorType.Core) { if (layer.type === null || layer.type === ArmorType.Core) {
return acc; return acc;
@@ -292,25 +295,25 @@ export function SpacedArmorSceneComponent({
*/ */
if ( if (
shot.layersIn.length > 1 || shot.in.layers.length > 1 ||
lastLayer.status === 'blocked' lastLayer.status === 'blocked'
) { ) {
shot.status = finalDamage > 0 ? 'splash' : 'blocked'; shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
shot.damage = finalDamage; shot.damage = finalDamage;
} else { } else {
shot.status = 'penetration'; shot.in.status = 'penetration';
shot.damage = shell.damage.armor; shot.damage = shell.damage.armor;
} }
} else { } else {
if (lastLayer.status === 'blocked') { if (lastLayer.status === 'blocked') {
shot.status = 'blocked'; shot.in.status = 'blocked';
shot.damage = 0; shot.damage = 0;
} else if (lastLayer.status === 'penetration') { } else if (lastLayer.status === 'penetration') {
shot.status = 'penetration'; shot.in.status = 'penetration';
shot.damage = shell.damage.armor; shot.damage = shell.damage.armor;
} else { } else {
// check second array for result // check second array for result
shot.status = 'ricochet'; shot.in.status = 'ricochet';
} }
} }
+12 -3
View File
@@ -75,14 +75,23 @@ export interface ShotLayerGap {
export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap; export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap;
export type Shot = { export type Shot = {
status: 'penetration' | 'blocked' | 'ricochet' | 'splash';
containsGaps: boolean; containsGaps: boolean;
damage: number; damage: number;
point: Vector3; point: Vector3;
layersIn: ShotLayer[];
layersOut?: ShotLayer[]; in: {
cameraNormal: Vector3;
status: ShotStatus;
layers: ShotLayer[];
};
out?: {
status: ShotStatus;
layers: ShotLayer[];
};
}; };
export type ShotStatus = 'penetration' | 'blocked' | 'ricochet' | 'splash';
export type ArmorPiercingLayer = export type ArmorPiercingLayer =
| { | {
distance: number; distance: number;