2025-09-01 22:56:39 -05:00
|
|
|
import { I_HAT, J_HAT } from "@blitzkit/core";
|
|
|
|
|
import { OrbitControls } from "@react-three/drei";
|
|
|
|
|
import { invalidate, useFrame, useThree } from "@react-three/fiber";
|
2025-09-30 16:34:08 -05:00
|
|
|
import type { QuicklimeEvent } from "quicklime";
|
2025-09-01 22:56:39 -05:00
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
|
import { PerspectiveCamera, Vector3 } from "three";
|
|
|
|
|
import { OrbitControls as OrbitControlsClass } from "three-stdlib";
|
|
|
|
|
import { Duel } from "../../../../../../stores/duel";
|
2026-09-23 17:25:51 -05:00
|
|
|
import { api } from "../api/dynamic";
|
|
|
|
|
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
|
|
|
|
|
import { hasEquipment } from "../core/blitzkit/hasEquipment";
|
|
|
|
|
import { Pose, poseEvent } from "../core/blitzkit/pose";
|
|
|
|
|
import { controlsEnabledEvent } from "../core/controlsEnabled";
|
|
|
|
|
import { Tankopedia } from "../stores/tankopedia";
|
2024-01-03 12:21:30 -06:00
|
|
|
|
2024-01-20 16:59:18 -06:00
|
|
|
const poseDistances: Record<Pose, number> = {
|
|
|
|
|
[Pose.HullDown]: 15,
|
|
|
|
|
[Pose.FaceHug]: 5,
|
|
|
|
|
[Pose.Default]: -1,
|
|
|
|
|
};
|
2024-01-19 22:46:10 -06:00
|
|
|
|
2026-06-26 14:01:39 -05:00
|
|
|
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
|
|
|
|
|
|
2026-08-30 14:26:08 -05:00
|
|
|
const modelDefinitions = await api.models();
|
2024-10-05 16:44:07 -05:00
|
|
|
|
2024-12-14 22:32:28 -06:00
|
|
|
interface ControlsProps {
|
2025-05-04 13:08:28 -05:00
|
|
|
autoRotate?: boolean;
|
|
|
|
|
zoomable?: boolean;
|
|
|
|
|
enableRotate?: boolean;
|
2024-12-14 22:32:28 -06:00
|
|
|
}
|
|
|
|
|
|
2026-09-23 17:25:51 -05:00
|
|
|
export function CanvasControl({
|
2025-05-04 13:08:28 -05:00
|
|
|
autoRotate = true,
|
|
|
|
|
zoomable = true,
|
|
|
|
|
enableRotate = true,
|
|
|
|
|
}: ControlsProps) {
|
2024-12-12 19:06:06 -06:00
|
|
|
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
2024-05-03 15:30:17 -05:00
|
|
|
const canvas = useThree((state) => state.gl.domElement);
|
2024-01-03 12:21:30 -06:00
|
|
|
const orbitControls = useRef<OrbitControlsClass>(null);
|
2024-12-01 20:01:19 -06:00
|
|
|
const protagonistTurret = Duel.use((state) => state.protagonist.turret);
|
|
|
|
|
const antagonistTurret = Duel.use((state) => state.antagonist.turret);
|
|
|
|
|
const protagonistTrack = Duel.use((state) => state.protagonist.track);
|
|
|
|
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
|
|
|
|
const antagonistTank = Duel.use((state) => state.antagonist.tank);
|
|
|
|
|
const protagonistGun = Duel.use((state) => state.protagonist.gun);
|
2024-01-20 16:59:18 -06:00
|
|
|
const protagonistModelDefinition =
|
2024-12-01 20:01:19 -06:00
|
|
|
modelDefinitions.models[protagonistTank.id];
|
2024-02-08 11:08:22 -06:00
|
|
|
const protagonistTrackModelDefinition =
|
2024-12-01 20:01:19 -06:00
|
|
|
modelDefinitions.models[protagonistTank.id].tracks[protagonistTrack.id];
|
|
|
|
|
const antagonistModelDefinition = modelDefinitions.models[antagonistTank.id];
|
2024-01-20 16:59:18 -06:00
|
|
|
const protagonistTurretModelDefinition =
|
2024-12-01 20:01:19 -06:00
|
|
|
protagonistModelDefinition.turrets[protagonistTurret.id];
|
2024-01-20 16:59:18 -06:00
|
|
|
const antagonistTurretModelDefinition =
|
2024-12-01 20:01:19 -06:00
|
|
|
antagonistModelDefinition.turrets[antagonistTurret.id];
|
2024-01-20 16:59:18 -06:00
|
|
|
const protagonistGunModelDefinition =
|
2025-08-18 12:06:14 -05:00
|
|
|
protagonistTurretModelDefinition.guns[protagonistGun.id];
|
2024-01-22 16:54:07 -06:00
|
|
|
const protagonistHullOrigin = new Vector3(
|
2026-03-31 14:11:45 -05:00
|
|
|
protagonistTrackModelDefinition.origin!.x,
|
|
|
|
|
protagonistTrackModelDefinition.origin!.y,
|
|
|
|
|
-protagonistTrackModelDefinition.origin!.z,
|
2024-01-22 16:54:07 -06:00
|
|
|
);
|
2024-01-20 16:59:18 -06:00
|
|
|
const protagonistTurretOrigin = new Vector3(
|
2026-03-31 14:11:45 -05:00
|
|
|
protagonistModelDefinition.turret_origin!.x,
|
|
|
|
|
protagonistModelDefinition.turret_origin!.y,
|
|
|
|
|
-protagonistModelDefinition.turret_origin!.z,
|
2024-01-19 22:46:10 -06:00
|
|
|
);
|
2024-01-20 16:59:18 -06:00
|
|
|
const protagonistGunOrigin = new Vector3(
|
2026-03-31 14:11:45 -05:00
|
|
|
protagonistTurretModelDefinition.gun_origin!.x,
|
|
|
|
|
protagonistTurretModelDefinition.gun_origin!.y,
|
|
|
|
|
-protagonistTurretModelDefinition.gun_origin!.z,
|
2024-01-19 22:46:10 -06:00
|
|
|
);
|
2024-01-20 16:59:18 -06:00
|
|
|
const antagonistGunHeight =
|
2026-03-31 14:11:45 -05:00
|
|
|
protagonistTrackModelDefinition.origin!.y +
|
|
|
|
|
antagonistModelDefinition.turret_origin!.y +
|
|
|
|
|
antagonistTurretModelDefinition.gun_origin!.y;
|
2025-09-01 22:56:39 -05:00
|
|
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
2025-05-04 22:11:41 -05:00
|
|
|
const doAutoRotate = autoRotate && !disturbed;
|
2024-12-01 21:47:09 -06:00
|
|
|
const gunHeight =
|
2024-12-01 20:01:19 -06:00
|
|
|
protagonistHullOrigin.y +
|
2024-12-01 21:47:09 -06:00
|
|
|
protagonistTurretOrigin.y +
|
|
|
|
|
protagonistGunOrigin.y;
|
2025-05-04 22:11:41 -05:00
|
|
|
|
2024-02-22 18:43:10 -06:00
|
|
|
useEffect(() => {
|
2025-09-30 16:34:08 -05:00
|
|
|
function handleControlsEnabled(event: QuicklimeEvent<boolean>) {
|
|
|
|
|
if (!orbitControls.current) return;
|
|
|
|
|
orbitControls.current.enabled = event.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
controlsEnabledEvent.on(handleControlsEnabled);
|
2024-01-13 16:48:20 -06:00
|
|
|
|
2025-08-29 13:42:44 -05:00
|
|
|
function handlePoseEvent(event: Pose) {
|
|
|
|
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
2024-04-04 13:16:12 -05:00
|
|
|
122,
|
2025-09-02 16:24:13 -05:00
|
|
|
Duel.state.protagonist.tank.equipment_preset,
|
2026-02-18 18:55:38 -06:00
|
|
|
Duel.state.protagonist.equipmentMatrix,
|
2024-04-04 13:16:12 -05:00
|
|
|
);
|
2025-08-29 13:42:44 -05:00
|
|
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
2025-03-11 10:13:02 -05:00
|
|
|
124,
|
2025-09-02 16:24:13 -05:00
|
|
|
Duel.state.protagonist.tank.equipment_preset,
|
2026-02-18 18:55:38 -06:00
|
|
|
Duel.state.protagonist.equipmentMatrix,
|
2025-03-11 10:13:02 -05:00
|
|
|
);
|
2024-03-12 18:00:00 -05:00
|
|
|
|
2024-01-19 22:46:10 -06:00
|
|
|
switch (event) {
|
|
|
|
|
case Pose.HullDown: {
|
2024-01-20 16:59:18 -06:00
|
|
|
const [pitch] = applyPitchYawLimits(
|
|
|
|
|
-Infinity,
|
|
|
|
|
0,
|
2026-03-31 14:11:45 -05:00
|
|
|
protagonistGunModelDefinition.pitch!,
|
2024-01-20 16:59:18 -06:00
|
|
|
protagonistTurretModelDefinition.yaw,
|
2024-03-12 18:00:00 -05:00
|
|
|
hasImprovedVerticalStabilizer,
|
2026-02-18 18:55:38 -06:00
|
|
|
hasDownImprovedVerticalStabilizer,
|
2024-01-20 16:59:18 -06:00
|
|
|
);
|
|
|
|
|
|
2024-01-19 22:46:10 -06:00
|
|
|
camera.position
|
|
|
|
|
.set(0, 0, 0)
|
2024-01-22 16:54:07 -06:00
|
|
|
.add(protagonistHullOrigin)
|
2024-01-20 16:59:18 -06:00
|
|
|
.add(protagonistTurretOrigin)
|
|
|
|
|
.add(protagonistGunOrigin)
|
2024-01-19 22:46:10 -06:00
|
|
|
.add(
|
|
|
|
|
new Vector3(
|
|
|
|
|
0,
|
2024-01-20 16:59:18 -06:00
|
|
|
poseDistances[event] * Math.sin(pitch),
|
2026-02-18 18:55:38 -06:00
|
|
|
poseDistances[event] * -Math.cos(pitch),
|
|
|
|
|
),
|
2024-01-19 22:46:10 -06:00
|
|
|
);
|
2024-01-20 16:59:18 -06:00
|
|
|
camera.lookAt(
|
2024-01-22 16:54:07 -06:00
|
|
|
protagonistHullOrigin
|
|
|
|
|
.clone()
|
|
|
|
|
.add(protagonistTurretOrigin)
|
2026-02-18 18:55:38 -06:00
|
|
|
.add(protagonistGunOrigin),
|
2024-01-20 16:59:18 -06:00
|
|
|
);
|
|
|
|
|
orbitControls.current?.target.set(0, antagonistGunHeight, 0);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case Pose.FaceHug: {
|
|
|
|
|
const [pitch] = applyPitchYawLimits(
|
|
|
|
|
0,
|
|
|
|
|
0,
|
2026-03-31 14:11:45 -05:00
|
|
|
protagonistGunModelDefinition.pitch!,
|
2024-01-20 16:59:18 -06:00
|
|
|
protagonistTurretModelDefinition.yaw,
|
2024-03-12 18:00:00 -05:00
|
|
|
hasImprovedVerticalStabilizer,
|
2026-02-18 18:55:38 -06:00
|
|
|
hasDownImprovedVerticalStabilizer,
|
2024-01-20 16:59:18 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
camera.position
|
|
|
|
|
.set(0, 0, 0)
|
2024-01-22 16:54:07 -06:00
|
|
|
.add(protagonistHullOrigin)
|
2024-01-20 16:59:18 -06:00
|
|
|
.add(protagonistTurretOrigin)
|
|
|
|
|
.add(protagonistGunOrigin)
|
|
|
|
|
.add(
|
|
|
|
|
new Vector3(
|
|
|
|
|
0,
|
|
|
|
|
poseDistances[event] * Math.sin(pitch),
|
2026-02-18 18:55:38 -06:00
|
|
|
poseDistances[event] * -Math.cos(pitch),
|
|
|
|
|
),
|
2024-01-20 16:59:18 -06:00
|
|
|
);
|
|
|
|
|
orbitControls.current?.target
|
|
|
|
|
.set(0, 0, 0)
|
2024-01-22 16:54:07 -06:00
|
|
|
.add(protagonistHullOrigin)
|
2024-01-20 16:59:18 -06:00
|
|
|
.add(protagonistTurretOrigin)
|
|
|
|
|
.add(protagonistGunOrigin)
|
|
|
|
|
.add(
|
|
|
|
|
new Vector3(
|
|
|
|
|
0,
|
|
|
|
|
0.5 * poseDistances[event] * Math.sin(pitch),
|
2026-02-18 18:55:38 -06:00
|
|
|
0.5 * poseDistances[event] * -Math.cos(pitch),
|
|
|
|
|
),
|
2024-01-20 16:59:18 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case Pose.Default: {
|
2025-05-04 21:02:50 -05:00
|
|
|
camera.position.copy(inspectModeInitialPosition);
|
2024-02-22 18:36:21 -06:00
|
|
|
orbitControls.current?.target.set(0, 1.25, 0);
|
2024-01-20 16:59:18 -06:00
|
|
|
break;
|
2024-01-19 22:46:10 -06:00
|
|
|
}
|
|
|
|
|
}
|
2024-11-11 12:55:39 -06:00
|
|
|
|
|
|
|
|
invalidate();
|
2024-01-19 22:46:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
poseEvent.on(handlePoseEvent);
|
|
|
|
|
|
|
|
|
|
return () => {
|
2025-09-30 16:34:08 -05:00
|
|
|
controlsEnabledEvent.off(handleControlsEnabled);
|
2024-01-19 22:46:10 -06:00
|
|
|
poseEvent.off(handlePoseEvent);
|
|
|
|
|
};
|
2024-12-01 20:01:19 -06:00
|
|
|
}, [camera, protagonistTank.id, antagonistTank.id]);
|
2024-01-03 12:21:30 -06:00
|
|
|
|
2024-05-03 15:30:17 -05:00
|
|
|
useEffect(() => {
|
2024-12-12 16:03:34 -06:00
|
|
|
function handleWheel(event: WheelEvent) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleScroll(event: Event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-14 19:26:41 -05:00
|
|
|
function disturb() {
|
2025-09-01 22:56:39 -05:00
|
|
|
Tankopedia.mutate((draft) => {
|
2025-04-14 19:26:41 -05:00
|
|
|
draft.disturbed = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
poseEvent.on(disturb);
|
2025-09-01 22:56:39 -05:00
|
|
|
canvas.addEventListener("pointerdown", disturb);
|
|
|
|
|
canvas.addEventListener("wheel", handleWheel);
|
|
|
|
|
document.body.addEventListener("scroll", handleScroll);
|
2024-12-12 16:03:34 -06:00
|
|
|
|
2024-12-12 19:06:06 -06:00
|
|
|
function updateCamera() {
|
|
|
|
|
if (!orbitControls.current) return;
|
|
|
|
|
|
|
|
|
|
orbitControls.current.rotateSpeed = 0.25;
|
|
|
|
|
|
2025-08-06 12:23:57 -05:00
|
|
|
camera.position.copy(inspectModeInitialPosition);
|
2025-11-27 17:24:12 -06:00
|
|
|
orbitControls.current.target.set(0, gunHeight * 0.6, 0);
|
2025-08-06 12:23:57 -05:00
|
|
|
orbitControls.current.enablePan = true;
|
|
|
|
|
orbitControls.current.enableZoom = true;
|
2025-11-27 17:24:12 -06:00
|
|
|
camera.fov = 25;
|
2024-12-12 19:06:06 -06:00
|
|
|
|
|
|
|
|
camera.updateProjectionMatrix();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateCamera();
|
|
|
|
|
|
2024-05-03 15:30:17 -05:00
|
|
|
return () => {
|
2025-09-01 22:56:39 -05:00
|
|
|
canvas.removeEventListener("pointerdown", disturb);
|
2025-04-14 19:26:41 -05:00
|
|
|
poseEvent.off(disturb);
|
2025-09-01 22:56:39 -05:00
|
|
|
canvas.removeEventListener("wheel", handleWheel);
|
|
|
|
|
document.body.removeEventListener("scroll", handleScroll);
|
2024-05-03 15:30:17 -05:00
|
|
|
};
|
2025-08-06 12:23:57 -05:00
|
|
|
}, []);
|
2024-05-03 15:30:17 -05:00
|
|
|
|
2024-01-06 18:17:04 -06:00
|
|
|
return (
|
2026-06-26 14:01:39 -05:00
|
|
|
<>
|
|
|
|
|
{doAutoRotate && <Animator />}
|
|
|
|
|
|
|
|
|
|
<OrbitControls
|
|
|
|
|
enableRotate={enableRotate}
|
|
|
|
|
maxDistance={40}
|
|
|
|
|
minDistance={5}
|
|
|
|
|
enableZoom={zoomable}
|
|
|
|
|
zoomSpeed={zoomable ? undefined : 0}
|
|
|
|
|
ref={orbitControls}
|
|
|
|
|
enabled={controlsEnabledEvent.last!}
|
|
|
|
|
enableDamping={false}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
2024-01-06 18:17:04 -06:00
|
|
|
);
|
2024-01-03 12:21:30 -06:00
|
|
|
}
|
2026-06-26 14:01:39 -05:00
|
|
|
|
|
|
|
|
function Animator() {
|
|
|
|
|
const clock = useThree((state) => state.clock);
|
|
|
|
|
const t0 = useRef(clock.elapsedTime);
|
|
|
|
|
|
|
|
|
|
useFrame(({ camera }) => {
|
|
|
|
|
const t = clock.elapsedTime - t0.current;
|
|
|
|
|
|
|
|
|
|
camera.position
|
|
|
|
|
.copy(inspectModeInitialPosition)
|
|
|
|
|
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
|
|
|
|
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
|
|
|
|
|
|
|
|
|
invalidate();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|