mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 12:09:51 +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 { Html } from '@react-three/drei';
|
||||||
import { ComponentProps } from 'react';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import { Shot, useTankopediaTemporary } from '../../../stores/tankopedia';
|
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 { ArmorType } from './SpacedArmorScene';
|
||||||
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
|
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ const externalLayerNames: Record<ExternalModuleVariant, string> = {
|
|||||||
track: 'Tracks',
|
track: 'Tracks',
|
||||||
};
|
};
|
||||||
|
|
||||||
const shotStatusNames: Record<Shot['status'], string> = {
|
const shotStatusNames: Record<ShotStatus, string> = {
|
||||||
blocked: 'Blocked',
|
blocked: 'Blocked',
|
||||||
penetration: 'penetration',
|
penetration: 'penetration',
|
||||||
ricochet: 'ricochet',
|
ricochet: 'ricochet',
|
||||||
@@ -25,7 +28,7 @@ const shotStatusNames: Record<Shot['status'], string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shotStatusColors: Record<
|
const shotStatusColors: Record<
|
||||||
Shot['status'],
|
ShotStatus,
|
||||||
ComponentProps<typeof Text>['color']
|
ComponentProps<typeof Text>['color']
|
||||||
> = {
|
> = {
|
||||||
blocked: 'red',
|
blocked: 'red',
|
||||||
@@ -40,13 +43,31 @@ const TRACER_THIN = TRACER_THICK / 2;
|
|||||||
|
|
||||||
export function ShotDisplay() {
|
export function ShotDisplay() {
|
||||||
const shot = useTankopediaTemporary((state) => state.shot);
|
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;
|
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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<Html
|
<Html
|
||||||
position={shot.point}
|
position={inLast.point}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
@@ -54,18 +75,18 @@ export function ShotDisplay() {
|
|||||||
>
|
>
|
||||||
<Card ml="9" style={{ width: 300 }}>
|
<Card ml="9" style={{ width: 300 }}>
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Text color={shotStatusColors[shot.status]} weight="bold">
|
<Text color={shotStatusColors[shot.in.status]} weight="bold">
|
||||||
{shot.status !== 'blocked' && (
|
{shot.in.status !== 'blocked' && (
|
||||||
<>
|
<>
|
||||||
{Math.round(shot.damage).toLocaleString()}
|
{Math.round(shot.damage).toLocaleString()}
|
||||||
<Text size="1">hp</Text>
|
<Text size="1">hp</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{` ${shotStatusNames[shot.status]} `}
|
{` ${shotStatusNames[shot.in.status]} `}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
{shot.layersIn.map((layer, index) => {
|
{shot.in.layers.map((layer, index) => {
|
||||||
const layerName =
|
const layerName =
|
||||||
layer.type === ArmorType.External
|
layer.type === ArmorType.External
|
||||||
? externalLayerNames[layer.variant]
|
? externalLayerNames[layer.variant]
|
||||||
@@ -74,7 +95,10 @@ export function ShotDisplay() {
|
|||||||
layer.type === null
|
layer.type === null
|
||||||
? null
|
? null
|
||||||
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
||||||
const shotStatusColor = shotStatusColors[layer.status];
|
const shotStatusColor =
|
||||||
|
shot.in.status === 'splash'
|
||||||
|
? shotStatusColors.splash
|
||||||
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align="center" gap="2">
|
<Flex align="center" gap="2">
|
||||||
@@ -155,6 +179,48 @@ export function ShotDisplay() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
</Html>
|
</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>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,17 +95,20 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
const { antagonist, protagonist } = useDuel.getState();
|
const { antagonist, protagonist } = useDuel.getState();
|
||||||
const shell = antagonist!.shell;
|
const shell = antagonist!.shell;
|
||||||
const shot: Shot = {
|
|
||||||
status: 'blocked',
|
|
||||||
damage: -1,
|
|
||||||
containsGaps: shell.type === ShellType.HEAT,
|
|
||||||
layersIn: [],
|
|
||||||
point: event.point,
|
|
||||||
};
|
|
||||||
const cameraNormal = camera.position
|
const cameraNormal = camera.position
|
||||||
.clone()
|
.clone()
|
||||||
.sub(event.point)
|
.sub(event.point)
|
||||||
.normalize();
|
.normalize();
|
||||||
|
const shot: Shot = {
|
||||||
|
damage: -1,
|
||||||
|
containsGaps: shell.type === ShellType.HEAT,
|
||||||
|
in: {
|
||||||
|
cameraNormal,
|
||||||
|
layers: [],
|
||||||
|
status: 'blocked',
|
||||||
|
},
|
||||||
|
point: event.point,
|
||||||
|
};
|
||||||
|
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = await hasEquipment(
|
||||||
103,
|
103,
|
||||||
@@ -157,14 +160,14 @@ export function SpacedArmorSceneComponent({
|
|||||||
const layer = intersection.object.userData;
|
const layer = intersection.object.userData;
|
||||||
|
|
||||||
if (shell.type === ShellType.HEAT && index > 0) {
|
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(
|
const distance = lastLayer.point.distanceTo(
|
||||||
intersection.point,
|
intersection.point,
|
||||||
);
|
);
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
||||||
const blocked = remainingPenetration <= 0;
|
const blocked = remainingPenetration <= 0;
|
||||||
|
|
||||||
shot.layersIn.push({
|
shot.in.layers.push({
|
||||||
type: null,
|
type: null,
|
||||||
distance,
|
distance,
|
||||||
status: blocked ? 'blocked' : 'penetration',
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
@@ -179,7 +182,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const blocked =
|
const blocked =
|
||||||
remainingPenetration <= 0 || shell.type === ShellType.HE;
|
remainingPenetration <= 0 || shell.type === ShellType.HE;
|
||||||
|
|
||||||
shot.layersIn.push({
|
shot.in.layers.push({
|
||||||
type: ArmorType.External,
|
type: ArmorType.External,
|
||||||
index,
|
index,
|
||||||
thickness,
|
thickness,
|
||||||
@@ -213,7 +216,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
if (!threeCalibersRule && angle >= ricochet) {
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
remainingPenetration *= 0.75;
|
remainingPenetration *= 0.75;
|
||||||
|
|
||||||
shot.layersIn.push({
|
shot.in.layers.push({
|
||||||
type: layer.type,
|
type: layer.type,
|
||||||
index,
|
index,
|
||||||
thickness,
|
thickness,
|
||||||
@@ -229,7 +232,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
remainingPenetration -= finalThickness;
|
remainingPenetration -= finalThickness;
|
||||||
const blocked = remainingPenetration <= 0;
|
const blocked = remainingPenetration <= 0;
|
||||||
|
|
||||||
shot.layersIn.push({
|
shot.in.layers.push({
|
||||||
type: layer.type,
|
type: layer.type,
|
||||||
index,
|
index,
|
||||||
thickness,
|
thickness,
|
||||||
@@ -249,11 +252,11 @@ export function SpacedArmorSceneComponent({
|
|||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastLayer = shot.layersIn.at(-1) as ShotLayerNonExternal;
|
const lastLayer = shot.in.layers.at(-1) as ShotLayerNonExternal;
|
||||||
const firstLayer = shot.layersIn[0] as ShotLayerBase;
|
const firstLayer = shot.in.layers[0] as ShotLayerBase;
|
||||||
|
|
||||||
if (shell.type === ShellType.HE) {
|
if (shell.type === ShellType.HE) {
|
||||||
const totalSpacedArmorThickness = shot.layersIn.reduce(
|
const totalSpacedArmorThickness = shot.in.layers.reduce(
|
||||||
(acc, layer) => {
|
(acc, layer) => {
|
||||||
if (layer.type === null || layer.type === ArmorType.Core) {
|
if (layer.type === null || layer.type === ArmorType.Core) {
|
||||||
return acc;
|
return acc;
|
||||||
@@ -292,25 +295,25 @@ export function SpacedArmorSceneComponent({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shot.layersIn.length > 1 ||
|
shot.in.layers.length > 1 ||
|
||||||
lastLayer.status === 'blocked'
|
lastLayer.status === 'blocked'
|
||||||
) {
|
) {
|
||||||
shot.status = finalDamage > 0 ? 'splash' : 'blocked';
|
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
|
||||||
shot.damage = finalDamage;
|
shot.damage = finalDamage;
|
||||||
} else {
|
} else {
|
||||||
shot.status = 'penetration';
|
shot.in.status = 'penetration';
|
||||||
shot.damage = shell.damage.armor;
|
shot.damage = shell.damage.armor;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (lastLayer.status === 'blocked') {
|
if (lastLayer.status === 'blocked') {
|
||||||
shot.status = 'blocked';
|
shot.in.status = 'blocked';
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
} else if (lastLayer.status === 'penetration') {
|
} else if (lastLayer.status === 'penetration') {
|
||||||
shot.status = 'penetration';
|
shot.in.status = 'penetration';
|
||||||
shot.damage = shell.damage.armor;
|
shot.damage = shell.damage.armor;
|
||||||
} else {
|
} else {
|
||||||
// check second array for result
|
// 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 ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap;
|
||||||
|
|
||||||
export type Shot = {
|
export type Shot = {
|
||||||
status: 'penetration' | 'blocked' | 'ricochet' | 'splash';
|
|
||||||
containsGaps: boolean;
|
containsGaps: boolean;
|
||||||
damage: number;
|
damage: number;
|
||||||
point: Vector3;
|
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 =
|
export type ArmorPiercingLayer =
|
||||||
| {
|
| {
|
||||||
distance: number;
|
distance: number;
|
||||||
|
|||||||
Reference in new issue
Block a user