mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +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
@@ -1,8 +1,11 @@
|
||||
import { ChevronLeftIcon, MixIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Button, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { Suspense, use, useEffect, useMemo, useState } from 'react';
|
||||
import { classIcons } from '../../../../../../components/ClassIcon';
|
||||
import { ThicknessRange } from '../../../../../../components/StaticArmor/components/StaticArmorScene';
|
||||
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
||||
import { tankDefinitions } from '../../../../../../core/blitzkit/tankDefinitions';
|
||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||
import strings from '../../../../../../lang/en-US.json';
|
||||
@@ -11,7 +14,11 @@ import { Options } from './components/Options';
|
||||
import { TankSandbox } from './TankSandbox';
|
||||
import { TankSandboxLoader } from './TankSandboxLoader';
|
||||
|
||||
export function HeroSection({ id }: { id: number }) {
|
||||
interface HeroSectionProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function HeroSection({ id }: HeroSectionProps) {
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const antagonist = Duel.use((state) => state.antagonist.tank);
|
||||
const compareTanks =
|
||||
@@ -27,6 +34,50 @@ export function HeroSection({ id }: { id: number }) {
|
||||
: protagonist.treeType === 'premium'
|
||||
? 'amber'
|
||||
: undefined;
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
const thicknessRange = useMemo(() => {
|
||||
const thicknesses = Object.values(awaitedTankDefinitions)
|
||||
.filter((thisTank) => thisTank.tier === tank.tier)
|
||||
.map((tank) => ({
|
||||
model: awaitedModelDefinitions[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];
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
setDummyLoader(false);
|
||||
@@ -129,11 +180,11 @@ export function HeroSection({ id }: { id: number }) {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TankSandbox />
|
||||
<TankSandbox thicknessRange={thicknessRange} />
|
||||
</Suspense>
|
||||
</Box>
|
||||
|
||||
<Options />
|
||||
<Options thicknessRange={thicknessRange} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Armor } from '../../../../../../components/Armor';
|
||||
import { ArmorPlateDisplay } from '../../../../../../components/Armor/components/ArmorPlateDisplay';
|
||||
import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay';
|
||||
import { StaticArmor } from '../../../../../../components/StaticArmor';
|
||||
import { ThicknessRange } from '../../../../../../components/StaticArmor/components/StaticArmorScene';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
||||
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||
@@ -20,7 +21,11 @@ import { SceneProps } from '../SceneProps';
|
||||
import { ModelLoader } from './components/ModelLoader';
|
||||
import { TankModel } from './components/TankModel';
|
||||
|
||||
export function TankSandbox() {
|
||||
interface TankSandboxProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
}
|
||||
|
||||
export function TankSandbox({ thicknessRange }: TankSandboxProps) {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
@@ -165,7 +170,7 @@ export function TankSandbox() {
|
||||
<Suspense fallback={<ModelLoader />}>
|
||||
{display === TankopediaDisplay.DynamicArmor && <Armor />}
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<StaticArmor awaitedTankDefinitions={awaitedTankDefinitions} />
|
||||
<StaticArmor thicknessRange={thicknessRange} />
|
||||
)}
|
||||
<Lighting />
|
||||
</Suspense>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
||||
import { SmallTankIcon } from '../../../../../../../components/SmallTankIcon';
|
||||
import { ThicknessRange } from '../../../../../../../components/StaticArmor/components/StaticArmorScene';
|
||||
import { resolveNearPenetration } from '../../../../../../../core/blitz/resolveNearPenetration';
|
||||
import { resolvePenetrationCoefficient } from '../../../../../../../core/blitz/resolvePenetrationCoefficient';
|
||||
import { asset } from '../../../../../../../core/blitzkit/asset';
|
||||
@@ -42,9 +43,14 @@ import * as TankopediaPersistent from '../../../../../../../stores/tankopediaPer
|
||||
import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants';
|
||||
import { TankSearch } from '../../../../components/TankSearch';
|
||||
import { ENVIRONMENTS } from '../../Lighting';
|
||||
import { RotationInputs } from '../../QuickInputs';
|
||||
import { QuickInputs } from '../../QuickInputs';
|
||||
import { Thicknesses } from '../../Thicknesses';
|
||||
|
||||
export function Options() {
|
||||
interface OptionsProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
}
|
||||
|
||||
export function Options({ thicknessRange }: OptionsProps) {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const displayRaw = TankopediaPersistent.use((state) => state.display);
|
||||
@@ -113,7 +119,11 @@ export function Options() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<RotationInputs />
|
||||
<QuickInputs />
|
||||
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<Thicknesses thicknessRange={thicknessRange} />
|
||||
)}
|
||||
|
||||
{display === TankopediaDisplay.DynamicArmor && (
|
||||
<Flex
|
||||
@@ -483,17 +493,17 @@ export function Options() {
|
||||
>
|
||||
<SegmentedControl.Item value={`${TankopediaDisplay.Model}`}>
|
||||
<Flex height="100%" align="center">
|
||||
<img src={imgur('jAdYf0m')} style={{ height: '1.5em' }} />
|
||||
<img src={imgur('jAdYf0m')} style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaDisplay.DynamicArmor}`}>
|
||||
<Flex height="100%" align="center">
|
||||
<img src={imgur('oe4Cq0g')} style={{ height: '1.5em' }} />
|
||||
<img src={imgur('oe4Cq0g')} style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaDisplay.StaticArmor}`}>
|
||||
<Flex height="100%" align="center">
|
||||
<img src={imgur('VQ4uDno')} style={{ height: '1.5em' }} />
|
||||
<img src={imgur('VQ4uDno')} style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
</SegmentedControl.Item>
|
||||
</SegmentedControl.Root>
|
||||
|
||||
@@ -9,12 +9,10 @@ import {
|
||||
ModelTransformEventData,
|
||||
} from '../../../../../../core/blitzkit/modelTransform';
|
||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||
import * as Duel from '../../../../../../stores/duel';
|
||||
import * as TankopediaPersistent from '../../../../../../stores/tankopediaPersistent';
|
||||
import * as styles from './index.css';
|
||||
|
||||
export function RotationInputs() {
|
||||
export function QuickInputs() {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const protagonist = Duel.use((state) => state.protagonist);
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
@@ -26,8 +24,6 @@ export function RotationInputs() {
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
const initialGunPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const isFullScreen = useFullScreen();
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import { ThicknessRange } from '../../../../../components/StaticArmor/components/StaticArmorScene';
|
||||
import * as TankopediaPersistent from '../../../../../stores/tankopediaPersistent';
|
||||
|
||||
interface ThicknessesProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
}
|
||||
|
||||
export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
const showExternalModules = TankopediaPersistent.use(
|
||||
(state) => state.model.visual.showExternalModules,
|
||||
);
|
||||
const showSpacedArmor = TankopediaPersistent.use(
|
||||
(state) => state.model.visual.showSpacedArmor,
|
||||
);
|
||||
const showCoreArmor = TankopediaPersistent.use(
|
||||
(state) => state.model.visual.showCoreArmor,
|
||||
);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="absolute"
|
||||
right="0"
|
||||
top="50%"
|
||||
mr="4"
|
||||
style={{ transform: 'translateY(-50%)' }}
|
||||
direction="column"
|
||||
gap="2"
|
||||
align="end"
|
||||
>
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showCoreArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.showCoreArmor =
|
||||
!draft.model.visual.showCoreArmor;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{thicknessRange.quartile.toFixed(0)}
|
||||
</Text>
|
||||
<Flex gap="1">
|
||||
{showCoreArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
<Text size="1">Core</Text>
|
||||
</Flex>
|
||||
<Text color="gray" size="1">
|
||||
0
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="16px"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(rgb(255, 0, 0), rgb(0, 255, 0))`,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.showSpacedArmor =
|
||||
!draft.model.visual.showSpacedArmor;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{thicknessRange.quartile.toFixed(0)}
|
||||
</Text>
|
||||
<Flex gap="1">
|
||||
{showSpacedArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
<Text size="1">Spaced</Text>
|
||||
</Flex>
|
||||
<Text color="gray" size="1">
|
||||
0
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="16px"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(rgb(32, 0, 223), rgb(255, 0, 255))`,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.showExternalModules =
|
||||
!draft.model.visual.showExternalModules;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Flex gap="1">
|
||||
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
<Text size="1">External</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
width="16px"
|
||||
height="16px"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgb(0, 64, 128)',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export interface TankopediaPersistent {
|
||||
greenPenetration: boolean;
|
||||
showEnvironment: boolean;
|
||||
showSpacedArmor: boolean;
|
||||
showExternalModules: boolean;
|
||||
showCoreArmor: boolean;
|
||||
};
|
||||
};
|
||||
sort: {
|
||||
@@ -47,6 +49,8 @@ export const { Provider, use, useMutation, useStore } = createNextSafeStore(
|
||||
model: {
|
||||
visual: {
|
||||
showSpacedArmor: true,
|
||||
showExternalModules: true,
|
||||
showCoreArmor: true,
|
||||
wireframe: false,
|
||||
opaque: false,
|
||||
environment: ENVIRONMENTS[0],
|
||||
|
||||
Reference in new issue
Block a user