mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: ricochet repenetration
This commit is contained in:
1 parent
8bb5032ee4
commit
901f1bfe42
8 files changed
+163
-33
No files matched your search
Vendored
+1
@@ -85,6 +85,7 @@
|
||||
"Pyogenics",
|
||||
"ratingsleaderboard",
|
||||
"ratingsplot",
|
||||
"raycaster",
|
||||
"researchable",
|
||||
"Resvg",
|
||||
"roboto",
|
||||
|
||||
@@ -36,6 +36,7 @@ export function Consumables() {
|
||||
|
||||
return (
|
||||
<ModuleButton
|
||||
key={consumable.id}
|
||||
first={index === 0}
|
||||
last={index === consumablesList.length - 1}
|
||||
rowChild
|
||||
|
||||
@@ -35,6 +35,7 @@ export function Provisions() {
|
||||
|
||||
return (
|
||||
<ModuleButton
|
||||
key={provision.id}
|
||||
first={index === 0}
|
||||
last={index === provisionsList.length - 1}
|
||||
rowChild
|
||||
|
||||
@@ -127,6 +127,7 @@ export function Characteristics() {
|
||||
{gun.type === 'autoReloader' ? (
|
||||
gun.reload.map((reload, index) => (
|
||||
<Info
|
||||
key={index}
|
||||
indent={index > 0}
|
||||
name={index > 0 ? `Shell ${index + 1}` : 'Reload on shell 1'}
|
||||
unit="s"
|
||||
@@ -144,25 +145,25 @@ export function Characteristics() {
|
||||
</Info>
|
||||
<Info name="Penetration" unit="mm" />
|
||||
{gun.shells.map((shell) => (
|
||||
<Info indent name={SHELL_NAMES[shell.type]}>
|
||||
<Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
|
||||
{resolveNearPenetration(shell.penetration)}
|
||||
</Info>
|
||||
))}
|
||||
<Info name="Damage" unit="hp" />
|
||||
{gun.shells.map((shell) => (
|
||||
<Info indent name={SHELL_NAMES[shell.type]}>
|
||||
<Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
|
||||
{shell.damage.armor}
|
||||
</Info>
|
||||
))}
|
||||
<Info name="Module damage" unit="hp" />
|
||||
{gun.shells.map((shell, index) => (
|
||||
<Info indent name={SHELL_NAMES[shell.type]}>
|
||||
<Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
|
||||
{shell.damage.module}
|
||||
</Info>
|
||||
))}
|
||||
<Info name="Shell velocity" unit="m/s" />
|
||||
{gun.shells.map((shell, index) => (
|
||||
<Info indent name={SHELL_NAMES[shell.type]}>
|
||||
<Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
|
||||
{shell.speed}
|
||||
</Info>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IUniform,
|
||||
Mesh,
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
ShaderMaterial,
|
||||
Vector2,
|
||||
Vector3,
|
||||
@@ -16,6 +17,7 @@ import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetr
|
||||
import { useDuel } from '../../../../stores/duel';
|
||||
import {
|
||||
ArmorPiercingLayer,
|
||||
Shot,
|
||||
mutateTankopediaTemporary,
|
||||
useTankopediaPersistent,
|
||||
} from '../../../../stores/tankopedia';
|
||||
@@ -49,6 +51,7 @@ export function ArmorMesh({
|
||||
const shellNormal = new Vector3();
|
||||
const surfaceNormal = new Vector3();
|
||||
const mesh = useRef<Mesh>(null);
|
||||
const scene = useThree((state) => state.scene);
|
||||
|
||||
useEffect(() => {
|
||||
function updateQuickEquipments() {
|
||||
@@ -189,8 +192,11 @@ export function ArmorMesh({
|
||||
|
||||
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
|
||||
|
||||
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor.map(
|
||||
(event, index) => {
|
||||
let hasRicochet = false;
|
||||
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
|
||||
.map((event, index) => {
|
||||
if (hasRicochet) return null;
|
||||
|
||||
switch (event.object.userData.type) {
|
||||
case 'externalModule': {
|
||||
return {
|
||||
@@ -198,9 +204,9 @@ export function ArmorMesh({
|
||||
event.object.userData.thickness * thicknessCoefficient,
|
||||
angled:
|
||||
event.object.userData.thickness * thicknessCoefficient,
|
||||
ricochet: false,
|
||||
type: 'external',
|
||||
distance: event.distance,
|
||||
ricochet: false,
|
||||
} satisfies ArmorPiercingLayer;
|
||||
}
|
||||
|
||||
@@ -210,7 +216,6 @@ export function ArmorMesh({
|
||||
shell.caliber >
|
||||
3 *
|
||||
(event.object.userData.thickness * thicknessCoefficient);
|
||||
let ricochet = false;
|
||||
|
||||
if (
|
||||
index === 0 &&
|
||||
@@ -218,7 +223,7 @@ export function ArmorMesh({
|
||||
!threeCalibersRule &&
|
||||
angle >= degToRad(shell.ricochet!)
|
||||
) {
|
||||
ricochet = true;
|
||||
hasRicochet = true;
|
||||
}
|
||||
|
||||
const twoCalibersRule =
|
||||
@@ -236,10 +241,10 @@ export function ArmorMesh({
|
||||
Math.cos(angle - degToRad(normalization));
|
||||
|
||||
return {
|
||||
ricochet: hasRicochet,
|
||||
nominal:
|
||||
event.object.userData.thickness * thicknessCoefficient,
|
||||
angled,
|
||||
ricochet,
|
||||
type:
|
||||
event.object.userData.type === 'coreArmor'
|
||||
? 'core'
|
||||
@@ -248,8 +253,8 @@ export function ArmorMesh({
|
||||
} satisfies ArmorPiercingLayer;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => {
|
||||
if (aIndex === armorPiercingLayersRaw.length - 1) return null;
|
||||
const layerB = armorPiercingLayersRaw[aIndex + 1];
|
||||
@@ -273,7 +278,11 @@ export function ArmorMesh({
|
||||
|
||||
armorPiercingLayers.forEach((layer) => {
|
||||
if (layer.type !== 'gap') {
|
||||
remainingPenetration -= layer.angled;
|
||||
if (layer.ricochet) {
|
||||
remainingPenetration *= 0.75;
|
||||
} else {
|
||||
remainingPenetration -= layer.angled;
|
||||
}
|
||||
} else if (explosiveCapable) {
|
||||
// there is a 50% penetration loss per meter for HE based shells
|
||||
remainingPenetration -= 0.5 * remainingPenetration * layer.gap;
|
||||
@@ -282,10 +291,79 @@ export function ArmorMesh({
|
||||
remainingPenetration = Math.max(0, remainingPenetration);
|
||||
});
|
||||
|
||||
const hasRicochet = armorPiercingLayers.some(
|
||||
(layer) => layer.type !== 'gap' && layer.ricochet,
|
||||
);
|
||||
const hasPenetrated = remainingPenetration > 0;
|
||||
let ricochet: Shot['ricochet'] = undefined;
|
||||
|
||||
if (hasRicochet) {
|
||||
const raycaster = new Raycaster();
|
||||
const bounceNormal = shellNormal
|
||||
.clone()
|
||||
.multiplyScalar(-1)
|
||||
.sub(
|
||||
surfaceNormal
|
||||
.clone()
|
||||
.multiplyScalar(
|
||||
2 *
|
||||
surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
|
||||
),
|
||||
);
|
||||
raycaster.set(event.point, bounceNormal);
|
||||
const intersections = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersection) =>
|
||||
typeof intersection.object.userData?.type === 'string',
|
||||
);
|
||||
const indexOfCoreArmor = intersections.findIndex(
|
||||
({ object }) => object.userData.type === 'coreArmor',
|
||||
);
|
||||
const intersectionsTillFirstCoreArmor = intersections.slice(
|
||||
0,
|
||||
indexOfCoreArmor + 1,
|
||||
);
|
||||
|
||||
if (intersectionsTillFirstCoreArmor.length > 0) {
|
||||
intersectionsTillFirstCoreArmor.forEach((intersection) => {
|
||||
switch (intersection.object.userData.type) {
|
||||
case 'externalModule': {
|
||||
remainingPenetration -=
|
||||
intersection.object.userData.thickness;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'spacedArmor':
|
||||
case 'coreArmor': {
|
||||
const angle = bounceNormal.angleTo(
|
||||
intersection.object.position.clone().sub(event.point),
|
||||
);
|
||||
const twoCalibersRule =
|
||||
shell.caliber >
|
||||
2 *
|
||||
(intersection.object.userData.thickness *
|
||||
thicknessCoefficient);
|
||||
const normalization = twoCalibersRule
|
||||
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
||||
(2 *
|
||||
intersection.object.userData.thickness *
|
||||
thicknessCoefficient)
|
||||
: shell.normalization ?? 0;
|
||||
const angled =
|
||||
(intersection.object.userData.thickness *
|
||||
thicknessCoefficient) /
|
||||
Math.cos(angle - degToRad(normalization));
|
||||
|
||||
remainingPenetration -= angled;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ricochet = {
|
||||
point: intersectionsTillFirstCoreArmor.at(-1)!.point.toArray(),
|
||||
distance: intersectionsTillFirstCoreArmor.at(-1)!.distance,
|
||||
penetration: remainingPenetration > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.shot = {
|
||||
@@ -299,6 +377,7 @@ export function ArmorMesh({
|
||||
shellNormal: shellNormal.toArray(),
|
||||
surfaceNormal: surfaceNormal.toArray(),
|
||||
angle,
|
||||
ricochet,
|
||||
};
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -4,10 +4,12 @@ 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> = {
|
||||
@@ -30,8 +32,12 @@ export function ShotDisplay() {
|
||||
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;
|
||||
@@ -39,7 +45,7 @@ export function ShotDisplay() {
|
||||
|
||||
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 * LENGTH);
|
||||
exitLine.current?.position.set(0, 0, t2 * secondLineLength);
|
||||
exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1);
|
||||
});
|
||||
|
||||
@@ -47,21 +53,21 @@ export function ShotDisplay() {
|
||||
if (!shot) return;
|
||||
|
||||
const point = new Vector3(...shot.point);
|
||||
const shotNormal = new Vector3(...shot.shellNormal);
|
||||
const shellNormal = new Vector3(...shot.shellNormal);
|
||||
const surfaceNormal = new Vector3(...shot.surfaceNormal);
|
||||
const bounceNormal = shotNormal
|
||||
const bounceNormal = shellNormal
|
||||
.clone()
|
||||
.multiplyScalar(-1)
|
||||
.sub(
|
||||
surfaceNormal
|
||||
.clone()
|
||||
.multiplyScalar(
|
||||
2 * surfaceNormal.dot(shotNormal.clone().multiplyScalar(-1)),
|
||||
2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
|
||||
),
|
||||
);
|
||||
|
||||
entryWrapper.current?.position.set(...shot.point);
|
||||
entryWrapper.current?.lookAt(point.clone().add(shotNormal));
|
||||
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);
|
||||
@@ -89,7 +95,7 @@ export function ShotDisplay() {
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
width: shot.thicknesses.length > 1 ? 300 : 200,
|
||||
width: number ? 300 : 200,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
@@ -104,12 +110,12 @@ export function ShotDisplay() {
|
||||
<Flex
|
||||
direction="column"
|
||||
style={{
|
||||
paddingLeft: shot.thicknesses.length > 1 ? 8 : undefined,
|
||||
paddingLeft: number ? 8 : undefined,
|
||||
}}
|
||||
>
|
||||
{shot.thicknesses.map((layer, index) => (
|
||||
<Flex gap="2">
|
||||
{shot.thicknesses.length > 1 && (
|
||||
<Flex gap="2" key={index}>
|
||||
{number && (
|
||||
<Text>
|
||||
{index + 1}. {layer.type[0].toUpperCase()}
|
||||
{layer.type.slice(1)}
|
||||
@@ -127,6 +133,9 @@ export function ShotDisplay() {
|
||||
: layer.nominal,
|
||||
)}
|
||||
mm
|
||||
{layer.type === 'gap' && isExplosive(shell.type)
|
||||
? ` (${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% pen. loss)`
|
||||
: ''}
|
||||
</Text>
|
||||
</Flex>
|
||||
{layer.type !== 'external' && layer.type !== 'gap' && (
|
||||
@@ -137,6 +146,26 @@ export function ShotDisplay() {
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
{shot.ricochet && (
|
||||
<>
|
||||
<Flex gap="1">
|
||||
<Text>{shot.thicknesses.length + 1}. Ricochet</Text>
|
||||
<Ricochet width={24} height={24} />
|
||||
</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>
|
||||
@@ -161,21 +190,34 @@ export function ShotDisplay() {
|
||||
<group ref={exitWrapper}>
|
||||
<mesh
|
||||
renderOrder={1}
|
||||
position={[0, 0, LENGTH / 2]}
|
||||
position={[0, 0, secondLineLength / 2]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
ref={exitLine}
|
||||
>
|
||||
<cylinderGeometry
|
||||
args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]}
|
||||
args={[THICKNESS / 2, THICKNESS / 2, secondLineLength, 8]}
|
||||
/>
|
||||
<meshBasicMaterial color="#ff8080" />
|
||||
</mesh>
|
||||
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<mesh
|
||||
position={[0, 0, secondLineLength / 2]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
>
|
||||
<cylinderGeometry
|
||||
args={[THICKNESS / 4, THICKNESS / 4, LENGTH, 8]}
|
||||
args={[THICKNESS / 4, THICKNESS / 4, secondLineLength, 8]}
|
||||
/>
|
||||
<meshBasicMaterial color="#ffffff" depthTest={false} />
|
||||
</mesh>
|
||||
|
||||
{shot.ricochet && (
|
||||
<mesh ref={ricochetCircle} position={[0, 0, secondLineLength]}>
|
||||
<torusGeometry args={[0.05, 0.0125, 4, 16]} />
|
||||
<meshBasicMaterial
|
||||
color={shot.ricochet.penetration ? 'green' : 'red'}
|
||||
depthTest={false}
|
||||
/>
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -15,8 +15,8 @@ export function AngledPenetration(props: ComponentProps<'svg'>) {
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M28 8H12L22 28H38L28 8ZM42 36H26L36 56H52L42 36Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
|
||||
@@ -69,14 +69,19 @@ interface TankopediaTemporary {
|
||||
shot?: Shot;
|
||||
}
|
||||
|
||||
export type Shot = {
|
||||
export interface Shot {
|
||||
point: Vector3Tuple;
|
||||
surfaceNormal: Vector3Tuple;
|
||||
shellNormal: Vector3Tuple;
|
||||
thicknesses: ArmorPiercingLayer[];
|
||||
type: 'ricochet' | 'penetration' | 'block';
|
||||
angle: number;
|
||||
};
|
||||
ricochet?: {
|
||||
distance: number;
|
||||
point: Vector3Tuple;
|
||||
penetration: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type ArmorPiercingLayer =
|
||||
| {
|
||||
|
||||
Reference in new issue
Block a user