mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
feat: on click display
This commit is contained in:
1 parent
b01a03c6ce
commit
c2e550e33d
5 files changed
+191
-9
No files matched your search
@@ -3,6 +3,7 @@ import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes';
|
|||||||
import { PerspectiveCamera } from '@react-three/drei';
|
import { PerspectiveCamera } from '@react-three/drei';
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
||||||
|
import { ShotDisplay } from '../../../../../../components/ShotDisplay';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
||||||
@@ -166,12 +167,22 @@ export function TankSandbox() {
|
|||||||
|
|
||||||
<div style={{ height: '100%' }}>
|
<div style={{ height: '100%' }}>
|
||||||
{loadModel ? (
|
{loadModel ? (
|
||||||
<Canvas shadows ref={canvas} onPointerDown={handlePointerDown}>
|
<Canvas
|
||||||
|
shadows
|
||||||
|
ref={canvas}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMissed={() => {
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PerspectiveCamera makeDefault fov={25} far={32} />
|
<PerspectiveCamera makeDefault fov={25} far={32} />
|
||||||
<Controls />
|
<Controls />
|
||||||
<SceneProps />
|
<SceneProps />
|
||||||
|
|
||||||
<Suspense fallback={<ModelLoader />}>
|
<Suspense fallback={<ModelLoader />}>
|
||||||
|
<ShotDisplay />
|
||||||
<Lighting />
|
<Lighting />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
<TankArmor />
|
<TankArmor />
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ export const TankModel = memo(() => {
|
|||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.controlsEnabled = false;
|
draft.model.visual.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
@@ -208,6 +211,9 @@ export const TankModel = memo(() => {
|
|||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.controlsEnabled = false;
|
draft.model.visual.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
@@ -272,6 +278,9 @@ export const TankModel = memo(() => {
|
|||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.controlsEnabled = false;
|
draft.model.visual.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
pitch = physical.pitch;
|
pitch = physical.pitch;
|
||||||
yaw = physical.yaw;
|
yaw = physical.yaw;
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
|
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { IUniform, ShaderMaterial, Vector2 } from 'three';
|
import { IUniform, Mesh, ShaderMaterial, Vector2, Vector3 } from 'three';
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { I_HAT } from '../../../../constants/axis';
|
||||||
import { canSplash } from '../../../../core/blitz/canSplash';
|
import { canSplash } from '../../../../core/blitz/canSplash';
|
||||||
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||||
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
||||||
import { useDuel } from '../../../../stores/duel';
|
import { useDuel } from '../../../../stores/duel';
|
||||||
import {
|
import {
|
||||||
|
ArmorPiercingLayer,
|
||||||
|
mutateTankopediaTemporary,
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
useTankopediaTemporary,
|
|
||||||
} from '../../../../stores/tankopedia';
|
} from '../../../../stores/tankopedia';
|
||||||
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
|
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
|
||||||
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
|
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
|
||||||
@@ -35,6 +37,10 @@ export function ArmorMesh({
|
|||||||
const initialTankopedia = useTankopediaPersistent.getState();
|
const initialTankopedia = useTankopediaPersistent.getState();
|
||||||
const material = useRef<ShaderMaterial>(null);
|
const material = useRef<ShaderMaterial>(null);
|
||||||
const explosionCapable = isExplosive(initialShell.type);
|
const explosionCapable = isExplosive(initialShell.type);
|
||||||
|
const cameraNormal = new Vector3();
|
||||||
|
const shellNormal = new Vector3();
|
||||||
|
const surfaceNormal = new Vector3();
|
||||||
|
const mesh = useRef<Mesh>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function updateQuickEquipments() {
|
function updateQuickEquipments() {
|
||||||
@@ -126,15 +132,106 @@ export function ArmorMesh({
|
|||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorMeshUserData
|
} satisfies ArmorMeshUserData
|
||||||
}
|
}
|
||||||
|
ref={mesh}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (useTankopediaTemporary.getState().mode !== 'armor') return;
|
// if (useTankopediaTemporary.getState().mode !== 'armor') return;
|
||||||
|
|
||||||
event.intersections
|
shellNormal.copy(
|
||||||
|
cameraNormal.copy(camera.position).sub(event.point).normalize(),
|
||||||
|
);
|
||||||
|
surfaceNormal.copy(event.normal!).applyAxisAngle(I_HAT, -Math.PI / 2);
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const intersections = event.intersections
|
||||||
.filter(({ object }) => Boolean(object.userData.type))
|
.filter(({ object }) => Boolean(object.userData.type))
|
||||||
.map(({ object }) => {
|
.map(
|
||||||
const data = object.userData as ArmorMeshUserData;
|
(event) =>
|
||||||
console.log(data.type, data.thickness);
|
event as typeof event & {
|
||||||
});
|
object: (typeof event)['object'] & {
|
||||||
|
userData: ArmorMeshUserData;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const intersectionsTillFirstCoreArmor = intersections.slice(
|
||||||
|
0,
|
||||||
|
intersections.findIndex(
|
||||||
|
({ object }) => object.userData.type === 'coreArmor',
|
||||||
|
) + 1,
|
||||||
|
);
|
||||||
|
const coreArmorMesh = intersectionsTillFirstCoreArmor.at(-1)!.object;
|
||||||
|
|
||||||
|
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
|
||||||
|
|
||||||
|
const armorPiercingLayers = intersectionsTillFirstCoreArmor.map(
|
||||||
|
(event, index) => {
|
||||||
|
switch (event.object.userData.type) {
|
||||||
|
case 'spacedArmor': {
|
||||||
|
return {
|
||||||
|
nominal: event.object.userData.thickness,
|
||||||
|
angled: event.object.userData.thickness,
|
||||||
|
ricochet: false,
|
||||||
|
} satisfies ArmorPiercingLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'externalModule':
|
||||||
|
case 'coreArmor': {
|
||||||
|
const angle = Math.acos(surfaceNormal.dot(shellNormal));
|
||||||
|
const threeCalibersRule =
|
||||||
|
shell.caliber > 3 * event.object.userData.thickness;
|
||||||
|
let hasRicochet = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
index === 0 &&
|
||||||
|
!isExplosive(shell.type) &&
|
||||||
|
!threeCalibersRule &&
|
||||||
|
angle >= degToRad(shell.ricochet!)
|
||||||
|
) {
|
||||||
|
hasRicochet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const twoCalibersRule =
|
||||||
|
shell.caliber > 2 * event.object.userData.thickness;
|
||||||
|
const normalization = twoCalibersRule
|
||||||
|
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
||||||
|
(2.0 * thickness)
|
||||||
|
: shell.normalization ?? 0;
|
||||||
|
const angledThickness =
|
||||||
|
thickness / Math.cos(angle - normalization);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nominal: event.object.userData.thickness,
|
||||||
|
angled: angledThickness,
|
||||||
|
ricochet: hasRicochet,
|
||||||
|
} satisfies ArmorPiercingLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRicochet = armorPiercingLayers.some(
|
||||||
|
({ ricochet }) => ricochet,
|
||||||
|
);
|
||||||
|
const accumulatedThickness = armorPiercingLayers.reduce(
|
||||||
|
(sum, { angled }) => sum + angled,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const hasPenetrated =
|
||||||
|
resolveNearPenetration(shell.penetration) >= accumulatedThickness;
|
||||||
|
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = {
|
||||||
|
type: hasRicochet
|
||||||
|
? 'ricochet'
|
||||||
|
: hasPenetrated
|
||||||
|
? 'penetration'
|
||||||
|
: 'block',
|
||||||
|
point: event.point.toArray(),
|
||||||
|
thicknesses: armorPiercingLayers,
|
||||||
|
shellNormal: shellNormal.toArray(),
|
||||||
|
surfaceNormal: surfaceNormal.toArray(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(draft.shot);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<shaderMaterial
|
<shaderMaterial
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Card, Flex, Text } from '@radix-ui/themes';
|
||||||
|
import { Html } from '@react-three/drei';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Group, Vector3 } from 'three';
|
||||||
|
import { SHOT_NAMES, useTankopediaTemporary } from '../stores/tankopedia';
|
||||||
|
|
||||||
|
const THICKNESS = 0.05;
|
||||||
|
const LENGTH = 2;
|
||||||
|
|
||||||
|
export function ShotDisplay() {
|
||||||
|
const shot = useTankopediaTemporary((state) => state.shot);
|
||||||
|
const wrapper = useRef<Group>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shot) return;
|
||||||
|
|
||||||
|
const point = new Vector3(...shot.point);
|
||||||
|
const shotNormal = new Vector3(...shot.shellNormal);
|
||||||
|
|
||||||
|
wrapper.current?.position.set(...shot.point);
|
||||||
|
wrapper.current?.lookAt(point.clone().add(shotNormal));
|
||||||
|
}, [shot]);
|
||||||
|
|
||||||
|
if (!shot) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={wrapper}>
|
||||||
|
<Html position={[0, 0, LENGTH]}>
|
||||||
|
<Card>
|
||||||
|
<Flex direction="column">
|
||||||
|
<Text>{SHOT_NAMES[shot.type]}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Html>
|
||||||
|
|
||||||
|
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
|
<cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} />
|
||||||
|
{/* <boxGeometry args={[THICKNESS, THICKNESS, LENGTH]} /> */}
|
||||||
|
<meshBasicMaterial color="white" depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
|
import { Vector3Tuple } from 'three';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||||
import { TankType, TreeType } from '../components/Tanks';
|
import { TankType, TreeType } from '../components/Tanks';
|
||||||
@@ -63,6 +64,27 @@ interface TankopediaTemporary {
|
|||||||
consumables: number[];
|
consumables: number[];
|
||||||
provisions: number[];
|
provisions: number[];
|
||||||
camouflage: boolean;
|
camouflage: boolean;
|
||||||
|
shot?: Shot;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Shot = {
|
||||||
|
point: Vector3Tuple;
|
||||||
|
surfaceNormal: Vector3Tuple;
|
||||||
|
shellNormal: Vector3Tuple;
|
||||||
|
thicknesses: ArmorPiercingLayer[];
|
||||||
|
type: 'ricochet' | 'penetration' | 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SHOT_NAMES: Record<Shot['type'], string> = {
|
||||||
|
ricochet: 'Ricochet',
|
||||||
|
penetration: 'Penetration',
|
||||||
|
block: 'Blocked',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ArmorPiercingLayer {
|
||||||
|
nominal: number;
|
||||||
|
angled: number;
|
||||||
|
ricochet: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTankopediaPersistent = create<TankopediaPersistent>()(
|
export const useTankopediaPersistent = create<TankopediaPersistent>()(
|
||||||
|
|||||||
Reference in new issue
Block a user