mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-27 04:29:50 +01:00
feat: moving components
This commit is contained in:
1 parent
c6249c5a83
commit
5810ca8f81
4 files changed
+430
-423
No files matched your search
@@ -1,16 +1,8 @@
|
|||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash';
|
import { useCallback } from 'react';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Box3,
|
|
||||||
Color,
|
|
||||||
DoubleSide,
|
|
||||||
FrontSide,
|
|
||||||
Intersection,
|
Intersection,
|
||||||
LineBasicMaterial,
|
|
||||||
Mesh,
|
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
MeshStandardMaterial,
|
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
@@ -27,7 +19,6 @@ import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
|||||||
import { ShellType } from '../../../../core/blitzkit/tankDefinitions';
|
import { ShellType } from '../../../../core/blitzkit/tankDefinitions';
|
||||||
import * as Duel from '../../../../stores/duel';
|
import * as Duel from '../../../../stores/duel';
|
||||||
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral';
|
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral';
|
||||||
import { ThicknessRange } from '../../../StaticArmor/components/StaticArmorScene';
|
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
import { ArmorType } from '../SpacedArmorScene';
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
||||||
@@ -36,6 +27,7 @@ type SpacedArmorSceneComponentProps = {
|
|||||||
node: Object3D;
|
node: Object3D;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
clip?: Plane;
|
clip?: Plane;
|
||||||
|
scene: Scene;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
@@ -44,18 +36,7 @@ type SpacedArmorSceneComponentProps = {
|
|||||||
type: ArmorType.External;
|
type: ArmorType.External;
|
||||||
variant: ExternalModuleVariant;
|
variant: ExternalModuleVariant;
|
||||||
}
|
}
|
||||||
) &
|
);
|
||||||
(
|
|
||||||
| {
|
|
||||||
static?: false;
|
|
||||||
scene: Scene;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
static: true;
|
|
||||||
name: string;
|
|
||||||
thicknessRange: ThicknessRange;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ExternalModuleVariant = 'gun' | 'track';
|
export type ExternalModuleVariant = 'gun' | 'track';
|
||||||
export type ArmorUserData = {
|
export type ArmorUserData = {
|
||||||
@@ -74,8 +55,6 @@ const omitMaterial = new MeshBasicMaterial({
|
|||||||
colorWrite: false,
|
colorWrite: false,
|
||||||
depthWrite: true,
|
depthWrite: true,
|
||||||
});
|
});
|
||||||
const unselectedColor = new Color(0x404040);
|
|
||||||
const externalModuleColor = new Color(0, 1 / 4, 1 / 2);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render orders allocations:
|
* Render orders allocations:
|
||||||
@@ -92,176 +71,8 @@ export function SpacedArmorSceneComponent({
|
|||||||
}: SpacedArmorSceneComponentProps) {
|
}: SpacedArmorSceneComponentProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
if (props.static) {
|
|
||||||
const x = thickness / props.thicknessRange.value;
|
|
||||||
|
|
||||||
let color: Color;
|
|
||||||
let opacity: number;
|
|
||||||
let depthWrite = true;
|
|
||||||
|
|
||||||
switch (props.type) {
|
|
||||||
case ArmorType.Primary:
|
|
||||||
if (x > 1) {
|
|
||||||
color = new Color(Math.max(2 - x, 0.5), 0, 0);
|
|
||||||
} else {
|
|
||||||
color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
|
|
||||||
}
|
|
||||||
opacity = 1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ArmorType.Spaced:
|
|
||||||
color = new Color(
|
|
||||||
clamp(1 - (7 / 8) * x, 0, 1),
|
|
||||||
0,
|
|
||||||
clamp(1 - (1 / 8) * x, 0, 1),
|
|
||||||
);
|
|
||||||
opacity = clamp(x + 1 / 2, 0, 1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ArmorType.External:
|
|
||||||
color = externalModuleColor;
|
|
||||||
opacity = 1 / 8;
|
|
||||||
depthWrite = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
opacity = clamp(opacity, 0, 1);
|
|
||||||
|
|
||||||
const surfaceMaterial = useMemo(
|
|
||||||
() =>
|
|
||||||
new MeshStandardMaterial({
|
|
||||||
color,
|
|
||||||
opacity,
|
|
||||||
transparent: true,
|
|
||||||
depthWrite,
|
|
||||||
...(clip ? { clippingPlanes: [clip] } : {}),
|
|
||||||
}),
|
|
||||||
[thickness],
|
|
||||||
);
|
|
||||||
const outlineMaterial = useMemo(
|
|
||||||
() =>
|
|
||||||
new LineBasicMaterial({
|
|
||||||
color: color.clone().multiplyScalar(2 ** 2),
|
|
||||||
}),
|
|
||||||
[thickness],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* hook inside an if statement?? don't panic! I assure you the static prop
|
|
||||||
* never mutates :)
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
function handleHighlightArmor(selectedName?: string) {
|
|
||||||
if (!props.static) return;
|
|
||||||
|
|
||||||
if (selectedName === undefined) {
|
|
||||||
// nothing selected, go back to defaults
|
|
||||||
surfaceMaterial.opacity = opacity;
|
|
||||||
surfaceMaterial.color = color;
|
|
||||||
surfaceMaterial.depthWrite = props.type !== ArmorType.External;
|
|
||||||
surfaceMaterial.side = FrontSide;
|
|
||||||
outlineMaterial.visible = true;
|
|
||||||
} else if (
|
|
||||||
selectedName === props.name ||
|
|
||||||
(props.name.startsWith('chassis_') &&
|
|
||||||
selectedName.startsWith('chassis_')) ||
|
|
||||||
(props.name.startsWith('gun_') &&
|
|
||||||
selectedName.startsWith('gun_') &&
|
|
||||||
!props.name.includes('_armor_') &&
|
|
||||||
!selectedName.includes('_armor_'))
|
|
||||||
) {
|
|
||||||
// this selected, stand out!
|
|
||||||
surfaceMaterial.opacity = 1;
|
|
||||||
surfaceMaterial.color = color;
|
|
||||||
surfaceMaterial.depthWrite = true;
|
|
||||||
surfaceMaterial.side = DoubleSide;
|
|
||||||
outlineMaterial.visible = true;
|
|
||||||
} else {
|
|
||||||
// something else selected, become background
|
|
||||||
surfaceMaterial.opacity = 1 / 4;
|
|
||||||
surfaceMaterial.color = unselectedColor;
|
|
||||||
surfaceMaterial.depthWrite = props.type !== ArmorType.External;
|
|
||||||
surfaceMaterial.side = FrontSide;
|
|
||||||
outlineMaterial.visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribe = tankopediaEphemeralStore.subscribe(
|
|
||||||
(state) => state.highlightArmor?.name,
|
|
||||||
handleHighlightArmor,
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
}, [thickness]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{jsxTree(node, {
|
|
||||||
material: surfaceMaterial,
|
|
||||||
userData: {
|
|
||||||
type: props.type,
|
|
||||||
variant: props.type === ArmorType.External ? props.variant : 'gun',
|
|
||||||
thickness,
|
|
||||||
} satisfies ArmorUserData,
|
|
||||||
|
|
||||||
onClick(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
|
||||||
|
|
||||||
if (editStatic) {
|
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
|
||||||
draft.editingPlate = {
|
|
||||||
name: props.name,
|
|
||||||
default: thickness,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const bounds = new Box3().setFromObject(event.object);
|
|
||||||
const point = bounds.min
|
|
||||||
.clone()
|
|
||||||
.add(bounds.max)
|
|
||||||
.divideScalar(2)
|
|
||||||
.setY(bounds.max.y);
|
|
||||||
const cameraNormal = camera.position
|
|
||||||
.clone()
|
|
||||||
.sub(point)
|
|
||||||
.normalize();
|
|
||||||
const surfaceNormal = event
|
|
||||||
.normal!.clone()
|
|
||||||
.applyQuaternion(
|
|
||||||
event.object.getWorldQuaternion(new Quaternion()),
|
|
||||||
);
|
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
|
||||||
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
|
||||||
|
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
|
||||||
draft.highlightArmor = {
|
|
||||||
type: props.type,
|
|
||||||
name: props.name,
|
|
||||||
point,
|
|
||||||
thickness,
|
|
||||||
thicknessAngled,
|
|
||||||
angle,
|
|
||||||
color: `#${color.getHexString()}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
|
|
||||||
<lineSegments material={outlineMaterial}>
|
|
||||||
<edgesGeometry args={[(node as Mesh).geometry, 45]} />
|
|
||||||
</lineSegments>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const shoot = useCallback(
|
const shoot = useCallback(
|
||||||
async (
|
async (
|
||||||
point: Vector3,
|
point: Vector3,
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import { useThree } from '@react-three/fiber';
|
||||||
|
import { clamp } from 'lodash';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Box3,
|
||||||
|
Color,
|
||||||
|
DoubleSide,
|
||||||
|
FrontSide,
|
||||||
|
LineBasicMaterial,
|
||||||
|
Mesh,
|
||||||
|
MeshStandardMaterial,
|
||||||
|
Object3D,
|
||||||
|
Plane,
|
||||||
|
Quaternion,
|
||||||
|
} from 'three';
|
||||||
|
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
||||||
|
import * as Duel from '../../../stores/duel';
|
||||||
|
import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral';
|
||||||
|
import { ThicknessRange } from '../../StaticArmor';
|
||||||
|
import { ArmorType } from './SpacedArmorScene';
|
||||||
|
import {
|
||||||
|
ArmorUserData,
|
||||||
|
ExternalModuleVariant,
|
||||||
|
} from './SpacedArmorSceneComponent';
|
||||||
|
|
||||||
|
type StaticArmorSceneComponentProps = {
|
||||||
|
name: string;
|
||||||
|
clip?: Plane;
|
||||||
|
thickness: number;
|
||||||
|
thicknessRange: ThicknessRange;
|
||||||
|
node: Object3D;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ArmorType.External;
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const unselectedColor = new Color(0x404040);
|
||||||
|
const externalModuleColor = new Color(0, 1 / 4, 1 / 2);
|
||||||
|
|
||||||
|
export function StaticArmorSceneComponent({
|
||||||
|
name,
|
||||||
|
thickness,
|
||||||
|
thicknessRange,
|
||||||
|
node,
|
||||||
|
clip,
|
||||||
|
...props
|
||||||
|
}: StaticArmorSceneComponentProps) {
|
||||||
|
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
||||||
|
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||||
|
const duelStore = Duel.useStore();
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
|
const x = thickness / thicknessRange.value;
|
||||||
|
let color: Color;
|
||||||
|
let opacity: number;
|
||||||
|
let depthWrite = true;
|
||||||
|
|
||||||
|
switch (props.type) {
|
||||||
|
case ArmorType.Primary:
|
||||||
|
if (x > 1) {
|
||||||
|
color = new Color(Math.max(2 - x, 0.5), 0, 0);
|
||||||
|
} else {
|
||||||
|
color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
|
||||||
|
}
|
||||||
|
opacity = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArmorType.Spaced:
|
||||||
|
color = new Color(
|
||||||
|
clamp(1 - (7 / 8) * x, 0, 1),
|
||||||
|
0,
|
||||||
|
clamp(1 - (1 / 8) * x, 0, 1),
|
||||||
|
);
|
||||||
|
opacity = clamp(x + 1 / 2, 0, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArmorType.External:
|
||||||
|
color = externalModuleColor;
|
||||||
|
opacity = 1 / 8;
|
||||||
|
depthWrite = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity = clamp(opacity, 0, 1);
|
||||||
|
|
||||||
|
const surfaceMaterial = useMemo(
|
||||||
|
() =>
|
||||||
|
new MeshStandardMaterial({
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
transparent: true,
|
||||||
|
depthWrite,
|
||||||
|
...(clip ? { clippingPlanes: [clip] } : {}),
|
||||||
|
}),
|
||||||
|
[thickness],
|
||||||
|
);
|
||||||
|
const outlineMaterial = useMemo(
|
||||||
|
() =>
|
||||||
|
new LineBasicMaterial({
|
||||||
|
color: color.clone().multiplyScalar(2 ** 2),
|
||||||
|
}),
|
||||||
|
[thickness],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hook inside an if statement?? don't panic! I assure you the static prop
|
||||||
|
* never mutates :)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
function handleHighlightArmor(selectedName?: string) {
|
||||||
|
if (selectedName === undefined) {
|
||||||
|
// nothing selected, go back to defaults
|
||||||
|
surfaceMaterial.opacity = opacity;
|
||||||
|
surfaceMaterial.color = color;
|
||||||
|
surfaceMaterial.depthWrite = props.type !== ArmorType.External;
|
||||||
|
surfaceMaterial.side = FrontSide;
|
||||||
|
outlineMaterial.visible = true;
|
||||||
|
} else if (
|
||||||
|
selectedName === name ||
|
||||||
|
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) ||
|
||||||
|
(name.startsWith('gun_') &&
|
||||||
|
selectedName.startsWith('gun_') &&
|
||||||
|
!name.includes('_armor_') &&
|
||||||
|
!selectedName.includes('_armor_'))
|
||||||
|
) {
|
||||||
|
// this selected, stand out!
|
||||||
|
surfaceMaterial.opacity = 1;
|
||||||
|
surfaceMaterial.color = color;
|
||||||
|
surfaceMaterial.depthWrite = true;
|
||||||
|
surfaceMaterial.side = DoubleSide;
|
||||||
|
outlineMaterial.visible = true;
|
||||||
|
} else {
|
||||||
|
// something else selected, become background
|
||||||
|
surfaceMaterial.opacity = 1 / 4;
|
||||||
|
surfaceMaterial.color = unselectedColor;
|
||||||
|
surfaceMaterial.depthWrite = props.type !== ArmorType.External;
|
||||||
|
surfaceMaterial.side = FrontSide;
|
||||||
|
outlineMaterial.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = tankopediaEphemeralStore.subscribe(
|
||||||
|
(state) => state.highlightArmor?.name,
|
||||||
|
handleHighlightArmor,
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [thickness]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsxTree(node, {
|
||||||
|
material: surfaceMaterial,
|
||||||
|
userData: {
|
||||||
|
type: props.type,
|
||||||
|
variant: props.type === ArmorType.External ? props.variant : 'gun',
|
||||||
|
thickness,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
|
|
||||||
|
onClick(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { editStatic } = tankopediaEphemeralStore.getState();
|
||||||
|
|
||||||
|
if (editStatic) {
|
||||||
|
mutateTankopediaEphemeralStore((draft) => {
|
||||||
|
draft.editingPlate = {
|
||||||
|
name: name,
|
||||||
|
default: thickness,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const bounds = new Box3().setFromObject(event.object);
|
||||||
|
const point = bounds.min
|
||||||
|
.clone()
|
||||||
|
.add(bounds.max)
|
||||||
|
.divideScalar(2)
|
||||||
|
.setY(bounds.max.y);
|
||||||
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||||
|
const surfaceNormal = event
|
||||||
|
.normal!.clone()
|
||||||
|
.applyQuaternion(
|
||||||
|
event.object.getWorldQuaternion(new Quaternion()),
|
||||||
|
);
|
||||||
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
|
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
||||||
|
|
||||||
|
mutateTankopediaEphemeralStore((draft) => {
|
||||||
|
draft.highlightArmor = {
|
||||||
|
type: props.type,
|
||||||
|
name,
|
||||||
|
point,
|
||||||
|
thickness,
|
||||||
|
thicknessAngled,
|
||||||
|
angle,
|
||||||
|
color: `#${color.getHexString()}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
|
||||||
|
<lineSegments material={outlineMaterial}>
|
||||||
|
<edgesGeometry args={[(node as Mesh).geometry, 45]} />
|
||||||
|
</lineSegments>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import { memo, useRef } from 'react';
|
|
||||||
import { Group, Plane, Vector3 } from 'three';
|
|
||||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
|
||||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
|
||||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
|
||||||
import { useArmor } from '../../../hooks/useArmor';
|
|
||||||
import { useModel } from '../../../hooks/useModel';
|
|
||||||
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
|
|
||||||
import { useTankTransform } from '../../../hooks/useTankTransform';
|
|
||||||
import * as Duel from '../../../stores/duel';
|
|
||||||
import * as TankopediaPersistent from '../../../stores/tankopediaPersistent';
|
|
||||||
import { ModelTankWrapper } from '../../Armor/components/ModelTankWrapper';
|
|
||||||
import { ArmorType } from '../../Armor/components/SpacedArmorScene';
|
|
||||||
import { SpacedArmorSceneComponent } from '../../Armor/components/SpacedArmorSceneComponent';
|
|
||||||
|
|
||||||
interface StaticArmorSceneProps {
|
|
||||||
thicknessRange: ThicknessRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThicknessRange {
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StaticArmorScene = memo<StaticArmorSceneProps>(
|
|
||||||
({ thicknessRange }) => {
|
|
||||||
const wrapper = useRef<Group>(null);
|
|
||||||
const turretContainer = useRef<Group>(null);
|
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
|
||||||
const armorGltf = useArmor(tank.id);
|
|
||||||
const { gltf: modelGltf } = useModel(tank.id);
|
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
|
||||||
const modelNodes = Object.values(modelGltf.nodes);
|
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
|
||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin);
|
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin);
|
|
||||||
const maskOrigin =
|
|
||||||
gunModelDefinition.mask === undefined
|
|
||||||
? undefined
|
|
||||||
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
|
||||||
const showPrimaryArmor = TankopediaPersistent.use(
|
|
||||||
(state) => state.model.visual.showPrimaryArmor,
|
|
||||||
);
|
|
||||||
const showSpacedArmor = TankopediaPersistent.use(
|
|
||||||
(state) => state.model.visual.showSpacedArmor,
|
|
||||||
);
|
|
||||||
const showExternalModules = TankopediaPersistent.use(
|
|
||||||
(state) => state.model.visual.showExternalModules,
|
|
||||||
);
|
|
||||||
|
|
||||||
useTankTransform(protagonist, turretContainer, gunContainer);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModelTankWrapper ref={wrapper}>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isHull = node.name.startsWith('hull_');
|
|
||||||
const isVisible = isHull;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
tankModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isVisible ||
|
|
||||||
thickness === undefined ||
|
|
||||||
(!showPrimaryArmor && !spaced) ||
|
|
||||||
(!showSpacedArmor && spaced)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
name={node.name}
|
|
||||||
thicknessRange={thicknessRange}
|
|
||||||
static
|
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
const isVisible = isWheel || isTrack;
|
|
||||||
|
|
||||||
if (!isVisible || !showExternalModules) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
name={node.name}
|
|
||||||
thicknessRange={thicknessRange}
|
|
||||||
static
|
|
||||||
key={node.uuid}
|
|
||||||
type={ArmorType.External}
|
|
||||||
thickness={trackModelDefinition.thickness}
|
|
||||||
variant="track"
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentTurret = node.name.startsWith(
|
|
||||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentTurret;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
turretModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isVisible ||
|
|
||||||
thickness === undefined ||
|
|
||||||
(!showPrimaryArmor && !spaced) ||
|
|
||||||
(!showSpacedArmor && spaced)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
<group key={node.uuid} position={turretOrigin}>
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
name={node.name}
|
|
||||||
thicknessRange={thicknessRange}
|
|
||||||
static
|
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={gunContainer}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentGun = node.name.startsWith(
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
gunModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isVisible ||
|
|
||||||
thickness === undefined ||
|
|
||||||
(!showPrimaryArmor && !spaced) ||
|
|
||||||
(!showSpacedArmor && spaced)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
<group
|
|
||||||
key={node.uuid}
|
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
|
||||||
>
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
name={node.name}
|
|
||||||
thicknessRange={thicknessRange}
|
|
||||||
static
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
|
||||||
const gunString = `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
|
||||||
const isCurrentGun = gunModelDefinition.mask
|
|
||||||
? node.name.startsWith(gunString)
|
|
||||||
: node.name === gunString;
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
|
|
||||||
if (!isVisible || !showExternalModules) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
name={node.name}
|
|
||||||
thicknessRange={thicknessRange}
|
|
||||||
static
|
|
||||||
key={node.uuid}
|
|
||||||
type={ArmorType.External}
|
|
||||||
thickness={gunModelDefinition.thickness}
|
|
||||||
variant="gun"
|
|
||||||
node={node}
|
|
||||||
clip={
|
|
||||||
maskOrigin === undefined
|
|
||||||
? undefined
|
|
||||||
: new Plane(new Vector3(0, 0, -1), -maskOrigin)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</ModelTankWrapper>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,12 +1,217 @@
|
|||||||
import {
|
import { memo, useRef } from 'react';
|
||||||
StaticArmorScene,
|
import { Group, Plane, Vector3 } from 'three';
|
||||||
ThicknessRange,
|
import { correctZYTuple } from '../../core/blitz/correctZYTuple';
|
||||||
} from './components/StaticArmorScene';
|
import { nameToArmorId } from '../../core/blitzkit/nameToArmorId';
|
||||||
|
import { resolveArmor } from '../../core/blitzkit/resolveThickness';
|
||||||
|
import { useArmor } from '../../hooks/useArmor';
|
||||||
|
import { useModel } from '../../hooks/useModel';
|
||||||
|
import { useTankModelDefinition } from '../../hooks/useTankModelDefinition';
|
||||||
|
import { useTankTransform } from '../../hooks/useTankTransform';
|
||||||
|
import * as Duel from '../../stores/duel';
|
||||||
|
import * as TankopediaPersistent from '../../stores/tankopediaPersistent';
|
||||||
|
import { ModelTankWrapper } from '../Armor/components/ModelTankWrapper';
|
||||||
|
import { ArmorType } from '../Armor/components/SpacedArmorScene';
|
||||||
|
import { StaticArmorSceneComponent } from '../Armor/components/StaticArmorSceneComponent';
|
||||||
|
|
||||||
interface StaticArmorProps {
|
interface ArmorSceneProps {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StaticArmor({ thicknessRange }: StaticArmorProps) {
|
export interface ThicknessRange {
|
||||||
return <StaticArmorScene thicknessRange={thicknessRange} />;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||||
|
const wrapper = useRef<Group>(null);
|
||||||
|
const turretContainer = useRef<Group>(null);
|
||||||
|
const gunContainer = useRef<Group>(null);
|
||||||
|
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
const armorGltf = useArmor(tank.id);
|
||||||
|
const { gltf: modelGltf } = useModel(tank.id);
|
||||||
|
const armorNodes = Object.values(armorGltf.nodes);
|
||||||
|
const modelNodes = Object.values(modelGltf.nodes);
|
||||||
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
|
const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin);
|
||||||
|
const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin);
|
||||||
|
const maskOrigin =
|
||||||
|
gunModelDefinition.mask === undefined
|
||||||
|
? undefined
|
||||||
|
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
||||||
|
const showPrimaryArmor = TankopediaPersistent.use(
|
||||||
|
(state) => state.model.visual.showPrimaryArmor,
|
||||||
|
);
|
||||||
|
const showSpacedArmor = TankopediaPersistent.use(
|
||||||
|
(state) => state.model.visual.showSpacedArmor,
|
||||||
|
);
|
||||||
|
const showExternalModules = TankopediaPersistent.use(
|
||||||
|
(state) => state.model.visual.showExternalModules,
|
||||||
|
);
|
||||||
|
|
||||||
|
useTankTransform(protagonist, turretContainer, gunContainer);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModelTankWrapper ref={wrapper}>
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
{armorNodes.map((node) => {
|
||||||
|
const isHull = node.name.startsWith('hull_');
|
||||||
|
const isVisible = isHull;
|
||||||
|
const armorId = nameToArmorId(node.name);
|
||||||
|
const { spaced, thickness } = resolveArmor(
|
||||||
|
tankModelDefinition.armor,
|
||||||
|
armorId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isVisible ||
|
||||||
|
thickness === undefined ||
|
||||||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
|
(!showSpacedArmor && spaced)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StaticArmorSceneComponent
|
||||||
|
name={node.name}
|
||||||
|
thicknessRange={thicknessRange}
|
||||||
|
key={node.uuid}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{modelNodes.map((node) => {
|
||||||
|
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||||
|
const isTrack = node.name.startsWith('chassis_track_');
|
||||||
|
const isVisible = isWheel || isTrack;
|
||||||
|
|
||||||
|
if (!isVisible || !showExternalModules) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StaticArmorSceneComponent
|
||||||
|
name={node.name}
|
||||||
|
thicknessRange={thicknessRange}
|
||||||
|
key={node.uuid}
|
||||||
|
type={ArmorType.External}
|
||||||
|
thickness={trackModelDefinition.thickness}
|
||||||
|
variant="track"
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group ref={turretContainer}>
|
||||||
|
{armorNodes.map((node) => {
|
||||||
|
const isCurrentTurret = node.name.startsWith(
|
||||||
|
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
const isVisible = isCurrentTurret;
|
||||||
|
const armorId = nameToArmorId(node.name);
|
||||||
|
const { spaced, thickness } = resolveArmor(
|
||||||
|
turretModelDefinition.armor,
|
||||||
|
armorId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isVisible ||
|
||||||
|
thickness === undefined ||
|
||||||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
|
(!showSpacedArmor && spaced)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
<group key={node.uuid} position={turretOrigin}>
|
||||||
|
<StaticArmorSceneComponent
|
||||||
|
name={node.name}
|
||||||
|
thicknessRange={thicknessRange}
|
||||||
|
key={node.uuid}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group ref={gunContainer}>
|
||||||
|
{armorNodes.map((node) => {
|
||||||
|
const isCurrentGun = node.name.startsWith(
|
||||||
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
const isVisible = isCurrentGun;
|
||||||
|
const armorId = nameToArmorId(node.name);
|
||||||
|
const { spaced, thickness } = resolveArmor(
|
||||||
|
gunModelDefinition.armor,
|
||||||
|
armorId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isVisible ||
|
||||||
|
thickness === undefined ||
|
||||||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
|
(!showSpacedArmor && spaced)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
<group
|
||||||
|
key={node.uuid}
|
||||||
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
|
>
|
||||||
|
<StaticArmorSceneComponent
|
||||||
|
name={node.name}
|
||||||
|
thicknessRange={thicknessRange}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{modelNodes.map((node) => {
|
||||||
|
const gunString = `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||||
|
const isCurrentGun = gunModelDefinition.mask
|
||||||
|
? node.name.startsWith(gunString)
|
||||||
|
: node.name === gunString;
|
||||||
|
const isVisible = isCurrentGun;
|
||||||
|
|
||||||
|
if (!isVisible || !showExternalModules) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StaticArmorSceneComponent
|
||||||
|
name={node.name}
|
||||||
|
thicknessRange={thicknessRange}
|
||||||
|
key={node.uuid}
|
||||||
|
type={ArmorType.External}
|
||||||
|
thickness={gunModelDefinition.thickness}
|
||||||
|
variant="gun"
|
||||||
|
node={node}
|
||||||
|
clip={
|
||||||
|
maskOrigin === undefined
|
||||||
|
? undefined
|
||||||
|
: new Plane(new Vector3(0, 0, -1), -maskOrigin)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</ModelTankWrapper>
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user