[mobile] implement basic working Game UI
This commit is contained in:
parent
341fe0e083
commit
2cca40034d
@ -12,16 +12,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/native": "^6.1.17",
|
"@react-navigation/native": "^6.1.17",
|
||||||
"@react-navigation/native-stack": "^6.9.26",
|
"@react-navigation/native-stack": "^6.9.26",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
"i18next": "^23.10.1",
|
"i18next": "^23.10.1",
|
||||||
"mancala.js": "^0.0.2-beta.3",
|
"mancala.js": "^0.0.2-beta.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-native": "0.73.6",
|
"react-native": "0.73.6",
|
||||||
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-safe-area-context": "^4.9.0",
|
"react-native-safe-area-context": "^4.9.0",
|
||||||
"react-native-screens": "^3.29.0",
|
"react-native-screens": "^3.29.0",
|
||||||
"react-native-snackbar": "^2.6.2",
|
"react-native-snackbar": "^2.6.2",
|
||||||
"tiny-emitter": "^2.1.0"
|
"tiny-emitter": "^2.1.0",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
|||||||
@ -12,6 +12,9 @@ import { ConnectionState } from './rtmt/rtmt';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Snackbar from 'react-native-snackbar';
|
import Snackbar from 'react-native-snackbar';
|
||||||
|
|
||||||
|
// https://github.com/uuidjs/uuid/issues/514#issuecomment-691475020
|
||||||
|
import 'react-native-get-random-values';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
|
|
||||||
const context = initContext();
|
const context = initContext();
|
||||||
|
|||||||
220
mobile/src/animation/PitAnimator.ts
Normal file
220
mobile/src/animation/PitAnimator.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import {
|
||||||
|
GameStep,
|
||||||
|
HistoryItem,
|
||||||
|
GAME_STEP_GAME_MOVE,
|
||||||
|
GAME_STEP_LAST_STONE_IN_EMPTY_PIT,
|
||||||
|
GAME_STEP_BOARD_CLEARED,
|
||||||
|
GAME_STEP_LAST_STONE_IN_BANK,
|
||||||
|
GAME_STEP_DOUBLE_STONE_IN_PIT,
|
||||||
|
MancalaGame,
|
||||||
|
} from "mancala.js";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { Context } from "../context/context";
|
||||||
|
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
|
||||||
|
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
|
||||||
|
import { Game } from "../models/Game";
|
||||||
|
import { Theme } from "../theme/Theme";
|
||||||
|
import { getColorByBrightness } from "../util/ColorUtil";
|
||||||
|
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||||
|
|
||||||
|
const animationUpdateInterval = 300;
|
||||||
|
|
||||||
|
export default class PitAnimator {
|
||||||
|
context: Context;
|
||||||
|
game: Game | undefined;
|
||||||
|
oldGame: Game | undefined;
|
||||||
|
currentIntervalID: number | undefined;
|
||||||
|
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
|
||||||
|
boardViewModel: BoardViewModel | undefined;
|
||||||
|
oldBoardViewModel: BoardViewModel | undefined;
|
||||||
|
animationIndex: number = 0;
|
||||||
|
currentHistoryItem: HistoryItem | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void
|
||||||
|
) {
|
||||||
|
this.context = context;
|
||||||
|
this.onBoardViewModelUpdate = onBoardViewModelUpdate;
|
||||||
|
this.context.themeManager.on("themechange", this.onThemeChange.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
get mancalaGame(): MancalaGame | undefined {
|
||||||
|
return this.game?.mancalaGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setNewGame(game: Game) {
|
||||||
|
this.reset();
|
||||||
|
this.game = game;
|
||||||
|
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUpdatedGame(game: Game) {
|
||||||
|
this.resetAnimationState();
|
||||||
|
if (!this.game) {
|
||||||
|
this.setNewGame(game);
|
||||||
|
} else {
|
||||||
|
this.oldGame = this.game;
|
||||||
|
this.game = game;
|
||||||
|
this.onGameMoveAnimationStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onGameMoveAnimationStart() {
|
||||||
|
this.stopCurrentAnimation();
|
||||||
|
if (this.game && this.oldGame && this.mancalaGame && this.mancalaGame?.history.length > 0) {
|
||||||
|
const lastHistoryItem = this.mancalaGame.history[this.mancalaGame.history.length - 1];
|
||||||
|
if (lastHistoryItem.gameSteps.length > 0) {
|
||||||
|
this.animationIndex = 0;
|
||||||
|
this.currentHistoryItem = lastHistoryItem;
|
||||||
|
this.boardViewModel = this.getBoardViewModelFromGame(this.game);
|
||||||
|
this.oldBoardViewModel = this.getBoardViewModelFromGame(this.oldGame);
|
||||||
|
this.startAnimationUpdateCyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAnimate() {
|
||||||
|
if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel || !this.mancalaGame) return;
|
||||||
|
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||||
|
} else {
|
||||||
|
const gameStep = this.currentHistoryItem.gameSteps[this.animationIndex];
|
||||||
|
const index = this.mancalaGame.board.getPitIndexCircularly(gameStep.index);
|
||||||
|
this.animatePit(index, this.oldBoardViewModel, gameStep);
|
||||||
|
this.onBoardViewModelUpdate?.(this.oldBoardViewModel);
|
||||||
|
}
|
||||||
|
this.animationIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameMoveStepCount(historyItem: HistoryItem) {
|
||||||
|
return historyItem.gameSteps.filter(
|
||||||
|
(gameStep) => gameStep.type === GAME_STEP_GAME_MOVE
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
animatePit(
|
||||||
|
index: number,
|
||||||
|
boardViewModel: BoardViewModel,
|
||||||
|
gameStep: GameStep
|
||||||
|
) {
|
||||||
|
if (!this.currentHistoryItem || !this.game || !this.mancalaGame) return;
|
||||||
|
const pitViewModel = boardViewModel.pits[index];
|
||||||
|
if (this.animationIndex === 0) {
|
||||||
|
//This is one stone move case, TODO: beautify it later
|
||||||
|
if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) {
|
||||||
|
const previousPitIndex = gameStep.index - 1;
|
||||||
|
if (previousPitIndex > 0) {
|
||||||
|
boardViewModel.pits[previousPitIndex].stoneCount = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pitViewModel.stoneCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const theme = this.context.themeManager.theme;
|
||||||
|
if (gameStep.type === GAME_STEP_GAME_MOVE) {
|
||||||
|
pitViewModel.stoneCount += 1;
|
||||||
|
pitViewModel.pitColor = theme.pitGameMoveAnimateColor;
|
||||||
|
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_EMPTY_PIT) {
|
||||||
|
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
pitViewModel.stoneCount = 0;
|
||||||
|
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(
|
||||||
|
gameStep.data.oppositeIndex
|
||||||
|
);
|
||||||
|
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
||||||
|
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
oppositePitViewModel.stoneCount = 0;
|
||||||
|
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_BANK) {
|
||||||
|
pitViewModel.pitColor = theme.pitLastStoneInBankPitAnimateColor;
|
||||||
|
} else if (gameStep.type === GAME_STEP_BOARD_CLEARED) {
|
||||||
|
for (const index of gameStep.data.pitIndexesThatHasStone) {
|
||||||
|
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(index);
|
||||||
|
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
||||||
|
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
oppositePitViewModel.stoneCount = 0;
|
||||||
|
}
|
||||||
|
} else if (gameStep.type === GAME_STEP_DOUBLE_STONE_IN_PIT) {
|
||||||
|
const _index = this.mancalaGame.board.getPitIndexCircularly(index);
|
||||||
|
const pitViewModel = boardViewModel.pits[_index];
|
||||||
|
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
pitViewModel.stoneCount = 0;
|
||||||
|
}
|
||||||
|
pitViewModel.stoneColor = getColorByBrightness(
|
||||||
|
pitViewModel.pitColor,
|
||||||
|
theme.stoneColor,
|
||||||
|
theme.stoneLightColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimationUpdateCyle() {
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
this.currentIntervalID = setInterval(
|
||||||
|
() => this.onAnimate(),
|
||||||
|
animationUpdateInterval
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCurrentAnimation() {
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
if (this.oldGame) {
|
||||||
|
this.onBoardViewModelUpdate?.(
|
||||||
|
this.getBoardViewModelFromGame(this.oldGame)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.resetAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCurrentInterval() {
|
||||||
|
if (this.currentIntervalID) {
|
||||||
|
clearInterval(this.currentIntervalID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBoardViewModelFromGame(game: Game): BoardViewModel {
|
||||||
|
const pitViewModels = this.createPitViewModelsFromGame(game);
|
||||||
|
return BoardViewModelFactory.create(v4(), pitViewModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPitViewModelsFromGame(game: Game) {
|
||||||
|
return game.mancalaGame.board.pits.map((pit) => {
|
||||||
|
const theme = this.context.themeManager.theme;
|
||||||
|
const stoneCount = pit.stoneCount;
|
||||||
|
const stoneColor = theme.stoneColor;
|
||||||
|
const pitColor = theme.pitColor;
|
||||||
|
const id = pit.index.toString();
|
||||||
|
return PitViewModelFactory.create({
|
||||||
|
id,
|
||||||
|
stoneCount,
|
||||||
|
stoneColor,
|
||||||
|
pitColor,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onThemeChange(theme: Theme){
|
||||||
|
if(!this.game) return;
|
||||||
|
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetAnimationState() {
|
||||||
|
this.animationIndex = -1;
|
||||||
|
this.currentHistoryItem = undefined;
|
||||||
|
this.boardViewModel = undefined;
|
||||||
|
this.oldBoardViewModel = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.resetAnimationState();
|
||||||
|
this.game = undefined;
|
||||||
|
this.oldGame = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.context.themeManager.off("themechange", this.onThemeChange.bind(this));
|
||||||
|
this.resetAnimationState();
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
mobile/src/components/Button.tsx
Normal file
35
mobile/src/components/Button.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Context } from "../context/context";
|
||||||
|
import { getColorByBrightness } from "../util/ColorUtil";
|
||||||
|
import { Pressable, View } from "react-native";
|
||||||
|
|
||||||
|
const Button: FunctionComponent<{
|
||||||
|
context: Context;
|
||||||
|
text: String;
|
||||||
|
onClick: () => void;
|
||||||
|
color: string;
|
||||||
|
}> = ({ context, text, color, onClick }) => {
|
||||||
|
const textColor = getColorByBrightness(
|
||||||
|
color,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Pressable onPress={onClick}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
//color: textColor,
|
||||||
|
margin: 5,
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 30,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
19
mobile/src/components/Center.tsx
Normal file
19
mobile/src/components/Center.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
const Center: FunctionComponent<{children: React.ReactNode}> = ({children}) => {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Center;
|
||||||
21
mobile/src/components/CircularPanel.tsx
Normal file
21
mobile/src/components/CircularPanel.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { View, ViewStyle } from 'react-native';
|
||||||
|
|
||||||
|
const CircularPanel: FunctionComponent<{
|
||||||
|
color: string;
|
||||||
|
style?: ViewStyle,
|
||||||
|
children: React.ReactNode
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<View style={[{backgroundColor: props.color}, props.style, {
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 30,
|
||||||
|
}]}>
|
||||||
|
{props.children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CircularPanel;
|
||||||
116
mobile/src/components/InfoPanel.tsx
Normal file
116
mobile/src/components/InfoPanel.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
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";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Text, View, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
function getInfoPanelTextByGameState(params: {
|
||||||
|
context: Context;
|
||||||
|
game?: Game;
|
||||||
|
currentUser: User;
|
||||||
|
whitePlayer: User;
|
||||||
|
blackPlayer: User;
|
||||||
|
leftPlayer?: User;
|
||||||
|
isSpectator?: boolean;
|
||||||
|
}): string | undefined {
|
||||||
|
const {
|
||||||
|
context,
|
||||||
|
game,
|
||||||
|
currentUser,
|
||||||
|
whitePlayer,
|
||||||
|
blackPlayer,
|
||||||
|
leftPlayer,
|
||||||
|
isSpectator
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (leftPlayer) {
|
||||||
|
return isSpectator ? `${leftPlayer.name} ${t('UserLeftTheGame')}` :
|
||||||
|
leftPlayer.id == currentUser.id ? t('YouLeftTheGame') : t('OpponentLeftTheGame');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGameEnded = game?.mancalaGame.state == "ended";
|
||||||
|
if (isGameEnded) {
|
||||||
|
const wonPlayerID = game.mancalaGame.getWonPlayerId();
|
||||||
|
let whoWon;
|
||||||
|
if (wonPlayerID) {
|
||||||
|
const wonPlayer = wonPlayerID == whitePlayer.id ? whitePlayer : blackPlayer;
|
||||||
|
whoWon = isSpectator ? `${wonPlayer.name} ${t('Won')}` :
|
||||||
|
game.mancalaGame.getWonPlayerId() === currentUser.id
|
||||||
|
? t('YouWon')
|
||||||
|
: t('YouLost');
|
||||||
|
} else {
|
||||||
|
whoWon = t('GameDraw');
|
||||||
|
}
|
||||||
|
return t('GameEnded') + " " + whoWon;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game) {
|
||||||
|
const playingPlayer = game.mancalaGame.checkIsPlayerTurn(whitePlayer.id) ? whitePlayer : blackPlayer;
|
||||||
|
return isSpectator ? `${playingPlayer.name} ${t('Playing')}` : game.mancalaGame.checkIsPlayerTurn(currentUser.id)
|
||||||
|
? t('YourTurn')
|
||||||
|
: t('OpponentTurn');
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoPanel: FunctionComponent<{
|
||||||
|
context: Context;
|
||||||
|
game?: Game;
|
||||||
|
currentUser: User;
|
||||||
|
whitePlayer: User;
|
||||||
|
blackPlayer: User;
|
||||||
|
leftPlayer?: User;
|
||||||
|
style?: ViewStyle;
|
||||||
|
visible?: boolean;
|
||||||
|
isSpectator?: boolean;
|
||||||
|
}> = ({
|
||||||
|
context,
|
||||||
|
game,
|
||||||
|
currentUser,
|
||||||
|
whitePlayer,
|
||||||
|
blackPlayer,
|
||||||
|
leftPlayer,
|
||||||
|
style,
|
||||||
|
visible,
|
||||||
|
isSpectator
|
||||||
|
}) => {
|
||||||
|
if (visible === false) return <></>;
|
||||||
|
const isUserTurn = currentUser.id ? game?.mancalaGame.checkIsPlayerTurn(currentUser.id) : false;
|
||||||
|
const containerColor = isUserTurn
|
||||||
|
? context.themeManager.theme.playerTurnColor
|
||||||
|
: context.themeManager.theme.boardColor;
|
||||||
|
const textColor = getColorByBrightness(
|
||||||
|
containerColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
const text = getInfoPanelTextByGameState({
|
||||||
|
context,
|
||||||
|
game,
|
||||||
|
currentUser,
|
||||||
|
whitePlayer,
|
||||||
|
blackPlayer,
|
||||||
|
leftPlayer,
|
||||||
|
isSpectator
|
||||||
|
});
|
||||||
|
if (text) {
|
||||||
|
return (
|
||||||
|
<CircularPanel style={style} color={containerColor}>
|
||||||
|
<Text style={{ margin: 0, color: textColor}}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</CircularPanel>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (<View></View>)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoPanel;
|
||||||
31
mobile/src/components/LoadingComponent.tsx
Normal file
31
mobile/src/components/LoadingComponent.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { Context } from '../context/context';
|
||||||
|
import { LoadingState } from '../models/LoadingState';
|
||||||
|
import { getColorByBrightness } from '../util/ColorUtil';
|
||||||
|
import CircularPanel from './CircularPanel';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Text } from 'react-native';
|
||||||
|
|
||||||
|
const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState<any> }> = ({ context, loadingState }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (loadingState.isUnset() || loadingState.isLoaded()) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
if (loadingState.isLoading() || loadingState.isError()) {
|
||||||
|
const textColorOnBoard = getColorByBrightness(
|
||||||
|
context.themeManager.theme.boardColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
const text = loadingState.isLoading() ? t('Loading') +"..." : loadingState.errorMessage;
|
||||||
|
return (
|
||||||
|
<CircularPanel color={context.themeManager.theme.boardColor}>
|
||||||
|
<Text style={{ margin: 0, color: textColorOnBoard }}>{`${text}`}</Text>
|
||||||
|
</CircularPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingComponent;
|
||||||
22
mobile/src/components/PageContainer.tsx
Normal file
22
mobile/src/components/PageContainer.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Theme } from "../theme/Theme";
|
||||||
|
import { Context } from "../context/context";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
const PageContainer: FunctionComponent<{ context: Context, children: React.ReactNode }> = (props) => {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: props.context.themeManager.theme?.background,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 400
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageContainer;
|
||||||
18
mobile/src/components/Row.tsx
Normal file
18
mobile/src/components/Row.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
const Row: FunctionComponent<{children: React.ReactNode}> = ({children}) => {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent:'space-between'
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Row;
|
||||||
11
mobile/src/components/Space.tsx
Normal file
11
mobile/src/components/Space.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
const Space: FunctionComponent<{ width?: number, height?: number }> = ({ width, height }) => {
|
||||||
|
return (
|
||||||
|
<View style={{ width, height}}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Space;
|
||||||
80
mobile/src/components/UserStatus.tsx
Normal file
80
mobile/src/components/UserStatus.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { Context } from '../context/context';
|
||||||
|
import { User } from '../models/User';
|
||||||
|
import { getColorByBrightness } from '../util/ColorUtil';
|
||||||
|
import Space from './Space';
|
||||||
|
import { Theme } from '../theme/Theme';
|
||||||
|
import { StyleSheet, Text, View, ViewStyle } from 'react-native';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export type LayoutMode = "right" | "left";
|
||||||
|
|
||||||
|
const UserStatus: FunctionComponent<{
|
||||||
|
context: Context,
|
||||||
|
user: User,
|
||||||
|
layoutMode: LayoutMode,
|
||||||
|
visible?: boolean,
|
||||||
|
style?: ViewStyle
|
||||||
|
}> = ({ context, user, layoutMode, visible, style }) => {
|
||||||
|
if (visible === false) return <></>;
|
||||||
|
const textColorOnBoard = getColorByBrightness(
|
||||||
|
context.themeManager.theme.background,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const styles = getDynamicStyles(context.themeManager.theme);
|
||||||
|
return (
|
||||||
|
<View style={[style, layoutMode === "right" ? styles.flexRtl : styles.flexLtr]}>
|
||||||
|
<Text style={[{color: textColorOnBoard}, styles.text]}>{
|
||||||
|
user.isAnonymous ? t("Anonymous") : user.name}
|
||||||
|
</Text>
|
||||||
|
<Space width={5} />
|
||||||
|
<View style={[styles.circle, (user.isOnline? styles.online: styles.offline)]} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDynamicStyles(theme: Theme) {
|
||||||
|
return StyleSheet.create({
|
||||||
|
online: {
|
||||||
|
backgroundColor: theme.boardColor
|
||||||
|
},
|
||||||
|
offline: {
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
},
|
||||||
|
circle: {
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
minWidth: 15,
|
||||||
|
minHeight: 15,
|
||||||
|
borderRadius: 15,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: theme.boardColor,
|
||||||
|
borderStyle: "solid",
|
||||||
|
},
|
||||||
|
flexRtl: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
flexLtr: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color : "grey",
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
fontSize: 32,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserStatus;
|
||||||
20
mobile/src/components/board/BoardToolbar.tsx
Normal file
20
mobile/src/components/board/BoardToolbar.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { View, ViewStyle } from 'react-native';
|
||||||
|
|
||||||
|
const BoardToolbar: FunctionComponent<{ visible?: boolean, style?: ViewStyle, children: React.ReactNode }> = ({ children, visible, style }) => {
|
||||||
|
if (visible === false) return <></>;
|
||||||
|
return (
|
||||||
|
<View style={[style, {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
}]} >
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BoardToolbar;
|
||||||
61
mobile/src/components/board/BoardView.tsx
Normal file
61
mobile/src/components/board/BoardView.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Context } from "../../context/context";
|
||||||
|
import BoardViewModel from "../../viewmodel/BoardViewModel";
|
||||||
|
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||||
|
import PitView from "./PitView";
|
||||||
|
import StoreView from "./StoreView";
|
||||||
|
import { Game } from "../../models/Game";
|
||||||
|
import { Pit } from "mancala.js";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
const BoardView: FunctionComponent<{
|
||||||
|
game: Game;
|
||||||
|
context: Context;
|
||||||
|
boardId: string;
|
||||||
|
boardViewModel: BoardViewModel;
|
||||||
|
revert: boolean;
|
||||||
|
onPitSelect: (index: number, pit: Pit) => void;
|
||||||
|
}> = ({ game, context, boardId, boardViewModel, revert, onPitSelect: onPitSelect }) => {
|
||||||
|
const mancalaGame = game?.mancalaGame;
|
||||||
|
const theme = context.themeManager.theme;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: theme.boardColor,
|
||||||
|
padding: 20,
|
||||||
|
//display: 'grid',
|
||||||
|
//gridTemplateColumns: 'repeat(8, 11vw)',
|
||||||
|
//gridTemplateRows: 'repeat(2, 11vw)',
|
||||||
|
borderRadius: 30
|
||||||
|
}}>
|
||||||
|
<StoreView
|
||||||
|
context={context}
|
||||||
|
pitViewModel={revert ? player2BankViewModel : player1BankViewModel}
|
||||||
|
gridColumn="8 / 9"
|
||||||
|
gridRow="1 / 3"
|
||||||
|
/>
|
||||||
|
<StoreView
|
||||||
|
context={context}
|
||||||
|
pitViewModel={revert ? player1BankViewModel : player2BankViewModel}
|
||||||
|
gridColumn="1 / 2"
|
||||||
|
gridRow="1 / 3"
|
||||||
|
/>
|
||||||
|
{revert ? player1Pits?.reverse() : player2Pits?.reverse()}
|
||||||
|
{revert ? player2Pits : player1Pits}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoardView;
|
||||||
36
mobile/src/components/board/PitView.tsx
Normal file
36
mobile/src/components/board/PitView.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import Util from "../../util/Util";
|
||||||
|
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||||
|
import StoneView from "./StoneView";
|
||||||
|
import { Pressable, View } from "react-native";
|
||||||
|
|
||||||
|
const PitView: FunctionComponent<{
|
||||||
|
pitViewModel: PitViewModel;
|
||||||
|
onClick: () => void;
|
||||||
|
}> = ({ pitViewModel, onClick }) => {
|
||||||
|
const stones = [...Util.range(pitViewModel.stoneCount)].map((i, index) => (
|
||||||
|
<StoneView key={index} color={pitViewModel.stoneColor} />
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable onPress={onClick}>
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: pitViewModel.pitColor,
|
||||||
|
margin: 5,
|
||||||
|
padding: 5,
|
||||||
|
borderRadius: 50,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
//justifyItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}>
|
||||||
|
{stones}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PitView;
|
||||||
18
mobile/src/components/board/StoneView.tsx
Normal file
18
mobile/src/components/board/StoneView.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
const StoneView: FunctionComponent<{ color: string }> = ({ color }) => {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
margin: 1,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StoneView;
|
||||||
54
mobile/src/components/board/StoreView.tsx
Normal file
54
mobile/src/components/board/StoreView.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Context } from "../../context/context";
|
||||||
|
import { getColorByBrightness } from "../../util/ColorUtil";
|
||||||
|
import Util from "../../util/Util";
|
||||||
|
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||||
|
import StoneView from "./StoneView";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
|
const StoreView: FunctionComponent<{
|
||||||
|
context: Context;
|
||||||
|
pitViewModel: PitViewModel;
|
||||||
|
gridColumn: string;
|
||||||
|
gridRow: string;
|
||||||
|
}> = ({ context, pitViewModel, gridColumn, gridRow }) => {
|
||||||
|
const stones = [...Util.range(pitViewModel.stoneCount)].map((i, index) => (
|
||||||
|
<StoneView key={index} color={pitViewModel.stoneColor} />
|
||||||
|
));
|
||||||
|
const textColor = getColorByBrightness(
|
||||||
|
pitViewModel.pitColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: pitViewModel.pitColor,
|
||||||
|
//gridColumn: gridColumn,
|
||||||
|
//gridRow: gridRow,
|
||||||
|
margin: 5,
|
||||||
|
borderRadius: 50,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
{stones}
|
||||||
|
<Text style={{
|
||||||
|
color: textColor,
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 20,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 20,
|
||||||
|
}}>
|
||||||
|
{stones.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StoreView;
|
||||||
11
mobile/src/factory/BoardViewModelFactory.ts
Normal file
11
mobile/src/factory/BoardViewModelFactory.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||||
|
import PitViewModel from "../viewmodel/PitViewModel";
|
||||||
|
|
||||||
|
export default class BoardViewModelFactory {
|
||||||
|
public static create(
|
||||||
|
id: string,
|
||||||
|
pitViewModels: PitViewModel[]
|
||||||
|
): BoardViewModel {
|
||||||
|
return new BoardViewModel(id, pitViewModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
mobile/src/factory/PitViewModelFactory.ts
Normal file
13
mobile/src/factory/PitViewModelFactory.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import PitViewModel from "../viewmodel/PitViewModel";
|
||||||
|
|
||||||
|
export class PitViewModelFactory {
|
||||||
|
public static create(params: {
|
||||||
|
id: string;
|
||||||
|
stoneCount: number;
|
||||||
|
stoneColor: string;
|
||||||
|
pitColor: string;
|
||||||
|
}): PitViewModel {
|
||||||
|
const { id, stoneCount, stoneColor, pitColor } = params;
|
||||||
|
return new PitViewModel(id, stoneCount, stoneColor, pitColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,314 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, Button, Text } from 'react-native';
|
import { View, Button, Text, useWindowDimensions } from 'react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { GameScreenProps } from '../types';
|
import { GameScreenProps } from '../types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Game, GameUsersConnectionInfo } from '../models/Game';
|
||||||
|
import BoardViewModel from '../viewmodel/BoardViewModel';
|
||||||
|
import { Link } from '@react-navigation/native';
|
||||||
|
import { MancalaGame, Pit } from 'mancala.js';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import PitAnimator from '../animation/PitAnimator';
|
||||||
|
import { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_listen_game_events, channel_unlisten_game_events, channel_leave_game, channel_game_move } from '../const/channel_names';
|
||||||
|
import { GameMove } from '../models/GameMove';
|
||||||
|
import { LoadingState } from '../models/LoadingState';
|
||||||
|
import { getColorByBrightness } from '../util/ColorUtil';
|
||||||
|
import Util from '../util/Util';
|
||||||
|
import Snackbar from 'react-native-snackbar';
|
||||||
|
import PageContainer from '../components/PageContainer';
|
||||||
|
import Center from '../components/Center';
|
||||||
|
import InfoPanel from '../components/InfoPanel';
|
||||||
|
import LoadingComponent from '../components/LoadingComponent';
|
||||||
|
import UserStatus from '../components/UserStatus';
|
||||||
|
import BoardToolbar from '../components/board/BoardToolbar';
|
||||||
|
import BoardView from '../components/board/BoardView';
|
||||||
export function GameScreen({ navigation, route }: GameScreenProps) {
|
export function GameScreen({ navigation, route }: GameScreenProps) {
|
||||||
|
|
||||||
const { context, gameId } = route.params;
|
const { context, gameId, userKey } = route.params;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
const [game, setGame] = useState<Game | undefined>(undefined);
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<Text>{gameId}</Text>
|
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string | undefined>(undefined);
|
||||||
</View>
|
|
||||||
|
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel | undefined>(undefined);
|
||||||
|
|
||||||
|
const [boardId, setBoardId] = useState<string>("-1");
|
||||||
|
|
||||||
|
const [pitAnimator, setPitAnimator] = useState<PitAnimator | undefined>(undefined);
|
||||||
|
|
||||||
|
// It is a flag for ongoing action such as send game move.
|
||||||
|
// We have to block future actions if there is an ongoing action.
|
||||||
|
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [gameUsersConnectionInfo, setGameUsersConnectionInfo] = useState<GameUsersConnectionInfo | undefined>();
|
||||||
|
|
||||||
|
const { height, width } = useWindowDimensions();
|
||||||
|
|
||||||
|
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);
|
||||||
|
pitAnimator.setUpdatedGame(newGame);
|
||||||
|
setHasOngoingAction(false);
|
||||||
|
setGameUsersConnectionInfo(newGame.gameUsersConnectionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserOnline = (userId: string) => {
|
||||||
|
if (!gameUsersConnectionInfo) return false;
|
||||||
|
const user1ConnectionInfo = gameUsersConnectionInfo.user1ConnectionInfo;
|
||||||
|
const user2ConnectionInfo = gameUsersConnectionInfo.user2ConnectionInfo;
|
||||||
|
if (user1ConnectionInfo.userId === userId) return user1ConnectionInfo.isOnline;
|
||||||
|
if (user2ConnectionInfo.userId === userId) return user2ConnectionInfo.isOnline;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGameUpdateEvent = (pitAnimator: PitAnimator, message: Object) => {
|
||||||
|
const newGame: Game = message as Game;
|
||||||
|
newGame.mancalaGame = MancalaGame.createFromMancalaGame(newGame.mancalaGame);
|
||||||
|
onGameUpdate(pitAnimator, newGame);
|
||||||
|
}
|
||||||
|
const onGameCrashed = (message: any) => {
|
||||||
|
const newCrashMessage = message as string;
|
||||||
|
Snackbar.show({text: t("InternalErrorOccurred")});
|
||||||
|
console.error("on_game_crash");
|
||||||
|
console.error(newCrashMessage);
|
||||||
|
}
|
||||||
|
const onGameUserLeave = (message: any) => {
|
||||||
|
const userKeyWhoLeave = message;
|
||||||
|
setUserKeyWhoLeave(userKeyWhoLeave);
|
||||||
|
setHasOngoingAction(false);
|
||||||
|
};
|
||||||
|
const onUserConnectionChange = (message: any) => {
|
||||||
|
const gameUsersConnectionInfo = message as GameUsersConnectionInfo;
|
||||||
|
setGameUsersConnectionInfo(gameUsersConnectionInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenMessages = (game: Game, pitAnimator: PitAnimator): () => void => {
|
||||||
|
const _onGameUpdate = (message: object) => onGameUpdateEvent(pitAnimator, message);
|
||||||
|
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 () => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
||||||
|
boardViewModel.id = v4();
|
||||||
|
setBoardId(boardViewModel.id);
|
||||||
|
setBoardViewModel(boardViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBoardIndex = (index: number) => {
|
||||||
|
if (!game || !mancalaGame) return -1;
|
||||||
|
const pitsLenght = mancalaGame.board.pits.length;
|
||||||
|
if (userKey === mancalaGame.player2Id) return index + pitsLenght / 2;
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOpponentId = () => mancalaGame?.player1Id === userKey ? mancalaGame?.player2Id : mancalaGame?.player1Id;
|
||||||
|
|
||||||
|
const checkHasAnOngoingAction = () => hasOngoingAction;
|
||||||
|
|
||||||
|
const onLeaveGameClick = () => {
|
||||||
|
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
//swal({
|
||||||
|
// title: context.texts.AreYouSureToLeaveGame,
|
||||||
|
// icon: "warning",
|
||||||
|
// buttons: [context.texts.Yes, context.texts.Cancel],
|
||||||
|
// dangerMode: true,
|
||||||
|
//})
|
||||||
|
// .then((cancel) => {
|
||||||
|
// if (!cancel) {
|
||||||
|
// context.rtmt.sendMessage(channel_leave_game, {});
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNewGameClick = () => {
|
||||||
|
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
|
||||||
|
navigation.navigate("Loby", { context })
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPitSelect = (index: number, pit: Pit) => {
|
||||||
|
if (!game || isSpectator || !userKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (userKeyWhoLeave) {
|
||||||
|
Snackbar.show({text: t("GameEnded")});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (game.mancalaGame.state === "ended") {
|
||||||
|
Snackbar.show({text: t("GameEnded")});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
|
||||||
|
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey) {
|
||||||
|
Snackbar.show({text: t("UCanOnlyPlayYourOwnPits")});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
|
||||||
|
if (!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
|
||||||
|
Snackbar.show({text: t("OpponentTurn")});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (checkHasAnOngoingAction()) {
|
||||||
|
Snackbar.show({text: t("UMustWaitUntilCurrentMoveComplete")});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!boardViewModel) return;
|
||||||
|
//TODO: this check should be in mancala.js
|
||||||
|
if (pit.stoneCount === 0) {
|
||||||
|
Snackbar.show({text: t("UCanNotPlayEmptyPit")});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasOngoingAction(true);
|
||||||
|
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
|
||||||
|
context.themeManager.theme.pitSelectedColor;
|
||||||
|
updateBoardViewModel(boardViewModel);
|
||||||
|
const gameMove: GameMove = { index: pitIndexForUser };
|
||||||
|
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let pitAnimator: PitAnimator | undefined;
|
||||||
|
let unlistenMessages: () => void;
|
||||||
|
setLoadingStateGame(LoadingState.Loading())
|
||||||
|
context.gameStore.get(gameId!!).then((game) => {
|
||||||
|
if (game) {
|
||||||
|
pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||||
|
setPitAnimator(pitAnimator);
|
||||||
|
onGameUpdate(pitAnimator, game);
|
||||||
|
unlistenMessages = listenMessages(game, pitAnimator);
|
||||||
|
setLoadingStateGame(LoadingState.Loaded({ value: game }))
|
||||||
|
} else {
|
||||||
|
setLoadingStateGame(LoadingState.Error({ errorMessage: t('GameNotFound') }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
unlistenMessages?.();
|
||||||
|
pitAnimator?.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const textColorOnAppBar = getColorByBrightness(
|
||||||
|
context.themeManager.theme.appBarBgColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
);
|
);
|
||||||
|
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) : context.rtmt.connectionState === "connected",
|
||||||
|
isAnonymous: true
|
||||||
|
};
|
||||||
|
const currentUser = isSpectator ? {
|
||||||
|
id: "2",
|
||||||
|
name: "Anonymous",
|
||||||
|
isOnline: context.rtmt.connectionState === "connected",
|
||||||
|
isAnonymous: true
|
||||||
|
} : bottomLocatedUser;
|
||||||
|
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
|
||||||
|
return (
|
||||||
|
<PageContainer context={context}>
|
||||||
|
{/* {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 />
|
||||||
|
// </Link>
|
||||||
|
// <Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
|
// <HeaderbarTitle title={context.texts.Mancala} color={textColorOnAppBar} />
|
||||||
|
// </Link>
|
||||||
|
// </Row>
|
||||||
|
// <Row>
|
||||||
|
// <ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
||||||
|
// <Button
|
||||||
|
// context={context}
|
||||||
|
// color={context.themeManager.theme.pitColor}
|
||||||
|
// text={renderNewGameBtn ? context.texts.NewGame : context.texts.Leave}
|
||||||
|
// onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
|
||||||
|
// </Row>
|
||||||
|
// </HeaderBar>;
|
||||||
|
//}
|
||||||
|
|
||||||
|
function renderMobileBoardToolbar() {
|
||||||
|
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
||||||
|
{buildInfoPanel({ visible: isMobile })}
|
||||||
|
</BoardToolbar>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBoardTopToolbar() {
|
||||||
|
return <BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
|
||||||
|
<UserStatus style={{
|
||||||
|
marginBottom: 15, marginLeft: "6%", maxWidth: isMobile ? 40 : 30,
|
||||||
|
width: isMobile ? 40 : 30
|
||||||
|
}} context={context} layoutMode="left" user={topLocatedUser} visible={showBoardView || false} />
|
||||||
|
{buildInfoPanel({ visible: !isMobile })}
|
||||||
|
<UserStatus style={{
|
||||||
|
marginBottom: 15, marginRight: "6%", maxWidth: isMobile ? 40: 30,
|
||||||
|
width: isMobile ? 40: 30
|
||||||
|
}} context={context} layoutMode="right" user={bottomLocatedUser} visible={showBoardView || false} />
|
||||||
|
</BoardToolbar>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInfoPanel(params: { visible: boolean }) {
|
||||||
|
return (
|
||||||
|
<InfoPanel
|
||||||
|
style={{ marginTop: 10, marginBottom: 10 }}
|
||||||
|
context={context}
|
||||||
|
game={game}
|
||||||
|
currentUser={currentUser}
|
||||||
|
whitePlayer={topLocatedUser}
|
||||||
|
blackPlayer={bottomLocatedUser}
|
||||||
|
leftPlayer={leftPlayer}
|
||||||
|
visible={params.visible}
|
||||||
|
isSpectator={isSpectator} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,10 @@ export default function LobyScreen({ navigation, route }: LobyScreenProps) {
|
|||||||
const { context } = route.params;
|
const { context } = route.params;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onGameStart = (message: Object) => {
|
const onGameStart = async (message: Object) => {
|
||||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||||
navigation.navigate("Game", { context, gameId: newGame.id })
|
const userKey = await context.userKeyStore.getUserKey();
|
||||||
|
navigation.navigate("Game", { context, gameId: newGame.id, userKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Context } from '../context/context';
|
|||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Home: { context: Context },
|
Home: { context: Context },
|
||||||
Loby: { context: Context },
|
Loby: { context: Context },
|
||||||
Game: { context: Context, gameId: string }
|
Game: { context: Context, gameId: string, userKey: string }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
||||||
|
|||||||
32
mobile/src/util/ColorUtil.ts
Normal file
32
mobile/src/util/ColorUtil.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
export function getBrightness(r: number, g: number, b: number) {
|
||||||
|
return (r * 299 + g * 587 + b * 114) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBrightnessFromHexColor(hexColor: string): number {
|
||||||
|
const [r, g, b] = hexToRgb(hexColor);
|
||||||
|
return getBrightness(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// from https://www.codegrepper.com/code-examples/javascript/javascript+convert+color+string+to+rgb
|
||||||
|
export function rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// from https://www.codegrepper.com/code-examples/javascript/javascript+convert+color+string+to+rgb
|
||||||
|
export function hexToRgb(
|
||||||
|
hex: string,
|
||||||
|
result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
|
): number[] {
|
||||||
|
//@ts-ignore
|
||||||
|
return result ? result.map((i) => parseInt(i, 16)).slice(1) : null;
|
||||||
|
//returns [23, 14, 45] -> reformat if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorByBrightness(
|
||||||
|
color: string,
|
||||||
|
lightColor: string,
|
||||||
|
darkColor: string
|
||||||
|
): string {
|
||||||
|
return getBrightnessFromHexColor(color) < 125 ? darkColor : lightColor;
|
||||||
|
}
|
||||||
34
mobile/src/util/Util.ts
Normal file
34
mobile/src/util/Util.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Context } from "../context/context";
|
||||||
|
import { ConnectionState } from "../rtmt/rtmt";
|
||||||
|
import Snackbar from "react-native-snackbar";
|
||||||
|
|
||||||
|
export default class Util {
|
||||||
|
public static range(size: number) {
|
||||||
|
var ans: number[] = [];
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
ans.push(i);
|
||||||
|
}
|
||||||
|
return ans;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static checkConnectionAndMaybeAlert(context: Context, message: string): boolean {
|
||||||
|
if (context.rtmt.connectionState !== "connected") {
|
||||||
|
Snackbar.show({text: message});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getTextByConnectionState(context: Context, connectionState: ConnectionState): string {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const map: { [key: string]: string } = {
|
||||||
|
connecting: t("Connecting"),
|
||||||
|
connected: t("Connected"),
|
||||||
|
error: t("CannotConnect"),
|
||||||
|
closed: t("ConnectingAgain"),
|
||||||
|
reconnecting: t("ConnectingAgain"),
|
||||||
|
};
|
||||||
|
return map[connectionState];
|
||||||
|
}
|
||||||
|
}
|
||||||
10
mobile/src/viewmodel/BoardViewModel.ts
Normal file
10
mobile/src/viewmodel/BoardViewModel.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import PitViewModel from "./PitViewModel";
|
||||||
|
|
||||||
|
export default class BoardViewModel {
|
||||||
|
id: string;
|
||||||
|
pits: PitViewModel[];
|
||||||
|
constructor(id: string, pits: PitViewModel[]) {
|
||||||
|
this.id = id;
|
||||||
|
this.pits = pits;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
mobile/src/viewmodel/PitViewModel.ts
Normal file
18
mobile/src/viewmodel/PitViewModel.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export default class PitViewModel {
|
||||||
|
id: string;
|
||||||
|
stoneCount: number;
|
||||||
|
stoneColor: string;
|
||||||
|
pitColor: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
stoneCount: number,
|
||||||
|
stoneColor: string,
|
||||||
|
pitColor: string
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.stoneCount = stoneCount;
|
||||||
|
this.stoneColor = stoneColor;
|
||||||
|
this.pitColor = pitColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2041,6 +2041,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
|
||||||
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
|
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
|
||||||
|
|
||||||
|
"@types/uuid@^9.0.8":
|
||||||
|
version "9.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
|
||||||
|
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
|
||||||
|
|
||||||
"@types/yargs-parser@*":
|
"@types/yargs-parser@*":
|
||||||
version "21.0.3"
|
version "21.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
|
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
|
||||||
@ -3420,6 +3425,11 @@ expect@^29.7.0:
|
|||||||
jest-message-util "^29.7.0"
|
jest-message-util "^29.7.0"
|
||||||
jest-util "^29.7.0"
|
jest-util "^29.7.0"
|
||||||
|
|
||||||
|
fast-base64-decode@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418"
|
||||||
|
integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==
|
||||||
|
|
||||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||||
@ -5644,6 +5654,13 @@ react-is@^17.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
|
react-native-get-random-values@^1.11.0:
|
||||||
|
version "1.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d"
|
||||||
|
integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==
|
||||||
|
dependencies:
|
||||||
|
fast-base64-decode "^1.0.0"
|
||||||
|
|
||||||
react-native-mmkv@^2.12.2:
|
react-native-mmkv@^2.12.2:
|
||||||
version "2.12.2"
|
version "2.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee"
|
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee"
|
||||||
@ -6595,6 +6612,11 @@ utils-merge@1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||||
|
|
||||||
|
uuid@^9.0.1:
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||||
|
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||||
|
|
||||||
v8-to-istanbul@^9.0.1:
|
v8-to-istanbul@^9.0.1:
|
||||||
version "9.2.0"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"
|
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user