Merge pull request #31 from jhalitaksoy/feature/spectator

Feature/spectator
This commit is contained in:
Halit Aksoy 2022-09-03 19:06:41 +03:00 committed by GitHub
commit f5b1a6f9b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 326 additions and 245 deletions

View File

@ -15,8 +15,10 @@
"dependencies": {
"@szhsin/react-menu": "^3.0.2",
"@types/": "szhsin/react-menu",
"@types/eventemitter2": "^4.1.0",
"@types/styled-jsx": "^3.4.4",
"@types/uuid": "^8.3.4",
"eventemitter2": "^6.4.7",
"mancala.js": "^0.0.2-beta.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -29,35 +29,32 @@ const MancalaApp: FunctionComponent = () => {
setConnetionState("connected");
};
const onConnectionLost = () => {
connectToServer("reconnecting");
setConnetionState("reconnecting");
connectToServer();
};
const onConnectionError = (event: Event) => {
setConnetionState("error");
connectToServer();
};
const connectToServer = async (connectionState: ConnectionState) => {
setConnetionState(connectionState);
const onThemeChange = (theme: Theme) => {
setTheme(theme)
}
const connectToServer = async () => {
const userKey = await context.userKeyStore.getUserKey();
setUserKey(userKey);
const rtmtws = context.rtmt as RTMTWS;
if (rtmtws) {
rtmtws.initWebSocket(
userKey,
onConnectionDone,
onConnectionLost,
onConnectionError
);
} else {
console.error("context.rtmt is not RTMTWS");
}
(context.rtmt as RTMTWS).initWebSocket(userKey);
};
React.useEffect(() => {
connectToServer("connecting");
context.themeManager.onThemeChange = (theme: Theme) => {
setTheme(theme);
}
context.rtmt.on("open", onConnectionDone);
context.rtmt.on("close", onConnectionLost);
context.rtmt.on("error", onConnectionError);
context.themeManager.on("themechange", onThemeChange);
setConnetionState("connecting");
connectToServer();
return () => {
// todo: dispose rtmt.dispose
//context.rtmt.dispose();
context.rtmt.dispose();
context.themeManager.off("themechange", onThemeChange);
};
}, []);
@ -77,6 +74,7 @@ const MancalaApp: FunctionComponent = () => {
context.themeManager.theme.textColor,
context.themeManager.theme.textLightColor
);
if (!userKey) return <></>;
return (
<>
<BrowserRouter>

View File

@ -2,70 +2,83 @@ import * as React from "react";
import { FunctionComponent } from "react";
import { Context } from "../context/context";
import { Game } from "../models/Game";
import { User } from "../models/User";
import { getColorByBrightness } from "../util/ColorUtil";
import CircularPanel from "./CircularPanel";
function getInfoPanelTextByGameState(params: {
context: Context;
game?: Game;
crashMessage?: string;
userKey?: string;
userKeyWhoLeave?: string;
currentUser: User;
whitePlayer: User;
blackPlayer: User;
leftPlayer?: User;
isSpectator?: boolean;
}): string | undefined {
const {
context,
game,
crashMessage,
userKey,
userKeyWhoLeave,
currentUser,
whitePlayer,
blackPlayer,
leftPlayer,
isSpectator
} = params;
if (crashMessage) {
return context.texts.GameCrashed + " " + crashMessage;
} else if (userKeyWhoLeave) {
let message = context.texts.OpponentLeavesTheGame;
if (userKeyWhoLeave == userKey) {
message = context.texts.YouLeftTheGame;
if (leftPlayer) {
return isSpectator ? `${leftPlayer.name} ${context.texts.UserLeftTheGame}` :
leftPlayer.id == currentUser.id ? context.texts.YouLeftTheGame : context.texts.OpponentLeftTheGame;
}
return message;
} else if (game?.mancalaGame.state == "ended") {
const wonPlayer = game.mancalaGame.getWonPlayerId();
let whoWon =
game.mancalaGame.getWonPlayerId() === userKey
const isGameEnded = game?.mancalaGame.state == "ended";
if (isGameEnded) {
const wonPlayerID = game.mancalaGame.getWonPlayerId();
const wonPlayer = wonPlayerID == whitePlayer.id ? whitePlayer : blackPlayer;
let whoWon;
if (wonPlayer) {
whoWon = isSpectator ? `${wonPlayer.name} ${context.texts.Won}` :
game.mancalaGame.getWonPlayerId() === currentUser.id
? context.texts.YouWon
: context.texts.YouLost;
if (!wonPlayer) {
} else {
whoWon = context.texts.GameDraw;
}
return context.texts.GameEnded + " " + whoWon;
} else {
}
if (game) {
return userKey ? game.mancalaGame.checkIsPlayerTurn(userKey)
const playingPlayer = game.mancalaGame.checkIsPlayerTurn(whitePlayer.id) ? whitePlayer : blackPlayer;
return isSpectator ? `${playingPlayer.name} ${context.texts.Playing}` : game.mancalaGame.checkIsPlayerTurn(currentUser.id)
? context.texts.YourTurn
: context.texts.OpponentTurn : undefined;
}
: context.texts.OpponentTurn;
}
return undefined;
}
const InfoPanel: FunctionComponent<{
context: Context;
game?: Game;
crashMessage?: string;
userKey?: string;
userKeyWhoLeave?: string;
currentUser: User;
whitePlayer: User;
blackPlayer: User;
leftPlayer?: User;
style?: React.CSSProperties;
visible?: boolean;
isSpectator?: boolean;
}> = ({
context,
game,
crashMessage,
userKey,
userKeyWhoLeave,
currentUser,
whitePlayer,
blackPlayer,
leftPlayer,
style,
visible
visible,
isSpectator
}) => {
if (visible === false) return <></>;
const isUserTurn = userKey ? game?.mancalaGame.checkIsPlayerTurn(userKey) : false;
const isUserTurn = currentUser.id ? game?.mancalaGame.checkIsPlayerTurn(currentUser.id) : false;
const containerColor = isUserTurn
? context.themeManager.theme.playerTurnColor
: context.themeManager.theme.boardColor;
@ -77,9 +90,11 @@ const InfoPanel: FunctionComponent<{
const text = getInfoPanelTextByGameState({
context,
game,
crashMessage,
userKey,
userKeyWhoLeave
currentUser,
whitePlayer,
blackPlayer,
leftPlayer,
isSpectator
});
if (text) {
return (

View File

@ -13,49 +13,39 @@ const BoardView: FunctionComponent<{
context: Context;
boardId: string;
boardViewModel: BoardViewModel;
userKey: string;
revert: boolean;
onPitSelect: (index: number, pit: Pit) => void;
}> = ({ game, context, boardId, boardViewModel, userKey, onPitSelect: onPitSelect }) => {
}> = ({ game, context, boardId, boardViewModel, revert, onPitSelect: onPitSelect }) => {
const mancalaGame = game?.mancalaGame;
const createPitView = (key: any, pitViewModel: PitViewModel, onClick: () => void) => {
return <PitView key={key} pitViewModel={pitViewModel} onClick={onClick} />;
};
const player1Pits = mancalaGame?.board.player1Pits.map((pit, index) => {
const pitViewModel = boardViewModel.pits[pit.index];
return createPitView(index, pitViewModel, () => {
if (mancalaGame?.turnPlayerId === mancalaGame?.player1Id && userKey === mancalaGame?.player1Id)
onPitSelect(mancalaGame?.board.player1Pits.indexOf(pit), pit);
});
});
const player2Pits = mancalaGame?.board.player2Pits.map((pit, index) => {
const pitViewModel = boardViewModel.pits[pit.index];
return createPitView(index, pitViewModel, () => {
if (mancalaGame?.turnPlayerId === mancalaGame?.player2Id && userKey === mancalaGame?.player2Id)
onPitSelect(mancalaGame?.board.player2Pits.indexOf(pit), pit);
});
});
const theme = context.themeManager.theme;
const player1BankViewModel =
boardViewModel.pits[mancalaGame?.board.player1BankIndex()];
const player2BankViewModel =
boardViewModel.pits[mancalaGame?.board.player2BankIndex()];
const isPlayer2 = userKey === mancalaGame?.player2Id;
const createPitView = (key: any, pit: Pit, pitViewModel: PitViewModel) => {
return <PitView key={key} pitViewModel={pitViewModel} onClick={() => onPitSelect(pit.index, pit)} />;
};
const createPitViewList = (pits: Pit[]) => pits.map((pit, index) => createPitView(index, pit, boardViewModel.pits[pit.index]));
const player1Pits = createPitViewList(mancalaGame?.board.player1Pits);
const player2Pits = createPitViewList(mancalaGame?.board.player2Pits);
const player1BankIndex = mancalaGame?.board.player1BankIndex();
const player2BankIndex = mancalaGame?.board.player2BankIndex();
const player1BankViewModel = boardViewModel.pits[player1BankIndex];
const player2BankViewModel = boardViewModel.pits[player2BankIndex];
return (
<div className="board" style={{ background: theme.boardColor }}>
<StoreView
context={context}
pitViewModel={isPlayer2 ? player2BankViewModel : player1BankViewModel}
pitViewModel={revert ? player2BankViewModel : player1BankViewModel}
gridColumn="8 / 9"
gridRow="1 / 3"
/>
<StoreView
context={context}
pitViewModel={isPlayer2 ? player1BankViewModel : player2BankViewModel}
pitViewModel={revert ? player1BankViewModel : player2BankViewModel}
gridColumn="1 / 2"
gridRow="1 / 3"
/>
{isPlayer2 ? player1Pits?.reverse() : player2Pits?.reverse()}
{isPlayer2 ? player2Pits : player1Pits}
{revert ? player1Pits?.reverse() : player2Pits?.reverse()}
{revert ? player2Pits : player1Pits}
<style jsx>{`
.board {
padding: 2vw;

View File

@ -9,3 +9,5 @@ export const channel_on_game_user_leave = "on_game_user_leave"
export const channel_ping = "ping"
export const channel_pong = "pong"
export const channel_on_user_connection_change = "channel_on_user_connection_change"
export const channel_listen_game_events = "channel_listen_game_events"
export const channel_unlisten_game_events = "channel_unlisten_game_events"

View File

@ -8,6 +8,7 @@ export type Texts = {
GameEnded: string,
GameCrashed: string,
YouWon: string,
Won: string,
YouLost: string,
Connecting: string,
Connected: string,
@ -16,14 +17,16 @@ export type Texts = {
ConnectingAgain: string,
ServerError: string,
SearchingOpponet: string,
OpponentLeavesTheGame: string,
OpponentLeftTheGame: string,
YouLeftTheGame: string,
UserLeftTheGame: string,
SearchingOpponent: string,
PleaseWait : string,
GameDraw : string,
Anonymous: string,
GameNotFound: string,
Loading: string,
Playing: string,
}
export const EnUs: Texts = {
@ -35,6 +38,7 @@ export const EnUs: Texts = {
GameEnded: "Game Ended",
GameCrashed: "Game Crashed",
YouWon: "You Won",
Won: "Won",
YouLost: "You Lost",
Connecting: "Connecting",
Connected: "Connected",
@ -43,14 +47,16 @@ export const EnUs: Texts = {
ConnectingAgain: "Connecting Again",
ServerError: "Server Error",
SearchingOpponet: "Searching Opponet",
OpponentLeavesTheGame: "Opponent Leaves The Game",
OpponentLeftTheGame: "Opponent Leaves The Game",
YouLeftTheGame: "You Left The Game",
UserLeftTheGame: "Left The Game",
SearchingOpponent: "Searching Opponent",
PleaseWait : "Please Wait",
GameDraw : "Draw",
Anonymous: "Anonymous",
GameNotFound: "Game Not Found",
Loading: "Loading",
Playing: "Playing",
}
export const TrTr: Texts = {
@ -62,6 +68,7 @@ export const TrTr: Texts = {
GameEnded: "Oyun Bitti",
GameCrashed: "Oyunda Hata Oluştu",
YouWon: "Kazandın",
Won: "Kazandı",
YouLost: "Kaybettin",
Connecting: "Bağlanılıyor",
Connected: "Bağlandı",
@ -70,12 +77,14 @@ export const TrTr: Texts = {
ConnectingAgain: "Tekrar Bağlanılıyor",
ServerError: "Sunucu Hatası",
SearchingOpponet: "Rakip Aranıyor",
OpponentLeavesTheGame: "Rakip Oyundan Ayrıldı",
OpponentLeftTheGame: "Rakip Oyundan Ayrıldı",
YouLeftTheGame: "Sen Oyundan Ayrıldın",
UserLeftTheGame: "Oyundan Ayrıldı",
SearchingOpponent: "Rakip Aranıyor",
PleaseWait: "Lütfen Bekleyin",
GameDraw : "Berabere",
Anonymous: "Anonim",
GameNotFound: "Oyun Bulunamadı",
Loading: "Yükleniyor",
Playing: "Oynuyor",
}

View File

@ -17,7 +17,7 @@ import LoadingComponent from '../components/LoadingComponent';
import PageContainer from '../components/PageContainer';
import Row from '../components/Row';
import UserStatus from '../components/UserStatus';
import { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move } from '../const/channel_names';
import { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move, channel_listen_game_events, channel_unlisten_game_events } from '../const/channel_names';
import { Context } from '../context/context';
import useWindowDimensions from '../hooks/useWindowDimensions';
import { ConnectionState } from '../models/ConnectionState';
@ -39,8 +39,6 @@ const GamePage: FunctionComponent<{
const [game, setGame] = useState<Game | undefined>(undefined);
const [crashMessage, setCrashMessage] = useState<string | undefined>(undefined);
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string | undefined>(undefined);
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel | undefined>(undefined);
@ -61,7 +59,12 @@ const GamePage: FunctionComponent<{
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<Game>>(LoadingState.Unset());
const checkIsSpectator = (game: Game) => userKey !== game.mancalaGame.player1Id && userKey !== game.mancalaGame.player2Id;
const mancalaGame: MancalaGame | undefined = game?.mancalaGame;
const isSpectator = game ? checkIsSpectator(game) : undefined;
const isPlayer2 = !isSpectator && userKey === mancalaGame?.player2Id;
const onGameUpdate = (pitAnimator: PitAnimator, newGame: Game) => {
setGame(newGame);
@ -88,7 +91,6 @@ const GamePage: FunctionComponent<{
const newCrashMessage = message as string;
console.error("on_game_crash");
console.error(newCrashMessage);
setCrashMessage(newCrashMessage);
}
const onGameUserLeave = (message: any) => {
const userKeyWhoLeave = message;
@ -100,17 +102,19 @@ const GamePage: FunctionComponent<{
setGameUsersConnectionInfo(gameUsersConnectionInfo);
};
const listenMessages = (pitAnimator: PitAnimator): () => void => {
const listenMessages = (game: Game, pitAnimator: PitAnimator): () => void => {
const _onGameUpdate = (message: object) => onGameUpdateEvent(pitAnimator, message);
context.rtmt.listenMessage(channel_on_game_update, _onGameUpdate);
context.rtmt.listenMessage(channel_on_game_crashed, onGameCrashed);
context.rtmt.listenMessage(channel_on_game_user_leave, onGameUserLeave);
context.rtmt.listenMessage(channel_on_user_connection_change, onUserConnectionChange)
context.rtmt.addMessageListener(channel_on_game_update, _onGameUpdate);
context.rtmt.addMessageListener(channel_on_game_crashed, onGameCrashed);
context.rtmt.addMessageListener(channel_on_game_user_leave, onGameUserLeave);
context.rtmt.addMessageListener(channel_on_user_connection_change, onUserConnectionChange);
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_listen_game_events, game.id);
return () => {
context.rtmt.unlistenMessage(channel_on_game_update, _onGameUpdate);
context.rtmt.unlistenMessage(channel_on_game_crashed, onGameCrashed);
context.rtmt.unlistenMessage(channel_on_game_user_leave, onGameUserLeave);
context.rtmt.unlistenMessage(channel_on_user_connection_change, onUserConnectionChange);
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_unlisten_game_events, game.id);
context.rtmt.removeMessageListener(channel_on_game_update, _onGameUpdate);
context.rtmt.removeMessageListener(channel_on_game_crashed, onGameCrashed);
context.rtmt.removeMessageListener(channel_on_game_user_leave, onGameUserLeave);
context.rtmt.removeMessageListener(channel_on_user_connection_change, onUserConnectionChange);
}
};
@ -140,20 +144,28 @@ const GamePage: FunctionComponent<{
};
const onPitSelect = (index: number, pit: Pit) => {
if (!game || isSpectator || !userKey) {
return;
}
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey ||
!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
return;
}
if (checkHasAnOngoingAction()) {
return;
}
setHasOngoingAction(true);
if (!boardViewModel) return;
//TODO : stoneCount comes from view model!
//TODO: this check should be in mancala.js
if (pit.stoneCount === 0) {
//TODO : warn user
return;
}
boardViewModel.pits[getBoardIndex(index)].pitColor =
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
context.themeManager.theme.pitSelectedColor;
updateBoardViewModel(boardViewModel);
const gameMove: GameMove = { index: index };
const gameMove: GameMove = { index: pitIndexForUser };
context.rtmt.sendMessage(channel_game_move, gameMove);
};
@ -166,7 +178,7 @@ const GamePage: FunctionComponent<{
pitAnimator = new PitAnimator(context, updateBoardViewModel);
setPitAnimator(pitAnimator);
onGameUpdate(pitAnimator, game);
unlistenMessages = listenMessages(pitAnimator);
unlistenMessages = listenMessages(game, pitAnimator);
setLoadingStateGame(LoadingState.Loaded({ value: game }))
} else {
setLoadingStateGame(LoadingState.Error({ errorMessage: context.texts.GameNotFound }))
@ -183,17 +195,53 @@ const GamePage: FunctionComponent<{
context.themeManager.theme.textColor,
context.themeManager.theme.textLightColor
);
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.mancalaGame.state == "ended");
const showBoardView = game && boardViewModel && userKey && true;
const opponentId = getOpponentId();
const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: opponentId ? isUserOnline(opponentId) : false, isAnonymous: true };
const user = { id: userKey || "1", name: "Anonymous", isOnline: connectionState === "connected", isAnonymous: true };
const isMobile = width < 600;
const renderNewGameBtn = isSpectator || (userKeyWhoLeave || !game || (game && game.mancalaGame.state == "ended"));
const showBoardView = game && boardViewModel && userKey && true;
const topLocatedUserId = (isSpectator ? mancalaGame?.player2Id : getOpponentId()) || "0";
const bottomLocatedUserId = (isSpectator ? mancalaGame?.player1Id : userKey) || "1";
const topLocatedUser = {
id: topLocatedUserId,
name: "Anonymous",
isOnline: isUserOnline(topLocatedUserId),
isAnonymous: true
};
const bottomLocatedUser = {
id: bottomLocatedUserId,
name: "Anonymous",
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : connectionState === "connected",
isAnonymous: true
};
const currentUser = isSpectator ? {
id: "2",
name: "Anonymous",
isOnline: connectionState === "connected",
isAnonymous: true
} : bottomLocatedUser;
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
return (
<PageContainer theme={theme!}>
<HeaderBar color={theme?.appBarBgColor}>
{renderHeaderBar()}
{renderMobileBoardToolbar()}
{buildBoardTopToolbar()}
{showBoardView && (
<BoardView
game={game}
boardId={boardId}
boardViewModel={boardViewModel}
context={context}
onPitSelect={onPitSelect}
revert={isPlayer2} />
)}
<Center>
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
</Center>
</PageContainer>
);
function renderHeaderBar() {
return <HeaderBar color={theme?.appBarBgColor}>
<Row>
<Link style={{ textDecoration: 'none' }} to={"/"}>
<HeaderbarIcon />
@ -210,50 +258,44 @@ const GamePage: FunctionComponent<{
text={renderNewGameBtn ? context.texts.NewGame : context.texts.Leave}
onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
</Row>
</HeaderBar>
<BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
</HeaderBar>;
}
function renderMobileBoardToolbar() {
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
{buildInfoPanel()}
</BoardToolbar>;
}
function buildBoardTopToolbar() {
return <BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
<UserStatus style={{
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="left" user={topLocatedUser} visible={showBoardView || false} />
{buildInfoPanel()}
<UserStatus style={{
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="right" user={bottomLocatedUser} visible={showBoardView || false} />
</BoardToolbar>;
}
function buildInfoPanel() {
return (
<InfoPanel
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
context={context}
game={game}
crashMessage={crashMessage}
userKey={userKey}
userKeyWhoLeave={userKeyWhoLeave} />
</BoardToolbar>
<BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
<UserStatus style={{
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="left" user={opponentUser} visible={showBoardView || false} />
<InfoPanel
style={{
marginTop: "0.5rem", marginBottom: "0.5rem",
}}
context={context}
game={game}
crashMessage={crashMessage}
userKey={userKey}
userKeyWhoLeave={userKeyWhoLeave}
visible={!isMobile} />
<UserStatus style={{
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
width: isMobile ? "40vw" : "30vw"
}} context={context} layoutMode="right" user={user} visible={showBoardView || false} />
</BoardToolbar>
{showBoardView && (
<BoardView
userKey={userKey}
game={game}
boardId={boardId}
boardViewModel={boardViewModel}
context={context}
onPitSelect={onPitSelect} />
)}
<Center>
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
</Center>
</PageContainer>
currentUser={currentUser}
whitePlayer={topLocatedUser}
blackPlayer={bottomLocatedUser}
leftPlayer={leftPlayer}
visible={!isMobile}
isSpectator={isSpectator} />
);
}
}
export default GamePage;

View File

@ -23,18 +23,19 @@ const LobyPage: FunctionComponent<{
let navigate = useNavigate();
useEffect(() => {
listenMessages();
context.rtmt.sendMessage("new_game", {});
}, []);
const listenMessages = () => {
context.rtmt.listenMessage(channel_on_game_start, (message: Object) => {
const onGameStart = (message: Object) => {
const newGame: CommonMancalaGame = message as CommonMancalaGame;
navigate(`/game/${newGame.id}`)
});
}
useEffect(() => {
context.rtmt.addMessageListener(channel_on_game_start, onGameStart);
context.rtmt.sendMessage("new_game", {});
return () => {
context.rtmt.removeMessageListener(channel_on_game_start, onGameStart);
}
}, []);
const textColorOnAppBar = getColorByBrightness(
context.themeManager.theme.appBarBgColor,
context.themeManager.theme.textColor,

View File

@ -1,8 +1,16 @@
import EventEmitter2, { Listener } from "eventemitter2"
export type Bytes = Uint8Array
export type OnMessage = (message : Object) => any
export interface RTMT{
sendMessage : (channel : string, message : Object) => any
listenMessage : (channel : string, callback : OnMessage) => any
unlistenMessage : (channel : string, callback : OnMessage) => any
export type RtmtEventTypes = "open" | "close" | "connected" | "error" | "disconnected" | "message";
export interface RTMT extends EventEmitter2 {
sendMessage: (channel: string, message: Object) => void;
addMessageListener(channel: string, callback: (message: any) => void);
removeMessageListener(channel: string, callback: (message: any) => void);
on(event: RtmtEventTypes, callback: (...value: any[]) => void): Listener | this;
off(event: RtmtEventTypes, callback: (...value: any[]) => void): this;
dispose();
}

View File

@ -1,101 +1,93 @@
import { decode, encode } from "./encode_decode_message";
import { Bytes, OnMessage, RTMT } from "./rtmt";
import { channel_ping, channel_pong } from "../const/channel_names";
import { server } from "../const/config";
import EventEmitter2, { Listener } from "eventemitter2";
import { Bytes, RTMT, RtmtEventTypes } from "./rtmt";
const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
const MESSAGE_CHANNEL_PREFIX = "message_channel";
export class RTMTWS implements RTMT {
private messageChannels: Map<String, OnMessage | undefined>;
private ws: WebSocket;
export class RTMTWS extends EventEmitter2 implements RTMT {
private webSocket: WebSocket;
private pingTimeout?: number = undefined;
constructor() {
this.messageChannels = new Map<String, OnMessage>();
super();
}
initWebSocket(
userKey: string,
onopen: () => any,
onClose: () => any,
onError: (event: Event) => any
) {
public initWebSocket(userKey: string) {
const url = server.wsServerAdress + "?userKey=" + userKey;
const ws = new WebSocket(url);
ws.binaryType = "arraybuffer"; //for firefox
ws.onopen = () => {
console.info("(RTMT) ws has opened");
this.ws = ws;
const webSocket = new WebSocket(url);
webSocket.onopen = () => {
console.info("(RTMT) WebSocket has opened");
this.webSocket = webSocket;
this.heartbeat();
onopen();
this.emit("open");
};
ws.onclose = () => {
console.info("(RTMT) ws has closed");
//this.ws = undefined
webSocket.onclose = () => {
console.info("(RTMT) WebSocket has closed");
//this.WebSocket = undefined
clearTimeout(this.pingTimeout);
onClose();
this.emit("close");
};
ws.onmessage = (event: MessageEvent) => {
this.onWebSocketMessage(this, event);
webSocket.onmessage = (event: MessageEvent) => {
const { channel, message } = decode(event.data);
this.onMessage(channel, message);
};
ws.addEventListener("error", (ev) => {
console.error({ ws_error: ev });
onError(ev);
});
webSocket.onerror = (error) => {
console.error(error);
this.emit("error", error);
}
}
heartbeat() {
private heartbeat() {
clearTimeout(this.pingTimeout);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
this.ws.close();
this.webSocket.close();
}, PING_INTERVAL + PING_INTERVAL_BUFFER_TIME);
}
sendMessage(channel: string, message: Object) {
if (this.ws === undefined) {
console.error("(RTMT) ws is undefined");
public sendMessage(channel: string, message: Object) {
if (this.webSocket === undefined) {
console.error("(RTMT) WebSocket is undefined");
return;
}
const data = encode(channel, message);
this.ws.send(data);
this.webSocket.send(data);
}
// todo: support multible listeners
listenMessage(channel: string, callback: OnMessage) {
this.messageChannels.set(channel, callback);
}
// todo: support multible listeners
unlistenMessage(channel : string, callback : OnMessage) {
this.messageChannels.set(channel, undefined);
}
onWebSocketMessage(rtmt: RTMTWS, event: MessageEvent) {
const { channel, message } = decode(event.data);
rtmt.onMessage(channel, message);
}
onMessage(channel: string, message: Bytes) {
private onMessage(channel: string, message: Bytes) {
if (channel === channel_ping) {
this.heartbeat();
this.sendMessage(channel_pong, {});
return;
}
const callback = this.messageChannels.get(channel);
// TODO: Maybe we should warn if there is not any listener for channel
this.emit(MESSAGE_CHANNEL_PREFIX + channel, message);
}
if (callback) {
callback(message);
} else {
console.warn("(RTMT) Channel callback not found!" + channel);
}
public on(event: RtmtEventTypes, callback: (...value: any[]) => void): Listener | this {
return super.on(event, callback);
}
public off(event: RtmtEventTypes, callback: (...value: any[]) => void): this {
return super.off(event, callback);
}
public addMessageListener(channel: string, callback: (message: any) => void) {
super.on(MESSAGE_CHANNEL_PREFIX + channel, callback);
}
public removeMessageListener(channel: string, callback: (message: any) => void) {
super.off(MESSAGE_CHANNEL_PREFIX + channel, callback);
}
public dispose() {
this.webSocket.close();
this.removeAllListeners();
}
}

View File

@ -2,15 +2,18 @@ import lightTheme from "./LightTheme";
import greyTheme from "./GreyTheme";
import { Theme } from "./Theme";
import darkTheme from "./DarkTheme";
import EventEmitter2, { Listener } from "eventemitter2";
export const themes = [lightTheme, darkTheme, greyTheme];
const THEME_ID = "theme_id";
export default class ThemeManager {
_theme: Theme;
onThemeChange: (theme: Theme) => void;
export type ThemeManagerEvents = "themechange";
export default class ThemeManager extends EventEmitter2 {
private _theme: Theme;
constructor() {
super();
this._theme = this.readFromLocalStorage() || lightTheme;
}
@ -20,7 +23,7 @@ export default class ThemeManager {
public set theme(value: Theme) {
this._theme = value;
this.onThemeChange?.(value);
this.emit("themechange", value);
this.writetToLocalStorage(value);
}
@ -28,7 +31,7 @@ export default class ThemeManager {
localStorage.setItem(THEME_ID, value.id);
}
private readFromLocalStorage(): Theme {
private readFromLocalStorage(): Theme | undefined {
const themeID = localStorage.getItem(THEME_ID);
const theme = themes.find((eachTheme: Theme) => themeID === eachTheme.id);
return theme;
@ -37,4 +40,11 @@ export default class ThemeManager {
public get themes(): Theme[] {
return themes;
}
public on(event: ThemeManagerEvents, callback: (...value: any[]) => void): Listener | this {
return super.on(event, callback);
}
public off(event: ThemeManagerEvents, callback: (...value: any[]) => void): this {
return super.off(event, callback);
}
}

View File

@ -1087,6 +1087,13 @@
prop-types "^15.7.2"
react-transition-state "^1.1.4"
"@types/eventemitter2@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@types/eventemitter2/-/eventemitter2-4.1.0.tgz#090b4a67f25aa0fc087c8f2403e46f5ccc3fdfbc"
integrity sha512-IyrCYFL+FakW3gVd/x2b0QIpcVrdgcNCkj985xoBVinc0rNwoV87IbBx7KlS5aP+bx7uIZxVypLCiSwmI4jZrg==
dependencies:
eventemitter2 "*"
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -1421,6 +1428,11 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
eventemitter2@*, eventemitter2@^6.4.7:
version "6.4.7"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
@ -1735,7 +1747,7 @@ prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.13.1"
"react-dom@^16.0.0 || ^17.0.0 || ^18.0.0":
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@ -1778,7 +1790,7 @@ react-transition-state@^1.1.4:
resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-1.1.4.tgz#113224eaa27e0ff81661305e44d5e0348cdf61ac"
integrity sha512-6nQLWWx95gYazCm6OdtD1zGbRiirvVXPrDtHAGsYb4xs9spMM7bA8Vx77KCpjL8PJ8qz1lXFGz2PTboCSvt7iw==
"react@^16.0.0 || ^17.0.0 || ^18.0.0":
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==