Files
blitzkit/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx
T

396 lines
12 KiB
TypeScript
Raw Normal View History

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
/>
)}
</>
);
}