Compare commits
6 Commits
650d7b7ed6
...
b00519347a
| Author | SHA1 | Date | |
|---|---|---|---|
| b00519347a | |||
| 8436cdbcdd | |||
| 135fdd332d | |||
| 8dc41fca2c | |||
| fc7cb8efb6 | |||
| 06bdc92190 |
115
src/App.tsx
115
src/App.tsx
@@ -1,57 +1,72 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useNakama } from "./providers/NakamaProvider";
|
import { useNakama } from "./providers/NakamaProvider";
|
||||||
import Player from "./Player";
|
|
||||||
import { PlayerModel } from "./interfaces/models";
|
|
||||||
|
|
||||||
import TicTacToeBoard from "./games/tictactoe/TicTacToeBoard";
|
import Player from "./Player";
|
||||||
import BattleShipBoard from "./games/battleship/BattleShipBoard";
|
import TicTacToeGame from "./games/tictactoe/TicTacToeGame";
|
||||||
|
import { TicTacToeGameProps } from "./games/tictactoe/props";
|
||||||
|
import BattleshipGame from "./games/battleship/BattleshipGame"
|
||||||
|
import { BattleshipGameProps } from "./games/battleship/props";
|
||||||
|
|
||||||
|
import { GameState } from "./interfaces/states";
|
||||||
|
import { GameProps } from "./interfaces/props";
|
||||||
|
|
||||||
|
const INITIAL_GAME_STATE: GameState = {
|
||||||
|
boards: {},
|
||||||
|
turn: 0,
|
||||||
|
winner: null,
|
||||||
|
gameOver: false,
|
||||||
|
players: [],
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
// setting up a 2D game boards
|
// unified game state
|
||||||
const [boards, setBoards] = useState<Record<string, { grid: string[][] }>>({});
|
const [game, setGame] = useState<GameState>(INITIAL_GAME_STATE);
|
||||||
const [turn, setTurn] = useState<number>(0);
|
const { onMatchData, matchId, session } = useNakama();
|
||||||
const [winner, setWinner] = useState<string | null>(null);
|
|
||||||
const [gameOver, setGameOver] = useState<boolean | null>(null);
|
|
||||||
const [players, setPlayers] = useState<PlayerModel[]>([]);
|
|
||||||
const [metadata, setMetadata] = useState<Record<string, any>>({});
|
|
||||||
|
|
||||||
const { sendMatchData, onMatchData, matchId, session } = useNakama();
|
const commonProps: GameProps = {
|
||||||
|
boards: game.boards,
|
||||||
|
turn: game.turn,
|
||||||
|
winner: game.winner,
|
||||||
|
gameOver: game.gameOver,
|
||||||
|
players: game.players,
|
||||||
|
myUserId: session?.user_id ?? null,
|
||||||
|
};
|
||||||
|
const ticTacToeProps: TicTacToeGameProps = {
|
||||||
|
...commonProps,
|
||||||
|
};
|
||||||
|
const battleshipProps: BattleshipGameProps = {
|
||||||
|
...commonProps,
|
||||||
|
metadata: game.metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------
|
||||||
|
// RENDER GAME BOARD
|
||||||
|
// ---------------------------------------------------
|
||||||
function renderGameBoard() {
|
function renderGameBoard() {
|
||||||
if (!matchId || !metadata?.game) return null;
|
if (!matchId || !game.metadata?.game) return null;
|
||||||
|
|
||||||
switch (metadata.game) {
|
switch (game.metadata.game) {
|
||||||
case "tictactoe":
|
case "tictactoe":
|
||||||
return (
|
return (
|
||||||
<TicTacToeBoard
|
<TicTacToeGame
|
||||||
boards={boards}
|
{...ticTacToeProps}
|
||||||
turn={turn}
|
|
||||||
winner={winner}
|
|
||||||
gameOver={gameOver}
|
|
||||||
players={players}
|
|
||||||
myUserId={session?.user_id ?? null}
|
|
||||||
onCellClick={handleCellClick}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "battleship":
|
case "battleship":
|
||||||
return (
|
return (
|
||||||
<BattleShipBoard
|
<BattleshipGame
|
||||||
boards={boards}
|
{...battleshipProps}
|
||||||
turn={turn}
|
|
||||||
winner={winner}
|
|
||||||
gameOver={gameOver}
|
|
||||||
players={players}
|
|
||||||
myUserId={session?.user_id ?? null}
|
|
||||||
metadata={metadata}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <div>Unknown game: {metadata.game}</div>;
|
return <div>Unknown game: {game.metadata.game}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
// MATCH DATA CALLBACK (from Player component)
|
// MATCH DATA CALLBACK (from Player component)
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
@@ -62,23 +77,21 @@ export default function App() {
|
|||||||
const state = msg.data;
|
const state = msg.data;
|
||||||
console.log("Match state:", state);
|
console.log("Match state:", state);
|
||||||
|
|
||||||
setBoards(state.boards);
|
setGame({
|
||||||
setTurn(state.turn);
|
boards: state.boards,
|
||||||
setGameOver(state.game_over);
|
turn: state.turn,
|
||||||
if (state.winner >= 0) {
|
gameOver: state.game_over,
|
||||||
setWinner(state.players[state.winner].username);
|
winner:
|
||||||
// } else if (state.game_over) {
|
state.winner >= 0 ? state.players[state.winner].username : null,
|
||||||
// // Game ended but winner = -1 → draw
|
players: state.players ?? [],
|
||||||
// setWinner("draw");
|
metadata: state.metadata ?? {},
|
||||||
} else {
|
});
|
||||||
// Ongoing game, no winner
|
|
||||||
setWinner(null);
|
|
||||||
}
|
|
||||||
setPlayers(state.players || []);
|
|
||||||
setMetadata(state.metadata || {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------
|
||||||
|
// EFFECTS
|
||||||
|
// ---------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
return () => {
|
return () => {
|
||||||
@@ -90,15 +103,9 @@ export default function App() {
|
|||||||
onMatchData(onMatchDataCallback);
|
onMatchData(onMatchDataCallback);
|
||||||
}, [onMatchData]);
|
}, [onMatchData]);
|
||||||
|
|
||||||
// ------------------------------------------
|
// ---------------------------------------------------
|
||||||
// SEND A MOVE
|
// UI LAYOUT
|
||||||
// ------------------------------------------
|
// ---------------------------------------------------
|
||||||
function handleCellClick(row: number, col: number) {
|
|
||||||
if (!matchId) return;
|
|
||||||
|
|
||||||
sendMatchData(matchId, 1, {data: {row, col}});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { useNakama } from "./providers/NakamaProvider";
|
import { useNakama } from "./providers/NakamaProvider";
|
||||||
|
import { PlayerProps } from "./interfaces/props";
|
||||||
|
|
||||||
export default function Player({
|
export default function Player({
|
||||||
onMatchDataCallback,
|
onMatchDataCallback,
|
||||||
}: {
|
}: PlayerProps) {
|
||||||
onMatchDataCallback: (msg: any) => void;
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
session,
|
session,
|
||||||
matchId,
|
matchId,
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useNakama } from "../../providers/NakamaProvider";
|
import { useNakama } from "../../providers/NakamaProvider";
|
||||||
import { PlayerModel } from "../../models/player";
|
|
||||||
|
|
||||||
import PlacementGrid from "./placement/PlacementGrid";
|
import PlacementGrid from "./placement/PlacementGrid";
|
||||||
import ShotGrid from "./battle/ShotGrid";
|
import ShotGrid from "./battle/ShotGrid";
|
||||||
|
import { BattleshipGameProps } from "./props";
|
||||||
interface BattleBoardProps {
|
import { BattleshipPayload } from "./models";
|
||||||
boards: Record<string, { grid: string[][] }>;
|
import {
|
||||||
players: PlayerModel[];
|
placePayload,
|
||||||
myUserId: string | null;
|
shootPayload,
|
||||||
turn: number;
|
} from "./utils";
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean | null;
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Fleet: Record<string, number> = {
|
const Fleet: Record<string, number> = {
|
||||||
carrier: 5,
|
carrier: 5,
|
||||||
@@ -25,7 +20,7 @@ const Fleet: Record<string, number> = {
|
|||||||
};
|
};
|
||||||
const FLEET_ORDER = ["carrier", "battleship", "cruiser", "submarine", "destroyer"];
|
const FLEET_ORDER = ["carrier", "battleship", "cruiser", "submarine", "destroyer"];
|
||||||
|
|
||||||
export default function BattleShipBoard({
|
export default function BattleshipGame({
|
||||||
boards,
|
boards,
|
||||||
players,
|
players,
|
||||||
myUserId,
|
myUserId,
|
||||||
@@ -33,7 +28,7 @@ export default function BattleShipBoard({
|
|||||||
winner,
|
winner,
|
||||||
gameOver,
|
gameOver,
|
||||||
metadata,
|
metadata,
|
||||||
}: BattleBoardProps) {
|
}: BattleshipGameProps) {
|
||||||
const { sendMatchData, matchId } = useNakama();
|
const { sendMatchData, matchId } = useNakama();
|
||||||
|
|
||||||
const myIndex = players.findIndex((p) => p.user_id === myUserId);
|
const myIndex = players.findIndex((p) => p.user_id === myUserId);
|
||||||
@@ -50,28 +45,10 @@ export default function BattleShipBoard({
|
|||||||
const nextShip = FLEET_ORDER[placed] || null;
|
const nextShip = FLEET_ORDER[placed] || null;
|
||||||
const nextShipSize = nextShip ? Fleet[nextShip] : null;
|
const nextShipSize = nextShip ? Fleet[nextShip] : null;
|
||||||
|
|
||||||
// ------------------- PLACE SHIP -------------------
|
function handleMove(matchPayload: BattleshipPayload) {
|
||||||
function handlePlace(ship: string, r: number, c: number, dir: "h" | "v") {
|
if (!matchId) return;
|
||||||
sendMatchData(matchId!, 1, {
|
|
||||||
action: "place",
|
|
||||||
data: {
|
|
||||||
ship: ship,
|
|
||||||
row: r,
|
|
||||||
col: c,
|
|
||||||
dir,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------- SHOOT -------------------
|
sendMatchData(matchId!, 1, matchPayload);
|
||||||
function handleShoot(r: number, c: number) {
|
|
||||||
sendMatchData(matchId!, 1, {
|
|
||||||
action: "shoot",
|
|
||||||
data: {
|
|
||||||
row: r,
|
|
||||||
col: c,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------- STATUS LABEL -------------------
|
// ------------------- STATUS LABEL -------------------
|
||||||
@@ -100,7 +77,11 @@ export default function BattleShipBoard({
|
|||||||
shipBoard={myShips}
|
shipBoard={myShips}
|
||||||
shipName={nextShip}
|
shipName={nextShip}
|
||||||
shipSize={nextShipSize}
|
shipSize={nextShipSize}
|
||||||
onPlace={handlePlace}
|
onPlace={(
|
||||||
|
s,r,c,d
|
||||||
|
) => handleMove(
|
||||||
|
placePayload(s,r,c,d)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -113,7 +94,11 @@ export default function BattleShipBoard({
|
|||||||
grid={myShots}
|
grid={myShots}
|
||||||
isMyTurn={isMyTurn}
|
isMyTurn={isMyTurn}
|
||||||
gameOver={!!gameOver}
|
gameOver={!!gameOver}
|
||||||
onShoot={handleShoot}
|
onShoot={(
|
||||||
|
r,c
|
||||||
|
) => handleMove(
|
||||||
|
shootPayload(r,c)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 style={{ marginTop: "18px" }}>Your Ships</h3>
|
<h3 style={{ marginTop: "18px" }}>Your Ships</h3>
|
||||||
15
src/games/battleship/models.ts
Normal file
15
src/games/battleship/models.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
MatchDataModel,
|
||||||
|
} from '../../interfaces/models'
|
||||||
|
|
||||||
|
export interface BattleshipPayload {
|
||||||
|
action: "place" | "shoot"; // extend as needed
|
||||||
|
data: {
|
||||||
|
ship?: string; // only for placement
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
dir?: "h" | "v";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BattleshipMatchDataModel = MatchDataModel<BattleshipPayload>;
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Board,
|
GameProps,
|
||||||
PlayerModel,
|
} from '../../interfaces/props'
|
||||||
} from '../../interfaces/models'
|
|
||||||
|
|
||||||
|
|
||||||
export interface BattleShipBoardProps {
|
export interface BattleshipGameProps extends GameProps {
|
||||||
boards: Record<string, Board>;
|
|
||||||
turn: number;
|
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean | null;
|
|
||||||
players: PlayerModel[];
|
|
||||||
myUserId: string | null;
|
|
||||||
metadata: Record<string, any>;
|
metadata: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/games/battleship/utils.ts
Normal file
22
src/games/battleship/utils.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
BattleshipPayload
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
export function placePayload(
|
||||||
|
ship: string,
|
||||||
|
row: number,
|
||||||
|
col: number,
|
||||||
|
dir: "h" | "v"
|
||||||
|
): BattleshipPayload {
|
||||||
|
return {
|
||||||
|
action: "place",
|
||||||
|
data: { ship, row, col, dir }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shootPayload(row: number, col: number): BattleshipPayload {
|
||||||
|
return {
|
||||||
|
action: "shoot",
|
||||||
|
data: { row, col }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,32 +2,23 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { useNakama } from "../../providers/NakamaProvider";
|
import { useNakama } from "../../providers/NakamaProvider";
|
||||||
import getHaiku from "../../utils/haikus";
|
import getHaiku from "../../utils/haikus";
|
||||||
import { PlayerModel } from "../../models/player";
|
|
||||||
|
|
||||||
interface BoardProps {
|
import { TicTacToeGameProps } from "./props";
|
||||||
boards: Record<string, { grid: string[][] }>;
|
import { TicTacToePayload } from "./models";
|
||||||
turn: number;
|
import { movePayload } from "./utils";
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean | null;
|
|
||||||
players: PlayerModel[];
|
|
||||||
myUserId: string | null;
|
|
||||||
onCellClick: (row: number, col: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TicTacToeBoard({
|
export default function TicTacToeGame({
|
||||||
boards,
|
boards,
|
||||||
turn,
|
turn,
|
||||||
winner,
|
winner,
|
||||||
gameOver,
|
gameOver,
|
||||||
players,
|
players,
|
||||||
myUserId,
|
myUserId,
|
||||||
onCellClick,
|
}: TicTacToeGameProps) {
|
||||||
}: BoardProps) {
|
const { sendMatchData, matchId } = useNakama();
|
||||||
|
|
||||||
const myIndex = players.findIndex(p => p.user_id === myUserId);
|
const myIndex = players.findIndex(p => p.user_id === myUserId);
|
||||||
const gameReady = players.length === 2;
|
const gameReady = players.length === 2;
|
||||||
const {
|
|
||||||
matchId
|
|
||||||
} = useNakama();
|
|
||||||
|
|
||||||
const mySymbol =
|
const mySymbol =
|
||||||
myIndex !== null && players[myIndex]
|
myIndex !== null && players[myIndex]
|
||||||
@@ -76,6 +67,12 @@ export default function TicTacToeBoard({
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [haikuIndex]);
|
}, [haikuIndex]);
|
||||||
|
|
||||||
|
function handleMove(matchPayload: TicTacToePayload) {
|
||||||
|
if (!matchId) return;
|
||||||
|
|
||||||
|
sendMatchData(matchId!, 1, matchPayload);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{matchId && (
|
{matchId && (
|
||||||
@@ -141,7 +138,9 @@ export default function TicTacToeBoard({
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
whileTap={!disabled ? { scale: 0.85 } : {}}
|
whileTap={!disabled ? { scale: 0.85 } : {}}
|
||||||
onClick={() => !disabled && onCellClick(rIdx, cIdx)}
|
onClick={() => !disabled && handleMove(
|
||||||
|
movePayload(rIdx, cIdx)
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: "80px",
|
width: "80px",
|
||||||
height: "80px",
|
height: "80px",
|
||||||
11
src/games/tictactoe/models.ts
Normal file
11
src/games/tictactoe/models.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
MatchDataModel,
|
||||||
|
} from '../../interfaces/models'
|
||||||
|
|
||||||
|
export interface TicTacToePayload {
|
||||||
|
data: {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type TicTacToeMatchDataModel = MatchDataModel<TicTacToePayload>;
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Board,
|
GameProps,
|
||||||
PlayerModel,
|
} from '../../interfaces/props'
|
||||||
} from '../../interfaces/models'
|
|
||||||
|
|
||||||
|
|
||||||
export interface TicTacToeBoardProps {
|
export interface TicTacToeGameProps extends GameProps {
|
||||||
boards: Record<string, Board>;
|
// metadata: Record<string, any>;
|
||||||
turn: number;
|
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean | null;
|
|
||||||
players: PlayerModel[];
|
|
||||||
myUserId: string | null;
|
|
||||||
onCellClick: (row: number, col: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/games/tictactoe/utils.ts
Normal file
12
src/games/tictactoe/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
TicTacToePayload
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
export function movePayload(
|
||||||
|
row: number,
|
||||||
|
col: number,
|
||||||
|
): TicTacToePayload {
|
||||||
|
return {
|
||||||
|
data: { row, col }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from "@heroiclabs/nakama-js/dist/api.gen"
|
} from "@heroiclabs/nakama-js/dist/api.gen"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GameMetadata,
|
GameMetadataModel,
|
||||||
MatchDataMessage,
|
MatchDataModel,
|
||||||
} from './models'
|
} from './models'
|
||||||
|
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ export interface NakamaContextType {
|
|||||||
loginOrRegister(username?: string): Promise<void>;
|
loginOrRegister(username?: string): Promise<void>;
|
||||||
logout(): Promise<void>;
|
logout(): Promise<void>;
|
||||||
|
|
||||||
joinMatchmaker(gameMetadata: GameMetadata): Promise<string>;
|
joinMatchmaker(gameMetadata: GameMetadataModel): Promise<string>;
|
||||||
exitMatchmaker(gameMetadata: GameMetadata): Promise<void>;
|
exitMatchmaker(gameMetadata: GameMetadataModel): Promise<void>;
|
||||||
joinMatch(matchId: string): Promise<void>;
|
joinMatch(matchId: string): Promise<void>;
|
||||||
|
|
||||||
sendMatchData(matchId: string, op: number, data: object): void;
|
sendMatchData(matchId: string, op: number, data: object): void;
|
||||||
|
|
||||||
onMatchData(cb: (msg: MatchDataMessage) => void): void;
|
onMatchData(cb: (msg: MatchDataModel) => void): void;
|
||||||
|
|
||||||
getLeaderboardTop(): Promise<ApiLeaderboardRecordList>;
|
getLeaderboardTop(): Promise<ApiLeaderboardRecordList>;
|
||||||
listOpenMatches(): Promise<ApiMatch[]>;
|
listOpenMatches(): Promise<ApiMatch[]>;
|
||||||
|
|||||||
@@ -5,32 +5,17 @@ export interface PlayerModel {
|
|||||||
metadata: Record<string, string>; // e.g. { symbol: "X" }
|
metadata: Record<string, string>; // e.g. { symbol: "X" }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchDataMessage<T = any> {
|
export interface MatchDataModel<T = any> {
|
||||||
opCode: number;
|
opCode: number;
|
||||||
data: T;
|
data: T;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Board {
|
export interface BoardModel {
|
||||||
grid: string[][];
|
grid: string[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameMetadataModel {
|
||||||
boards: Record<string, Board>;
|
|
||||||
turn: number;
|
|
||||||
winner: string | null;
|
|
||||||
gameOver: boolean;
|
|
||||||
players: PlayerModel[];
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameMetadata {
|
|
||||||
game: string;
|
game: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchDataMessage<T = any> {
|
|
||||||
opCode: number;
|
|
||||||
data: T;
|
|
||||||
userId: string | null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
MatchDataMessage,
|
MatchDataModel,
|
||||||
} from './models'
|
} from './models'
|
||||||
|
|
||||||
|
import {
|
||||||
|
GameState
|
||||||
|
} from "./states";
|
||||||
|
|
||||||
export interface PlayerProps {
|
export interface PlayerProps {
|
||||||
onMatchDataCallback: (msg:MatchDataMessage) => void;
|
onMatchDataCallback: (msg:MatchDataModel) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameProps
|
||||||
|
extends Pick<
|
||||||
|
GameState,
|
||||||
|
"boards" | "turn" | "winner" | "gameOver" | "players"
|
||||||
|
> {
|
||||||
|
myUserId: string | null;
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
} from "@heroiclabs/nakama-js";
|
} from "@heroiclabs/nakama-js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GameMetadata,
|
GameMetadataModel,
|
||||||
} from './models'
|
} from './models'
|
||||||
|
|
||||||
|
|
||||||
export interface NakamaRefs {
|
export interface NakamaRefs {
|
||||||
socketRef: React.RefObject<Socket | null>;
|
socketRef: React.RefObject<Socket | null>;
|
||||||
gameMetadataRef: React.RefObject<GameMetadata | null>;
|
gameMetadataRef: React.RefObject<GameMetadataModel | null>;
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,24 @@ import {
|
|||||||
Socket
|
Socket
|
||||||
} from "@heroiclabs/nakama-js";
|
} from "@heroiclabs/nakama-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoardModel,
|
||||||
|
PlayerModel,
|
||||||
|
} from "./models"
|
||||||
|
|
||||||
|
|
||||||
export interface NakamaProviderState {
|
export interface NakamaProviderState {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
matchId: string | null;
|
matchId: string | null;
|
||||||
matchmakerTicket: string | null;
|
matchmakerTicket: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
boards: Record<string, BoardModel>;
|
||||||
|
turn: number;
|
||||||
|
winner: string | null;
|
||||||
|
gameOver: boolean;
|
||||||
|
players: PlayerModel[];
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { NakamaContextType } from "../interfaces/contexts";
|
import { NakamaContextType } from "../interfaces/contexts";
|
||||||
import { NakamaRefs } from "../interfaces/refs";
|
import { NakamaRefs } from "../interfaces/refs";
|
||||||
import { NakamaProviderState } from "../interfaces/states";
|
import { NakamaProviderState } from "../interfaces/states";
|
||||||
import { GameMetadata, MatchDataMessage } from "../interfaces/models";
|
import { GameMetadataModel, MatchDataModel } from "../interfaces/models";
|
||||||
|
|
||||||
function getOrCreateDeviceId(): string {
|
function getOrCreateDeviceId(): string {
|
||||||
const key = "nakama.deviceId";
|
const key = "nakama.deviceId";
|
||||||
@@ -68,7 +68,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// --------------------------------------
|
// --------------------------------------
|
||||||
const refs: NakamaRefs = {
|
const refs: NakamaRefs = {
|
||||||
socketRef: useRef<Socket | null>(null),
|
socketRef: useRef<Socket | null>(null),
|
||||||
gameMetadataRef: useRef<GameMetadata | null>(null),
|
gameMetadataRef: useRef<GameMetadataModel | null>(null),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helpers to update internal state cleanly
|
// Helpers to update internal state cleanly
|
||||||
@@ -202,7 +202,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// MATCHMAKING
|
// MATCHMAKING
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
async function joinMatchmaker(gameMetadata: GameMetadata) {
|
async function joinMatchmaker(gameMetadata: GameMetadataModel) {
|
||||||
const socket = refs.socketRef.current;
|
const socket = refs.socketRef.current;
|
||||||
if (!socket) throw new Error("Socket missing");
|
if (!socket) throw new Error("Socket missing");
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// MATCH DATA LISTENER
|
// MATCH DATA LISTENER
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
function onMatchData(cb: (msg: MatchDataMessage) => void) {
|
function onMatchData(cb: (msg: MatchDataModel) => void) {
|
||||||
if (!internal.socket) return;
|
if (!internal.socket) return;
|
||||||
|
|
||||||
internal.socket.onmatchdata = (m: MatchData) => {
|
internal.socket.onmatchdata = (m: MatchData) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user