diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 2ced1ebfc..6b4d23528 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -900,7 +900,7 @@ "element": { "primary": "Primary", "spaced": "Spaced", - "external": "External", + "modules": "Modules", "gap": "Gap", "gun": "Gun", "track": "Tracks" @@ -922,7 +922,7 @@ "name": "Static armor", "primary": "Primary", "spaced": "Spaced", - "external": "External", + "modules": "Modules", "edit": { "label": "Edit", "reset": "Reset" diff --git a/packages/website/src/components/Armor/components/StaticArmor.tsx b/packages/website/src/components/Armor/components/StaticArmor.tsx index 9ed87f8ce..3191e7023 100644 --- a/packages/website/src/components/Armor/components/StaticArmor.tsx +++ b/packages/website/src/components/Armor/components/StaticArmor.tsx @@ -1,10 +1,4 @@ -import { asset } from '@blitzkit/core'; -import { useGLTF } from '@react-three/drei'; import { memo } from 'react'; -import { Color, Material, MeshBasicMaterial } from 'three'; -import { jsxTree } from '../../../core/blitzkit/jsxTree'; -import { Duel } from '../../../stores/duel'; -import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { StaticArmorSceneComponents } from './StaticArmorSceneComponents'; export interface ThicknessRange { @@ -55,11 +49,16 @@ export const StaticArmor = memo(() => { // useTankTransform(track, turret, turretContainer, gunContainer); - return ( - + + + + + + + ); }); diff --git a/packages/website/src/components/Armor/components/StaticArmorSceneComponents.tsx b/packages/website/src/components/Armor/components/StaticArmorSceneComponents.tsx index 3fab91bb0..801880fed 100644 --- a/packages/website/src/components/Armor/components/StaticArmorSceneComponents.tsx +++ b/packages/website/src/components/Armor/components/StaticArmorSceneComponents.tsx @@ -1,39 +1,45 @@ import { asset } from '@blitzkit/core'; import { useGLTF } from '@react-three/drei'; -import { Color, Material, MeshStandardMaterial } from 'three'; +import { + Color, + LineBasicMaterial, + 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 = { +interface StaticArmorSceneComponentsProps { group: string; -}; +} -const unselectedColor = new Color(0x404040); -const externalModuleColor = new Color(192 / 255, 192 / 255, 192 / 255); +const moduleColor = new Color(192 / 255, 192 / 255, 192 / 255); +const groupNameRegex = /([a-z]+)(_\d+)?/; export function StaticArmorSceneComponents({ group, }: StaticArmorSceneComponentsProps) { + const tank = Duel.use((state) => state.protagonist.tank); 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 groupName = group.match(groupNameRegex)![1]; + const groupArmor = armor.groups[groupName]; const thicknessRange = TankopediaEphemeral.use( (state) => state.thicknessRange, ); return jsxTree(gltf.scene, { - mesh(mesh) { + mesh(mesh, props) { if (!(mesh.material instanceof Material)) { - return { visible: false }; + return null; } const armorName = mesh.material.name; @@ -44,216 +50,67 @@ export function StaticArmorSceneComponents({ switch (armorPlate.type) { case 'Armor': + if (!showArmor) return null; + 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': + if (!showArmorScreen) return null; + 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; + if (!showModules) return null; + + material.color = moduleColor; + material.opacity = 1 / 2; + break; } + + const outlineMaterial = new LineBasicMaterial({ + color: material.color.clone(), + }); + + switch (armorPlate.type) { + case 'Armor': + outlineMaterial.color.multiplyScalar(0.5); + break; + + case 'ArmorScreen': + outlineMaterial.color.multiplyScalar(4); + break; + + default: + outlineMaterial.color.multiplyScalar(0.5); + break; + } + material.opacity = clamp(material.opacity, 0, 1); material.transparent = material.opacity < 1; - return { material }; + return ( + <> + + + + + + + ); }, }); - - // 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 && ( - // - // - // - // )} - // - // ); } diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/Thicknesses.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/Thicknesses.tsx index 08d47a0ea..398a7996c 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/Thicknesses.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/Options/components/Thicknesses.tsx @@ -125,7 +125,7 @@ export function Thicknesses() { > - {strings.website.tools.tanks.sandbox.static.external} + {strings.website.tools.tanks.sandbox.static.modules} diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx index 1b4bdc593..6d5004c3d 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx @@ -1,13 +1,16 @@ import { OrbitControls } from '@react-three/drei'; import { useThree } from '@react-three/fiber'; -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { PerspectiveCamera } from 'three'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; import { degToRad } from 'three/src/math/MathUtils.js'; import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; import { Pose } from '../../../../../../core/blitzkit/pose'; import { Duel } from '../../../../../../stores/duel'; -import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; +import { + SHOOTING_RANGE_ZOOM_COEFFICIENTS, + TankopediaEphemeral, +} from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants'; const poseDistances: Record = { @@ -80,11 +83,12 @@ export function Controls({ naked }: ControlsProps) { // protagonistHullOrigin.y + // protagonistTurretOrigin.y + // protagonistGunOrigin.y; - // const inspectModeInitialPosition = [ - // -8, - // gunHeight + (naked ? 10 : 4), - // -13, - // ] as const; + const gunHeight = 2; + const inspectModeInitialPosition = [ + -8, + gunHeight + (naked ? 10 : 4), + -13, + ] as const; // const protagonistGunOriginOnlyY = new Vector3( // 0, // protagonistTurretModelDefinition.gun_origin.y, @@ -206,131 +210,131 @@ export function Controls({ naked }: ControlsProps) { // }; // }, [camera, protagonistTank.id, antagonistTank.id]); - // useEffect(() => { - // let isInitialIteration = true; + useEffect(() => { + let isInitialIteration = true; - // function handleDisturbance() { - // setAutoRotate(false); - // } + function handleDisturbance() { + setAutoRotate(false); + } - // function handleKeyDown(event: KeyboardEvent) { - // if (display !== TankopediaDisplay.ShootingRange || event.repeat) return; + // function handleKeyDown(event: KeyboardEvent) { + // if (display !== TankopediaDisplay.ShootingRange || event.repeat) return; - // if (event.key === 'Shift') { - // mutateTankopediaEphemeral((draft) => { - // draft.shootingRangeZoom = - // draft.shootingRangeZoom === ShootingRangeZoom.Arcade - // ? ShootingRangeZoom.Zoom0 - // : ShootingRangeZoom.Arcade; - // }); - // } - // } + // if (event.key === 'Shift') { + // mutateTankopediaEphemeral((draft) => { + // draft.shootingRangeZoom = + // draft.shootingRangeZoom === ShootingRangeZoom.Arcade + // ? ShootingRangeZoom.Zoom0 + // : ShootingRangeZoom.Arcade; + // }); + // } + // } - // function handleWheel(event: WheelEvent) { - // if (display !== TankopediaDisplay.ShootingRange) return; + // function handleWheel(event: WheelEvent) { + // if (display !== TankopediaDisplay.ShootingRange) return; - // event.preventDefault(); + // event.preventDefault(); - // mutateTankopediaEphemeral((draft) => { - // draft.shootingRangeZoom = clamp( - // draft.shootingRangeZoom - Math.sign(event.deltaY), - // ShootingRangeZoom.Arcade, - // ShootingRangeZoom.Zoom2, - // ); - // }); - // } + // mutateTankopediaEphemeral((draft) => { + // draft.shootingRangeZoom = clamp( + // draft.shootingRangeZoom - Math.sign(event.deltaY), + // ShootingRangeZoom.Arcade, + // ShootingRangeZoom.Zoom2, + // ); + // }); + // } - // function handleScroll(event: Event) { - // if (display !== TankopediaDisplay.ShootingRange) return; + // function handleScroll(event: Event) { + // if (display !== TankopediaDisplay.ShootingRange) return; - // event.preventDefault(); - // } + // event.preventDefault(); + // } - // poseEvent.on(handleDisturbance); - // canvas.addEventListener('pointerdown', handleDisturbance); - // window.addEventListener('keydown', handleKeyDown); - // canvas.addEventListener('wheel', handleWheel); - // document.body.addEventListener('scroll', handleScroll); + // poseEvent.on(handleDisturbance); + canvas.addEventListener('pointerdown', handleDisturbance); + // window.addEventListener('keydown', handleKeyDown); + // canvas.addEventListener('wheel', handleWheel); + // document.body.addEventListener('scroll', handleScroll); - // function updateCamera() { - // if (!orbitControls.current) return; + function updateCamera() { + if (!orbitControls.current) return; - // orbitControls.current.rotateSpeed = 0.25; + orbitControls.current.rotateSpeed = 0.25; - // if (display === TankopediaDisplay.ShootingRange) { - // const { shootingRangeZoom } = tankopediaEphemeralStore.getState(); + if (display === TankopediaDisplay.ShootingRange) { + const { shootingRangeZoom } = tankopediaEphemeralStore.getState(); - // orbitControls.current.enablePan = false; - // orbitControls.current.enableZoom = false; - // orbitControls.current.rotateSpeed *= - // SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom]; - // camera.fov = - // ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom]; + orbitControls.current.enablePan = false; + orbitControls.current.enableZoom = false; + orbitControls.current.rotateSpeed *= + SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom]; + camera.fov = + ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom]; - // if (shootingRangeZoom === ShootingRangeZoom.Arcade) { - // orbitControls.current.target.set(0, gunHeight + 3, 0); + // if (shootingRangeZoom === ShootingRangeZoom.Arcade) { + // orbitControls.current.target.set(0, gunHeight + 3, 0); - // if (isInitialIteration) { - // isInitialIteration = false; - // camera.position.set( - // 0, - // gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE), - // ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE), - // ); - // } else { - // camera.position - // .copy(aimTarget) - // .sub(orbitControls.current.target) - // .normalize() - // .multiplyScalar(-ARCADE_MODE_DISTANCE) - // .add(orbitControls.current.target); - // } - // } else { - // orbitControls.current.target.copy(shellOrigin); + // if (isInitialIteration) { + // isInitialIteration = false; + // camera.position.set( + // 0, + // gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE), + // ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE), + // ); + // } else { + // camera.position + // .copy(aimTarget) + // .sub(orbitControls.current.target) + // .normalize() + // .multiplyScalar(-ARCADE_MODE_DISTANCE) + // .add(orbitControls.current.target); + // } + // } else { + // orbitControls.current.target.copy(shellOrigin); - // camera.position - // .copy(aimTarget) - // .sub(shellOrigin) - // .normalize() - // .multiplyScalar(-Number.EPSILON) - // .add(shellOrigin); - // } - // } else { - // camera.position.set(...inspectModeInitialPosition); - // orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0); - // orbitControls.current.enablePan = true; - // orbitControls.current.enableZoom = true; - // camera.fov = naked ? 20 : 25; - // } + // camera.position + // .copy(aimTarget) + // .sub(shellOrigin) + // .normalize() + // .multiplyScalar(-Number.EPSILON) + // .add(shellOrigin); + // } + } else { + camera.position.set(...inspectModeInitialPosition); + orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0); + orbitControls.current.enablePan = true; + orbitControls.current.enableZoom = true; + camera.fov = naked ? 20 : 25; + } - // camera.updateProjectionMatrix(); - // } + camera.updateProjectionMatrix(); + } - // updateCamera(); + updateCamera(); - // const unsubscribeDisplay = tankopediaEphemeralStore.subscribe( - // (state) => state.display, - // () => { - // handleDisturbance(); - // updateCamera(); - // }, - // ); + const unsubscribeDisplay = tankopediaEphemeralStore.subscribe( + (state) => state.display, + () => { + handleDisturbance(); + updateCamera(); + }, + ); - // const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe( - // (state) => state.shootingRangeZoom, - // updateCamera, - // ); + const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe( + (state) => state.shootingRangeZoom, + updateCamera, + ); - // return () => { - // canvas.removeEventListener('pointerdown', handleDisturbance); - // poseEvent.off(handleDisturbance); - // unsubscribeDisplay(); - // unsubscribeShootingRangeZoom(); - // window.removeEventListener('keydown', handleKeyDown); - // canvas.removeEventListener('wheel', handleWheel); - // document.body.removeEventListener('scroll', handleScroll); - // }; - // }, [display === TankopediaDisplay.ShootingRange]); + return () => { + canvas.removeEventListener('pointerdown', handleDisturbance); + // poseEvent.off(handleDisturbance); + unsubscribeDisplay(); + unsubscribeShootingRangeZoom(); + // window.removeEventListener('keydown', handleKeyDown); + // canvas.removeEventListener('wheel', handleWheel); + // document.body.removeEventListener('scroll', handleScroll); + }; + }, [display === TankopediaDisplay.ShootingRange]); return ( MeshProps); - group?: GroupProps | ((object3d: Object3D) => GroupProps); + mesh?: (mesh: Mesh, props: MeshProps) => ReactNode; + group?: (object3d: Object3D, props: Object3DProps) => ReactNode; }, key?: string, -) { +): ReactNode { if (node instanceof Mesh) { - return ( - - ); + const props = { + key, + geometry: node.geometry, + material: node.material, + position: node.position, + rotation: node.rotation, + scale: node.scale, + }; + + return mergers?.mesh?.(node, props) ?? ; } else if (node instanceof Object3D) { - return ( - - {node.children.map((child) => jsxTree(child, mergers, child.uuid))} - - ); + const props = { + key, + position: node.position, + rotation: node.rotation, + scale: node.scale, + children: node.children.map((child) => + jsxTree(child, mergers, child.uuid), + ), + }; + + return mergers?.group?.(node, props) ?? ; } return null;