From c2e550e33d8a7ac20cec77b636823687d41e7b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:08:41 -0600 Subject: [PATCH] feat: on click display --- .../[id]/components/Model/TankSandbox.tsx | 13 +- .../components/Model/components/TankModel.tsx | 9 ++ .../ArmorMesh/components/Armor/index.tsx | 113 ++++++++++++++++-- src/components/ShotDisplay.tsx | 43 +++++++ src/stores/tankopedia.ts | 22 ++++ 5 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 src/components/ShotDisplay.tsx diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index 17bbdfae3..2123cfc14 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -3,6 +3,7 @@ import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes'; import { PerspectiveCamera } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; import { Suspense, use, useEffect, useRef, useState } from 'react'; +import { ShotDisplay } from '../../../../../../components/ShotDisplay'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; @@ -166,12 +167,22 @@ export function TankSandbox() {
{loadModel ? ( - + { + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); + }} + > }> + diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx index 8c959d386..f7539c514 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -144,6 +144,9 @@ export const TankModel = memo(() => { mutateTankopediaPersistent((draft) => { draft.model.visual.controlsEnabled = false; }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); @@ -208,6 +211,9 @@ export const TankModel = memo(() => { mutateTankopediaPersistent((draft) => { draft.model.visual.controlsEnabled = false; }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); } @@ -272,6 +278,9 @@ export const TankModel = memo(() => { mutateTankopediaPersistent((draft) => { draft.model.visual.controlsEnabled = false; }); + mutateTankopediaTemporary((draft) => { + draft.shot = undefined; + }); pitch = physical.pitch; yaw = physical.yaw; window.addEventListener('pointermove', handlePointerMove); diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx index ab7e0a246..5376cf755 100644 --- a/src/components/ArmorMesh/components/Armor/index.tsx +++ b/src/components/ArmorMesh/components/Armor/index.tsx @@ -1,14 +1,16 @@ import { MeshProps, useFrame, useThree } from '@react-three/fiber'; import { useEffect, useRef } from 'react'; -import { IUniform, ShaderMaterial, Vector2 } from 'three'; +import { IUniform, Mesh, ShaderMaterial, Vector2, Vector3 } from 'three'; import { degToRad } from 'three/src/math/MathUtils'; +import { I_HAT } from '../../../../constants/axis'; import { canSplash } from '../../../../core/blitz/canSplash'; import { isExplosive } from '../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { useDuel } from '../../../../stores/duel'; import { + ArmorPiercingLayer, + mutateTankopediaTemporary, useTankopediaPersistent, - useTankopediaTemporary, } from '../../../../stores/tankopedia'; import { externalModuleMaskRenderTarget } from '../ExternalModuleMask'; import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth'; @@ -35,6 +37,10 @@ export function ArmorMesh({ const initialTankopedia = useTankopediaPersistent.getState(); const material = useRef(null); const explosionCapable = isExplosive(initialShell.type); + const cameraNormal = new Vector3(); + const shellNormal = new Vector3(); + const surfaceNormal = new Vector3(); + const mesh = useRef(null); useEffect(() => { function updateQuickEquipments() { @@ -126,15 +132,106 @@ export function ArmorMesh({ thickness, } satisfies ArmorMeshUserData } + ref={mesh} onClick={(event) => { - if (useTankopediaTemporary.getState().mode !== 'armor') return; + // if (useTankopediaTemporary.getState().mode !== 'armor') return; - event.intersections + shellNormal.copy( + cameraNormal.copy(camera.position).sub(event.point).normalize(), + ); + surfaceNormal.copy(event.normal!).applyAxisAngle(I_HAT, -Math.PI / 2); + const shell = useDuel.getState().antagonist!.shell; + const intersections = event.intersections .filter(({ object }) => Boolean(object.userData.type)) - .map(({ object }) => { - const data = object.userData as ArmorMeshUserData; - console.log(data.type, data.thickness); - }); + .map( + (event) => + event as typeof event & { + object: (typeof event)['object'] & { + userData: ArmorMeshUserData; + }; + }, + ); + const intersectionsTillFirstCoreArmor = intersections.slice( + 0, + intersections.findIndex( + ({ object }) => object.userData.type === 'coreArmor', + ) + 1, + ); + const coreArmorMesh = intersectionsTillFirstCoreArmor.at(-1)!.object; + + if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return; + + const armorPiercingLayers = intersectionsTillFirstCoreArmor.map( + (event, index) => { + switch (event.object.userData.type) { + case 'spacedArmor': { + return { + nominal: event.object.userData.thickness, + angled: event.object.userData.thickness, + ricochet: false, + } satisfies ArmorPiercingLayer; + } + + case 'externalModule': + case 'coreArmor': { + const angle = Math.acos(surfaceNormal.dot(shellNormal)); + const threeCalibersRule = + shell.caliber > 3 * event.object.userData.thickness; + let hasRicochet = false; + + if ( + index === 0 && + !isExplosive(shell.type) && + !threeCalibersRule && + angle >= degToRad(shell.ricochet!) + ) { + hasRicochet = true; + } + + const twoCalibersRule = + shell.caliber > 2 * event.object.userData.thickness; + const normalization = twoCalibersRule + ? ((shell.normalization ?? 0) * 1.4 * shell.caliber) / + (2.0 * thickness) + : shell.normalization ?? 0; + const angledThickness = + thickness / Math.cos(angle - normalization); + + return { + nominal: event.object.userData.thickness, + angled: angledThickness, + ricochet: hasRicochet, + } satisfies ArmorPiercingLayer; + } + } + }, + ); + + const hasRicochet = armorPiercingLayers.some( + ({ ricochet }) => ricochet, + ); + const accumulatedThickness = armorPiercingLayers.reduce( + (sum, { angled }) => sum + angled, + 0, + ); + const hasPenetrated = + resolveNearPenetration(shell.penetration) >= accumulatedThickness; + + mutateTankopediaTemporary((draft) => { + draft.shot = { + type: hasRicochet + ? 'ricochet' + : hasPenetrated + ? 'penetration' + : 'block', + point: event.point.toArray(), + thicknesses: armorPiercingLayers, + shellNormal: shellNormal.toArray(), + surfaceNormal: surfaceNormal.toArray(), + }; + + console.log(draft.shot); + }); }} > state.shot); + const wrapper = useRef(null); + + useEffect(() => { + if (!shot) return; + + const point = new Vector3(...shot.point); + const shotNormal = new Vector3(...shot.shellNormal); + + wrapper.current?.position.set(...shot.point); + wrapper.current?.lookAt(point.clone().add(shotNormal)); + }, [shot]); + + if (!shot) return null; + + return ( + + + + + {SHOT_NAMES[shot.type]} + + + + + + + {/* */} + + + + ); +} diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 5343690b1..daf61c8a2 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -1,4 +1,5 @@ import { produce } from 'immer'; +import { Vector3Tuple } from 'three'; import { create } from 'zustand'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { TankType, TreeType } from '../components/Tanks'; @@ -63,6 +64,27 @@ interface TankopediaTemporary { consumables: number[]; provisions: number[]; camouflage: boolean; + shot?: Shot; +} + +type Shot = { + point: Vector3Tuple; + surfaceNormal: Vector3Tuple; + shellNormal: Vector3Tuple; + thicknesses: ArmorPiercingLayer[]; + type: 'ricochet' | 'penetration' | 'block'; +}; + +export const SHOT_NAMES: Record = { + ricochet: 'Ricochet', + penetration: 'Penetration', + block: 'Blocked', +}; + +export interface ArmorPiercingLayer { + nominal: number; + angled: number; + ricochet: boolean; } export const useTankopediaPersistent = create()(