mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: more rendering
This commit is contained in:
1 parent
5b16e22750
commit
cbd2c2a719
10 files changed
+315
-379
No files matched your search
@@ -5,16 +5,13 @@ import { Color, Material, MeshBasicMaterial } from 'three';
|
||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
|
||||
interface ArmorSceneProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
}
|
||||
import { StaticArmorSceneComponents } from './StaticArmorSceneComponents';
|
||||
|
||||
export interface ThicknessRange {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
export const StaticArmor = memo(() => {
|
||||
// const wrapper = useRef<Group>(null);
|
||||
// const turretContainer = useRef<Group>(null);
|
||||
// const gunContainer = useRef<Group>(null);
|
||||
@@ -58,44 +55,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
|
||||
// useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const armor = TankopediaEphemeral.use((state) => state.armor);
|
||||
const hullArmor = armor.groups.hull;
|
||||
const hullGltf = useGLTF(asset(`tanks/${tank.id}/collision/hull.glb`));
|
||||
|
||||
return (
|
||||
<group>
|
||||
{jsxTree(hullGltf.scene, {
|
||||
mesh(mesh) {
|
||||
if (!(mesh.material instanceof Material)) {
|
||||
return { visible: false };
|
||||
}
|
||||
|
||||
const armorName = mesh.material.name;
|
||||
const material = new MeshBasicMaterial({});
|
||||
const armorPlate = hullArmor.armors[armorName];
|
||||
|
||||
switch (armorPlate.type) {
|
||||
case 'Armor':
|
||||
material.color = new Color('#ec6142');
|
||||
material.transparent = true;
|
||||
material.opacity = 0.5;
|
||||
break;
|
||||
|
||||
case 'ArmorScreen':
|
||||
material.color = new Color('#ff8dcc');
|
||||
material.transparent = true;
|
||||
material.opacity = 0.5;
|
||||
break;
|
||||
|
||||
default:
|
||||
material.color = new Color('#70b8ff');
|
||||
break;
|
||||
}
|
||||
|
||||
return { material };
|
||||
},
|
||||
})}
|
||||
<StaticArmorSceneComponents group="hull" />
|
||||
|
||||
</group>
|
||||
);
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { type MeshProps, useThree } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Box3,
|
||||
Color,
|
||||
DoubleSide,
|
||||
FrontSide,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshStandardMaterial,
|
||||
Object3D,
|
||||
Plane,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
|
||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
||||
import {
|
||||
type ModelTransformEventData,
|
||||
modelTransformEvent,
|
||||
} from '../../../core/blitzkit/modelTransform';
|
||||
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
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?: MeshProps['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,
|
||||
thicknessRange,
|
||||
node,
|
||||
onPointerDown,
|
||||
...props
|
||||
}: StaticArmorSceneComponentProps) {
|
||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const camera = useThree((state) => state.camera);
|
||||
const x = thickness / thicknessRange.value;
|
||||
const xClamped = clamp(x, 0, 1);
|
||||
let color: Color;
|
||||
let opacity: number;
|
||||
let depthWrite = true;
|
||||
|
||||
switch (props.type) {
|
||||
case ArmorType.Primary:
|
||||
if (x > 1) {
|
||||
color = new Color(clamp(2 - x, 0.5, 1), 0, 0);
|
||||
} else {
|
||||
color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
|
||||
}
|
||||
opacity = 1;
|
||||
break;
|
||||
|
||||
case ArmorType.Spaced:
|
||||
color = new Color(
|
||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||
0,
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
||||
);
|
||||
opacity = clamp(x + 1 / 2, 0, 1);
|
||||
break;
|
||||
|
||||
case ArmorType.External:
|
||||
color = externalModuleColor;
|
||||
opacity = 1 / 8;
|
||||
depthWrite = false;
|
||||
break;
|
||||
}
|
||||
|
||||
opacity = clamp(opacity, 0, 1);
|
||||
|
||||
const surfaceMaterial = useMemo(
|
||||
() =>
|
||||
new MeshStandardMaterial({
|
||||
color,
|
||||
opacity,
|
||||
transparent: opacity < 1,
|
||||
depthWrite,
|
||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||
}),
|
||||
[thickness],
|
||||
);
|
||||
const outlineMaterial = useMemo(
|
||||
() =>
|
||||
new LineBasicMaterial({
|
||||
color: color
|
||||
.clone()
|
||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||
}),
|
||||
[thickness],
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const unsubscribes = [
|
||||
tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.highlightArmor?.name,
|
||||
handleHighlightArmor,
|
||||
),
|
||||
];
|
||||
|
||||
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(data: ModelTransformEventData) {
|
||||
clip.normal
|
||||
.set(0, 0, -1)
|
||||
.applyAxisAngle(I_HAT, data.pitch)
|
||||
.applyAxisAngle(J_HAT, data.yaw ?? 0);
|
||||
clip.constant = 0;
|
||||
|
||||
point
|
||||
.copy(gunOrigin)
|
||||
.applyAxisAngle(J_HAT, data.yaw ?? 0)
|
||||
.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());
|
||||
};
|
||||
}, [thickness]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(node, {
|
||||
mesh: {
|
||||
material: surfaceMaterial,
|
||||
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 } = tankopediaEphemeralStore.getState();
|
||||
|
||||
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);
|
||||
|
||||
mutateTankopediaEphemeralStore((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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { Color, Material, MeshStandardMaterial } from 'three';
|
||||
import { clamp } from 'three/src/math/MathUtils.js';
|
||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
|
||||
type StaticArmorSceneComponentsProps = {
|
||||
group: string;
|
||||
};
|
||||
|
||||
const unselectedColor = new Color(0x404040);
|
||||
const externalModuleColor = new Color(192 / 255, 192 / 255, 192 / 255);
|
||||
|
||||
export function StaticArmorSceneComponents({
|
||||
group,
|
||||
}: StaticArmorSceneComponentsProps) {
|
||||
const showModules = TankopediaPersistent.use((state) => state.showModules);
|
||||
const showArmorScreen = TankopediaPersistent.use(
|
||||
(state) => state.showArmorScreen,
|
||||
);
|
||||
const showArmor = TankopediaPersistent.use((state) => state.showArmor);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const gltf = useGLTF(asset(`tanks/${tank.id}/collision/${group}.glb`));
|
||||
const armor = TankopediaEphemeral.use((state) => state.armor);
|
||||
const groupArmor = armor.groups[group];
|
||||
const thicknessRange = TankopediaEphemeral.use(
|
||||
(state) => state.thicknessRange,
|
||||
);
|
||||
|
||||
return jsxTree(gltf.scene, {
|
||||
mesh(mesh) {
|
||||
if (!(mesh.material instanceof Material)) {
|
||||
return { visible: false };
|
||||
}
|
||||
|
||||
const armorName = mesh.material.name;
|
||||
const armorPlate = groupArmor.armors[armorName];
|
||||
const x = armorPlate.thickness / thicknessRange;
|
||||
const xClamped = clamp(x, 0, 1);
|
||||
const material = new MeshStandardMaterial();
|
||||
|
||||
switch (armorPlate.type) {
|
||||
case 'Armor':
|
||||
if (x > 1) {
|
||||
material.color = new Color(clamp(2 - x, 0.5, 1), 0, 0);
|
||||
} else {
|
||||
material.color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
|
||||
}
|
||||
material.opacity = 1;
|
||||
material.visible = showArmor;
|
||||
break;
|
||||
|
||||
case 'ArmorScreen':
|
||||
material.color = new Color(
|
||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||
0,
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
||||
);
|
||||
material.opacity = clamp(x + 1 / 2, 0, 1);
|
||||
material.visible = showArmorScreen;
|
||||
break;
|
||||
|
||||
default:
|
||||
material.color = externalModuleColor;
|
||||
material.opacity = 7 / 8;
|
||||
material.depthWrite = false;
|
||||
material.visible = showModules;
|
||||
break;
|
||||
}
|
||||
material.opacity = clamp(material.opacity, 0, 1);
|
||||
material.transparent = material.opacity < 1;
|
||||
|
||||
return { material };
|
||||
},
|
||||
});
|
||||
|
||||
// const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
||||
// const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
// const camera = useThree((state) => state.camera);
|
||||
// const x = thickness / thicknessRange.value;
|
||||
// const xClamped = clamp(x, 0, 1);
|
||||
// let color: Color;
|
||||
// let opacity: number;
|
||||
// let depthWrite = true;
|
||||
// switch (props.type) {
|
||||
// case ArmorType.Primary:
|
||||
// if (x > 1) {
|
||||
// color = new Color(clamp(2 - x, 0.5, 1), 0, 0);
|
||||
// } else {
|
||||
// color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0);
|
||||
// }
|
||||
// opacity = 1;
|
||||
// break;
|
||||
// case ArmorType.Spaced:
|
||||
// color = new Color(
|
||||
// clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||
// 0,
|
||||
// clamp(1 - (1 / 8) * xClamped, 0, 1),
|
||||
// );
|
||||
// opacity = clamp(x + 1 / 2, 0, 1);
|
||||
// break;
|
||||
// case ArmorType.External:
|
||||
// color = externalModuleColor;
|
||||
// opacity = 1 / 8;
|
||||
// depthWrite = false;
|
||||
// break;
|
||||
// }
|
||||
// opacity = clamp(opacity, 0, 1);
|
||||
// const surfaceMaterial = useMemo(
|
||||
// () =>
|
||||
// new MeshStandardMaterial({
|
||||
// color,
|
||||
// opacity,
|
||||
// transparent: opacity < 1,
|
||||
// depthWrite,
|
||||
// ...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||
// }),
|
||||
// [thickness],
|
||||
// );
|
||||
// const outlineMaterial = useMemo(
|
||||
// () =>
|
||||
// new LineBasicMaterial({
|
||||
// color: color
|
||||
// .clone()
|
||||
// .multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||
// }),
|
||||
// [thickness],
|
||||
// );
|
||||
// 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;
|
||||
// }
|
||||
// const unsubscribes = [
|
||||
// tankopediaEphemeralStore.subscribe(
|
||||
// (state) => state.highlightArmor?.name,
|
||||
// handleHighlightArmor,
|
||||
// ),
|
||||
// ];
|
||||
// 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(data: ModelTransformEventData) {
|
||||
// clip.normal
|
||||
// .set(0, 0, -1)
|
||||
// .applyAxisAngle(I_HAT, data.pitch)
|
||||
// .applyAxisAngle(J_HAT, data.yaw ?? 0);
|
||||
// clip.constant = 0;
|
||||
// point
|
||||
// .copy(gunOrigin)
|
||||
// .applyAxisAngle(J_HAT, data.yaw ?? 0)
|
||||
// .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());
|
||||
// };
|
||||
// }, [thickness]);
|
||||
// return (
|
||||
// <>
|
||||
// {jsxTree(node, {
|
||||
// mesh: {
|
||||
// material: surfaceMaterial,
|
||||
// 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 } = tankopediaEphemeralStore.getState();
|
||||
// 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);
|
||||
// mutateTankopediaEphemeralStore((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>
|
||||
// )}
|
||||
// </>
|
||||
// );
|
||||
}
|
||||
+20
-28
@@ -4,27 +4,21 @@ import { Button, Checkbox, Flex, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
|
||||
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
|
||||
|
||||
interface ThicknessesProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
}
|
||||
|
||||
export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
const showExternalModules = TankopediaPersistent.use(
|
||||
(state) => state.showExternalModules,
|
||||
);
|
||||
const showSpacedArmor = TankopediaPersistent.use(
|
||||
(state) => state.showSpacedArmor,
|
||||
);
|
||||
const showPrimaryArmor = TankopediaPersistent.use(
|
||||
(state) => state.showPrimaryArmor,
|
||||
export function Thicknesses() {
|
||||
const showModules = TankopediaPersistent.use((state) => state.showModules);
|
||||
const showArmorScreen = TankopediaPersistent.use(
|
||||
(state) => state.showArmorScreen,
|
||||
);
|
||||
const showArmor = TankopediaPersistent.use((state) => state.showArmor);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const { strings } = useLocale();
|
||||
const thicknessRange = TankopediaEphemeral.use(
|
||||
(state) => state.thicknessRange,
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -41,10 +35,10 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
style={{ opacity: showArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||
draft.showArmor = !draft.showArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
@@ -54,7 +48,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, [
|
||||
(thicknessRange.value * 1.5).toFixed(0),
|
||||
(thicknessRange * 1.5).toFixed(0),
|
||||
])}
|
||||
</Text>
|
||||
<Text size="1">
|
||||
@@ -75,17 +69,17 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
color: 'black',
|
||||
}}
|
||||
>
|
||||
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
{showArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
style={{ opacity: showArmorScreen ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||
draft.showArmorScreen = !draft.showArmorScreen;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
@@ -94,9 +88,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
>
|
||||
<Flex direction="column" align="end" justify="between">
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, [
|
||||
thicknessRange.value.toFixed(0),
|
||||
])}
|
||||
{literals(strings.common.units.mm, [thicknessRange.toFixed(0)])}
|
||||
</Text>
|
||||
<Text size="1">
|
||||
{strings.website.tools.tanks.sandbox.static.spaced}
|
||||
@@ -115,16 +107,16 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
|
||||
}}
|
||||
>
|
||||
{showSpacedArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
{showArmorScreen ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
|
||||
style={{ opacity: showModules ? 1 : 0.5, cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showExternalModules = !draft.showExternalModules;
|
||||
draft.showModules = !draft.showModules;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
@@ -148,7 +140,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
color: 'black',
|
||||
}}
|
||||
>
|
||||
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
{showModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -179,7 +171,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editStatic = false;
|
||||
draft.model = tankopediaEphemeralStore.getInitialState().model;
|
||||
draft.armor = tankopediaEphemeralStore.getInitialState().armor;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,15 +12,13 @@ import {
|
||||
} from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
|
||||
import { Thicknesses } from './components/Thicknesses';
|
||||
|
||||
interface OptionsProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
canvas: RefObject<HTMLCanvasElement>;
|
||||
}
|
||||
|
||||
export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
export function Options({ canvas }: OptionsProps) {
|
||||
const hasCustomShell = TankopediaEphemeral.use(
|
||||
(state) => state.customShell !== undefined,
|
||||
);
|
||||
@@ -75,9 +73,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<Thicknesses thicknessRange={thicknessRange} />
|
||||
)}
|
||||
{display === TankopediaDisplay.StaticArmor && <Thicknesses />}
|
||||
|
||||
{/* {display === TankopediaDisplay.DynamicArmor && (
|
||||
<Flex
|
||||
|
||||
+3
-9
@@ -8,10 +8,7 @@ import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/co
|
||||
import { Armor } from '../../../../Armor';
|
||||
import { ArmorPlateDisplay } from '../../../../Armor/components/ArmorPlateDisplay';
|
||||
import { ShotDisplay } from '../../../../Armor/components/ShotDisplay';
|
||||
import {
|
||||
StaticArmor,
|
||||
type ThicknessRange,
|
||||
} from '../../../../Armor/components/StaticArmor';
|
||||
import { StaticArmor } from '../../../../Armor/components/StaticArmor';
|
||||
import { AutoClear } from './components/AutoClear';
|
||||
import { Controls } from './components/Control';
|
||||
import { Lighting } from './components/Lighting';
|
||||
@@ -20,12 +17,11 @@ import { SceneProps } from './components/SceneProps';
|
||||
import { TankModel } from './components/TankModel';
|
||||
|
||||
interface TankSandboxProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
naked?: boolean;
|
||||
}
|
||||
|
||||
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
({ thicknessRange, naked }, ref) => {
|
||||
({ naked }, ref) => {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
// const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
@@ -193,9 +189,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
|
||||
<Suspense fallback={<ModelLoader />}>
|
||||
{display === TankopediaDisplay.DynamicArmor && <Armor />}
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<StaticArmor thicknessRange={thicknessRange} />
|
||||
)}
|
||||
{display === TankopediaDisplay.StaticArmor && <StaticArmor />}
|
||||
<Lighting />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
|
||||
@@ -42,7 +42,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
// return { value } satisfies ThicknessRange;
|
||||
// }, [protagonist]);
|
||||
const thicknessRange = { value: 300 };
|
||||
// const duelStore = Duel.useStore();
|
||||
// const mutateDuel = Duel.useMutation();
|
||||
// const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
@@ -140,11 +139,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
{skeleton && <TankSandboxLoader id={protagonist.id} />}
|
||||
|
||||
<Suspense fallback={<TankSandboxLoader id={protagonist.id} />}>
|
||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||
<TankSandbox ref={canvas} />
|
||||
</Suspense>
|
||||
</Box>
|
||||
|
||||
<Options canvas={canvas} thicknessRange={thicknessRange} />
|
||||
<Options canvas={canvas} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -20,7 +20,7 @@ type PageProps = MaybeSkeletonComponentProps &
|
||||
export function Page({ tank, armor, skeleton, locale }: PageProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<TankopediaEphemeral.Provider data={armor}>
|
||||
<TankopediaEphemeral.Provider data={{ armor, thicknessRange: 193 }}>
|
||||
<App.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Duel.Provider
|
||||
|
||||
@@ -96,6 +96,7 @@ export const SHOOTING_RANGE_ZOOM_COEFFICIENTS: Record<
|
||||
};
|
||||
|
||||
interface TankopediaEphemeral {
|
||||
thicknessRange: number;
|
||||
shot?: Shot;
|
||||
shootingRangeZoom: ShootingRangeZoom;
|
||||
skills: Record<string, number>;
|
||||
@@ -124,17 +125,20 @@ interface TankopediaEphemeral {
|
||||
|
||||
const skillDefinitions = await awaitableSkillDefinitions;
|
||||
|
||||
export const TankopediaEphemeral = createContextualStore((armor: TankArmor) => {
|
||||
return create<TankopediaEphemeral>()(
|
||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
||||
shootingRangeZoom: ShootingRangeZoom.Arcade,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
editStatic: false,
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
armor,
|
||||
controlsEnabled: true,
|
||||
xpMultiplier: 1,
|
||||
display: TankopediaDisplay.StaticArmor,
|
||||
})),
|
||||
);
|
||||
});
|
||||
export const TankopediaEphemeral = createContextualStore(
|
||||
({ armor, thicknessRange }: { armor: TankArmor; thicknessRange: number }) => {
|
||||
return create<TankopediaEphemeral>()(
|
||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
||||
thicknessRange,
|
||||
shootingRangeZoom: ShootingRangeZoom.Arcade,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
editStatic: false,
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
armor,
|
||||
controlsEnabled: true,
|
||||
xpMultiplier: 1,
|
||||
display: TankopediaDisplay.StaticArmor,
|
||||
})),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -14,9 +14,9 @@ export interface TankopediaPersistentStore {
|
||||
advancedHighlighting: boolean;
|
||||
showGrid: boolean;
|
||||
greenPenetration: boolean;
|
||||
showSpacedArmor: boolean;
|
||||
showExternalModules: boolean;
|
||||
showPrimaryArmor: boolean;
|
||||
showArmorScreen: boolean;
|
||||
showModules: boolean;
|
||||
showArmor: boolean;
|
||||
recentlyViewed: number[];
|
||||
hideTankModelUnderArmor: boolean;
|
||||
|
||||
@@ -36,9 +36,9 @@ export const TankopediaPersistent = createContextualStore(() =>
|
||||
advancedHighlighting: false,
|
||||
showGrid: true,
|
||||
greenPenetration: false,
|
||||
showSpacedArmor: true,
|
||||
showExternalModules: true,
|
||||
showPrimaryArmor: true,
|
||||
showArmorScreen: true,
|
||||
showModules: true,
|
||||
showArmor: true,
|
||||
recentlyViewed: [],
|
||||
sort: {
|
||||
by: 'meta.none',
|
||||
|
||||
Reference in new issue
Block a user