feat: on click display

This commit is contained in:
TrèsAbhi committed 2024-02-02 16:08:41 -06:00
1 parent b01a03c6ce
commit c2e550e33d
5 files changed
+191 -9

No files matched your search

@@ -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() {
<div style={{ height: '100%' }}>
{loadModel ? (
<Canvas shadows ref={canvas} onPointerDown={handlePointerDown}>
<Canvas
shadows
ref={canvas}
onPointerDown={handlePointerDown}
onPointerMissed={() => {
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
>
<PerspectiveCamera makeDefault fov={25} far={32} />
<Controls />
<SceneProps />
<Suspense fallback={<ModelLoader />}>
<ShotDisplay />
<Lighting />
<TankModel />
<TankArmor />
@@ -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);
@@ -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<ShaderMaterial>(null);
const explosionCapable = isExplosive(initialShell.type);
const cameraNormal = new Vector3();
const shellNormal = new Vector3();
const surfaceNormal = new Vector3();
const mesh = useRef<Mesh>(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);
});
}}
>
<shaderMaterial
+43
View File
@@ -0,0 +1,43 @@
import { Card, Flex, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useEffect, useRef } from 'react';
import { Group, Vector3 } from 'three';
import { SHOT_NAMES, useTankopediaTemporary } from '../stores/tankopedia';
const THICKNESS = 0.05;
const LENGTH = 2;
export function ShotDisplay() {
const shot = useTankopediaTemporary((state) => state.shot);
const wrapper = useRef<Group>(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 (
<group ref={wrapper}>
<Html position={[0, 0, LENGTH]}>
<Card>
<Flex direction="column">
<Text>{SHOT_NAMES[shot.type]}</Text>
</Flex>
</Card>
</Html>
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} />
{/* <boxGeometry args={[THICKNESS, THICKNESS, LENGTH]} /> */}
<meshBasicMaterial color="white" depthTest={false} />
</mesh>
</group>
);
}
+22
View File
@@ -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<Shot['type'], string> = {
ricochet: 'Ricochet',
penetration: 'Penetration',
block: 'Blocked',
};
export interface ArmorPiercingLayer {
nominal: number;
angled: number;
ricochet: boolean;
}
export const useTankopediaPersistent = create<TankopediaPersistent>()(