diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx index 1b5569249..14a8d0a9c 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx @@ -111,6 +111,9 @@ export function Options() { mutateDuel((draft) => { draft.antagonist!.shell = thisShell; }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); }} > { + draft.shot = undefined; + }); }} > { + draft.shot = undefined; + }); }} > { - equipment: number; - active: boolean; -} - -export function QuickEquipmentButton({ - equipment, - active, - ...props -}: QuickEquipmentButtonProps) { - return ( - - - - ); -} diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index c8ea82b80..0a5442730 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,27 +1,9 @@ import { Card, Flex, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; -import { useFrame } from '@react-three/fiber'; -import { useEffect, useRef } from 'react'; -import { Mesh } from 'three'; -import { radToDeg } from 'three/src/math/MathUtils'; -import { canSplash } from '../../../core/blitz/canSplash'; -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'; -import { Ricochet } from '../../../icons/Ricochet'; -import { useDuel } from '../../../stores/duel'; -import { - ShotLayer, - ShotLayerBase, - ShotLayerGap, - ShotLayerNonExternal, - mutateTankopediaTemporary, - useTankopediaTemporary, -} from '../../../stores/tankopedia'; +import { ComponentProps } from 'react'; +import { Shot, useTankopediaTemporary } from '../../../stores/tankopedia'; import { ArmorType } from './SpacedArmorScene'; +import { ExternalModuleVariant } from './SpacedArmorSceneComponent'; const layerTypeNames: Record = { [ArmorType.Core]: 'Core', @@ -30,305 +12,149 @@ const layerTypeNames: Record = { null: 'Gap', }; +const externalLayerNames: Record = { + gun: 'Gun', + track: 'Tracks', +}; + +const shotStatusNames: Record = { + blocked: 'Blocked', + penetration: 'penetration', + ricochet: 'ricochet', + splash: 'splash', +}; + +const shotStatusColors: Record< + Shot['status'], + ComponentProps['color'] +> = { + blocked: 'red', + penetration: 'green', + ricochet: 'yellow', + splash: 'orange', +}; + const LENGTH_INFINITY = 4; const TRACER_THICK = 1 / 64; const TRACER_THIN = TRACER_THICK / 2; export function ShotDisplay() { - return null; - const shot = useTankopediaTemporary((state) => state.shot); - const preMidPointTracer = useRef(null); - const postMidPointTracer = useRef(null); - const hasMultipleLayers = shot?.layersIn.some( - (layer, index) => index !== 0 && layer.type !== null, - ); - const { shell } = useDuel.getState().antagonist!; - const explosive = isExplosive(shell.type); - const splashing = canSplash(shell.type); - const midPointLayer = (shot?.layersIn.findLast( - (layer) => layer.type !== null && layer.status === 'ricochet', - ) ?? - shot?.layersIn.findLast((layer) => layer.type !== null)) as ShotLayerBase; - const firstLayer = shot?.layersIn[0] as Exclude; - const lastLayer = shot?.layersIn.at(-1)!; - const distanceFromSpacedArmor = shot - ? firstLayer.point.distanceTo(shot.point) - : undefined; - const preMidPointLayers = shot?.layersIn.slice( - 0, - shot.layersIn.indexOf(midPointLayer as ShotLayer), - ); - const postMidPointLayers = shot?.layersIn.slice( - shot.layersIn.indexOf(midPointLayer as ShotLayer), - shot.layersIn.length, - ); - const preMidPointGap = shot - ? preMidPointLayers!.reduce((accumulator, layer) => { - if (layer.type === null) return accumulator + layer.distance; - return accumulator; - }, 0) + LENGTH_INFINITY - : undefined; - const preMidPointThickness = shot - ? preMidPointLayers!.reduce( - (accumulator, layer) => - accumulator + - (layer.type === null - ? 0 - : layer.type === ArmorType.External - ? layer.thickness - : layer.thicknessAngled), - 0, - ) - : undefined; - const postMidPointGap = shot - ? postMidPointLayers!.reduce((accumulator, layer) => { - if (layer.type === null) return accumulator + layer.distance; - return accumulator; - }, 0) || LENGTH_INFINITY - : undefined; - const spacedBlockedHE = - shell.type === 'he' && postMidPointGap && postMidPointGap > 0; - - 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 * postMidPointGap!, 0); - postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1); - }); - - useEffect(() => { - function resetShot() { - mutateTankopediaTemporary((draft) => { - draft.shot = undefined; - }); - } - - const unsubscribes = [ - useDuel.subscribe((state) => state.protagonist?.tank, resetShot), - useDuel.subscribe((state) => state.antagonist?.tank, resetShot), - ]; - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); if (!shot) return null; return ( - <> - - - - - - - - - - - - - {midPointLayer.status === 'ricochet' && ( - - - - - - - - - - - - )} - - {shot.layersIn.map((layer, index) => { - if (layer.type === null) return; - - const shellRotation = normalToEuler(layer.shellNormal); - const nextLayer = shot.layersIn[index + 1] as ShotLayerGap | undefined; - const trueStatus = - nextLayer?.status === 'blocked' ? 'blocked' : layer.status; - - return ( - <> - - - - {hasMultipleLayers && ( - - {layer.index + 1} - - )} - - - - > - ); - })} - + - - - - {lastLayer.status[0].toUpperCase()} - {lastLayer.status.slice(1)} - - {lastLayer.status === 'penetration' && - !spacedBlockedHE && - `: ${Math.round(shell.damage.armor)}hp`} - - {(lastLayer.status === 'blocked' || spacedBlockedHE) && - shell.type === 'he' && - `: ${NaNFallback( - Math.max( - 0, - Math.round( - 0.5 * - shell.damage.armor * - (1 - - distanceFromSpacedArmor! / shell.explosionRadius!) - - 1.1 * - ((midPointLayer as ShotLayerNonExternal) - .thicknessAngled + - preMidPointThickness!), - ), - ), - 0, - )}hp`} + + + + {shot.status !== 'blocked' && ( + <> + {Math.round(shot.damage).toLocaleString()} + hp + > + )} + {` ${shotStatusNames[shot.status]} `} - {shot.layersIn.map((layer, index) => { - if (layer.type === null && (!explosive || splashing)) return null; + + {shot.layersIn.map((layer, index) => { + const layerName = + layer.type === ArmorType.External + ? externalLayerNames[layer.variant] + : layerTypeNames[`${layer.type}`]; + const layerIndex = + layer.type === null + ? null + : `${(shot.containsGaps ? index / 2 : index) + 1}.`; + const shotStatusColor = shotStatusColors[layer.status]; - return ( - - - {hasMultipleLayers && ( - - {layer.type === null ? null : `${layer.index + 1}.`} + return ( + + + {layerIndex} + + + {layerName} + + + {layer.type === null && ( + <> + + {Math.round(layer.distance * 1000).toLocaleString()} + mm + + + + {Math.max(-100, Math.round(-50 * layer.distance))}% + penetration + + > + )} + + {layer.type === ArmorType.External && ( + + {Math.round(layer.thickness).toLocaleString()} + mm )} - {layerTypeNames[`${layer.type}`]} + + {(layer.type === ArmorType.Core || + layer.type === ArmorType.Spaced) && ( + <> + + {Math.round(layer.thicknessAngled).toLocaleString()} + mm + + + + {Math.round(layer.thickness).toLocaleString()} + mm nominal + + > + )} - - {layer.type !== null && ( - <> - - - {Math.round(layer.thickness)}mm - - - {layer.type !== ArmorType.External && ( - - {layer.status === 'ricochet' ? ( - - ) : layer.status === 'blocked' ? ( - - ) : ( - - )} - - {Math.round(layer.thicknessAngled)}mm ( - {Math.round(radToDeg(layer.angle))}°) - - - )} - > - )} - - {layer.type === null && ( - - {Math.round(layer.distance * 1000)}mm (- - {Math.round(Math.min(100, layer.distance * 50))}% - penetration) - - )} - - ); - })} + ); + })} + - > + ); } diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index bfdce108b..e847f94c2 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -93,14 +93,15 @@ export function SpacedArmorSceneComponent({ async onClick(event) { event.stopPropagation(); + 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 { antagonist, protagonist } = useDuel.getState(); - const shell = antagonist!.shell; const cameraNormal = camera.position .clone() .sub(event.point) @@ -185,6 +186,7 @@ export function SpacedArmorSceneComponent({ point: intersection.point, surfaceNormal: intersection.face!.normal, status: blocked ? 'blocked' : 'penetration', + variant: layer.variant, }); } else { const thickness = layer.thickness; diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 9a4322d6b..b38b6b541 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -5,6 +5,7 @@ import { create } from 'zustand'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting'; import { ArmorType } from '../components/Armor/components/SpacedArmorScene'; +import { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent'; import { TankClass, TreeType } from '../components/Tanks'; import { Tier } from '../core/blitzkit/tankDefinitions'; @@ -56,6 +57,7 @@ export interface ShotLayerBase { interface ShotLayerExternal extends ShotLayerBase { type: ArmorType.External; + variant: ExternalModuleVariant; } export interface ShotLayerNonExternal extends ShotLayerBase { @@ -74,6 +76,7 @@ export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap; export type Shot = { status: 'penetration' | 'blocked' | 'ricochet' | 'splash'; + containsGaps: boolean; damage: number; point: Vector3; layersIn: ShotLayer[];