mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-14 05:06:11 +01:00
flatten armor dir
This commit is contained in:
1 parent
e18d6f58f9
commit
e5b44d4eb8
28 files changed
+90
-101
No files matched your search
@@ -0,0 +1,317 @@
|
||||
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import { useEffect, useRef, type ComponentProps, type RefObject } from "react";
|
||||
import {
|
||||
BufferGeometry,
|
||||
DoubleSide,
|
||||
Euler,
|
||||
Group,
|
||||
Line,
|
||||
LineDashedMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Path,
|
||||
Quaternion,
|
||||
} from "three";
|
||||
import { isHalloween } from "../core/blitzkit/isHalloween";
|
||||
import { LocaleProvider, useLocale } from "../hooks/useLocale";
|
||||
import {
|
||||
Tankopedia,
|
||||
type ShotLayerNonExternal,
|
||||
type ShotStatus,
|
||||
} from "../stores/tankopedia";
|
||||
import { ShotDisplayCard } from "./ShotDisplayCard";
|
||||
|
||||
export const shotStatusColors: Record<
|
||||
ShotStatus,
|
||||
ComponentProps<typeof Text>["color"]
|
||||
> = {
|
||||
blocked: "red",
|
||||
penetration: "green",
|
||||
ricochet: "yellow",
|
||||
splash: "orange",
|
||||
};
|
||||
|
||||
const SURFACE_POINT_SIZE = 12;
|
||||
export const SHOT_DISPLAY_LENGTH_INFINITY = 4;
|
||||
const TRACER_THICK = 1 / 32;
|
||||
const TRACER_THIN = TRACER_THICK / 2;
|
||||
|
||||
export function ShotDisplay() {
|
||||
const shot = Tankopedia.use((state) => state.shot);
|
||||
const inTracer = useRef<Mesh>(null);
|
||||
const outTracer = useRef<Mesh>(null);
|
||||
const splashRadiusWrapper = useRef<Group>(null);
|
||||
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
|
||||
const { locale } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
if (shot?.splashRadius !== undefined) {
|
||||
const outlineGeometry = new BufferGeometry().setFromPoints(
|
||||
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50),
|
||||
);
|
||||
const outlineMaterial = new LineDashedMaterial({
|
||||
color: "orange",
|
||||
dashSize: 0.05,
|
||||
gapSize: 0.025,
|
||||
});
|
||||
const outline = new Line(
|
||||
outlineGeometry,
|
||||
outlineMaterial,
|
||||
).computeLineDistances();
|
||||
|
||||
splashRadiusWrapper.current?.add(outline);
|
||||
|
||||
return () => {
|
||||
splashRadiusWrapper.current?.remove(outline);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isHalloween() &&
|
||||
shot?.splashRadius !== undefined &&
|
||||
shot.in.status === "penetration"
|
||||
) {
|
||||
const audio = new Audio("/assets/audio/lotta-damage.mp3");
|
||||
|
||||
audio.currentTime = 0.7;
|
||||
audio.volume = 0.25;
|
||||
audio.play();
|
||||
}
|
||||
}, [shot]);
|
||||
|
||||
if (!shot) return null;
|
||||
|
||||
const inLength =
|
||||
shot.in.layers.reduce((accumulator, layer) => {
|
||||
if (layer.type === null) return accumulator + layer.distance;
|
||||
return accumulator;
|
||||
}, 0) + SHOT_DISPLAY_LENGTH_INFINITY;
|
||||
const outLength =
|
||||
shot.out && shot.out.layers.length > 0
|
||||
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
|
||||
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
|
||||
)
|
||||
: SHOT_DISPLAY_LENGTH_INFINITY;
|
||||
const inLast = shot.in.layers.findLast(
|
||||
(layer) => layer.type !== null,
|
||||
) as ShotLayerNonExternal;
|
||||
const outTitleColor = shot.out
|
||||
? shotStatusColors[shot.out.status]
|
||||
: undefined;
|
||||
const inTitleColor = shotStatusColors[shot.in.status];
|
||||
const tracerGoingUp =
|
||||
shot.in.surfaceNormal.y + (shot.out?.surfaceNormal.y ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<group>
|
||||
<Animator
|
||||
inTracer={inTracer}
|
||||
outTracer={outTracer}
|
||||
inLength={inLength}
|
||||
outLength={outLength}
|
||||
splashRadiusWrapper={splashRadiusWrapper}
|
||||
splashRadiusMaterial={splashRadiusMaterial}
|
||||
/>
|
||||
|
||||
{shot.splashRadius && (
|
||||
<group
|
||||
position={inLast.point}
|
||||
ref={splashRadiusWrapper}
|
||||
rotation={new Euler().setFromQuaternion(
|
||||
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
|
||||
)}
|
||||
>
|
||||
<mesh>
|
||||
<circleGeometry />
|
||||
<meshBasicMaterial
|
||||
side={DoubleSide}
|
||||
color="orange"
|
||||
transparent
|
||||
ref={splashRadiusMaterial}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
)}
|
||||
|
||||
<Html position={inLast.point} center>
|
||||
<Box
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
||||
}}
|
||||
mb={tracerGoingUp ? "0" : "9"}
|
||||
pb={tracerGoingUp ? "0" : "4"}
|
||||
mt={tracerGoingUp ? "9" : "0"}
|
||||
pt={tracerGoingUp ? "4" : "0"}
|
||||
>
|
||||
<LocaleProvider locale={locale}>
|
||||
<ShotDisplayCard shot={shot} />
|
||||
</LocaleProvider>
|
||||
</Box>
|
||||
</Html>
|
||||
|
||||
{[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => {
|
||||
if (layer.type === null) return null;
|
||||
|
||||
const shotStatusColor =
|
||||
shot.in.status === "splash"
|
||||
? shotStatusColors.splash
|
||||
: shotStatusColors[layer.status];
|
||||
|
||||
return (
|
||||
<Html
|
||||
position={layer.point}
|
||||
key={index}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
transform: "translateY(-50%)",
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
style={{
|
||||
marginLeft: -SURFACE_POINT_SIZE / 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: SURFACE_POINT_SIZE,
|
||||
height: SURFACE_POINT_SIZE,
|
||||
backgroundColor: shotStatusColor,
|
||||
opacity: 0.75,
|
||||
borderRadius: SURFACE_POINT_SIZE / 2,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Html>
|
||||
);
|
||||
})}
|
||||
|
||||
<group
|
||||
position={inLast.point}
|
||||
rotation={new Euler().setFromQuaternion(
|
||||
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal),
|
||||
)}
|
||||
>
|
||||
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
|
||||
<cylinderGeometry
|
||||
args={[TRACER_THIN / 2, TRACER_THIN / 2, inLength]}
|
||||
/>
|
||||
<meshBasicMaterial depthTest={false} transparent />
|
||||
</mesh>
|
||||
|
||||
<mesh ref={inTracer} position={[0, inLength / 2, 0]} renderOrder={3}>
|
||||
<cylinderGeometry
|
||||
args={[TRACER_THICK / 2, TRACER_THICK / 2, inLength]}
|
||||
/>
|
||||
<meshBasicMaterial
|
||||
color={inTitleColor}
|
||||
depthTest={false}
|
||||
transparent
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{shot.out && (
|
||||
<group
|
||||
position={inLast.point}
|
||||
rotation={new Euler().setFromQuaternion(
|
||||
new Quaternion().setFromUnitVectors(
|
||||
J_HAT,
|
||||
shot.out.surfaceNormal.clone(),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
|
||||
<cylinderGeometry
|
||||
args={[TRACER_THIN / 2, TRACER_THIN / 2, outLength]}
|
||||
/>
|
||||
<meshBasicMaterial depthTest={false} transparent />
|
||||
</mesh>
|
||||
|
||||
<mesh
|
||||
ref={outTracer}
|
||||
position={[0, outLength / 2, 0]}
|
||||
renderOrder={3}
|
||||
>
|
||||
<cylinderGeometry
|
||||
args={[TRACER_THICK / 2, TRACER_THICK / 2, outLength]}
|
||||
/>
|
||||
<meshBasicMaterial
|
||||
color={outTitleColor}
|
||||
depthTest={false}
|
||||
transparent
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
interface AnimatorProps {
|
||||
inTracer: RefObject<Mesh | null>;
|
||||
outTracer: RefObject<Mesh | null>;
|
||||
|
||||
splashRadiusWrapper: RefObject<Group | null>;
|
||||
splashRadiusMaterial: RefObject<MeshBasicMaterial | null>;
|
||||
|
||||
inLength: number;
|
||||
outLength: number;
|
||||
}
|
||||
|
||||
function Animator({
|
||||
inTracer,
|
||||
outTracer,
|
||||
splashRadiusWrapper,
|
||||
splashRadiusMaterial,
|
||||
inLength,
|
||||
outLength,
|
||||
}: AnimatorProps) {
|
||||
const shot = Tankopedia.use((state) => state.shot!);
|
||||
|
||||
let animationStartTime: number | null = null;
|
||||
|
||||
useFrame(({ clock, invalidate }) => {
|
||||
const t = clock.elapsedTime;
|
||||
|
||||
const tracerT1 = t % 1;
|
||||
const tracerT2 = (t + 0.5) % 1;
|
||||
|
||||
if (inTracer.current) {
|
||||
inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1);
|
||||
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0);
|
||||
|
||||
if (outTracer.current) {
|
||||
outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1);
|
||||
outTracer.current.position.set(0, tracerT2 * outLength, 0);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
if (
|
||||
shot.splashRadius !== undefined &&
|
||||
splashRadiusWrapper.current &&
|
||||
splashRadiusMaterial.current
|
||||
) {
|
||||
if (animationStartTime === null) animationStartTime = t;
|
||||
|
||||
const scale =
|
||||
clamp(4 * (t - animationStartTime), 0, 1) * shot.splashRadius;
|
||||
const opacity = 1 - clamp(2 * (t - animationStartTime), 0, 1);
|
||||
|
||||
splashRadiusWrapper.current.scale.set(scale, scale, scale);
|
||||
splashRadiusMaterial.current.opacity = opacity;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user