-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Remnove `useStateCallback * Remove unncesary info * Remove game.setUserMode * UserModeType -> UserMode * Create useGame hook * Move userMode to useGame * Move player info to useGamte * Create usePlayers * Remove memoization * Move theme change logic to useGame * Create useGameSocket * Move useGameSocket to useGame * Move game ticking logic to useGame * Create useGameKeyboard * create useGameTheme * Create useGetGame * Define DEFAULT_COLOR in const * Create lib/hooks/game/ folder * Format
- Loading branch information
1 parent
ab33f00
commit 6ce09a6
Showing
12 changed files
with
422 additions
and
393 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { useEffect, useRef, useState } from "react"; | ||
import { useUserMode } from "../useUserMode"; | ||
import { UserEntity } from "../../dtos"; | ||
import usePlayers from "../usePlayers"; | ||
import useGameSocket from "./useGameSocket"; | ||
import { TARGET_FRAME_MS } from "@/app/pong/[id]/const"; | ||
import useGameKeyboard from "./useGameKeyboard"; | ||
import useGameTheme from "./useGameTheme"; | ||
import useGetGame from "./useGetGame"; | ||
|
||
export default function useGame( | ||
id: string, | ||
currentUser?: UserEntity, | ||
resolvedTheme?: string, | ||
) { | ||
const canvasRef = useRef<HTMLCanvasElement | null>(null); // only initialized once | ||
const [userMode, setUserMode] = useUserMode(); | ||
const { getGame } = useGetGame(canvasRef, userMode); | ||
const [logs, setLogs] = useState<string[]>([]); | ||
const [startDisabled, setStartDisabled] = useState(true); | ||
|
||
const { leftPlayer, rightPlayer, getPlayerSetterFromPlayerNumber } = | ||
usePlayers(userMode, currentUser); | ||
|
||
useGameTheme(getGame, resolvedTheme); | ||
useGameKeyboard(getGame); | ||
|
||
const { start } = useGameSocket( | ||
id, | ||
getGame, | ||
setLogs, | ||
userMode, | ||
setUserMode, | ||
getPlayerSetterFromPlayerNumber, | ||
setStartDisabled, | ||
currentUser, | ||
); | ||
|
||
useEffect(() => { | ||
const game = getGame(); | ||
game.draw_canvas(); | ||
const intervalId = setInterval(game.update, TARGET_FRAME_MS); | ||
|
||
return () => clearInterval(intervalId); | ||
}, [getGame]); | ||
|
||
return { | ||
getGame, | ||
canvasRef, | ||
userMode, | ||
leftPlayer, | ||
rightPlayer, | ||
logs, | ||
start, | ||
startDisabled, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { PongGame } from "@/app/pong/[id]/PongGame"; | ||
import { useEffect } from "react"; | ||
|
||
export default function useGameKeyboard(getGame: () => PongGame) { | ||
useEffect(() => { | ||
const game = getGame(); | ||
|
||
const handleKeyUp = (event: KeyboardEvent) => { | ||
if (event.key == "ArrowDown" || event.key == "ArrowUp") { | ||
game.setMovingDirection("none"); | ||
} | ||
}; | ||
const handleKeyDown = (event: KeyboardEvent) => { | ||
if (event.key == "ArrowDown") { | ||
game.setMovingDirection("right"); | ||
} else if (event.key == "ArrowUp") { | ||
game.setMovingDirection("left"); | ||
} | ||
}; | ||
|
||
document.addEventListener("keydown", handleKeyDown); | ||
document.addEventListener("keyup", handleKeyUp); | ||
return () => { | ||
document.removeEventListener("keydown", handleKeyDown); | ||
document.removeEventListener("keyup", handleKeyUp); | ||
}; | ||
}, [getGame]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
import { PongGame } from "@/app/pong/[id]/PongGame"; | ||
import { useCallback, useEffect, useRef } from "react"; | ||
import { Socket, io } from "socket.io-client"; | ||
import { UserMode } from "../useUserMode"; | ||
import { UserEntity } from "../../dtos"; | ||
import { POINT_TO_WIN } from "@/app/pong/[id]/const"; | ||
|
||
type Status = | ||
| "too-many-players" | ||
| "joined-as-player" | ||
| "joined-as-viewer" | ||
| "ready" | ||
| "login-required" | ||
| "friend-joined" | ||
| "friend-left" | ||
| "won" | ||
| "lost" | ||
| "finish"; | ||
|
||
interface HandleActionProps { | ||
playerNumber: number; | ||
} | ||
|
||
const getLogFromStatus = (status: Status): string => { | ||
switch (status) { | ||
case "too-many-players": | ||
return "There are too many players. You can only watch the game"; | ||
case "joined-as-player": | ||
return "You have joined as player"; | ||
case "joined-as-viewer": | ||
return "You have joined as viewer"; | ||
case "ready": | ||
return "Your friend is already here. The game is ready to start"; | ||
case "login-required": | ||
return "You need to login to play."; | ||
case "friend-joined": | ||
return "Your friend has joined the game"; | ||
case "friend-left": | ||
return "Your friend has left"; | ||
case "won": | ||
return "You won!"; | ||
case "lost": | ||
return "You lost!"; | ||
case "finish": | ||
return "The game has finished"; | ||
} | ||
}; | ||
|
||
export default function useGameSocket( | ||
id: string, | ||
getGame: () => PongGame, | ||
setLogs: (fun: (logs: string[]) => string[]) => void, | ||
userMode: UserMode, | ||
setUserMode: (mode: UserMode) => void, | ||
getPlayerSetterFromPlayerNumber: ( | ||
playerNumber: number, | ||
) => (user: any) => void, | ||
setStartDisabled: (disabled: boolean) => void, | ||
currentUser?: UserEntity, | ||
) { | ||
const socketRef = useRef<Socket | null>(null); // updated on `id` change | ||
|
||
const start = useCallback(() => { | ||
if (!userMode) return; | ||
const game = getGame(); | ||
|
||
setStartDisabled(true); | ||
|
||
const { vx, vy } = game.start({ vx: undefined, vy: undefined }); | ||
socketRef.current?.emit("start", { | ||
vx: -vx, | ||
vy: -vy, | ||
}); | ||
}, [getGame, userMode, setStartDisabled]); | ||
|
||
const runSideEffectForStatusUpdate = useCallback( | ||
(status: Status, payload: any) => { | ||
const game = getGame(); | ||
|
||
switch (status) { | ||
case "too-many-players": | ||
// TODO: users cannot really see the log | ||
setUserMode("viewer"); | ||
break; | ||
case "login-required": | ||
// TODO: instead of redirect. Show modal to login | ||
setUserMode("viewer"); | ||
break; | ||
case "friend-joined": | ||
const { playerNumber, user } = payload; | ||
const setter = getPlayerSetterFromPlayerNumber(playerNumber); | ||
setter(user); | ||
currentUser && setStartDisabled(false); | ||
game.resetPlayerPosition(); | ||
break; | ||
case "friend-left": | ||
{ | ||
const { playerNumber } = payload; | ||
const setter = getPlayerSetterFromPlayerNumber(playerNumber); | ||
setter(undefined); | ||
setStartDisabled(true); | ||
} | ||
break; | ||
case "joined-as-viewer": | ||
{ | ||
const { players } = payload; | ||
players.forEach(({ playerNumber, user }: any) => { | ||
const setter = getPlayerSetterFromPlayerNumber(playerNumber); | ||
setter(user); | ||
}); | ||
} | ||
break; | ||
case "ready": { | ||
{ | ||
const { user, playerNumber } = payload; | ||
const setter = getPlayerSetterFromPlayerNumber(playerNumber); | ||
setter(user); | ||
} | ||
break; | ||
} | ||
} | ||
}, | ||
[ | ||
currentUser, | ||
setUserMode, | ||
getGame, | ||
getPlayerSetterFromPlayerNumber, | ||
setStartDisabled, | ||
], | ||
); | ||
|
||
useEffect(() => { | ||
const socket = io("/pong", { | ||
query: { game_id: id, is_player: userMode === "player" }, | ||
forceNew: true, | ||
}); | ||
socketRef.current = socket; | ||
|
||
const game = getGame(); | ||
game.onAction = (action: string) => { | ||
socket.emit(action); | ||
}; | ||
|
||
const handleUpdateStatus = ({ | ||
status, | ||
payload, | ||
}: { | ||
status: Status; | ||
payload: any; | ||
}) => { | ||
runSideEffectForStatusUpdate(status, payload); | ||
const log = getLogFromStatus(status); | ||
setLogs((logs) => [...logs, log]); | ||
}; | ||
const handleConnect = () => { | ||
console.log(`Connected: ${socketRef.current?.id}`); | ||
const log = "Connected to server"; | ||
setLogs((logs) => [...logs, log]); | ||
}; | ||
|
||
const handleStart = (data: { vx: number; vy: number }) => { | ||
game.start(data); | ||
setStartDisabled(true); | ||
}; | ||
|
||
const handleRight = ({ playerNumber }: HandleActionProps) => { | ||
if (userMode !== "player" && playerNumber == 1) { | ||
game.movePlayer1Left(); | ||
} else { | ||
game.movePlayer2Left(); | ||
} | ||
}; | ||
|
||
const handleLeft = ({ playerNumber }: HandleActionProps) => { | ||
if (userMode !== "player" && playerNumber == 1) { | ||
game.movePlayer1Right(); | ||
} else { | ||
game.movePlayer2Right(); | ||
} | ||
}; | ||
|
||
const handleBounce = ({ playerNumber }: HandleActionProps) => { | ||
if (userMode !== "player" && playerNumber == 1) { | ||
game.bounceOffPaddlePlayer1(); | ||
} else { | ||
game.bounceOffPaddlePlayer2(); | ||
} | ||
}; | ||
|
||
const handleCollide = (msg: HandleActionProps) => { | ||
const { playerNumber } = msg; | ||
if (userMode === "player") { | ||
const score = game.increaseScorePlayer1(); | ||
if (score != POINT_TO_WIN) { | ||
setTimeout(() => start(), 1000); | ||
} | ||
} else { | ||
if (playerNumber == 1) { | ||
game.increaseScorePlayer2(); | ||
} else { | ||
game.increaseScorePlayer1(); | ||
} | ||
} | ||
game.endRound(); | ||
}; | ||
|
||
const handleFinish = () => { | ||
const game = getGame(); | ||
game.stop(); | ||
}; | ||
|
||
socket.on("connect", handleConnect); | ||
socket.on("start", handleStart); | ||
socket.on("right", handleRight); | ||
socket.on("left", handleLeft); | ||
socket.on("bounce", handleBounce); | ||
socket.on("collide", handleCollide); | ||
socket.on("update-status", handleUpdateStatus); | ||
socket.on("finish", handleFinish); | ||
|
||
return () => { | ||
socket.off("connect", handleConnect); | ||
socket.off("start", handleStart); | ||
socket.off("right", handleRight); | ||
socket.off("left", handleLeft); | ||
socket.off("bounce", handleBounce); | ||
socket.off("collide", handleCollide); | ||
socket.off("update-status", handleUpdateStatus); | ||
socket.off("finish", handleFinish); | ||
socket.disconnect(); | ||
}; | ||
}, [ | ||
id, | ||
getGame, | ||
setLogs, | ||
start, | ||
userMode, | ||
setUserMode, | ||
runSideEffectForStatusUpdate, | ||
setStartDisabled, | ||
]); | ||
|
||
return { socketRef, start }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { PongGame } from "@/app/pong/[id]/PongGame"; | ||
import { useEffect } from "react"; | ||
|
||
export default function useGameTheme( | ||
getGame: () => PongGame, | ||
resolvedTheme?: string, | ||
) { | ||
useEffect(() => { | ||
// TODO: Use --foreground color from CSS | ||
// Somehow it didn't work (theme is changed but not yet committed to CSS/DOM?) | ||
const game = getGame(); | ||
const color = | ||
resolvedTheme === "dark" ? "hsl(0, 0%, 100%)" : "hsl(0, 0%, 0%)"; | ||
game.setColor(color); | ||
game.draw_canvas(); | ||
}, [resolvedTheme, getGame]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { PongGame } from "@/app/pong/[id]/PongGame"; | ||
import { MutableRefObject, useCallback, useRef } from "react"; | ||
import { UserMode } from "../useUserMode"; | ||
import { DEFAULT_COLOR } from "@/app/pong/[id]/const"; | ||
|
||
export default function useGetGame( | ||
canvasRef: MutableRefObject<HTMLCanvasElement | null>, | ||
userMode: UserMode, | ||
) { | ||
const gameRef = useRef<PongGame | null>(null); // only initialized once | ||
|
||
const getGame = useCallback(() => { | ||
const ctx = canvasRef.current?.getContext("2d"); | ||
if (!ctx) { | ||
throw new Error("canvas not ready"); | ||
} | ||
if (!gameRef.current) { | ||
const game = new PongGame(ctx, DEFAULT_COLOR, DEFAULT_COLOR, userMode); | ||
gameRef.current = game; | ||
return game; | ||
} | ||
return gameRef.current; | ||
}, [canvasRef, userMode]); | ||
|
||
return { getGame }; | ||
} |
Oops, something went wrong.