mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-22 19:19:52 +02:00
feat: armor toggle buttons
This commit is contained in:
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}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
Reference in new issue
Block a user