diff --git a/.vscode/settings.json b/.vscode/settings.json index 923aa3721..110cdf13e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -89,6 +89,7 @@ "lossrate", "mantlets", "metallicness", + "metalness", "minitelrose", "Minotauro", "MIPMAPCOUNT", diff --git a/src/app/tools/tankopedia/[id]/components/Control.tsx b/src/app/tools/tankopedia/[id]/components/Control.tsx index 316d4529d..10ba7cd5a 100644 --- a/src/app/tools/tankopedia/[id]/components/Control.tsx +++ b/src/app/tools/tankopedia/[id]/components/Control.tsx @@ -9,7 +9,7 @@ import { modelDefinitions } from '../../../../../core/blitzkit/modelDefinitions' import { Pose, poseEvent } from '../../../../../core/blitzkit/pose'; import { useAwait } from '../../../../../hooks/useAwait'; import { useDuel } from '../../../../../stores/duel'; -import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; +import { useTankopediaTemporary } from '../../../../../stores/tankopedia'; const poseDistances: Record = { [Pose.HullDown]: 15, @@ -62,8 +62,8 @@ export function Controls() { }, [camera]); useEffect(() => { - const unsubscribeTankopedia = useTankopediaPersistent.subscribe( - (state) => state.model.visual.controlsEnabled, + const unsubscribeTankopedia = useTankopediaTemporary.subscribe( + (state) => state.controlsEnabled, (enabled) => { if (orbitControls.current) orbitControls.current.enabled = enabled; }, @@ -182,7 +182,7 @@ export function Controls() { maxDistance={20} minDistance={5} ref={orbitControls} - enabled={useTankopediaPersistent.getState().model.visual.controlsEnabled} + enabled={useTankopediaTemporary.getState().controlsEnabled} rotateSpeed={0.25} enableDamping={false} autoRotate={autoRotate} diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx index 34928bb07..17e8c53da 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -1,6 +1,13 @@ import { ThreeEvent, useThree } from '@react-three/fiber'; import { memo, useEffect, useRef } from 'react'; -import { Euler, Group, Vector2, Vector3 } from 'three'; +import { + Euler, + Group, + Mesh, + MeshStandardMaterial, + Vector2, + Vector3, +} from 'three'; import { degToRad } from 'three/src/math/MathUtils'; import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis'; import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits'; @@ -15,9 +22,7 @@ import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngle import { useAwait } from '../../../../../../../hooks/useAwait'; import { useModel } from '../../../../../../../hooks/useModel'; import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; -import mutateTankopediaPersistent, { - mutateTankopediaTemporary, -} from '../../../../../../../stores/tankopedia'; +import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia'; export const TankModel = memo(() => { const awaitedModelDefinitions = useAwait(modelDefinitions); @@ -117,11 +122,51 @@ export const TankModel = memo(() => { const isWheel = node.name.startsWith('chassis_wheel_'); const isTrack = node.name.startsWith('chassis_track_'); const isVisible = isHull || isWheel || isTrack; + const position = new Vector2(); + const delta = new Vector2(); if (!isVisible) return null; + function translateTexture(offset: number) { + const mesh = node.children[0] as Mesh; + const material = mesh?.material as MeshStandardMaterial; + + if (!material) return; + + if (material.map) material.map.offset.y += offset; + if (material.aoMap) material.aoMap.offset.y += offset; + if (material.normalMap) material.normalMap.offset.y += offset; + if (material.roughnessMap) material.roughnessMap.offset.y += offset; + if (material.metalnessMap) material.metalnessMap.offset.y += offset; + } + function onPointerDown(event: ThreeEvent) { + if (!isTrack) return; + event.stopPropagation(); + + mutateTankopediaTemporary((draft) => { + draft.controlsEnabled = false; + }); + + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + function handlePointerMove(event: PointerEvent) { + delta.set(event.clientX, event.clientY).sub(position); + position.set(event.clientX, event.clientY); + const deltaX = delta.x / window.innerWidth; + const deltaY = delta.y / window.innerHeight; + + translateTexture(deltaX + deltaY); + } + function handlePointerUp() { + mutateTankopediaTemporary((draft) => { + draft.controlsEnabled = true; + }); + + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); } return jsxTree( @@ -158,8 +203,8 @@ export const TankModel = memo(() => { yaw = protagonist.yaw; pitch = protagonist.pitch; - mutateTankopediaPersistent((draft) => { - draft.model.visual.controlsEnabled = false; + mutateTankopediaTemporary((draft) => { + draft.controlsEnabled = false; }); mutateTankopediaTemporary((draft) => { draft.shot = undefined; @@ -194,8 +239,8 @@ export const TankModel = memo(() => { draft.protagonist!.pitch = normalizeAngleRad(pitch); draft.protagonist!.yaw = normalizeAngleRad(yaw); }); - mutateTankopediaPersistent((draft) => { - draft.model.visual.controlsEnabled = true; + mutateTankopediaTemporary((draft) => { + draft.controlsEnabled = true; }); window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); @@ -234,8 +279,8 @@ export const TankModel = memo(() => { function onPointerDown(event: ThreeEvent) { event.stopPropagation(); - mutateTankopediaPersistent((draft) => { - draft.model.visual.controlsEnabled = false; + mutateTankopediaTemporary((draft) => { + draft.controlsEnabled = false; }); mutateTankopediaTemporary((draft) => { draft.shot = undefined; @@ -269,8 +314,8 @@ export const TankModel = memo(() => { modelTransformEvent.emit({ pitch, yaw }); } function handlePointerUp() { - mutateTankopediaPersistent((draft) => { - draft.model.visual.controlsEnabled = true; + mutateTankopediaTemporary((draft) => { + draft.controlsEnabled = true; }); mutateDuel((draft) => { draft.protagonist!.pitch = normalizeAngleRad(pitch); diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 4cdd1da0e..88d746e77 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -20,7 +20,6 @@ export interface TankopediaPersistent { wireframe: boolean; opaque: boolean; environment: (typeof ENVIRONMENTS)[number]; - controlsEnabled: boolean; showGrid: boolean; greenPenetration: boolean; showEnvironment: boolean; @@ -45,6 +44,7 @@ export interface TankopediaPersistent { interface TankopediaTemporary { shot?: Shot; skills: Record; + controlsEnabled: boolean; } export interface ShotLayerBase { @@ -199,6 +199,7 @@ export const useTankopediaPersistent = create()( export const useTankopediaTemporary = create()( subscribeWithSelector(() => ({ skills: {}, + controlsEnabled: true, })), );