Convert game into functional component
This commit is contained in:
parent
14365a92a4
commit
9ae585bdf8
10 changed files with 856 additions and 770 deletions
44
src/screens/Game/components/FullGamePodium.tsx
Normal file
44
src/screens/Game/components/FullGamePodium.tsx
Normal 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>
|
||||
);
|
||||
}
|
65
src/screens/Game/components/GameBrackground.tsx
Normal file
65
src/screens/Game/components/GameBrackground.tsx
Normal 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>
|
||||
);
|
||||
}
|
62
src/screens/Game/components/GameControls.tsx
Normal file
62
src/screens/Game/components/GameControls.tsx
Normal 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);
|
95
src/screens/Game/components/GamePodium.tsx
Normal file
95
src/screens/Game/components/GamePodium.tsx
Normal 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>
|
||||
);
|
||||
}
|
81
src/screens/Game/components/GameScore.tsx
Normal file
81
src/screens/Game/components/GameScore.tsx
Normal 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
|
||||
);
|
96
src/screens/Game/components/GameStatus.tsx
Normal file
96
src/screens/Game/components/GameStatus.tsx
Normal 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
|
||||
);
|
130
src/screens/Game/components/PostGameContent.tsx
Normal file
130
src/screens/Game/components/PostGameContent.tsx
Normal 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>
|
||||
);
|
||||
}
|
70
src/screens/Game/components/WelcomeGameContent.tsx
Normal file
70
src/screens/Game/components/WelcomeGameContent.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue