Convert game into functional component

This commit is contained in:
Arnaud Vergnet 2021-05-22 15:39:35 +02:00
parent 14365a92a4
commit 9ae585bdf8
10 changed files with 856 additions and 770 deletions

View file

@ -0,0 +1,44 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { GamePodium } from './GamePodium';
type Props = {
scores: Array<number>;
isHighScore: boolean;
};
const styles = StyleSheet.create({
topScoreContainer: {
marginBottom: 20,
marginTop: 20,
},
topScoreSubcontainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
});
export default function FullGamePodium(props: Props) {
const { scores, isHighScore } = props;
const gold = scores.length > 0 ? scores[0] : '-';
const silver = scores.length > 1 ? scores[1] : '-';
const bronze = scores.length > 2 ? scores[2] : '-';
return (
<View style={styles.topScoreContainer}>
<GamePodium place={1} score={gold.toString()} isHighScore={isHighScore} />
<View style={styles.topScoreSubcontainer}>
<GamePodium
place={3}
score={silver.toString()}
isHighScore={isHighScore}
/>
<GamePodium
place={2}
score={bronze.toString()}
isHighScore={isHighScore}
/>
</View>
</View>
);
}

View file

@ -0,0 +1,65 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import { useTheme } from 'react-native-paper';
import GridManager from '../logic/GridManager';
import Piece from '../logic/Piece';
import GridComponent from './GridComponent';
const styles = StyleSheet.create({
pieceContainer: {
position: 'absolute',
width: '100%',
height: '100%',
},
pieceBackground: {
position: 'absolute',
},
});
export default function GameBackground() {
const theme = useTheme();
const gridManager = new GridManager(4, 4, theme);
const gridList = [];
for (let i = 0; i < 18; i += 1) {
gridList.push(gridManager.getEmptyGrid(4, 4));
const piece = new Piece(theme);
piece.toGrid(gridList[i], true);
}
return (
<View style={styles.pieceContainer}>
{gridList.map((item, index) => {
const size = 10 + Math.floor(Math.random() * 30);
const top = Math.floor(Math.random() * 100);
const rot = Math.floor(Math.random() * 360);
const left = (index % 6) * 20;
const animDelay = size * 20;
const animDuration = 2 * (2000 - size * 30);
return (
<Animatable.View
useNativeDriver={true}
animation={'fadeInDownBig'}
delay={animDelay}
duration={animDuration}
key={`piece${index.toString()}`}
style={{
width: `${size}%`,
top: `${top}%`,
left: `${left}%`,
...styles.pieceBackground,
}}
>
<GridComponent
width={4}
height={4}
grid={item}
style={{
transform: [{ rotateZ: `${rot}deg` }],
}}
/>
</Animatable.View>
);
})}
</View>
);
}

View file

@ -0,0 +1,62 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, useTheme } from 'react-native-paper';
import GENERAL_STYLES from '../../../constants/Styles';
import GameLogic, { MovementCallbackType } from '../logic/GameLogic';
type Props = {
logic: GameLogic;
onDirectionPressed: MovementCallbackType;
};
const styles = StyleSheet.create({
controlsContainer: {
height: 80,
flexDirection: 'row',
},
directionsContainer: {
flexDirection: 'row',
flex: 4,
},
});
function GameControls(props: Props) {
const { logic } = props;
const theme = useTheme();
return (
<View style={styles.controlsContainer}>
<IconButton
icon={'rotate-right-variant'}
size={40}
onPress={() => logic.rotatePressed(props.onDirectionPressed)}
style={GENERAL_STYLES.flex}
/>
<View style={styles.directionsContainer}>
<IconButton
icon={'chevron-left'}
size={40}
style={GENERAL_STYLES.flex}
onPress={() => logic.pressedOut()}
onPressIn={() => logic.leftPressedIn(props.onDirectionPressed)}
/>
<IconButton
icon={'chevron-right'}
size={40}
style={GENERAL_STYLES.flex}
onPress={() => logic.pressedOut()}
onPressIn={() => logic.rightPressed(props.onDirectionPressed)}
/>
</View>
<IconButton
icon={'arrow-down-bold'}
size={40}
onPressIn={() => logic.downPressedIn(props.onDirectionPressed)}
onPress={() => logic.pressedOut()}
style={GENERAL_STYLES.flex}
color={theme.colors.tetrisScore}
/>
</View>
);
}
export default React.memo(GameControls, () => true);

View file

@ -0,0 +1,95 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from 'react-native-animatable';
type Props = {
place: 1 | 2 | 3;
score: string;
isHighScore: boolean;
};
const styles = StyleSheet.create({
podiumContainer: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
},
podiumIconContainer: {
position: 'absolute',
top: -20,
},
centertext: {
textAlign: 'center',
},
});
export function GamePodium(props: Props) {
const { place, score, isHighScore } = props;
const theme = useTheme();
let icon = 'podium-gold';
let color = theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = 'podium-silver';
color = theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = 'podium-bronze';
color = theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
const marginLeft = place === 2 ? 20 : 'auto';
const marginRight = place === 3 ? 20 : 'auto';
const fontWeight = place === 1 ? 'bold' : undefined;
return (
<View
style={{
marginLeft: marginLeft,
marginRight: marginRight,
...styles.podiumContainer,
}}
>
{isHighScore && place === 1 ? (
<Animatable.View
animation="swing"
iterationCount="infinite"
duration={2000}
delay={1000}
useNativeDriver
style={styles.podiumIconContainer}
>
<Animatable.View
animation="pulse"
iterationCount="infinite"
useNativeDriver
>
<MaterialCommunityIcons
name="decagram"
color={theme.colors.gameGold}
size={150}
/>
</Animatable.View>
</Animatable.View>
) : null}
<MaterialCommunityIcons
name={icon}
color={isHighScore && place === 1 ? '#fff' : color}
size={size}
/>
<Text
style={{
fontWeight: fontWeight,
fontSize,
...styles.centertext,
}}
>
{score}
</Text>
</View>
);
}

View file

@ -0,0 +1,81 @@
import React from 'react';
import { View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js';
import { Text, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native';
import GENERAL_STYLES from '../../../constants/Styles';
type Props = {
score: number;
highScore?: number;
};
const styles = StyleSheet.create({
scoreMainContainer: {
marginTop: 10,
marginBottom: 10,
},
scoreCurrentContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
scoreText: {
marginLeft: 5,
fontSize: 20,
},
scoreBestContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 5,
},
centerVerticalSmallMargin: {
...GENERAL_STYLES.centerVertical,
marginLeft: 5,
},
});
function GameScore(props: Props) {
const { score, highScore } = props;
const theme = useTheme();
const displayHighScore =
highScore == null || score > highScore ? score : highScore;
return (
<View style={styles.scoreMainContainer}>
<View style={styles.scoreCurrentContainer}>
<Text style={styles.scoreText}>
{i18n.t('screens.game.score', { score: score })}
</Text>
<MaterialCommunityIcons
name="star"
color={theme.colors.tetrisScore}
size={20}
style={styles.centerVerticalSmallMargin}
/>
</View>
<View style={styles.scoreBestContainer}>
<Text
style={{
...styles.scoreText,
color: theme.colors.textDisabled,
}}
>
{i18n.t('screens.game.highScore', { score: displayHighScore })}
</Text>
<MaterialCommunityIcons
name="star"
color={theme.colors.tetrisScore}
size={10}
style={styles.centerVerticalSmallMargin}
/>
</View>
</View>
);
}
export default React.memo(
GameScore,
(pp, np) => pp.highScore === np.highScore && pp.score === np.score
);

View file

@ -0,0 +1,96 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Caption, Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import GENERAL_STYLES from '../../../constants/Styles';
import i18n from 'i18n-js';
type Props = {
time: number;
level: number;
};
const styles = StyleSheet.create({
centerSmallMargin: {
...GENERAL_STYLES.centerHorizontal,
marginBottom: 5,
},
centerBigMargin: {
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20,
},
statusContainer: {
flexDirection: 'row',
},
statusIcon: {
marginLeft: 5,
},
});
function getFormattedTime(seconds: number): string {
const date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds);
let format;
if (date.getHours()) {
format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
} else if (date.getMinutes()) {
format = `${date.getMinutes()}:${date.getSeconds()}`;
} else {
format = date.getSeconds().toString();
}
return format;
}
function GameStatus(props: Props) {
const theme = useTheme();
return (
<View
style={{
...GENERAL_STYLES.flex,
...GENERAL_STYLES.centerVertical,
}}
>
<View style={GENERAL_STYLES.centerHorizontal}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.time')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name={'timer'}
color={theme.colors.subtitle}
size={20}
/>
<Text
style={{
...styles.statusIcon,
color: theme.colors.subtitle,
}}
>
{getFormattedTime(props.time)}
</Text>
</View>
</View>
<View style={styles.centerBigMargin}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.level')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name={'gamepad-square'}
color={theme.colors.text}
size={20}
/>
<Text style={styles.statusIcon}>{props.level}</Text>
</View>
</View>
</View>
);
}
export default React.memo(
GameStatus,
(pp, np) => pp.level === np.level && pp.time === np.time
);

View file

@ -0,0 +1,130 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Card, Divider, Headline, Text, useTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import GENERAL_STYLES from '../../../constants/Styles';
import i18n from 'i18n-js';
type GameStatsType = {
score: number;
level: number;
time: number;
};
type Props = {
isHighScore: boolean;
stats: GameStatsType;
};
const styles = StyleSheet.create({
recapCard: {
borderWidth: 2,
marginLeft: 20,
marginRight: 20,
},
recapContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
recapScoreContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
},
recapScore: {
fontSize: 20,
},
recapScoreIcon: {
marginLeft: 5,
},
recapIcon: {
marginRight: 5,
marginLeft: 5,
},
centertext: {
textAlign: 'center',
},
});
export default function PostGameContent(props: Props) {
const { isHighScore, stats } = props;
const theme = useTheme();
const width = isHighScore ? '50%' : '30%';
const margin = isHighScore ? 'auto' : undefined;
const marginLeft = isHighScore ? '60%' : '20%';
const color = isHighScore ? theme.colors.gameGold : theme.colors.primary;
return (
<View style={GENERAL_STYLES.flex}>
<Mascot
emotion={isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
animated={isHighScore}
style={{
width: width,
marginLeft: margin,
marginRight: margin,
}}
/>
<SpeechArrow
style={{ marginLeft: marginLeft }}
size={20}
color={theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: theme.colors.mascotMessageArrow,
...styles.recapCard,
}}
>
<Card.Content>
<Headline
style={{
color: color,
...styles.centertext,
}}
>
{isHighScore
? i18n.t('screens.game.newHighScore')
: i18n.t('screens.game.gameOver')}
</Headline>
<Divider />
<View style={styles.recapScoreContainer}>
<Text style={styles.recapScore}>
{i18n.t('screens.game.score', { score: stats.score })}
</Text>
<MaterialCommunityIcons
name={'star'}
color={theme.colors.tetrisScore}
size={30}
style={styles.recapScoreIcon}
/>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.level')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name={'gamepad-square'}
size={20}
color={theme.colors.textDisabled}
/>
<Text>{stats.level}</Text>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.time')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name={'timer'}
size={20}
color={theme.colors.textDisabled}
/>
<Text>{stats.time}</Text>
</View>
</Card.Content>
</Card>
</View>
);
}

View file

@ -0,0 +1,70 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
Card,
Divider,
Headline,
Paragraph,
useTheme,
} from 'react-native-paper';
import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import i18n from 'i18n-js';
const styles = StyleSheet.create({
welcomeMascot: {
width: '40%',
marginLeft: 'auto',
marginRight: 'auto',
},
welcomeCard: {
borderWidth: 2,
marginLeft: 10,
marginRight: 10,
},
speechArrow: {
marginLeft: '60%',
},
welcomeText: {
textAlign: 'center',
marginTop: 10,
},
centertext: {
textAlign: 'center',
},
});
export default function WelcomeGameContent() {
const theme = useTheme();
return (
<View>
<Mascot emotion={MASCOT_STYLE.COOL} style={styles.welcomeMascot} />
<SpeechArrow
style={styles.speechArrow}
size={20}
color={theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: theme.colors.mascotMessageArrow,
...styles.welcomeCard,
}}
>
<Card.Content>
<Headline
style={{
color: theme.colors.primary,
...styles.centertext,
}}
>
{i18n.t('screens.game.welcomeTitle')}
</Headline>
<Divider />
<Paragraph style={styles.welcomeText}>
{i18n.t('screens.game.welcomeMessage')}
</Paragraph>
</Card.Content>
</Card>
</View>
);
}

View file

@ -17,10 +17,9 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import React, { useLayoutEffect, useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Caption, IconButton, Text, withTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTheme } from 'react-native-paper';
import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import GameLogic from '../logic/GameLogic';
@ -34,25 +33,15 @@ import type { OptionsDialogButtonType } from '../../../components/Dialogs/Option
import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
import GENERAL_STYLES from '../../../constants/Styles';
import { MainRoutes } from '../../../navigation/MainNavigator';
type PropsType = {
navigation: StackNavigationProp<any>;
route: { params: { highScore: number } };
theme: ReactNativePaper.Theme;
};
type StateType = {
grid: GridType;
gameTime: number;
gameScore: number;
gameLevel: number;
dialogVisible: boolean;
dialogTitle: string;
dialogMessage: string;
dialogButtons: Array<OptionsDialogButtonType>;
onDialogDismiss: () => void;
};
import GameStatus from '../components/GameStatus';
import GameControls from '../components/GameControls';
import GameScore from '../components/GameScore';
import { usePreferences } from '../../../context/preferencesContext';
import {
getPreferenceObject,
PreferenceKeys,
} from '../../../utils/asyncStorage';
import { useNavigation } from '@react-navigation/core';
const styles = StyleSheet.create({
container: {
@ -62,44 +51,6 @@ const styles = StyleSheet.create({
gridContainer: {
flex: 4,
},
centerSmallMargin: {
...GENERAL_STYLES.centerHorizontal,
marginBottom: 5,
},
centerVerticalSmallMargin: {
...GENERAL_STYLES.centerVertical,
marginLeft: 5,
},
centerBigMargin: {
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20,
},
statusContainer: {
flexDirection: 'row',
},
statusIcon: {
marginLeft: 5,
},
scoreMainContainer: {
marginTop: 10,
marginBottom: 10,
},
scoreCurrentContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
scoreText: {
marginLeft: 5,
fontSize: 20,
},
scoreBestContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 5,
},
controlsContainer: {
height: 80,
flexDirection: 'row',
@ -115,273 +66,125 @@ const styles = StyleSheet.create({
},
});
class GameMainScreen extends React.Component<PropsType, StateType> {
static getFormattedTime(seconds: number): string {
const date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds);
let format;
if (date.getHours()) {
format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
} else if (date.getMinutes()) {
format = `${date.getMinutes()}:${date.getSeconds()}`;
export default function GameMainScreen() {
const theme = useTheme();
const navigation = useNavigation<StackNavigationProp<any>>();
const logic = useRef(new GameLogic(20, 10, theme));
const [gameTime, setGameTime] = useState(0);
const [gameState, setGameState] = useState({
grid: logic.current.getCurrentGrid(),
gameScore: 0,
gameLevel: 0,
});
const [dialogContent, setDialogContent] = useState<{
dialogTitle: string;
dialogMessage: string;
dialogButtons: Array<OptionsDialogButtonType>;
onDialogDismiss: () => void;
}>();
const { preferences, updatePreferences } = usePreferences();
function getScores() {
const pref = getPreferenceObject(PreferenceKeys.gameScores, preferences) as
| Array<number>
| undefined;
if (pref) {
return pref.sort((a, b) => b - a);
} else {
format = date.getSeconds().toString();
}
return format;
}
logic: GameLogic;
highScore: number | null;
constructor(props: PropsType) {
super(props);
this.highScore = null;
this.logic = new GameLogic(20, 10, props.theme);
this.state = {
grid: this.logic.getCurrentGrid(),
gameTime: 0,
gameScore: 0,
gameLevel: 0,
dialogVisible: false,
dialogTitle: '',
dialogMessage: '',
dialogButtons: [],
onDialogDismiss: () => {},
};
if (props.route.params != null) {
this.highScore = props.route.params.highScore;
return [];
}
}
componentDidMount() {
const { navigation } = this.props;
const savedScores = getScores();
const highScore = savedScores.length > 0 ? savedScores[0] : undefined;
useLayoutEffect(() => {
navigation.setOptions({
headerRight: this.getRightButton,
headerRight: getRightButton,
});
this.startGame();
}
startGame();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation]);
componentWillUnmount() {
this.logic.endGame(true);
}
const getRightButton = () => (
<MaterialHeaderButtons>
<Item title={'pause'} iconName={'pause'} onPress={togglePause} />
</MaterialHeaderButtons>
);
getRightButton = () => {
return (
<MaterialHeaderButtons>
<Item title="pause" iconName="pause" onPress={this.togglePause} />
</MaterialHeaderButtons>
);
};
onTick = (score: number, level: number, newGrid: GridType) => {
this.setState({
const onTick = (score: number, level: number, newGrid: GridType) => {
setGameState({
gameScore: score,
gameLevel: level,
grid: newGrid,
});
};
onClock = (time: number) => {
this.setState({
gameTime: time,
});
};
const onDialogDismiss = () => setDialogContent(undefined);
onDialogDismiss = () => {
this.setState({ dialogVisible: false });
};
onGameEnd = (time: number, score: number, isRestart: boolean) => {
const { props, state } = this;
this.setState({
gameTime: time,
const onGameEnd = (time: number, score: number, isRestart: boolean) => {
setGameState((prevState) => ({
...prevState,
gameScore: score,
});
}));
setGameTime(time);
const newScores = [...savedScores];
const isHighScore = newScores.length === 0 || score > newScores[0];
for (let i = 0; i < 3; i += 1) {
if (newScores.length > i && score > newScores[i]) {
newScores.splice(i, 0, score);
break;
} else if (newScores.length <= i) {
newScores.push(score);
break;
}
}
if (newScores.length > 3) {
newScores.splice(3, 1);
}
console.log(newScores);
updatePreferences(PreferenceKeys.gameScores, newScores);
if (!isRestart) {
props.navigation.replace(MainRoutes.GameStart, {
score: state.gameScore,
level: state.gameLevel,
time: state.gameTime,
navigation.replace(MainRoutes.GameStart, {
score: score,
level: gameState.gameLevel,
time: time,
isHighScore: isHighScore,
});
}
};
getStatusIcons() {
const { props, state } = this;
return (
<View
style={{
...GENERAL_STYLES.flex,
...GENERAL_STYLES.centerVertical,
}}
>
<View style={GENERAL_STYLES.centerHorizontal}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.time')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name="timer"
color={props.theme.colors.subtitle}
size={20}
/>
<Text
style={{
...styles.statusIcon,
color: props.theme.colors.subtitle,
}}
>
{GameMainScreen.getFormattedTime(state.gameTime)}
</Text>
</View>
</View>
<View style={styles.centerBigMargin}>
<Caption style={styles.centerSmallMargin}>
{i18n.t('screens.game.level')}
</Caption>
<View style={styles.statusContainer}>
<MaterialCommunityIcons
name="gamepad-square"
color={props.theme.colors.text}
size={20}
/>
<Text style={styles.statusIcon}>{state.gameLevel}</Text>
</View>
</View>
</View>
);
}
getScoreIcon() {
const { props, state } = this;
const highScore =
this.highScore == null || state.gameScore > this.highScore
? state.gameScore
: this.highScore;
return (
<View style={styles.scoreMainContainer}>
<View style={styles.scoreCurrentContainer}>
<Text style={styles.scoreText}>
{i18n.t('screens.game.score', { score: state.gameScore })}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={20}
style={styles.centerVerticalSmallMargin}
/>
</View>
<View style={styles.scoreBestContainer}>
<Text
style={{
...styles.scoreText,
color: props.theme.colors.textDisabled,
}}
>
{i18n.t('screens.game.highScore', { score: highScore })}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={10}
style={styles.centerVerticalSmallMargin}
/>
</View>
</View>
);
}
getControlButtons() {
const { props } = this;
return (
<View style={styles.controlsContainer}>
<IconButton
icon="rotate-right-variant"
size={40}
onPress={() => {
this.logic.rotatePressed(this.updateGrid);
}}
style={GENERAL_STYLES.flex}
/>
<View style={styles.directionsContainer}>
<IconButton
icon="chevron-left"
size={40}
style={GENERAL_STYLES.flex}
onPress={() => {
this.logic.pressedOut();
}}
onPressIn={() => {
this.logic.leftPressedIn(this.updateGrid);
}}
/>
<IconButton
icon="chevron-right"
size={40}
style={GENERAL_STYLES.flex}
onPress={() => {
this.logic.pressedOut();
}}
onPressIn={() => {
this.logic.rightPressed(this.updateGrid);
}}
/>
</View>
<IconButton
icon="arrow-down-bold"
size={40}
onPressIn={() => {
this.logic.downPressedIn(this.updateGridScore);
}}
onPress={() => {
this.logic.pressedOut();
}}
style={GENERAL_STYLES.flex}
color={props.theme.colors.tetrisScore}
/>
</View>
);
}
updateGrid = (newGrid: GridType) => {
this.setState({
grid: newGrid,
});
};
updateGridScore = (newGrid: GridType, score?: number) => {
this.setState((prevState: StateType): {
grid: GridType;
gameScore: number;
} => ({
const onDirectionPressed = (newGrid: GridType, score?: number) => {
setGameState((prevState) => ({
...prevState,
grid: newGrid,
gameScore: score != null ? score : prevState.gameScore,
}));
};
togglePause = () => {
this.logic.togglePause();
if (this.logic.isGamePaused()) {
this.showPausePopup();
const togglePause = () => {
logic.current.togglePause();
if (logic.current.isGamePaused()) {
showPausePopup();
}
};
showPausePopup = () => {
const showPausePopup = () => {
const onDismiss = () => {
this.togglePause();
this.onDialogDismiss();
togglePause();
onDialogDismiss();
};
this.setState({
dialogVisible: true,
setDialogContent({
dialogTitle: i18n.t('screens.game.pause'),
dialogMessage: i18n.t('screens.game.pauseMessage'),
dialogButtons: [
{
title: i18n.t('screens.game.restart.text'),
onPress: this.showRestartConfirm,
onPress: showRestartConfirm,
},
{
title: i18n.t('screens.game.resume'),
@ -392,71 +195,68 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
});
};
showRestartConfirm = () => {
this.setState({
dialogVisible: true,
const showRestartConfirm = () => {
setDialogContent({
dialogTitle: i18n.t('screens.game.restart.confirm'),
dialogMessage: i18n.t('screens.game.restart.confirmMessage'),
dialogButtons: [
{
title: i18n.t('screens.game.restart.confirmYes'),
onPress: () => {
this.onDialogDismiss();
this.startGame();
onDialogDismiss();
startGame();
},
},
{
title: i18n.t('screens.game.restart.confirmNo'),
onPress: this.showPausePopup,
onPress: showPausePopup,
},
],
onDialogDismiss: this.showPausePopup,
onDialogDismiss: showPausePopup,
});
};
startGame = () => {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
const startGame = () => {
logic.current.startGame(onTick, setGameTime, onGameEnd);
};
render() {
const { props, state } = this;
return (
<View style={GENERAL_STYLES.flex}>
<View style={styles.container}>
{this.getStatusIcons()}
<View style={styles.gridContainer}>
{this.getScoreIcon()}
<GridComponent
width={this.logic.getWidth()}
height={this.logic.getHeight()}
grid={state.grid}
style={{
backgroundColor: props.theme.colors.tetrisBackground,
...GENERAL_STYLES.flex,
...GENERAL_STYLES.centerHorizontal,
}}
/>
</View>
<View style={GENERAL_STYLES.flex}>
<Preview
items={this.logic.getNextPiecesPreviews()}
style={styles.preview}
/>
</View>
return (
<View style={GENERAL_STYLES.flex}>
<View style={styles.container}>
<GameStatus time={gameTime} level={gameState.gameLevel} />
<View style={styles.gridContainer}>
<GameScore score={gameState.gameScore} highScore={highScore} />
<GridComponent
width={logic.current.getWidth()}
height={logic.current.getHeight()}
grid={gameState.grid}
style={{
backgroundColor: theme.colors.tetrisBackground,
...GENERAL_STYLES.flex,
...GENERAL_STYLES.centerHorizontal,
}}
/>
</View>
<View style={GENERAL_STYLES.flex}>
<Preview
items={logic.current.getNextPiecesPreviews()}
style={styles.preview}
/>
</View>
{this.getControlButtons()}
<OptionsDialog
visible={state.dialogVisible}
title={state.dialogTitle}
message={state.dialogMessage}
buttons={state.dialogButtons}
onDismiss={state.onDialogDismiss}
/>
</View>
);
}
<GameControls
logic={logic.current}
onDirectionPressed={onDirectionPressed}
/>
{dialogContent ? (
<OptionsDialog
visible={dialogContent !== undefined}
title={dialogContent.dialogTitle}
message={dialogContent.dialogMessage}
buttons={dialogContent.dialogButtons}
onDismiss={dialogContent.onDialogDismiss}
/>
) : null}
</View>
);
}
export default withTheme(GameMainScreen);

View file

@ -18,478 +18,121 @@
*/
import * as React from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import {
Button,
Card,
Divider,
Headline,
Paragraph,
Text,
withTheme,
} from 'react-native-paper';
import { Button, useTheme } from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import LinearGradient from 'react-native-linear-gradient';
import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import MascotPopup from '../../../components/Mascot/MascotPopup';
import type { GridType } from '../components/GridComponent';
import GridComponent from '../components/GridComponent';
import GridManager from '../logic/GridManager';
import Piece from '../logic/Piece';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import GENERAL_STYLES from '../../../constants/Styles';
import GameBackground from '../components/GameBrackground';
import PostGameContent from '../components/PostGameContent';
import WelcomeGameContent from '../components/WelcomeGameContent';
import FullGamePodium from '../components/FullGamePodium';
import { useNavigation } from '@react-navigation/core';
import { usePreferences } from '../../../context/preferencesContext';
import {
getPreferenceObject,
PreferenceKeys,
} from '../../../utils/asyncStorage';
import { StackNavigationProp } from '@react-navigation/stack';
type GameStatsType = {
score: number;
level: number;
time: number;
isHighScore: boolean;
};
type PropsType = {
navigation: StackNavigationProp<any>;
type Props = {
route: {
params: GameStatsType;
params?: GameStatsType;
};
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({
pieceContainer: {
position: 'absolute',
width: '100%',
height: '100%',
},
pieceBackground: {
position: 'absolute',
},
playButton: {
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
},
recapCard: {
borderWidth: 2,
marginLeft: 20,
marginRight: 20,
},
recapContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
recapScoreContainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
},
recapScore: {
fontSize: 20,
},
recapScoreIcon: {
marginLeft: 5,
},
recapIcon: {
marginRight: 5,
marginLeft: 5,
},
welcomeMascot: {
width: '40%',
marginLeft: 'auto',
marginRight: 'auto',
},
welcomeCard: {
borderWidth: 2,
marginLeft: 10,
marginRight: 10,
},
centertext: {
textAlign: 'center',
},
welcomeText: {
textAlign: 'center',
marginTop: 10,
},
speechArrow: {
marginLeft: '60%',
},
podiumContainer: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
},
podiumIconContainer: {
position: 'absolute',
top: -20,
},
topScoreContainer: {
marginBottom: 20,
marginTop: 20,
},
topScoreSubcontainer: {
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
},
});
class GameStartScreen extends React.Component<PropsType> {
gridManager: GridManager;
export default function GameStartScreen(props: Props) {
const theme = useTheme();
const navigation = useNavigation<StackNavigationProp<any>>();
scores: Array<number>;
const { preferences } = usePreferences();
gameStats?: GameStatsType;
isHighScore: boolean;
constructor(props: PropsType) {
super(props);
this.isHighScore = false;
this.gridManager = new GridManager(4, 4, props.theme);
// TODO
// this.scores = AsyncStorageManager.getObject(
// AsyncStorageManager.PREFERENCES.gameScores.key
// );
this.scores = [];
this.scores.sort((a: number, b: number): number => b - a);
if (props.route.params != null) {
this.recoverGameScore();
function getScores() {
const pref = getPreferenceObject(PreferenceKeys.gameScores, preferences) as
| Array<number>
| undefined;
if (pref) {
return pref.sort((a, b) => b - a);
} else {
return [];
}
}
getPiecesBackground() {
const { theme } = this.props;
const gridList = [];
for (let i = 0; i < 18; i += 1) {
gridList.push(this.gridManager.getEmptyGrid(4, 4));
const piece = new Piece(theme);
piece.toGrid(gridList[i], true);
}
return (
<View style={styles.pieceContainer}>
{gridList.map((item: GridType, index: number) => {
const size = 10 + Math.floor(Math.random() * 30);
const top = Math.floor(Math.random() * 100);
const rot = Math.floor(Math.random() * 360);
const left = (index % 6) * 20;
const animDelay = size * 20;
const animDuration = 2 * (2000 - size * 30);
return (
<Animatable.View
useNativeDriver
animation="fadeInDownBig"
delay={animDelay}
duration={animDuration}
key={`piece${index.toString()}`}
style={{
width: `${size}%`,
top: `${top}%`,
left: `${left}%`,
...styles.pieceBackground,
}}
>
<GridComponent
width={4}
height={4}
grid={item}
style={{
transform: [{ rotateZ: `${rot}deg` }],
}}
/>
</Animatable.View>
);
})}
</View>
);
}
const scores = getScores();
const lastGameStats = props.route.params;
getPostGameContent(stats: GameStatsType) {
const { props } = this;
const width = this.isHighScore ? '50%' : '30%';
const margin = this.isHighScore ? 'auto' : undefined;
const marginLeft = this.isHighScore ? '60%' : '20%';
const color = this.isHighScore
? props.theme.colors.gameGold
: props.theme.colors.primary;
const getMainContent = () => {
return (
<View style={GENERAL_STYLES.flex}>
<Mascot
emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
animated={this.isHighScore}
style={{
width: width,
marginLeft: margin,
marginRight: margin,
}}
/>
<SpeechArrow
style={{ marginLeft: marginLeft }}
size={20}
color={props.theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: props.theme.colors.mascotMessageArrow,
...styles.recapCard,
}}
>
<Card.Content>
<Headline
style={{
color: color,
...styles.centertext,
}}
>
{this.isHighScore
? i18n.t('screens.game.newHighScore')
: i18n.t('screens.game.gameOver')}
</Headline>
<Divider />
<View style={styles.recapScoreContainer}>
<Text style={styles.recapScore}>
{i18n.t('screens.game.score', { score: stats.score })}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={30}
style={styles.recapScoreIcon}
/>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.level')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name="gamepad-square"
size={20}
color={props.theme.colors.textDisabled}
/>
<Text>{stats.level}</Text>
</View>
<View style={styles.recapContainer}>
<Text>{i18n.t('screens.game.time')}</Text>
<MaterialCommunityIcons
style={styles.recapIcon}
name="timer"
size={20}
color={props.theme.colors.textDisabled}
/>
<Text>{stats.time}</Text>
</View>
</Card.Content>
</Card>
</View>
);
}
getWelcomeText() {
const { props } = this;
return (
<View>
<Mascot emotion={MASCOT_STYLE.COOL} style={styles.welcomeMascot} />
<SpeechArrow
style={styles.speechArrow}
size={20}
color={props.theme.colors.mascotMessageArrow}
/>
<Card
style={{
borderColor: props.theme.colors.mascotMessageArrow,
...styles.welcomeCard,
}}
>
<Card.Content>
<Headline
style={{
color: props.theme.colors.primary,
...styles.centertext,
}}
>
{i18n.t('screens.game.welcomeTitle')}
</Headline>
<Divider />
<Paragraph style={styles.welcomeText}>
{i18n.t('screens.game.welcomeMessage')}
</Paragraph>
</Card.Content>
</Card>
</View>
);
}
getPodiumRender(place: 1 | 2 | 3, score: string) {
const { props } = this;
let icon = 'podium-gold';
let color = props.theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = 'podium-silver';
color = props.theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = 'podium-bronze';
color = props.theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
const marginLeft = place === 2 ? 20 : 'auto';
const marginRight = place === 3 ? 20 : 'auto';
const fontWeight = place === 1 ? 'bold' : undefined;
return (
<View
style={{
marginLeft: marginLeft,
marginRight: marginRight,
...styles.podiumContainer,
}}
>
{this.isHighScore && place === 1 ? (
<Animatable.View
animation="swing"
iterationCount="infinite"
duration={2000}
delay={1000}
useNativeDriver
style={styles.podiumIconContainer}
>
<Animatable.View
animation="pulse"
iterationCount="infinite"
useNativeDriver
>
<MaterialCommunityIcons
name="decagram"
color={props.theme.colors.gameGold}
size={150}
/>
</Animatable.View>
</Animatable.View>
) : null}
<MaterialCommunityIcons
name={icon}
color={this.isHighScore && place === 1 ? '#fff' : color}
size={size}
/>
<Text
style={{
fontWeight: fontWeight,
fontSize,
...styles.centertext,
}}
>
{score}
</Text>
</View>
);
}
getTopScoresRender() {
const gold = this.scores.length > 0 ? this.scores[0] : '-';
const silver = this.scores.length > 1 ? this.scores[1] : '-';
const bronze = this.scores.length > 2 ? this.scores[2] : '-';
return (
<View style={styles.topScoreContainer}>
{this.getPodiumRender(1, gold.toString())}
<View style={styles.topScoreSubcontainer}>
{this.getPodiumRender(3, bronze.toString())}
{this.getPodiumRender(2, silver.toString())}
</View>
</View>
);
}
getMainContent() {
const { props } = this;
return (
<View style={GENERAL_STYLES.flex}>
{this.gameStats != null
? this.getPostGameContent(this.gameStats)
: this.getWelcomeText()}
{lastGameStats ? (
<PostGameContent
stats={lastGameStats}
isHighScore={lastGameStats.isHighScore}
/>
) : (
<WelcomeGameContent />
)}
<Button
icon="play"
mode="contained"
icon={'play'}
mode={'contained'}
onPress={() => {
props.navigation.replace('game-main', {
highScore: this.scores.length > 0 ? this.scores[0] : null,
});
navigation.replace('game-main');
}}
style={styles.playButton}
>
{i18n.t('screens.game.play')}
</Button>
{this.getTopScoresRender()}
<FullGamePodium
scores={scores}
isHighScore={lastGameStats?.isHighScore === true}
/>
</View>
);
}
};
keyExtractor = (item: number): string => item.toString();
recoverGameScore() {
const { route } = this.props;
this.gameStats = route.params;
if (this.gameStats.score != null) {
this.isHighScore =
this.scores.length === 0 || this.gameStats.score > this.scores[0];
for (let i = 0; i < 3; i += 1) {
if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
this.scores.splice(i, 0, this.gameStats.score);
break;
} else if (this.scores.length <= i) {
this.scores.push(this.gameStats.score);
break;
}
}
if (this.scores.length > 3) {
this.scores.splice(3, 1);
}
// TODO
// AsyncStorageManager.set(
// AsyncStorageManager.PREFERENCES.gameScores.key,
// this.scores
// );
}
}
render() {
const { props } = this;
return (
<View style={GENERAL_STYLES.flex}>
{this.getPiecesBackground()}
<LinearGradient
style={GENERAL_STYLES.flex}
colors={[
`${props.theme.colors.background}00`,
props.theme.colors.background,
]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<CollapsibleScrollView headerColors={'transparent'}>
{this.getMainContent()}
<MascotPopup
title={i18n.t('screens.game.mascotDialog.title')}
message={i18n.t('screens.game.mascotDialog.message')}
icon="gamepad-variant"
buttons={{
cancel: {
message: i18n.t('screens.game.mascotDialog.button'),
icon: 'check',
},
}}
emotion={MASCOT_STYLE.COOL}
/>
</CollapsibleScrollView>
</LinearGradient>
</View>
);
}
return (
<View style={GENERAL_STYLES.flex}>
<GameBackground />
<LinearGradient
style={GENERAL_STYLES.flex}
colors={[`${theme.colors.background}00`, theme.colors.background]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<CollapsibleScrollView headerColors={'transparent'}>
{getMainContent()}
<MascotPopup
title={i18n.t('screens.game.mascotDialog.title')}
message={i18n.t('screens.game.mascotDialog.message')}
icon="gamepad-variant"
buttons={{
cancel: {
message: i18n.t('screens.game.mascotDialog.button'),
icon: 'check',
},
}}
emotion={MASCOT_STYLE.COOL}
/>
</CollapsibleScrollView>
</LinearGradient>
</View>
);
}
export default withTheme(GameStartScreen);