From 7f8797a4494204de35dfb5299b068dc50a97cf13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 26 May 2024 13:42:51 -0500 Subject: [PATCH] feat: shot display --- .../Armor/components/ShotDisplay.tsx | 88 ++++++++++++++++--- .../SpacedArmorSceneComponent/index.tsx | 45 +++++----- src/stores/tankopedia.ts | 15 +++- 3 files changed, 113 insertions(+), 35 deletions(-) diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index 0a5442730..d16cde3b7 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -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 { ComponentProps } from 'react'; -import { Shot, useTankopediaTemporary } from '../../../stores/tankopedia'; +import { useFrame } from '@react-three/fiber'; +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 { ExternalModuleVariant } from './SpacedArmorSceneComponent'; @@ -17,7 +20,7 @@ const externalLayerNames: Record = { track: 'Tracks', }; -const shotStatusNames: Record = { +const shotStatusNames: Record = { blocked: 'Blocked', penetration: 'penetration', ricochet: 'ricochet', @@ -25,7 +28,7 @@ const shotStatusNames: Record = { }; const shotStatusColors: Record< - Shot['status'], + ShotStatus, ComponentProps['color'] > = { blocked: 'red', @@ -40,13 +43,31 @@ const TRACER_THIN = TRACER_THICK / 2; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); + const inTracer = useRef(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; + 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 ( - - {shot.status !== 'blocked' && ( + + {shot.in.status !== 'blocked' && ( <> {Math.round(shot.damage).toLocaleString()} hp )} - {` ${shotStatusNames[shot.status]} `} + {` ${shotStatusNames[shot.in.status]} `} - {shot.layersIn.map((layer, index) => { + {shot.in.layers.map((layer, index) => { const layerName = layer.type === ArmorType.External ? externalLayerNames[layer.variant] @@ -74,7 +95,10 @@ export function ShotDisplay() { layer.type === null ? null : `${(shot.containsGaps ? index / 2 : index) + 1}.`; - const shotStatusColor = shotStatusColors[layer.status]; + const shotStatusColor = + shot.in.status === 'splash' + ? shotStatusColors.splash + : shotStatusColors[layer.status]; return ( @@ -155,6 +179,48 @@ export function ShotDisplay() { + + {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 ( + + + {layerIndex} + + + ); + })} + + + + + + + + + + + + ); } diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index e847f94c2..3087c6fa9 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -95,17 +95,20 @@ export function SpacedArmorSceneComponent({ const { antagonist, protagonist } = useDuel.getState(); const shell = antagonist!.shell; - const shot: Shot = { - status: 'blocked', - damage: -1, - containsGaps: shell.type === ShellType.HEAT, - layersIn: [], - point: event.point, - }; const cameraNormal = camera.position .clone() .sub(event.point) .normalize(); + const shot: Shot = { + damage: -1, + containsGaps: shell.type === ShellType.HEAT, + in: { + cameraNormal, + layers: [], + status: 'blocked', + }, + point: event.point, + }; const hasCalibratedShells = await hasEquipment( 103, @@ -157,14 +160,14 @@ export function SpacedArmorSceneComponent({ const layer = intersection.object.userData; 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( intersection.point, ); remainingPenetration -= 0.5 * remainingPenetration * distance; const blocked = remainingPenetration <= 0; - shot.layersIn.push({ + shot.in.layers.push({ type: null, distance, status: blocked ? 'blocked' : 'penetration', @@ -179,7 +182,7 @@ export function SpacedArmorSceneComponent({ const blocked = remainingPenetration <= 0 || shell.type === ShellType.HE; - shot.layersIn.push({ + shot.in.layers.push({ type: ArmorType.External, index, thickness, @@ -213,7 +216,7 @@ export function SpacedArmorSceneComponent({ if (!threeCalibersRule && angle >= ricochet) { remainingPenetration *= 0.75; - shot.layersIn.push({ + shot.in.layers.push({ type: layer.type, index, thickness, @@ -229,7 +232,7 @@ export function SpacedArmorSceneComponent({ remainingPenetration -= finalThickness; const blocked = remainingPenetration <= 0; - shot.layersIn.push({ + shot.in.layers.push({ type: layer.type, index, thickness, @@ -249,11 +252,11 @@ export function SpacedArmorSceneComponent({ index++; } - const lastLayer = shot.layersIn.at(-1) as ShotLayerNonExternal; - const firstLayer = shot.layersIn[0] as ShotLayerBase; + const lastLayer = shot.in.layers.at(-1) as ShotLayerNonExternal; + const firstLayer = shot.in.layers[0] as ShotLayerBase; if (shell.type === ShellType.HE) { - const totalSpacedArmorThickness = shot.layersIn.reduce( + const totalSpacedArmorThickness = shot.in.layers.reduce( (acc, layer) => { if (layer.type === null || layer.type === ArmorType.Core) { return acc; @@ -292,25 +295,25 @@ export function SpacedArmorSceneComponent({ */ if ( - shot.layersIn.length > 1 || + shot.in.layers.length > 1 || lastLayer.status === 'blocked' ) { - shot.status = finalDamage > 0 ? 'splash' : 'blocked'; + shot.in.status = finalDamage > 0 ? 'splash' : 'blocked'; shot.damage = finalDamage; } else { - shot.status = 'penetration'; + shot.in.status = 'penetration'; shot.damage = shell.damage.armor; } } else { if (lastLayer.status === 'blocked') { - shot.status = 'blocked'; + shot.in.status = 'blocked'; shot.damage = 0; } else if (lastLayer.status === 'penetration') { - shot.status = 'penetration'; + shot.in.status = 'penetration'; shot.damage = shell.damage.armor; } else { // check second array for result - shot.status = 'ricochet'; + shot.in.status = 'ricochet'; } } diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index b38b6b541..9610c97ff 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -75,14 +75,23 @@ export interface ShotLayerGap { export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap; export type Shot = { - status: 'penetration' | 'blocked' | 'ricochet' | 'splash'; containsGaps: boolean; damage: number; 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 = | { distance: number;