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