Compare commits

..

11 commits

38 changed files with 1392 additions and 655 deletions

View file

@ -363,6 +363,14 @@
},
"game": {
"title": "Game",
"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}",
"highScore": "High score: %{score}",
"newHighScore": "New High Score!",
"time": "Time:",
"level": "Level:",
"pause": "Game Paused",
"pauseMessage": "The game is paused",
"resume": "Resume",
@ -379,6 +387,11 @@
"level": "Level: ",
"time": "Time: ",
"exit": "leave Game"
},
"mascotDialog": {
"title": "Play !",
"message": "Play.",
"button": "Yes !"
}
},
"debug": {

View file

@ -361,16 +361,24 @@
"homeButtonSubtitle": "Contacte le développeur de l'appli"
},
"game": {
"title": "Le jeu trop ouf",
"title": "Jeu trop ouf",
"welcomeTitle": "Bienvenue !",
"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",
"pauseMessage": "Le jeu est en pause",
"pauseMessage": "T'as fait pause, t'es nul",
"resume": "Continuer",
"restart": {
"text": "Redémarrer",
"confirm": "Est-tu sûr de vouloir redémarrer ?",
"confirm": "T'es sûr de vouloir redémarrer ?",
"confirmMessage": "Tout ton progrès sera perdu, continuer ?",
"confirmYes": "Oui",
"confirmNo": "Non"
"confirmNo": "Oula non"
},
"gameOver": {
"text": "Game Over",
@ -378,6 +386,11 @@
"level": "Niveau: ",
"time": "Temps: ",
"exit": "Quitter"
},
"mascotDialog": {
"title": "Jeu !",
"message": "Jouer.",
"button": "Oui !"
}
},
"debug": {

View file

@ -2,6 +2,7 @@
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from "i18n-js";
type Props = {
visible: boolean,
@ -23,7 +24,7 @@ class AlertDialog extends React.PureComponent<Props> {
<Paragraph>{this.props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={this.props.onDismiss}>OK</Button>
<Button onPress={this.props.onDismiss}>{i18n.t("dialog.ok")}</Button>
</Dialog.Actions>
</Dialog>
</Portal>

View file

@ -0,0 +1,56 @@
// @flow
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import {FlatList} from "react-native";
export type OptionsDialogButton = {
title: string,
onPress: () => void,
}
type Props = {
visible: boolean,
title: string,
message: string,
buttons: Array<OptionsDialogButton>,
onDismiss: () => void,
}
class OptionsDialog extends React.PureComponent<Props> {
getButtonRender = ({item}: { item: OptionsDialogButton }) => {
return <Button
onPress={item.onPress}>
{item.title}
</Button>;
}
keyExtractor = (item: OptionsDialogButton) => item.title;
render() {
return (
<Portal>
<Dialog
visible={this.props.visible}
onDismiss={this.props.onDismiss}>
<Dialog.Title>{this.props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{this.props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<FlatList
data={this.props.buttons}
renderItem={this.getButtonRender}
keyExtractor={this.keyExtractor}
horizontal={true}
inverted={true}
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
}
export default OptionsDialog;

View file

@ -3,9 +3,10 @@
import * as React from 'react';
import * as Animatable from "react-native-animatable";
import {Image, TouchableWithoutFeedback, View} from "react-native";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type Props = {
size: number,
style?: ViewStyle,
emotion: number,
animated: boolean,
entryAnimation: Animatable.AnimatableProperties | null,
@ -116,9 +117,10 @@ class Mascot extends React.Component<Props, State> {
if (this.props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRef) => {
if (viewRef.current != null) {
let ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
viewRef.current.rubberBand(1500).then(() => {
ref.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.initialEmotion});
});
@ -130,9 +132,10 @@ class Mascot extends React.Component<Props, State> {
if (this.props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRef) => {
if (viewRef.current != null) {
let ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
viewRef.current.tada(1000).then(() => {
ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion});
});
@ -153,8 +156,8 @@ class Mascot extends React.Component<Props, State> {
position: "absolute",
top: "15%",
left: 0,
width: this.props.size,
height: this.props.size,
width: "100%",
height: "100%",
}}
/>
}
@ -168,8 +171,8 @@ class Mascot extends React.Component<Props, State> {
position: "absolute",
top: "15%",
left: isRight ? "-11%" : "11%",
width: this.props.size,
height: this.props.size,
width: "100%",
height: "100%",
transform: [{rotateY: rotation}]
}}
/>
@ -181,8 +184,8 @@ class Mascot extends React.Component<Props, State> {
key={"container"}
style={{
position: "absolute",
width: this.props.size,
height: this.props.size,
width: "100%",
height: "100%",
}}/>);
if (emotion === MASCOT_STYLE.CUTE) {
final.push(this.getEye(EYE_STYLE.CUTE, true));
@ -217,14 +220,13 @@ class Mascot extends React.Component<Props, State> {
}
render() {
const size = this.props.size;
const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
return (
<Animatable.View
style={{
width: size,
height: size,
aspectRatio: 1,
...this.props.style
}}
{...entryAnimation}
>
@ -241,8 +243,8 @@ class Mascot extends React.Component<Props, State> {
<Image
source={MASCOT_IMAGE}
style={{
width: size,
height: size,
width: "100%",
height:"100%",
}}
/>
{this.getEyes(this.state.currentEmotion)}

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,
@ -160,7 +153,7 @@ class MascotPopup extends React.Component<Props, State> {
duration={this.props.visible ? 1500 : 200}
>
<Mascot
size={this.mascotSize}
style={{width: this.mascotSize}}
animated={true}
emotion={this.props.emotion}
/>
@ -241,15 +234,16 @@ class MascotPopup extends React.Component<Props, State> {
}}>
<View style={{
marginTop: -80,
width: "100%"
}}>
{this.getMascot()}
{this.getSpeechBubble()}
</View>
</View>
</View>
</Portal>
)
;
</View>
</Portal>
)
;
} else
return null;

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

@ -146,20 +146,27 @@ export default class CustomIntroSlider extends React.Component<Props, State> {
</View>
<Animatable.View
animation={"fadeIn"}>
{index !== 0 && index !== this.introSlides.length -1
? <Animatable.View
animation={"pulse"}
iterationCount={"infinite"}
duration={2000}
{index !== 0 && index !== this.introSlides.length - 1
?
<Mascot
style={{
marginLeft: 30,
marginBottom: 0,
width: 100,
marginTop: -30,
}}>
<Mascot emotion={item.mascotStyle} size={100}/>
</Animatable.View> : null}
}}
emotion={item.mascotStyle}
animated={true}
entryAnimation={{
animation: "slideInLeft",
duration: 500
}}
loopAnimation={{
animation: "pulse",
iterationCount: "infinite",
duration: 2000,
}}
/> : null}
<View style={{
marginLeft: 50,
width: 0,
@ -204,23 +211,23 @@ export default class CustomIntroSlider extends React.Component<Props, State> {
getEndView = () => {
return (
<View style={{flex: 1}}>
<View
style={styles.center}>
<Mascot
size={250}
emotion={MASCOT_STYLE.COOL}
animated={true}
entryAnimation={{
animation: "slideInDown",
duration: 2000,
}}
loopAnimation={{
animation: "pulse",
duration: 2000,
iterationCount: "infinite"
}}
/>
</View>
<Mascot
style={{
...styles.center,
height: "80%"
}}
emotion={MASCOT_STYLE.COOL}
animated={true}
entryAnimation={{
animation: "slideInDown",
duration: 2000,
}}
loopAnimation={{
animation: "pulse",
duration: 2000,
iterationCount: "infinite"
}}
/>
</View>
);
}
@ -228,55 +235,52 @@ export default class CustomIntroSlider extends React.Component<Props, State> {
getWelcomeView = () => {
return (
<View style={{flex: 1}}>
<View
style={styles.center}>
<Mascot
size={250}
emotion={MASCOT_STYLE.NORMAL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
<Mascot
style={{
...styles.center,
height: "80%"
}}
emotion={MASCOT_STYLE.NORMAL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
style={{
style={{
color: "#fff",
textAlign: "center",
fontSize: 25,
}}>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
delay={200}
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
delay={200}
style={{
position: "absolute",
bottom: 30,
right: "20%",
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
position: "absolute",
top: 210,
left: 160,
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: "auto",
marginBottom: "auto",
transform: [{rotateZ: "70deg"}],
}}
name={"undo"}
color={'#fff'}
size={40}/>
</Animatable.View>
</View>
...styles.center,
transform: [{rotateZ: "70deg"}],
}}
name={"undo"}
color={'#fff'}
size={40}/>
</Animatable.View>
</View>
)
}
@ -403,5 +407,5 @@ const styles = StyleSheet.create({
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
}
},
});

View file

@ -78,7 +78,7 @@ class CustomTabBar extends React.Component<Props, State> {
canPreventDefault: true,
});
if (route.name === "home" && !event.defaultPrevented)
this.props.navigation.navigate('tetris');
this.props.navigation.navigate('game-start');
}
/**

View file

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

View file

@ -1,14 +1,14 @@
// @flow
import AsyncStorageManager from "./AsyncStorageManager";
import {DarkTheme, DefaultTheme, Theme} from 'react-native-paper';
import {DarkTheme, DefaultTheme} from 'react-native-paper';
import AprilFoolsManager from "./AprilFoolsManager";
import {Appearance} from 'react-native-appearance';
const colorScheme = Appearance.getColorScheme();
export type CustomTheme = {
...Theme,
...DefaultTheme,
colors: {
primary: string,
accent: string,
@ -56,6 +56,10 @@ export type CustomTheme = {
tetrisJ: string,
tetrisL: string,
gameGold: string,
gameSilver: string,
gameBronze: string,
// Mascot Popup
mascotMessageArrow: string,
},
@ -119,8 +123,7 @@ export default class ThemeManager {
tutorinsaColor: '#f93943',
// Tetris
tetrisBackground: '#e6e6e6',
tetrisBorder: '#2f2f2f',
tetrisBackground: '#f0f0f0',
tetrisScore: '#e2bd33',
tetrisI: '#3cd9e6',
tetrisO: '#ffdd00',
@ -130,6 +133,10 @@ export default class ThemeManager {
tetrisJ: '#2a67e3',
tetrisL: '#da742d',
gameGold: "#ffd610",
gameSilver: "#7b7b7b",
gameBronze: "#a15218",
// Mascot Popup
mascotMessageArrow: "#dedede",
},
@ -182,8 +189,7 @@ export default class ThemeManager {
tutorinsaColor: '#f93943',
// Tetris
tetrisBackground: '#2c2c2c',
tetrisBorder: '#1b1b1b',
tetrisBackground: '#181818',
tetrisScore: '#e2d707',
tetrisI: '#30b3be',
tetrisO: '#c1a700',
@ -193,6 +199,10 @@ export default class ThemeManager {
tetrisJ: '#0f37b9',
tetrisL: '#b96226',
gameGold: "#ffd610",
gameSilver: "#7b7b7b",
gameBronze: "#a15218",
// Mascot Popup
mascotMessageArrow: "#323232",
},

View file

@ -8,7 +8,7 @@ import DebugScreen from '../screens/About/DebugScreen';
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
import i18n from "i18n-js";
import TabNavigator from "./TabNavigator";
import TetrisScreen from "../screens/Tetris/TetrisScreen";
import GameMainScreen from "../screens/Game/screens/GameMainScreen";
import VoteScreen from "../screens/Amicale/VoteScreen";
import LoginScreen from "../screens/Amicale/LoginScreen";
import {Platform} from "react-native";
@ -27,6 +27,8 @@ import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen";
import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen";
import GameStartScreen from "../screens/Game/screens/GameStartScreen";
import GameEndScreen from "../screens/Game/screens/GameEndScreen";
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
@ -92,8 +94,22 @@ function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
}}
/>
<MainStack.Screen
name="tetris"
component={TetrisScreen}
name="game-start"
component={GameStartScreen}
options={{
title: i18n.t("screens.game.title"),
}}
/>
<MainStack.Screen
name="game-main"
component={GameMainScreen}
options={{
title: i18n.t("screens.game.title"),
}}
/>
<MainStack.Screen
name="game-end"
component={GameEndScreen}
options={{
title: i18n.t("screens.game.title"),
}}

View file

@ -117,8 +117,10 @@ function HomeStackComponent(initialRoute: string | null, defaultData: { [key: st
},
headerTitle: (props) => <View style={{flexDirection: "row"}}>
<Mascot
style={{
width: 50
}}
emotion={MASCOT_STYLE.RANDOM}
size={50}
animated={true}
entryAnimation={{
animation: "bounceIn",

View file

@ -158,15 +158,18 @@ class ProfileScreen extends React.Component<Props, State> {
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
left={() => <Mascot
emotion={MASCOT_STYLE.COOL}
size={60}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 1000
}}
/>}
left={() =>
<Mascot
style={{
width: 60
}}
emotion={MASCOT_STYLE.COOL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 1000
}}
/>}
titleStyle={{marginLeft: 10}}
/>
<Card.Content>

View file

@ -1,10 +1,14 @@
// @flow
export type coordinates = {
import type {CustomTheme} from "../../../managers/ThemeManager";
export type Coordinates = {
x: number,
y: number,
}
type Shape = Array<Array<number>>;
/**
* Abstract class used to represent a BaseShape.
* Abstract classes do not exist by default in Javascript: we force it by throwing errors in the constructor
@ -12,16 +16,18 @@ export type coordinates = {
*/
export default class BaseShape {
#currentShape: Array<Array<number>>;
#currentShape: Shape;
#rotation: number;
position: coordinates;
position: Coordinates;
theme: CustomTheme;
/**
* Prevent instantiation if classname is BaseShape to force class to be abstract
*/
constructor() {
constructor(theme: CustomTheme) {
if (this.constructor === BaseShape)
throw new Error("Abstract class can't be instantiated");
this.theme = theme;
this.#rotation = 0;
this.position = {x: 0, y: 0};
this.#currentShape = this.getShapes()[this.#rotation];
@ -41,16 +47,14 @@ export default class BaseShape {
*
* Used by tests to read private fields
*/
getShapes(): Array<Array<Array<number>>> {
getShapes(): Array<Shape> {
throw new Error("Method 'getShapes()' must be implemented");
}
/**
* Gets this object's current shape.
*
* Used by tests to read private fields
*/
getCurrentShape(): Array<Array<number>> {
getCurrentShape(): Shape {
return this.#currentShape;
}
@ -59,9 +63,9 @@ export default class BaseShape {
* This will return an array of coordinates representing the positions of the cells used by this object.
*
* @param isAbsolute Should we take into account the current position of the object?
* @return {Array<coordinates>} This object cells coordinates
* @return {Array<Coordinates>} This object cells coordinates
*/
getCellsCoordinates(isAbsolute: boolean): Array<coordinates> {
getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> {
let coordinates = [];
for (let row = 0; row < this.#currentShape.length; row++) {
for (let col = 0; col < this.#currentShape[row].length; col++) {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeI extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 3;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisI;
return this.theme.colors.tetrisI;
}
getShapes() {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeJ extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 3;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisJ;
return this.theme.colors.tetrisJ;
}
getShapes() {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeL extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 3;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisL;
return this.theme.colors.tetrisL;
}
getShapes() {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeO extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 4;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisO;
return this.theme.colors.tetrisO;
}
getShapes() {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeS extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 3;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisS;
return this.theme.colors.tetrisS;
}
getShapes() {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeT extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 3;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisT;
return this.theme.colors.tetrisT;
}
getShapes() {

View file

@ -1,19 +1,17 @@
// @flow
import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeZ extends BaseShape {
#colors: Object;
constructor(colors: Object) {
super();
constructor(theme: CustomTheme) {
super(theme);
this.position.x = 3;
this.#colors = colors;
}
getColor(): string {
return this.#colors.tetrisZ;
return this.theme.colors.tetrisZ;
}
getShapes() {

View file

@ -1,7 +1,7 @@
import React from 'react';
import GridManager from "../GridManager";
import ScoreManager from "../ScoreManager";
import Piece from "../Piece";
import GridManager from "../logic/GridManager";
import ScoreManager from "../logic/ScoreManager";
import Piece from "../logic/Piece";
let colors = {
tetrisBackground: "#000002"

View file

@ -1,5 +1,5 @@
import React from 'react';
import Piece from "../Piece";
import Piece from "../logic/Piece";
import ShapeI from "../Shapes/ShapeI";
let colors = {

View file

@ -1,5 +1,5 @@
import React from 'react';
import ScoreManager from "../ScoreManager";
import ScoreManager from "../logic/ScoreManager";
test('incrementScore', () => {

View file

@ -3,30 +3,26 @@
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager";
export type Cell = {color: string, isEmpty: boolean, key: string};
type Props = {
item: Object
cell: Cell,
theme: CustomTheme,
}
class Cell extends React.PureComponent<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
class CellComponent extends React.PureComponent<Props> {
render() {
const item = this.props.item;
const item = this.props.cell;
return (
<View
style={{
flex: 1,
backgroundColor: item.isEmpty ? 'transparent' : item.color,
borderColor: item.isEmpty ? 'transparent' : this.colors.tetrisBorder,
borderStyle: 'solid',
borderRadius: 2,
borderColor: 'transparent',
borderRadius: 4,
borderWidth: 1,
aspectRatio: 1,
}}
@ -38,4 +34,4 @@ class Cell extends React.PureComponent<Props> {
}
export default withTheme(Cell);
export default withTheme(CellComponent);

View file

@ -3,35 +3,26 @@
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import Cell from "./Cell";
import type {Cell} from "./CellComponent";
import CellComponent from "./CellComponent";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
export type Grid = Array<Array<CellComponent>>;
type Props = {
navigation: Object,
grid: Array<Array<Object>>,
backgroundColor: string,
height: number,
width: number,
containerMaxHeight: number | string,
containerMaxWidth: number | string,
style: ViewStyle,
}
class Grid extends React.Component<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
class GridComponent extends React.Component<Props> {
getRow(rowNumber: number) {
let cells = this.props.grid[rowNumber].map(this.getCellRender);
return (
<View
style={{
flexDirection: 'row',
backgroundColor: this.props.backgroundColor,
}}
style={{flexDirection: 'row',}}
key={rowNumber.toString()}
>
{cells}
@ -39,8 +30,8 @@ class Grid extends React.Component<Props> {
);
}
getCellRender = (item: Object) => {
return <Cell item={item} key={item.key}/>;
getCellRender = (item: Cell) => {
return <CellComponent cell={item}/>;
};
getGrid() {
@ -54,12 +45,9 @@ class Grid extends React.Component<Props> {
render() {
return (
<View style={{
flexDirection: 'column',
maxWidth: this.props.containerMaxWidth,
maxHeight: this.props.containerMaxHeight,
aspectRatio: this.props.width / this.props.height,
marginLeft: 'auto',
marginRight: 'auto',
borderRadius: 4,
...this.props.style
}}>
{this.getGrid()}
</View>
@ -67,4 +55,4 @@ class Grid extends React.Component<Props> {
}
}
export default withTheme(Grid);
export default withTheme(GridComponent);

View file

@ -0,0 +1,53 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import type {Grid} from "./GridComponent";
import GridComponent from "./GridComponent";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type Props = {
items: Array<Grid>,
style: ViewStyle
}
class Preview extends React.PureComponent<Props> {
getGrids() {
let grids = [];
for (let i = 0; i < this.props.items.length; i++) {
grids.push(this.getGridRender(this.props.items[i], i));
}
return grids;
}
getGridRender(item: Grid, index: number) {
return <GridComponent
width={item[0].length}
height={item.length}
grid={item}
style={{
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
}}
key={index.toString()}
/>;
};
render() {
if (this.props.items.length > 0) {
return (
<View style={this.props.style}>
{this.getGrids()}
</View>
);
} else
return null;
}
}
export default withTheme(Preview);

View file

@ -3,6 +3,7 @@
import Piece from "./Piece";
import ScoreManager from "./ScoreManager";
import GridManager from "./GridManager";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class GameLogic {
@ -45,20 +46,20 @@ export default class GameLogic {
#onClock: Function;
endCallback: Function;
#colors: Object;
#theme: CustomTheme;
constructor(height: number, width: number, colors: Object) {
constructor(height: number, width: number, theme: CustomTheme) {
this.#height = height;
this.#width = width;
this.#gameRunning = false;
this.#gamePaused = false;
this.#colors = colors;
this.#theme = theme;
this.#autoRepeatActivationDelay = 300;
this.#autoRepeatDelay = 50;
this.#nextPieces = [];
this.#nextPiecesCount = 3;
this.#scoreManager = new ScoreManager();
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors);
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
}
getHeight(): number {
@ -144,7 +145,7 @@ export default class GameLogic {
callback(this.#gridManager.getCurrentGrid());
}
this.#pressInInterval = setTimeout(() =>
this.movePressedRepeat(false, callback, x, y),
this.movePressedRepeat(false, callback, x, y),
isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay
);
}
@ -165,7 +166,8 @@ export default class GameLogic {
getNextPiecesPreviews() {
let finalArray = [];
for (let i = 0; i < this.#nextPieces.length; i++) {
finalArray.push(this.#gridManager.getEmptyGrid(4, 4));
const gridSize = this.#nextPieces[i].getCurrentShape().getCurrentShape()[0].length;
finalArray.push(this.#gridManager.getEmptyGrid(gridSize, gridSize));
this.#nextPieces[i].toGrid(finalArray[i], true);
}
@ -179,7 +181,7 @@ export default class GameLogic {
generateNextPieces() {
while (this.#nextPieces.length < this.#nextPiecesCount) {
this.#nextPieces.push(new Piece(this.#colors));
this.#nextPieces.push(new Piece(this.#theme));
}
}
@ -219,7 +221,7 @@ export default class GameLogic {
this.#gameTime = 0;
this.#scoreManager = new ScoreManager();
this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()];
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors);
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
this.#nextPieces = [];
this.generateNextPieces();
this.createTetromino();

View file

@ -2,39 +2,37 @@
import Piece from "./Piece";
import ScoreManager from "./ScoreManager";
import type {coordinates} from './Shapes/BaseShape';
export type cell = {color: string, isEmpty: boolean, key: string};
export type grid = Array<Array<cell>>;
import type {Coordinates} from '../Shapes/BaseShape';
import type {Grid} from "../components/GridComponent";
import type {Cell} from "../components/CellComponent";
import type {CustomTheme} from "../../../managers/ThemeManager";
/**
* Class used to manage the game grid
*
*/
export default class GridManager {
#currentGrid: grid;
#colors: Object;
#currentGrid: Grid;
#theme: CustomTheme;
/**
* Initializes a grid of the given size
*
* @param width The grid width
* @param height The grid height
* @param colors Object containing current theme colors
* @param theme Object containing current theme
*/
constructor(width: number, height: number, colors: Object) {
this.#colors = colors;
constructor(width: number, height: number, theme: CustomTheme) {
this.#theme = theme;
this.#currentGrid = this.getEmptyGrid(height, width);
}
/**
* Get the current grid
*
* @return {grid} The current grid
* @return {Grid} The current grid
*/
getCurrentGrid(): grid {
getCurrentGrid(): Grid {
return this.#currentGrid;
}
@ -42,13 +40,13 @@ export default class GridManager {
* Get a new empty grid line of the given size
*
* @param width The line size
* @return {Array<cell>}
* @return {Array<Cell>}
*/
getEmptyLine(width: number): Array<cell> {
getEmptyLine(width: number): Array<Cell> {
let line = [];
for (let col = 0; col < width; col++) {
line.push({
color: this.#colors.tetrisBackground,
color: this.#theme.colors.tetrisBackground,
isEmpty: true,
key: col.toString(),
});
@ -61,9 +59,9 @@ export default class GridManager {
*
* @param width The grid width
* @param height The grid height
* @return {grid} A new empty grid
* @return {Grid} A new empty grid
*/
getEmptyGrid(height: number, width: number): grid {
getEmptyGrid(height: number, width: number): Grid {
let grid = [];
for (let row = 0; row < height; row++) {
grid.push(this.getEmptyLine(width));
@ -91,21 +89,21 @@ export default class GridManager {
* Gets the lines to clear around the given piece's coordinates.
* The piece's coordinates are used for optimization and to prevent checking the whole grid.
*
* @param coord The piece's coordinates to check lines at
* @param pos The piece's coordinates to check lines at
* @return {Array<number>} An array containing the line numbers to clear
*/
getLinesToClear(coord: Array<coordinates>): Array<number> {
getLinesToClear(pos: Array<Coordinates>): Array<number> {
let rows = [];
for (let i = 0; i < coord.length; i++) {
for (let i = 0; i < pos.length; i++) {
let isLineFull = true;
for (let col = 0; col < this.#currentGrid[coord[i].y].length; col++) {
if (this.#currentGrid[coord[i].y][col].isEmpty) {
for (let col = 0; col < this.#currentGrid[pos[i].y].length; col++) {
if (this.#currentGrid[pos[i].y][col].isEmpty) {
isLineFull = false;
break;
}
}
if (isLineFull && rows.indexOf(coord[i].y) === -1)
rows.push(coord[i].y);
if (isLineFull && rows.indexOf(pos[i].y) === -1)
rows.push(pos[i].y);
}
return rows;
}

View file

@ -1,12 +1,14 @@
import ShapeL from "./Shapes/ShapeL";
import ShapeI from "./Shapes/ShapeI";
import ShapeJ from "./Shapes/ShapeJ";
import ShapeO from "./Shapes/ShapeO";
import ShapeS from "./Shapes/ShapeS";
import ShapeT from "./Shapes/ShapeT";
import ShapeZ from "./Shapes/ShapeZ";
import type {coordinates} from './Shapes/BaseShape';
import type {grid} from './GridManager';
import ShapeL from "../Shapes/ShapeL";
import ShapeI from "../Shapes/ShapeI";
import ShapeJ from "../Shapes/ShapeJ";
import ShapeO from "../Shapes/ShapeO";
import ShapeS from "../Shapes/ShapeS";
import ShapeT from "../Shapes/ShapeT";
import ShapeZ from "../Shapes/ShapeZ";
import type {Coordinates} from '../Shapes/BaseShape';
import BaseShape from "../Shapes/BaseShape";
import type {Grid} from "../components/GridComponent";
import type {CustomTheme} from "../../../managers/ThemeManager";
/**
* Class used as an abstraction layer for shapes.
@ -24,26 +26,26 @@ export default class Piece {
ShapeT,
ShapeZ,
];
#currentShape: Object;
#colors: Object;
#currentShape: BaseShape;
#theme: CustomTheme;
/**
* Initializes this piece's color and shape
*
* @param colors Object containing current theme colors
* @param theme Object containing current theme
*/
constructor(colors: Object) {
this.#currentShape = this.getRandomShape(colors);
this.#colors = colors;
constructor(theme: CustomTheme) {
this.#currentShape = this.getRandomShape(theme);
this.#theme = theme;
}
/**
* Gets a random shape object
*
* @param colors Object containing current theme colors
* @param theme Object containing current theme
*/
getRandomShape(colors: Object) {
return new this.#shapes[Math.floor(Math.random() * 7)](colors);
getRandomShape(theme: CustomTheme) {
return new this.#shapes[Math.floor(Math.random() * 7)](theme);
}
/**
@ -51,13 +53,13 @@ export default class Piece {
*
* @param grid The grid to remove the piece from
*/
removeFromGrid(grid: grid) {
const coord: Array<coordinates> = this.#currentShape.getCellsCoordinates(true);
for (let i = 0; i < coord.length; i++) {
grid[coord[i].y][coord[i].x] = {
color: this.#colors.tetrisBackground,
removeFromGrid(grid: Grid) {
const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(true);
for (let i = 0; i < pos.length; i++) {
grid[pos[i].y][pos[i].x] = {
color: this.#theme.colors.tetrisBackground,
isEmpty: true,
key: grid[coord[i].y][coord[i].x].key
key: grid[pos[i].y][pos[i].x].key
};
}
}
@ -68,13 +70,13 @@ export default class Piece {
* @param grid The grid to add the piece to
* @param isPreview Should we use this piece's current position to determine the cells?
*/
toGrid(grid: grid, isPreview: boolean) {
const coord: Array<coordinates> = this.#currentShape.getCellsCoordinates(!isPreview);
for (let i = 0; i < coord.length; i++) {
grid[coord[i].y][coord[i].x] = {
toGrid(grid: Grid, isPreview: boolean) {
const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(!isPreview);
for (let i = 0; i < pos.length; i++) {
grid[pos[i].y][pos[i].x] = {
color: this.#currentShape.getColor(),
isEmpty: false,
key: grid[coord[i].y][coord[i].x].key
key: grid[pos[i].y][pos[i].x].key
};
}
}
@ -87,15 +89,15 @@ export default class Piece {
* @param height The grid's height
* @return {boolean} If the position is valid
*/
isPositionValid(grid: grid, width: number, height: number) {
isPositionValid(grid: Grid, width: number, height: number) {
let isValid = true;
const coord: Array<coordinates> = this.#currentShape.getCellsCoordinates(true);
for (let i = 0; i < coord.length; i++) {
if (coord[i].x >= width
|| coord[i].x < 0
|| coord[i].y >= height
|| coord[i].y < 0
|| !grid[coord[i].y][coord[i].x].isEmpty) {
const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(true);
for (let i = 0; i < pos.length; i++) {
if (pos[i].x >= width
|| pos[i].x < 0
|| pos[i].y >= height
|| pos[i].y < 0
|| !grid[pos[i].y][pos[i].x].isEmpty) {
isValid = false;
break;
}
@ -114,7 +116,7 @@ export default class Piece {
* @param freezeCallback Callback to use if the piece should freeze itself
* @return {boolean} True if the move was valid, false otherwise
*/
tryMove(x: number, y: number, grid: grid, width: number, height: number, freezeCallback: Function) {
tryMove(x: number, y: number, grid: Grid, width: number, height: number, freezeCallback: () => void) {
if (x > 1) x = 1; // Prevent moving from more than one tile
if (x < -1) x = -1;
if (y > 1) y = 1;
@ -143,7 +145,7 @@ export default class Piece {
* @param height The grid's height
* @return {boolean} True if the rotation was valid, false otherwise
*/
tryRotate(grid: grid, width: number, height: number) {
tryRotate(grid: Grid, width: number, height: number) {
this.removeFromGrid(grid);
this.#currentShape.rotate(true);
if (!this.isPositionValid(grid, width, height)) {
@ -158,9 +160,13 @@ export default class Piece {
/**
* Gets this piece used cells coordinates
*
* @return {Array<coordinates>} An array of coordinates
* @return {Array<Coordinates>} An array of coordinates
*/
getCoordinates(): Array<coordinates> {
getCoordinates(): Array<Coordinates> {
return this.#currentShape.getCellsCoordinates(true);
}
getCurrentShape() {
return this.#currentShape;
}
}

View file

@ -0,0 +1,26 @@
// @flow
import * as React from "react";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import {withTheme} from "react-native-paper";
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme,
}
type State = {
}
class GameEndScreen extends React.Component<Props, State> {
render() {
return (
null
);
}
}
export default withTheme(GameEndScreen);

View file

@ -0,0 +1,424 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {Caption, IconButton, Text, withTheme} from 'react-native-paper';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import GameLogic from "../logic/GameLogic";
import type {Grid} from "../components/GridComponent";
import GridComponent from "../components/GridComponent";
import Preview from "../components/Preview";
import i18n from "i18n-js";
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {OptionsDialogButton} from "../../../components/Dialogs/OptionsDialog";
import OptionsDialog from "../../../components/Dialogs/OptionsDialog";
type Props = {
navigation: StackNavigationProp,
route: { params: { highScore: number }, ... },
theme: CustomTheme,
}
type State = {
grid: Grid,
gameRunning: boolean,
gameTime: number,
gameScore: number,
gameLevel: number,
dialogVisible: boolean,
dialogTitle: string,
dialogMessage: string,
dialogButtons: Array<OptionsDialogButton>,
onDialogDismiss: () => void,
}
class GameMainScreen extends React.Component<Props, State> {
logic: GameLogic;
highScore: number | null;
constructor(props) {
super(props);
this.logic = new GameLogic(20, 10, this.props.theme);
this.state = {
grid: this.logic.getCurrentGrid(),
gameRunning: false,
gameTime: 0,
gameScore: 0,
gameLevel: 0,
dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
dialogButtons: [],
onDialogDismiss: () => {
},
};
if (this.props.route.params != null)
this.highScore = this.props.route.params.highScore;
}
componentDidMount() {
this.props.navigation.setOptions({
headerRight: this.getRightButton,
});
this.startGame();
}
getRightButton = () => {
return <MaterialHeaderButtons>
<Item title="pause" iconName="pause" onPress={this.togglePause}/>
</MaterialHeaderButtons>;
}
getFormattedTime(seconds: number) {
let 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();
return format;
}
onTick = (score: number, level: number, newGrid: Grid) => {
this.setState({
gameScore: score,
gameLevel: level,
grid: newGrid,
});
}
onClock = (time: number) => {
this.setState({
gameTime: time,
});
}
updateGrid = (newGrid: Grid) => {
this.setState({
grid: newGrid,
});
}
updateGridScore = (newGrid: Grid, score: number) => {
this.setState({
grid: newGrid,
gameScore: score,
});
}
togglePause = () => {
this.logic.togglePause();
if (this.logic.isGamePaused())
this.showPausePopup();
}
onDialogDismiss = () => this.setState({dialogVisible: false});
showPausePopup = () => {
const onDismiss = () => {
this.togglePause();
this.onDialogDismiss();
};
this.setState({
dialogVisible: true,
dialogTitle: i18n.t("screens.game.pause"),
dialogMessage: i18n.t("screens.game.pauseMessage"),
dialogButtons: [
{
title: i18n.t("screens.game.restart.text"),
onPress: this.showRestartConfirm
},
{
title: i18n.t("screens.game.resume"),
onPress: onDismiss
}
],
onDialogDismiss: onDismiss,
});
}
showRestartConfirm = () => {
this.setState({
dialogVisible: true,
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();
}
},
{
title: i18n.t("screens.game.restart.confirmNo"),
onPress: this.showPausePopup
}
],
onDialogDismiss: this.showPausePopup,
});
}
showGameOverConfirm() {
let message = i18n.t("screens.game.gameOver.score") + this.state.gameScore + '\n';
message += i18n.t("screens.game.gameOver.level") + this.state.gameLevel + '\n';
message += i18n.t("screens.game.gameOver.time") + this.getFormattedTime(this.state.gameTime) + '\n';
const onDismiss = () => {
this.onDialogDismiss();
this.startGame();
};
this.setState({
dialogVisible: true,
dialogTitle: i18n.t("screens.game.gameOver.text"),
dialogMessage: message,
dialogButtons: [
{
title: i18n.t("screens.game.gameOver.exit"),
onPress: () => this.props.navigation.goBack()
},
{
title: i18n.t("screens.game.resume"),
onPress: onDismiss
}
],
onDialogDismiss: onDismiss,
});
}
startGame = () => {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
this.setState({
gameRunning: true,
});
}
onGameEnd = (time: number, score: number, isRestart: boolean) => {
this.setState({
gameTime: time,
gameScore: score,
gameRunning: false,
});
if (!isRestart)
this.props.navigation.replace(
"game-start",
{
score: this.state.gameScore,
level: this.state.gameLevel,
time: this.state.gameTime,
}
);
}
getStatusIcons() {
return (
<View style={{
flex: 1,
marginTop: "auto",
marginBottom: "auto"
}}>
<View style={{
marginLeft: 'auto',
marginRight: 'auto',
}}>
<Caption style={{
marginLeft: "auto",
marginRight: "auto",
marginBottom: 5,
}}>{i18n.t("screens.game.time")}</Caption>
<View style={{
flexDirection: "row"
}}>
<MaterialCommunityIcons
name={'timer'}
color={this.props.theme.colors.subtitle}
size={20}/>
<Text style={{
marginLeft: 5,
color: this.props.theme.colors.subtitle
}}>{this.getFormattedTime(this.state.gameTime)}</Text>
</View>
</View>
<View style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 20,
}}>
<Caption style={{
marginLeft: "auto",
marginRight: "auto",
marginBottom: 5,
}}>{i18n.t("screens.game.level")}</Caption>
<View style={{
flexDirection: "row"
}}>
<MaterialCommunityIcons
name={'gamepad-square'}
color={this.props.theme.colors.text}
size={20}/>
<Text style={{
marginLeft: 5
}}>{this.state.gameLevel}</Text>
</View>
</View>
</View>
);
}
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",
}}>
<Text style={{
marginLeft: 5,
fontSize: 20,
}}>{i18n.t("screens.game.score", {score: this.state.gameScore})}</Text>
<MaterialCommunityIcons
name={'star'}
color={this.props.theme.colors.tetrisScore}
size={20}
style={{
marginTop: "auto",
marginBottom: "auto",
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>
);
}
getControlButtons() {
return (
<View style={{
height: 80,
flexDirection: "row"
}}>
<IconButton
icon="rotate-right-variant"
size={40}
onPress={() => this.logic.rotatePressed(this.updateGrid)}
style={{flex: 1}}
/>
<View style={{
flexDirection: 'row',
flex: 4
}}>
<IconButton
icon="chevron-left"
size={40}
style={{flex: 1}}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.leftPressedIn(this.updateGrid)}
/>
<IconButton
icon="chevron-right"
size={40}
style={{flex: 1}}
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={{flex: 1}}
color={this.props.theme.colors.tetrisScore}
/>
</View>
);
}
render() {
return (
<View style={{flex: 1}}>
<View style={{
flex: 1,
flexDirection: "row",
}}>
{this.getStatusIcons()}
<View style={{flex: 4}}>
{this.getScoreIcon()}
<GridComponent
width={this.logic.getWidth()}
height={this.logic.getHeight()}
grid={this.state.grid}
style={{
backgroundColor: this.props.theme.colors.tetrisBackground,
flex: 1,
marginLeft: "auto",
marginRight: "auto",
}}
/>
</View>
<View style={{flex: 1}}>
<Preview
items={this.logic.getNextPiecesPreviews()}
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
}}
/>
</View>
</View>
{this.getControlButtons()}
<OptionsDialog
visible={this.state.dialogVisible}
title={this.state.dialogTitle}
message={this.state.dialogMessage}
buttons={this.state.dialogButtons}
onDismiss={this.state.onDialogDismiss}
/>
</View>
);
}
}
export default withTheme(GameMainScreen);

View file

@ -0,0 +1,453 @@
// @flow
import * as React from "react";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
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";
import AsyncStorageManager from "../../../managers/AsyncStorageManager";
import type {Grid} from "../components/GridComponent";
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,
}
type State = {
mascotDialogVisible: boolean,
}
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",
}
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 = () => {
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.gameStartShowBanner.key,
'0'
);
this.setState({mascotDialogVisible: false})
};
getPiecesBackground() {
let gridList = [];
for (let i = 0; i < 18; i++) {
gridList.push(this.gridManager.getEmptyGrid(4, 4));
const piece = new Piece(this.props.theme);
piece.toGrid(gridList[i], true);
}
return (
<View style={{
position: "absolute",
width: "100%",
height: "100%",
}}>
{gridList.map((item: Grid, 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
animation={"fadeInDownBig"}
delay={animDelay}
duration={animDuration}
key={index.toString()}
style={{
width: size + "%",
position: "absolute",
top: top + "%",
left: left + "%",
}}
>
<View style={{
transform: [{rotateZ: rot + "deg"}],
}}>
<GridComponent
width={4}
height={4}
grid={item}
style={{
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
}}
/>
</View>
</Animatable.View>
);
})}
</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>
<Mascot emotion={MASCOT_STYLE.COOL} style={{
width: "40%",
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,
}}>
<Card.Content>
<Headline
style={{
textAlign: "center",
color: this.props.theme.colors.primary
}}>
{i18n.t("screens.game.welcomeTitle")}
</Headline>
<Divider/>
<Paragraph
style={{
textAlign: "center",
marginTop: 10,
}}>
{i18n.t("screens.game.welcomeMessage")}
</Paragraph>
</Card.Content>
</Card>
</View>
);
}
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.gameStats != null
? this.getPostGameContent(this.gameStats)
: this.getWelcomeText()
}
<Button
icon={"play"}
mode={"contained"}
onPress={() => this.props.navigation.replace(
"game-main",
{
highScore: this.scores.length > 0
? this.scores[0]
: null
}
)}
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: 10,
}}
>
{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")}
message={i18n.t("screens.game.mascotDialog.message")}
icon={"gamepad-variant"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.game.mascotDialog.button"),
icon: "check",
onPress: this.hideMascotDialog,
}
}}
emotion={MASCOT_STYLE.COOL}
/>
</ScrollView>
</View>
);
}
}
export default withTheme(GameStartScreen);

View file

@ -1,299 +0,0 @@
// @flow
import * as React from 'react';
import {Alert, View} from 'react-native';
import {IconButton, Text, withTheme} from 'react-native-paper';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import GameLogic from "./GameLogic";
import Grid from "./components/Grid";
import Preview from "./components/Preview";
import i18n from "i18n-js";
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
type Props = {
navigation: Object,
}
type State = {
grid: Array<Array<Object>>,
gameRunning: boolean,
gameTime: number,
gameScore: number,
gameLevel: number,
}
class TetrisScreen extends React.Component<Props, State> {
colors: Object;
logic: GameLogic;
onTick: Function;
onClock: Function;
onGameEnd: Function;
updateGrid: Function;
updateGridScore: Function;
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.logic = new GameLogic(20, 10, this.colors);
this.state = {
grid: this.logic.getCurrentGrid(),
gameRunning: false,
gameTime: 0,
gameScore: 0,
gameLevel: 0,
};
this.onTick = this.onTick.bind(this);
this.onClock = this.onClock.bind(this);
this.onGameEnd = this.onGameEnd.bind(this);
this.updateGrid = this.updateGrid.bind(this);
this.updateGridScore = this.updateGridScore.bind(this);
this.props.navigation.addListener('blur', this.onScreenBlur.bind(this));
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
}
componentDidMount() {
const rightButton = this.getRightButton.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
this.startGame();
}
getRightButton() {
return <MaterialHeaderButtons>
<Item title="pause" iconName="pause" onPress={() => this.togglePause()}/>
</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);
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();
return format;
}
onTick(score: number, level: number, newGrid: Array<Array<Object>>) {
this.setState({
gameScore: score,
gameLevel: level,
grid: newGrid,
});
}
onClock(time: number) {
this.setState({
gameTime: time,
});
}
updateGrid(newGrid: Array<Array<Object>>) {
this.setState({
grid: newGrid,
});
}
updateGridScore(newGrid: Array<Array<Object>>, score: number) {
this.setState({
grid: newGrid,
gameScore: score,
});
}
togglePause() {
this.logic.togglePause();
if (this.logic.isGamePaused())
this.showPausePopup();
}
showPausePopup() {
Alert.alert(
i18n.t("screens.game.pause"),
i18n.t("screens.game.pauseMessage"),
[
{text: i18n.t("screens.game.restart.text"), onPress: () => this.showRestartConfirm()},
{text: i18n.t("screens.game.resume"), onPress: () => this.togglePause()},
],
{cancelable: false},
);
}
showRestartConfirm() {
Alert.alert(
i18n.t("screens.game.restart.confirm"),
i18n.t("screens.game.restart.confirmMessage"),
[
{text: i18n.t("screens.game.restart.confirmNo"), onPress: () => this.showPausePopup()},
{text: i18n.t("screens.game.restart.confirmYes"), onPress: () => this.startGame()},
],
{cancelable: false},
);
}
showGameOverConfirm() {
let message = i18n.t("screens.game.gameOver.score") + this.state.gameScore + '\n';
message += i18n.t("screens.game.gameOver.level") + this.state.gameLevel + '\n';
message += i18n.t("screens.game.gameOver.time") + this.getFormattedTime(this.state.gameTime) + '\n';
Alert.alert(
i18n.t("screens.game.gameOver.text"),
message,
[
{text: i18n.t("screens.game.gameOver.exit"), onPress: () => this.props.navigation.goBack()},
{text: i18n.t("screens.game.restart.text"), onPress: () => this.startGame()},
],
{cancelable: false},
);
}
startGame() {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
this.setState({
gameRunning: true,
});
}
onGameEnd(time: number, score: number, isRestart: boolean) {
this.setState({
gameTime: time,
gameScore: score,
gameRunning: false,
});
if (!isRestart)
this.showGameOverConfirm();
}
render() {
return (
<View style={{
width: '100%',
height: '100%',
}}>
<View style={{
flexDirection: 'row',
position: 'absolute',
top: 5,
left: 10,
}}>
<MaterialCommunityIcons
name={'timer'}
color={this.colors.subtitle}
size={20}/>
<Text style={{
marginLeft: 5,
color: this.colors.subtitle
}}>{this.getFormattedTime(this.state.gameTime)}</Text>
</View>
<View style={{
flexDirection: 'row',
position: 'absolute',
top: 50,
left: 10,
}}>
<MaterialCommunityIcons
name={'gamepad'}
color={this.colors.text}
size={20}/>
<Text style={{
marginLeft: 5
}}>{this.state.gameLevel}</Text>
</View>
<View style={{
flexDirection: 'row',
marginRight: 'auto',
marginLeft: 'auto',
}}>
<MaterialCommunityIcons
name={'star'}
color={this.colors.tetrisScore}
size={30}/>
<Text style={{
marginLeft: 5,
fontSize: 22,
}}>{this.state.gameScore}</Text>
</View>
<Grid
width={this.logic.getWidth()}
height={this.logic.getHeight()}
containerMaxHeight={'80%'}
containerMaxWidth={'60%'}
grid={this.state.grid}
backgroundColor={this.colors.tetrisBackground}
/>
<View style={{
position: 'absolute',
top: 50,
right: 5,
}}>
<Preview
next={this.logic.getNextPiecesPreviews()}
/>
</View>
<View style={{
position: 'absolute',
bottom: 0,
flexDirection: 'row',
width: '100%',
}}>
<IconButton
icon="rotate-right-variant"
size={40}
onPress={() => this.logic.rotatePressed(this.updateGrid)}
style={{marginRight: 'auto'}}
/>
<View style={{
flexDirection: 'row',
}}>
<IconButton
icon="arrow-left"
size={40}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.leftPressedIn(this.updateGrid)}
/>
<IconButton
icon="arrow-right"
size={40}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.rightPressed(this.updateGrid)}
/>
</View>
<IconButton
icon="arrow-down"
size={40}
onPressIn={() => this.logic.downPressedIn(this.updateGridScore)}
onPress={() => this.logic.pressedOut()}
style={{marginLeft: 'auto'}}
color={this.colors.tetrisScore}
/>
</View>
</View>
);
}
}
export default withTheme(TetrisScreen);

View file

@ -1,57 +0,0 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import Grid from "./Grid";
type Props = {
next: Object,
}
class Preview extends React.PureComponent<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
getGrids() {
let grids = [];
for (let i = 0; i < this.props.next.length; i++) {
grids.push(
this.getGridRender(this.props.next[i], i)
);
}
return grids;
}
getGridRender(item: Object, index: number) {
return <Grid
width={item[0].length}
height={item.length}
grid={item}
containerMaxHeight={50}
containerMaxWidth={50}
backgroundColor={'transparent'}
key={index.toString()}
/>;
};
render() {
if (this.props.next.length > 0) {
return (
<View>
{this.getGrids()}
</View>
);
} else
return null;
}
}
export default withTheme(Preview);