Added game score save support

This commit is contained in:
Arnaud Vergnet 2020-07-21 20:43:03 +02:00
parent 4f911ce32d
commit b2ff90855f
8 changed files with 427 additions and 82 deletions

View file

@ -366,9 +366,11 @@
"welcomeTitle": "Welcome !",
"welcomeMessage": "Stuck on the toilet? The teacher is late?\nThis game is for you!\n\nTry to get the best score and beat your friends.",
"play": "Play!",
"score": "Score : %{score}",
"time": "Time :",
"level": "Level :",
"score": "Score: %{score}",
"highScore": "High score: %{score}",
"newHighScore": "New High Score!",
"time": "Time:",
"level": "Level:",
"pause": "Game Paused",
"pauseMessage": "The game is paused",
"resume": "Resume",

View file

@ -366,6 +366,8 @@
"welcomeMessage": "Coincé sur les WC ? Le prof est pas là ?\nCe jeu est fait pour toi !\n\nEssaie d'avoir le meilleur score et de battre tes amis.",
"play": "Jouer !",
"score": "Score : %{score}",
"highScore": "Meilleur score : %{score}",
"newHighScore": "Meilleur score !",
"time": "Temps :",
"level": "Niveau :",
"pause": "Pause",

View file

@ -6,6 +6,7 @@ import Mascot from "./Mascot";
import * as Animatable from "react-native-animatable";
import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager";
import SpeechArrow from "./SpeechArrow";
type Props = {
visible: boolean,
@ -102,19 +103,11 @@ class MascotPopup extends React.Component<Props, State> {
animation={this.props.visible ? "bounceInLeft" : "bounceOutLeft"}
duration={this.props.visible ? 1000 : 300}
>
<View style={{
marginLeft: this.mascotSize / 3,
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: 20,
borderBottomWidth: 20,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: this.props.theme.colors.mascotMessageArrow,
}}/>
<SpeechArrow
style={{marginLeft: this.mascotSize / 3}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 4,

View file

@ -0,0 +1,33 @@
// @flow
import * as React from 'react';
import {View} from "react-native";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type Props = {
style?: ViewStyle,
size: number,
color: string,
}
export default class SpeechArrow extends React.Component<Props> {
render() {
return (
<View style={this.props.style}>
<View style={{
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: this.props.size,
borderBottomWidth: this.props.size,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: this.props.color,
}}/>
</View>
);
}
}

View file

@ -136,6 +136,11 @@ export default class AsyncStorageManager {
]),
current: '',
},
gameScores: {
key: 'gameScores',
default: '[]',
current: '',
},
};
/**

View file

@ -56,6 +56,10 @@ export type CustomTheme = {
tetrisJ: string,
tetrisL: string,
gameGold: string,
gameSilver: string,
gameBronze: string,
// Mascot Popup
mascotMessageArrow: string,
},
@ -129,6 +133,10 @@ export default class ThemeManager {
tetrisJ: '#2a67e3',
tetrisL: '#da742d',
gameGold: "#ffd610",
gameSilver: "#7b7b7b",
gameBronze: "#a15218",
// Mascot Popup
mascotMessageArrow: "#dedede",
},
@ -191,6 +199,10 @@ export default class ThemeManager {
tetrisJ: '#0f37b9',
tetrisL: '#b96226',
gameGold: "#ffd610",
gameSilver: "#7b7b7b",
gameBronze: "#a15218",
// Mascot Popup
mascotMessageArrow: "#323232",
},

View file

@ -17,6 +17,7 @@ import OptionsDialog from "../../../components/Dialogs/OptionsDialog";
type Props = {
navigation: StackNavigationProp,
route: { params: { highScore: number }, ... },
theme: CustomTheme,
}
@ -37,6 +38,7 @@ type State = {
class GameMainScreen extends React.Component<Props, State> {
logic: GameLogic;
highScore: number | null;
constructor(props) {
super(props);
@ -54,8 +56,8 @@ class GameMainScreen extends React.Component<Props, State> {
onDialogDismiss: () => {
},
};
this.props.navigation.addListener('blur', this.onScreenBlur);
this.props.navigation.addListener('focus', this.onScreenFocus);
if (this.props.route.params != null)
this.highScore = this.props.route.params.highScore;
}
componentDidMount() {
@ -71,21 +73,6 @@ class GameMainScreen extends React.Component<Props, State> {
</MaterialHeaderButtons>;
}
/**
* Remove any interval on un-focus
*/
onScreenBlur = () => {
if (!this.logic.isGamePaused())
this.logic.togglePause();
}
onScreenFocus = () => {
if (!this.logic.isGameRunning())
this.startGame();
else if (this.logic.isGamePaused())
this.showPausePopup();
}
getFormattedTime(seconds: number) {
let date = new Date();
date.setHours(0);
@ -221,7 +208,14 @@ class GameMainScreen extends React.Component<Props, State> {
gameRunning: false,
});
if (!isRestart)
this.showGameOverConfirm();
this.props.navigation.replace(
"game-start",
{
score: this.state.gameScore,
level: this.state.gameLevel,
time: this.state.gameTime,
}
);
}
getStatusIcons() {
@ -281,17 +275,22 @@ class GameMainScreen extends React.Component<Props, State> {
}
getScoreIcon() {
let highScore = this.highScore == null || this.state.gameScore > this.highScore
? this.state.gameScore
: this.highScore;
return (
<View style={{
marginTop: 10,
marginBottom: 10,
}}>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
marginTop: 10,
marginBottom: 10,
}}>
<Text style={{
marginLeft: 5,
fontSize: 22,
fontSize: 20,
}}>{i18n.t("screens.game.score", {score: this.state.gameScore})}</Text>
<MaterialCommunityIcons
name={'star'}
@ -303,6 +302,29 @@ class GameMainScreen extends React.Component<Props, State> {
marginLeft: 5
}}/>
</View>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
marginTop: 5,
}}>
<Text style={{
marginLeft: 5,
fontSize: 10,
color: this.props.theme.colors.textDisabled
}}>{i18n.t("screens.game.highScore", {score: highScore})}</Text>
<MaterialCommunityIcons
name={'star'}
color={this.props.theme.colors.tetrisScore}
size={10}
style={{
marginTop: "auto",
marginBottom: "auto",
marginLeft: 5
}}/>
</View>
</View>
);
}

View file

@ -3,8 +3,8 @@
import * as React from "react";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import {Button, Card, Divider, Headline, Paragraph, withTheme} from "react-native-paper";
import {View} from "react-native";
import {Button, Card, Divider, Headline, Paragraph, Text, withTheme} from "react-native-paper";
import {ScrollView, View} from "react-native";
import i18n from "i18n-js";
import Mascot, {MASCOT_STYLE} from "../../../components/Mascot/Mascot";
import MascotPopup from "../../../components/Mascot/MascotPopup";
@ -14,9 +14,21 @@ import GridComponent from "../components/GridComponent";
import GridManager from "../logic/GridManager";
import Piece from "../logic/Piece";
import * as Animatable from "react-native-animatable";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import LinearGradient from "react-native-linear-gradient";
import SpeechArrow from "../../../components/Mascot/SpeechArrow";
type GameStats = {
score: number,
level: number,
time: number,
}
type Props = {
navigation: StackNavigationProp,
route: {
params: GameStats
},
theme: CustomTheme,
}
@ -27,6 +39,10 @@ type State = {
class GameStartScreen extends React.Component<Props, State> {
gridManager: GridManager;
scores: Array<number>;
gameStats: GameStats | null;
isHighScore: boolean;
state = {
mascotDialogVisible: AsyncStorageManager.getInstance().preferences.gameStartShowBanner.current === "1",
@ -35,6 +51,30 @@ class GameStartScreen extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.gridManager = new GridManager(4, 4, props.theme);
this.scores = JSON.parse(AsyncStorageManager.getInstance().preferences.gameScores.current);
this.scores.sort((a, b) => b - a);
if (this.props.route.params != null)
this.recoverGameScore();
}
recoverGameScore() {
this.gameStats = this.props.route.params;
this.isHighScore = this.scores.length === 0 || this.gameStats.score > this.scores[0];
for (let i = 0; i < 3; i++) {
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);
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.gameScores.key,
JSON.stringify(this.scores)
);
}
hideMascotDialog = () => {
@ -97,10 +137,110 @@ class GameStartScreen extends React.Component<Props, State> {
);
})}
</View>
);
}
getPostGameContent(stats: GameStats) {
return (
<View style={{
flex: 1
}}>
<Mascot
emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
animated={this.isHighScore}
style={{
width: this.isHighScore ? "50%" : "30%",
marginLeft: this.isHighScore ? "auto" : null,
marginRight: this.isHighScore ? "auto" : null,
}}/>
<SpeechArrow
style={{marginLeft: this.isHighScore ? "60%" : "20%"}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 2,
marginLeft: 20,
marginRight: 20,
}}>
<Card.Content>
<Headline
style={{
textAlign: "center",
color: this.isHighScore
? this.props.theme.colors.gameGold
: this.props.theme.colors.primary
}}>
{this.isHighScore
? i18n.t("screens.game.newHighScore")
: i18n.t("screens.game.gameOver.text")}
</Headline>
<Divider/>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
marginTop: 10,
marginBottom: 10,
}}>
<Text style={{
fontSize: 20,
}}>
{i18n.t("screens.game.score", {score: stats.score})}
</Text>
<MaterialCommunityIcons
name={'star'}
color={this.props.theme.colors.tetrisScore}
size={30}
style={{
marginLeft: 5
}}/>
</View>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
<Text>{i18n.t("screens.game.level")}</Text>
<MaterialCommunityIcons
style={{
marginRight: 5,
marginLeft: 5,
}}
name={"gamepad-square"}
size={20}
color={this.props.theme.colors.textDisabled}
/>
<Text>
{stats.level}
</Text>
</View>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
<Text>{i18n.t("screens.game.time")}</Text>
<MaterialCommunityIcons
style={{
marginRight: 5,
marginLeft: 5,
}}
name={"timer"}
size={20}
color={this.props.theme.colors.textDisabled}
/>
<Text>
{stats.time}
</Text>
</View>
</Card.Content>
</Card>
</View>
)
}
getWelcomeText() {
return (
<View>
@ -109,7 +249,14 @@ class GameStartScreen extends React.Component<Props, State> {
marginLeft: "auto",
marginRight: "auto",
}}/>
<SpeechArrow
style={{marginLeft: "60%"}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 2,
marginLeft: 10,
marginRight: 10,
}}>
@ -131,20 +278,133 @@ class GameStartScreen extends React.Component<Props, State> {
</Paragraph>
</Card.Content>
</Card>
</View>
);
}
render() {
getPodiumRender(place: 1 | 2 | 3, score: string) {
let icon = "podium-gold";
let color = this.props.theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = "podium-silver";
color = this.props.theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = "podium-bronze";
color = this.props.theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
return (
<View style={{
marginLeft: place === 2 ? 20 : "auto",
marginRight: place === 3 ? 20 : "auto",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-end",
}}>
{
this.isHighScore && place === 1
?
<Animatable.View
animation={"swing"}
iterationCount={"infinite"}
duration={2000}
delay={1000}
useNativeDriver={true}
style={{
position: "absolute",
top: -20
}}
>
<Animatable.View
animation={"pulse"}
iterationCount={"infinite"}
useNativeDriver={true}
>
<MaterialCommunityIcons
name={"decagram"}
color={this.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={{
textAlign: "center",
fontWeight: place === 1 ? "bold" : null,
fontSize: fontSize,
}}>{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={{
marginBottom: 20,
marginTop: 20
}}>
{this.getPodiumRender(1, gold.toString())}
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
{this.getPodiumRender(3, bronze.toString())}
{this.getPodiumRender(2, silver.toString())}
</View>
</View>
);
}
getMainContent() {
return (
<LinearGradient
style={{flex: 1}}
colors={[
this.props.theme.colors.background + "00",
this.props.theme.colors.background
]}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}
>
<View style={{flex: 1}}>
{this.getPiecesBackground()}
{this.getWelcomeText()}
{
this.gameStats != null
? this.getPostGameContent(this.gameStats)
: this.getWelcomeText()
}
<Button
icon={"play"}
mode={"contained"}
onPress={() => this.props.navigation.navigate("game-main")}
onPress={() => this.props.navigation.replace(
"game-main",
{
highScore: this.scores.length > 0
? this.scores[0]
: null
}
)}
style={{
marginLeft: "auto",
marginRight: "auto",
@ -153,6 +413,20 @@ class GameStartScreen extends React.Component<Props, State> {
>
{i18n.t("screens.game.play")}
</Button>
{this.getTopScoresRender()}
</View>
</LinearGradient>
)
}
keyExtractor = (item: number) => item.toString();
render() {
return (
<View style={{flex: 1}}>
{this.getPiecesBackground()}
<ScrollView>
{this.getMainContent()}
<MascotPopup
visible={this.state.mascotDialogVisible}
title={i18n.t("screens.game.mascotDialog.title")}
@ -168,7 +442,9 @@ class GameStartScreen extends React.Component<Props, State> {
}}
emotion={MASCOT_STYLE.COOL}
/>
</ScrollView>
</View>
);
}
}