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