diff --git a/src/app/tools/tankopedia/[id]/components/Control.tsx b/src/app/tools/tankopedia/[id]/components/Control.tsx index 1f634cb0b..fe5b6d741 100644 --- a/src/app/tools/tankopedia/[id]/components/Control.tsx +++ b/src/app/tools/tankopedia/[id]/components/Control.tsx @@ -1,26 +1,88 @@ import { OrbitControls } from '@react-three/drei'; import { useThree } from '@react-three/fiber'; import { useEffect, useRef } from 'react'; +import usePromise from 'react-promise-suspense'; +import { Vector3 } from 'three'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; +import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; +import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; +import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose'; import { useTankopedia } from '../../../../../stores/tankopedia'; +import { Duel } from '../page'; -export function Controls() { +const DISTANCE = 15; + +interface ControlsProps { + duel: Duel; +} + +export function Controls({ duel }: ControlsProps) { const camera = useThree((state) => state.camera); const orbitControls = useRef(null); + const awaitedModelDefinitions = usePromise( + async () => await modelDefinitions, + [], + ); + const tankModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id]; + const turretModelDefinition = + tankModelDefinition.turrets[duel.protagonist.turret.id]; + const gunModelDefinition = + turretModelDefinition.guns[duel.protagonist.gun.id]; + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ); useEffect(() => { camera.position.set(-5, 4, -20); orbitControls.current?.target.set(0, 1.5, 0); - const unsubscribe = useTankopedia.subscribe( + const unsubscribeTankopedia = useTankopedia.subscribe( (state) => state.model.visual.controlsEnabled, (enabled) => { if (orbitControls.current) orbitControls.current.enabled = enabled; }, ); - return unsubscribe; - }, []); + function handlePoseEvent(event: Pose) { + const [pitch] = applyPitchYawLimits( + -Infinity, + 0, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + switch (event) { + case Pose.HullDown: { + camera.position + .set(0, 0, 0) + .add(turretOrigin) + .add(gunOrigin) + .add( + new Vector3( + 0, + DISTANCE * Math.sin(pitch), + DISTANCE * -Math.cos(pitch), + ), + ); + camera.lookAt(turretOrigin.clone().add(gunOrigin)); + orbitControls.current?.target.set(0, 1.5, 0); + } + } + } + + poseEvent.on(handlePoseEvent); + + return () => { + unsubscribeTankopedia(); + poseEvent.off(handlePoseEvent); + }; + }, [camera]); return ( (null); const [isFullScreen, setIsFullScreen] = useState(false); const mode = useTankopedia((state) => state.mode); + const tankModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id]; + const turretModelDefinition = + tankModelDefinition.turrets[duel.protagonist.turret.id]; + const gunModelDefinition = + turretModelDefinition.guns[duel.protagonist.gun.id]; function handlePointerDown() { window.addEventListener('pointermove', handlePointerMove); @@ -49,23 +56,37 @@ export function TankDisplay({ duel }: TankDisplayProps) { setIsFullScreen(document.fullscreenElement !== null); } + function handlePoseEvent(pose: Pose) { + switch (pose) { + case Pose.HullDown: { + mutateTankopedia((draft) => { + const [pitch, yaw] = applyPitchYawLimits( + -Infinity, + 0, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + + modelTransformEvent.emit({ pitch, yaw }); + draft.model.physical.pitch = pitch; + draft.model.physical.yaw = yaw; + }); + } + } + } + + poseEvent.on(handlePoseEvent); document.addEventListener('fullscreenchange', handleFullScreenChange); return () => { document.removeEventListener('fullscreenchange', handleFullScreenChange); + poseEvent.off(handlePoseEvent); }; }); useEffect(() => { if (!duel.protagonist) return void 0; - const tankModelDefinition = - awaitedModelDefinitions[duel.protagonist.tank.id]; - const turretModelDefinition = - tankModelDefinition.turrets[duel.protagonist.turret.id]; - const gunModelDefinition = - turretModelDefinition.guns[duel.protagonist.gun.id]; - mutateTankopedia((draft) => { [draft.model.physical.pitch, draft.model.physical.yaw] = applyPitchYawLimits( @@ -116,7 +137,7 @@ export function TankDisplay({ duel }: TankDisplayProps) { camera={{ fov: 20 }} onPointerDown={handlePointerDown} > - + diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx index ab1433776..91008a25d 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx @@ -1,10 +1,12 @@ import { EnterFullScreenIcon, ExitFullScreenIcon, + EyeOpenIcon, GearIcon, } from '@radix-ui/react-icons'; import { Button, DropdownMenu, Flex } from '@radix-ui/themes'; import { RefObject } from 'react'; +import { Pose, poseEvent } from '../../../../../../../core/blitzkrieg/pose'; import mutateTankopedia, { useTankopedia, } from '../../../../../../../stores/tankopedia'; @@ -30,6 +32,25 @@ export function Options({ isFullScreen, canvas }: OptionsProps) { right: 18, }} > + + + + + + + { + console.log('called?'); + poseEvent.emit(Pose.HullDown); + }} + > + Hull down + + + +