Files
blitzkit/packages/website/src/components/TankopediaSandbox/index.tsx
T
2026-11-10 14:54:22 -06:00

228 lines
7.2 KiB
TypeScript

import type { QuicklimeEvent } from "quicklime";
import { Suspense, useEffect, useRef } from "react";
import { applyPitchYawLimits } from "../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../../core/blitzkit/modelTransform";
import { Pose, poseEvent } from "../../core/blitzkit/pose";
import { DuelModule, useDuel } from "../../hooks/useDuel";
import { useEquipment } from "../../hooks/useEquipment";
import { useModel } from "../../hooks/useModel";
import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import type { ComputedCharacteristics } from "../../types/characteristics";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { ArmorPlateDisplay } from "../ArmorPlateDisplay";
import { CanvasAutoClear } from "../CanvasAutoClear";
import { CanvasControl } from "../CanvasControl";
import { InitialCameraAligner } from "../InitialCameraAligner";
import { Lighting, transitionEvent } from "../Lighting";
import { SceneProps } from "../SceneProps";
import { ShotCard } from "../ShotCard";
import { SmartCanvas } from "../SmartCanvas";
import { TankDynamicArmor } from "../TankDynamicArmor";
import { TankModel } from "../TankModel";
import { TankopediaAntagonistControl } from "../TankopediaAntagonistControl";
import { TankopediaOptions } from "../TankopediaOptions";
import { TankopediaQuickInputs } from "../TankopediaQuickInputs";
import { TankopediaSkinSwitcher } from "../TankopediaSkinSwitcher";
import { TankSandboxLoader } from "../TankSandboxLoader";
import { TankStaticArmor, type ThicknessRange } from "../TankStaticArmor";
import { Thicknesses } from "../Thicknesses";
import styles from "./index.module.css";
type TankSandboxProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange;
antagonistCharacteristics: ComputedCharacteristics;
};
export function TankopediaSandbox({
skeleton,
thicknessRange,
antagonistCharacteristics,
}: TankSandboxProps) {
const wrapper = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment("protagonist", 122);
const hasDownImprovedVerticalStabilizer = useEquipment("protagonist", 124);
const gun = useDuel("protagonist", DuelModule.Gun);
const turret = useDuel("protagonist", DuelModule.Turret);
const model = Tankopedia.use((state) => state.protagonist.model);
const turretModelDefinition = model.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const display = Tankopedia.use((state) => state.loaders.display.loaded);
function handlePointerDown() {
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
event.preventDefault();
}
function handlePointerUp(event: PointerEvent) {
event.preventDefault();
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
useEffect(() => {
function handlePoseEvent(pose: Pose) {
switch (pose) {
case Pose.HullDown: {
const [pitch, yaw] = applyPitchYawLimits(
-Infinity,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
case Pose.FaceHug: {
const [pitch, yaw] = applyPitchYawLimits(
Infinity,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
case Pose.Default:
const [pitch, yaw] = applyPitchYawLimits(
0,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
if (!wrapper.current) return;
}
poseEvent.on(handlePoseEvent);
transitionEvent.on(handleTransitionEvent);
return () => {
poseEvent.off(handlePoseEvent);
transitionEvent.off(handleTransitionEvent);
};
}, []);
useEffect(() => {
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
}, [gun, turret]);
useEffect(() => {
if (display !== TankopediaDisplay.DynamicArmor) {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
if (display !== TankopediaDisplay.StaticArmor) {
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
}, [display]);
return (
<div className={styles.wrapper} ref={wrapper}>
<SmartCanvas
className={styles.canvas}
resize={{ debounce: 0 }}
ref={canvas}
gl={{
clippingPlanes: [],
localClippingEnabled: true,
preserveDrawingBuffer: true,
}}
shadows="soft"
onPointerDown={handlePointerDown}
onPointerMissed={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
}}
>
{!skeleton && <Content thicknessRange={thicknessRange} />}
</SmartCanvas>
<TankSandboxLoader />
<TankopediaSkinSwitcher />
<TankopediaQuickInputs />
<Thicknesses thicknessRange={thicknessRange} />
<TankopediaAntagonistControl
antagonistCharacteristics={antagonistCharacteristics}
/>
<TankopediaOptions />
</div>
);
}
function Content({ thicknessRange }: { thicknessRange: ThicknessRange }) {
const display = Tankopedia.use((state) => state.loaders.display.loaded);
const tank = useDuel("protagonist", DuelModule.Tank);
const { hasPbr } = useModel(tank.id);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor
);
return (
<>
<Lighting hasPbr={hasPbr} />
<SceneProps />
{(display === TankopediaDisplay.Model ||
(display === TankopediaDisplay.DynamicArmor &&
!hideTankModelUnderArmor)) && <TankModel />}
{display === TankopediaDisplay.DynamicArmor && <TankDynamicArmor />}
{display === TankopediaDisplay.StaticArmor && (
<TankStaticArmor thicknessRange={thicknessRange} />
)}
<ShotCard />
<ArmorPlateDisplay />
<CanvasAutoClear />
<Suspense>
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
<CanvasControl />
</Suspense>
<InitialCameraAligner />
</>
);
}