mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
flatten armor dir
This commit is contained in:
1 parent
e18d6f58f9
commit
e5b44d4eb8
28 files changed
+90
-101
No files matched your search
@@ -0,0 +1,329 @@
|
||||
import { I_HAT, J_HAT } from "@blitzkit/core";
|
||||
import { invalidate, useThree } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useMemo, type ComponentProps } from "react";
|
||||
import {
|
||||
Box3,
|
||||
Color,
|
||||
DoubleSide,
|
||||
FrontSide,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Plane,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import { unrotateDavaVector } from "../core/blitz/unrotateDavaVector";
|
||||
import { jsxTree } from "../core/blitzkit/jsxTree";
|
||||
import {
|
||||
modelTransformEvent,
|
||||
type ModelTransformEventData,
|
||||
} from "../core/blitzkit/modelTransform";
|
||||
import { defaultEqualizer } from "../core/blitzkit/tankToDuelMember";
|
||||
import { discardClippingPlane } from "../core/three/discardClippingPlane";
|
||||
import { useEquipment } from "../hooks/useEquipment";
|
||||
import { Duel } from "../stores/duel";
|
||||
import { Tankopedia } from "../stores/tankopedia";
|
||||
import { transitionEvent } from "./Lighting";
|
||||
import { ArmorType } from "./SpacedArmorScene";
|
||||
import type {
|
||||
ArmorUserData,
|
||||
ExternalModuleVariant,
|
||||
} from "./SpacedArmorSceneComponent";
|
||||
import type { ThicknessRange } from "./StaticArmor";
|
||||
|
||||
type StaticArmorSceneComponentProps = {
|
||||
name: string;
|
||||
thickness: number;
|
||||
thicknessRange: ThicknessRange;
|
||||
node: Object3D;
|
||||
onPointerDown?: ComponentProps<"mesh">["onPointerDown"];
|
||||
} & (
|
||||
| {
|
||||
type: Exclude<ArmorType, ArmorType.External>;
|
||||
}
|
||||
| {
|
||||
type: ArmorType.External;
|
||||
variant: ExternalModuleVariant;
|
||||
}
|
||||
) &
|
||||
(
|
||||
| {
|
||||
clip?: undefined;
|
||||
}
|
||||
| {
|
||||
clip: Plane;
|
||||
hullOrigin: Vector3;
|
||||
turretOrigin: Vector3;
|
||||
gunOrigin: Vector3;
|
||||
}
|
||||
);
|
||||
|
||||
const unselectedColor = new Color(0x404040);
|
||||
const externalModuleColor = new Color(192 / 255, 192 / 255, 192 / 255);
|
||||
|
||||
export function StaticArmorSceneComponent({
|
||||
name,
|
||||
thickness: thicknessRaw,
|
||||
thicknessRange,
|
||||
node,
|
||||
onPointerDown,
|
||||
...props
|
||||
}: StaticArmorSceneComponentProps) {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const hasEnhancedArmor = useEquipment("protagonist", 110);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const equalize = Duel.use((state) => state.equalize);
|
||||
const equalizer = (equalize ? tank.equalizer : undefined) ?? defaultEqualizer;
|
||||
const thicknessCoefficient = (hasEnhancedArmor ? 1.04 : 1) * equalizer.armor;
|
||||
const thickness = thicknessRaw * thicknessCoefficient;
|
||||
const x = thickness / thicknessRange.value;
|
||||
const xClamped = clamp(x, 0, 1);
|
||||
const color = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case ArmorType.Primary: {
|
||||
if (x > 1) {
|
||||
return new Color(clamp(2 - x, 0.5, 1), 0, 0);
|
||||
} else {
|
||||
return new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
case ArmorType.Spaced: {
|
||||
return new Color(
|
||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||
0,
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
||||
);
|
||||
}
|
||||
|
||||
case ArmorType.External: {
|
||||
return externalModuleColor;
|
||||
}
|
||||
}
|
||||
}, [props.type, xClamped]);
|
||||
const opacity = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case ArmorType.Primary:
|
||||
return 1;
|
||||
|
||||
case ArmorType.Spaced:
|
||||
return clamp(x + 1 / 2, 0, 1);
|
||||
|
||||
case ArmorType.External:
|
||||
return 1 / 8;
|
||||
}
|
||||
}, [props.type, x]);
|
||||
const depthWrite = useMemo(() => {
|
||||
switch (props.type) {
|
||||
case ArmorType.Primary:
|
||||
return true;
|
||||
|
||||
case ArmorType.Spaced:
|
||||
return true;
|
||||
|
||||
case ArmorType.External:
|
||||
return false;
|
||||
}
|
||||
}, [props.type]);
|
||||
|
||||
const surfaceMaterial = useMemo(
|
||||
() =>
|
||||
new MeshBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
depthWrite,
|
||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||
opacity: 0,
|
||||
userData: { opacity0: opacity },
|
||||
}),
|
||||
[hasEnhancedArmor, equalize],
|
||||
);
|
||||
const outlineMaterial = useMemo(
|
||||
() =>
|
||||
new LineBasicMaterial({
|
||||
color: color
|
||||
.clone()
|
||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||
opacity: 0,
|
||||
transparent: true,
|
||||
}),
|
||||
[hasEnhancedArmor, equalize],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function handleHighlightArmor(selectedName?: string) {
|
||||
if (selectedName === undefined) {
|
||||
// nothing selected, go back to defaults
|
||||
surfaceMaterial.opacity = opacity;
|
||||
surfaceMaterial.transparent = opacity < 1;
|
||||
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.transparent = false;
|
||||
surfaceMaterial.color = color;
|
||||
surfaceMaterial.depthWrite = true;
|
||||
surfaceMaterial.side = DoubleSide;
|
||||
outlineMaterial.visible = true;
|
||||
} else {
|
||||
// something else selected, become background
|
||||
surfaceMaterial.opacity = 1 / 4;
|
||||
surfaceMaterial.transparent = true;
|
||||
surfaceMaterial.color = unselectedColor;
|
||||
surfaceMaterial.depthWrite = props.type !== ArmorType.External;
|
||||
surfaceMaterial.side = FrontSide;
|
||||
outlineMaterial.visible = false;
|
||||
}
|
||||
|
||||
surfaceMaterial.needsUpdate = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
handleHighlightArmor();
|
||||
|
||||
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
|
||||
outlineMaterial.opacity = event.data;
|
||||
|
||||
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
|
||||
outlineMaterial.transparent = outlineMaterial.opacity < 1;
|
||||
}
|
||||
|
||||
transitionEvent.on(handleTransitionEvent);
|
||||
|
||||
const unsubscribes = [
|
||||
Tankopedia.on(
|
||||
(state) => state.highlightArmor?.name,
|
||||
handleHighlightArmor,
|
||||
),
|
||||
() => transitionEvent.off(handleTransitionEvent),
|
||||
];
|
||||
|
||||
if (props.clip) {
|
||||
/**
|
||||
* hook inside an if statement?? don't panic! I assure you the clip prop
|
||||
* never mutates :)
|
||||
*/
|
||||
|
||||
const { clip } = props;
|
||||
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
||||
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
||||
unrotateDavaVector(props.turretOrigin.clone()),
|
||||
);
|
||||
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||
const point = new Vector3();
|
||||
|
||||
function handleModelTransform(
|
||||
event: QuicklimeEvent<ModelTransformEventData>,
|
||||
) {
|
||||
clip.normal
|
||||
.set(0, 0, -1)
|
||||
.applyAxisAngle(I_HAT, event.data.pitch)
|
||||
.applyAxisAngle(J_HAT, event.data.yaw);
|
||||
clip.constant = 0;
|
||||
|
||||
point
|
||||
.copy(gunOrigin)
|
||||
.applyAxisAngle(J_HAT, event.data.yaw)
|
||||
.add(clip.normal.multiplyScalar(-distanceToBarrel))
|
||||
.add(neckOrigin);
|
||||
clip.normal.normalize();
|
||||
clip.constant -= clip.distanceToPoint(point);
|
||||
}
|
||||
|
||||
modelTransformEvent.on(handleModelTransform);
|
||||
|
||||
unsubscribes.push(() => modelTransformEvent.off(handleModelTransform));
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [hasEnhancedArmor, equalize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(node, {
|
||||
mesh(_, meshProps, key) {
|
||||
return (
|
||||
<mesh
|
||||
{...meshProps}
|
||||
key={key}
|
||||
castShadow
|
||||
material={surfaceMaterial}
|
||||
onPointerDown={onPointerDown}
|
||||
userData={
|
||||
{
|
||||
type: props.type,
|
||||
variant:
|
||||
props.type === ArmorType.External ? props.variant : "gun",
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
onClick={(event) => {
|
||||
if (discardClippingPlane(event.object, event.point)) return;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const { editStatic } = Tankopedia.state;
|
||||
|
||||
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);
|
||||
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name,
|
||||
point,
|
||||
thickness,
|
||||
thicknessAngled,
|
||||
angle,
|
||||
color: `#${color.getHexString()}`,
|
||||
editingPlate: editStatic,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
|
||||
{node instanceof Mesh && (
|
||||
<lineSegments material={outlineMaterial}>
|
||||
<edgesGeometry args={[node.geometry, 45]} />
|
||||
</lineSegments>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user