import { create } from 'zustand';
import io from 'socket.io-client';

const useSocketStore = create((set, get) => ({
    socket: null,
    rooms: [],

    initSocket: (url) => {
        const newSocket = io(url);
        newSocket.on('ROOMS_UPDATE', (rooms) => {
            console.log('ROOMS_UPDATE received', rooms);
            set({ rooms });
        })
        set({ socket: newSocket});
    },

    closeSocket: () => {
        const { socket } = get();
        if (socket) {
            socket.close();
            set({ socket: null });
        }
    },

    addRoom: (room, callback) => {
        const { socket } = get();
        if (socket) {
            socket.emit('ADD_ROOM', room, (newRoom) => {
                if (callback) callback(newRoom);
            });
        }
    },

    joinRoom: (roomId) => {
        const { socket } = get();
        if (socket) {
            socket.emit('join room', roomId);
        }
    },

    emitOffer: (offer, roomId) => {
        const { socket } = get();
        if (socket) {
            socket.emit('offer', { type: offer.type, sdp: offer.sdp, roomId });
        }
    },

    emitAnswer: (answer, roomId) => {
        const { socket } = get();
        if (socket) {
            socket.emit('answer', { type: answer.type, sdp: answer.sdp, roomId });
        }
    },

    emitCandidate: (candidate, roomId) => {
        const { socket } = get();
        if (socket) {
            socket.emit('candidate', { candidate, roomId });
        }
    },

    emitDamage: (damage, roomId) => {
        const { socket } = get();
        if (socket) {
            socket.emit('damage', { roomId: roomId.current, amount: damage });
        }
    },
    
}));

export default useSocketStore;

import { create } from 'zustand';

const useGameStore = create((set, get ) => ({
  opponentHealth: 100,
  playerHealth: 100,
  playerName: '',
  opponentName: '',
  gameStatus: 'playing', // 'idle', 'ready' ,'playing', 'finished', 'replaying'
  opponentReady: false,
  setOpponentReady: (state) => set({opponentReady: state}),

  setPlayerName: (name) => set({ playerName: name }),
  setOpponentName: (name) => set({ opponentName: name }),

  startGame: () => set({
    opponentHealth: 100,
    playerHealth: 100,
    gameStatus: 'playing'
  }),

  endGame: () => set({ gameStatus: 'finished' }),

  resetGame: () => set({
    opponentHealth: 100,
    playerHealth: 100,
    gameStatus: 'idle'
  }),

  decreaseOpponentHealth: (amount) => set((state) => ({ 
    opponentHealth: Math.max(0, state.opponentHealth - amount)
  })),

  decreasePlayerHealth: (amount) => set((state) => ({ 
    playerHealth: Math.max(0, state.playerHealth - amount)
  })),

  getWinner: () => {
    const { playerHealth, opponentHealth, playerName, opponentName } = get();
    if (playerHealth <= 0) return opponentName;
    if (opponentHealth <= 0) return playerName;
  }
}));

export default useGameStore;
'use client';

import useGameStore from '@/store/gameStore';
import useGameLogic from '@/hooks/useGameLogic';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

export default function ReadyCanvas({ onReady, landmarks, canvasSize, opponentReady, sendPlayerReadyState }) {
  const canvasRef = useRef(null);
  const timerRef = useRef(null);
  const [similarityResult, setSimilarityResult] = useState(null);
  const [remainingTime, setRemainingTime] = useState(5);
  const keypoints = useMemo(() => ['nose', 'leftShoulder', 'rightShoulder'], []);
  const similarityThreshold = 0.10;

  const targetPose = useRef({
    nose: { x: 0.5, y: 0.45 },
    leftShoulder: { x: 0.7, y: 0.65 },
    rightShoulder: { x: 0.3, y: 0.65 }
  });

  const extractRequiredLandmarks = useCallback((landmarks) => {
    const requiredLandmarks = {};
    for (const keypoint of keypoints) {
      if (landmarks && landmarks[keypoint]) {
        requiredLandmarks[keypoint] = landmarks[keypoint];
      }
    }
    return requiredLandmarks;
  }, [keypoints]);

  const calculatePoseSimilarity = useCallback((detectedPose, targetPose) => {
    let totalDistance = 0;
    let count = 0;

    for (const keypoint of keypoints) {
      if (detectedPose[keypoint] && targetPose[keypoint] && detectedPose[keypoint] !== null) {
        const dx = detectedPose[keypoint].x - targetPose[keypoint].x;
        const dy = detectedPose[keypoint].y - targetPose[keypoint].y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        totalDistance += distance;
        count++;
      }
    }

    if (count === 0) return 0;

    const avgDistance = totalDistance / count;
    const maxAllowedDistance = 0.15;
    const similarity = Math.max(0, 1 - avgDistance / maxAllowedDistance);

    return similarity;
  }, [keypoints]);

  const startTimer = useCallback(() => {
    if (timerRef.current) return;
    
    setRemainingTime(5); // 타이머 초기화
    timerRef.current = setInterval(() => {
      setRemainingTime((prevTime) => {
        if (prevTime <= 1) {
          clearInterval(timerRef.current);
          timerRef.current = null;
          setIsReadyPose(true);
          onReady();
          return 0;
        }
        return prevTime - 1;
      });
    }, 1000);
  }, [onReady]);

  const checkReadyPose = useCallback(() => {
    if (landmarks && canvasRef.current) {
      const requiredLandmarks = extractRequiredLandmarks(landmarks);
      const similarity = calculatePoseSimilarity(requiredLandmarks, targetPose.current);
      setSimilarityResult(similarity);
  
      if (similarity >= similarityThreshold) {
        console.log(opponentReady, ":", similarity, '>', similarityThreshold);
        sendPlayerReadyState(true);
        if (opponentReady && !timerRef.current) {
          startTimer();
        }
      } else {
        sendPlayerReadyState(false);
        if (timerRef.current) {
          clearInterval(timerRef.current);
          timerRef.current = null;
          setRemainingTime(5);
        }
      }
    }
  }, [landmarks, calculatePoseSimilarity, similarityThreshold, extractRequiredLandmarks, sendPlayerReadyState, opponentReady, startTimer]);
  useEffect(() => {
    const canvas = canvasRef.current;
    canvas.width = canvasSize.width;
    canvas.height = canvasSize.height;
  }, [canvasSize]);

  useEffect(() => {
    checkReadyPose();
  }, [landmarks, checkReadyPose]);

  const [headerText, setHeaderText] = useState("점선에 어깨와 머리를 맞춰주세요");
  const [headerColor, setHeaderColor] = useState('white');

  useEffect(() => {
    setHeaderText(timerRef.current ? "거리를 유지해주세요" : "점선에 어깨와 머리를 맞춰주세요");
  }, []); 
  
  useEffect(() => {
    const checkTimer = () => {
      setHeaderText(timerRef.current ? "거리를 유지해주세요" : "점선에 어깨와 머리를 맞춰주세요");
    };
    checkTimer();
    const intervalId = setInterval(checkTimer, 100);
  
    return () => clearInterval(intervalId);
  }, []);

  useEffect(() => {
    if (similarityResult >= 0.7) setHeaderColor('green');
    else if (similarityResult >= 0.5) setHeaderColor('yellow');
    else setHeaderColor('red');
  }, [similarityResult]);

  return (
    <div className="relative w-full h-full">
      <div className="ready-header" style={{ '--header-color': headerColor }}>
        {headerText}
      </div>
      <canvas ref={canvasRef} className="absolute inset-0" />
      <div className="similarity-info">
        Similarity: {similarityResult ? similarityResult.toFixed(2) : 'N/A'}
      </div>
      {(timerRef.current && remainingTime <= 4) && (
        <div className="countdown-text">
          {remainingTime === 1 ? 'GO!' : remainingTime - 1}
        </div>
      )}
    </div>
  );
}
'use client';

import { forwardRef, useRef, useEffect, useState, useCallback, useImperativeHandle } from 'react'
import { useFrame} from '@react-three/fiber'
import { useGLTF } from '@react-three/drei'
import * as THREE from 'three'
import useGameStore from '../../store/gameStore';
import useSocketStore from '@/store/socketStore';

// Opponent 전용 Head 컴포넌트
const OpponentHead = forwardRef(({ position, rotation, scale, name, hit }, ref) => {
    const localRef = useRef()
    const { scene, materials } = useGLTF('/models/opponent-head.glb')
    const opacity = 0.9
    useImperativeHandle(
      ref,
      () => ({
        getWorldPosition: (target) => {
          if (localRef.current) {
            return localRef.current.getWorldPosition(target || new THREE.Vector3())
          }
          return new THREE.Vector3()
        },
        position: localRef.current?.position,
        rotation: localRef.current?.rotation,
      }),
      [localRef], 
    )
  
    useEffect(() => {
      if (localRef.current) {
        localRef.current.position.set((position.x - 0.5) * 5, -(position.y - 0.5) * 5, -(position.z+0.01) * 15)
        if (rotation) {
          localRef.current.rotation.set(rotation[0], rotation[1], rotation[2]);
        }
      }
    }, [position, rotation])

    useEffect(() => {
      Object.values(materials).forEach((material) => {
        material.transparent = true
        material.opacity = opacity
        material.color.setRGB(hit ? 1 : 1, hit ? 0 : 1, hit ? 0 : 1) // Set color to red when hit
      })
    }, [materials, hit])
    return <primitive ref={localRef} object={scene} scale={scale} name={name} />
  })
  
OpponentHead.displayName = "OpponentHead";

// Opponent 전용 Hand 컴포넌트
const OpponentHand = forwardRef(({ position, rotation, scale, name }, ref) => {
    const localRef = useRef()
    const { scene } = useGLTF(name === 'opponentLeftHand' ? '/models/left-hand.glb' : '/models/right-hand.glb')

    useEffect(() => {
      scene.traverse((child) => {
        if (child.isMesh) {
          child.material = child.material.clone()
          child.material.color.set('red')
        }
      });
    }, [scene]);

    useFrame(() => {
      if (localRef.current && position) {
        if (rotation) {
          localRef.current.rotation.set(rotation[0],rotation[1],rotation[2])
        }
        localRef.current.position.set((position[0]-0.5)*4, -(position[1]-0.5)*4, -position[2]*30)
      }
    })
  
    return <primitive ref={localRef} object={scene.clone()} scale={scale} name={name} />
  })

OpponentHand.displayName = "OpponentHand";

export function Opponent({ position, landmarks, opponentData }) {
  const groupRef = useRef(null)
  const headRef = useRef(null)
  const [hit, setHit] = useState(false)
  const lastHitTime = useRef(0)
  const decreaseOpponentHealth = useGameStore((state) => state.decreaseOpponentHealth)
  const count_optm = useRef(0)
  const roomId = useRef('')
  const hitSoundRef = useRef(null);
  const emitDamage = useSocketStore(state=>state.emitDamage);

  useEffect(() => {
    hitSoundRef.current = new Audio('./sounds/hit.MP3');
  }, []);

  useEffect(() => {
    const searchParams = new URLSearchParams(window.location.search);
    roomId.current = searchParams.get('roomId');
  }, [roomId]);

  const playHitSound = useCallback(() => {
    if (hitSoundRef.current) {
      hitSoundRef.current.play();
    }
  }, []);

  const checkHit = useCallback(() => {
    if(!headRef.current || !landmarks.leftHand || !landmarks.rightHand) return

    const headPosition = new THREE.Vector3()
    headRef.current.getWorldPosition(headPosition)

    const hands = [landmarks.leftHand, landmarks.rightHand]
    const currentTime = performance.now()

    hands.forEach((hand, index) => {
      // console.log(hand[2])
      if(hand[2] !== 0) return // 주먹상태인지 확인

      const handPosition = new THREE.Vector3(
        (hand[0][0] - 0.5) * 4,
        -(hand[0][1] - 0.5) * 4,
        -(hand[0][2] * 30)
      )
      // Opponent의 회전을 적용합니다 (y축 주위로 180도 회전).
      // handPosition.applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
      // Player의 위치 오프셋을 적용합니다.
      const distanceAdjustment = new THREE.Vector3(0, 0, -2.5); // 1 - (-5) = 6
      handPosition.add(distanceAdjustment);

      const distance = headPosition.distanceTo(handPosition)

      if (distance < 1.3 && currentTime - lastHitTime.current > 1000) {
        const velocity = hand[0].reduce((sum, coord) => sum + Math.abs(coord), 0)
        const damage = Math.floor(velocity * 10)

        setHit(true)
        decreaseOpponentHealth(damage)
        
        // 데미지 정보를 서버로 전송
        emitDamage(damage, roomId);
        

        playHitSound()
        lastHitTime.current = currentTime
        setTimeout(() => setHit(false), 200)
        // console.log('===velocity:', velocity, 'damage:',damage)
      }
      // console.log('distance:', distance)
    })
  }, [landmarks, decreaseOpponentHealth, playHitSound])

  useFrame(() => {
    if(count_optm.current % 10 === 0) {
      checkHit()
    }
    if (count_optm.current > 1000000) count_optm.current = 0;
    count_optm.current++;
      // console.log('myhead',landmarks?.current?.head?.[0])
  })

  return (
    <group ref={groupRef} position={position} rotation={[0, Math.PI, 0]}>
       {opponentData.head && (
        <OpponentHead
          ref={headRef}
          position={new THREE.Vector3(opponentData.head[0][0], opponentData.head[0][1], opponentData.head[0][2])}
          rotation={[0, -opponentData.head[1][1] * (Math.PI / 180), -opponentData.head[1][2] * (Math.PI / 180)]}
          scale={0.25}
          name='opponentHead'
          hit={hit}
        />)}
        {opponentData.rightHand && (
          <OpponentHand
          position={opponentData.rightHand[0]}
          rotation={[(opponentData.rightHand[1][0]-Math.PI), Math.PI , -(opponentData.rightHand[1][1]-Math.PI/2)]}
            scale={0.33}
            name='opponentRightHand'
          />
        )}
        {opponentData.leftHand && (
          <OpponentHand
            position={opponentData.leftHand[0]}
            rotation={[-(opponentData.leftHand[1][0]-Math.PI), 0 ,-(opponentData.leftHand[1][1]+Math.PI/2)]}
            scale={0.33}
            name='opponentLeftHand'
          />
        )}
    </group>
  );
}

// 모델 미리 로드
useGLTF.preload('/models/head.glb');
useGLTF.preload('/models/left-hand.glb');
useGLTF.preload('/models/right-hand.glb');
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import useSocketStore from '@/store/socketStore';
import useGameStore from '@/store/gameStore';

const useGameLogic = () => {
  const { roomId } = useParams();
  const {
    playerName,
    opponentName,
    setPlayerName,
    opponentReady,
    setOpponentName,
    setOpponentReady,
    startGame,
    gameStatus,
    endGame,
    resetGame,
    decreasePlayerHealth,
    decreaseOpponentHealth,
    getWinner
  } = useGameStore();

  // 소켓 연결 설정
  const socket = useSocketStore(state => state.socket);

  // 소켓 이벤트 리스너 설정
  useEffect(() => {
    if (!socket) return;

    socket.on('gameUpdate', handleGameUpdate);
    socket.on('playerJoined', handlePlayerJoined);
    socket.on('playerLeft', handlePlayerLeft);
    socket.on('gameStart', handleGameStart);
    socket.on('roundEnd', handleRoundEnd);
    socket.on('gameEnd', handleGameEnd);
    socket.on('damage', handleDamage);
    socket.on('ready', opponentReadyState);

    // 방에 입장
    socket.emit('joinRoom', roomId);

    return () => {
      socket.off('gameUpdate');
      socket.off('playerJoined');
      socket.off('playerLeft');
      socket.off('gameStart');
      socket.off('roundEnd');
      socket.off('gameEnd');
      socket.off('ready');
    };
  }, [socket, roomId]);

  // 게임 상태 업데이트 처리
  const handleGameUpdate = useCallback((newState) => {
    setGameState(newState);
  }, []);

  // 플레이어 입장 처리
  const handlePlayerJoined = useCallback((player) => {
    setGameState(prev => ({
      ...prev,
      players: { ...prev.players, [player.id]: player }
    }));
  }, []);

  // 플레이어 퇴장 처리
  const handlePlayerLeft = useCallback((playerId) => {
    setGameState(prev => {
      const newPlayers = { ...prev.players };
      delete newPlayers[playerId];
      return { ...prev, players: newPlayers };
    });
  }, []);

  // 게임 시작 처리
  const handleGameStart = useCallback(() => {
    setGameState(prev => ({ ...prev, gameStarted: true }));
  }, []);

  // 라운드 종료 처리
  const handleRoundEnd = useCallback((roundResult) => {
    setGameState(prev => ({ ...prev, round: prev.round + 1 }));
    // 라운드 결과에 따른 추가 로직
  }, []);

  // 게임 종료 처리
  const handleGameEnd = useCallback((result) => {
    setGameState(prev => ({ ...prev, gameStarted: false, winner: result.winner }));
  }, []);

  // 액션 수행 (예: 펀치)
  // const performAction = useCallback((action) => {
  //   if (socket) {
  //     socket.emit('playerAction', { roomId, playerId: localPlayer, action });
  //   }
  // }, [socket, roomId, localPlayer]);

  // 펀치
  const handleDamage = useCallback((data) => {
    if (socket) {
      if (gameStatus === "playing"){
        decreasePlayerHealth(data.amount);
      }
    }
  }, [socket, decreasePlayerHealth])

  // 상대플레이어 상태 받기
  const opponentReadyState = useCallback((data) => {
    if (socket) {
      setOpponentReady(data.state);
    }
  }, [socket])

  // 플레이어 상태 보내기
  const sendPlayerReadyState = useCallback((state)=>{
    if (socket) {
      socket.emit('ready', {roomId: roomId, data: state})
    }

  }, [socket, roomId])

  return {
    opponentReady,
    sendPlayerReadyState,
  };
};

export default useGameLogic;