mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-11 03:36:11 +01:00
228 lines
7.2 KiB
TypeScript
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 />
|
|
</>
|
|
);
|
|
}
|