Repository: 2505-future/58hack-front
Pull request #83: feat: sendAction のつなぎ込み
Author: login: ulxsth
Created: 2025-05-17T23:30:55Z | Merged: 2025-05-17T23:44:54Z

# Details
<!-- 作業の内容を3行程度で説明 -->

## 変更の目的
<!-- このタスクを行う理由などが明確であれば、ここに記載 -->

# デバッグ手順
<!-- あなたの作業が成功していることを示すデバッグの手順を記載する -->
<!-- 必ず「どうなれば成功か」を記載すること！！ -->

1. 〇〇の画面を開く
2. △△のボタンをクリックする
3. ...

# その他


<!-- 以下消さないでください！！ -->

<!-- for GitHub Copilot -->
<!-- レビューの際は、必ず以下のコメントに示す条件を満たしてください。 -->
<!-- 1. 日本語を使用すること -->
<!-- 2. 口調は軽快で親しみやすい関西弁にすること。ただし、やや抑えめで読みやすさを優先すること。 -->
<!-- 3. 絵文字を多用すること。絵文字は、前後の文に関係のあるものを添付すること。 -->
<!-- よい例：このプルリクエスト、ホーム画面の改善めっちゃ分かりやすいわ！ヘッダーのデザイン、マジでお洒落やん 🚀✨ -->
<!-- 悪い例：このプルリクエストでは、ホーム画面に変更が加えられています。ヘッダーのスタイルにより顕著な変更が見られます。-->


Diff:
diff --git a/app/api/sendAction.server.ts b/app/api/sendAction.client.ts
similarity index 84%
rename from app/api/sendAction.server.ts
rename to app/api/sendAction.client.ts
index 2f8bc60..744dbbf 100644
--- a/app/api/sendAction.server.ts
+++ b/app/api/sendAction.client.ts
@@ -1,5 +1,7 @@
 type Message = {
 	id: string;
+	// FIXME: numberでよかった（angleはひとつ）
+	// とりあえず[angle, 0]で運用
 	angle: number[];
 	pull_power: number;
 };
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index c56bc71..9066948 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -5,14 +5,14 @@
  */
 
 import { RemixBrowser } from "@remix-run/react";
-import { StrictMode, startTransition } from "react";
+import { startTransition } from "react";
 import { hydrateRoot } from "react-dom/client";
 
 startTransition(() => {
 	hydrateRoot(
 		document,
-		<StrictMode>
+		// <StrictMode>
 			<RemixBrowser />
-		</StrictMode>,
+		// </StrictMode>,
 	);
 });
diff --git a/app/features/game/atoms/api.ts b/app/features/game/atoms/api.ts
new file mode 100644
index 0000000..6cc449f
--- /dev/null
+++ b/app/features/game/atoms/api.ts
@@ -0,0 +1,3 @@
+import { atom } from "jotai";
+
+export const apiUrlAtom = atom<string>();
\ No newline at end of file
diff --git a/app/features/game/components/PhaserGame.tsx b/app/features/game/components/PhaserGame.tsx
index b88a446..76ba047 100644
--- a/app/features/game/components/PhaserGame.tsx
+++ b/app/features/game/components/PhaserGame.tsx
@@ -10,6 +10,8 @@ interface PhaserGameProps {
 	gameSettings: GameSettings;
 	players?: PlayerData[];
 	mainPlayerId?: string;
+	apiUrl?: string;
+	roomId?: string;
 }
 
 /**
@@ -23,12 +25,52 @@ export default function PhaserGame({
 	gameSettings,
 	players = [],
 	mainPlayerId,
+	apiUrl,
+	roomId,
 }: PhaserGameProps) {
+	if (!apiUrl) {
+		throw new Error("API URL is required");
+	}
+	if (!roomId) {
+		throw new Error("Room ID is required");
+	}
+
 	const containerRef = useRef<HTMLDivElement>(null);
 	const gameInitializedRef = useRef<boolean>(false);
+	// const roomId
 	const ws = useAtomValue(websocketAtom);
+	if (!ws) {
+		throw new Error("WebSocket is required");
+	}
 	const navigate = useNavigate();
 
+	// ws debug
+	useEffect(() => {
+		ws.onopen = () => {
+			console.log("WebSocket connected");
+		};
+
+		ws.onmessage = (event) => {
+			console.log("message", event)
+		};
+
+		ws.onerror = (error) => {
+			console.error("WebSocket error:", error);
+		};
+
+		ws.onclose = (event) => {
+			console.warn("WebSocket closed", {
+				code: event.code,
+				reason: event.reason,
+				wasClean: event.wasClean,
+			});
+		};
+
+		return () => {
+			ws.close();
+		};
+	}, []);
+
 	// ゲーム終了時のリダイレクトイベントリスナー
 	useEffect(() => {
 		const handleGameRedirect = () => {
@@ -69,6 +111,8 @@ export default function PhaserGame({
 			const config = await createGameConfig(
 				gameSettingsWithPlayers,
 				containerRef.current,
+				apiUrl,
+				roomId
 			);
 
 			const game = new Phaser.Game(config);
diff --git a/app/features/game/core/config/configLoader.ts b/app/features/game/core/config/configLoader.ts
index 83333c9..0da2ec0 100644
--- a/app/features/game/core/config/configLoader.ts
+++ b/app/features/game/core/config/configLoader.ts
@@ -16,6 +16,8 @@ const stateManager = ClientGameStateManager.getInstance();
 export const createGameConfig = async (
 	gameSettings: GameSettings,
 	parent: HTMLElement | undefined,
+	apiUrl: string,
+	roomId: string,
 ): Promise<Phaser.Types.Core.GameConfig> => {
 	const players = gameSettings.players;
 
@@ -188,8 +190,8 @@ export const createGameConfig = async (
 								isFirstClick = false;
 							} else {
 								// 2回目のクリック：ひっぱりを完了して移動
-								const didMove = mainPlayer.completeDrag(pointer.x, pointer.y);
-								isFirstClick = true; // 次回のクリックに備えてリセット
+								const didMove = mainPlayer.completeDrag(pointer.x, pointer.y, apiUrl, roomId);
+								isFirstClick = true;
 
 								if (!didMove) {
 									// 移動できなかった場合は直接新しいドラッグを開始
@@ -235,9 +237,6 @@ export const createGameConfig = async (
 						if (player && typeof player.update === "function") {
 							// プレイヤーのアップデート関数を呼び出し
 							player.update();
-							console.log(
-								`Updating player ${player.id} at (${player.x}, ${player.y})`,
-							);
 						}
 					}
 
diff --git a/app/features/game/core/objects/player.ts b/app/features/game/core/objects/player.ts
index 6ab6551..2ef23e6 100644
--- a/app/features/game/core/objects/player.ts
+++ b/app/features/game/core/objects/player.ts
@@ -1,4 +1,5 @@
 import { GameObjects } from "phaser";
+import sendAction from "~/api/sendAction.client";
 import { COLORS } from "../config/config";
 import ClientGameStateManager from "../state/ClientGameStateManager";
 
@@ -26,9 +27,9 @@ export class Player extends GameObjects.Arc {
 	private youIndicator: Phaser.GameObjects.Text | null = null;
 
 	// マスク更新最適化用の変数
-	private lastMaskX = 0;
-	private lastMaskY = 0;
-	private lastMaskRadius = 0;
+	private lastMaskX: number = 0;
+	private lastMaskY: number = 0;
+	private lastMaskRadius: number = 0;
 
 	// ハイライト更新最適化用の変数
 	private lastHighlightX = 0;
@@ -406,7 +407,7 @@ export class Player extends GameObjects.Arc {
 	 * @param y ひっぱり終了地点のY座標
 	 * @returns 移動が実行されたかどうか
 	 */
-	public completeDrag(x: number, y: number): boolean {
+	public completeDrag(x: number, y: number, apiUrl: string, roomId: string): boolean {
 		// クールダウン中、ドラッグ中でない、または死亡している場合は操作を受け付けない
 		if (this.isInCooldown() || !this.dragStartPoint || !this.isAlive)
 			return false;
@@ -461,6 +462,17 @@ export class Player extends GameObjects.Arc {
 			// バウンス（跳ね返り）を小さめに設定（重い方が跳ね返りが小さい）
 			body.setBounce(0.3 - weightFactor * 0.2); // 0.1～0.3の間
 
+			// イベントの送信
+			// TODO: apiUrl
+			sendAction({
+				type: "action",
+				message: {
+					id: this.id,
+					angle: [angle, 0],
+					pull_power: finalStrength,
+				}
+			}, apiUrl, roomId)
+
 			// 速度の適用
 			body.setVelocity(velocityX, velocityY);
 		}
diff --git a/app/features/game/loader.ts b/app/features/game/loader.ts
index f5912cd..de207b2 100644
--- a/app/features/game/loader.ts
+++ b/app/features/game/loader.ts
@@ -1,10 +1,12 @@
-import { data } from "@remix-run/cloudflare";
+import { data, LoaderFunctionArgs } from "@remix-run/cloudflare";
 import { defaultGameSettings } from "./core/config/gameSettings";
 
 /**
  * ゲーム設定を取得するローダー関数
  */
-export async function getGameSettings() {
+export async function getGameSettings({ context }: LoaderFunctionArgs) {
+	const apiUrl = context.cloudflare.env.API_URL;
+
 	const gameSettings = defaultGameSettings;
-	return data({ gameSettings });
+	return data({ apiUrl, gameSettings });
 }
diff --git a/app/routes/game.tsx b/app/routes/game.tsx
index b08d485..03a68b1 100644
--- a/app/routes/game.tsx
+++ b/app/routes/game.tsx
@@ -51,7 +51,11 @@ export const meta: MetaFunction = () => {
  * PhaserGameコンポーネントを使用してゲーム機能を表示します
  */
 export default function GameScreen() {
-	const { gameSettings } = useLoaderData<typeof loader>();
+	const { apiUrl, gameSettings } = useLoaderData<typeof loader>();
+	const roomId = new URLSearchParams(window.location.search).get("roomId");
+	if(!roomId) {
+		throw new Error("Room ID is required");
+	}
 
 	// メインプレイヤーのIDを設定
 	const mainPlayerId = "ulxsth";
@@ -64,6 +68,8 @@ export default function GameScreen() {
 					gameSettings={gameSettings}
 					players={samplePlayers}
 					mainPlayerId={mainPlayerId}
+					apiUrl={apiUrl}
+					roomId={roomId}
 				/>
 			</div>
 			<div className="mt-4 text-sm text-gray-600">
diff --git a/app/routes/waiting.tsx b/app/routes/waiting.tsx
index 1f4494c..48b3a49 100644
--- a/app/routes/waiting.tsx
+++ b/app/routes/waiting.tsx
@@ -176,7 +176,7 @@ const WaitingRoom = () => {
 				);
 			} else if (data.type === "start") {
 				redirectRef.current = true;
-				router(`/game?roomId=${roomID}`);
+				router(`/game?roomId=${roomID}&hoge=${redirectRef.current}`);
 			}
 		};
 
@@ -193,11 +193,11 @@ const WaitingRoom = () => {
 		};
 
 		return () => {
-			if (!redirectRef.current) {
-				ws.close();
+			if(!redirectRef.current) {
+				// ws.close();
 			}
 		};
-	}, [websocketUrl, githubUser]);
+	}, []);
 
 	// クリップボードにコピー
 	const copyToClipboard = async (text: string) => {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

この PR は sendAction のつなぎ込みを行い、ゲームプレイ中のアクション送信機能を実装するための変更やそれに伴う各所の API 呼び出し更新を行ってます 🤖✨  
- WaitingRoom のリダイレクト時にクエリパラメータを追加  
- GameScreen や Loader、PhaserGame コンポーネント等で API URL や Room ID の引き渡し処理を更新  
- Player クラス内で sendAction を呼び出す処理を追加

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

<details>
<summary>Show a summary per file</summary>

| File                                            | Description                                               |
| ----------------------------------------------- | --------------------------------------------------------- |
| app/routes/waiting.tsx                          | リダイレクト処理と WS クローズ処理の変更                   |
| app/routes/game.tsx                             | API URL、Room ID の取得およびエラーチェックの追加           |
| app/features/game/loader.ts                     | Loader における API URL の取り扱いの更新                  |
| app/features/game/core/objects/player.ts        | completeDrag に API URL・Room ID を追加し sendAction 呼び出し  |
| app/features/game/core/config/configLoader.ts   | createGameConfig 呼び出しの引数に API URL・Room ID を追加    |
| app/features/game/components/PhaserGame.tsx      | コンポーネントに API URL、Room ID の必須チェックを追加       |
| app/features/game/atoms/api.ts                   | 新規 atom の追加                                         |
| app/entry.client.tsx                            | StrictMode の使用をコメントアウトする変更                |
| app/api/sendAction.server.ts                    | FIXME コメントの追加                                    |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**app/features/game/core/objects/player.ts:466**
* sendAction は非同期関数やから、返り値のエラーハンドリングや await 処理が抜けてると将来的に問題になる可能性があるで。非同期処理として扱うか、エラー状態を適切に処理することを検討してほしいな🚀
```
sendAction({ type: "action", message: { id: this.id, angle: [angle, 0], pull_power: finalStrength } }, apiUrl, roomId)
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T23:31:38Z
user: login: ogatakatsuya
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T23:43:20Z

Review comments:
diff_hunk: @@ -165,7 +165,7 @@ const WaitingRoom = () => {
 				);
 			} else if (data.type === "start") {
 				redirectRef.current = true;
-				router(`/game?roomId=${roomID}`);
+				router(`/game?roomId=${roomID}&hoge=${redirectRef.current}`);
path: app/routes/waiting.tsx
user: login: Copilot
body: 『hoge』というクエリパラメータは一時的なデバッグ用かもしれへんけど、意図が明確ならその旨のコメントを追加してほしいで😊
```suggestion
				// The `isRedirect` query parameter indicates whether the user was redirected to the game.
				router(`/game?roomId=${roomID}&isRedirect=${redirectRef.current}`);
```
created_at: 2025-05-17T23:31:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,10 +183,10 @@ const WaitingRoom = () => {
 
 		return () => {
 			if(!redirectRef.current) {
path: app/routes/waiting.tsx
user: login: Copilot
body: WS のクローズ処理をコメントアウトしてるけど、意図的な変更なら理由をコメントで残すと後々分かりやすいと思うで👍
```suggestion
			if(!redirectRef.current) {
				// Intentionally commented out to keep the WebSocket open when redirecting.
				// Uncomment this line if you want to ensure the WebSocket is always closed on unmount.
```
created_at: 2025-05-17T23:31:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,14 +5,14 @@
  */
 
 import { RemixBrowser } from "@remix-run/react";
-import { StrictMode, startTransition } from "react";
+import { startTransition } from "react";
 import { hydrateRoot } from "react-dom/client";
 
 startTransition(() => {
 	hydrateRoot(
 		document,
-		<StrictMode>
+		// <StrictMode>
path: app/entry.client.tsx
user: login: Copilot
body: [nitpick] StrictMode の部分をコメントアウトしてるけど、意図的な変更ならその理由をコード内のコメントに明記すると親切やと思うで😊
```suggestion
		// <StrictMode>
		// StrictMode is commented out to [insert reason here, e.g., avoid double rendering during development].
```
created_at: 2025-05-17T23:31:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,7 @@
 type Message = {
 	id: string;
+	// FIXME: numberでよかった（angleはひとつ）
path: app/api/sendAction.server.ts
user: login: Copilot
body: [nitpick] FIXME コメントが残ってるね。修正予定があるなら、対応内容や理由をコメントで補足すると後で見た人が分かりやすいで👍
created_at: 2025-05-17T23:31:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-17T23:35:00Z
body: ## 🚀 Deploying 58hack-front with Cloudflare Workers
**Branch:** `feature/send-action`

**Preview URL:** https://73cea403-58-hack-front.ogata-1225-koroku.workers.dev

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-17T23:37:10Z
body: ## 🚀 Deploying 58hack-front with Cloudflare Workers
**Branch:** `feature/send-action`

**Preview URL:** https://b95ad692-58-hack-front.ogata-1225-koroku.workers.dev

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-17T23:43:46Z
body: ## 🚀 Deploying 58hack-front with Cloudflare Workers
**Branch:** `feature/send-action`

**Preview URL:** https://6ee0feec-58-hack-front.ogata-1225-koroku.workers.dev

author_association: NONE