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