mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02: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 { 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user