Application Android et IOS pour l'amicale des élèves
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GameStartScreen.tsx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /*
  2. * Copyright (c) 2019 - 2020 Arnaud Vergnet.
  3. *
  4. * This file is part of Campus INSAT.
  5. *
  6. * Campus INSAT is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Campus INSAT is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import * as React from 'react';
  20. import {StackNavigationProp} from '@react-navigation/stack';
  21. import {
  22. Button,
  23. Card,
  24. Divider,
  25. Headline,
  26. Paragraph,
  27. Text,
  28. withTheme,
  29. } from 'react-native-paper';
  30. import {View} from 'react-native';
  31. import i18n from 'i18n-js';
  32. import * as Animatable from 'react-native-animatable';
  33. import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
  34. import LinearGradient from 'react-native-linear-gradient';
  35. import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
  36. import MascotPopup from '../../../components/Mascot/MascotPopup';
  37. import AsyncStorageManager from '../../../managers/AsyncStorageManager';
  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. import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
  44. type GameStatsType = {
  45. score: number;
  46. level: number;
  47. time: number;
  48. };
  49. type PropsType = {
  50. navigation: StackNavigationProp<any>;
  51. route: {
  52. params: GameStatsType;
  53. };
  54. theme: ReactNativePaper.Theme;
  55. };
  56. class GameStartScreen extends React.Component<PropsType> {
  57. gridManager: GridManager;
  58. scores: Array<number>;
  59. gameStats?: GameStatsType;
  60. isHighScore: boolean;
  61. constructor(props: PropsType) {
  62. super(props);
  63. this.isHighScore = false;
  64. this.gridManager = new GridManager(4, 4, props.theme);
  65. this.scores = AsyncStorageManager.getObject(
  66. AsyncStorageManager.PREFERENCES.gameScores.key,
  67. );
  68. this.scores.sort((a: number, b: number): number => b - a);
  69. if (props.route.params != null) {
  70. this.recoverGameScore();
  71. }
  72. }
  73. getPiecesBackground() {
  74. const {theme} = this.props;
  75. const gridList = [];
  76. for (let i = 0; i < 18; i += 1) {
  77. gridList.push(this.gridManager.getEmptyGrid(4, 4));
  78. const piece = new Piece(theme);
  79. piece.toGrid(gridList[i], true);
  80. }
  81. return (
  82. <View
  83. style={{
  84. position: 'absolute',
  85. width: '100%',
  86. height: '100%',
  87. }}>
  88. {gridList.map((item: GridType, index: number) => {
  89. const size = 10 + Math.floor(Math.random() * 30);
  90. const top = Math.floor(Math.random() * 100);
  91. const rot = Math.floor(Math.random() * 360);
  92. const left = (index % 6) * 20;
  93. const animDelay = size * 20;
  94. const animDuration = 2 * (2000 - size * 30);
  95. return (
  96. <Animatable.View
  97. useNativeDriver
  98. animation="fadeInDownBig"
  99. delay={animDelay}
  100. duration={animDuration}
  101. key={`piece${index.toString()}`}
  102. style={{
  103. width: `${size}%`,
  104. position: 'absolute',
  105. top: `${top}%`,
  106. left: `${left}%`,
  107. }}>
  108. <GridComponent
  109. width={4}
  110. height={4}
  111. grid={item}
  112. style={{
  113. transform: [{rotateZ: `${rot}deg`}],
  114. }}
  115. />
  116. </Animatable.View>
  117. );
  118. })}
  119. </View>
  120. );
  121. }
  122. getPostGameContent(stats: GameStatsType) {
  123. const {props} = this;
  124. return (
  125. <View
  126. style={{
  127. flex: 1,
  128. }}>
  129. <Mascot
  130. emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
  131. animated={this.isHighScore}
  132. style={{
  133. width: this.isHighScore ? '50%' : '30%',
  134. marginLeft: this.isHighScore ? 'auto' : undefined,
  135. marginRight: this.isHighScore ? 'auto' : undefined,
  136. }}
  137. />
  138. <SpeechArrow
  139. style={{marginLeft: this.isHighScore ? '60%' : '20%'}}
  140. size={20}
  141. color={props.theme.colors.mascotMessageArrow}
  142. />
  143. <Card
  144. style={{
  145. borderColor: props.theme.colors.mascotMessageArrow,
  146. borderWidth: 2,
  147. marginLeft: 20,
  148. marginRight: 20,
  149. }}>
  150. <Card.Content>
  151. <Headline
  152. style={{
  153. textAlign: 'center',
  154. color: this.isHighScore
  155. ? props.theme.colors.gameGold
  156. : props.theme.colors.primary,
  157. }}>
  158. {this.isHighScore
  159. ? i18n.t('screens.game.newHighScore')
  160. : i18n.t('screens.game.gameOver')}
  161. </Headline>
  162. <Divider />
  163. <View
  164. style={{
  165. flexDirection: 'row',
  166. marginLeft: 'auto',
  167. marginRight: 'auto',
  168. marginTop: 10,
  169. marginBottom: 10,
  170. }}>
  171. <Text
  172. style={{
  173. fontSize: 20,
  174. }}>
  175. {i18n.t('screens.game.score', {score: stats.score})}
  176. </Text>
  177. <MaterialCommunityIcons
  178. name="star"
  179. color={props.theme.colors.tetrisScore}
  180. size={30}
  181. style={{
  182. marginLeft: 5,
  183. }}
  184. />
  185. </View>
  186. <View
  187. style={{
  188. flexDirection: 'row',
  189. marginLeft: 'auto',
  190. marginRight: 'auto',
  191. }}>
  192. <Text>{i18n.t('screens.game.level')}</Text>
  193. <MaterialCommunityIcons
  194. style={{
  195. marginRight: 5,
  196. marginLeft: 5,
  197. }}
  198. name="gamepad-square"
  199. size={20}
  200. color={props.theme.colors.textDisabled}
  201. />
  202. <Text>{stats.level}</Text>
  203. </View>
  204. <View
  205. style={{
  206. flexDirection: 'row',
  207. marginLeft: 'auto',
  208. marginRight: 'auto',
  209. }}>
  210. <Text>{i18n.t('screens.game.time')}</Text>
  211. <MaterialCommunityIcons
  212. style={{
  213. marginRight: 5,
  214. marginLeft: 5,
  215. }}
  216. name="timer"
  217. size={20}
  218. color={props.theme.colors.textDisabled}
  219. />
  220. <Text>{stats.time}</Text>
  221. </View>
  222. </Card.Content>
  223. </Card>
  224. </View>
  225. );
  226. }
  227. getWelcomeText() {
  228. const {props} = this;
  229. return (
  230. <View>
  231. <Mascot
  232. emotion={MASCOT_STYLE.COOL}
  233. style={{
  234. width: '40%',
  235. marginLeft: 'auto',
  236. marginRight: 'auto',
  237. }}
  238. />
  239. <SpeechArrow
  240. style={{marginLeft: '60%'}}
  241. size={20}
  242. color={props.theme.colors.mascotMessageArrow}
  243. />
  244. <Card
  245. style={{
  246. borderColor: props.theme.colors.mascotMessageArrow,
  247. borderWidth: 2,
  248. marginLeft: 10,
  249. marginRight: 10,
  250. }}>
  251. <Card.Content>
  252. <Headline
  253. style={{
  254. textAlign: 'center',
  255. color: props.theme.colors.primary,
  256. }}>
  257. {i18n.t('screens.game.welcomeTitle')}
  258. </Headline>
  259. <Divider />
  260. <Paragraph
  261. style={{
  262. textAlign: 'center',
  263. marginTop: 10,
  264. }}>
  265. {i18n.t('screens.game.welcomeMessage')}
  266. </Paragraph>
  267. </Card.Content>
  268. </Card>
  269. </View>
  270. );
  271. }
  272. getPodiumRender(place: 1 | 2 | 3, score: string) {
  273. const {props} = this;
  274. let icon = 'podium-gold';
  275. let color = props.theme.colors.gameGold;
  276. let fontSize = 20;
  277. let size = 70;
  278. if (place === 2) {
  279. icon = 'podium-silver';
  280. color = props.theme.colors.gameSilver;
  281. fontSize = 18;
  282. size = 60;
  283. } else if (place === 3) {
  284. icon = 'podium-bronze';
  285. color = props.theme.colors.gameBronze;
  286. fontSize = 15;
  287. size = 50;
  288. }
  289. return (
  290. <View
  291. style={{
  292. marginLeft: place === 2 ? 20 : 'auto',
  293. marginRight: place === 3 ? 20 : 'auto',
  294. flexDirection: 'column',
  295. alignItems: 'center',
  296. justifyContent: 'flex-end',
  297. }}>
  298. {this.isHighScore && place === 1 ? (
  299. <Animatable.View
  300. animation="swing"
  301. iterationCount="infinite"
  302. duration={2000}
  303. delay={1000}
  304. useNativeDriver
  305. style={{
  306. position: 'absolute',
  307. top: -20,
  308. }}>
  309. <Animatable.View
  310. animation="pulse"
  311. iterationCount="infinite"
  312. useNativeDriver>
  313. <MaterialCommunityIcons
  314. name="decagram"
  315. color={props.theme.colors.gameGold}
  316. size={150}
  317. />
  318. </Animatable.View>
  319. </Animatable.View>
  320. ) : null}
  321. <MaterialCommunityIcons
  322. name={icon}
  323. color={this.isHighScore && place === 1 ? '#fff' : color}
  324. size={size}
  325. />
  326. <Text
  327. style={{
  328. textAlign: 'center',
  329. fontWeight: place === 1 ? 'bold' : undefined,
  330. fontSize,
  331. }}>
  332. {score}
  333. </Text>
  334. </View>
  335. );
  336. }
  337. getTopScoresRender() {
  338. const gold = this.scores.length > 0 ? this.scores[0] : '-';
  339. const silver = this.scores.length > 1 ? this.scores[1] : '-';
  340. const bronze = this.scores.length > 2 ? this.scores[2] : '-';
  341. return (
  342. <View
  343. style={{
  344. marginBottom: 20,
  345. marginTop: 20,
  346. }}>
  347. {this.getPodiumRender(1, gold.toString())}
  348. <View
  349. style={{
  350. flexDirection: 'row',
  351. marginLeft: 'auto',
  352. marginRight: 'auto',
  353. }}>
  354. {this.getPodiumRender(3, bronze.toString())}
  355. {this.getPodiumRender(2, silver.toString())}
  356. </View>
  357. </View>
  358. );
  359. }
  360. getMainContent() {
  361. const {props} = this;
  362. return (
  363. <View style={{flex: 1}}>
  364. {this.gameStats != null
  365. ? this.getPostGameContent(this.gameStats)
  366. : this.getWelcomeText()}
  367. <Button
  368. icon="play"
  369. mode="contained"
  370. onPress={() => {
  371. props.navigation.replace('game-main', {
  372. highScore: this.scores.length > 0 ? this.scores[0] : null,
  373. });
  374. }}
  375. style={{
  376. marginLeft: 'auto',
  377. marginRight: 'auto',
  378. marginTop: 10,
  379. }}>
  380. {i18n.t('screens.game.play')}
  381. </Button>
  382. {this.getTopScoresRender()}
  383. </View>
  384. );
  385. }
  386. keyExtractor = (item: number): string => item.toString();
  387. recoverGameScore() {
  388. const {route} = this.props;
  389. this.gameStats = route.params;
  390. if (this.gameStats.score != null) {
  391. this.isHighScore =
  392. this.scores.length === 0 || this.gameStats.score > this.scores[0];
  393. for (let i = 0; i < 3; i += 1) {
  394. if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
  395. this.scores.splice(i, 0, this.gameStats.score);
  396. break;
  397. } else if (this.scores.length <= i) {
  398. this.scores.push(this.gameStats.score);
  399. break;
  400. }
  401. }
  402. if (this.scores.length > 3) {
  403. this.scores.splice(3, 1);
  404. }
  405. AsyncStorageManager.set(
  406. AsyncStorageManager.PREFERENCES.gameScores.key,
  407. this.scores,
  408. );
  409. }
  410. }
  411. render() {
  412. const {props} = this;
  413. return (
  414. <View style={{flex: 1}}>
  415. {this.getPiecesBackground()}
  416. <LinearGradient
  417. style={{flex: 1}}
  418. colors={[
  419. `${props.theme.colors.background}00`,
  420. props.theme.colors.background,
  421. ]}
  422. start={{x: 0, y: 0}}
  423. end={{x: 0, y: 1}}>
  424. <CollapsibleScrollView>
  425. {this.getMainContent()}
  426. <MascotPopup
  427. prefKey={AsyncStorageManager.PREFERENCES.gameStartMascot.key}
  428. title={i18n.t('screens.game.mascotDialog.title')}
  429. message={i18n.t('screens.game.mascotDialog.message')}
  430. icon="gamepad-variant"
  431. buttons={{
  432. cancel: {
  433. message: i18n.t('screens.game.mascotDialog.button'),
  434. icon: 'check',
  435. },
  436. }}
  437. emotion={MASCOT_STYLE.COOL}
  438. />
  439. </CollapsibleScrollView>
  440. </LinearGradient>
  441. </View>
  442. );
  443. }
  444. }
  445. export default withTheme(GameStartScreen);