feat: armor toggle buttons

This commit is contained in:
TrèsAbhi committed 2024-07-29 14:08:52 -05:00
1 parent cddfef1b9f
commit f86a4022ea
9 files changed
+353 -215

No files matched your search

@@ -36,7 +36,6 @@ function to255(value: number) {
type SpacedArmorSceneComponentProps = {
node: Object3D;
thickness: number;
scene: Scene;
clip?: Plane;
} & (
| {
@@ -50,6 +49,7 @@ type SpacedArmorSceneComponentProps = {
(
| {
static?: false;
scene: Scene;
}
| {
static: true;
@@ -88,7 +88,6 @@ const externalModuleColor = new Color(0, 1 / 4, 1 / 2);
export function SpacedArmorSceneComponent({
node,
thickness,
scene,
clip,
...props
}: SpacedArmorSceneComponentProps) {
@@ -97,6 +96,131 @@ export function SpacedArmorSceneComponent({
const camera = useThree((state) => state.camera);
const duelStore = Duel.useStore();
if (props.static) {
const x = clamp(thickness / props.thicknessRange.quartile, 0, 1);
const y = Math.sqrt(x);
let color: Color;
let opacity: number;
let renderOrder = 1;
let depthWrite = true;
switch (props.type) {
case ArmorType.Core:
color = new Color(-((1 - y) ** 2) + 1, -(y ** 2) + 1, 0);
opacity = 1;
break;
case ArmorType.Spaced:
color = new Color(1 - (7 / 8) * y, 0, 1 - (1 / 8) * y);
opacity = x + 1 / 2;
break;
case ArmorType.External:
color = externalModuleColor;
opacity = 1 / 8;
renderOrder = 0;
depthWrite = false;
break;
}
opacity = clamp(opacity, 0, 1);
const material = useMemo(
() =>
new MeshStandardMaterial({
color,
opacity,
transparent: true,
depthWrite,
...(clip ? { clippingPlanes: [clip] } : {}),
}),
[],
);
/**
* 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
material.opacity = opacity;
material.transparent = opacity < 1;
material.color = color;
material.depthWrite = props.type !== ArmorType.External;
material.side = FrontSide;
} 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!
material.opacity = 1;
material.transparent = false;
material.color = color;
material.depthWrite = true;
material.side = DoubleSide;
} else {
// something else selected, become background
material.opacity = 1 / 4;
material.transparent = true;
material.color = unselectedColor;
material.depthWrite = props.type !== ArmorType.External;
material.side = FrontSide;
}
}
const unsubscribe = tankopediaEphemeralStore.subscribe(
(state) => state.highlightArmor?.name,
handleHighlightArmor,
);
return unsubscribe;
}, []);
return jsxTree(node, {
material,
renderOrder,
userData: {
type: props.type,
variant: props.type === ArmorType.External ? props.variant : 'gun',
thickness,
} satisfies ArmorUserData,
onClick(event) {
event.stopPropagation();
const { point } = event;
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()}`,
};
});
},
});
}
const shoot = useCallback(
async (
point: Vector3,
@@ -345,7 +469,7 @@ export function SpacedArmorSceneComponent({
caster.set(lastLayer.point, ricochetNormal);
const ricochetIntersections = caster.intersectObjects(
scene.children,
props.scene.children,
true,
);
const outShot = await shoot(
@@ -377,131 +501,6 @@ export function SpacedArmorSceneComponent({
[camera],
);
if (props.static) {
const x = clamp(thickness / props.thicknessRange.quartile, 0, 1);
const y = Math.sqrt(x);
let color: Color;
let opacity: number;
let renderOrder = 1;
let depthWrite = true;
switch (props.type) {
case ArmorType.Core:
color = new Color(-((1 - y) ** 2) + 1, -(y ** 2) + 1, 0);
opacity = 1;
break;
case ArmorType.Spaced:
color = new Color(1 - (7 / 8) * y, 0, 1 - (1 / 8) * y);
opacity = x + 1 / 2;
break;
case ArmorType.External:
color = externalModuleColor;
opacity = 1 / 8;
renderOrder = 0;
depthWrite = false;
break;
}
opacity = clamp(opacity, 0, 1);
const material = useMemo(
() =>
new MeshStandardMaterial({
color,
opacity,
transparent: true,
depthWrite,
...(clip ? { clippingPlanes: [clip] } : {}),
}),
[],
);
/**
* 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
material.opacity = opacity;
material.transparent = opacity < 1;
material.color = color;
material.depthWrite = props.type !== ArmorType.External;
material.side = FrontSide;
} 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!
material.opacity = 1;
material.transparent = false;
material.color = color;
material.depthWrite = true;
material.side = DoubleSide;
} else {
// something else selected, become background
material.opacity = 1 / 4;
material.transparent = true;
material.color = unselectedColor;
material.depthWrite = props.type !== ArmorType.External;
material.side = FrontSide;
}
}
const unsubscribe = tankopediaEphemeralStore.subscribe(
(state) => state.highlightArmor?.name,
handleHighlightArmor,
);
return unsubscribe;
}, []);
return jsxTree(node, {
material,
renderOrder,
userData: {
type: props.type,
variant: props.type === ArmorType.External ? props.variant : 'gun',
thickness,
} satisfies ArmorUserData,
onClick(event) {
event.stopPropagation();
const { point } = event;
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()}`,
};
});
},
});
}
return (
<>
{props.type === ArmorType.Core &&
@@ -1,9 +1,8 @@
import { memo, useMemo, useRef } from 'react';
import { Group, Plane, Scene, Vector3 } from 'three';
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 { TankDefinitions } from '../../../core/blitzkit/tankDefinitions';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
@@ -14,8 +13,7 @@ import { ArmorType } from '../../Armor/components/SpacedArmorScene';
import { SpacedArmorSceneComponent } from '../../Armor/components/SpacedArmorSceneComponent';
interface StaticArmorSceneProps {
scene: Scene;
awaitedTankDefinitions: TankDefinitions;
thicknessRange: ThicknessRange;
}
export interface ThicknessRange {
@@ -24,7 +22,7 @@ export interface ThicknessRange {
}
export const StaticArmorScene = memo<StaticArmorSceneProps>(
({ scene, awaitedTankDefinitions }) => {
({ thicknessRange }) => {
const wrapper = useRef<Group>(null);
const awaitedTankModelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null);
@@ -49,50 +47,6 @@ export const StaticArmorScene = memo<StaticArmorSceneProps>(
gunModelDefinition.mask === undefined
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
let thisTankThicknesses: number[] = [];
const thicknessRange = useMemo(() => {
const thicknesses = Object.values(awaitedTankDefinitions)
.filter((thisTank) => thisTank.tier === tank.tier)
.map((tank) => ({
model: awaitedTankModelDefinitions[tank.id],
id: tank.id,
}))
.filter(Boolean)
.map(({ model, id }) => {
const tankThicknesses = Object.values(model.armor.thickness);
const turretThicknesses = Object.values(model.turrets)
.map((turret) => {
const turretThicknesses = Object.values(turret.armor.thickness);
const gunThicknesses = Object.values(turret.guns)
.map((gun) => Object.values(gun.armor.thickness))
.flat();
return [...turretThicknesses, ...gunThicknesses];
})
.flat();
const totalThicknesses = [...tankThicknesses, ...turretThicknesses];
if (id === tank.id) thisTankThicknesses = totalThicknesses;
return totalThicknesses;
})
.flat();
const sortedThicknesses = thicknesses.sort((a, b) => a - b);
const quartileIndex = (1 - 2 ** -6) * (sortedThicknesses.length + 1) - 1;
let quartile: number;
if (Number.isInteger(quartileIndex)) {
quartile = sortedThicknesses[quartileIndex];
} else {
quartile =
(sortedThicknesses[Math.floor(quartileIndex)] +
sortedThicknesses[Math.ceil(quartileIndex)]) /
2;
}
return { quartile } satisfies ThicknessRange;
}, [tank.tier]);
useTankTransform(protagonist, turretContainer, gunContainer);
@@ -115,7 +69,6 @@ export const StaticArmorScene = memo<StaticArmorSceneProps>(
name={node.name}
thicknessRange={thicknessRange}
static
scene={scene}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness}
@@ -137,7 +90,6 @@ export const StaticArmorScene = memo<StaticArmorSceneProps>(
name={node.name}
thicknessRange={thicknessRange}
static
scene={scene}
key={node.uuid}
type={ArmorType.External}
thickness={trackModelDefinition.thickness}
@@ -168,7 +120,6 @@ export const StaticArmorScene = memo<StaticArmorSceneProps>(
name={node.name}
thicknessRange={thicknessRange}
static
scene={scene}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness}
@@ -203,7 +154,6 @@ export const StaticArmorScene = memo<StaticArmorSceneProps>(
name={node.name}
thicknessRange={thicknessRange}
static
scene={scene}
type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness}
node={node}
@@ -227,7 +177,6 @@ export const StaticArmorScene = memo<StaticArmorSceneProps>(
name={node.name}
thicknessRange={thicknessRange}
static
scene={scene}
key={node.uuid}
type={ArmorType.External}
thickness={gunModelDefinition.thickness}
+9 -15
View File
@@ -1,18 +1,12 @@
import { useThree } from '@react-three/fiber';
import { TankDefinitions } from '../../core/blitzkit/tankDefinitions';
import { StaticArmorScene } from './components/StaticArmorScene';
import {
StaticArmorScene,
ThicknessRange,
} from './components/StaticArmorScene';
export function StaticArmor({
awaitedTankDefinitions,
}: {
awaitedTankDefinitions: TankDefinitions;
}) {
const scene = useThree((state) => state.scene);
interface StaticArmorProps {
thicknessRange: ThicknessRange;
}
return (
<StaticArmorScene
awaitedTankDefinitions={awaitedTankDefinitions}
scene={scene}
/>
);
export function StaticArmor({ thicknessRange }: StaticArmorProps) {
return <StaticArmorScene thicknessRange={thicknessRange} />;
}