diff --git a/apps/client/src/scenes/game/input/GameInputOverlay.tsx b/apps/client/src/scenes/game/input/GameInputOverlay.tsx index ec1df11..e8e2db8 100644 --- a/apps/client/src/scenes/game/input/GameInputOverlay.tsx +++ b/apps/client/src/scenes/game/input/GameInputOverlay.tsx @@ -4,7 +4,7 @@ * ジョイスティック層と爆弾ボタン層を分離して配置する */ import { config } from "@client/config"; -import { JoystickInputPresenter } from "./joystick/JoystickInputPresenter"; +import { JoystickInputPresenter } from "./joystick/presentation/JoystickInputPresenter"; import { BombButton } from "./bomb/BombButton"; import { useCooldownClock } from "./hooks/useCooldownClock"; diff --git a/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx b/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx index e3fec49..107992e 100644 --- a/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx +++ b/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx @@ -1,64 +1,8 @@ /** * JoystickInputPresenter - * ジョイスティック入力の受け取りと表示状態の橋渡しを担うプレゼンター - * 入力イベントをコントローラーへ委譲し,描画用状態をViewへ渡す + * presentation配下のJoystickInputPresenterを段階移行のため再公開する + * 既存import互換を維持して段階的な参照置換を可能にする */ -import { useJoystickController } from "./useJoystickController"; -import { JoystickView } from "./JoystickView"; -import type { UseJoystickInputPresenterProps } from "./common"; -import { useEffect } from "react"; -/** 入力と表示状態の橋渡しを行う */ -export const JoystickInputPresenter = ({ - onInput, - maxDist, - isEnabled = true, -}: UseJoystickInputPresenterProps) => { - const { - isMoving, - center, - knobOffset, - radius, - handleStart, - handleMove, - handleEnd, - reset, - } = useJoystickController({ onInput, maxDist }); - - useEffect(() => { - if (isEnabled) { - return; - } - - reset(); - }, [isEnabled, reset]); - - return ( -
- {/* 入力イベントをコントローラーへ渡し,描画用状態をViewへ渡す */} - -
- ); -}; +/** presentation配下のコンポーネントを互換再エクスポートする */ +export { JoystickInputPresenter } from "./presentation/JoystickInputPresenter"; diff --git a/apps/client/src/scenes/game/input/joystick/JoystickModel.ts b/apps/client/src/scenes/game/input/joystick/JoystickModel.ts index 06193f5..023930e 100644 --- a/apps/client/src/scenes/game/input/joystick/JoystickModel.ts +++ b/apps/client/src/scenes/game/input/joystick/JoystickModel.ts @@ -1,38 +1,11 @@ /** * JoystickModel - * ジョイスティック入力の座標計算を担うモデル - * ノブ表示オフセットと正規化入力ベクトルを算出する + * model配下のJoystickModelを段階移行のため再公開する + * 既存import互換を維持して段階的な参照置換を可能にする */ -import type { NormalizedInput, Point } from './common'; -/** ジョイスティック計算結果を表す型 */ -export type JoystickComputed = { - knobOffset: Point; - normalized: NormalizedInput; -}; +/** model配下の型を互換再エクスポートする */ +export type { JoystickComputed } from "./model/JoystickModel"; -/** 座標差分からノブオフセットと正規化入力を計算する */ -export const computeJoystick = ( - center: Point, - current: Point, - radius: number -): JoystickComputed => { - const safeRadius = radius > 0 ? radius : 1; - - const dx = current.x - center.x; - const dy = current.y - center.y; - const dist = Math.hypot(dx, dy); - const angle = Math.atan2(dy, dx); - - const limitedDist = Math.min(dist, safeRadius); - const offsetX = Math.cos(angle) * limitedDist; - const offsetY = Math.sin(angle) * limitedDist; - - return { - knobOffset: { x: offsetX, y: offsetY }, - normalized: { - x: offsetX / safeRadius, - y: offsetY / safeRadius, - }, - }; -}; +/** model配下の関数を互換再エクスポートする */ +export { computeJoystick } from "./model/JoystickModel"; diff --git a/apps/client/src/scenes/game/input/joystick/JoystickView.tsx b/apps/client/src/scenes/game/input/joystick/JoystickView.tsx index 62d449f..5ea8c7b 100644 --- a/apps/client/src/scenes/game/input/joystick/JoystickView.tsx +++ b/apps/client/src/scenes/game/input/joystick/JoystickView.tsx @@ -1,50 +1,8 @@ /** * JoystickView - * ジョイスティックの見た目だけを描画するコンポーネント - * 入力処理は持たず,受け取った座標情報をもとにUIを描く + * presentation配下のJoystickViewを段階移行のため再公開する + * 既存import互換を維持して段階的な参照置換を可能にする */ -import { - JOYSTICK_BASE_BG_COLOR, - JOYSTICK_BASE_BORDER_COLOR, - JOYSTICK_BASE_BORDER_WIDTH, - JOYSTICK_KNOB_BG_COLOR, - JOYSTICK_KNOB_SHADOW, - JOYSTICK_KNOB_SIZE, -} from "./common"; -import type { UseJoystickViewProps } from "./common"; -/** UIの見た目だけを描画するビュー */ -export const JoystickView = ({ isActive, center, knobOffset, radius }: UseJoystickViewProps) => { - if (!isActive) return null; - - // ベースリングとノブの描画 - return ( -
-
-
- ); -}; +/** presentation配下のコンポーネントを互換再エクスポートする */ +export { JoystickView } from "./presentation/JoystickView"; diff --git a/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts b/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts new file mode 100644 index 0000000..db339a1 --- /dev/null +++ b/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts @@ -0,0 +1,94 @@ +/** + * useJoystickController + * 入力イベントとジョイスティック計算結果の仲介を担うフック + * useJoystickState の出力を受けて onInput 通知と終了時リセット通知を統一する + */ +import { useCallback, useRef } from "react"; +import type { + JoystickPointerEvent, + NormalizedInput, + UseJoystickControllerProps, + UseJoystickControllerReturn, +} from "../common"; +import { + JOYSTICK_MIN_MOVEMENT_DELTA, + JOYSTICK_SEND_ZERO_ON_END, +} from "../common"; +import { useJoystickState } from "./useJoystickState"; + +/** 入力イベントと通知処理を仲介するフック */ +export const useJoystickController = ({ + onInput, + maxDist, +}: UseJoystickControllerProps): UseJoystickControllerReturn => { + const { + isMoving, + center, + knobOffset, + radius, + handleStart, + handleMove: baseHandleMove, + handleEnd: baseHandleEnd, + reset: baseReset, + } = useJoystickState({ maxDist }); + + const lastEmittedRef = useRef(null); + + const emitInput = useCallback( + (normalized: NormalizedInput) => { + onInput(normalized.x, normalized.y); + }, + [onInput], + ); + + const handleMove = useCallback( + (e: JoystickPointerEvent) => { + const normalized = baseHandleMove(e); + if (!normalized) return; + + const last = lastEmittedRef.current; + if (last) { + const delta = Math.hypot(normalized.x - last.x, normalized.y - last.y); + if (delta < JOYSTICK_MIN_MOVEMENT_DELTA) { + return; + } + } + + emitInput(normalized); + lastEmittedRef.current = normalized; + }, + [baseHandleMove, emitInput], + ); + + const handleEnd = useCallback( + (e: JoystickPointerEvent) => { + baseHandleEnd(e); + + if (JOYSTICK_SEND_ZERO_ON_END) { + emitInput({ x: 0, y: 0 }); + lastEmittedRef.current = { x: 0, y: 0 }; + return; + } + + lastEmittedRef.current = null; + }, + [baseHandleEnd, emitInput], + ); + + const reset = useCallback(() => { + baseReset(); + emitInput({ x: 0, y: 0 }); + lastEmittedRef.current = { x: 0, y: 0 }; + }, [baseReset, emitInput]); + + return { + isMoving, + center, + knobOffset, + radius, + handleStart, + handleMove, + handleEnd, + reset, + }; +}; diff --git a/apps/client/src/scenes/game/input/joystick/hooks/useJoystickState.ts b/apps/client/src/scenes/game/input/joystick/hooks/useJoystickState.ts new file mode 100644 index 0000000..77bf87e --- /dev/null +++ b/apps/client/src/scenes/game/input/joystick/hooks/useJoystickState.ts @@ -0,0 +1,125 @@ +/** + * useJoystickState + * ジョイスティック入力状態の管理と入力ハンドラの提供を担うフック + * UI描画に必要な中心点,ノブ位置,半径を保持する + */ +import { useCallback, useEffect, useRef, useState } from "react"; +import { JOYSTICK_DEADZONE, MAX_DIST } from "../common"; +import { computeJoystick } from "../model/JoystickModel"; +import type { + JoystickPointerEvent, + Point, + UseJoystickStateProps, + UseJoystickStateReturn, +} from "../common"; + +const getClientPoint = (e: JoystickPointerEvent): Point | null => { + return { x: e.clientX, y: e.clientY }; +}; + +/** ジョイスティック入力状態と入力ハンドラを提供する */ +export const useJoystickState = ({ + maxDist, +}: UseJoystickStateProps): UseJoystickStateReturn => { + const [isMoving, setIsMoving] = useState(false); + const [center, setCenter] = useState({ x: 0, y: 0 }); + const [knobOffset, setKnobOffset] = useState({ x: 0, y: 0 }); + const activePointerIdRef = useRef(null); + const activePointerTargetRef = useRef(null); + const radius = maxDist ?? MAX_DIST; + + const reset = useCallback(() => { + const pointerId = activePointerIdRef.current; + const pointerTarget = activePointerTargetRef.current; + if ( + pointerId !== null + && pointerTarget + && pointerTarget.hasPointerCapture(pointerId) + ) { + pointerTarget.releasePointerCapture(pointerId); + } + + activePointerIdRef.current = null; + activePointerTargetRef.current = null; + setIsMoving(false); + setKnobOffset({ x: 0, y: 0 }); + }, []); + + const handleStart = useCallback((e: JoystickPointerEvent) => { + if (activePointerIdRef.current !== null) return; + + const point = getClientPoint(e); + if (!point) return; + if (point.x > window.innerWidth / 2) return; + + activePointerIdRef.current = e.pointerId; + activePointerTargetRef.current = e.currentTarget; + e.currentTarget.setPointerCapture(e.pointerId); + setCenter(point); + setKnobOffset({ x: 0, y: 0 }); + setIsMoving(true); + }, []); + + const handleMove = useCallback( + (e: JoystickPointerEvent) => { + if (!isMoving || activePointerIdRef.current !== e.pointerId) return null; + const point = getClientPoint(e); + if (!point) return null; + + const computed = computeJoystick(center, point, radius); + + const magnitude = Math.hypot( + computed.normalized.x, + computed.normalized.y, + ); + if (magnitude < JOYSTICK_DEADZONE) { + setKnobOffset({ x: 0, y: 0 }); + return { x: 0, y: 0 }; + } + + setKnobOffset(computed.knobOffset); + return computed.normalized; + }, + [isMoving, center.x, center.y, radius], + ); + + const handleEnd = useCallback( + (e: JoystickPointerEvent) => { + if (activePointerIdRef.current !== e.pointerId) return; + + reset(); + }, + [reset], + ); + + useEffect(() => { + const handleWindowBlur = () => { + reset(); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === "hidden") { + reset(); + } + }; + + window.addEventListener("blur", handleWindowBlur); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.removeEventListener("blur", handleWindowBlur); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [reset]); + + return { + isMoving, + center, + knobOffset, + radius, + handleStart, + handleMove, + handleEnd, + reset, + }; +}; diff --git a/apps/client/src/scenes/game/input/joystick/model/JoystickModel.ts b/apps/client/src/scenes/game/input/joystick/model/JoystickModel.ts new file mode 100644 index 0000000..3786e5a --- /dev/null +++ b/apps/client/src/scenes/game/input/joystick/model/JoystickModel.ts @@ -0,0 +1,38 @@ +/** + * JoystickModel + * ジョイスティック入力の座標計算を担うモデル + * ノブ表示オフセットと正規化入力ベクトルを算出する + */ +import type { NormalizedInput, Point } from "../common"; + +/** ジョイスティック計算結果を表す型 */ +export type JoystickComputed = { + knobOffset: Point; + normalized: NormalizedInput; +}; + +/** 座標差分からノブオフセットと正規化入力を計算する */ +export const computeJoystick = ( + center: Point, + current: Point, + radius: number, +): JoystickComputed => { + const safeRadius = radius > 0 ? radius : 1; + + const dx = current.x - center.x; + const dy = current.y - center.y; + const dist = Math.hypot(dx, dy); + const angle = Math.atan2(dy, dx); + + const limitedDist = Math.min(dist, safeRadius); + const offsetX = Math.cos(angle) * limitedDist; + const offsetY = Math.sin(angle) * limitedDist; + + return { + knobOffset: { x: offsetX, y: offsetY }, + normalized: { + x: offsetX / safeRadius, + y: offsetY / safeRadius, + }, + }; +}; diff --git a/apps/client/src/scenes/game/input/joystick/presentation/JoystickInputPresenter.tsx b/apps/client/src/scenes/game/input/joystick/presentation/JoystickInputPresenter.tsx new file mode 100644 index 0000000..de09ab1 --- /dev/null +++ b/apps/client/src/scenes/game/input/joystick/presentation/JoystickInputPresenter.tsx @@ -0,0 +1,62 @@ +/** + * JoystickInputPresenter + * ジョイスティック入力の受け取りと表示状態の橋渡しを担うプレゼンター + * 入力イベントをコントローラーへ委譲し,描画用状態をViewへ渡す + */ +import { useEffect } from "react"; +import { useJoystickController } from "../hooks/useJoystickController"; +import { JoystickView } from "./JoystickView.tsx"; +import type { UseJoystickInputPresenterProps } from "../common"; + +/** 入力と表示状態の橋渡しを行う */ +export const JoystickInputPresenter = ({ + onInput, + maxDist, + isEnabled = true, +}: UseJoystickInputPresenterProps) => { + const { + isMoving, + center, + knobOffset, + radius, + handleStart, + handleMove, + handleEnd, + reset, + } = useJoystickController({ onInput, maxDist }); + + useEffect(() => { + if (isEnabled) { + return; + } + + reset(); + }, [isEnabled, reset]); + + return ( +
+ +
+ ); +}; diff --git a/apps/client/src/scenes/game/input/joystick/presentation/JoystickView.tsx b/apps/client/src/scenes/game/input/joystick/presentation/JoystickView.tsx new file mode 100644 index 0000000..7023788 --- /dev/null +++ b/apps/client/src/scenes/game/input/joystick/presentation/JoystickView.tsx @@ -0,0 +1,54 @@ +/** + * JoystickView + * ジョイスティックの見た目だけを描画するコンポーネント + * 入力処理は持たず,受け取った座標情報をもとにUIを描く + */ +import { + JOYSTICK_BASE_BG_COLOR, + JOYSTICK_BASE_BORDER_COLOR, + JOYSTICK_BASE_BORDER_WIDTH, + JOYSTICK_KNOB_BG_COLOR, + JOYSTICK_KNOB_SHADOW, + JOYSTICK_KNOB_SIZE, +} from "../common"; +import type { UseJoystickViewProps } from "../common"; + +/** UIの見た目だけを描画するビュー */ +export const JoystickView = ({ + isActive, + center, + knobOffset, + radius, +}: UseJoystickViewProps) => { + if (!isActive) return null; + + return ( +
+
+
+ ); +}; diff --git a/apps/client/src/scenes/game/input/joystick/useJoystickController.ts b/apps/client/src/scenes/game/input/joystick/useJoystickController.ts index 70ce011..de5d854 100644 --- a/apps/client/src/scenes/game/input/joystick/useJoystickController.ts +++ b/apps/client/src/scenes/game/input/joystick/useJoystickController.ts @@ -1,95 +1,8 @@ /** * useJoystickController - * 入力イベントとジョイスティック計算結果の仲介を担うフック - * useJoystickState の出力を受けて onInput 通知と終了時リセット通知を統一する + * hooks配下のuseJoystickControllerを段階移行のため再公開する + * 既存import互換を維持して段階的な参照置換を可能にする */ -import { useCallback } from "react"; -import { useRef } from "react"; -import type { - JoystickPointerEvent, - NormalizedInput, - UseJoystickControllerProps, - UseJoystickControllerReturn, -} from "./common"; -import { - JOYSTICK_MIN_MOVEMENT_DELTA, - JOYSTICK_SEND_ZERO_ON_END, -} from "./common"; -import { useJoystickState } from "./useJoystickState"; -/** 入力イベントと通知処理を仲介するフック */ -export const useJoystickController = ({ - onInput, - maxDist, -}: UseJoystickControllerProps): UseJoystickControllerReturn => { - const { - isMoving, - center, - knobOffset, - radius, - handleStart, - handleMove: baseHandleMove, - handleEnd: baseHandleEnd, - reset: baseReset, - } = useJoystickState({ maxDist }); - - const lastEmittedRef = useRef(null); - - const emitInput = useCallback( - (normalized: NormalizedInput) => { - onInput(normalized.x, normalized.y); - }, - [onInput], - ); - - const handleMove = useCallback( - (e: JoystickPointerEvent) => { - const normalized = baseHandleMove(e); - if (!normalized) return; - - const last = lastEmittedRef.current; - if (last) { - const delta = Math.hypot(normalized.x - last.x, normalized.y - last.y); - if (delta < JOYSTICK_MIN_MOVEMENT_DELTA) { - return; - } - } - - emitInput(normalized); - lastEmittedRef.current = normalized; - }, - [baseHandleMove, emitInput], - ); - - const handleEnd = useCallback( - (e: JoystickPointerEvent) => { - baseHandleEnd(e); - - if (JOYSTICK_SEND_ZERO_ON_END) { - emitInput({ x: 0, y: 0 }); - lastEmittedRef.current = { x: 0, y: 0 }; - return; - } - - lastEmittedRef.current = null; - }, - [baseHandleEnd, emitInput], - ); - - const reset = useCallback(() => { - baseReset(); - emitInput({ x: 0, y: 0 }); - lastEmittedRef.current = { x: 0, y: 0 }; - }, [baseReset, emitInput]); - - return { - isMoving, - center, - knobOffset, - radius, - handleStart, - handleMove, - handleEnd, - reset, - }; -}; +/** hooks配下のフックを互換再エクスポートする */ +export { useJoystickController } from "./hooks/useJoystickController"; diff --git a/apps/client/src/scenes/game/input/joystick/useJoystickState.ts b/apps/client/src/scenes/game/input/joystick/useJoystickState.ts index b9e8ee2..284ee43 100644 --- a/apps/client/src/scenes/game/input/joystick/useJoystickState.ts +++ b/apps/client/src/scenes/game/input/joystick/useJoystickState.ts @@ -1,129 +1,8 @@ /** * useJoystickState - * ジョイスティック入力状態の管理と入力ハンドラの提供を担うフック - * UI描画に必要な中心点,ノブ位置,半径を保持する + * hooks配下のuseJoystickStateを段階移行のため再公開する + * 既存import互換を維持して段階的な参照置換を可能にする */ -import { useCallback, useEffect, useRef, useState } from "react"; -import { JOYSTICK_DEADZONE, MAX_DIST } from "./common"; -import { computeJoystick } from "./JoystickModel"; -import type { - JoystickPointerEvent, - Point, - UseJoystickStateProps, - UseJoystickStateReturn, -} from "./common"; -/** タッチとマウスからクライアント座標を共通化して取得する */ -const getClientPoint = (e: JoystickPointerEvent): Point | null => { - return { x: e.clientX, y: e.clientY }; -}; - -/** ジョイスティック入力状態と入力ハンドラを提供する */ -export const useJoystickState = ({ - maxDist, -}: UseJoystickStateProps): UseJoystickStateReturn => { - const [isMoving, setIsMoving] = useState(false); - const [center, setCenter] = useState({ x: 0, y: 0 }); - const [knobOffset, setKnobOffset] = useState({ x: 0, y: 0 }); - const activePointerIdRef = useRef(null); - const activePointerTargetRef = useRef(null); - const radius = maxDist ?? MAX_DIST; - - const reset = useCallback(() => { - const pointerId = activePointerIdRef.current; - const pointerTarget = activePointerTargetRef.current; - if ( - pointerId !== null && - pointerTarget && - pointerTarget.hasPointerCapture(pointerId) - ) { - pointerTarget.releasePointerCapture(pointerId); - } - - activePointerIdRef.current = null; - activePointerTargetRef.current = null; - setIsMoving(false); - setKnobOffset({ x: 0, y: 0 }); - }, []); - - // 入力開始時の基準座標をセットする - const handleStart = useCallback((e: JoystickPointerEvent) => { - if (activePointerIdRef.current !== null) return; - - const point = getClientPoint(e); - if (!point) return; - if (point.x > window.innerWidth / 2) return; - - activePointerIdRef.current = e.pointerId; - activePointerTargetRef.current = e.currentTarget; - e.currentTarget.setPointerCapture(e.pointerId); - setCenter(point); - setKnobOffset({ x: 0, y: 0 }); - setIsMoving(true); - }, []); - - // 入力座標からベクトルを計算し,半径でクランプして正規化する - const handleMove = useCallback( - (e: JoystickPointerEvent) => { - if (!isMoving || activePointerIdRef.current !== e.pointerId) return null; - const point = getClientPoint(e); - if (!point) return null; - - const computed = computeJoystick(center, point, radius); - - const magnitude = Math.hypot( - computed.normalized.x, - computed.normalized.y, - ); - if (magnitude < JOYSTICK_DEADZONE) { - setKnobOffset({ x: 0, y: 0 }); - return { x: 0, y: 0 }; - } - - setKnobOffset(computed.knobOffset); - return computed.normalized; - }, - [isMoving, center.x, center.y, radius], - ); - - // 入力終了時に状態をリセットする - const handleEnd = useCallback( - (e: JoystickPointerEvent) => { - if (activePointerIdRef.current !== e.pointerId) return; - - reset(); - }, - [reset], - ); - - useEffect(() => { - const handleWindowBlur = () => { - reset(); - }; - - const handleVisibilityChange = () => { - if (document.visibilityState === "hidden") { - reset(); - } - }; - - window.addEventListener("blur", handleWindowBlur); - document.addEventListener("visibilitychange", handleVisibilityChange); - - return () => { - window.removeEventListener("blur", handleWindowBlur); - document.removeEventListener("visibilitychange", handleVisibilityChange); - }; - }, [reset]); - - return { - isMoving, - center, - knobOffset, - radius, - handleStart, - handleMove, - handleEnd, - reset, - }; -}; +/** hooks配下のフックを互換再エクスポートする */ +export { useJoystickState } from "./hooks/useJoystickState";