feat: dragger

This commit is contained in:
tresabhi committed 2025-03-23 17:18:42 -05:00
1 parent dcd7ce61f7
commit 5982fdde8c
4 files changed
+283 -186

No files matched your search

@@ -1,4 +1,16 @@
import { asset } from '@blitzkit/core';
import { asset, J_HAT } from '@blitzkit/core';
import {
amberDark,
blueDark,
bronzeDark,
indigoDark,
mauveDark,
orangeDark,
plumDark,
redDark,
skyDark,
slateDark,
} from '@radix-ui/colors';
import {
Base,
Geometry,
@@ -15,6 +27,7 @@ import {
LineBasicMaterial,
Material,
MeshStandardMaterial,
Plane,
Vector3,
} from 'three';
import {
@@ -33,28 +46,28 @@ interface DissectorSceneComponentProps {
const NEGATIVE_SIZE = 20;
const armorColor = new Color().multiplyScalar(2 ** -8);
const armorColor = new Color().multiplyScalar(2 ** -4);
const armorOutlineColor = armorColor.clone().multiplyScalar(2 ** 4.5);
const moduleColor: Record<string, Color> = {
Armor: armorColor,
ArmorScreen: armorColor,
Commander: new Color('white'),
Driver: new Color('white'),
Gunner1: new Color('white'),
Loader1: new Color('white'),
Commander: new Color(mauveDark.mauve11),
Driver: new Color(mauveDark.mauve11),
Gunner1: new Color(mauveDark.mauve11),
Loader1: new Color(mauveDark.mauve11),
SurveyingDevice: new Color('purple'),
Radio: new Color('indigo'),
Gun: new Color('pink'),
FuelTank: new Color('yellow'),
Engine: new Color('orange'),
AmmoBay: new Color('gold'),
LeftTrack: new Color('green'),
RightTrack: new Color('green'),
TurretRotator: new Color('blue'),
Transmission: new Color('cyan'),
SurveyingDevice: new Color(plumDark.plum8),
Radio: new Color(slateDark.slate8),
Gun: new Color(blueDark.blue8),
FuelTank: new Color(orangeDark.orange8),
Engine: new Color(redDark.red8),
AmmoBay: new Color(amberDark.amber8),
LeftTrack: new Color(bronzeDark.bronze8),
RightTrack: new Color(bronzeDark.bronze8),
TurretRotator: new Color(indigoDark.indigo8),
Transmission: new Color(skyDark.sky8),
};
const moduleOutlineColor: Record<string, Color> = {
Armor: armorOutlineColor,
@@ -87,27 +100,56 @@ export function DissectorSceneComponent({
const armorName = mesh.material.name;
const armorPlate = groupArmor.armors[armorName];
if (armorPlate.type === 'Armor' || armorPlate.type === 'ArmorScreen') {
return null;
}
const isModule =
armorPlate.type !== 'Armor' && armorPlate.type !== 'ArmorScreen';
const isTrack =
armorPlate.type === 'LeftTrack' || armorPlate.type === 'RightTrack';
if (!(armorPlate.type in moduleColor)) {
throw new TypeError(`Unhandled armor type: ${armorPlate.type}`);
}
const material = new MeshStandardMaterial({
side: DoubleSide,
color: moduleColor[armorPlate.type],
});
const outlineMaterial = new LineBasicMaterial({
side: DoubleSide,
color: moduleOutlineColor[armorPlate.type],
});
const clippingPlane = useRef(new Plane());
const clippingPlaneInverse = useRef(new Plane());
const material = useRef(
new MeshStandardMaterial({
transparent: isTrack,
opacity: 2 ** -1,
side: DoubleSide,
color: moduleColor[armorPlate.type],
clippingPlanes: isModule ? undefined : [clippingPlane.current],
}),
);
const outlineMaterial = useRef(
new LineBasicMaterial({
transparent: !isModule,
opacity: 2 ** -3,
side: DoubleSide,
color: moduleOutlineColor[armorPlate.type],
clippingPlanes: isModule ? undefined : [clippingPlane.current],
}),
);
const outlineMaterialInverse = useRef(
new LineBasicMaterial({
transparent: !isModule,
opacity: 2 ** -6,
side: DoubleSide,
color: moduleOutlineColor[armorPlate.type],
clippingPlanes: isModule ? undefined : [clippingPlaneInverse.current],
}),
);
const worldPosition = new Vector3();
useEffect(() => {
const offset = new Vector3();
function handleDissectEvent(event: DissectEventData) {
clippingPlane.current.normal
.set(0, 0, 1)
.applyAxisAngle(J_HAT, event.rotation - Math.PI / 2);
clippingPlane.current.constant = event.offset;
clippingPlaneInverse.current.copy(clippingPlane.current).negate();
if (
!subtractionWrapper.current ||
!csg.current ||
@@ -116,9 +158,10 @@ export function DissectorSceneComponent({
return;
baseWrapper.current.getWorldPosition(worldPosition);
offset.set(-event.offset, 0, 0).applyAxisAngle(J_HAT, event.rotation);
subtractionWrapper.current.position
.copy(worldPosition)
.add(offset)
.multiplyScalar(-1);
subtractionWrapper.current.rotation.y = event.rotation;
csg.current.update();
@@ -132,13 +175,11 @@ export function DissectorSceneComponent({
};
}, []);
return (
<>
{/* <mesh {...props} material={material} /> */}
<mesh material={material} ref={baseWrapper}>
if (isModule) {
return (
<mesh material={material.current} ref={baseWrapper}>
<Geometry ref={csg}>
<Base geometry={props.geometry}></Base>
<Base geometry={props.geometry} />
<group rotation={[0, 0, 0]} ref={subtractionWrapper}>
<Subtraction position={[NEGATIVE_SIZE / 2, 0, 0]}>
@@ -149,10 +190,17 @@ export function DissectorSceneComponent({
</group>
</Geometry>
</mesh>
);
}
{/* <lineSegments material={outlineMaterial}>
return (
<>
<lineSegments material={outlineMaterial.current}>
<edgesGeometry args={[props.geometry, 45]} />
</lineSegments> */}
</lineSegments>
<lineSegments material={outlineMaterialInverse.current}>
<edgesGeometry args={[props.geometry, 45]} />
</lineSegments>
</>
);
},
@@ -85,9 +85,9 @@ export function Controls({ naked }: ControlsProps) {
// protagonistGunOrigin.y;
const gunHeight = 2;
const inspectModeInitialPosition = [
-8,
13,
gunHeight + (naked ? 10 : 4),
-13,
-8,
] as const;
// const protagonistGunOriginOnlyY = new Vector3(
// 0,
@@ -99,116 +99,120 @@ export function Controls({ naked }: ControlsProps) {
// .add(protagonistTurretOrigin)
// .add(protagonistGunOriginOnlyY);
// useEffect(() => {
// const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
// (state) => state.controlsEnabled,
// (enabled) => {
// if (orbitControls.current) orbitControls.current.enabled = enabled;
// },
// );
useEffect(() => {
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
(state) => state.controlsEnabled,
(enabled) => {
if (orbitControls.current) orbitControls.current.enabled = enabled;
},
);
// async function handlePoseEvent(event: Pose) {
// const duel = duelStore.getState();
// const hasImprovedVerticalStabilizer = await hasEquipment(
// 122,
// duel.protagonist.tank.equipment_preset,
// duel.protagonist.equipmentMatrix,
// );
// const hasDownImprovedVerticalStabilizer = await hasEquipment(
// 124,
// duel.protagonist.tank.equipment_preset,
// duel.protagonist.equipmentMatrix,
// );
// async function handlePoseEvent(event: Pose) {
// const duel = duelStore.getState();
// const hasImprovedVerticalStabilizer = await hasEquipment(
// 122,
// duel.protagonist.tank.equipment_preset,
// duel.protagonist.equipmentMatrix,
// );
// const hasDownImprovedVerticalStabilizer = await hasEquipment(
// 124,
// duel.protagonist.tank.equipment_preset,
// duel.protagonist.equipmentMatrix,
// );
// switch (event) {
// case Pose.HullDown: {
// const [pitch] = applyPitchYawLimits(
// -Infinity,
// 0,
// protagonistGunModelDefinition.pitch,
// protagonistTurretModelDefinition.yaw,
// hasImprovedVerticalStabilizer,
// hasDownImprovedVerticalStabilizer,
// );
// switch (event) {
// case Pose.HullDown: {
// const [pitch] = applyPitchYawLimits(
// -Infinity,
// 0,
// protagonistGunModelDefinition.pitch,
// protagonistTurretModelDefinition.yaw,
// hasImprovedVerticalStabilizer,
// hasDownImprovedVerticalStabilizer,
// );
// camera.position
// .set(0, 0, 0)
// .add(protagonistHullOrigin)
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin)
// .add(
// new Vector3(
// 0,
// poseDistances[event] * Math.sin(pitch),
// poseDistances[event] * -Math.cos(pitch),
// ),
// );
// camera.lookAt(
// protagonistHullOrigin
// .clone()
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin),
// );
// orbitControls.current?.target.set(0, antagonistGunHeight, 0);
// camera.position
// .set(0, 0, 0)
// .add(protagonistHullOrigin)
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin)
// .add(
// new Vector3(
// 0,
// poseDistances[event] * Math.sin(pitch),
// poseDistances[event] * -Math.cos(pitch),
// ),
// );
// camera.lookAt(
// protagonistHullOrigin
// .clone()
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin),
// );
// orbitControls.current?.target.set(0, antagonistGunHeight, 0);
// break;
// }
// break;
// }
// case Pose.FaceHug: {
// const [pitch] = applyPitchYawLimits(
// 0,
// 0,
// protagonistGunModelDefinition.pitch,
// protagonistTurretModelDefinition.yaw,
// hasImprovedVerticalStabilizer,
// hasDownImprovedVerticalStabilizer,
// );
// case Pose.FaceHug: {
// const [pitch] = applyPitchYawLimits(
// 0,
// 0,
// protagonistGunModelDefinition.pitch,
// protagonistTurretModelDefinition.yaw,
// hasImprovedVerticalStabilizer,
// hasDownImprovedVerticalStabilizer,
// );
// camera.position
// .set(0, 0, 0)
// .add(protagonistHullOrigin)
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin)
// .add(
// new Vector3(
// 0,
// poseDistances[event] * Math.sin(pitch),
// poseDistances[event] * -Math.cos(pitch),
// ),
// );
// orbitControls.current?.target
// .set(0, 0, 0)
// .add(protagonistHullOrigin)
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin)
// .add(
// new Vector3(
// 0,
// 0.5 * poseDistances[event] * Math.sin(pitch),
// 0.5 * poseDistances[event] * -Math.cos(pitch),
// ),
// );
// camera.position
// .set(0, 0, 0)
// .add(protagonistHullOrigin)
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin)
// .add(
// new Vector3(
// 0,
// poseDistances[event] * Math.sin(pitch),
// poseDistances[event] * -Math.cos(pitch),
// ),
// );
// orbitControls.current?.target
// .set(0, 0, 0)
// .add(protagonistHullOrigin)
// .add(protagonistTurretOrigin)
// .add(protagonistGunOrigin)
// .add(
// new Vector3(
// 0,
// 0.5 * poseDistances[event] * Math.sin(pitch),
// 0.5 * poseDistances[event] * -Math.cos(pitch),
// ),
// );
// break;
// }
// break;
// }
// case Pose.Default: {
// camera.position.set(...inspectModeInitialPosition);
// orbitControls.current?.target.set(0, 1.25, 0);
// break;
// }
// }
// case Pose.Default: {
// camera.position.set(...inspectModeInitialPosition);
// orbitControls.current?.target.set(0, 1.25, 0);
// break;
// }
// }
// invalidate();
// }
// invalidate();
// }
// poseEvent.on(handlePoseEvent);
// poseEvent.on(handlePoseEvent);
// return () => {
// unsubscribeTankopediaEphemeral();
// poseEvent.off(handlePoseEvent);
// };
// }, [camera, protagonistTank.id, antagonistTank.id]);
return () => {
unsubscribeTankopediaEphemeral();
// poseEvent.off(handlePoseEvent);
};
}, [
camera,
// protagonistTank.id,
// antagonistTank.id
]);
useEffect(() => {
let isInitialIteration = true;
@@ -1,33 +1,32 @@
import { imgur } from '@blitzkit/core';
import { useFrame, useLoader } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import { useCallback, useRef } from 'react';
import { MeshStandardMaterial, TextureLoader, Vector2 } from 'three';
import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions';
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions';
import { imgur, J_HAT } from '@blitzkit/core';
import { useLoader } from '@react-three/fiber';
import { useCallback, useEffect, useRef } from 'react';
import { Group, Plane, TextureLoader, Vector2 } from 'three';
import { clamp } from 'three/src/math/MathUtils.js';
import {
dissectEvent,
lastDissection,
type DissectEventData,
} from '../../../../../../../core/blitzkit/dissect';
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants';
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
await Promise.all([
awaitableModelDefinitions,
awaitableEquipmentDefinitions,
awaitableProvisionDefinitions,
]);
// const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
// await Promise.all([
// awaitableModelDefinitions,
// awaitableEquipmentDefinitions,
// awaitableProvisionDefinitions,
// ]);
const emptyVector = new Vector2();
// const emptyVector = new Vector2();
const GRID_SIZE = 10;
export function SceneProps() {
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
// const targetCircleWrapper = useRef<Group>(null);
// const clientTargetCircle = useRef<HTMLDivElement>(null);
// const serverTargetCircle = useRef<HTMLDivElement>(null);
const material = useRef<MeshStandardMaterial>(null);
// const playground = useRef<Group>(null);
// let lastPitch = Duel.use((state) => state.protagonist.pitch);
@@ -90,19 +89,12 @@ export function SceneProps() {
// let { dispersion } = characteristics;
// const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed);
// const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`));
const texture = useLoader(
TextureLoader,
imgur(display === TankopediaDisplay.Dissector ? 'GgYPSVA' : 'C28Z8nU'),
);
texture.anisotropy = 2;
const gridTexture = useLoader(TextureLoader, imgur('iq0iOCJ'));
const dragTexture = useLoader(TextureLoader, imgur('Vi3xXsb'));
gridTexture.anisotropy = 2;
// const path = new Vector3();
// const direction = new Vector3();
useFrame(({ camera }) => {
if (!material.current) return;
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
});
// let lastTime = 0;
// let DEBUG_disable = false;
@@ -225,15 +217,23 @@ export function SceneProps() {
// serverTargetCircle.current.style.height = targetCircleDotsSize;
// });
const position = new Vector2();
const delta = new Vector2();
const dissector = useRef<Group>(null);
const position = useRef(new Vector2());
const delta = useRef(new Vector2());
const clippingPlane = useRef(new Plane());
const handlePointerMove = useCallback((event: PointerEvent) => {
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
delta.current.set(event.clientX, event.clientY).sub(position.current);
position.current.set(event.clientX, event.clientY);
dissectEvent.emit({
rotation: lastDissection.rotation + delta.x / window.innerWidth,
offset: lastDissection.offset,
rotation:
lastDissection.rotation + (4 * delta.current.x) / window.innerWidth,
// offset: 0,
offset: clamp(
lastDissection.offset + (4 * delta.current.y) / window.innerHeight,
-5,
5,
),
});
}, []);
const handlePointerUp = useCallback(() => {
@@ -245,26 +245,68 @@ export function SceneProps() {
window.removeEventListener('pointerup', handlePointerUp);
}, []);
useEffect(() => {
function handleDissectEvent(event: DissectEventData) {
clippingPlane.current.normal
.set(0, 0, 1)
.applyAxisAngle(J_HAT, event.rotation - Math.PI / 2);
clippingPlane.current.constant = event.offset;
if (!dissector.current) return;
dissector.current.rotation.set(0, event.rotation, 0);
dissector.current.position
.set(event.offset, 0, 0)
.applyAxisAngle(J_HAT, event.rotation);
}
dissectEvent.on(handleDissectEvent);
handleDissectEvent(lastDissection);
return () => {
dissectEvent.off(handleDissectEvent);
};
}, []);
return (
<>
{display !== TankopediaDisplay.ShootingRange && (
<mesh
rotation={[-Math.PI / 2, 0, 0]}
onPointerDown={(event) => {
event.stopPropagation();
<>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[GRID_SIZE, GRID_SIZE]} />
<meshStandardMaterial
clippingPlanes={[clippingPlane.current]}
map={gridTexture}
transparent
/>
</mesh>
position.set(event.nativeEvent.clientX, event.nativeEvent.clientY);
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
<group ref={dissector}>
<mesh
position={[GRID_SIZE / 8, 0, 0]}
rotation={[-Math.PI / 2, 0, Math.PI / 2]}
onPointerDown={(event) => {
if (display !== TankopediaDisplay.Dissector) return;
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}}
>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial ref={material} map={texture} transparent />
</mesh>
event.stopPropagation();
position.current.set(
event.nativeEvent.clientX,
event.nativeEvent.clientY,
);
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}}
>
<planeGeometry args={[GRID_SIZE / 2, GRID_SIZE / 4]} />
<meshStandardMaterial map={dragTexture} transparent />
</mesh>
</group>
</>
)}
{/* {display === TankopediaDisplay.ShootingRange && (
@@ -7,6 +7,9 @@ export interface DissectEventData {
export const dissectEvent = new EventManager<DissectEventData>();
export const lastDissection: DissectEventData = { rotation: 0, offset: 0 };
export const lastDissection: DissectEventData = {
rotation: Math.PI / 2,
offset: 0,
};
dissectEvent.on((data) => Object.assign(lastDissection, data));