feat: dispaly

This commit is contained in:
TrèsAbhi committed 2024-05-25 14:21:36 -05:00
1 parent dc50c4a758
commit 3438e51ef1
5 files changed
+139 -325

No files matched your search

@@ -111,6 +111,9 @@ export function Options() {
mutateDuel((draft) => {
draft.antagonist!.shell = thisShell;
});
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
>
<img
@@ -138,6 +141,9 @@ export function Options() {
? 0
: 1;
});
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
>
<img
@@ -162,6 +168,9 @@ export function Options() {
? 0
: -1;
});
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}}
>
<img
@@ -1,26 +0,0 @@
import { Button } from '@radix-ui/themes';
import { ComponentProps } from 'react';
import { asset } from '../../../../../../../core/blitzkit/asset';
interface QuickEquipmentButtonProps extends ComponentProps<typeof Button> {
equipment: number;
active: boolean;
}
export function QuickEquipmentButton({
equipment,
active,
...props
}: QuickEquipmentButtonProps) {
return (
<Button variant="ghost" {...props}>
<img
alt={`Equipment ${equipment}`}
style={{ opacity: active ? 1 : 0.5 }}
src={asset(`icons/equipment/${equipment}.webp`)}
width={24}
height={24}
/>
</Button>
);
}
+123 -297
View File
@@ -1,27 +1,9 @@
import { Card, Flex, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import { Mesh } from 'three';
import { radToDeg } from 'three/src/math/MathUtils';
import { canSplash } from '../../../core/blitz/canSplash';
import { isExplosive } from '../../../core/blitz/isExplosive';
import { NaNFallback } from '../../../core/math/NaNFallback';
import { normalToEuler } from '../../../core/math/normalToEuler';
import { AngledPenetration } from '../../../icons/AngledPenetration';
import { Block } from '../../../icons/Block';
import { NominalPenetration } from '../../../icons/NominalPenetration';
import { Ricochet } from '../../../icons/Ricochet';
import { useDuel } from '../../../stores/duel';
import {
ShotLayer,
ShotLayerBase,
ShotLayerGap,
ShotLayerNonExternal,
mutateTankopediaTemporary,
useTankopediaTemporary,
} from '../../../stores/tankopedia';
import { ComponentProps } from 'react';
import { Shot, useTankopediaTemporary } from '../../../stores/tankopedia';
import { ArmorType } from './SpacedArmorScene';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
const layerTypeNames: Record<ArmorType | 'null', string> = {
[ArmorType.Core]: 'Core',
@@ -30,305 +12,149 @@ const layerTypeNames: Record<ArmorType | 'null', string> = {
null: 'Gap',
};
const externalLayerNames: Record<ExternalModuleVariant, string> = {
gun: 'Gun',
track: 'Tracks',
};
const shotStatusNames: Record<Shot['status'], string> = {
blocked: 'Blocked',
penetration: 'penetration',
ricochet: 'ricochet',
splash: 'splash',
};
const shotStatusColors: Record<
Shot['status'],
ComponentProps<typeof Text>['color']
> = {
blocked: 'red',
penetration: 'green',
ricochet: 'yellow',
splash: 'orange',
};
const LENGTH_INFINITY = 4;
const TRACER_THICK = 1 / 64;
const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() {
return null;
const shot = useTankopediaTemporary((state) => state.shot);
const preMidPointTracer = useRef<Mesh>(null);
const postMidPointTracer = useRef<Mesh>(null);
const hasMultipleLayers = shot?.layersIn.some(
(layer, index) => index !== 0 && layer.type !== null,
);
const { shell } = useDuel.getState().antagonist!;
const explosive = isExplosive(shell.type);
const splashing = canSplash(shell.type);
const midPointLayer = (shot?.layersIn.findLast(
(layer) => layer.type !== null && layer.status === 'ricochet',
) ??
shot?.layersIn.findLast((layer) => layer.type !== null)) as ShotLayerBase;
const firstLayer = shot?.layersIn[0] as Exclude<ShotLayer, ShotLayerGap>;
const lastLayer = shot?.layersIn.at(-1)!;
const distanceFromSpacedArmor = shot
? firstLayer.point.distanceTo(shot.point)
: undefined;
const preMidPointLayers = shot?.layersIn.slice(
0,
shot.layersIn.indexOf(midPointLayer as ShotLayer),
);
const postMidPointLayers = shot?.layersIn.slice(
shot.layersIn.indexOf(midPointLayer as ShotLayer),
shot.layersIn.length,
);
const preMidPointGap = shot
? preMidPointLayers!.reduce((accumulator, layer) => {
if (layer.type === null) return accumulator + layer.distance;
return accumulator;
}, 0) + LENGTH_INFINITY
: undefined;
const preMidPointThickness = shot
? preMidPointLayers!.reduce(
(accumulator, layer) =>
accumulator +
(layer.type === null
? 0
: layer.type === ArmorType.External
? layer.thickness
: layer.thicknessAngled),
0,
)
: undefined;
const postMidPointGap = shot
? postMidPointLayers!.reduce((accumulator, layer) => {
if (layer.type === null) return accumulator + layer.distance;
return accumulator;
}, 0) || LENGTH_INFINITY
: undefined;
const spacedBlockedHE =
shell.type === 'he' && postMidPointGap && postMidPointGap > 0;
useFrame(({ clock }) => {
if (!shot || !preMidPointTracer.current) return;
const t1 = clock.elapsedTime % 1;
const t2 = (clock.elapsedTime + 0.5) % 1;
preMidPointTracer.current.position.set(0, (1 - t1) * preMidPointGap!, 0);
preMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t1 - 0.5), 1);
if (!postMidPointTracer.current) return;
postMidPointTracer.current.position.set(0, t2 * postMidPointGap!, 0);
postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1);
});
useEffect(() => {
function resetShot() {
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}
const unsubscribes = [
useDuel.subscribe((state) => state.protagonist?.tank, resetShot),
useDuel.subscribe((state) => state.antagonist?.tank, resetShot),
];
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
});
if (!shot) return null;
return (
<>
<group
position={midPointLayer.point}
rotation={normalToEuler(midPointLayer.shellNormal)}
>
<mesh position={[0, preMidPointGap! / 2, 0]}>
<cylinderGeometry args={[TRACER_THIN, TRACER_THIN, preMidPointGap]} />
<meshBasicMaterial depthTest={false} />
</mesh>
<mesh position={[0, preMidPointGap! / 2, 0]} ref={preMidPointTracer}>
<cylinderGeometry
args={[TRACER_THICK, TRACER_THICK, preMidPointGap]}
/>
<meshBasicMaterial
color={
lastLayer.status === 'blocked'
? 0xff4040
: lastLayer.status === 'penetration'
? 0x00ff00
: 0xffff00
}
/>
</mesh>
</group>
{midPointLayer.status === 'ricochet' && (
<group
position={midPointLayer.point}
rotation={normalToEuler(
midPointLayer.shellNormal
.clone()
.reflect(midPointLayer.surfaceNormal)
.multiplyScalar(-1),
)}
>
<mesh position={[0, postMidPointGap! / 2, 0]}>
<cylinderGeometry
args={[TRACER_THIN, TRACER_THIN, postMidPointGap]}
/>
<meshBasicMaterial depthTest={false} />
</mesh>
<mesh
position={[0, postMidPointGap! / 2, 0]}
ref={postMidPointTracer}
>
<cylinderGeometry
args={[TRACER_THICK, TRACER_THICK, postMidPointGap]}
/>
<meshBasicMaterial color={0xffff00} />
</mesh>
</group>
)}
{shot.layersIn.map((layer, index) => {
if (layer.type === null) return;
const shellRotation = normalToEuler(layer.shellNormal);
const nextLayer = shot.layersIn[index + 1] as ShotLayerGap | undefined;
const trueStatus =
nextLayer?.status === 'blocked' ? 'blocked' : layer.status;
return (
<>
<group
key={`${index}-line`}
position={layer.point}
rotation={shellRotation}
>
<Html
center
style={{
pointerEvents: 'none',
}}
>
<div
style={{
backgroundColor:
trueStatus === 'penetration'
? '#00ff0080'
: trueStatus === 'blocked'
? '#ff000080'
: '#ffff0080',
borderRadius: 8,
border: 'none',
padding: 4,
width: hasMultipleLayers ? undefined : 8,
height: hasMultipleLayers ? undefined : 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{hasMultipleLayers && (
<Text
size="2"
style={{
width: '100%',
display: 'block',
textAlign: 'center',
color: trueStatus === 'ricochet' ? 'black' : undefined,
}}
>
<b>{layer.index + 1}</b>
</Text>
)}
</div>
</Html>
</group>
</>
);
})}
<group>
<Html
position={midPointLayer.point}
position={shot.point}
style={{
transform: 'translateY(-50%)',
marginLeft: 32,
pointerEvents: 'none',
transform: 'translateY(-50%)',
}}
>
<Card style={{ width: 256 + 32 }}>
<Flex direction="column">
<Text>
{lastLayer.status[0].toUpperCase()}
{lastLayer.status.slice(1)}
{lastLayer.status === 'penetration' &&
!spacedBlockedHE &&
`: ${Math.round(shell.damage.armor)}hp`}
{(lastLayer.status === 'blocked' || spacedBlockedHE) &&
shell.type === 'he' &&
`: ${NaNFallback(
Math.max(
0,
Math.round(
0.5 *
shell.damage.armor *
(1 -
distanceFromSpacedArmor! / shell.explosionRadius!) -
1.1 *
((midPointLayer as ShotLayerNonExternal)
.thicknessAngled +
preMidPointThickness!),
),
),
0,
)}hp`}
<Card ml="9" style={{ width: 300 }}>
<Flex direction="column" gap="1">
<Text color={shotStatusColors[shot.status]} weight="bold">
{shot.status !== 'blocked' && (
<>
{Math.round(shot.damage).toLocaleString()}
<Text size="1">hp</Text>
</>
)}
{` ${shotStatusNames[shot.status]} `}
</Text>
{shot.layersIn.map((layer, index) => {
if (layer.type === null && (!explosive || splashing)) return null;
<Flex direction="column">
{shot.layersIn.map((layer, index) => {
const layerName =
layer.type === ArmorType.External
? externalLayerNames[layer.variant]
: layerTypeNames[`${layer.type}`];
const layerIndex =
layer.type === null
? null
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor = shotStatusColors[layer.status];
return (
<Flex key={index} align="center" gap="2">
<Flex>
{hasMultipleLayers && (
<Text size="2" style={{ width: 16 }}>
{layer.type === null ? null : `${layer.index + 1}.`}
return (
<Flex align="center" gap="2">
<Text
size="1"
color="gray"
style={{
width: 12,
}}
>
{layerIndex}
</Text>
<Text
size="2"
color={shotStatusColor}
style={{ width: 47 }}
>
{layerName}
</Text>
{layer.type === null && (
<>
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.distance * 1000).toLocaleString()}
<Text size="1">mm</Text>
</Text>
<Text size="2" color="gray">
{Math.max(-100, Math.round(-50 * layer.distance))}%
penetration
</Text>
</>
)}
{layer.type === ArmorType.External && (
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.thickness).toLocaleString()}
<Text size="1">mm</Text>
</Text>
)}
<Text size="2">{layerTypeNames[`${layer.type}`]}</Text>
{(layer.type === ArmorType.Core ||
layer.type === ArmorType.Spaced) && (
<>
<Text
size="2"
color={shotStatusColor}
style={{
width: 64,
}}
>
{Math.round(layer.thicknessAngled).toLocaleString()}
<Text size="1">mm</Text>
</Text>
<Text size="2" color="gray">
{Math.round(layer.thickness).toLocaleString()}
<Text size="1">mm</Text> nominal
</Text>
</>
)}
</Flex>
{layer.type !== null && (
<>
<Flex align="center" style={{ width: 64 }}>
<NominalPenetration width={16} height={16} />
<Text size="2">{Math.round(layer.thickness)}mm</Text>
</Flex>
{layer.type !== ArmorType.External && (
<Flex align="center">
{layer.status === 'ricochet' ? (
<Ricochet width={16} height={16} />
) : layer.status === 'blocked' ? (
<Block width={16} height={16} />
) : (
<AngledPenetration width={16} height={16} />
)}
<Text size="2">
{Math.round(layer.thicknessAngled)}mm (
{Math.round(radToDeg(layer.angle))}°)
</Text>
</Flex>
)}
</>
)}
{layer.type === null && (
<Text size="2">
{Math.round(layer.distance * 1000)}mm (-
{Math.round(Math.min(100, layer.distance * 50))}%
penetration)
</Text>
)}
</Flex>
);
})}
);
})}
</Flex>
</Flex>
</Card>
</Html>
</>
</group>
);
}
@@ -93,14 +93,15 @@ export function SpacedArmorSceneComponent({
async onClick(event) {
event.stopPropagation();
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 { antagonist, protagonist } = useDuel.getState();
const shell = antagonist!.shell;
const cameraNormal = camera.position
.clone()
.sub(event.point)
@@ -185,6 +186,7 @@ export function SpacedArmorSceneComponent({
point: intersection.point,
surfaceNormal: intersection.face!.normal,
status: blocked ? 'blocked' : 'penetration',
variant: layer.variant,
});
} else {
const thickness = layer.thickness;
+3
View File
@@ -5,6 +5,7 @@ import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { ENVIRONMENTS } from '../app/tools/tankopedia/[id]/components/Lighting';
import { ArmorType } from '../components/Armor/components/SpacedArmorScene';
import { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent';
import { TankClass, TreeType } from '../components/Tanks';
import { Tier } from '../core/blitzkit/tankDefinitions';
@@ -56,6 +57,7 @@ export interface ShotLayerBase {
interface ShotLayerExternal extends ShotLayerBase {
type: ArmorType.External;
variant: ExternalModuleVariant;
}
export interface ShotLayerNonExternal extends ShotLayerBase {
@@ -74,6 +76,7 @@ export type ShotLayer = ShotLayerExternal | ShotLayerNonExternal | ShotLayerGap;
export type Shot = {
status: 'penetration' | 'blocked' | 'ricochet' | 'splash';
containsGaps: boolean;
damage: number;
point: Vector3;
layersIn: ShotLayer[];