import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber"; import { useRef } from "react"; import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three"; import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits"; import { jsxTree } from "../core/blitzkit/jsxTree"; import { modelTransformEvent } from "../core/blitzkit/modelTransform"; import { controlsEnabledEvent } from "../core/controlsEnabled"; import { DuelModule, getDuel, useDuel } from "../hooks/useDuel"; import { useDuelModel } from "../hooks/useDuelModel"; import { hasEquipment } from "../hooks/useEquipment"; import { useModel } from "../hooks/useModel"; import { useTankTransform } from "../hooks/useTankTransform"; import { Tankopedia } from "../stores/tankopedia"; import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants"; export function TankModel() { const protagonist = Tankopedia.use((state) => state.protagonist); const track = useDuel("protagonist", DuelModule.Track); const turret = useDuel("protagonist", DuelModule.Turret); const canvas = useThree((state) => state.gl.domElement); const turretContainer = useRef(null!); const gunContainer = useRef(null!); const tankModelDefinition = useDuelModel("protagonist"); const trackModelDefinition = tankModelDefinition.tracks[track.id]; const turretModelDefinition = tankModelDefinition.turrets[protagonist.turret]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun]; const { gltf } = useModel(protagonist.tank); const nodes = Object.values(gltf.nodes); useTankTransform(track, turret, turretContainer, gunContainer); return ( <> {/* */} {nodes.map((node) => { const isHull = node.name === "hull"; const isWheel = node.name.startsWith("chassis_wheel_"); const isTrack = node.name.startsWith("chassis_track_"); const isVisible = isHull || isWheel || isTrack; const position = new Vector2(); const delta = new Vector2(); if (!isVisible) return null; function translateTexture(offset: number) { const mesh = node.children[0] as Mesh; const material = mesh?.material as MeshStandardMaterial; if (!material) return; invalidate(); 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; } } function onPointerDown(event: ThreeEvent) { if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) { position.set(event.clientX, event.clientY); event.stopPropagation(); controlsEnabledEvent.dispatch(false); window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); } else { event.stopPropagation(); } } 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() { controlsEnabledEvent.dispatch(true); window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); } return jsxTree(node, { mesh(_, props, key) { return ( ); }, }); })} {nodes.map((node) => { const isTurret = node.name.startsWith("turret_"); const isCurrentTurret = node.name === `turret_${turretModelDefinition.model_id .toString() .padStart(2, "0")}`; const isVisible = isCurrentTurret; const position = new Vector2(); const delta = new Vector2(); if (!isVisible) return null; function onPointerDown(event: ThreeEvent) { event.stopPropagation(); if (!isTurret) return; position.set(event.clientX, event.clientY); controlsEnabledEvent.dispatch(false); Tankopedia.mutate((draft) => { draft.shot = undefined; draft.highlightArmor = undefined; }); window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); } function handlePointerMove(event: PointerEvent) { const protagonist = getDuel("protagonist", DuelModule.Tank); const hasImprovedVerticalStabilizer = hasEquipment( 122, protagonist.equipment_preset, Tankopedia.state.protagonist.equipment, ); const hasDownImprovedVerticalStabilizer = hasEquipment( 124, protagonist.equipment_preset, Tankopedia.state.protagonist.equipment, ); const boundingRect = canvas.getBoundingClientRect(); delta.set(event.clientX, event.clientY).sub(position); position.set(event.clientX, event.clientY); const [pitch, yaw] = applyPitchYawLimits( modelTransformEvent.last!.pitch, modelTransformEvent.last!.yaw + delta.x * (Math.PI / boundingRect.width), gunModelDefinition.pitch!, turretModelDefinition.yaw, hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer, ); modelTransformEvent.dispatch({ pitch, yaw }); } function handlePointerUp() { controlsEnabledEvent.dispatch(true); window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); } return jsxTree(node, { mesh(_, props, key) { return ( ); }, }); })} {nodes.map((node) => { const isCurrentMantlet = node.name === `gun_${gunModelDefinition.model_id .toString() .padStart(2, "0")}_mask`; const isCurrentGun = node.name === `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`; const isVisible = isCurrentGun || isCurrentMantlet; const position = new Vector2(); const delta = new Vector2(); if (!isVisible) return null; let pitch = 0; let yaw = 0; function onPointerDown(event: ThreeEvent) { event.stopPropagation(); controlsEnabledEvent.dispatch(false); Tankopedia.mutate((draft) => { draft.shot = undefined; draft.highlightArmor = undefined; }); position.set(event.clientX, event.clientY); pitch = modelTransformEvent.last!.pitch; yaw = modelTransformEvent.last!.yaw; window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); } function handlePointerMove(event: PointerEvent) { const protagonist = getDuel("protagonist", DuelModule.Tank); const hasImprovedVerticalStabilizer = hasEquipment( 122, protagonist.equipment_preset, Tankopedia.state.protagonist.equipment, ); const hasDownImprovedVerticalStabilizer = hasEquipment( 124, protagonist.equipment_preset, Tankopedia.state.protagonist.equipment, ); const boundingRect = canvas.getBoundingClientRect(); delta.set(event.clientX, event.clientY).sub(position); position.set(event.clientX, event.clientY); [pitch, yaw] = applyPitchYawLimits( pitch - delta.y * (Math.PI / boundingRect.height), yaw + delta.x * (Math.PI / boundingRect.width), gunModelDefinition.pitch!, turretModelDefinition.yaw, hasImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer, ); modelTransformEvent.dispatch({ pitch, yaw }); } function handlePointerUp() { controlsEnabledEvent.dispatch(true); window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); } return jsxTree(node, { mesh(_, props, key) { return ( ); }, }); })} ); }