2024-02-28 17:41:59 -06:00
|
|
|
import { useThree } from '@react-three/fiber';
|
2024-02-28 22:29:21 -06:00
|
|
|
import {
|
|
|
|
|
Intersection,
|
|
|
|
|
MeshBasicMaterial,
|
|
|
|
|
Object3D,
|
2024-04-13 10:36:13 -05:00
|
|
|
Plane,
|
2024-02-28 22:29:21 -06:00
|
|
|
Quaternion,
|
2024-05-26 14:39:16 -05:00
|
|
|
Raycaster,
|
2024-02-28 22:29:21 -06:00
|
|
|
Scene,
|
2024-05-26 14:39:16 -05:00
|
|
|
Vector3,
|
2024-02-28 22:29:21 -06:00
|
|
|
} from 'three';
|
2024-02-28 23:06:37 -06:00
|
|
|
import { degToRad } from 'three/src/math/MathUtils';
|
2024-02-28 17:41:59 -06:00
|
|
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
|
|
|
|
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
2024-05-24 20:32:39 -05:00
|
|
|
import { resolvePenetrationCoefficient } from '../../../../core/blitz/resolvePenetrationCoefficient';
|
2024-04-23 09:57:59 -05:00
|
|
|
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
|
|
|
|
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
2024-05-24 20:32:39 -05:00
|
|
|
import { ShellType } from '../../../../core/blitzkit/tankDefinitions';
|
2024-07-09 20:35:36 -05:00
|
|
|
import * as Duel from '../../../../stores/duel';
|
2024-07-09 14:43:33 -05:00
|
|
|
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral';
|
2024-02-28 17:41:59 -06:00
|
|
|
import { ArmorType } from '../SpacedArmorScene';
|
|
|
|
|
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
|
|
|
|
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
|
|
|
|
|
|
|
|
|
type SpacedArmorSceneComponentProps = {
|
|
|
|
|
node: Object3D;
|
|
|
|
|
thickness: number;
|
2024-02-28 22:29:21 -06:00
|
|
|
scene: Scene;
|
2024-04-13 10:36:13 -05:00
|
|
|
clip?: Plane;
|
2024-02-28 17:41:59 -06:00
|
|
|
} & (
|
|
|
|
|
| {
|
|
|
|
|
type: Exclude<ArmorType, ArmorType.External>;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
type: ArmorType.External;
|
|
|
|
|
variant: ExternalModuleVariant;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export type ExternalModuleVariant = 'gun' | 'track';
|
|
|
|
|
export type ArmorUserData = {
|
|
|
|
|
thickness: number;
|
|
|
|
|
} & (
|
|
|
|
|
| {
|
2024-02-28 22:29:21 -06:00
|
|
|
type: Exclude<ArmorType, ArmorType.External>;
|
2024-02-28 17:41:59 -06:00
|
|
|
}
|
|
|
|
|
| {
|
2024-02-28 22:29:21 -06:00
|
|
|
type: ArmorType.External;
|
2024-02-28 17:41:59 -06:00
|
|
|
variant: ExternalModuleVariant;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
type ArmorMeshIntersection = Intersection & {
|
|
|
|
|
object: Object3D & { userData: ArmorUserData };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const omitMaterial = new MeshBasicMaterial({
|
|
|
|
|
colorWrite: false,
|
|
|
|
|
depthWrite: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render orders allocations:
|
|
|
|
|
* - 0 : core omission
|
|
|
|
|
* - 1-2: spaced without depth buffer write but do test to allow additive blending
|
|
|
|
|
* - 3-4: external with depth buffer write in first pass and read in second to simplify geometry
|
|
|
|
|
* - 5 : spaced depth write
|
|
|
|
|
*/
|
|
|
|
|
export function SpacedArmorSceneComponent({
|
|
|
|
|
node,
|
|
|
|
|
thickness,
|
2024-02-28 22:29:21 -06:00
|
|
|
scene,
|
2024-04-13 10:36:13 -05:00
|
|
|
clip,
|
2024-02-28 17:41:59 -06:00
|
|
|
...props
|
|
|
|
|
}: SpacedArmorSceneComponentProps) {
|
2024-07-09 14:43:33 -05:00
|
|
|
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
2024-02-28 17:41:59 -06:00
|
|
|
const camera = useThree((state) => state.camera);
|
2024-07-09 20:35:36 -05:00
|
|
|
const duelStore = Duel.useStore();
|
2024-02-28 17:41:59 -06:00
|
|
|
|
2024-05-26 14:39:16 -05:00
|
|
|
async function shoot(
|
|
|
|
|
point: Vector3,
|
|
|
|
|
intersections: Intersection[],
|
|
|
|
|
allowRicochet: boolean,
|
2024-05-27 20:42:48 -05:00
|
|
|
remainingPenetrationInput?: number,
|
2024-05-26 14:39:16 -05:00
|
|
|
) {
|
2024-07-09 20:35:36 -05:00
|
|
|
const { antagonist, protagonist } = duelStore.getState();
|
2024-07-10 13:11:09 -05:00
|
|
|
const shell = antagonist.shell;
|
2024-05-26 14:39:16 -05:00
|
|
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
2024-07-09 14:43:33 -05:00
|
|
|
const shot: TankopediaEphemeral.Shot = {
|
2024-05-26 14:39:16 -05:00
|
|
|
damage: -1,
|
|
|
|
|
containsGaps: shell.type === ShellType.HEAT,
|
|
|
|
|
in: {
|
|
|
|
|
surfaceNormal: cameraNormal,
|
|
|
|
|
layers: [],
|
|
|
|
|
status: 'blocked',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hasCalibratedShells = await hasEquipment(
|
|
|
|
|
103,
|
2024-07-10 13:11:09 -05:00
|
|
|
antagonist.tank.equipment,
|
|
|
|
|
antagonist.equipmentMatrix,
|
2024-05-26 14:39:16 -05:00
|
|
|
);
|
|
|
|
|
const hasEnhancedArmor = await hasEquipment(
|
|
|
|
|
110,
|
2024-07-10 13:11:09 -05:00
|
|
|
protagonist.tank.equipment,
|
|
|
|
|
protagonist.equipmentMatrix,
|
2024-05-26 14:39:16 -05:00
|
|
|
);
|
|
|
|
|
const penetration =
|
|
|
|
|
resolveNearPenetration(shell.penetration) *
|
|
|
|
|
resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
|
|
|
|
|
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
|
|
|
|
const filteredIntersections = intersections.filter(
|
|
|
|
|
(intersection) => 'type' in intersection.object.userData,
|
|
|
|
|
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
|
|
|
|
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
|
|
|
|
const noDuplicateIntersections: typeof filteredIntersections = [];
|
|
|
|
|
|
|
|
|
|
for (const intersection of filteredIntersections) {
|
|
|
|
|
if (intersection.object.userData.type === ArmorType.External) {
|
|
|
|
|
if (
|
|
|
|
|
!encounteredExternalModuleVariants.includes(
|
|
|
|
|
intersection.object.userData.variant,
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
noDuplicateIntersections.push(intersection);
|
|
|
|
|
encounteredExternalModuleVariants.push(
|
|
|
|
|
intersection.object.userData.variant,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
noDuplicateIntersections.push(intersection);
|
|
|
|
|
|
|
|
|
|
if (intersection.object.userData.type === ArmorType.Core) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 20:42:48 -05:00
|
|
|
let remainingPenetration = remainingPenetrationInput ?? penetration;
|
2024-05-26 14:39:16 -05:00
|
|
|
let index = 0;
|
|
|
|
|
for (const intersection of noDuplicateIntersections) {
|
|
|
|
|
const layer = intersection.object.userData;
|
|
|
|
|
const surfaceNormal = intersection
|
|
|
|
|
.normal!.clone()
|
|
|
|
|
.applyQuaternion(
|
|
|
|
|
intersection.object.getWorldQuaternion(new Quaternion()),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (shell.type === ShellType.HEAT && index > 0) {
|
2024-07-09 14:43:33 -05:00
|
|
|
const lastLayer = shot.in.layers.at(
|
|
|
|
|
-1,
|
|
|
|
|
) as TankopediaEphemeral.ShotLayerBase;
|
2024-05-26 14:39:16 -05:00
|
|
|
const distance = lastLayer.point.distanceTo(intersection.point);
|
|
|
|
|
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
|
|
|
|
const blocked = remainingPenetration <= 0;
|
|
|
|
|
|
|
|
|
|
shot.in.layers.push({
|
|
|
|
|
type: null,
|
|
|
|
|
distance,
|
|
|
|
|
status: blocked ? 'blocked' : 'penetration',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (blocked) break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layer.type === ArmorType.External) {
|
|
|
|
|
const thickness = layer.thickness * thicknessCoefficient;
|
|
|
|
|
remainingPenetration -= thickness;
|
|
|
|
|
const blocked =
|
|
|
|
|
remainingPenetration <= 0 || shell.type === ShellType.HE;
|
|
|
|
|
|
|
|
|
|
shot.in.layers.push({
|
|
|
|
|
type: ArmorType.External,
|
|
|
|
|
index,
|
|
|
|
|
thickness,
|
|
|
|
|
point: intersection.point,
|
|
|
|
|
surfaceNormal,
|
2024-05-27 21:01:07 -05:00
|
|
|
status:
|
|
|
|
|
shell.type === ShellType.HE
|
|
|
|
|
? 'blocked'
|
|
|
|
|
: blocked
|
|
|
|
|
? 'blocked'
|
|
|
|
|
: 'penetration',
|
2024-05-26 14:39:16 -05:00
|
|
|
variant: layer.variant,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const thickness = layer.thickness;
|
|
|
|
|
const ricochet = degToRad(
|
|
|
|
|
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
|
|
|
|
);
|
|
|
|
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
|
|
|
|
const threeCalibersRule =
|
|
|
|
|
shell.caliber > thickness * 3 || index > 0 || !allowRicochet;
|
|
|
|
|
const twoCalibersRule = shell.caliber > thickness * 2;
|
|
|
|
|
const normalization = degToRad(shell.normalization ?? 0);
|
|
|
|
|
const finalNormalization = twoCalibersRule
|
|
|
|
|
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
|
|
|
|
|
: normalization;
|
|
|
|
|
const finalThickness =
|
|
|
|
|
thickness / Math.cos(Math.max(0, angle - finalNormalization));
|
|
|
|
|
|
|
|
|
|
if (!threeCalibersRule && angle >= ricochet) {
|
|
|
|
|
remainingPenetration *= 0.75;
|
|
|
|
|
|
|
|
|
|
shot.in.layers.push({
|
|
|
|
|
type: layer.type,
|
|
|
|
|
index,
|
|
|
|
|
thickness,
|
|
|
|
|
thicknessAngled: finalThickness,
|
|
|
|
|
point: intersection.point,
|
|
|
|
|
surfaceNormal,
|
|
|
|
|
status: 'ricochet',
|
|
|
|
|
angle,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
} else {
|
|
|
|
|
remainingPenetration -= finalThickness;
|
|
|
|
|
const blocked = remainingPenetration <= 0;
|
|
|
|
|
|
|
|
|
|
shot.in.layers.push({
|
|
|
|
|
type: layer.type,
|
|
|
|
|
index,
|
|
|
|
|
thickness,
|
|
|
|
|
thicknessAngled: finalThickness,
|
|
|
|
|
point: intersection.point,
|
|
|
|
|
surfaceNormal: intersection.face!.normal,
|
2024-05-27 21:01:07 -05:00
|
|
|
status:
|
2024-06-05 13:14:08 -05:00
|
|
|
shell.type === ShellType.HE && layer.type !== ArmorType.Core
|
2024-05-27 21:01:07 -05:00
|
|
|
? 'blocked'
|
|
|
|
|
: blocked
|
|
|
|
|
? 'blocked'
|
|
|
|
|
: 'penetration',
|
2024-05-26 14:39:16 -05:00
|
|
|
angle,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shell.type !== ShellType.HE && remainingPenetration <= 0) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index++;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 14:43:33 -05:00
|
|
|
const lastLayer = shot.in.layers.at(-1) as
|
|
|
|
|
| TankopediaEphemeral.ShotLayerNonExternal
|
|
|
|
|
| undefined;
|
|
|
|
|
const firstLayer = shot.in.layers[0] as
|
|
|
|
|
| TankopediaEphemeral.ShotLayerBase
|
|
|
|
|
| undefined;
|
2024-05-26 14:39:16 -05:00
|
|
|
|
|
|
|
|
if (!lastLayer || !firstLayer) {
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
if (shell.type === ShellType.HE) {
|
|
|
|
|
const totalSpacedArmorThickness = shot.in.layers.reduce(
|
|
|
|
|
(acc, layer) => {
|
|
|
|
|
if (layer.type === null || layer.type === ArmorType.Core) {
|
|
|
|
|
return acc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layer.type === ArmorType.External) {
|
|
|
|
|
return acc + layer.thickness;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return acc + layer.thicknessAngled;
|
|
|
|
|
},
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
|
|
|
|
lastLayer.point,
|
|
|
|
|
);
|
|
|
|
|
const finalDamage = Math.max(
|
|
|
|
|
0,
|
|
|
|
|
0.5 *
|
|
|
|
|
shell.damage.armor *
|
|
|
|
|
(1 - distanceFromSpacedArmor / shell.explosionRadius!) -
|
|
|
|
|
1.1 *
|
|
|
|
|
(lastLayer.thicknessAngled +
|
|
|
|
|
Math.min(penetration, totalSpacedArmorThickness)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Above is some peculiar math. We're multiplying 1.1 by the
|
|
|
|
|
* thickness of the core plate plus the total spaced armor
|
|
|
|
|
* thickness capped to the penetration itself. This is because
|
|
|
|
|
* the shader is normalized by the penetration value; hence,
|
|
|
|
|
* the total spaced armor thickness cannot ever be thicker
|
|
|
|
|
* than the penetration. This is a precision error on the
|
|
|
|
|
* shader-side and I do not intend on fixing it. Instead,
|
|
|
|
|
* for parity's sake, I am capping the value here.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
|
|
|
|
|
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
|
|
|
|
|
shot.damage = finalDamage;
|
|
|
|
|
} else {
|
|
|
|
|
shot.in.status = 'penetration';
|
|
|
|
|
shot.damage = shell.damage.armor;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (lastLayer.status === 'blocked') {
|
|
|
|
|
shot.in.status = 'blocked';
|
|
|
|
|
shot.damage = 0;
|
|
|
|
|
} else if (lastLayer.status === 'penetration') {
|
|
|
|
|
shot.in.status = 'penetration';
|
|
|
|
|
shot.damage = shell.damage.armor;
|
|
|
|
|
} else {
|
|
|
|
|
const caster = new Raycaster();
|
|
|
|
|
// https://math.stackexchange.com/a/13263/1222875
|
|
|
|
|
const shellNormal = cameraNormal.clone().multiplyScalar(-1);
|
|
|
|
|
const ricochetNormal = shellNormal
|
|
|
|
|
.clone()
|
|
|
|
|
.sub(
|
|
|
|
|
lastLayer.surfaceNormal
|
|
|
|
|
.clone()
|
|
|
|
|
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
caster.set(lastLayer.point, ricochetNormal);
|
|
|
|
|
|
|
|
|
|
const ricochetIntersections = caster.intersectObjects(
|
|
|
|
|
scene.children,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
const outShot = await shoot(
|
|
|
|
|
lastLayer.point,
|
|
|
|
|
ricochetIntersections,
|
|
|
|
|
false,
|
2024-05-27 20:42:48 -05:00
|
|
|
remainingPenetration,
|
2024-05-26 14:39:16 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
shot.in.status = 'ricochet';
|
|
|
|
|
if (outShot === null) {
|
|
|
|
|
shot.damage = 0;
|
|
|
|
|
shot.out = {
|
|
|
|
|
surfaceNormal: ricochetNormal,
|
|
|
|
|
layers: [],
|
|
|
|
|
status: 'ricochet',
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
shot.damage = outShot.damage;
|
|
|
|
|
shot.out = outShot.in;
|
2024-05-27 20:42:48 -05:00
|
|
|
shot.out.surfaceNormal = ricochetNormal;
|
2024-05-26 14:39:16 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return shot;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-28 17:41:59 -06:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{props.type === ArmorType.Core &&
|
2024-04-21 11:09:24 -05:00
|
|
|
jsxTree(
|
|
|
|
|
node,
|
|
|
|
|
{
|
|
|
|
|
renderOrder: 0,
|
|
|
|
|
material: omitMaterial,
|
|
|
|
|
userData: {
|
|
|
|
|
type: ArmorType.Core,
|
|
|
|
|
thickness,
|
|
|
|
|
} satisfies ArmorUserData,
|
2024-02-28 17:41:59 -06:00
|
|
|
|
2024-04-21 11:09:24 -05:00
|
|
|
async onClick(event) {
|
|
|
|
|
event.stopPropagation();
|
2024-02-28 17:41:59 -06:00
|
|
|
|
2024-05-26 14:39:16 -05:00
|
|
|
const shot = (await shoot(
|
|
|
|
|
event.point,
|
|
|
|
|
event.intersections,
|
|
|
|
|
true,
|
|
|
|
|
))!;
|
2024-04-21 11:09:24 -05:00
|
|
|
|
2024-07-09 14:43:33 -05:00
|
|
|
tankopediaEphemeralStore.setState({ shot });
|
2024-04-21 11:09:24 -05:00
|
|
|
},
|
2024-02-28 17:41:59 -06:00
|
|
|
},
|
2024-04-21 11:09:24 -05:00
|
|
|
node.uuid,
|
|
|
|
|
)}
|
2024-02-28 17:41:59 -06:00
|
|
|
|
|
|
|
|
{props.type === ArmorType.Spaced && (
|
|
|
|
|
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{props.type === ArmorType.External && (
|
|
|
|
|
<SpacedArmorSubExternal
|
|
|
|
|
node={node}
|
|
|
|
|
thickness={thickness}
|
|
|
|
|
variant={props.variant}
|
2024-04-13 10:36:13 -05:00
|
|
|
clip={clip}
|
2024-02-28 17:41:59 -06:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|