Files
blitzkit/packages/website/src/components/TankModel.tsx
T

286 lines
10 KiB
TypeScript
Raw Normal View History

2025-09-01 22:56:39 -05:00
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
import { useRef } from "react";
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
2026-09-23 17:25:51 -05:00
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { jsxTree } from "../core/blitzkit/jsxTree";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
import { controlsEnabledEvent } from "../core/controlsEnabled";
2026-09-30 19:00:25 -05:00
import { DuelModule, getDuel, useDuel } from "../hooks/useDuel";
2026-09-30 17:56:31 -05:00
import { useDuelModel } from "../hooks/useDuelModel";
import { hasEquipment } from "../hooks/useEquipment";
2026-09-23 17:25:51 -05:00
import { useModel } from "../hooks/useModel";
import { useTankTransform } from "../hooks/useTankTransform";
import { Tankopedia } from "../stores/tankopedia";
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
2024-01-10 13:00:37 -06:00
2024-12-12 18:32:27 -06:00
export function TankModel() {
2026-09-30 17:56:31 -05:00
const protagonist = Tankopedia.use((state) => state.protagonist);
const track = useDuel("protagonist", DuelModule.Track);
const turret = useDuel("protagonist", DuelModule.Turret);
2024-01-13 18:24:04 -06:00
const canvas = useThree((state) => state.gl.domElement);
2025-09-27 16:07:45 -05:00
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
2026-09-30 17:56:31 -05:00
const tankModelDefinition = useDuelModel("protagonist");
2026-09-30 20:59:18 -05:00
const trackModelDefinition = tankModelDefinition.tracks[track.id];
2026-09-30 17:56:31 -05:00
const turretModelDefinition = tankModelDefinition.turrets[protagonist.turret];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun];
const { gltf } = useModel(protagonist.tank);
2024-01-10 13:22:55 -06:00
const nodes = Object.values(gltf.nodes);
2024-01-10 13:00:37 -06:00
useTankTransform(track, turret, turretContainer, gunContainer);
2024-07-30 13:24:55 -05:00
2024-01-10 13:00:37 -06:00
return (
2026-09-30 17:56:31 -05:00
<>
2026-09-30 20:59:18 -05:00
{/* <mesh
scale={0.1}
position={[
trackModelDefinition.origin!.x + tankModelDefinition.turret_origin!.x,
trackModelDefinition.origin!.y + tankModelDefinition.turret_origin!.y,
trackModelDefinition.origin!.z + tankModelDefinition.turret_origin!.z,
]}
>
<sphereGeometry />
<meshBasicMaterial color="red" depthTest={false} />
</mesh> */}
2024-01-10 13:00:37 -06:00
{nodes.map((node) => {
2025-09-01 22:56:39 -05:00
const isHull = node.name === "hull";
const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith("chassis_track_");
2024-01-10 13:00:37 -06:00
const isVisible = isHull || isWheel || isTrack;
2024-06-29 13:15:24 -05:00
const position = new Vector2();
const delta = new Vector2();
2024-01-10 13:00:37 -06:00
if (!isVisible) return null;
2024-01-11 21:44:44 -06:00
2024-06-29 13:15:24 -05:00
function translateTexture(offset: number) {
const mesh = node.children[0] as Mesh;
const material = mesh?.material as MeshStandardMaterial;
if (!material) return;
2024-11-10 22:19:05 -06:00
invalidate();
2024-08-28 18:02:20 -05:00
2026-10-04 20:00:51 +13:00
const textures = new Set([
material.map,
material.aoMap,
material.normalMap,
material.roughnessMap,
material.metalnessMap,
]);
for (const texture of textures) {
if (texture) texture.offset.y += offset;
}
2024-06-29 13:15:24 -05:00
}
2024-02-23 15:52:09 -06:00
function onPointerDown(event: ThreeEvent<PointerEvent>) {
2025-11-27 13:20:06 -06:00
if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) {
2024-07-13 14:34:55 -05:00
position.set(event.clientX, event.clientY);
event.stopPropagation();
2024-06-29 13:15:24 -05:00
2025-10-02 10:19:50 -05:00
controlsEnabledEvent.dispatch(false);
2024-06-29 13:15:24 -05:00
2025-09-01 22:56:39 -05:00
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
2024-07-13 14:34:55 -05:00
} else {
event.stopPropagation();
}
2024-06-29 13:15:24 -05:00
}
function handlePointerMove(event: PointerEvent) {
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
const deltaX = delta.x / window.innerWidth;
const deltaY = delta.y / window.innerHeight;
translateTexture(deltaX + deltaY);
}
function handlePointerUp() {
2025-10-02 10:19:50 -05:00
controlsEnabledEvent.dispatch(true);
2024-06-29 13:15:24 -05:00
2025-09-01 22:56:39 -05:00
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
2024-02-23 15:52:09 -06:00
}
2025-04-15 00:11:49 -05:00
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
2025-08-28 16:52:23 -05:00
receiveShadow
2025-04-15 00:11:49 -05:00
/>
);
2024-04-21 11:09:24 -05:00
},
2025-04-15 00:11:49 -05:00
});
2024-01-10 13:00:37 -06:00
})}
2024-01-13 18:24:04 -06:00
<group ref={turretContainer}>
2024-01-10 13:00:37 -06:00
{nodes.map((node) => {
2025-09-01 22:56:39 -05:00
const isTurret = node.name.startsWith("turret_");
2024-01-10 13:00:37 -06:00
const isCurrentTurret =
node.name ===
2025-11-26 21:10:03 -06:00
`turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`;
2024-01-10 13:00:37 -06:00
const isVisible = isCurrentTurret;
2024-06-05 13:28:45 -05:00
const position = new Vector2();
const delta = new Vector2();
2024-02-22 12:20:29 -06:00
if (!isVisible) return null;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
2024-01-10 13:00:37 -06:00
event.stopPropagation();
2025-08-06 12:23:57 -05:00
if (!isTurret) return;
2024-01-10 13:00:37 -06:00
2024-08-01 20:38:37 -05:00
position.set(event.clientX, event.clientY);
2025-10-02 10:19:50 -05:00
controlsEnabledEvent.dispatch(false);
2025-09-01 22:56:39 -05:00
Tankopedia.mutate((draft) => {
2024-08-01 20:38:37 -05:00
draft.shot = undefined;
draft.highlightArmor = undefined;
});
2025-09-01 22:56:39 -05:00
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
2024-01-10 13:00:37 -06:00
}
2025-08-29 13:42:44 -05:00
function handlePointerMove(event: PointerEvent) {
2026-09-30 20:59:18 -05:00
const protagonist = getDuel("protagonist", DuelModule.Tank);
2026-09-30 17:56:31 -05:00
2025-08-29 13:42:44 -05:00
const hasImprovedVerticalStabilizer = hasEquipment(
2024-04-04 13:16:12 -05:00
122,
2026-09-30 17:56:31 -05:00
protagonist.equipment_preset,
Tankopedia.state.protagonist.equipment,
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,
2026-09-30 17:56:31 -05:00
protagonist.equipment_preset,
Tankopedia.state.protagonist.equipment,
2025-03-11 10:13:02 -05:00
);
2024-06-04 17:37:59 -05:00
const boundingRect = canvas.getBoundingClientRect();
2024-06-05 13:28:45 -05:00
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
2025-08-12 13:43:11 -05:00
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width),
2026-03-31 14:11:45 -05:00
gunModelDefinition.pitch!,
2024-01-10 13:00:37 -06:00
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
2026-03-31 14:11:45 -05:00
hasDownImprovedVerticalStabilizer,
2024-01-10 13:00:37 -06:00
);
2025-08-10 20:36:48 -05:00
modelTransformEvent.dispatch({ pitch, yaw });
2024-01-10 13:00:37 -06:00
}
function handlePointerUp() {
2025-10-02 10:19:50 -05:00
controlsEnabledEvent.dispatch(true);
2025-09-01 22:56:39 -05:00
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
2024-01-10 13:00:37 -06:00
}
2025-04-15 00:11:49 -05:00
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
2025-08-28 16:52:23 -05:00
receiveShadow
2025-04-15 00:11:49 -05:00
/>
);
2024-04-21 11:09:24 -05:00
},
2025-04-15 00:11:49 -05:00
});
2024-01-10 13:00:37 -06:00
})}
2024-01-13 18:24:04 -06:00
<group ref={gunContainer}>
2024-01-10 13:00:37 -06:00
{nodes.map((node) => {
const isCurrentMantlet =
node.name ===
2024-10-07 18:45:11 -05:00
`gun_${gunModelDefinition.model_id
2024-01-10 13:00:37 -06:00
.toString()
2025-09-01 22:56:39 -05:00
.padStart(2, "0")}_mask`;
2024-01-10 13:00:37 -06:00
const isCurrentGun =
node.name ===
2025-09-01 22:56:39 -05:00
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
2024-01-10 13:00:37 -06:00
const isVisible = isCurrentGun || isCurrentMantlet;
2024-06-05 13:28:45 -05:00
const position = new Vector2();
const delta = new Vector2();
2024-02-22 12:20:29 -06:00
if (!isVisible) return null;
2024-01-13 18:24:04 -06:00
let pitch = 0;
let yaw = 0;
2024-01-10 13:00:37 -06:00
2024-02-22 12:20:29 -06:00
function onPointerDown(event: ThreeEvent<PointerEvent>) {
2024-01-10 13:00:37 -06:00
event.stopPropagation();
2025-10-02 10:19:50 -05:00
controlsEnabledEvent.dispatch(false);
2025-09-01 22:56:39 -05:00
Tankopedia.mutate((draft) => {
2024-02-02 16:08:41 -06:00
draft.shot = undefined;
2024-07-27 14:43:33 -05:00
draft.highlightArmor = undefined;
2024-02-02 16:08:41 -06:00
});
2024-06-05 13:28:45 -05:00
position.set(event.clientX, event.clientY);
2025-08-12 13:43:11 -05:00
pitch = modelTransformEvent.last!.pitch;
yaw = modelTransformEvent.last!.yaw;
2024-06-05 13:28:45 -05:00
2025-09-01 22:56:39 -05:00
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
2024-01-10 13:00:37 -06:00
}
2025-08-29 13:42:44 -05:00
function handlePointerMove(event: PointerEvent) {
2026-09-30 19:00:25 -05:00
const protagonist = getDuel("protagonist", DuelModule.Tank);
2026-09-30 17:56:31 -05:00
2025-08-29 13:42:44 -05:00
const hasImprovedVerticalStabilizer = hasEquipment(
2024-04-04 13:16:12 -05:00
122,
2026-09-30 17:56:31 -05:00
protagonist.equipment_preset,
Tankopedia.state.protagonist.equipment,
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,
2026-09-30 17:56:31 -05:00
protagonist.equipment_preset,
Tankopedia.state.protagonist.equipment,
2025-03-11 10:13:02 -05:00
);
2024-06-04 17:37:59 -05:00
const boundingRect = canvas.getBoundingClientRect();
2024-06-05 13:28:45 -05:00
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
2024-01-13 18:24:04 -06:00
[pitch, yaw] = applyPitchYawLimits(
2024-06-05 13:28:45 -05:00
pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width),
2026-03-31 14:11:45 -05:00
gunModelDefinition.pitch!,
2024-01-10 13:00:37 -06:00
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
2026-03-31 14:11:45 -05:00
hasDownImprovedVerticalStabilizer,
2024-01-10 13:00:37 -06:00
);
2025-08-10 20:36:48 -05:00
modelTransformEvent.dispatch({ pitch, yaw });
2024-01-10 13:00:37 -06:00
}
function handlePointerUp() {
2025-10-02 10:19:50 -05:00
controlsEnabledEvent.dispatch(true);
2025-09-01 22:56:39 -05:00
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
2024-01-10 13:00:37 -06:00
}
2025-04-15 00:11:49 -05:00
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
2025-08-28 16:52:23 -05:00
receiveShadow
2025-04-15 00:11:49 -05:00
/>
);
2024-04-21 11:09:24 -05:00
},
2025-04-15 00:11:49 -05:00
});
2024-01-10 13:00:37 -06:00
})}
</group>
</group>
2026-09-30 17:56:31 -05:00
</>
2024-01-10 13:00:37 -06:00
);
2024-12-12 18:32:27 -06:00
}