feat: moving components

This commit is contained in:
TrèsAbhi committed 2024-07-31 13:36:56 -05:00
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 { clamp } from 'lodash';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback } from 'react';
import {
Box3,
Color,
DoubleSide,
FrontSide,
Intersection,
LineBasicMaterial,
Mesh,
MeshBasicMaterial,
MeshStandardMaterial,
Object3D,
Plane,
Quaternion,
@@ -27,7 +19,6 @@ import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import { ShellType } from '../../../../core/blitzkit/tankDefinitions';
import * as Duel from '../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral';
import { ThicknessRange } from '../../../StaticArmor/components/StaticArmorScene';
import { ArmorType } from '../SpacedArmorScene';
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
@@ -36,6 +27,7 @@ type SpacedArmorSceneComponentProps = {
node: Object3D;
thickness: number;
clip?: Plane;
scene: Scene;
} & (
| {
type: Exclude<ArmorType, ArmorType.External>;
@@ -44,18 +36,7 @@ type SpacedArmorSceneComponentProps = {
type: ArmorType.External;
variant: ExternalModuleVariant;
}
) &
(
| {
static?: false;
scene: Scene;
}
| {
static: true;
name: string;
thicknessRange: ThicknessRange;
}
);
);
export type ExternalModuleVariant = 'gun' | 'track';
export type ArmorUserData = {
@@ -74,8 +55,6 @@ const omitMaterial = new MeshBasicMaterial({
colorWrite: false,
depthWrite: true,
});
const unselectedColor = new Color(0x404040);
const externalModuleColor = new Color(0, 1 / 4, 1 / 2);
/**
* Render orders allocations:
@@ -92,176 +71,8 @@ export function SpacedArmorSceneComponent({
}: SpacedArmorSceneComponentProps) {
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const duelStore = Duel.useStore();
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
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(
async (
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>
);
},
);
+212 -7
View File
@@ -1,12 +1,217 @@
import {
StaticArmorScene,
ThicknessRange,
} from './components/StaticArmorScene';
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 { StaticArmorSceneComponent } from '../Armor/components/StaticArmorSceneComponent';
interface StaticArmorProps {
interface ArmorSceneProps {
thicknessRange: ThicknessRange;
}
export function StaticArmor({ thicknessRange }: StaticArmorProps) {
return <StaticArmorScene thicknessRange={thicknessRange} />;
export interface 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>
);
});