mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-14 21:26:08 +01:00
feat: armor display
This commit is contained in:
1 parent
6aa4686c30
commit
a7d8f08522
8 files changed
+186
-18
No files matched your search
@@ -19,6 +19,7 @@ import { Loader } from '../../../../components/Loader';
|
||||
import { Controls } from '../Control';
|
||||
import { RotationInputs } from '../RotationInputs';
|
||||
import { SceneProps } from '../SceneProps';
|
||||
import { TankArmor } from './components/TankArmor';
|
||||
import { TankModel } from './components/TankModel';
|
||||
|
||||
export function TankDisplay() {
|
||||
@@ -131,6 +132,7 @@ export function TankDisplay() {
|
||||
}
|
||||
>
|
||||
<TankModel ref={hullContainer} />
|
||||
{mode === 'armor' && <TankArmor />}
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Group, Mesh, Vector3 } from 'three';
|
||||
import { GLTFLoader } from 'three-stdlib';
|
||||
import { X_AXIS } from '../../../../../../../constants/axis';
|
||||
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
||||
import {
|
||||
ModelDefinitions,
|
||||
modelDefinitions,
|
||||
} from '../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import { useTankopedia } from '../../../../../../../stores/tankopedia';
|
||||
|
||||
export const TankArmor = forwardRef<Group>((_props, ref) => {
|
||||
const hullContainer = useRef<Group>(null);
|
||||
const protagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.protagonist;
|
||||
});
|
||||
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
|
||||
ModelDefinitions | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setAwaitedModelDefinitions(await modelDefinitions);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => hullContainer.current!);
|
||||
|
||||
if (!awaitedModelDefinitions) return null;
|
||||
if (!protagonist) return null;
|
||||
|
||||
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonist.turret.id];
|
||||
const turretOrigin = new Vector3(
|
||||
tankModelDefinition.turretOrigin[0],
|
||||
tankModelDefinition.turretOrigin[1],
|
||||
-tankModelDefinition.turretOrigin[2],
|
||||
).applyAxisAngle(X_AXIS, Math.PI / 2);
|
||||
const gunOrigin = new Vector3(
|
||||
turretModelDefinition.gunOrigin[0],
|
||||
turretModelDefinition.gunOrigin[1],
|
||||
-turretModelDefinition.gunOrigin[2],
|
||||
).applyAxisAngle(X_AXIS, Math.PI / 2);
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
const gltf = useLoader(
|
||||
GLTFLoader,
|
||||
asset(`3d/tanks/armor/${protagonist.tank.id}.glb`),
|
||||
);
|
||||
|
||||
return (
|
||||
<group ref={hullContainer} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
{Object.values(gltf.nodes).map((node) => {
|
||||
const armor = Number(node.name.match(/.+_armor_(\d+)/)?.[1]);
|
||||
const position = new Vector3();
|
||||
|
||||
if (node.name.startsWith('turret_')) position.add(turretOrigin);
|
||||
if (node.name.startsWith('gun_'))
|
||||
position.add(turretOrigin).add(gunOrigin);
|
||||
|
||||
return (
|
||||
<mesh
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={position}
|
||||
>
|
||||
<meshBasicMaterial color="red" transparent opacity={0.5} />
|
||||
</mesh>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
});
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { ThreeEvent, useThree } from '@react-three/fiber';
|
||||
import { RefObject, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { Group, Mesh, Object3D, Vector3 } from 'three';
|
||||
import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import {
|
||||
InitialTurretRotation,
|
||||
PitchLimits,
|
||||
YawLimits,
|
||||
} from '../../../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree';
|
||||
import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180';
|
||||
|
||||
interface GunContainerProps {
|
||||
turretOrigin: Vector3;
|
||||
gunOrigin: Vector3;
|
||||
pitch: number;
|
||||
yaw: number;
|
||||
objects: Object3D[];
|
||||
model: number;
|
||||
yawLimits?: YawLimits;
|
||||
pitchLimits: PitchLimits;
|
||||
onPitchStart: () => void;
|
||||
onPitchEnd: (pitch: number, yaw: number) => void;
|
||||
initialTurretRotation?: InitialTurretRotation;
|
||||
turretContainer: RefObject<Group>;
|
||||
}
|
||||
|
||||
export const GunContainer = forwardRef<Group, GunContainerProps>(
|
||||
(
|
||||
{
|
||||
gunOrigin,
|
||||
turretOrigin,
|
||||
pitch,
|
||||
initialTurretRotation,
|
||||
yaw,
|
||||
objects,
|
||||
yawLimits,
|
||||
onPitchStart,
|
||||
turretContainer,
|
||||
pitchLimits,
|
||||
model,
|
||||
onPitchEnd,
|
||||
}: GunContainerProps,
|
||||
ref,
|
||||
) => {
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
|
||||
useImperativeHandle(ref, () => gunContainer.current!);
|
||||
|
||||
return (
|
||||
<group
|
||||
position={new Vector3()
|
||||
.sub(turretOrigin)
|
||||
.sub(gunOrigin)
|
||||
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
|
||||
.add(turretOrigin)
|
||||
.add(gunOrigin)}
|
||||
rotation={[pitch, 0, 0]}
|
||||
ref={gunContainer}
|
||||
>
|
||||
{objects.map((object) => {
|
||||
const isCurrentMantlet =
|
||||
object.name === `gun_${model.toString().padStart(2, '0')}_mask`;
|
||||
const isCurrentGun =
|
||||
object.name === `gun_${model.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||
let draftPitch = 0;
|
||||
let draftYaw = 0;
|
||||
|
||||
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
onPitchStart();
|
||||
draftPitch = pitch;
|
||||
draftYaw = yaw;
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
[draftPitch, draftYaw] = applyPitchYawLimits(
|
||||
draftPitch - event.movementY * (Math.PI / canvas.height),
|
||||
draftYaw + event.movementX * (Math.PI / canvas.width),
|
||||
pitchLimits,
|
||||
yawLimits,
|
||||
);
|
||||
|
||||
if (gunContainer.current) {
|
||||
gunContainer.current.position
|
||||
.set(0, 0, 0)
|
||||
.sub(turretOrigin)
|
||||
.sub(gunOrigin)
|
||||
.applyAxisAngle(new Vector3(1, 0, 0), draftPitch)
|
||||
.add(turretOrigin)
|
||||
.add(gunOrigin);
|
||||
gunContainer.current.rotation.x = draftPitch;
|
||||
}
|
||||
|
||||
if (turretContainer.current) {
|
||||
turretContainer.current.position
|
||||
.set(0, 0, 0)
|
||||
.sub(turretOrigin)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), draftYaw);
|
||||
turretContainer.current.rotation.z = draftYaw;
|
||||
|
||||
if (initialTurretRotation) {
|
||||
const pitch = -initialTurretRotation.pitch * (Math.PI / 180);
|
||||
const yaw = -initialTurretRotation.yaw * (Math.PI / 180);
|
||||
const roll = -initialTurretRotation.roll * (Math.PI / 180);
|
||||
|
||||
turretContainer.current.position
|
||||
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
|
||||
.applyAxisAngle(new Vector3(0, 1, 0), roll)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||
}
|
||||
|
||||
turretContainer.current.position.add(turretOrigin);
|
||||
}
|
||||
}
|
||||
function handlePointerUp() {
|
||||
onPitchEnd(
|
||||
normalizeAnglePI(draftPitch),
|
||||
normalizeAnglePI(draftYaw),
|
||||
);
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
|
||||
if (!isVisible) return null;
|
||||
return (
|
||||
<mesh
|
||||
visible={isVisible}
|
||||
onPointerDown={handlePointerDown}
|
||||
children={resolveJsxTree(object)}
|
||||
key={object.uuid}
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={(object as Mesh).geometry}
|
||||
material={(object as Mesh).material}
|
||||
position={object.position}
|
||||
rotation={object.rotation}
|
||||
scale={object.scale}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
},
|
||||
);
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { ThreeEvent, useThree } from '@react-three/fiber';
|
||||
import { ReactNode, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { Group, Mesh, MeshStandardMaterial, Object3D, Vector2 } from 'three';
|
||||
import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree';
|
||||
import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180';
|
||||
|
||||
interface HullContainerProps {
|
||||
children: ReactNode;
|
||||
yaw: number;
|
||||
objects: Object3D[];
|
||||
onYawStart: () => void;
|
||||
onYawEnd: (yaw: number) => void;
|
||||
onTrackStart: () => void;
|
||||
onTrackEnd: () => void;
|
||||
}
|
||||
|
||||
export const HullContainer = forwardRef<Group, HullContainerProps>(
|
||||
(
|
||||
{ children, yaw, objects, onYawStart, onYawEnd, onTrackEnd, onTrackStart },
|
||||
ref,
|
||||
) => {
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const hullContainer = useRef<Group>(null);
|
||||
|
||||
useImperativeHandle(ref, () => hullContainer.current!);
|
||||
|
||||
return (
|
||||
<group ref={hullContainer} rotation={[-Math.PI / 2, 0, yaw]}>
|
||||
{objects.map((object) => {
|
||||
const isHull = object.name === 'hull';
|
||||
const isWheel = object.name.startsWith('chassis_wheel_');
|
||||
const isTrack = object.name.startsWith('chassis_track_');
|
||||
const isVisible = isHull || isWheel || isTrack;
|
||||
let draftHullYaw = 0;
|
||||
|
||||
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (isHull) {
|
||||
draftHullYaw = yaw;
|
||||
|
||||
onYawStart();
|
||||
window.addEventListener('pointermove', handlePointerMoveHull);
|
||||
window.addEventListener('pointerup', handlePointerUpHull);
|
||||
} else if (isTrack) {
|
||||
onTrackStart();
|
||||
window.addEventListener('pointermove', handlePointerMoveTrack);
|
||||
window.addEventListener('pointerup', handlePointerUpTrack);
|
||||
}
|
||||
}
|
||||
function handlePointerMoveHull(event: PointerEvent) {
|
||||
draftHullYaw += event.movementX * ((2 * Math.PI) / canvas.width);
|
||||
|
||||
if (hullContainer.current) {
|
||||
hullContainer.current.rotation.z = draftHullYaw;
|
||||
}
|
||||
}
|
||||
function handlePointerUpHull() {
|
||||
onYawEnd(normalizeAnglePI(draftHullYaw));
|
||||
window.removeEventListener('pointermove', handlePointerMoveHull);
|
||||
window.removeEventListener('pointerup', handlePointerUpHull);
|
||||
}
|
||||
function handlePointerMoveTrack(event: PointerEvent) {
|
||||
const mesh = object as Mesh;
|
||||
const material = mesh.material as MeshStandardMaterial;
|
||||
const offset = new Vector2(
|
||||
0,
|
||||
event.movementY * (6 / canvas.height) +
|
||||
event.movementX * (6 / canvas.width),
|
||||
);
|
||||
|
||||
material.map?.offset.add(offset);
|
||||
material.roughnessMap?.offset.add(offset);
|
||||
material.normalMap?.offset.add(offset);
|
||||
}
|
||||
function handlePointerUpTrack() {
|
||||
onTrackEnd();
|
||||
window.removeEventListener('pointermove', handlePointerMoveTrack);
|
||||
window.removeEventListener('pointerup', handlePointerUpTrack);
|
||||
}
|
||||
|
||||
if (!isVisible) return null;
|
||||
return (
|
||||
<mesh
|
||||
children={resolveJsxTree(object)}
|
||||
key={object.uuid}
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={(object as Mesh).geometry}
|
||||
material={(object as Mesh).material}
|
||||
position={object.position}
|
||||
rotation={object.rotation}
|
||||
scale={object.scale}
|
||||
onPointerDown={handlePointerDown}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{children}
|
||||
</group>
|
||||
);
|
||||
},
|
||||
);
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import { ThreeEvent, useThree } from '@react-three/fiber';
|
||||
import {
|
||||
ReactNode,
|
||||
RefObject,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Euler, Group, Mesh, Object3D, Vector3 } from 'three';
|
||||
import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import {
|
||||
InitialTurretRotation,
|
||||
PitchLimits,
|
||||
YawLimits,
|
||||
} from '../../../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree';
|
||||
import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180';
|
||||
|
||||
interface TurretContainerProps {
|
||||
turretOrigin: Vector3;
|
||||
gunOrigin: Vector3;
|
||||
objects: Object3D[];
|
||||
children: ReactNode;
|
||||
yaw: number;
|
||||
pitch: number;
|
||||
model: number;
|
||||
yawLimits?: YawLimits;
|
||||
pitchLimits: PitchLimits;
|
||||
onYawStart: () => void;
|
||||
onYawEnd: (pitch: number, yaw: number) => void;
|
||||
gunContainer: RefObject<Group>;
|
||||
initialTurretRotation?: InitialTurretRotation;
|
||||
}
|
||||
|
||||
export const TurretContainer = forwardRef(
|
||||
(
|
||||
{
|
||||
gunContainer,
|
||||
turretOrigin,
|
||||
yaw,
|
||||
children,
|
||||
objects,
|
||||
yawLimits,
|
||||
model,
|
||||
onYawEnd,
|
||||
pitch,
|
||||
gunOrigin,
|
||||
pitchLimits,
|
||||
onYawStart,
|
||||
initialTurretRotation,
|
||||
}: TurretContainerProps,
|
||||
ref,
|
||||
) => {
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const position = new Vector3()
|
||||
.sub(turretOrigin)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||
const rotation = new Euler(0, 0, yaw);
|
||||
|
||||
if (initialTurretRotation) {
|
||||
const pitch = -initialTurretRotation.pitch * (Math.PI / 180);
|
||||
const yaw = -initialTurretRotation.yaw * (Math.PI / 180);
|
||||
const roll = -initialTurretRotation.roll * (Math.PI / 180);
|
||||
|
||||
position
|
||||
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
|
||||
.applyAxisAngle(new Vector3(0, 1, 0), roll)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||
rotation.x += pitch;
|
||||
rotation.y += roll;
|
||||
rotation.z += yaw;
|
||||
}
|
||||
|
||||
position.add(turretOrigin);
|
||||
|
||||
useImperativeHandle(ref, () => turretContainer.current!);
|
||||
|
||||
return (
|
||||
<group position={position} rotation={rotation} ref={turretContainer}>
|
||||
{objects.map((object) => {
|
||||
const isTurret = object.name.startsWith('turret_');
|
||||
const isCurrentTurret =
|
||||
object.name === `turret_${model.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
let draftPitch = 0;
|
||||
let draftYaw = 0;
|
||||
|
||||
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (isTurret) {
|
||||
draftYaw = yaw;
|
||||
draftPitch = pitch;
|
||||
|
||||
onYawStart();
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
[draftPitch, draftYaw] = applyPitchYawLimits(
|
||||
draftPitch,
|
||||
draftYaw + event.movementX * (Math.PI / canvas.width),
|
||||
pitchLimits,
|
||||
yawLimits,
|
||||
);
|
||||
|
||||
if (gunContainer.current) {
|
||||
gunContainer.current.position
|
||||
.set(0, 0, 0)
|
||||
.sub(turretOrigin)
|
||||
.sub(gunOrigin)
|
||||
.applyAxisAngle(new Vector3(1, 0, 0), draftPitch)
|
||||
.add(turretOrigin)
|
||||
.add(gunOrigin);
|
||||
gunContainer.current.rotation.x = draftPitch;
|
||||
}
|
||||
|
||||
if (turretContainer.current) {
|
||||
turretContainer.current.position
|
||||
.set(0, 0, 0)
|
||||
.sub(turretOrigin)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), draftYaw);
|
||||
turretContainer.current.rotation.z = draftYaw;
|
||||
|
||||
if (initialTurretRotation) {
|
||||
const pitch = -initialTurretRotation.pitch * (Math.PI / 180);
|
||||
const yaw = -initialTurretRotation.yaw * (Math.PI / 180);
|
||||
const roll = -initialTurretRotation.roll * (Math.PI / 180);
|
||||
|
||||
turretContainer.current.position
|
||||
.applyAxisAngle(new Vector3(1, 0, 0), pitch)
|
||||
.applyAxisAngle(new Vector3(0, 1, 0), roll)
|
||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||
}
|
||||
|
||||
turretContainer.current.position.add(turretOrigin);
|
||||
}
|
||||
}
|
||||
function handlePointerUp() {
|
||||
onYawEnd(normalizeAnglePI(draftPitch), normalizeAnglePI(draftYaw));
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
|
||||
if (!isVisible) return null;
|
||||
return (
|
||||
<mesh
|
||||
visible={isVisible}
|
||||
onPointerDown={handlePointerDown}
|
||||
children={resolveJsxTree(object)}
|
||||
key={object.uuid}
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={(object as Mesh).geometry}
|
||||
material={(object as Mesh).material}
|
||||
position={object.position}
|
||||
rotation={object.rotation}
|
||||
scale={object.scale}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{children}
|
||||
</group>
|
||||
);
|
||||
},
|
||||
);
|
||||
+7
-7
@@ -8,18 +8,18 @@ import {
|
||||
} from 'react';
|
||||
import { Group, Vector3 } from 'three';
|
||||
import { GLTFLoader } from 'three-stdlib';
|
||||
import { X_AXIS } from '../../../../../../../constants/axis';
|
||||
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
||||
import { X_AXIS } from '../../../../../../../../constants/axis';
|
||||
import { asset } from '../../../../../../../../core/blitzkrieg/asset';
|
||||
import {
|
||||
ModelDefinitions,
|
||||
modelDefinitions,
|
||||
} from '../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
} from '../../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../../../stores/tankopedia';
|
||||
import { GunContainer } from '../../GunContainer';
|
||||
import { HullContainer } from '../../HullContainer';
|
||||
import { TurretContainer } from '../../TurretContainer';
|
||||
} from '../../../../../../../../stores/tankopedia';
|
||||
import { GunContainer } from './components/GunContainer';
|
||||
import { HullContainer } from './components/HullContainer';
|
||||
import { TurretContainer } from './components/TurretContainer';
|
||||
|
||||
export const TankModel = forwardRef<Group>((_props, ref) => {
|
||||
// "TypeError: dispatcher.use is not a function"
|
||||
Reference in new issue
Block a user