mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: shot display
This commit is contained in:
1 parent
69c388f243
commit
f55313e0cd
10 files changed
+333
-351
No files matched your search
@@ -5,6 +5,7 @@ import { Canvas } from '@react-three/fiber';
|
|||||||
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
||||||
import { isMobile } from 'react-device-detect';
|
import { isMobile } from 'react-device-detect';
|
||||||
import { Armor } from '../../../../../../components/Armor';
|
import { Armor } from '../../../../../../components/Armor';
|
||||||
|
import { ShotScene } from '../../../../../../components/Armor/components/ShotScene';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
||||||
@@ -195,7 +196,7 @@ export function TankSandbox() {
|
|||||||
<SceneProps />
|
<SceneProps />
|
||||||
|
|
||||||
<Suspense fallback={<ModelLoader />}>
|
<Suspense fallback={<ModelLoader />}>
|
||||||
{/* <ShotDisplay /> */}
|
{/* TODO: add rotation cube to the top right */}
|
||||||
{/* <SpacedArmorScene /> */}
|
{/* <SpacedArmorScene /> */}
|
||||||
<Lighting />
|
<Lighting />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Text } from '@radix-ui/themes';
|
||||||
|
import { Html } from '@react-three/drei';
|
||||||
|
import { DoubleSide, Euler, Quaternion, Vector3 } from 'three';
|
||||||
|
import { J_HAT } from '../../../constants/axis';
|
||||||
|
import {
|
||||||
|
ShotLayerGap,
|
||||||
|
useTankopediaTemporary,
|
||||||
|
} from '../../../stores/tankopedia';
|
||||||
|
|
||||||
|
// export const SHOT_NAMES: Record<Shot['type'], string> = {
|
||||||
|
// ricochet: 'Ricochet',
|
||||||
|
// penetration: 'Penetration',
|
||||||
|
// block: 'Blocked',
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export const SHOT_ICONS: Record<Shot['type'], ReactNode> = {
|
||||||
|
// ricochet: <Ricochet width={24} height={24} />,
|
||||||
|
// block: <Block width={24} height={24} />,
|
||||||
|
// penetration: <NominalPenetration width={24} height={24} />,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const THICKNESS = 0.05;
|
||||||
|
// const LENGTH = 4;
|
||||||
|
|
||||||
|
export function ShotScene() {
|
||||||
|
const shot = useTankopediaTemporary((state) => state.shot);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{shot?.map((layer, index) => {
|
||||||
|
if (layer.type === null) return;
|
||||||
|
|
||||||
|
const shellRotation = new Euler().setFromQuaternion(
|
||||||
|
new Quaternion().setFromAxisAngle(
|
||||||
|
new Vector3().crossVectors(J_HAT, layer.shellNormal).normalize(),
|
||||||
|
J_HAT.angleTo(layer.shellNormal),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<mesh renderOrder={1} key={`${index}-point`} position={layer.point}>
|
||||||
|
<icosahedronGeometry args={[1 / 16, 1]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
side={DoubleSide}
|
||||||
|
color={
|
||||||
|
layer.status === 'penetration'
|
||||||
|
? 0x00ff00
|
||||||
|
: layer.status === 'blocked'
|
||||||
|
? 0xff0000
|
||||||
|
: 0xffff00
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<group
|
||||||
|
key={`${index}-line`}
|
||||||
|
position={layer.point}
|
||||||
|
rotation={shellRotation}
|
||||||
|
>
|
||||||
|
{shot.length > 1 && (
|
||||||
|
<Html>
|
||||||
|
<Text>{layer.index + 1}</Text>
|
||||||
|
</Html>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
let length = 4;
|
||||||
|
|
||||||
|
if (layer.index !== 0) {
|
||||||
|
const gap = shot[index - 1] as ShotLayerGap;
|
||||||
|
length = gap.distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh position={[0, length / 2, 0]} renderOrder={0}>
|
||||||
|
<cylinderGeometry args={[1 / 64, 1 / 64, length]} />
|
||||||
|
<meshBasicMaterial color={0xffffff} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { memo, useEffect, useRef } from 'react';
|
import { memo, useEffect, useRef } from 'react';
|
||||||
import { Euler, Group, Vector3 } from 'three';
|
import { Euler, Group, Scene, Vector3 } from 'three';
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis';
|
import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis';
|
||||||
import {
|
import {
|
||||||
@@ -21,7 +21,11 @@ export enum ArmorType {
|
|||||||
External,
|
External,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpacedArmorScene = memo(() => {
|
interface SpacedArmorSceneProps {
|
||||||
|
scene: Scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const modelDefinitions = useModelDefinitions();
|
const modelDefinitions = useModelDefinitions();
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
@@ -162,6 +166,7 @@ export const SpacedArmorScene = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
@@ -179,6 +184,7 @@ export const SpacedArmorScene = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
type={ArmorType.External}
|
type={ArmorType.External}
|
||||||
thickness={trackModelDefinition.thickness}
|
thickness={trackModelDefinition.thickness}
|
||||||
@@ -206,6 +212,7 @@ export const SpacedArmorScene = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<group key={node.uuid} position={turretOrigin}>
|
<group key={node.uuid} position={turretOrigin}>
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
@@ -236,6 +243,7 @@ export const SpacedArmorScene = memo(() => {
|
|||||||
position={turretOrigin.clone().add(gunOrigin)}
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
>
|
>
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -255,6 +263,7 @@ export const SpacedArmorScene = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SpacedArmorSceneComponent
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
type={ArmorType.External}
|
type={ArmorType.External}
|
||||||
thickness={gunModelDefinition.thickness}
|
thickness={gunModelDefinition.thickness}
|
||||||
|
|||||||
+2
-1
@@ -12,6 +12,7 @@ import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
|||||||
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
|
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
|
||||||
import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel';
|
import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel';
|
||||||
|
import { ArmorType } from '../../../SpacedArmorScene';
|
||||||
import fragmentShader from './shaders/fragment.glsl';
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
import vertexShader from './shaders/vertex.glsl';
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ export function SpacedArmorSubExternal({
|
|||||||
}),
|
}),
|
||||||
onClick() {},
|
onClick() {},
|
||||||
userData: {
|
userData: {
|
||||||
type: 'external',
|
type: ArmorType.External,
|
||||||
thickness,
|
thickness,
|
||||||
variant,
|
variant,
|
||||||
} satisfies ArmorUserData,
|
} satisfies ArmorUserData,
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@ import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
|||||||
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
|
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
|
||||||
import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel';
|
import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel';
|
||||||
|
import { ArmorType } from '../../../SpacedArmorScene';
|
||||||
import fragmentShader from './shaders/fragment.glsl';
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
import vertexShader from './shaders/vertex.glsl';
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ export function SpacedArmorSubSpaced({
|
|||||||
material,
|
material,
|
||||||
onClick() {},
|
onClick() {},
|
||||||
userData: {
|
userData: {
|
||||||
type: 'spaced',
|
type: ArmorType.Spaced,
|
||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData,
|
} satisfies ArmorUserData,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from '@react-three/fiber';
|
||||||
import { Intersection, MeshBasicMaterial, Object3D, Vector3 } from 'three';
|
import {
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
Intersection,
|
||||||
import { I_HAT } from '../../../../constants/axis';
|
MeshBasicMaterial,
|
||||||
|
Object3D,
|
||||||
|
Quaternion,
|
||||||
|
Raycaster,
|
||||||
|
Scene,
|
||||||
|
Vector3,
|
||||||
|
} from 'three';
|
||||||
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
|
||||||
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||||
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
||||||
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
|
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
|
||||||
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
||||||
import { useDuel } from '../../../../stores/duel';
|
import { useDuel } from '../../../../stores/duel';
|
||||||
|
import { Shot, useTankopediaTemporary } from '../../../../stores/tankopedia';
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
import { ArmorType } from '../SpacedArmorScene';
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
||||||
@@ -14,6 +22,7 @@ import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
|||||||
type SpacedArmorSceneComponentProps = {
|
type SpacedArmorSceneComponentProps = {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
|
scene: Scene;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
@@ -29,15 +38,14 @@ export type ArmorUserData = {
|
|||||||
thickness: number;
|
thickness: number;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: 'core' | 'spaced';
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'external';
|
type: ArmorType.External;
|
||||||
variant: ExternalModuleVariant;
|
variant: ExternalModuleVariant;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
type ArmorMeshIntersection = Intersection & {
|
type ArmorMeshIntersection = Intersection & {
|
||||||
eventObject: Object3D;
|
|
||||||
object: Object3D & { userData: ArmorUserData };
|
object: Object3D & { userData: ArmorUserData };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,6 +64,7 @@ const omitMaterial = new MeshBasicMaterial({
|
|||||||
export function SpacedArmorSceneComponent({
|
export function SpacedArmorSceneComponent({
|
||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
|
scene,
|
||||||
...props
|
...props
|
||||||
}: SpacedArmorSceneComponentProps) {
|
}: SpacedArmorSceneComponentProps) {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
@@ -68,53 +77,13 @@ export function SpacedArmorSceneComponent({
|
|||||||
renderOrder: 0,
|
renderOrder: 0,
|
||||||
material: omitMaterial,
|
material: omitMaterial,
|
||||||
userData: {
|
userData: {
|
||||||
type: 'core',
|
type: ArmorType.Core,
|
||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData,
|
} satisfies ArmorUserData,
|
||||||
|
|
||||||
async onClick(event) {
|
async onClick(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const intersectionsAll = event.intersections.filter(
|
|
||||||
({ object }) => object.userData.type !== undefined,
|
|
||||||
) as ArmorMeshIntersection[];
|
|
||||||
const intersectionsWithDuplicates = intersectionsAll.slice(
|
|
||||||
0,
|
|
||||||
intersectionsAll.findIndex(
|
|
||||||
({ object }) => object.userData.type === 'core',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const firstGunIntersectionIndex =
|
|
||||||
intersectionsWithDuplicates.findIndex(
|
|
||||||
({ object }) =>
|
|
||||||
object.userData.type === 'external' &&
|
|
||||||
object.userData.variant === 'gun',
|
|
||||||
);
|
|
||||||
const firstTrackIntersectionIndex =
|
|
||||||
intersectionsWithDuplicates.findIndex(
|
|
||||||
({ object }) =>
|
|
||||||
object.userData.type === 'external' &&
|
|
||||||
object.userData.variant === 'track',
|
|
||||||
);
|
|
||||||
let intersections = intersectionsWithDuplicates;
|
|
||||||
|
|
||||||
if (firstGunIntersectionIndex !== -1) {
|
|
||||||
intersections = intersections.filter(({ object }, index) => {
|
|
||||||
return object.userData.type === 'external' &&
|
|
||||||
object.userData.variant === 'gun'
|
|
||||||
? index === firstGunIntersectionIndex
|
|
||||||
: true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (firstTrackIntersectionIndex !== -1) {
|
|
||||||
intersections = intersections.filter(({ object }, index) => {
|
|
||||||
return object.userData.type === 'external' &&
|
|
||||||
object.userData.variant === 'track'
|
|
||||||
? index === firstTrackIntersectionIndex
|
|
||||||
: true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasCalibratedShells = await hasEquipment(103, true);
|
const hasCalibratedShells = await hasEquipment(103, true);
|
||||||
const hasEnhancedArmor = await hasEquipment(110);
|
const hasEnhancedArmor = await hasEquipment(110);
|
||||||
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
||||||
@@ -126,73 +95,189 @@ export function SpacedArmorSceneComponent({
|
|||||||
? 1.1
|
? 1.1
|
||||||
: 1.05
|
: 1.05
|
||||||
: 1);
|
: 1);
|
||||||
const shotLog: ({
|
const shot: Shot = []; // TODO: type this
|
||||||
thickness: number;
|
|
||||||
status: 'blocked' | 'penetration' | 'ricochet';
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: ArmorType.External;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
|
||||||
thicknessAngled: number;
|
|
||||||
}
|
|
||||||
))[] = []; // TODO: type this
|
|
||||||
let remainingPenetration = penetration;
|
let remainingPenetration = penetration;
|
||||||
let penetrationChance = -1;
|
let penetrationChance = -1;
|
||||||
let splashChance = -1;
|
let splashChance = -1;
|
||||||
|
|
||||||
camera.getWorldDirection(cameraNormal).multiplyScalar(-1);
|
const shellNormal = camera.position
|
||||||
|
.clone()
|
||||||
|
.sub(event.point)
|
||||||
|
.normalize();
|
||||||
|
shellNormal
|
||||||
|
.copy(camera.getWorldDirection(cameraNormal))
|
||||||
|
.multiplyScalar(-1);
|
||||||
|
pushLogs(
|
||||||
|
shellNormal,
|
||||||
|
filterIntersections(event.intersections),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
let index = 0;
|
console.clear();
|
||||||
for (const intersection of intersections) {
|
console.log(
|
||||||
const thickness =
|
JSON.stringify(
|
||||||
thicknessCoefficient * intersection.object.userData.thickness;
|
shot.map((i) => ({
|
||||||
|
...i,
|
||||||
|
...(i.angle ? { angle: radToDeg(i.angle) } : {}),
|
||||||
|
})),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (intersection.object.userData.type === 'external') {
|
function filterIntersections(intersectionsRaw: Intersection[]) {
|
||||||
remainingPenetration -= thickness;
|
const intersectionsAll = intersectionsRaw.filter(
|
||||||
const blocked = remainingPenetration < 0;
|
({ object }) => object.userData.type !== undefined,
|
||||||
|
) as ArmorMeshIntersection[];
|
||||||
shotLog.push({
|
const intersectionsWithDuplicates = intersectionsAll.slice(
|
||||||
type: ArmorType.External,
|
0,
|
||||||
thickness: intersection.object.userData.thickness,
|
intersectionsAll.findIndex(
|
||||||
status: blocked ? 'blocked' : 'penetration',
|
({ object }) => object.userData.type === ArmorType.Core,
|
||||||
});
|
) + 1,
|
||||||
|
);
|
||||||
if (blocked) break;
|
const firstGunIntersectionIndex =
|
||||||
} else if (intersection.object.userData.type === 'spaced') {
|
intersectionsWithDuplicates.findIndex(
|
||||||
const normal = intersection.normal!.applyAxisAngle(
|
({ object }) =>
|
||||||
I_HAT,
|
object.userData.type === ArmorType.External &&
|
||||||
-Math.PI / 2,
|
object.userData.variant === 'gun',
|
||||||
);
|
);
|
||||||
const angle = normal.angleTo(cameraNormal);
|
const firstTrackIntersectionIndex =
|
||||||
const ricochet = degToRad(shell.ricochet ?? 90);
|
intersectionsWithDuplicates.findIndex(
|
||||||
const normalization = degToRad(shell.normalization ?? 0);
|
({ object }) =>
|
||||||
const threeCalibersRule =
|
object.userData.type === ArmorType.External &&
|
||||||
shell.caliber > thickness || index > 0;
|
object.userData.variant === 'track',
|
||||||
const twoCalibersRule = shell.caliber > thickness * 2;
|
);
|
||||||
const finalNormalization = twoCalibersRule
|
let intersections = intersectionsWithDuplicates;
|
||||||
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
|
|
||||||
: normalization;
|
|
||||||
const finalThickness =
|
|
||||||
thickness / Math.cos(angle - finalNormalization);
|
|
||||||
remainingPenetration -= finalThickness;
|
|
||||||
|
|
||||||
if (!threeCalibersRule && angle >= ricochet) {
|
if (firstGunIntersectionIndex !== -1) {
|
||||||
shotLog.push({
|
intersections = intersections.filter(({ object }, index) => {
|
||||||
type: ArmorType.Spaced,
|
return object.userData.type === ArmorType.External &&
|
||||||
thickness,
|
object.userData.variant === 'gun'
|
||||||
thicknessAngled: finalThickness,
|
? index === firstGunIntersectionIndex
|
||||||
status: 'ricochet',
|
: true;
|
||||||
});
|
});
|
||||||
continue;
|
}
|
||||||
}
|
if (firstTrackIntersectionIndex !== -1) {
|
||||||
} else throw new Error('Core should be omitted');
|
intersections = intersections.filter(({ object }, index) => {
|
||||||
|
return object.userData.type === ArmorType.External &&
|
||||||
|
object.userData.variant === 'track'
|
||||||
|
? index === firstTrackIntersectionIndex
|
||||||
|
: true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
index++;
|
return intersections;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify(shotLog, null, 2));
|
function pushLogs(
|
||||||
|
shellNormal: Vector3,
|
||||||
|
intersections: ArmorMeshIntersection[],
|
||||||
|
allowRicochets: boolean,
|
||||||
|
) {
|
||||||
|
let index = 0;
|
||||||
|
for (const intersection of intersections) {
|
||||||
|
const surfaceNormal = intersection
|
||||||
|
.normal!.clone()
|
||||||
|
.applyQuaternion(
|
||||||
|
event.object.getWorldQuaternion(new Quaternion()),
|
||||||
|
);
|
||||||
|
const thickness =
|
||||||
|
thicknessCoefficient * intersection.object.userData.thickness;
|
||||||
|
|
||||||
|
if (index !== 0) {
|
||||||
|
const previousIntersection = intersections[index - 1];
|
||||||
|
shot.push({
|
||||||
|
type: null,
|
||||||
|
distance:
|
||||||
|
intersection.distance - previousIntersection.distance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersection.object.userData.type === ArmorType.External) {
|
||||||
|
remainingPenetration -= thickness;
|
||||||
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
|
shot.push({
|
||||||
|
type: ArmorType.External,
|
||||||
|
index,
|
||||||
|
shellNormal,
|
||||||
|
surfaceNormal,
|
||||||
|
point: intersection.point,
|
||||||
|
thickness: intersection.object.userData.thickness,
|
||||||
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blocked) break;
|
||||||
|
} else {
|
||||||
|
const type = intersection.object.userData.type;
|
||||||
|
shellNormal.copy(
|
||||||
|
cameraNormal
|
||||||
|
.copy(camera.position)
|
||||||
|
.sub(event.point)
|
||||||
|
.normalize(),
|
||||||
|
);
|
||||||
|
const angle = surfaceNormal.angleTo(shellNormal);
|
||||||
|
const ricochet = degToRad(shell.ricochet ?? 90);
|
||||||
|
const normalization = degToRad(shell.normalization ?? 0);
|
||||||
|
const threeCalibersRule =
|
||||||
|
shell.caliber > thickness * 3 ||
|
||||||
|
index > 0 ||
|
||||||
|
!allowRicochets;
|
||||||
|
const twoCalibersRule = shell.caliber > thickness * 2;
|
||||||
|
const finalNormalization = twoCalibersRule
|
||||||
|
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
|
||||||
|
: normalization;
|
||||||
|
const finalThickness =
|
||||||
|
thickness / Math.cos(angle - finalNormalization);
|
||||||
|
remainingPenetration -= finalThickness;
|
||||||
|
|
||||||
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
|
shot.push({
|
||||||
|
type,
|
||||||
|
index,
|
||||||
|
shellNormal,
|
||||||
|
surfaceNormal,
|
||||||
|
point: intersection.point,
|
||||||
|
thickness,
|
||||||
|
thicknessAngled: finalThickness,
|
||||||
|
angle,
|
||||||
|
status: 'ricochet',
|
||||||
|
});
|
||||||
|
|
||||||
|
const raycaster = new Raycaster();
|
||||||
|
const newShellNormal = cameraNormal
|
||||||
|
.clone()
|
||||||
|
.reflect(surfaceNormal);
|
||||||
|
|
||||||
|
raycaster.set(intersection.point, newShellNormal);
|
||||||
|
pushLogs(
|
||||||
|
newShellNormal,
|
||||||
|
raycaster.intersectObjects(scene.children, true),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const blocked = remainingPenetration < 0;
|
||||||
|
shot.push({
|
||||||
|
type,
|
||||||
|
index,
|
||||||
|
shellNormal,
|
||||||
|
surfaceNormal,
|
||||||
|
point: intersection.point,
|
||||||
|
thickness,
|
||||||
|
thicknessAngled: finalThickness,
|
||||||
|
angle,
|
||||||
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useTankopediaTemporary.setState({ shot });
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { createPortal, useFrame } from '@react-three/fiber';
|
import { createPortal, useFrame } from '@react-three/fiber';
|
||||||
import { memo } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||||
import { CoreArmorScene } from './components/CoreArmorScene';
|
import { CoreArmorScene } from './components/CoreArmorScene';
|
||||||
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
|
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
|
||||||
|
import { ShotScene } from './components/ShotScene';
|
||||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||||
|
|
||||||
export const Armor = memo(() => {
|
export const Armor = memo(() => {
|
||||||
const spacedArmorScene = new Scene();
|
const [spacedArmorScene] = useState(() => new Scene());
|
||||||
const coreArmorScene = new Scene();
|
const [coreArmorScene] = useState(() => new Scene());
|
||||||
|
const [shotScene] = useState(() => new Scene());
|
||||||
const spacedArmorPortal = createPortal(
|
const spacedArmorPortal = createPortal(
|
||||||
<SpacedArmorScene />,
|
<SpacedArmorScene scene={spacedArmorScene} />,
|
||||||
spacedArmorScene,
|
spacedArmorScene,
|
||||||
);
|
);
|
||||||
const coreArmorPortal = createPortal(<CoreArmorScene />, coreArmorScene);
|
const coreArmorPortal = createPortal(<CoreArmorScene />, coreArmorScene);
|
||||||
|
const shotPortal = createPortal(<ShotScene />, shotScene);
|
||||||
|
|
||||||
const renderSize = new Vector2();
|
const renderSize = new Vector2();
|
||||||
const newRenderSize = new Vector2();
|
const newRenderSize = new Vector2();
|
||||||
@@ -41,12 +44,16 @@ export const Armor = memo(() => {
|
|||||||
|
|
||||||
gl.clearDepth();
|
gl.clearDepth();
|
||||||
gl.render(coreArmorScene, camera);
|
gl.render(coreArmorScene, camera);
|
||||||
|
|
||||||
|
gl.clearDepth();
|
||||||
|
gl.render(shotScene, camera);
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{spacedArmorPortal}
|
{spacedArmorPortal}
|
||||||
{coreArmorPortal}
|
{coreArmorPortal}
|
||||||
|
{shotPortal}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
import { Card, Flex, Text } from '@radix-ui/themes';
|
|
||||||
import { Html } from '@react-three/drei';
|
|
||||||
import { useFrame } from '@react-three/fiber';
|
|
||||||
import { ReactNode, useEffect, useRef } from 'react';
|
|
||||||
import { Group, Mesh, Vector3 } from 'three';
|
|
||||||
import { radToDeg } from 'three/src/math/MathUtils';
|
|
||||||
import { isExplosive } from '../core/blitz/isExplosive';
|
|
||||||
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 { Shot, useTankopediaTemporary } from '../stores/tankopedia';
|
|
||||||
|
|
||||||
export const SHOT_NAMES: Record<Shot['type'], string> = {
|
|
||||||
ricochet: 'Ricochet',
|
|
||||||
penetration: 'Penetration',
|
|
||||||
block: 'Blocked',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SHOT_ICONS: Record<Shot['type'], ReactNode> = {
|
|
||||||
ricochet: <Ricochet width={24} height={24} />,
|
|
||||||
block: <Block width={24} height={24} />,
|
|
||||||
penetration: <NominalPenetration width={24} height={24} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
const THICKNESS = 0.05;
|
|
||||||
const LENGTH = 4;
|
|
||||||
|
|
||||||
export function ShotDisplay() {
|
|
||||||
const shot = useTankopediaTemporary((state) => state.shot);
|
|
||||||
const entryWrapper = useRef<Group>(null);
|
|
||||||
const entryLine = useRef<Mesh>(null);
|
|
||||||
const circle = useRef<Mesh>(null);
|
|
||||||
const ricochetCircle = useRef<Mesh>(null);
|
|
||||||
const exitWrapper = useRef<Group>(null);
|
|
||||||
const exitLine = useRef<Mesh>(null);
|
|
||||||
const secondLineLength = shot?.ricochet ? shot?.ricochet.distance : LENGTH;
|
|
||||||
const number = shot && (shot.thicknesses.length > 1 || shot.ricochet);
|
|
||||||
const shell = useDuel((state) => state.antagonist!.shell);
|
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
|
||||||
const t1 = clock.elapsedTime % 1;
|
|
||||||
const t2 = (clock.elapsedTime + 0.5) % 1;
|
|
||||||
|
|
||||||
entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH);
|
|
||||||
entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1);
|
|
||||||
exitLine.current?.position.set(0, 0, t2 * secondLineLength);
|
|
||||||
exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!shot) return;
|
|
||||||
|
|
||||||
const point = new Vector3(...shot.point);
|
|
||||||
const shellNormal = new Vector3(...shot.shellNormal);
|
|
||||||
const surfaceNormal = new Vector3(...shot.surfaceNormal);
|
|
||||||
const bounceNormal = shellNormal
|
|
||||||
.clone()
|
|
||||||
.multiplyScalar(-1)
|
|
||||||
.sub(
|
|
||||||
surfaceNormal
|
|
||||||
.clone()
|
|
||||||
.multiplyScalar(
|
|
||||||
2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
entryWrapper.current?.position.set(...shot.point);
|
|
||||||
entryWrapper.current?.lookAt(point.clone().add(shellNormal));
|
|
||||||
exitWrapper.current?.position.set(...shot.point);
|
|
||||||
exitWrapper.current?.lookAt(point.clone().add(bounceNormal));
|
|
||||||
circle.current?.position.set(...shot.point);
|
|
||||||
circle.current?.lookAt(point.clone().add(surfaceNormal));
|
|
||||||
}, [shot]);
|
|
||||||
|
|
||||||
if (!shot) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<mesh ref={circle}>
|
|
||||||
<torusGeometry args={[0.05, 0.0125, 4, 16]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color={shot.type === 'penetration' ? 'green' : 'red'}
|
|
||||||
depthTest={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<group ref={entryWrapper}>
|
|
||||||
<Html
|
|
||||||
position={[0, 0, 0]}
|
|
||||||
style={{
|
|
||||||
paddingLeft: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
style={{
|
|
||||||
width: number ? 300 : 200,
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex direction="column" gap="1">
|
|
||||||
<Flex align="center" gap="2">
|
|
||||||
{SHOT_ICONS[shot.type]}
|
|
||||||
<Text>
|
|
||||||
{SHOT_NAMES[shot.type]} at {Math.round(radToDeg(shot.angle))}°
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
style={{
|
|
||||||
paddingLeft: number ? 8 : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{shot.thicknesses.map((layer, index) => (
|
|
||||||
<Flex gap="2" key={index}>
|
|
||||||
{number && (
|
|
||||||
<Text>
|
|
||||||
{index + 1}. {layer.type[0].toUpperCase()}
|
|
||||||
{layer.type.slice(1)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex gap="1">
|
|
||||||
{layer.type !== 'gap' && (
|
|
||||||
<NominalPenetration width={24} height={24} />
|
|
||||||
)}
|
|
||||||
<Text>
|
|
||||||
{Math.round(
|
|
||||||
layer.type === 'gap'
|
|
||||||
? layer.gap * 1000
|
|
||||||
: layer.nominal,
|
|
||||||
)}
|
|
||||||
mm
|
|
||||||
{layer.type === 'gap' && isExplosive(shell.type)
|
|
||||||
? ` (-${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% penetration)`
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
{layer.type !== 'external' && layer.type !== 'gap' && (
|
|
||||||
<Flex gap="1">
|
|
||||||
<AngledPenetration width={24} height={24} />
|
|
||||||
<Text>{Math.round(layer.angled)}mm</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{shot.ricochet && (
|
|
||||||
<>
|
|
||||||
<Flex gap="1">
|
|
||||||
<Text>{shot.thicknesses.length + 1}. Ricochet</Text>
|
|
||||||
<Ricochet width={24} height={24} />
|
|
||||||
<Text>(-25% penetration)</Text>
|
|
||||||
</Flex>
|
|
||||||
{shot.ricochet.penetration ? (
|
|
||||||
<Flex gap="1">
|
|
||||||
<Text>{shot.thicknesses.length + 2}. Penetration</Text>
|
|
||||||
<NominalPenetration width={24} height={24} />
|
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
<Flex gap="1">
|
|
||||||
<Text>{shot.thicknesses.length + 2}. Blocked</Text>
|
|
||||||
<Block width={24} height={24} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Html>
|
|
||||||
|
|
||||||
<mesh
|
|
||||||
renderOrder={1}
|
|
||||||
position={[0, 0, LENGTH / 2]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
ref={entryLine}
|
|
||||||
>
|
|
||||||
<cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} />
|
|
||||||
<meshBasicMaterial color="#ffff80" />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
|
|
||||||
<cylinderGeometry args={[THICKNESS / 4, THICKNESS / 4, LENGTH, 8]} />
|
|
||||||
<meshBasicMaterial color="#ffffff" depthTest={false} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{shot.type === 'ricochet' && (
|
|
||||||
<group ref={exitWrapper}>
|
|
||||||
<mesh
|
|
||||||
renderOrder={1}
|
|
||||||
position={[0, 0, secondLineLength / 2]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
ref={exitLine}
|
|
||||||
>
|
|
||||||
<cylinderGeometry
|
|
||||||
args={[THICKNESS / 2, THICKNESS / 2, secondLineLength, 8]}
|
|
||||||
/>
|
|
||||||
<meshBasicMaterial color="#ff8080" />
|
|
||||||
</mesh>
|
|
||||||
<mesh
|
|
||||||
position={[0, 0, secondLineLength / 2]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
>
|
|
||||||
<cylinderGeometry
|
|
||||||
args={[THICKNESS / 4, THICKNESS / 4, secondLineLength, 8]}
|
|
||||||
/>
|
|
||||||
<meshBasicMaterial color="#ffffff" depthTest={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{shot.ricochet && (
|
|
||||||
<mesh ref={ricochetCircle} position={[0, 0, secondLineLength]}>
|
|
||||||
<icosahedronGeometry args={[0.05, 1]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color={shot.ricochet.penetration ? 'green' : 'red'}
|
|
||||||
depthTest={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
</group>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Vector3 } from 'three';
|
import { Vector3 } from 'three';
|
||||||
|
|
||||||
|
export const ZERO_VECTOR = new Vector3();
|
||||||
export const I_HAT = new Vector3(1, 0, 0);
|
export const I_HAT = new Vector3(1, 0, 0);
|
||||||
export const J_HAT = new Vector3(0, 1, 0);
|
export const J_HAT = new Vector3(0, 1, 0);
|
||||||
export const K_HAT = new Vector3(0, 0, 1);
|
export const K_HAT = new Vector3(0, 0, 1);
|
||||||
+28
-13
@@ -1,9 +1,10 @@
|
|||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import { Vector3Tuple } from 'three';
|
import { Vector3 } from 'three';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||||
import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting';
|
import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting';
|
||||||
|
import { ArmorType } from '../components/Armor/components/SpacedArmorScene';
|
||||||
import { TankType, TreeType } from '../components/Tanks';
|
import { TankType, TreeType } from '../components/Tanks';
|
||||||
import { Tier } from '../core/blitzkrieg/tankDefinitions';
|
import { Tier } from '../core/blitzkrieg/tankDefinitions';
|
||||||
|
|
||||||
@@ -43,20 +44,34 @@ interface TankopediaTemporary {
|
|||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Shot {
|
export interface ShotLayerBase {
|
||||||
point: Vector3Tuple;
|
index: number;
|
||||||
surfaceNormal: Vector3Tuple;
|
thickness: number;
|
||||||
shellNormal: Vector3Tuple;
|
point: Vector3;
|
||||||
thicknesses: ArmorPiercingLayer[];
|
shellNormal: Vector3;
|
||||||
type: 'ricochet' | 'penetration' | 'block';
|
surfaceNormal: Vector3;
|
||||||
angle: number;
|
status: 'blocked' | 'penetration' | 'ricochet';
|
||||||
ricochet?: {
|
|
||||||
distance: number;
|
|
||||||
point: Vector3Tuple;
|
|
||||||
penetration: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShotLayerExternal extends ShotLayerBase {
|
||||||
|
type: ArmorType.External;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShotLayerNonExternal extends ShotLayerBase {
|
||||||
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
|
angle: number;
|
||||||
|
thicknessAngled: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShotLayerGap {
|
||||||
|
type: null;
|
||||||
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap;
|
||||||
|
|
||||||
|
export type Shot = ShotLayer[];
|
||||||
|
|
||||||
export type ArmorPiercingLayer =
|
export type ArmorPiercingLayer =
|
||||||
| {
|
| {
|
||||||
distance: number;
|
distance: number;
|
||||||
|
|||||||
Reference in new issue
Block a user