Browse Source

Convert game into functional component

Arnaud Vergnet 6 months ago
parent
commit
9ae585bdf8

+ 44
- 0
src/screens/Game/components/FullGamePodium.tsx View File

@@ -0,0 +1,44 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import { GamePodium } from './GamePodium';
4
+
5
+type Props = {
6
+  scores: Array<number>;
7
+  isHighScore: boolean;
8
+};
9
+
10
+const styles = StyleSheet.create({
11
+  topScoreContainer: {
12
+    marginBottom: 20,
13
+    marginTop: 20,
14
+  },
15
+  topScoreSubcontainer: {
16
+    flexDirection: 'row',
17
+    marginLeft: 'auto',
18
+    marginRight: 'auto',
19
+  },
20
+});
21
+
22
+export default function FullGamePodium(props: Props) {
23
+  const { scores, isHighScore } = props;
24
+  const gold = scores.length > 0 ? scores[0] : '-';
25
+  const silver = scores.length > 1 ? scores[1] : '-';
26
+  const bronze = scores.length > 2 ? scores[2] : '-';
27
+  return (
28
+    <View style={styles.topScoreContainer}>
29
+      <GamePodium place={1} score={gold.toString()} isHighScore={isHighScore} />
30
+      <View style={styles.topScoreSubcontainer}>
31
+        <GamePodium
32
+          place={3}
33
+          score={silver.toString()}
34
+          isHighScore={isHighScore}
35
+        />
36
+        <GamePodium
37
+          place={2}
38
+          score={bronze.toString()}
39
+          isHighScore={isHighScore}
40
+        />
41
+      </View>
42
+    </View>
43
+  );
44
+}

+ 65
- 0
src/screens/Game/components/GameBrackground.tsx View File

@@ -0,0 +1,65 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import * as Animatable from 'react-native-animatable';
4
+import { useTheme } from 'react-native-paper';
5
+import GridManager from '../logic/GridManager';
6
+import Piece from '../logic/Piece';
7
+import GridComponent from './GridComponent';
8
+
9
+const styles = StyleSheet.create({
10
+  pieceContainer: {
11
+    position: 'absolute',
12
+    width: '100%',
13
+    height: '100%',
14
+  },
15
+  pieceBackground: {
16
+    position: 'absolute',
17
+  },
18
+});
19
+
20
+export default function GameBackground() {
21
+  const theme = useTheme();
22
+  const gridManager = new GridManager(4, 4, theme);
23
+  const gridList = [];
24
+  for (let i = 0; i < 18; i += 1) {
25
+    gridList.push(gridManager.getEmptyGrid(4, 4));
26
+    const piece = new Piece(theme);
27
+    piece.toGrid(gridList[i], true);
28
+  }
29
+  return (
30
+    <View style={styles.pieceContainer}>
31
+      {gridList.map((item, index) => {
32
+        const size = 10 + Math.floor(Math.random() * 30);
33
+        const top = Math.floor(Math.random() * 100);
34
+        const rot = Math.floor(Math.random() * 360);
35
+        const left = (index % 6) * 20;
36
+        const animDelay = size * 20;
37
+        const animDuration = 2 * (2000 - size * 30);
38
+        return (
39
+          <Animatable.View
40
+            useNativeDriver={true}
41
+            animation={'fadeInDownBig'}
42
+            delay={animDelay}
43
+            duration={animDuration}
44
+            key={`piece${index.toString()}`}
45
+            style={{
46
+              width: `${size}%`,
47
+              top: `${top}%`,
48
+              left: `${left}%`,
49
+              ...styles.pieceBackground,
50
+            }}
51
+          >
52
+            <GridComponent
53
+              width={4}
54
+              height={4}
55
+              grid={item}
56
+              style={{
57
+                transform: [{ rotateZ: `${rot}deg` }],
58
+              }}
59
+            />
60
+          </Animatable.View>
61
+        );
62
+      })}
63
+    </View>
64
+  );
65
+}

+ 62
- 0
src/screens/Game/components/GameControls.tsx View File

@@ -0,0 +1,62 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import { IconButton, useTheme } from 'react-native-paper';
4
+import GENERAL_STYLES from '../../../constants/Styles';
5
+import GameLogic, { MovementCallbackType } from '../logic/GameLogic';
6
+
7
+type Props = {
8
+  logic: GameLogic;
9
+  onDirectionPressed: MovementCallbackType;
10
+};
11
+
12
+const styles = StyleSheet.create({
13
+  controlsContainer: {
14
+    height: 80,
15
+    flexDirection: 'row',
16
+  },
17
+  directionsContainer: {
18
+    flexDirection: 'row',
19
+    flex: 4,
20
+  },
21
+});
22
+
23
+function GameControls(props: Props) {
24
+  const { logic } = props;
25
+  const theme = useTheme();
26
+  return (
27
+    <View style={styles.controlsContainer}>
28
+      <IconButton
29
+        icon={'rotate-right-variant'}
30
+        size={40}
31
+        onPress={() => logic.rotatePressed(props.onDirectionPressed)}
32
+        style={GENERAL_STYLES.flex}
33
+      />
34
+      <View style={styles.directionsContainer}>
35
+        <IconButton
36
+          icon={'chevron-left'}
37
+          size={40}
38
+          style={GENERAL_STYLES.flex}
39
+          onPress={() => logic.pressedOut()}
40
+          onPressIn={() => logic.leftPressedIn(props.onDirectionPressed)}
41
+        />
42
+        <IconButton
43
+          icon={'chevron-right'}
44
+          size={40}
45
+          style={GENERAL_STYLES.flex}
46
+          onPress={() => logic.pressedOut()}
47
+          onPressIn={() => logic.rightPressed(props.onDirectionPressed)}
48
+        />
49
+      </View>
50
+      <IconButton
51
+        icon={'arrow-down-bold'}
52
+        size={40}
53
+        onPressIn={() => logic.downPressedIn(props.onDirectionPressed)}
54
+        onPress={() => logic.pressedOut()}
55
+        style={GENERAL_STYLES.flex}
56
+        color={theme.colors.tetrisScore}
57
+      />
58
+    </View>
59
+  );
60
+}
61
+
62
+export default React.memo(GameControls, () => true);

+ 95
- 0
src/screens/Game/components/GamePodium.tsx View File

@@ -0,0 +1,95 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import { Text, useTheme } from 'react-native-paper';
4
+import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
5
+import * as Animatable from 'react-native-animatable';
6
+
7
+type Props = {
8
+  place: 1 | 2 | 3;
9
+  score: string;
10
+  isHighScore: boolean;
11
+};
12
+
13
+const styles = StyleSheet.create({
14
+  podiumContainer: {
15
+    flexDirection: 'column',
16
+    alignItems: 'center',
17
+    justifyContent: 'flex-end',
18
+  },
19
+  podiumIconContainer: {
20
+    position: 'absolute',
21
+    top: -20,
22
+  },
23
+  centertext: {
24
+    textAlign: 'center',
25
+  },
26
+});
27
+
28
+export function GamePodium(props: Props) {
29
+  const { place, score, isHighScore } = props;
30
+  const theme = useTheme();
31
+  let icon = 'podium-gold';
32
+  let color = theme.colors.gameGold;
33
+  let fontSize = 20;
34
+  let size = 70;
35
+  if (place === 2) {
36
+    icon = 'podium-silver';
37
+    color = theme.colors.gameSilver;
38
+    fontSize = 18;
39
+    size = 60;
40
+  } else if (place === 3) {
41
+    icon = 'podium-bronze';
42
+    color = theme.colors.gameBronze;
43
+    fontSize = 15;
44
+    size = 50;
45
+  }
46
+  const marginLeft = place === 2 ? 20 : 'auto';
47
+  const marginRight = place === 3 ? 20 : 'auto';
48
+  const fontWeight = place === 1 ? 'bold' : undefined;
49
+  return (
50
+    <View
51
+      style={{
52
+        marginLeft: marginLeft,
53
+        marginRight: marginRight,
54
+        ...styles.podiumContainer,
55
+      }}
56
+    >
57
+      {isHighScore && place === 1 ? (
58
+        <Animatable.View
59
+          animation="swing"
60
+          iterationCount="infinite"
61
+          duration={2000}
62
+          delay={1000}
63
+          useNativeDriver
64
+          style={styles.podiumIconContainer}
65
+        >
66
+          <Animatable.View
67
+            animation="pulse"
68
+            iterationCount="infinite"
69
+            useNativeDriver
70
+          >
71
+            <MaterialCommunityIcons
72
+              name="decagram"
73
+              color={theme.colors.gameGold}
74
+              size={150}
75
+            />
76
+          </Animatable.View>
77
+        </Animatable.View>
78
+      ) : null}
79
+      <MaterialCommunityIcons
80
+        name={icon}
81
+        color={isHighScore && place === 1 ? '#fff' : color}
82
+        size={size}
83
+      />
84
+      <Text
85
+        style={{
86
+          fontWeight: fontWeight,
87
+          fontSize,
88
+          ...styles.centertext,
89
+        }}
90
+      >
91
+        {score}
92
+      </Text>
93
+    </View>
94
+  );
95
+}

+ 81
- 0
src/screens/Game/components/GameScore.tsx View File

@@ -0,0 +1,81 @@
1
+import React from 'react';
2
+import { View } from 'react-native';
3
+import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
4
+import i18n from 'i18n-js';
5
+import { Text, useTheme } from 'react-native-paper';
6
+import { StyleSheet } from 'react-native';
7
+import GENERAL_STYLES from '../../../constants/Styles';
8
+
9
+type Props = {
10
+  score: number;
11
+  highScore?: number;
12
+};
13
+
14
+const styles = StyleSheet.create({
15
+  scoreMainContainer: {
16
+    marginTop: 10,
17
+    marginBottom: 10,
18
+  },
19
+  scoreCurrentContainer: {
20
+    flexDirection: 'row',
21
+    marginLeft: 'auto',
22
+    marginRight: 'auto',
23
+  },
24
+  scoreText: {
25
+    marginLeft: 5,
26
+    fontSize: 20,
27
+  },
28
+  scoreBestContainer: {
29
+    flexDirection: 'row',
30
+    marginLeft: 'auto',
31
+    marginRight: 'auto',
32
+    marginTop: 5,
33
+  },
34
+  centerVerticalSmallMargin: {
35
+    ...GENERAL_STYLES.centerVertical,
36
+    marginLeft: 5,
37
+  },
38
+});
39
+
40
+function GameScore(props: Props) {
41
+  const { score, highScore } = props;
42
+  const theme = useTheme();
43
+  const displayHighScore =
44
+    highScore == null || score > highScore ? score : highScore;
45
+  return (
46
+    <View style={styles.scoreMainContainer}>
47
+      <View style={styles.scoreCurrentContainer}>
48
+        <Text style={styles.scoreText}>
49
+          {i18n.t('screens.game.score', { score: score })}
50
+        </Text>
51
+        <MaterialCommunityIcons
52
+          name="star"
53
+          color={theme.colors.tetrisScore}
54
+          size={20}
55
+          style={styles.centerVerticalSmallMargin}
56
+        />
57
+      </View>
58
+      <View style={styles.scoreBestContainer}>
59
+        <Text
60
+          style={{
61
+            ...styles.scoreText,
62
+            color: theme.colors.textDisabled,
63
+          }}
64
+        >
65
+          {i18n.t('screens.game.highScore', { score: displayHighScore })}
66
+        </Text>
67
+        <MaterialCommunityIcons
68
+          name="star"
69
+          color={theme.colors.tetrisScore}
70
+          size={10}
71
+          style={styles.centerVerticalSmallMargin}
72
+        />
73
+      </View>
74
+    </View>
75
+  );
76
+}
77
+
78
+export default React.memo(
79
+  GameScore,
80
+  (pp, np) => pp.highScore === np.highScore && pp.score === np.score
81
+);

+ 96
- 0
src/screens/Game/components/GameStatus.tsx View File

@@ -0,0 +1,96 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import { Caption, Text, useTheme } from 'react-native-paper';
4
+import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
5
+import GENERAL_STYLES from '../../../constants/Styles';
6
+import i18n from 'i18n-js';
7
+
8
+type Props = {
9
+  time: number;
10
+  level: number;
11
+};
12
+
13
+const styles = StyleSheet.create({
14
+  centerSmallMargin: {
15
+    ...GENERAL_STYLES.centerHorizontal,
16
+    marginBottom: 5,
17
+  },
18
+  centerBigMargin: {
19
+    marginLeft: 'auto',
20
+    marginRight: 'auto',
21
+    marginBottom: 20,
22
+  },
23
+  statusContainer: {
24
+    flexDirection: 'row',
25
+  },
26
+  statusIcon: {
27
+    marginLeft: 5,
28
+  },
29
+});
30
+
31
+function getFormattedTime(seconds: number): string {
32
+  const date = new Date();
33
+  date.setHours(0);
34
+  date.setMinutes(0);
35
+  date.setSeconds(seconds);
36
+  let format;
37
+  if (date.getHours()) {
38
+    format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
39
+  } else if (date.getMinutes()) {
40
+    format = `${date.getMinutes()}:${date.getSeconds()}`;
41
+  } else {
42
+    format = date.getSeconds().toString();
43
+  }
44
+  return format;
45
+}
46
+
47
+function GameStatus(props: Props) {
48
+  const theme = useTheme();
49
+  return (
50
+    <View
51
+      style={{
52
+        ...GENERAL_STYLES.flex,
53
+        ...GENERAL_STYLES.centerVertical,
54
+      }}
55
+    >
56
+      <View style={GENERAL_STYLES.centerHorizontal}>
57
+        <Caption style={styles.centerSmallMargin}>
58
+          {i18n.t('screens.game.time')}
59
+        </Caption>
60
+        <View style={styles.statusContainer}>
61
+          <MaterialCommunityIcons
62
+            name={'timer'}
63
+            color={theme.colors.subtitle}
64
+            size={20}
65
+          />
66
+          <Text
67
+            style={{
68
+              ...styles.statusIcon,
69
+              color: theme.colors.subtitle,
70
+            }}
71
+          >
72
+            {getFormattedTime(props.time)}
73
+          </Text>
74
+        </View>
75
+      </View>
76
+      <View style={styles.centerBigMargin}>
77
+        <Caption style={styles.centerSmallMargin}>
78
+          {i18n.t('screens.game.level')}
79
+        </Caption>
80
+        <View style={styles.statusContainer}>
81
+          <MaterialCommunityIcons
82
+            name={'gamepad-square'}
83
+            color={theme.colors.text}
84
+            size={20}
85
+          />
86
+          <Text style={styles.statusIcon}>{props.level}</Text>
87
+        </View>
88
+      </View>
89
+    </View>
90
+  );
91
+}
92
+
93
+export default React.memo(
94
+  GameStatus,
95
+  (pp, np) => pp.level === np.level && pp.time === np.time
96
+);

+ 130
- 0
src/screens/Game/components/PostGameContent.tsx View File

@@ -0,0 +1,130 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import { Card, Divider, Headline, Text, useTheme } from 'react-native-paper';
4
+import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
5
+import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
6
+import SpeechArrow from '../../../components/Mascot/SpeechArrow';
7
+import GENERAL_STYLES from '../../../constants/Styles';
8
+import i18n from 'i18n-js';
9
+
10
+type GameStatsType = {
11
+  score: number;
12
+  level: number;
13
+  time: number;
14
+};
15
+
16
+type Props = {
17
+  isHighScore: boolean;
18
+  stats: GameStatsType;
19
+};
20
+
21
+const styles = StyleSheet.create({
22
+  recapCard: {
23
+    borderWidth: 2,
24
+    marginLeft: 20,
25
+    marginRight: 20,
26
+  },
27
+  recapContainer: {
28
+    flexDirection: 'row',
29
+    marginLeft: 'auto',
30
+    marginRight: 'auto',
31
+  },
32
+  recapScoreContainer: {
33
+    flexDirection: 'row',
34
+    marginLeft: 'auto',
35
+    marginRight: 'auto',
36
+    marginTop: 10,
37
+    marginBottom: 10,
38
+  },
39
+  recapScore: {
40
+    fontSize: 20,
41
+  },
42
+  recapScoreIcon: {
43
+    marginLeft: 5,
44
+  },
45
+  recapIcon: {
46
+    marginRight: 5,
47
+    marginLeft: 5,
48
+  },
49
+  centertext: {
50
+    textAlign: 'center',
51
+  },
52
+});
53
+
54
+export default function PostGameContent(props: Props) {
55
+  const { isHighScore, stats } = props;
56
+  const theme = useTheme();
57
+  const width = isHighScore ? '50%' : '30%';
58
+  const margin = isHighScore ? 'auto' : undefined;
59
+  const marginLeft = isHighScore ? '60%' : '20%';
60
+  const color = isHighScore ? theme.colors.gameGold : theme.colors.primary;
61
+  return (
62
+    <View style={GENERAL_STYLES.flex}>
63
+      <Mascot
64
+        emotion={isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
65
+        animated={isHighScore}
66
+        style={{
67
+          width: width,
68
+          marginLeft: margin,
69
+          marginRight: margin,
70
+        }}
71
+      />
72
+      <SpeechArrow
73
+        style={{ marginLeft: marginLeft }}
74
+        size={20}
75
+        color={theme.colors.mascotMessageArrow}
76
+      />
77
+      <Card
78
+        style={{
79
+          borderColor: theme.colors.mascotMessageArrow,
80
+          ...styles.recapCard,
81
+        }}
82
+      >
83
+        <Card.Content>
84
+          <Headline
85
+            style={{
86
+              color: color,
87
+              ...styles.centertext,
88
+            }}
89
+          >
90
+            {isHighScore
91
+              ? i18n.t('screens.game.newHighScore')
92
+              : i18n.t('screens.game.gameOver')}
93
+          </Headline>
94
+          <Divider />
95
+          <View style={styles.recapScoreContainer}>
96
+            <Text style={styles.recapScore}>
97
+              {i18n.t('screens.game.score', { score: stats.score })}
98
+            </Text>
99
+            <MaterialCommunityIcons
100
+              name={'star'}
101
+              color={theme.colors.tetrisScore}
102
+              size={30}
103
+              style={styles.recapScoreIcon}
104
+            />
105
+          </View>
106
+          <View style={styles.recapContainer}>
107
+            <Text>{i18n.t('screens.game.level')}</Text>
108
+            <MaterialCommunityIcons
109
+              style={styles.recapIcon}
110
+              name={'gamepad-square'}
111
+              size={20}
112
+              color={theme.colors.textDisabled}
113
+            />
114
+            <Text>{stats.level}</Text>
115
+          </View>
116
+          <View style={styles.recapContainer}>
117
+            <Text>{i18n.t('screens.game.time')}</Text>
118
+            <MaterialCommunityIcons
119
+              style={styles.recapIcon}
120
+              name={'timer'}
121
+              size={20}
122
+              color={theme.colors.textDisabled}
123
+            />
124
+            <Text>{stats.time}</Text>
125
+          </View>
126
+        </Card.Content>
127
+      </Card>
128
+    </View>
129
+  );
130
+}

+ 70
- 0
src/screens/Game/components/WelcomeGameContent.tsx View File

@@ -0,0 +1,70 @@
1
+import React from 'react';
2
+import { StyleSheet, View } from 'react-native';
3
+import {
4
+  Card,
5
+  Divider,
6
+  Headline,
7
+  Paragraph,
8
+  useTheme,
9
+} from 'react-native-paper';
10
+import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
11
+import SpeechArrow from '../../../components/Mascot/SpeechArrow';
12
+import i18n from 'i18n-js';
13
+
14
+const styles = StyleSheet.create({
15
+  welcomeMascot: {
16
+    width: '40%',
17
+    marginLeft: 'auto',
18
+    marginRight: 'auto',
19
+  },
20
+  welcomeCard: {
21
+    borderWidth: 2,
22
+    marginLeft: 10,
23
+    marginRight: 10,
24
+  },
25
+  speechArrow: {
26
+    marginLeft: '60%',
27
+  },
28
+  welcomeText: {
29
+    textAlign: 'center',
30
+    marginTop: 10,
31
+  },
32
+  centertext: {
33
+    textAlign: 'center',
34
+  },
35
+});
36
+
37
+export default function WelcomeGameContent() {
38
+  const theme = useTheme();
39
+  return (
40
+    <View>
41
+      <Mascot emotion={MASCOT_STYLE.COOL} style={styles.welcomeMascot} />
42
+      <SpeechArrow
43
+        style={styles.speechArrow}
44
+        size={20}
45
+        color={theme.colors.mascotMessageArrow}
46
+      />
47
+      <Card
48
+        style={{
49
+          borderColor: theme.colors.mascotMessageArrow,
50
+          ...styles.welcomeCard,
51
+        }}
52
+      >
53
+        <Card.Content>
54
+          <Headline
55
+            style={{
56
+              color: theme.colors.primary,
57
+              ...styles.centertext,
58
+            }}
59
+          >
60
+            {i18n.t('screens.game.welcomeTitle')}
61
+          </Headline>
62
+          <Divider />
63
+          <Paragraph style={styles.welcomeText}>
64
+            {i18n.t('screens.game.welcomeMessage')}
65
+          </Paragraph>
66
+        </Card.Content>
67
+      </Card>
68
+    </View>
69
+  );
70
+}

+ 136
- 336
src/screens/Game/screens/GameMainScreen.tsx View File

@@ -17,10 +17,9 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
20
+import React, { useLayoutEffect, useRef, useState } from 'react';
21 21
 import { StyleSheet, View } from 'react-native';
22
-import { Caption, IconButton, Text, withTheme } from 'react-native-paper';
23
-import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
22
+import { useTheme } from 'react-native-paper';
24 23
 import i18n from 'i18n-js';
25 24
 import { StackNavigationProp } from '@react-navigation/stack';
26 25
 import GameLogic from '../logic/GameLogic';
@@ -34,25 +33,15 @@ import type { OptionsDialogButtonType } from '../../../components/Dialogs/Option
34 33
 import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
35 34
 import GENERAL_STYLES from '../../../constants/Styles';
36 35
 import { MainRoutes } from '../../../navigation/MainNavigator';
37
-
38
-type PropsType = {
39
-  navigation: StackNavigationProp<any>;
40
-  route: { params: { highScore: number } };
41
-  theme: ReactNativePaper.Theme;
42
-};
43
-
44
-type StateType = {
45
-  grid: GridType;
46
-  gameTime: number;
47
-  gameScore: number;
48
-  gameLevel: number;
49
-
50
-  dialogVisible: boolean;
51
-  dialogTitle: string;
52
-  dialogMessage: string;
53
-  dialogButtons: Array<OptionsDialogButtonType>;
54
-  onDialogDismiss: () => void;
55
-};
36
+import GameStatus from '../components/GameStatus';
37
+import GameControls from '../components/GameControls';
38
+import GameScore from '../components/GameScore';
39
+import { usePreferences } from '../../../context/preferencesContext';
40
+import {
41
+  getPreferenceObject,
42
+  PreferenceKeys,
43
+} from '../../../utils/asyncStorage';
44
+import { useNavigation } from '@react-navigation/core';
56 45
 
57 46
 const styles = StyleSheet.create({
58 47
   container: {
@@ -62,44 +51,6 @@ const styles = StyleSheet.create({
62 51
   gridContainer: {
63 52
     flex: 4,
64 53
   },
65
-  centerSmallMargin: {
66
-    ...GENERAL_STYLES.centerHorizontal,
67
-    marginBottom: 5,
68
-  },
69
-  centerVerticalSmallMargin: {
70
-    ...GENERAL_STYLES.centerVertical,
71
-    marginLeft: 5,
72
-  },
73
-  centerBigMargin: {
74
-    marginLeft: 'auto',
75
-    marginRight: 'auto',
76
-    marginBottom: 20,
77
-  },
78
-  statusContainer: {
79
-    flexDirection: 'row',
80
-  },
81
-  statusIcon: {
82
-    marginLeft: 5,
83
-  },
84
-  scoreMainContainer: {
85
-    marginTop: 10,
86
-    marginBottom: 10,
87
-  },
88
-  scoreCurrentContainer: {
89
-    flexDirection: 'row',
90
-    marginLeft: 'auto',
91
-    marginRight: 'auto',
92
-  },
93
-  scoreText: {
94
-    marginLeft: 5,
95
-    fontSize: 20,
96
-  },
97
-  scoreBestContainer: {
98
-    flexDirection: 'row',
99
-    marginLeft: 'auto',
100
-    marginRight: 'auto',
101
-    marginTop: 5,
102
-  },
103 54
   controlsContainer: {
104 55
     height: 80,
105 56
     flexDirection: 'row',
@@ -115,273 +66,125 @@ const styles = StyleSheet.create({
115 66
   },
116 67
 });
117 68
 
118
-class GameMainScreen extends React.Component<PropsType, StateType> {
119
-  static getFormattedTime(seconds: number): string {
120
-    const date = new Date();
121
-    date.setHours(0);
122
-    date.setMinutes(0);
123
-    date.setSeconds(seconds);
124
-    let format;
125
-    if (date.getHours()) {
126
-      format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
127
-    } else if (date.getMinutes()) {
128
-      format = `${date.getMinutes()}:${date.getSeconds()}`;
129
-    } else {
130
-      format = date.getSeconds().toString();
131
-    }
132
-    return format;
133
-  }
69
+export default function GameMainScreen() {
70
+  const theme = useTheme();
71
+  const navigation = useNavigation<StackNavigationProp<any>>();
72
+  const logic = useRef(new GameLogic(20, 10, theme));
134 73
 
135
-  logic: GameLogic;
74
+  const [gameTime, setGameTime] = useState(0);
136 75
 
137
-  highScore: number | null;
76
+  const [gameState, setGameState] = useState({
77
+    grid: logic.current.getCurrentGrid(),
78
+    gameScore: 0,
79
+    gameLevel: 0,
80
+  });
138 81
 
139
-  constructor(props: PropsType) {
140
-    super(props);
141
-    this.highScore = null;
142
-    this.logic = new GameLogic(20, 10, props.theme);
143
-    this.state = {
144
-      grid: this.logic.getCurrentGrid(),
145
-      gameTime: 0,
146
-      gameScore: 0,
147
-      gameLevel: 0,
148
-      dialogVisible: false,
149
-      dialogTitle: '',
150
-      dialogMessage: '',
151
-      dialogButtons: [],
152
-      onDialogDismiss: () => {},
153
-    };
154
-    if (props.route.params != null) {
155
-      this.highScore = props.route.params.highScore;
82
+  const [dialogContent, setDialogContent] = useState<{
83
+    dialogTitle: string;
84
+    dialogMessage: string;
85
+    dialogButtons: Array<OptionsDialogButtonType>;
86
+    onDialogDismiss: () => void;
87
+  }>();
88
+
89
+  const { preferences, updatePreferences } = usePreferences();
90
+
91
+  function getScores() {
92
+    const pref = getPreferenceObject(PreferenceKeys.gameScores, preferences) as
93
+      | Array<number>
94
+      | undefined;
95
+    if (pref) {
96
+      return pref.sort((a, b) => b - a);
97
+    } else {
98
+      return [];
156 99
     }
157 100
   }
158 101
 
159
-  componentDidMount() {
160
-    const { navigation } = this.props;
102
+  const savedScores = getScores();
103
+  const highScore = savedScores.length > 0 ? savedScores[0] : undefined;
104
+
105
+  useLayoutEffect(() => {
161 106
     navigation.setOptions({
162
-      headerRight: this.getRightButton,
107
+      headerRight: getRightButton,
163 108
     });
164
-    this.startGame();
165
-  }
109
+    startGame();
110
+    // eslint-disable-next-line react-hooks/exhaustive-deps
111
+  }, [navigation]);
166 112
 
167
-  componentWillUnmount() {
168
-    this.logic.endGame(true);
169
-  }
113
+  const getRightButton = () => (
114
+    <MaterialHeaderButtons>
115
+      <Item title={'pause'} iconName={'pause'} onPress={togglePause} />
116
+    </MaterialHeaderButtons>
117
+  );
170 118
 
171
-  getRightButton = () => {
172
-    return (
173
-      <MaterialHeaderButtons>
174
-        <Item title="pause" iconName="pause" onPress={this.togglePause} />
175
-      </MaterialHeaderButtons>
176
-    );
177
-  };
178
-
179
-  onTick = (score: number, level: number, newGrid: GridType) => {
180
-    this.setState({
119
+  const onTick = (score: number, level: number, newGrid: GridType) => {
120
+    setGameState({
181 121
       gameScore: score,
182 122
       gameLevel: level,
183 123
       grid: newGrid,
184 124
     });
185 125
   };
186 126
 
187
-  onClock = (time: number) => {
188
-    this.setState({
189
-      gameTime: time,
190
-    });
191
-  };
192
-
193
-  onDialogDismiss = () => {
194
-    this.setState({ dialogVisible: false });
195
-  };
127
+  const onDialogDismiss = () => setDialogContent(undefined);
196 128
 
197
-  onGameEnd = (time: number, score: number, isRestart: boolean) => {
198
-    const { props, state } = this;
199
-    this.setState({
200
-      gameTime: time,
129
+  const onGameEnd = (time: number, score: number, isRestart: boolean) => {
130
+    setGameState((prevState) => ({
131
+      ...prevState,
201 132
       gameScore: score,
202
-    });
133
+    }));
134
+    setGameTime(time);
135
+    const newScores = [...savedScores];
136
+    const isHighScore = newScores.length === 0 || score > newScores[0];
137
+    for (let i = 0; i < 3; i += 1) {
138
+      if (newScores.length > i && score > newScores[i]) {
139
+        newScores.splice(i, 0, score);
140
+        break;
141
+      } else if (newScores.length <= i) {
142
+        newScores.push(score);
143
+        break;
144
+      }
145
+    }
146
+    if (newScores.length > 3) {
147
+      newScores.splice(3, 1);
148
+    }
149
+    console.log(newScores);
150
+    updatePreferences(PreferenceKeys.gameScores, newScores);
203 151
     if (!isRestart) {
204
-      props.navigation.replace(MainRoutes.GameStart, {
205
-        score: state.gameScore,
206
-        level: state.gameLevel,
207
-        time: state.gameTime,
152
+      navigation.replace(MainRoutes.GameStart, {
153
+        score: score,
154
+        level: gameState.gameLevel,
155
+        time: time,
156
+        isHighScore: isHighScore,
208 157
       });
209 158
     }
210 159
   };
211 160
 
212
-  getStatusIcons() {
213
-    const { props, state } = this;
214
-    return (
215
-      <View
216
-        style={{
217
-          ...GENERAL_STYLES.flex,
218
-          ...GENERAL_STYLES.centerVertical,
219
-        }}
220
-      >
221
-        <View style={GENERAL_STYLES.centerHorizontal}>
222
-          <Caption style={styles.centerSmallMargin}>
223
-            {i18n.t('screens.game.time')}
224
-          </Caption>
225
-          <View style={styles.statusContainer}>
226
-            <MaterialCommunityIcons
227
-              name="timer"
228
-              color={props.theme.colors.subtitle}
229
-              size={20}
230
-            />
231
-            <Text
232
-              style={{
233
-                ...styles.statusIcon,
234
-                color: props.theme.colors.subtitle,
235
-              }}
236
-            >
237
-              {GameMainScreen.getFormattedTime(state.gameTime)}
238
-            </Text>
239
-          </View>
240
-        </View>
241
-        <View style={styles.centerBigMargin}>
242
-          <Caption style={styles.centerSmallMargin}>
243
-            {i18n.t('screens.game.level')}
244
-          </Caption>
245
-          <View style={styles.statusContainer}>
246
-            <MaterialCommunityIcons
247
-              name="gamepad-square"
248
-              color={props.theme.colors.text}
249
-              size={20}
250
-            />
251
-            <Text style={styles.statusIcon}>{state.gameLevel}</Text>
252
-          </View>
253
-        </View>
254
-      </View>
255
-    );
256
-  }
257
-
258
-  getScoreIcon() {
259
-    const { props, state } = this;
260
-    const highScore =
261
-      this.highScore == null || state.gameScore > this.highScore
262
-        ? state.gameScore
263
-        : this.highScore;
264
-    return (
265
-      <View style={styles.scoreMainContainer}>
266
-        <View style={styles.scoreCurrentContainer}>
267
-          <Text style={styles.scoreText}>
268
-            {i18n.t('screens.game.score', { score: state.gameScore })}
269
-          </Text>
270
-          <MaterialCommunityIcons
271
-            name="star"
272
-            color={props.theme.colors.tetrisScore}
273
-            size={20}
274
-            style={styles.centerVerticalSmallMargin}
275
-          />
276
-        </View>
277
-        <View style={styles.scoreBestContainer}>
278
-          <Text
279
-            style={{
280
-              ...styles.scoreText,
281
-              color: props.theme.colors.textDisabled,
282
-            }}
283
-          >
284
-            {i18n.t('screens.game.highScore', { score: highScore })}
285
-          </Text>
286
-          <MaterialCommunityIcons
287
-            name="star"
288
-            color={props.theme.colors.tetrisScore}
289
-            size={10}
290
-            style={styles.centerVerticalSmallMargin}
291
-          />
292
-        </View>
293
-      </View>
294
-    );
295
-  }
296
-
297
-  getControlButtons() {
298
-    const { props } = this;
299
-    return (
300
-      <View style={styles.controlsContainer}>
301
-        <IconButton
302
-          icon="rotate-right-variant"
303
-          size={40}
304
-          onPress={() => {
305
-            this.logic.rotatePressed(this.updateGrid);
306
-          }}
307
-          style={GENERAL_STYLES.flex}
308
-        />
309
-        <View style={styles.directionsContainer}>
310
-          <IconButton
311
-            icon="chevron-left"
312
-            size={40}
313
-            style={GENERAL_STYLES.flex}
314
-            onPress={() => {
315
-              this.logic.pressedOut();
316
-            }}
317
-            onPressIn={() => {
318
-              this.logic.leftPressedIn(this.updateGrid);
319
-            }}
320
-          />
321
-          <IconButton
322
-            icon="chevron-right"
323
-            size={40}
324
-            style={GENERAL_STYLES.flex}
325
-            onPress={() => {
326
-              this.logic.pressedOut();
327
-            }}
328
-            onPressIn={() => {
329
-              this.logic.rightPressed(this.updateGrid);
330
-            }}
331
-          />
332
-        </View>
333
-        <IconButton
334
-          icon="arrow-down-bold"
335
-          size={40}
336
-          onPressIn={() => {
337
-            this.logic.downPressedIn(this.updateGridScore);
338
-          }}
339
-          onPress={() => {
340
-            this.logic.pressedOut();
341
-          }}
342
-          style={GENERAL_STYLES.flex}
343
-          color={props.theme.colors.tetrisScore}
344
-        />
345
-      </View>
346
-    );
347
-  }
348
-
349
-  updateGrid = (newGrid: GridType) => {
350
-    this.setState({
351
-      grid: newGrid,
352
-    });
353
-  };
354
-
355
-  updateGridScore = (newGrid: GridType, score?: number) => {
356
-    this.setState((prevState: StateType): {
357
-      grid: GridType;
358
-      gameScore: number;
359
-    } => ({
161
+  const onDirectionPressed = (newGrid: GridType, score?: number) => {
162
+    setGameState((prevState) => ({
163
+      ...prevState,
360 164
       grid: newGrid,
361 165
       gameScore: score != null ? score : prevState.gameScore,
362 166
     }));
363 167
   };
364 168
 
365
-  togglePause = () => {
366
-    this.logic.togglePause();
367
-    if (this.logic.isGamePaused()) {
368
-      this.showPausePopup();
169
+  const togglePause = () => {
170
+    logic.current.togglePause();
171
+    if (logic.current.isGamePaused()) {
172
+      showPausePopup();
369 173
     }
370 174
   };
371 175
 
372
-  showPausePopup = () => {
176
+  const showPausePopup = () => {
373 177
     const onDismiss = () => {
374
-      this.togglePause();
375
-      this.onDialogDismiss();
178
+      togglePause();
179
+      onDialogDismiss();
376 180
     };
377
-    this.setState({
378
-      dialogVisible: true,
181
+    setDialogContent({
379 182
       dialogTitle: i18n.t('screens.game.pause'),
380 183
       dialogMessage: i18n.t('screens.game.pauseMessage'),
381 184
       dialogButtons: [
382 185
         {
383 186
           title: i18n.t('screens.game.restart.text'),
384
-          onPress: this.showRestartConfirm,
187
+          onPress: showRestartConfirm,
385 188
         },
386 189
         {
387 190
           title: i18n.t('screens.game.resume'),
@@ -392,71 +195,68 @@ class GameMainScreen extends React.Component<PropsType, StateType> {
392 195
     });
393 196
   };
394 197
 
395
-  showRestartConfirm = () => {
396
-    this.setState({
397
-      dialogVisible: true,
198
+  const showRestartConfirm = () => {
199
+    setDialogContent({
398 200
       dialogTitle: i18n.t('screens.game.restart.confirm'),
399 201
       dialogMessage: i18n.t('screens.game.restart.confirmMessage'),
400 202
       dialogButtons: [
401 203
         {
402 204
           title: i18n.t('screens.game.restart.confirmYes'),
403 205
           onPress: () => {
404
-            this.onDialogDismiss();
405
-            this.startGame();
206
+            onDialogDismiss();
207
+            startGame();
406 208
           },
407 209
         },
408 210
         {
409 211
           title: i18n.t('screens.game.restart.confirmNo'),
410
-          onPress: this.showPausePopup,
212
+          onPress: showPausePopup,
411 213
         },
412 214
       ],
413
-      onDialogDismiss: this.showPausePopup,
215
+      onDialogDismiss: showPausePopup,
414 216
     });
415 217
   };
416 218
 
417
-  startGame = () => {
418
-    this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
219
+  const startGame = () => {
220
+    logic.current.startGame(onTick, setGameTime, onGameEnd);
419 221
   };
420 222
 
421
-  render() {
422
-    const { props, state } = this;
423
-    return (
424
-      <View style={GENERAL_STYLES.flex}>
425
-        <View style={styles.container}>
426
-          {this.getStatusIcons()}
427
-          <View style={styles.gridContainer}>
428
-            {this.getScoreIcon()}
429
-            <GridComponent
430
-              width={this.logic.getWidth()}
431
-              height={this.logic.getHeight()}
432
-              grid={state.grid}
433
-              style={{
434
-                backgroundColor: props.theme.colors.tetrisBackground,
435
-                ...GENERAL_STYLES.flex,
436
-                ...GENERAL_STYLES.centerHorizontal,
437
-              }}
438
-            />
439
-          </View>
440
-
441
-          <View style={GENERAL_STYLES.flex}>
442
-            <Preview
443
-              items={this.logic.getNextPiecesPreviews()}
444
-              style={styles.preview}
445
-            />
446
-          </View>
223
+  return (
224
+    <View style={GENERAL_STYLES.flex}>
225
+      <View style={styles.container}>
226
+        <GameStatus time={gameTime} level={gameState.gameLevel} />
227
+        <View style={styles.gridContainer}>
228
+          <GameScore score={gameState.gameScore} highScore={highScore} />
229
+          <GridComponent
230
+            width={logic.current.getWidth()}
231
+            height={logic.current.getHeight()}
232
+            grid={gameState.grid}
233
+            style={{
234
+              backgroundColor: theme.colors.tetrisBackground,
235
+              ...GENERAL_STYLES.flex,
236
+              ...GENERAL_STYLES.centerHorizontal,
237
+            }}
238
+          />
447 239
         </View>
448
-        {this.getControlButtons()}
449
-
240
+        <View style={GENERAL_STYLES.flex}>
241
+          <Preview
242
+            items={logic.current.getNextPiecesPreviews()}
243
+            style={styles.preview}
244
+          />
245
+        </View>
246
+      </View>
247
+      <GameControls
248
+        logic={logic.current}
249
+        onDirectionPressed={onDirectionPressed}
250
+      />
251
+      {dialogContent ? (
450 252
         <OptionsDialog
451
-          visible={state.dialogVisible}
452
-          title={state.dialogTitle}
453
-          message={state.dialogMessage}
454
-          buttons={state.dialogButtons}
455
-          onDismiss={state.onDialogDismiss}
253
+          visible={dialogContent !== undefined}
254
+          title={dialogContent.dialogTitle}
255
+          message={dialogContent.dialogMessage}
256
+          buttons={dialogContent.dialogButtons}
257
+          onDismiss={dialogContent.onDialogDismiss}
456 258
         />
457
-      </View>
458
-    );
459
-  }
259
+      ) : null}
260
+    </View>
261
+  );
460 262
 }
461
-
462
-export default withTheme(GameMainScreen);

+ 74
- 431
src/screens/Game/screens/GameStartScreen.tsx View File

@@ -18,478 +18,121 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import { StackNavigationProp } from '@react-navigation/stack';
22
-import {
23
-  Button,
24
-  Card,
25
-  Divider,
26
-  Headline,
27
-  Paragraph,
28
-  Text,
29
-  withTheme,
30
-} from 'react-native-paper';
21
+import { Button, useTheme } from 'react-native-paper';
31 22
 import { StyleSheet, View } from 'react-native';
32 23
 import i18n from 'i18n-js';
33
-import * as Animatable from 'react-native-animatable';
34
-import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
35 24
 import LinearGradient from 'react-native-linear-gradient';
36
-import Mascot, { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
25
+import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
37 26
 import MascotPopup from '../../../components/Mascot/MascotPopup';
38
-import type { GridType } from '../components/GridComponent';
39
-import GridComponent from '../components/GridComponent';
40
-import GridManager from '../logic/GridManager';
41
-import Piece from '../logic/Piece';
42
-import SpeechArrow from '../../../components/Mascot/SpeechArrow';
43 27
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
44 28
 import GENERAL_STYLES from '../../../constants/Styles';
29
+import GameBackground from '../components/GameBrackground';
30
+import PostGameContent from '../components/PostGameContent';
31
+import WelcomeGameContent from '../components/WelcomeGameContent';
32
+import FullGamePodium from '../components/FullGamePodium';
33
+import { useNavigation } from '@react-navigation/core';
34
+import { usePreferences } from '../../../context/preferencesContext';
35
+import {
36
+  getPreferenceObject,
37
+  PreferenceKeys,
38
+} from '../../../utils/asyncStorage';
39
+import { StackNavigationProp } from '@react-navigation/stack';
45 40
 
46 41
 type GameStatsType = {
47 42
   score: number;
48 43
   level: number;
49 44
   time: number;
45
+  isHighScore: boolean;
50 46
 };
51 47
 
52
-type PropsType = {
53
-  navigation: StackNavigationProp<any>;
48
+type Props = {
54 49
   route: {
55
-    params: GameStatsType;
50
+    params?: GameStatsType;
56 51
   };
57
-  theme: ReactNativePaper.Theme;
58 52
 };
59 53
 
60 54
 const styles = StyleSheet.create({
61
-  pieceContainer: {
62
-    position: 'absolute',
63
-    width: '100%',
64
-    height: '100%',
65
-  },
66
-  pieceBackground: {
67
-    position: 'absolute',
68
-  },
69 55
   playButton: {
70 56
     marginLeft: 'auto',
71 57
     marginRight: 'auto',
72 58
     marginTop: 10,
73 59
   },
74
-  recapCard: {
75
-    borderWidth: 2,
76
-    marginLeft: 20,
77
-    marginRight: 20,
78
-  },
79
-  recapContainer: {
80
-    flexDirection: 'row',
81
-    marginLeft: 'auto',
82
-    marginRight: 'auto',
83
-  },
84
-  recapScoreContainer: {
85
-    flexDirection: 'row',
86
-    marginLeft: 'auto',
87
-    marginRight: 'auto',
88
-    marginTop: 10,
89
-    marginBottom: 10,
90
-  },
91
-  recapScore: {
92
-    fontSize: 20,
93
-  },
94
-  recapScoreIcon: {
95
-    marginLeft: 5,
96
-  },
97
-  recapIcon: {
98
-    marginRight: 5,
99
-    marginLeft: 5,
100
-  },
101
-  welcomeMascot: {
102
-    width: '40%',
103
-    marginLeft: 'auto',
104
-    marginRight: 'auto',
105
-  },
106
-  welcomeCard: {
107
-    borderWidth: 2,
108
-    marginLeft: 10,
109
-    marginRight: 10,
110
-  },
111
-  centertext: {
112
-    textAlign: 'center',
113
-  },
114
-  welcomeText: {
115
-    textAlign: 'center',
116
-    marginTop: 10,
117
-  },
118
-  speechArrow: {
119
-    marginLeft: '60%',
120
-  },
121
-  podiumContainer: {
122
-    flexDirection: 'column',
123
-    alignItems: 'center',
124
-    justifyContent: 'flex-end',
125
-  },
126
-  podiumIconContainer: {
127
-    position: 'absolute',
128
-    top: -20,
129
-  },
130
-  topScoreContainer: {
131
-    marginBottom: 20,
132
-    marginTop: 20,
133
-  },
134
-  topScoreSubcontainer: {
135
-    flexDirection: 'row',
136
-    marginLeft: 'auto',
137
-    marginRight: 'auto',
138
-  },
139 60
 });
140 61
 
141
-class GameStartScreen extends React.Component<PropsType> {
142
-  gridManager: GridManager;
143
-
144
-  scores: Array<number>;
145
-
146
-  gameStats?: GameStatsType;
62
+export default function GameStartScreen(props: Props) {
63
+  const theme = useTheme();
64
+  const navigation = useNavigation<StackNavigationProp<any>>();
147 65
 
148
-  isHighScore: boolean;
66
+  const { preferences } = usePreferences();
149 67
 
150
-  constructor(props: PropsType) {
151
-    super(props);
152
-    this.isHighScore = false;
153
-    this.gridManager = new GridManager(4, 4, props.theme);
154
-    // TODO
155
-    // this.scores = AsyncStorageManager.getObject(
156
-    //   AsyncStorageManager.PREFERENCES.gameScores.key
157
-    // );
158
-    this.scores = [];
159
-    this.scores.sort((a: number, b: number): number => b - a);
160
-    if (props.route.params != null) {
161
-      this.recoverGameScore();
68
+  function getScores() {
69
+    const pref = getPreferenceObject(PreferenceKeys.gameScores, preferences) as
70
+      | Array<number>
71
+      | undefined;
72
+    if (pref) {
73
+      return pref.sort((a, b) => b - a);
74
+    } else {
75
+      return [];
162 76
     }
163 77
   }
164 78
 
165
-  getPiecesBackground() {
166
-    const { theme } = this.props;
167
-    const gridList = [];
168
-    for (let i = 0; i < 18; i += 1) {
169
-      gridList.push(this.gridManager.getEmptyGrid(4, 4));
170
-      const piece = new Piece(theme);
171
-      piece.toGrid(gridList[i], true);
172
-    }
173
-    return (
174
-      <View style={styles.pieceContainer}>
175
-        {gridList.map((item: GridType, index: number) => {
176
-          const size = 10 + Math.floor(Math.random() * 30);
177
-          const top = Math.floor(Math.random() * 100);
178
-          const rot = Math.floor(Math.random() * 360);
179
-          const left = (index % 6) * 20;
180
-          const animDelay = size * 20;
181
-          const animDuration = 2 * (2000 - size * 30);
182
-          return (
183
-            <Animatable.View
184
-              useNativeDriver
185
-              animation="fadeInDownBig"
186
-              delay={animDelay}
187
-              duration={animDuration}
188
-              key={`piece${index.toString()}`}
189
-              style={{
190
-                width: `${size}%`,
191
-                top: `${top}%`,
192
-                left: `${left}%`,
193
-                ...styles.pieceBackground,
194
-              }}
195
-            >
196
-              <GridComponent
197
-                width={4}
198
-                height={4}
199
-                grid={item}
200
-                style={{
201
-                  transform: [{ rotateZ: `${rot}deg` }],
202
-                }}
203
-              />
204
-            </Animatable.View>
205
-          );
206
-        })}
207
-      </View>
208
-    );
209
-  }
79
+  const scores = getScores();
80
+  const lastGameStats = props.route.params;
210 81
 
211
-  getPostGameContent(stats: GameStatsType) {
212
-    const { props } = this;
213
-    const width = this.isHighScore ? '50%' : '30%';
214
-    const margin = this.isHighScore ? 'auto' : undefined;
215
-    const marginLeft = this.isHighScore ? '60%' : '20%';
216
-    const color = this.isHighScore
217
-      ? props.theme.colors.gameGold
218
-      : props.theme.colors.primary;
82
+  const getMainContent = () => {
219 83
     return (
220 84
       <View style={GENERAL_STYLES.flex}>
221
-        <Mascot
222
-          emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
223
-          animated={this.isHighScore}
224
-          style={{
225
-            width: width,
226
-            marginLeft: margin,
227
-            marginRight: margin,
228
-          }}
229
-        />
230
-        <SpeechArrow
231
-          style={{ marginLeft: marginLeft }}
232
-          size={20}
233
-          color={props.theme.colors.mascotMessageArrow}
234
-        />
235
-        <Card
236
-          style={{
237
-            borderColor: props.theme.colors.mascotMessageArrow,
238
-            ...styles.recapCard,
239
-          }}
240
-        >
241
-          <Card.Content>
242
-            <Headline
243
-              style={{
244
-                color: color,
245
-                ...styles.centertext,
246
-              }}
247
-            >
248
-              {this.isHighScore
249
-                ? i18n.t('screens.game.newHighScore')
250
-                : i18n.t('screens.game.gameOver')}
251
-            </Headline>
252
-            <Divider />
253
-            <View style={styles.recapScoreContainer}>
254
-              <Text style={styles.recapScore}>
255
-                {i18n.t('screens.game.score', { score: stats.score })}
256
-              </Text>
257
-              <MaterialCommunityIcons
258
-                name="star"
259
-                color={props.theme.colors.tetrisScore}
260
-                size={30}
261
-                style={styles.recapScoreIcon}
262
-              />
263
-            </View>
264
-            <View style={styles.recapContainer}>
265
-              <Text>{i18n.t('screens.game.level')}</Text>
266
-              <MaterialCommunityIcons
267
-                style={styles.recapIcon}
268
-                name="gamepad-square"
269
-                size={20}
270
-                color={props.theme.colors.textDisabled}
271
-              />
272
-              <Text>{stats.level}</Text>
273
-            </View>
274
-            <View style={styles.recapContainer}>
275
-              <Text>{i18n.t('screens.game.time')}</Text>
276
-              <MaterialCommunityIcons
277
-                style={styles.recapIcon}
278
-                name="timer"
279
-                size={20}
280
-                color={props.theme.colors.textDisabled}
281
-              />
282
-              <Text>{stats.time}</Text>
283
-            </View>
284
-          </Card.Content>
285
-        </Card>
286
-      </View>
287
-    );
288
-  }
289
-
290
-  getWelcomeText() {
291
-    const { props } = this;
292
-    return (
293
-      <View>
294
-        <Mascot emotion={MASCOT_STYLE.COOL} style={styles.welcomeMascot} />
295
-        <SpeechArrow
296
-          style={styles.speechArrow}
297
-          size={20}
298
-          color={props.theme.colors.mascotMessageArrow}
299
-        />
300
-        <Card
301
-          style={{
302
-            borderColor: props.theme.colors.mascotMessageArrow,
303
-            ...styles.welcomeCard,
304
-          }}
305
-        >
306
-          <Card.Content>
307
-            <Headline
308
-              style={{
309
-                color: props.theme.colors.primary,
310
-                ...styles.centertext,
311
-              }}
312
-            >
313
-              {i18n.t('screens.game.welcomeTitle')}
314
-            </Headline>
315
-            <Divider />
316
-            <Paragraph style={styles.welcomeText}>
317
-              {i18n.t('screens.game.welcomeMessage')}
318
-            </Paragraph>
319
-          </Card.Content>
320
-        </Card>
321
-      </View>
322
-    );
323
-  }
324
-
325
-  getPodiumRender(place: 1 | 2 | 3, score: string) {
326
-    const { props } = this;
327
-    let icon = 'podium-gold';
328
-    let color = props.theme.colors.gameGold;
329
-    let fontSize = 20;
330
-    let size = 70;
331
-    if (place === 2) {
332
-      icon = 'podium-silver';
333
-      color = props.theme.colors.gameSilver;
334
-      fontSize = 18;
335
-      size = 60;
336
-    } else if (place === 3) {
337
-      icon = 'podium-bronze';
338
-      color = props.theme.colors.gameBronze;
339
-      fontSize = 15;
340
-      size = 50;
341
-    }
342
-    const marginLeft = place === 2 ? 20 : 'auto';
343
-    const marginRight = place === 3 ? 20 : 'auto';
344
-    const fontWeight = place === 1 ? 'bold' : undefined;
345
-    return (
346
-      <View
347
-        style={{
348
-          marginLeft: marginLeft,
349
-          marginRight: marginRight,
350
-          ...styles.podiumContainer,
351
-        }}
352
-      >
353
-        {this.isHighScore && place === 1 ? (
354
-          <Animatable.View
355
-            animation="swing"
356
-            iterationCount="infinite"
357
-            duration={2000}
358
-            delay={1000}
359
-            useNativeDriver
360
-            style={styles.podiumIconContainer}
361
-          >
362
-            <Animatable.View
363
-              animation="pulse"
364
-              iterationCount="infinite"
365
-              useNativeDriver
366
-            >
367
-              <MaterialCommunityIcons
368
-                name="decagram"
369
-                color={props.theme.colors.gameGold}
370
-                size={150}
371
-              />
372
-            </Animatable.View>
373
-          </Animatable.View>
374
-        ) : null}
375
-        <MaterialCommunityIcons
376
-          name={icon}
377
-          color={this.isHighScore && place === 1 ? '#fff' : color}
378
-          size={size}
379
-        />
380
-        <Text
381
-          style={{
382
-            fontWeight: fontWeight,
383
-            fontSize,
384
-            ...styles.centertext,
385
-          }}
386
-        >
387
-          {score}
388
-        </Text>
389
-      </View>
390
-    );
391
-  }
392
-
393
-  getTopScoresRender() {
394
-    const gold = this.scores.length > 0 ? this.scores[0] : '-';
395
-    const silver = this.scores.length > 1 ? this.scores[1] : '-';
396
-    const bronze = this.scores.length > 2 ? this.scores[2] : '-';
397
-    return (
398
-      <View style={styles.topScoreContainer}>
399
-        {this.getPodiumRender(1, gold.toString())}
400
-        <View style={styles.topScoreSubcontainer}>
401
-          {this.getPodiumRender(3, bronze.toString())}
402
-          {this.getPodiumRender(2, silver.toString())}
403
-        </View>
404
-      </View>
405
-    );
406
-  }
407
-
408
-  getMainContent() {
409
-    const { props } = this;
410
-    return (
411
-      <View style={GENERAL_STYLES.flex}>
412
-        {this.gameStats != null
413
-          ? this.getPostGameContent(this.gameStats)
414
-          : this.getWelcomeText()}
85
+        {lastGameStats ? (
86
+          <PostGameContent
87
+            stats={lastGameStats}
88
+            isHighScore={lastGameStats.isHighScore}
89
+          />
90
+        ) : (
91
+          <WelcomeGameContent />
92
+        )}
415 93
         <Button
416
-          icon="play"
417
-          mode="contained"
94
+          icon={'play'}
95
+          mode={'contained'}
418 96
           onPress={() => {
419
-            props.navigation.replace('game-main', {
420
-              highScore: this.scores.length > 0 ? this.scores[0] : null,
421
-            });
97
+            navigation.replace('game-main');
422 98
           }}
423 99
           style={styles.playButton}
424 100
         >
425 101
           {i18n.t('screens.game.play')}
426 102
         </Button>
427
-        {this.getTopScoresRender()}
103
+        <FullGamePodium
104
+          scores={scores}
105
+          isHighScore={lastGameStats?.isHighScore === true}
106
+        />
428 107
       </View>
429 108
     );
430
-  }
431
-
432
-  keyExtractor = (item: number): string => item.toString();
433
-
434
-  recoverGameScore() {
435
-    const { route } = this.props;
436
-    this.gameStats = route.params;
437
-    if (this.gameStats.score != null) {
438
-      this.isHighScore =
439
-        this.scores.length === 0 || this.gameStats.score > this.scores[0];
440
-      for (let i = 0; i < 3; i += 1) {
441
-        if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
442
-          this.scores.splice(i, 0, this.gameStats.score);
443
-          break;
444
-        } else if (this.scores.length <= i) {
445
-          this.scores.push(this.gameStats.score);
446
-          break;
447
-        }
448
-      }
449
-      if (this.scores.length > 3) {
450
-        this.scores.splice(3, 1);
451
-      }
452
-      // TODO
453
-      // AsyncStorageManager.set(
454
-      //   AsyncStorageManager.PREFERENCES.gameScores.key,
455
-      //   this.scores
456
-      // );
457
-    }
458
-  }
109
+  };
459 110
 
460
-  render() {
461
-    const { props } = this;
462
-    return (
463
-      <View style={GENERAL_STYLES.flex}>
464
-        {this.getPiecesBackground()}
465
-        <LinearGradient
466
-          style={GENERAL_STYLES.flex}
467
-          colors={[
468
-            `${props.theme.colors.background}00`,
469
-            props.theme.colors.background,
470
-          ]}
471
-          start={{ x: 0, y: 0 }}
472
-          end={{ x: 0, y: 1 }}
473
-        >
474
-          <CollapsibleScrollView headerColors={'transparent'}>
475
-            {this.getMainContent()}
476
-            <MascotPopup
477
-              title={i18n.t('screens.game.mascotDialog.title')}
478
-              message={i18n.t('screens.game.mascotDialog.message')}
479
-              icon="gamepad-variant"
480
-              buttons={{
481
-                cancel: {
482
-                  message: i18n.t('screens.game.mascotDialog.button'),
483
-                  icon: 'check',
484
-                },
485
-              }}
486
-              emotion={MASCOT_STYLE.COOL}
487
-            />
488
-          </CollapsibleScrollView>
489
-        </LinearGradient>
490
-      </View>
491
-    );
492
-  }
111
+  return (
112
+    <View style={GENERAL_STYLES.flex}>
113
+      <GameBackground />
114
+      <LinearGradient
115
+        style={GENERAL_STYLES.flex}
116
+        colors={[`${theme.colors.background}00`, theme.colors.background]}
117
+        start={{ x: 0, y: 0 }}
118
+        end={{ x: 0, y: 1 }}
119
+      >
120
+        <CollapsibleScrollView headerColors={'transparent'}>
121
+          {getMainContent()}
122
+          <MascotPopup
123
+            title={i18n.t('screens.game.mascotDialog.title')}
124
+            message={i18n.t('screens.game.mascotDialog.message')}
125
+            icon="gamepad-variant"
126
+            buttons={{
127
+              cancel: {
128
+                message: i18n.t('screens.game.mascotDialog.button'),
129
+                icon: 'check',
130
+              },
131
+            }}
132
+            emotion={MASCOT_STYLE.COOL}
133
+          />
134
+        </CollapsibleScrollView>
135
+      </LinearGradient>
136
+    </View>
137
+  );
493 138
 }
494
-
495
-export default withTheme(GameStartScreen);

Loading…
Cancel
Save