feat: zooming in

This commit is contained in:
tresabhi committed 2024-12-12 16:03:34 -06:00
1 parent 0c54905384
commit 143c84f52f
4 files changed
+136 -39

No files matched your search

@@ -1,5 +1,6 @@
import { OrbitControls } from '@react-three/drei';
import { invalidate, useThree } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import { useEffect, useRef, useState } from 'react';
import { PerspectiveCamera, Vector3 } from 'three';
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
@@ -9,7 +10,11 @@ import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawL
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import {
SHOOTING_RANGE_ZOOM_COEFFICIENTS,
ShootingRangeZoom,
TankopediaEphemeral,
} from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
const poseDistances: Record<Pose, number> = {
@@ -21,11 +26,16 @@ const poseDistances: Record<Pose, number> = {
const modelDefinitions = await awaitableModelDefinitions;
const ARCADE_MODE_DISTANCE = 19;
const ARCADE_MODE_ANGLE = Math.PI / 8;
// const ARCADE_MODE_ANGLE = Math.PI / 8;
const ARCADE_MODE_ANGLE = degToRad(10);
export const ARCADE_MODE_FOV = 54;
export const INSPECT_MODE_FOV = 25;
export function Controls() {
const shootingRangeZoom = TankopediaEphemeral.use(
(state) => state.shootingRangeZoom,
);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const display = TankopediaEphemeral.use((state) => state.display);
const duelStore = Duel.useStore();
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
@@ -78,35 +88,53 @@ export function Controls() {
protagonistTurretOrigin.y +
protagonistGunOrigin.y;
const initialPosition = [-8, gunHeight + 4, -13] as const;
const protagonistGunOriginOnlyY = new Vector3(
0,
protagonistTurretModelDefinition.gun_origin.y,
0,
);
const shellOrigin = protagonistHullOrigin
.clone()
.add(protagonistTurretOrigin)
.add(protagonistGunOriginOnlyY);
useEffect(() => {
if (!orbitControls.current) return;
const cameraPosition = new Vector3();
let fov: number;
const target = new Vector3();
let enablePan: boolean;
let enableZoom: boolean;
let rotationSpeed = 0.25;
if (display === TankopediaDisplay.ShootingRange) {
(camera as PerspectiveCamera).fov = ARCADE_MODE_FOV;
camera.position.set(
0,
gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE),
ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE),
);
orbitControls.current.target.set(0, gunHeight + 3, 0);
orbitControls.current.enablePan = false;
// orbitControls.current.enableZoom = false;
if (display === TankopediaDisplay.ShootingRange) {
cameraPosition.set(
0,
gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE),
ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE),
);
enablePan = false;
enableZoom = false;
if (shootingRangeZoom === ShootingRangeZoom.Arcade) {
target.set(0, gunHeight + 3, 0);
fov = ARCADE_MODE_FOV;
} else {
(camera as PerspectiveCamera).fov = INSPECT_MODE_FOV;
camera.position.set(...initialPosition);
orbitControls.current.target.set(0, gunHeight / 2, 0);
orbitControls.current.enablePan = true;
// orbitControls.current.enableZoom = true;
target.copy(shellOrigin);
cameraPosition.copy(shellOrigin.add(new Vector3(0, 0, Number.EPSILON)));
fov =
ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
rotationSpeed *= SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
}
} else {
cameraPosition.set(...initialPosition);
target.set(0, gunHeight / 2, 0);
enablePan = true;
enableZoom = true;
fov = INSPECT_MODE_FOV;
}
camera.updateProjectionMatrix();
}, [
camera,
protagonistTrack,
protagonistTank,
display === TankopediaDisplay.ShootingRange,
]);
camera.position.copy(cameraPosition);
(camera as PerspectiveCamera).fov = fov;
camera.updateProjectionMatrix();
useEffect(() => {
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
@@ -217,8 +245,47 @@ export function Controls() {
setAutoRotate(false);
}
function handleKeyDown(event: KeyboardEvent) {
if (display !== TankopediaDisplay.ShootingRange || event.repeat) return;
if (event.key === 'Shift') {
mutateTankopediaEphemeral((draft) => {
draft.shootingRangeZoom =
draft.shootingRangeZoom === ShootingRangeZoom.Arcade
? ShootingRangeZoom.Zoom0
: ShootingRangeZoom.Arcade;
});
}
}
function handleWheel(event: WheelEvent) {
if (display !== TankopediaDisplay.ShootingRange) return;
event.preventDefault();
mutateTankopediaEphemeral((draft) => {
draft.shootingRangeZoom = clamp(
draft.shootingRangeZoom + Math.sign(event.deltaY),
ShootingRangeZoom.Arcade,
ShootingRangeZoom.Zoom2,
);
console.log(draft.shootingRangeZoom);
});
}
function handleScroll(event: Event) {
if (display !== TankopediaDisplay.ShootingRange) return;
event.preventDefault();
}
poseEvent.on(handleDisturbance);
canvas.addEventListener('pointerdown', handleDisturbance);
window.addEventListener('keydown', handleKeyDown);
canvas.addEventListener('wheel', handleWheel);
document.body.addEventListener('scroll', handleScroll);
const unsubscribeTankopediaPersistent = tankopediaEphemeralStore.subscribe(
(state) => state.display,
handleDisturbance,
@@ -228,16 +295,22 @@ export function Controls() {
canvas.removeEventListener('pointerdown', handleDisturbance);
poseEvent.off(handleDisturbance);
unsubscribeTankopediaPersistent();
window.removeEventListener('keydown', handleKeyDown);
canvas.removeEventListener('wheel', handleWheel);
document.body.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<OrbitControls
target={target}
enablePan={enablePan}
enableZoom={enableZoom}
maxDistance={20}
minDistance={5}
ref={orbitControls}
enabled={tankopediaEphemeralStore.getState().controlsEnabled}
rotateSpeed={0.25}
rotateSpeed={rotationSpeed}
enableDamping={false}
maxPolarAngle={degToRad(100)}
autoRotate={autoRotate}
@@ -71,10 +71,7 @@ export function SceneProps() {
-tankModelDefinition.turret_origin.z,
);
const gunOriginOnlyY = new Vector3(0, turretModelDefinition.gun_origin.y, 0);
const shellOrigin = new Vector3()
.add(hullOrigin)
.add(turretOrigin)
.add(gunOriginOnlyY);
const shellOrigin = hullOrigin.clone().add(turretOrigin).add(gunOriginOnlyY);
const camouflage = Duel.use((state) => state.protagonist.camouflage);
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
const consumables = Duel.use((state) => state.protagonist.consumables);
@@ -274,11 +271,6 @@ export function SceneProps() {
<meshStandardMaterial color={0xff8040} />
</mesh>
<mesh position={shellOrigin}>
<sphereGeometry args={[0.1, 32, 32]} />
<meshStandardMaterial depthTest={false} />
</mesh>
<group
position={[0, 0, -182]}
rotation={[-Math.PI / 2, 0, Math.PI]}
@@ -13,7 +13,10 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
import { useOnScreen } from '../../../../../hooks/useOnScreen';
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import {
ShootingRangeZoom,
TankopediaEphemeral,
} from '../../../../../stores/tankopediaEphemeral';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
import { Armor } from '../../../../Armor';
import { ArmorPlateDisplay } from '../../../../Armor/components/ArmorPlateDisplay';
@@ -48,6 +51,9 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
turretModelDefinition.guns[protagonistGun.gun_type!.value.base.id];
const display = TankopediaEphemeral.use((state) => state.display);
const onScreen = useOnScreen(canvas);
const shootingRangeZoom = TankopediaEphemeral.use(
(state) => state.shootingRangeZoom,
);
useImperativeHandle(ref, () => canvas.current!, []);
@@ -158,7 +164,13 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
return (
<Canvas
frameloop={onScreen ? 'demand' : 'never'}
frameloop={
onScreen
? display === TankopediaDisplay.ShootingRange
? 'always'
: 'demand'
: 'never'
}
gl={{
clippingPlanes: [],
localClippingEnabled: true,
@@ -180,7 +192,8 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
>
<Controls />
<SceneProps />
{display !== TankopediaDisplay.StaticArmor && <TankModel />}
{display !== TankopediaDisplay.StaticArmor &&
shootingRangeZoom === ShootingRangeZoom.Arcade && <TankModel />}
<ShotDisplay />
<ArmorPlateDisplay />
<AutoClear />
@@ -78,8 +78,26 @@ export enum TankopediaRelativeAgainst {
All,
}
export enum ShootingRangeZoom {
Arcade,
Zoom0,
Zoom1,
Zoom2,
}
export const SHOOTING_RANGE_ZOOM_COEFFICIENTS: Record<
ShootingRangeZoom,
number
> = {
[ShootingRangeZoom.Arcade]: 1,
[ShootingRangeZoom.Zoom0]: 1 / 8,
[ShootingRangeZoom.Zoom1]: 1 / 16,
[ShootingRangeZoom.Zoom2]: 1 / 25,
};
interface TankopediaEphemeral {
shot?: Shot;
shootingRangeZoom: ShootingRangeZoom;
skills: Record<string, number>;
relativeAgainst: TankopediaRelativeAgainst;
controlsEnabled: boolean;
@@ -110,6 +128,7 @@ export const TankopediaEphemeral = createContextualStore(
(model: ModelDefinition) => {
return create<TankopediaEphemeral>()(
subscribeWithSelector<TankopediaEphemeral>(() => ({
shootingRangeZoom: ShootingRangeZoom.Arcade,
relativeAgainst: TankopediaRelativeAgainst.Class,
editStatic: false,
skills: createDefaultSkills(skillDefinitions),