mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: shot display
This commit is contained in:
1 parent
3438e51ef1
commit
7f8797a449
3 files changed
+113
-35
No files matched your search
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user