diff --git a/packages/website-ue/src/components/TankopediaControls.tsx b/packages/website-ue/src/components/TankopediaControls.tsx index 26d8d08d5..b27a3087b 100644 --- a/packages/website-ue/src/components/TankopediaControls.tsx +++ b/packages/website-ue/src/components/TankopediaControls.tsx @@ -1,12 +1,17 @@ import { I_HAT, J_HAT } from "@blitzkit/core"; -import { invalidate, useThree } from "@react-three/fiber"; -import { useEffect } from "react"; +import { invalidate, useFrame, useThree } from "@react-three/fiber"; +import { useCallback, useEffect, useRef, type RefObject } from "react"; import { Quaternion, Vector2, Vector3 } from "three"; import { + cameraSwingHorizontalAmplitude, + cameraSwingHorizontalFrequency, + cameraSwingVerticalAmplitude, + cameraSwingVerticalFrequency, initialCameraPhi, initialCameraR, initialCameraTheta, } from "../config/camera"; +import { Tankopedia } from "../stores/tankopedia"; const clientPosition = new Vector2(); const workingVector3 = new Vector3(); @@ -20,10 +25,38 @@ export function TankopediaControls() { const canvas = useThree((state) => state.gl.domElement); const camera = useThree((state) => state.camera); + const r = useRef(initialCameraR); + const phi = useRef(initialCameraPhi); + const theta = useRef(initialCameraTheta); + + const disturbed = Tankopedia.use((state) => state.disturbed); + + const updateCameraPosition = useCallback(() => { + workingVector3 + .set( + Math.sin(phi.current) * Math.sin(theta.current), + Math.cos(phi.current), + Math.sin(phi.current) * Math.cos(theta.current), + ) + .multiplyScalar(r.current); + camera.position.copy(cameraCenter).add(workingVector3); + + workingQuaternion.setFromAxisAngle(I_HAT, phi.current - Math.PI / 2); + camera.quaternion + .setFromAxisAngle(J_HAT, theta.current) + .multiply(workingQuaternion); + + invalidate(); + }, []); + useEffect(() => { - setCameraPosition(initialCameraR, initialCameraPhi, initialCameraTheta); + updateCameraPosition(); function handlePointerDown(event: PointerEvent) { + Tankopedia.mutate((draft) => { + draft.disturbed = true; + }); + clientPosition.set(event.clientX, event.clientY); window.addEventListener("pointermove", handlePointerMove); @@ -35,21 +68,22 @@ export function TankopediaControls() { workingVector3.copy(camera.position).sub(cameraCenter); - const r = workingVector3.length(); + r.current = workingVector3.length(); const dx = event.movementX / window.innerWidth; const dy = event.movementY / window.innerHeight; const dTheta = -dx * Math.PI; const dPhi = -dy * Math.PI; - const theta = Math.atan2(workingVector3.x, workingVector3.z) + dTheta; - const phi0 = Math.acos(workingVector3.y / r); - let phi = phi0 + dPhi; - const maxPhi = Math.PI / 2 + Math.asin(cameraCenter.y / r) - PHI_EPSILON; + theta.current = Math.atan2(workingVector3.x, workingVector3.z) + dTheta; + const phi0 = Math.acos(workingVector3.y / r.current); + phi.current = phi0 + dPhi; + const maxPhi = + Math.PI / 2 + Math.asin(cameraCenter.y / r.current) - PHI_EPSILON; - if (phi < 0 || phi > maxPhi) phi = phi0; + if (phi.current < 0 || phi.current > maxPhi) phi.current = phi0; - setCameraPosition(r, phi, theta); + updateCameraPosition(); } function handlePointerUp() { @@ -57,24 +91,6 @@ export function TankopediaControls() { window.removeEventListener("pointerup", handlePointerUp); } - function setCameraPosition(r: number, phi: number, theta: number) { - workingVector3 - .set( - Math.sin(phi) * Math.sin(theta), - Math.cos(phi), - Math.sin(phi) * Math.cos(theta), - ) - .multiplyScalar(r); - camera.position.copy(cameraCenter).add(workingVector3); - - workingQuaternion.setFromAxisAngle(I_HAT, phi - Math.PI / 2); - camera.quaternion - .setFromAxisAngle(J_HAT, theta) - .multiply(workingQuaternion); - - invalidate(); - } - canvas.addEventListener("pointerdown", handlePointerDown); return () => { @@ -82,5 +98,39 @@ export function TankopediaControls() { }; }, []); + return ( + !disturbed && ( + + ) + ); +} + +interface SwingProps { + r: RefObject; + phi: RefObject; + theta: RefObject; + updateCameraPosition: () => void; +} + +export function Swing({ r, phi, theta, updateCameraPosition }: SwingProps) { + useFrame(({ clock }) => { + const t = 2 * Math.PI * clock.getElapsedTime(); + + phi.current = + initialCameraPhi + + cameraSwingVerticalAmplitude * Math.cos(cameraSwingVerticalFrequency * t); + theta.current = + initialCameraTheta + + cameraSwingHorizontalAmplitude * + Math.sin(cameraSwingHorizontalFrequency * t); + + updateCameraPosition(); + }); + return null; } diff --git a/packages/website-ue/src/config/camera.ts b/packages/website-ue/src/config/camera.ts index 0667226b2..c83e65b68 100644 --- a/packages/website-ue/src/config/camera.ts +++ b/packages/website-ue/src/config/camera.ts @@ -5,3 +5,8 @@ export const fov = 20; export const initialCameraR = 15; export const initialCameraPhi = degToRad(90 - 10); export const initialCameraTheta = degToRad(90 + 15); + +export const cameraSwingVerticalAmplitude = degToRad(5); +export const cameraSwingVerticalFrequency = Math.E ** -6; +export const cameraSwingHorizontalAmplitude = degToRad(30); +export const cameraSwingHorizontalFrequency = Math.E ** -5; diff --git a/packages/website-ue/src/stores/tankopedia.ts b/packages/website-ue/src/stores/tankopedia.ts index 7707d38b7..3223c0f11 100644 --- a/packages/website-ue/src/stores/tankopedia.ts +++ b/packages/website-ue/src/stores/tankopedia.ts @@ -5,6 +5,8 @@ import { type TankState } from "../tankopedia/tankState"; import { characteristicsOrder } from "../tankopedia/characteristicsOrder"; interface Tankopedia { + disturbed: boolean; + compare: TankopediaCompare; groups: Record; @@ -24,6 +26,8 @@ for (const { name } of characteristicsOrder) { } export const Tankopedia = new Varuna((tank) => ({ + disturbed: false, + compare: TankopediaCompare.TierAndClass, groups, diff --git a/packages/website-ue/src/tankopedia/createTankState.ts b/packages/website-ue/src/tankopedia/createTankState.ts index 24fbd5850..21940e548 100644 --- a/packages/website-ue/src/tankopedia/createTankState.ts +++ b/packages/website-ue/src/tankopedia/createTankState.ts @@ -43,6 +43,10 @@ export function createTankState({ tank, id }: Tank) { speed: 0, terrainHardness: TerrainHardness.Hard, + isHullRotating: false, + isTurretRotating: false, + isGunRotating: false, + status: status as Record, } satisfies TankState; }