- Removed onCellClick from TicTacToeGameProps and migrated move sending inside TicTacToeGame
- Updated TicTacToeGame to:
- import TicTacToePayload
- use movePayload() builder
- send moves using handleMove() with matchId + sendMatchData
- remove old matchId destructuring duplication
- Updated BattleshipGame to:
- import BattleshipPayload
- use placePayload() and shootPayload() helpers
- collapse place and shoot handlers into a single handleMove()
- send typed payloads instead of raw objects
- Updated App.tsx:
- Removed handleCellClick and no longer pass onCellClick down
- Created typed ticTacToeProps and battleshipProps without UI callbacks
- Cleaned unused state and simplified board rendering
- Use {...commonProps} to propagate shared game state
- Updated props:
- Removed TicTacToeGameProps.onCellClick
- BattleshipGameProps continues to extend GameProps
- Removed duplicate MatchDataModel definition from interfaces/models
- Fixed imports to use revised models and payload types
This refactor completes the transition from UI-triggered handlers to
typed action payloads per game, significantly improving type safety,
consistency, and separation of concerns.
133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
import React, { useMemo } from "react";
|
||
import { motion } from "framer-motion";
|
||
import { useNakama } from "../../providers/NakamaProvider";
|
||
|
||
import PlacementGrid from "./placement/PlacementGrid";
|
||
import ShotGrid from "./battle/ShotGrid";
|
||
import { BattleshipGameProps } from "./props";
|
||
import { BattleshipPayload } from "./models";
|
||
import {
|
||
placePayload,
|
||
shootPayload,
|
||
} from "./utils";
|
||
|
||
const Fleet: Record<string, number> = {
|
||
carrier: 5,
|
||
battleship: 4,
|
||
cruiser: 3,
|
||
submarine: 3,
|
||
destroyer: 2,
|
||
};
|
||
const FLEET_ORDER = ["carrier", "battleship", "cruiser", "submarine", "destroyer"];
|
||
|
||
export default function BattleshipGame({
|
||
boards,
|
||
players,
|
||
myUserId,
|
||
turn,
|
||
winner,
|
||
gameOver,
|
||
metadata,
|
||
}: BattleshipGameProps) {
|
||
const { sendMatchData, matchId } = useNakama();
|
||
|
||
const myIndex = players.findIndex((p) => p.user_id === myUserId);
|
||
const oppIndex = myIndex === 0 ? 1 : 0;
|
||
|
||
const phase = metadata["phase"] ?? "lobby";
|
||
const isMyTurn = phase === "battle" && turn === myIndex;
|
||
|
||
const myShips = boards[`p${myIndex}_ships`]?.grid ?? [[]];
|
||
const myShots = boards[`p${myIndex}_shots`] ?.grid ?? [[]];
|
||
|
||
const placed = metadata[`p${myIndex}_placed`] ?? 0;
|
||
|
||
const nextShip = FLEET_ORDER[placed] || null;
|
||
const nextShipSize = nextShip ? Fleet[nextShip] : null;
|
||
|
||
function handleMove(matchPayload: BattleshipPayload) {
|
||
if (!matchId) return;
|
||
|
||
sendMatchData(matchId!, 1, matchPayload);
|
||
}
|
||
|
||
// ------------------- STATUS LABEL -------------------
|
||
const status = useMemo(() => {
|
||
if (phase === "lobby") return `In Lobby`;
|
||
if (winner !== null) return `Winner: Player ${winner}`;
|
||
if (gameOver) return "Game over — draw";
|
||
if (phase === "placement") return `Place your ${nextShip ?? ""}`;
|
||
if (myIndex === -1) return "Spectating";
|
||
if (!isMyTurn) return "Opponent’s turn";
|
||
return "Your turn";
|
||
}, [winner, gameOver, phase, isMyTurn, myIndex, nextShip]);
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 6 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.25 }}
|
||
style={{ textAlign: "center" }}
|
||
>
|
||
<h2 style={{ marginBottom: 8 }}>{status}</h2>
|
||
|
||
{/* ---------------- PHASE 1: PLACEMENT ---------------- */}
|
||
{phase === "placement" && nextShip && (
|
||
<PlacementGrid
|
||
shipBoard={myShips}
|
||
shipName={nextShip}
|
||
shipSize={nextShipSize}
|
||
onPlace={(
|
||
s,r,c,d
|
||
) => handleMove(
|
||
placePayload(s,r,c,d)
|
||
)}
|
||
/>
|
||
)}
|
||
|
||
{/* ---------------- PHASE 2: BATTLE ---------------- */}
|
||
{phase === "battle" && (
|
||
<>
|
||
<h3>Your Shots</h3>
|
||
|
||
<ShotGrid
|
||
grid={myShots}
|
||
isMyTurn={isMyTurn}
|
||
gameOver={!!gameOver}
|
||
onShoot={(
|
||
r,c
|
||
) => handleMove(
|
||
shootPayload(r,c)
|
||
)}
|
||
/>
|
||
|
||
<h3 style={{ marginTop: "18px" }}>Your Ships</h3>
|
||
<PlacementGrid
|
||
shipBoard={myShips}
|
||
shipName="readonly"
|
||
shipSize={0}
|
||
onPlace={() => {}}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{/* ---------------- WINNER UI ---------------- */}
|
||
{winner !== null && (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1, scale: [1, 1.05, 1] }}
|
||
transition={{ repeat: Infinity, duration: 1.4 }}
|
||
style={{
|
||
marginTop: 12,
|
||
fontSize: "20px",
|
||
fontWeight: "bold",
|
||
color: "#f1c40f",
|
||
}}
|
||
>
|
||
🎉 Player {winner} Wins! 🎉
|
||
</motion.div>
|
||
)}
|
||
</motion.div>
|
||
);
|
||
}
|