feat: shot display

This commit is contained in:
TrèsAbhi committed 2024-05-26 13:42:51 -05:00
1 parent 3438e51ef1
commit 7f8797a449
3 files changed
+113 -35

No files matched your search

+77 -11
View File
@@ -1,7 +1,10 @@
import { Card, Flex, Text } from '@radix-ui/themes';
import { Card, Flex, Kbd, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { ComponentProps } from 'react';
import { Shot, useTankopediaTemporary } from '../../../stores/tankopedia';
import { useFrame } from '@react-three/fiber';
import { ComponentProps, useRef } from 'react';
import { Euler, Mesh, Quaternion } from 'three';
import { J_HAT } from '../../../constants/axis';
import { ShotStatus, useTankopediaTemporary } from '../../../stores/tankopedia';
import { ArmorType } from './SpacedArmorScene';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
@@ -17,7 +20,7 @@ const externalLayerNames: Record<ExternalModuleVariant, string> = {
track: 'Tracks',
};
const shotStatusNames: Record<Shot['status'], string> = {
const shotStatusNames: Record<ShotStatus, string> = {
blocked: 'Blocked',
penetration: 'penetration',
ricochet: 'ricochet',
@@ -25,7 +28,7 @@ const shotStatusNames: Record<Shot['status'], string> = {
};
const shotStatusColors: Record<
Shot['status'],
ShotStatus,
ComponentProps<typeof Text>['color']
> = {
blocked: 'red',
@@ -40,13 +43,31 @@ const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() {
const shot = useTankopediaTemporary((state) => state.shot);
const inTracer = useRef<Mesh>(null);
useFrame(({ clock }) => {
if (!shot || !inTracer.current) return;
const t = clock.elapsedTime % 1;
const scale1 = -Math.abs(2 * t - 1) + 1;
inTracer.current.scale.set(1, scale1, 1);
inTracer.current.position.set(0, (1 - t) * inLength, 0);
});
if (!shot) return null;
const inLength =
shot.in.layers.reduce((accumulator, layer) => {
if (layer.type === null) return accumulator + layer.distance;
return accumulator;
}, 0) + LENGTH_INFINITY;
const inLast = shot.in.layers.findLast((layer) => layer.type !== null)!;
return (
<group>
<Html
position={shot.point}
position={inLast.point}
style={{
pointerEvents: 'none',
transform: 'translateY(-50%)',
@@ -54,18 +75,18 @@ export function ShotDisplay() {
>
<Card ml="9" style={{ width: 300 }}>
<Flex direction="column" gap="1">
<Text color={shotStatusColors[shot.status]} weight="bold">
{shot.status !== 'blocked' && (
<Text color={shotStatusColors[shot.in.status]} weight="bold">
{shot.in.status !== 'blocked' && (
<>
{Math.round(shot.damage).toLocaleString()}
<Text size="1">hp</Text>
</>
)}
{` ${shotStatusNames[shot.status]} `}
{` ${shotStatusNames[shot.in.status]} `}
</Text>
<Flex direction="column">
{shot.layersIn.map((layer, index) => {
{shot.in.layers.map((layer, index) => {
const layerName =
layer.type === ArmorType.External
? externalLayerNames[layer.variant]
@@ -74,7 +95,10 @@ export function ShotDisplay() {
layer.type === null
? null
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor = shotStatusColors[layer.status];
const shotStatusColor =
shot.in.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<Flex align="center" gap="2">
@@ -155,6 +179,48 @@ export function ShotDisplay() {
</Flex>
</Card>
</Html>
{shot.in.layers.map((layer, index) => {
if (layer.type === null) return null;
const layerIndex =
layer.type === null
? null
: (shot.containsGaps ? index / 2 : index) + 1;
const shotStatusColor =
shot.in.status === 'splash'
? shotStatusColors.splash
: shotStatusColors[layer.status];
return (
<Html position={layer.point} center>
<Kbd>
<Text color={shotStatusColor}>{layerIndex}</Text>
</Kbd>
</Html>
);
})}
<group
position={inLast.point}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(J_HAT, shot.in.cameraNormal),
)}
>
<mesh position={[0, inLength / 2, 0]}>
<cylinderGeometry
args={[TRACER_THIN / 2, TRACER_THIN / 2, inLength]}
/>
<meshBasicMaterial depthTest={false} />
</mesh>
<mesh ref={inTracer} position={[0, inLength / 2, 0]}>
<cylinderGeometry
args={[TRACER_THICK / 2, TRACER_THICK / 2, inLength]}
/>
<meshBasicMaterial color="red" />
</mesh>
</group>
</group>
);
}
@@ -95,17 +95,20 @@ export function SpacedArmorSceneComponent({
const { antagonist, protagonist } = useDuel.getState();
const shell = antagonist!.shell;
const shot: Shot = {
status: 'blocked',
damage: -1,
containsGaps: shell.type === ShellType.HEAT,
layersIn: [],
point: event.point,
};
const cameraNormal = camera.position
.clone()
.sub(event.point)
.normalize();
const shot: Shot = {
damage: -1,
containsGaps: shell.type === ShellType.HEAT,
in: {
cameraNormal,
layers: [],
status: 'blocked',
},
point: event.point,
};
const hasCalibratedShells = await hasEquipment(
103,
@@ -157,14 +160,14 @@ export function SpacedArmorSceneComponent({
const layer = intersection.object.userData;
if (shell.type === ShellType.HEAT && index > 0) {
const lastLayer = shot.layersIn.at(-1) as ShotLayerBase;
const lastLayer = shot.in.layers.at(-1) as ShotLayerBase;
const distance = lastLayer.point.distanceTo(
intersection.point,
);
remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration <= 0;
shot.layersIn.push({
shot.in.layers.push({
type: null,
distance,
status: blocked ? 'blocked' : 'penetration',
@@ -179,7 +182,7 @@ export function SpacedArmorSceneComponent({
const blocked =
remainingPenetration <= 0 || shell.type === ShellType.HE;
shot.layersIn.push({
shot.in.layers.push({
type: ArmorType.External,
index,
thickness,
@@ -213,7 +216,7 @@ export function SpacedArmorSceneComponent({
if (!threeCalibersRule && angle >= ricochet) {
remainingPenetration *= 0.75;
shot.layersIn.push({
shot.in.layers.push({
type: layer.type,
index,
thickness,
@@ -229,7 +232,7 @@ export function SpacedArmorSceneComponent({
remainingPenetration -= finalThickness;
const blocked = remainingPenetration <= 0;
shot.layersIn.push({
shot.in.layers.push({
type: layer.type,
index,
thickness,
@@ -249,11 +252,11 @@ export function SpacedArmorSceneComponent({
index++;
}
const lastLayer = shot.layersIn.at(-1) as ShotLayerNonExternal;
const firstLayer = shot.layersIn[0] as ShotLayerBase;
const lastLayer = shot.in.layers.at(-1) as ShotLayerNonExternal;
const firstLayer = shot.in.layers[0] as ShotLayerBase;
if (shell.type === ShellType.HE) {
const totalSpacedArmorThickness = shot.layersIn.reduce(
const totalSpacedArmorThickness = shot.in.layers.reduce(
(acc, layer) => {
if (layer.type === null || layer.type === ArmorType.Core) {
return acc;
@@ -292,25 +295,25 @@ export function SpacedArmorSceneComponent({
*/
if (
shot.layersIn.length > 1 ||
shot.in.layers.length > 1 ||
lastLayer.status === 'blocked'
) {
shot.status = finalDamage > 0 ? 'splash' : 'blocked';
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
shot.damage = finalDamage;
} else {
shot.status = 'penetration';
shot.in.status = 'penetration';
shot.damage = shell.damage.armor;
}
} else {
if (lastLayer.status === 'blocked') {
shot.status = 'blocked';
shot.in.status = 'blocked';
shot.damage = 0;
} else if (lastLayer.status === 'penetration') {
shot.status = 'penetration';
shot.in.status = 'penetration';
shot.damage = shell.damage.armor;
} else {
// check second array for result
shot.status = 'ricochet';
shot.in.status = 'ricochet';
}
}
+12 -3
View File
@@ -75,14 +75,23 @@ export interface ShotLayerGap {
export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap;
export type Shot = {
status: 'penetration' | 'blocked' | 'ricochet' | 'splash';
containsGaps: boolean;
damage: number;
point: Vector3;
layersIn: ShotLayer[];
layersOut?: ShotLayer[];
in: {
cameraNormal: Vector3;
status: ShotStatus;
layers: ShotLayer[];
};
out?: {
status: ShotStatus;
layers: ShotLayer[];
};
};
export type ShotStatus = 'penetration' | 'blocked' | 'ricochet' | 'splash';
export type ArmorPiercingLayer =
| {
distance: number;