feat: static armor

This commit is contained in:
TrèsAbhi committed 2024-07-26 13:24:18 -05:00
1 parent e88fdd861f
commit ceb759aba1
6 files changed
+341 -36

No files matched your search

@@ -7,6 +7,7 @@ import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawL
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
import { tankDefinitions } from '../../../../../../core/blitzkit/tankDefinitions';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import * as Duel from '../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../../stores/tankopediaEphemeral';
@@ -22,6 +23,7 @@ export function TankSandbox() {
const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122);
const awaitedModelDefinitions = use(modelDefinitions);
const awaitedTankDefinitions = use(tankDefinitions);
const protagonist = Duel.use((state) => state.protagonist);
const mutateDuel = Duel.useMutation();
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
@@ -137,13 +139,14 @@ export function TankSandbox() {
>
<Controls />
<SceneProps />
<TankModel />
{/* idk why the shot display doesn't work without suspense here lol */}
<Suspense fallback={<ModelLoader />}>
{armorMode === 'blitz' && <Armor />}
{armorMode === 'static' && <StaticArmor />}
{armorMode === 'static' && (
<StaticArmor awaitedTankDefinitions={awaitedTankDefinitions} />
)}
<Lighting />
</Suspense>
@@ -1,5 +1,5 @@
import { ThreeEvent, useThree } from '@react-three/fiber';
import { memo, useRef } from 'react';
import { memo, useEffect, useRef } from 'react';
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
import { ModelTankWrapper } from '../../../../../../../components/Armor/components/ModelTankWrapper';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
@@ -13,12 +13,14 @@ import { useModel } from '../../../../../../../hooks/useModel';
import { useTankTransform } from '../../../../../../../hooks/useTankTransform';
import * as Duel from '../../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../../../stores/tankopediaEphemeral';
import * as TankopediaPersistent from '../../../../../../../stores/tankopediaPersistent';
export const TankModel = memo(() => {
const mutateDuel = Duel.useMutation();
const duelStore = Duel.useStore();
const awaitedModelDefinitions = useAwait(modelDefinitions);
const protagonist = Duel.use((draft) => draft.protagonist);
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const canvas = useThree((state) => state.gl.domElement);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
@@ -36,6 +38,32 @@ export const TankModel = memo(() => {
const { gltf } = useModel(protagonist.tank.id);
const nodes = Object.values(gltf.nodes);
useEffect(() => {
function handleVisibility() {
if (!hullContainer.current) return;
const state = tankopediaPersistentStore.getState();
hullContainer.current.visible =
state.mode === 'model' || state.armorMode === 'blitz';
}
const unsubscribes = [
tankopediaPersistentStore.subscribe(
(state) => state.mode,
handleVisibility,
),
tankopediaPersistentStore.subscribe(
(state) => state.armorMode,
handleVisibility,
),
];
handleVisibility();
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
}, []);
return (
<ModelTankWrapper ref={hullContainer}>
{nodes.map((node) => {
@@ -16,23 +16,7 @@ export const CoreArmorScene = memo(() => {
const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const initialTankopediaState = tankopediaPersistentStore.getState();
const protagonist = Duel.use((draft) => draft.protagonist);
useTankTransform(protagonist, turretContainer, gunContainer);
useEffect(() => {
const unsubscribe = tankopediaPersistentStore.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
@@ -46,6 +30,21 @@ export const CoreArmorScene = memo(() => {
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin);
const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin);
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const initialTankopediaState = tankopediaPersistentStore.getState();
useTankTransform(protagonist, turretContainer, gunContainer);
useEffect(() => {
const unsubscribe = tankopediaPersistentStore.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
return (
<ModelTankWrapper
@@ -1,8 +1,10 @@
import { useThree } from '@react-three/fiber';
import { ThreeEvent, useThree } from '@react-three/fiber';
import { clamp } from 'lodash';
import { useCallback } from 'react';
import {
Intersection,
MeshBasicMaterial,
MeshStandardMaterial,
Object3D,
Plane,
Quaternion,
@@ -19,15 +21,22 @@ 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';
function to255(value: number) {
return Math.round(255 * value);
}
type SpacedArmorSceneComponentProps = {
static?: boolean;
node: Object3D;
thickness: number;
scene: Scene;
clip?: Plane;
thicknessRange: ThicknessRange;
} & (
| {
type: Exclude<ArmorType, ArmorType.External>;
@@ -68,6 +77,8 @@ export function SpacedArmorSceneComponent({
thickness,
scene,
clip,
static: staticMode,
thicknessRange,
...props
}: SpacedArmorSceneComponentProps) {
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
@@ -354,6 +365,41 @@ export function SpacedArmorSceneComponent({
[camera],
);
const onClick = useCallback(
async (event: ThreeEvent<MouseEvent>) => {
event.stopPropagation();
const shot = (await shoot(event.point, event.intersections, true))!;
tankopediaEphemeralStore.setState({ shot });
},
[camera],
);
if (staticMode) {
const x = clamp(thickness / thicknessRange.max, 0, 1);
const r = -((1 - x) ** 2) + 1;
const g = -(x ** 2) + 1;
return jsxTree(node, {
material: new MeshStandardMaterial({
color:
props.type === ArmorType.Core
? `rgb(${to255(r)}, ${to255(g)}, 0)`
: props.type === ArmorType.Spaced
? `rgb(${to255(1 - x)}, 0, ${to255(1 - x)})`
: `rgb(0, 255, 255)`,
opacity: props.type === ArmorType.External ? 0.25 : 1,
transparent: props.type === ArmorType.External,
}),
userData: {
type: props.type,
variant: props.type === ArmorType.External ? props.variant : 'gun',
thickness,
} satisfies ArmorUserData,
onClick: props.type === ArmorType.Core ? onClick : () => {},
});
}
return (
<>
{props.type === ArmorType.Core &&
@@ -366,18 +412,7 @@ export function SpacedArmorSceneComponent({
type: ArmorType.Core,
thickness,
} satisfies ArmorUserData,
async onClick(event) {
event.stopPropagation();
const shot = (await shoot(
event.point,
event.intersections,
true,
))!;
tankopediaEphemeralStore.setState({ shot });
},
onClick,
},
node.uuid,
)}
@@ -1,3 +1,230 @@
export function StaticArmorScene() {
return null;
import { memo, useMemo, useRef } from 'react';
import { Group, Plane, Scene, 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';
import { useTankTransform } from '../../../hooks/useTankTransform';
import * as Duel from '../../../stores/duel';
import { ModelTankWrapper } from '../../Armor/components/ModelTankWrapper';
import { ArmorType } from '../../Armor/components/SpacedArmorScene';
import { SpacedArmorSceneComponent } from '../../Armor/components/SpacedArmorSceneComponent';
interface StaticArmorSceneProps {
scene: Scene;
awaitedTankDefinitions: TankDefinitions;
}
export interface ThicknessRange {
max: number;
}
export const StaticArmorScene = memo<StaticArmorSceneProps>(
({ scene, awaitedTankDefinitions }) => {
const wrapper = useRef<Group>(null);
const awaitedTankModelDefinitions = useModelDefinitions();
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 = awaitedTankModelDefinitions[tank.id];
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 thicknessRange = useMemo(() => {
const thicknesses = Object.values(awaitedTankDefinitions)
.filter((thisTank) => thisTank.tier === tank.tier)
.map((tank) => awaitedTankModelDefinitions[tank.id])
.filter(Boolean)
.map((model) => {
const tankThicknesses = Object.values(model.armor.thickness);
const trackThicknesses = Object.values(model.tracks)
.map((track) => track.thickness)
.flat();
const turretThicknesses = Object.values(model.turrets)
.map((turret) => {
const turretThicknesses = Object.values(turret.armor.thickness);
const gunThicknesses = Object.values(turret.guns)
.map((gun) => {
return Object.values(gun.armor.thickness);
})
.flat();
return [...turretThicknesses, ...gunThicknesses];
})
.flat();
return [
...tankThicknesses,
...trackThicknesses,
...turretThicknesses,
];
})
.flat();
const max = Math.max(...thicknesses) * 0.75;
return { max } satisfies ThicknessRange;
}, [tank.tier]);
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) return null;
return (
<SpacedArmorSceneComponent
thicknessRange={thicknessRange}
static
scene={scene}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Core}
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) return null;
return (
<SpacedArmorSceneComponent
thicknessRange={thicknessRange}
static
scene={scene}
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) return null;
return (
<group key={node.uuid} position={turretOrigin}>
<SpacedArmorSceneComponent
thicknessRange={thicknessRange}
static
scene={scene}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness}
node={node}
/>
</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) return null;
return (
<group
key={node.uuid}
position={turretOrigin.clone().add(gunOrigin)}
>
<SpacedArmorSceneComponent
thicknessRange={thicknessRange}
static
scene={scene}
type={spaced ? ArmorType.Spaced : ArmorType.Core}
thickness={thickness}
node={node}
/>
</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) return null;
return (
<SpacedArmorSceneComponent
thicknessRange={thicknessRange}
static
scene={scene}
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>
);
},
);
+15 -2
View File
@@ -1,5 +1,18 @@
import { useThree } from '@react-three/fiber';
import { TankDefinitions } from '../../core/blitzkit/tankDefinitions';
import { StaticArmorScene } from './components/StaticArmorScene';
export function StaticArmor() {
return <StaticArmorScene />;
export function StaticArmor({
awaitedTankDefinitions,
}: {
awaitedTankDefinitions: TankDefinitions;
}) {
const scene = useThree((state) => state.scene);
return (
<StaticArmorScene
awaitedTankDefinitions={awaitedTankDefinitions}
scene={scene}
/>
);
}