Improve Game files to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-05 20:24:08 +02:00
parent 569e659779
commit cbe3777957
16 changed files with 1750 additions and 1610 deletions

View file

@ -1,13 +1,13 @@
// @flow // @flow
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
export type Coordinates = { export type CoordinatesType = {
x: number, x: number,
y: number, y: number,
} };
type Shape = Array<Array<number>>; export type ShapeType = Array<Array<number>>;
/** /**
* Abstract class used to represent a BaseShape. * Abstract class used to represent a BaseShape.
@ -15,96 +15,98 @@ type Shape = Array<Array<number>>;
* and in methods to implement * and in methods to implement
*/ */
export default class BaseShape { export default class BaseShape {
#currentShape: ShapeType;
#currentShape: Shape; #rotation: number;
#rotation: number;
position: Coordinates;
theme: CustomTheme;
/** position: CoordinatesType;
* Prevent instantiation if classname is BaseShape to force class to be abstract
*/
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];
}
/** theme: CustomThemeType;
* Gets this shape's color.
* Must be implemented by child class
*/
getColor(): string {
throw new Error("Method 'getColor()' must be implemented");
}
/** /**
* Gets this object's all possible shapes as an array. * Prevent instantiation if classname is BaseShape to force class to be abstract
* Must be implemented by child class. */
* constructor(theme: CustomThemeType) {
* Used by tests to read private fields if (this.constructor === BaseShape)
*/ throw new Error("Abstract class can't be instantiated");
getShapes(): Array<Shape> { this.theme = theme;
throw new Error("Method 'getShapes()' must be implemented"); this.#rotation = 0;
} this.position = {x: 0, y: 0};
this.#currentShape = this.getShapes()[this.#rotation];
}
/** /**
* Gets this object's current shape. * Gets this shape's color.
*/ * Must be implemented by child class
getCurrentShape(): Shape { */
return this.#currentShape; // eslint-disable-next-line class-methods-use-this
} getColor(): string {
throw new Error("Method 'getColor()' must be implemented");
}
/** /**
* Gets this object's coordinates. * Gets this object's all possible shapes as an array.
* This will return an array of coordinates representing the positions of the cells used by this object. * Must be implemented by child class.
* *
* @param isAbsolute Should we take into account the current position of the object? * Used by tests to read private fields
* @return {Array<Coordinates>} This object cells coordinates */
*/ // eslint-disable-next-line class-methods-use-this
getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> { getShapes(): Array<ShapeType> {
let coordinates = []; throw new Error("Method 'getShapes()' must be implemented");
for (let row = 0; row < this.#currentShape.length; row++) { }
for (let col = 0; col < this.#currentShape[row].length; col++) {
if (this.#currentShape[row][col] === 1) /**
if (isAbsolute) * Gets this object's current shape.
coordinates.push({x: this.position.x + col, y: this.position.y + row}); */
else getCurrentShape(): ShapeType {
coordinates.push({x: col, y: row}); return this.#currentShape;
} }
/**
* Gets this object's coordinates.
* 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<CoordinatesType>} This object cells coordinates
*/
getCellsCoordinates(isAbsolute: boolean): Array<CoordinatesType> {
const coordinates = [];
for (let row = 0; row < this.#currentShape.length; row += 1) {
for (let col = 0; col < this.#currentShape[row].length; col += 1) {
if (this.#currentShape[row][col] === 1) {
if (isAbsolute) {
coordinates.push({
x: this.position.x + col,
y: this.position.y + row,
});
} else coordinates.push({x: col, y: row});
} }
return coordinates; }
} }
return coordinates;
}
/** /**
* Rotate this object * Rotate this object
* *
* @param isForward Should we rotate clockwise? * @param isForward Should we rotate clockwise?
*/ */
rotate(isForward: boolean) { rotate(isForward: boolean) {
if (isForward) if (isForward) this.#rotation += 1;
this.#rotation++; else this.#rotation -= 1;
else if (this.#rotation > 3) this.#rotation = 0;
this.#rotation--; else if (this.#rotation < 0) this.#rotation = 3;
if (this.#rotation > 3) this.#currentShape = this.getShapes()[this.#rotation];
this.#rotation = 0; }
else if (this.#rotation < 0)
this.#rotation = 3;
this.#currentShape = this.getShapes()[this.#rotation];
}
/**
* Move this object
*
* @param x Position X offset to add
* @param y Position Y offset to add
*/
move(x: number, y: number) {
this.position.x += x;
this.position.y += y;
}
/**
* Move this object
*
* @param x Position X offset to add
* @param y Position Y offset to add
*/
move(x: number, y: number) {
this.position.x += x;
this.position.y += y;
}
} }

View file

@ -1,45 +1,46 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeI extends BaseShape { export default class ShapeI extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisI;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisI; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 0, 0, 0],
return [ [1, 1, 1, 1],
[ [0, 0, 0, 0],
[0, 0, 0, 0], [0, 0, 0, 0],
[1, 1, 1, 1], ],
[0, 0, 0, 0], [
[0, 0, 0, 0], [0, 0, 1, 0],
], [0, 0, 1, 0],
[ [0, 0, 1, 0],
[0, 0, 1, 0], [0, 0, 1, 0],
[0, 0, 1, 0], ],
[0, 0, 1, 0], [
[0, 0, 1, 0], [0, 0, 0, 0],
], [0, 0, 0, 0],
[ [1, 1, 1, 1],
[0, 0, 0, 0], [0, 0, 0, 0],
[0, 0, 0, 0], ],
[1, 1, 1, 1], [
[0, 0, 0, 0], [0, 1, 0, 0],
], [0, 1, 0, 0],
[ [0, 1, 0, 0],
[0, 1, 0, 0], [0, 1, 0, 0],
[0, 1, 0, 0], ],
[0, 1, 0, 0], ];
[0, 1, 0, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeJ extends BaseShape { export default class ShapeJ extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisJ;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisJ; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [1, 0, 0],
return [ [1, 1, 1],
[ [0, 0, 0],
[1, 0, 0], ],
[1, 1, 1], [
[0, 0, 0], [0, 1, 1],
], [0, 1, 0],
[ [0, 1, 0],
[0, 1, 1], ],
[0, 1, 0], [
[0, 1, 0], [0, 0, 0],
], [1, 1, 1],
[ [0, 0, 1],
[0, 0, 0], ],
[1, 1, 1], [
[0, 0, 1], [0, 1, 0],
], [0, 1, 0],
[ [1, 1, 0],
[0, 1, 0], ],
[0, 1, 0], ];
[1, 1, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeL extends BaseShape { export default class ShapeL extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisL;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisL; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 0, 1],
return [ [1, 1, 1],
[ [0, 0, 0],
[0, 0, 1], ],
[1, 1, 1], [
[0, 0, 0], [0, 1, 0],
], [0, 1, 0],
[ [0, 1, 1],
[0, 1, 0], ],
[0, 1, 0], [
[0, 1, 1], [0, 0, 0],
], [1, 1, 1],
[ [1, 0, 0],
[0, 0, 0], ],
[1, 1, 1], [
[1, 0, 0], [1, 1, 0],
], [0, 1, 0],
[ [0, 1, 0],
[1, 1, 0], ],
[0, 1, 0], ];
[0, 1, 0], }
],
];
}
} }

View file

@ -1,37 +1,38 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeO extends BaseShape { export default class ShapeO extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 4;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisO;
this.position.x = 4; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisO; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [1, 1],
return [ [1, 1],
[ ],
[1, 1], [
[1, 1], [1, 1],
], [1, 1],
[ ],
[1, 1], [
[1, 1], [1, 1],
], [1, 1],
[ ],
[1, 1], [
[1, 1], [1, 1],
], [1, 1],
[ ],
[1, 1], ];
[1, 1], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeS extends BaseShape { export default class ShapeS extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisS;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisS; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 1, 1],
return [ [1, 1, 0],
[ [0, 0, 0],
[0, 1, 1], ],
[1, 1, 0], [
[0, 0, 0], [0, 1, 0],
], [0, 1, 1],
[ [0, 0, 1],
[0, 1, 0], ],
[0, 1, 1], [
[0, 0, 1], [0, 0, 0],
], [0, 1, 1],
[ [1, 1, 0],
[0, 0, 0], ],
[0, 1, 1], [
[1, 1, 0], [1, 0, 0],
], [1, 1, 0],
[ [0, 1, 0],
[1, 0, 0], ],
[1, 1, 0], ];
[0, 1, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeT extends BaseShape { export default class ShapeT extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisT;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisT; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 1, 0],
return [ [1, 1, 1],
[ [0, 0, 0],
[0, 1, 0], ],
[1, 1, 1], [
[0, 0, 0], [0, 1, 0],
], [0, 1, 1],
[ [0, 1, 0],
[0, 1, 0], ],
[0, 1, 1], [
[0, 1, 0], [0, 0, 0],
], [1, 1, 1],
[ [0, 1, 0],
[0, 0, 0], ],
[1, 1, 1], [
[0, 1, 0], [0, 1, 0],
], [1, 1, 0],
[ [0, 1, 0],
[0, 1, 0], ],
[1, 1, 0], ];
[0, 1, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeZ extends BaseShape { export default class ShapeZ extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisZ;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisZ; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [1, 1, 0],
return [ [0, 1, 1],
[ [0, 0, 0],
[1, 1, 0], ],
[0, 1, 1], [
[0, 0, 0], [0, 0, 1],
], [0, 1, 1],
[ [0, 1, 0],
[0, 0, 1], ],
[0, 1, 1], [
[0, 1, 0], [0, 0, 0],
], [1, 1, 0],
[ [0, 1, 1],
[0, 0, 0], ],
[1, 1, 0], [
[0, 1, 1], [0, 1, 0],
], [1, 1, 0],
[ [1, 0, 0],
[0, 1, 0], ],
[1, 1, 0], ];
[1, 0, 0], }
],
];
}
} }

View file

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

View file

@ -3,56 +3,55 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import type {Cell} from "./CellComponent"; import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import CellComponent from "./CellComponent"; import type {CellType} from './CellComponent';
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet"; import CellComponent from './CellComponent';
export type Grid = Array<Array<CellComponent>>; export type GridType = Array<Array<CellComponent>>;
type Props = { type PropsType = {
grid: Array<Array<Object>>, grid: Array<Array<CellType>>,
height: number, height: number,
width: number, width: number,
style: ViewStyle, style: ViewStyle,
} };
class GridComponent extends React.Component<Props> { class GridComponent extends React.Component<PropsType> {
getRow(rowNumber: number): React.Node {
const {grid} = this.props;
return (
<View style={{flexDirection: 'row'}} key={rowNumber.toString()}>
{grid[rowNumber].map(this.getCellRender)}
</View>
);
}
getRow(rowNumber: number) { getCellRender = (item: CellType): React.Node => {
let cells = this.props.grid[rowNumber].map(this.getCellRender); return <CellComponent cell={item} key={item.key} />;
return ( };
<View
style={{flexDirection: 'row',}} getGrid(): React.Node {
key={rowNumber.toString()} const {height} = this.props;
> const rows = [];
{cells} for (let i = 0; i < height; i += 1) {
</View> rows.push(this.getRow(i));
);
} }
return rows;
}
getCellRender = (item: Cell) => { render(): React.Node {
return <CellComponent cell={item} key={item.key}/>; const {style, width, height} = this.props;
}; return (
<View
getGrid() { style={{
let rows = []; aspectRatio: width / height,
for (let i = 0; i < this.props.height; i++) { borderRadius: 4,
rows.push(this.getRow(i)); ...style,
} }}>
return rows; {this.getGrid()}
} </View>
);
render() { }
return (
<View style={{
aspectRatio: this.props.width / this.props.height,
borderRadius: 4,
...this.props.style
}}>
{this.getGrid()}
</View>
);
}
} }
export default withTheme(GridComponent); export default withTheme(GridComponent);

View file

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

View file

@ -1,243 +1,318 @@
// @flow // @flow
import Piece from "./Piece"; import Piece from './Piece';
import ScoreManager from "./ScoreManager"; import ScoreManager from './ScoreManager';
import GridManager from "./GridManager"; import GridManager from './GridManager';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {GridType} from '../components/GridComponent';
export type TickCallbackType = (
score: number,
level: number,
grid: GridType,
) => void;
export type ClockCallbackType = (time: number) => void;
export type EndCallbackType = (
time: number,
score: number,
isRestart: boolean,
) => void;
export type MovementCallbackType = (grid: GridType, score?: number) => void;
export default class GameLogic { export default class GameLogic {
static levelTicks = [1000, 800, 600, 400, 300, 200, 150, 100];
static levelTicks = [ scoreManager: ScoreManager;
1000,
800,
600,
400,
300,
200,
150,
100,
];
#scoreManager: ScoreManager; gridManager: GridManager;
#gridManager: GridManager;
#height: number; height: number;
#width: number;
#gameRunning: boolean; width: number;
#gamePaused: boolean;
#gameTime: number;
#currentObject: Piece; gameRunning: boolean;
#gameTick: number; gamePaused: boolean;
#gameTickInterval: IntervalID;
#gameTimeInterval: IntervalID;
#pressInInterval: TimeoutID; gameTime: number;
#isPressedIn: boolean;
#autoRepeatActivationDelay: number;
#autoRepeatDelay: number;
#nextPieces: Array<Piece>; currentObject: Piece;
#nextPiecesCount: number;
#onTick: Function; gameTick: number;
#onClock: Function;
endCallback: Function;
#theme: CustomTheme; gameTickInterval: IntervalID;
constructor(height: number, width: number, theme: CustomTheme) { gameTimeInterval: IntervalID;
this.#height = height;
this.#width = width;
this.#gameRunning = false;
this.#gamePaused = false;
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.#theme);
}
getHeight(): number { pressInInterval: TimeoutID;
return this.#height;
}
getWidth(): number { isPressedIn: boolean;
return this.#width;
}
getCurrentGrid() { autoRepeatActivationDelay: number;
return this.#gridManager.getCurrentGrid();
}
isGameRunning(): boolean { autoRepeatDelay: number;
return this.#gameRunning;
}
isGamePaused(): boolean { nextPieces: Array<Piece>;
return this.#gamePaused;
}
onFreeze() { nextPiecesCount: number;
this.#gridManager.freezeTetromino(this.#currentObject, this.#scoreManager);
this.createTetromino();
}
setNewGameTick(level: number) { tickCallback: TickCallbackType;
if (level >= GameLogic.levelTicks.length)
return;
this.#gameTick = GameLogic.levelTicks[level];
clearInterval(this.#gameTickInterval);
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
}
onTick(callback: Function) { clockCallback: ClockCallbackType;
this.#currentObject.tryMove(0, 1,
this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(), endCallback: EndCallbackType;
() => this.onFreeze());
theme: CustomThemeType;
constructor(height: number, width: number, theme: CustomThemeType) {
this.height = height;
this.width = width;
this.gameRunning = false;
this.gamePaused = false;
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.theme,
);
}
getHeight(): number {
return this.height;
}
getWidth(): number {
return this.width;
}
getCurrentGrid(): GridType {
return this.gridManager.getCurrentGrid();
}
isGamePaused(): boolean {
return this.gamePaused;
}
onFreeze = () => {
this.gridManager.freezeTetromino(this.currentObject, this.scoreManager);
this.createTetromino();
};
setNewGameTick(level: number) {
if (level >= GameLogic.levelTicks.length) return;
this.gameTick = GameLogic.levelTicks[level];
this.stopTick();
this.startTick();
}
startClock() {
this.gameTimeInterval = setInterval(() => {
this.onClock(this.clockCallback);
}, 1000);
}
startTick() {
this.gameTickInterval = setInterval(() => {
this.onTick(this.tickCallback);
}, this.gameTick);
}
stopClock() {
clearInterval(this.gameTimeInterval);
}
stopTick() {
clearInterval(this.gameTickInterval);
}
stopGameTime() {
this.stopClock();
this.stopTick();
}
startGameTime() {
this.startClock();
this.startTick();
}
onTick(callback: TickCallbackType) {
this.currentObject.tryMove(
0,
1,
this.gridManager.getCurrentGrid(),
this.getWidth(),
this.getHeight(),
this.onFreeze,
);
callback(
this.scoreManager.getScore(),
this.scoreManager.getLevel(),
this.gridManager.getCurrentGrid(),
);
if (this.scoreManager.canLevelUp())
this.setNewGameTick(this.scoreManager.getLevel());
}
onClock(callback: ClockCallbackType) {
this.gameTime += 1;
callback(this.gameTime);
}
canUseInput(): boolean {
return this.gameRunning && !this.gamePaused;
}
rightPressed(callback: MovementCallbackType) {
this.isPressedIn = true;
this.movePressedRepeat(true, callback, 1, 0);
}
leftPressedIn(callback: MovementCallbackType) {
this.isPressedIn = true;
this.movePressedRepeat(true, callback, -1, 0);
}
downPressedIn(callback: MovementCallbackType) {
this.isPressedIn = true;
this.movePressedRepeat(true, callback, 0, 1);
}
movePressedRepeat(
isInitial: boolean,
callback: MovementCallbackType,
x: number,
y: number,
) {
if (!this.canUseInput() || !this.isPressedIn) return;
const moved = this.currentObject.tryMove(
x,
y,
this.gridManager.getCurrentGrid(),
this.getWidth(),
this.getHeight(),
this.onFreeze,
);
if (moved) {
if (y === 1) {
this.scoreManager.incrementScore();
callback( callback(
this.#scoreManager.getScore(), this.gridManager.getCurrentGrid(),
this.#scoreManager.getLevel(), this.scoreManager.getScore(),
this.#gridManager.getCurrentGrid());
if (this.#scoreManager.canLevelUp())
this.setNewGameTick(this.#scoreManager.getLevel());
}
onClock(callback: Function) {
this.#gameTime++;
callback(this.#gameTime);
}
canUseInput() {
return this.#gameRunning && !this.#gamePaused
}
rightPressed(callback: Function) {
this.#isPressedIn = true;
this.movePressedRepeat(true, callback, 1, 0);
}
leftPressedIn(callback: Function) {
this.#isPressedIn = true;
this.movePressedRepeat(true, callback, -1, 0);
}
downPressedIn(callback: Function) {
this.#isPressedIn = true;
this.movePressedRepeat(true, callback, 0, 1);
}
movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) {
if (!this.canUseInput() || !this.#isPressedIn)
return;
const moved = this.#currentObject.tryMove(x, y,
this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
() => this.onFreeze());
if (moved) {
if (y === 1) {
this.#scoreManager.incrementScore();
callback(this.#gridManager.getCurrentGrid(), this.#scoreManager.getScore());
} else
callback(this.#gridManager.getCurrentGrid());
}
this.#pressInInterval = setTimeout(() =>
this.movePressedRepeat(false, callback, x, y),
isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay
); );
} else callback(this.gridManager.getCurrentGrid());
} }
this.pressInInterval = setTimeout(
() => {
this.movePressedRepeat(false, callback, x, y);
},
isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay,
);
}
pressedOut() { pressedOut() {
this.#isPressedIn = false; this.isPressedIn = false;
clearTimeout(this.#pressInInterval); clearTimeout(this.pressInInterval);
}
rotatePressed(callback: MovementCallbackType) {
if (!this.canUseInput()) return;
if (
this.currentObject.tryRotate(
this.gridManager.getCurrentGrid(),
this.getWidth(),
this.getHeight(),
)
)
callback(this.gridManager.getCurrentGrid());
}
getNextPiecesPreviews(): Array<GridType> {
const finalArray = [];
for (let i = 0; i < this.nextPieces.length; i += 1) {
const gridSize = this.nextPieces[i].getCurrentShape().getCurrentShape()[0]
.length;
finalArray.push(this.gridManager.getEmptyGrid(gridSize, gridSize));
this.nextPieces[i].toGrid(finalArray[i], true);
} }
return finalArray;
}
rotatePressed(callback: Function) { recoverNextPiece() {
if (!this.canUseInput()) this.currentObject = this.nextPieces.shift();
return; this.generateNextPieces();
}
if (this.#currentObject.tryRotate(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) generateNextPieces() {
callback(this.#gridManager.getCurrentGrid()); while (this.nextPieces.length < this.nextPiecesCount) {
this.nextPieces.push(new Piece(this.theme));
} }
}
getNextPiecesPreviews() { createTetromino() {
let finalArray = []; this.pressedOut();
for (let i = 0; i < this.#nextPieces.length; i++) { this.recoverNextPiece();
const gridSize = this.#nextPieces[i].getCurrentShape().getCurrentShape()[0].length; if (
finalArray.push(this.#gridManager.getEmptyGrid(gridSize, gridSize)); !this.currentObject.isPositionValid(
this.#nextPieces[i].toGrid(finalArray[i], true); this.gridManager.getCurrentGrid(),
} this.getWidth(),
this.getHeight(),
)
)
this.endGame(false);
}
return finalArray; togglePause() {
} if (!this.gameRunning) return;
this.gamePaused = !this.gamePaused;
if (this.gamePaused) this.stopGameTime();
else this.startGameTime();
}
recoverNextPiece() { endGame(isRestart: boolean) {
this.#currentObject = this.#nextPieces.shift(); this.gameRunning = false;
this.generateNextPieces(); this.gamePaused = false;
} this.stopGameTime();
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart);
}
generateNextPieces() { startGame(
while (this.#nextPieces.length < this.#nextPiecesCount) { tickCallback: TickCallbackType,
this.#nextPieces.push(new Piece(this.#theme)); clockCallback: ClockCallbackType,
} endCallback: EndCallbackType,
} ) {
if (this.gameRunning) this.endGame(true);
createTetromino() { this.gameRunning = true;
this.pressedOut(); this.gamePaused = false;
this.recoverNextPiece(); this.gameTime = 0;
if (!this.#currentObject.isPositionValid(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight())) this.scoreManager = new ScoreManager();
this.endGame(false); this.gameTick = GameLogic.levelTicks[this.scoreManager.getLevel()];
} this.gridManager = new GridManager(
this.getWidth(),
togglePause() { this.getHeight(),
if (!this.#gameRunning) this.theme,
return; );
this.#gamePaused = !this.#gamePaused; this.nextPieces = [];
if (this.#gamePaused) { this.generateNextPieces();
clearInterval(this.#gameTickInterval); this.createTetromino();
clearInterval(this.#gameTimeInterval); tickCallback(
} else { this.scoreManager.getScore(),
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick); this.scoreManager.getLevel(),
this.#gameTimeInterval = setInterval(this.#onClock, 1000); this.gridManager.getCurrentGrid(),
} );
} clockCallback(this.gameTime);
this.startTick();
stopGame() { this.startClock();
this.#gameRunning = false; this.tickCallback = tickCallback;
this.#gamePaused = false; this.clockCallback = clockCallback;
clearInterval(this.#gameTickInterval); this.endCallback = endCallback;
clearInterval(this.#gameTimeInterval); }
}
endGame(isRestart: boolean) {
this.stopGame();
this.endCallback(this.#gameTime, this.#scoreManager.getScore(), isRestart);
}
startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) {
if (this.#gameRunning)
this.endGame(true);
this.#gameRunning = true;
this.#gamePaused = false;
this.#gameTime = 0;
this.#scoreManager = new ScoreManager();
this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()];
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
this.#nextPieces = [];
this.generateNextPieces();
this.createTetromino();
tickCallback(
this.#scoreManager.getScore(),
this.#scoreManager.getLevel(),
this.#gridManager.getCurrentGrid());
clockCallback(this.#gameTime);
this.#onTick = this.onTick.bind(this, tickCallback);
this.#onClock = this.onClock.bind(this, clockCallback);
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
this.#gameTimeInterval = setInterval(this.#onClock, 1000);
this.endCallback = endCallback;
}
} }

View file

@ -1,120 +1,122 @@
// @flow // @flow
import Piece from "./Piece"; import Piece from './Piece';
import ScoreManager from "./ScoreManager"; import ScoreManager from './ScoreManager';
import type {Coordinates} from '../Shapes/BaseShape'; import type {CoordinatesType} from '../Shapes/BaseShape';
import type {Grid} from "../components/GridComponent"; import type {GridType} from '../components/GridComponent';
import type {Cell} from "../components/CellComponent"; import type {CellType} from '../components/CellComponent';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
/** /**
* Class used to manage the game grid * Class used to manage the game grid
*/ */
export default class GridManager { export default class GridManager {
#currentGrid: GridType;
#currentGrid: Grid; #theme: CustomThemeType;
#theme: CustomTheme;
/** /**
* Initializes a grid of the given size * Initializes a grid of the given size
* *
* @param width The grid width * @param width The grid width
* @param height The grid height * @param height The grid height
* @param theme Object containing current theme * @param theme Object containing current theme
*/ */
constructor(width: number, height: number, theme: CustomTheme) { constructor(width: number, height: number, theme: CustomThemeType) {
this.#theme = theme; this.#theme = theme;
this.#currentGrid = this.getEmptyGrid(height, width); this.#currentGrid = this.getEmptyGrid(height, width);
}
/**
* Get the current grid
*
* @return {GridType} The current grid
*/
getCurrentGrid(): GridType {
return this.#currentGrid;
}
/**
* Get a new empty grid line of the given size
*
* @param width The line size
* @return {Array<CellType>}
*/
getEmptyLine(width: number): Array<CellType> {
const line = [];
for (let col = 0; col < width; col += 1) {
line.push({
color: this.#theme.colors.tetrisBackground,
isEmpty: true,
key: col.toString(),
});
} }
return line;
}
/** /**
* Get the current grid * Gets a new empty grid
* *
* @return {Grid} The current grid * @param width The grid width
*/ * @param height The grid height
getCurrentGrid(): Grid { * @return {GridType} A new empty grid
return this.#currentGrid; */
getEmptyGrid(height: number, width: number): GridType {
const grid = [];
for (let row = 0; row < height; row += 1) {
grid.push(this.getEmptyLine(width));
} }
return grid;
}
/** /**
* Get a new empty grid line of the given size * Removes the given lines from the grid,
* * shifts down every line on top and adds new empty lines on top.
* @param width The line size *
* @return {Array<Cell>} * @param lines An array of line numbers to remove
*/ * @param scoreManager A reference to the score manager
getEmptyLine(width: number): Array<Cell> { */
let line = []; clearLines(lines: Array<number>, scoreManager: ScoreManager) {
for (let col = 0; col < width; col++) { lines.sort();
line.push({ for (let i = 0; i < lines.length; i += 1) {
color: this.#theme.colors.tetrisBackground, this.#currentGrid.splice(lines[i], 1);
isEmpty: true, this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length));
key: col.toString(), }
}); scoreManager.addLinesRemovedPoints(lines.length);
}
/**
* 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 pos The piece's coordinates to check lines at
* @return {Array<number>} An array containing the line numbers to clear
*/
getLinesToClear(pos: Array<CoordinatesType>): Array<number> {
const rows = [];
for (let i = 0; i < pos.length; i += 1) {
let isLineFull = true;
for (let col = 0; col < this.#currentGrid[pos[i].y].length; col += 1) {
if (this.#currentGrid[pos[i].y][col].isEmpty) {
isLineFull = false;
break;
} }
return line; }
if (isLineFull && rows.indexOf(pos[i].y) === -1) rows.push(pos[i].y);
} }
return rows;
}
/** /**
* Gets a new empty grid * Freezes the given piece to the grid
* *
* @param width The grid width * @param currentObject The piece to freeze
* @param height The grid height * @param scoreManager A reference to the score manager
* @return {Grid} A new empty grid */
*/ freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) {
getEmptyGrid(height: number, width: number): Grid { this.clearLines(
let grid = []; this.getLinesToClear(currentObject.getCoordinates()),
for (let row = 0; row < height; row++) { scoreManager,
grid.push(this.getEmptyLine(width)); );
} }
return grid;
}
/**
* Removes the given lines from the grid,
* shifts down every line on top and adds new empty lines on top.
*
* @param lines An array of line numbers to remove
* @param scoreManager A reference to the score manager
*/
clearLines(lines: Array<number>, scoreManager: ScoreManager) {
lines.sort();
for (let i = 0; i < lines.length; i++) {
this.#currentGrid.splice(lines[i], 1);
this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length));
}
scoreManager.addLinesRemovedPoints(lines.length);
}
/**
* 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 pos The piece's coordinates to check lines at
* @return {Array<number>} An array containing the line numbers to clear
*/
getLinesToClear(pos: Array<Coordinates>): Array<number> {
let rows = [];
for (let i = 0; i < pos.length; i++) {
let isLineFull = true;
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(pos[i].y) === -1)
rows.push(pos[i].y);
}
return rows;
}
/**
* Freezes the given piece to the grid
*
* @param currentObject The piece to freeze
* @param scoreManager A reference to the score manager
*/
freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) {
this.clearLines(this.getLinesToClear(currentObject.getCoordinates()), scoreManager);
}
} }

View file

@ -4,98 +4,100 @@
* Class used to manage game score * Class used to manage game score
*/ */
export default class ScoreManager { export default class ScoreManager {
#scoreLinesModifier = [40, 100, 300, 1200];
#scoreLinesModifier = [40, 100, 300, 1200]; #score: number;
#score: number; #level: number;
#level: number;
#levelProgression: number;
/** #levelProgression: number;
* Initializes score to 0
*/ /**
constructor() { * Initializes score to 0
this.#score = 0; */
this.#level = 0; constructor() {
this.#levelProgression = 0; this.#score = 0;
this.#level = 0;
this.#levelProgression = 0;
}
/**
* Gets the current score
*
* @return {number} The current score
*/
getScore(): number {
return this.#score;
}
/**
* Gets the current level
*
* @return {number} The current level
*/
getLevel(): number {
return this.#level;
}
/**
* Gets the current level progression
*
* @return {number} The current level progression
*/
getLevelProgression(): number {
return this.#levelProgression;
}
/**
* Increments the score by one
*/
incrementScore() {
this.#score += 1;
}
/**
* Add score corresponding to the number of lines removed at the same time.
* Also updates the level progression.
*
* The more lines cleared at the same time, the more points and level progression the player gets.
*
* @param numberRemoved The number of lines removed at the same time
*/
addLinesRemovedPoints(numberRemoved: number) {
if (numberRemoved < 1 || numberRemoved > 4) return;
this.#score +=
this.#scoreLinesModifier[numberRemoved - 1] * (this.#level + 1);
switch (numberRemoved) {
case 1:
this.#levelProgression += 1;
break;
case 2:
this.#levelProgression += 3;
break;
case 3:
this.#levelProgression += 5;
break;
case 4: // Did a tetris !
this.#levelProgression += 8;
break;
default:
break;
} }
}
/** /**
* Gets the current score * Checks if the player can go to the next level.
* *
* @return {number} The current score * If he can, change the level.
*/ *
getScore(): number { * @return {boolean} True if the current level has changed
return this.#score; */
canLevelUp(): boolean {
const canLevel = this.#levelProgression > this.#level * 5;
if (canLevel) {
this.#levelProgression -= this.#level * 5;
this.#level += 1;
} }
return canLevel;
/** }
* Gets the current level
*
* @return {number} The current level
*/
getLevel(): number {
return this.#level;
}
/**
* Gets the current level progression
*
* @return {number} The current level progression
*/
getLevelProgression(): number {
return this.#levelProgression;
}
/**
* Increments the score by one
*/
incrementScore() {
this.#score++;
}
/**
* Add score corresponding to the number of lines removed at the same time.
* Also updates the level progression.
*
* The more lines cleared at the same time, the more points and level progression the player gets.
*
* @param numberRemoved The number of lines removed at the same time
*/
addLinesRemovedPoints(numberRemoved: number) {
if (numberRemoved < 1 || numberRemoved > 4)
return;
this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1);
switch (numberRemoved) {
case 1:
this.#levelProgression += 1;
break;
case 2:
this.#levelProgression += 3;
break;
case 3:
this.#levelProgression += 5;
break;
case 4: // Did a tetris !
this.#levelProgression += 8;
break;
}
}
/**
* Checks if the player can go to the next level.
*
* If he can, change the level.
*
* @return {boolean} True if the current level has changed
*/
canLevelUp() {
let canLevel = this.#levelProgression > this.#level * 5;
if (canLevel){
this.#levelProgression -= this.#level * 5;
this.#level++;
}
return canLevel;
}
} }

View file

@ -3,400 +3,447 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {Caption, IconButton, Text, withTheme} from 'react-native-paper'; import {Caption, IconButton, Text, withTheme} from 'react-native-paper';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import GameLogic from "../logic/GameLogic"; import i18n from 'i18n-js';
import type {Grid} from "../components/GridComponent"; import {StackNavigationProp} from '@react-navigation/stack';
import GridComponent from "../components/GridComponent"; import GameLogic from '../logic/GameLogic';
import Preview from "../components/Preview"; import type {GridType} from '../components/GridComponent';
import i18n from "i18n-js"; import GridComponent from '../components/GridComponent';
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; import Preview from '../components/Preview';
import {StackNavigationProp} from "@react-navigation/stack"; import MaterialHeaderButtons, {
import type {CustomTheme} from "../../../managers/ThemeManager"; Item,
import type {OptionsDialogButton} from "../../../components/Dialogs/OptionsDialog"; } from '../../../components/Overrides/CustomHeaderButton';
import OptionsDialog from "../../../components/Dialogs/OptionsDialog"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog';
import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: { highScore: number }, ... }, route: {params: {highScore: number}},
theme: CustomTheme, theme: CustomThemeType,
} };
type State = { type StateType = {
grid: Grid, grid: GridType,
gameRunning: boolean, gameTime: number,
gameTime: number, gameScore: number,
gameScore: number, gameLevel: number,
gameLevel: number,
dialogVisible: boolean, dialogVisible: boolean,
dialogTitle: string, dialogTitle: string,
dialogMessage: string, dialogMessage: string,
dialogButtons: Array<OptionsDialogButton>, dialogButtons: Array<OptionsDialogButtonType>,
onDialogDismiss: () => void, onDialogDismiss: () => void,
} };
class GameMainScreen extends React.Component<Props, State> { class GameMainScreen extends React.Component<PropsType, StateType> {
static getFormattedTime(seconds: number): string {
const date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds);
let format;
if (date.getHours())
format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
else if (date.getMinutes())
format = `${date.getMinutes()}:${date.getSeconds()}`;
else format = date.getSeconds().toString();
return format;
}
logic: GameLogic; logic: GameLogic;
highScore: number | null;
constructor(props) { highScore: number | null;
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() { constructor(props: PropsType) {
this.props.navigation.setOptions({ super(props);
headerRight: this.getRightButton, this.logic = new GameLogic(20, 10, props.theme);
}); this.state = {
this.startGame(); grid: this.logic.getCurrentGrid(),
} gameTime: 0,
gameScore: 0,
gameLevel: 0,
dialogVisible: false,
dialogTitle: '',
dialogMessage: '',
dialogButtons: [],
onDialogDismiss: () => {},
};
if (props.route.params != null)
this.highScore = props.route.params.highScore;
}
componentWillUnmount() { componentDidMount() {
this.logic.stopGame(); const {navigation} = this.props;
} navigation.setOptions({
headerRight: this.getRightButton,
});
this.startGame();
}
getRightButton = () => { componentWillUnmount() {
return <MaterialHeaderButtons> this.logic.endGame(false);
<Item title="pause" iconName="pause" onPress={this.togglePause}/> }
</MaterialHeaderButtons>;
}
getFormattedTime(seconds: number) { getRightButton = (): React.Node => {
let date = new Date(); return (
date.setHours(0); <MaterialHeaderButtons>
date.setMinutes(0); <Item title="pause" iconName="pause" onPress={this.togglePause} />
date.setSeconds(seconds); </MaterialHeaderButtons>
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) => { onTick = (score: number, level: number, newGrid: GridType) => {
this.setState({ this.setState({
gameScore: score, gameScore: score,
gameLevel: level, gameLevel: level,
grid: newGrid, grid: newGrid,
}); });
} };
onClock = (time: number) => { onClock = (time: number) => {
this.setState({ this.setState({
gameTime: time, gameTime: time,
}); });
} };
updateGrid = (newGrid: Grid) => { onDialogDismiss = () => {
this.setState({ this.setState({dialogVisible: false});
grid: newGrid, };
});
}
updateGridScore = (newGrid: Grid, score: number) => { onGameEnd = (time: number, score: number, isRestart: boolean) => {
this.setState({ const {props, state} = this;
grid: newGrid, this.setState({
gameScore: score, gameTime: time,
}); gameScore: score,
} });
if (!isRestart)
props.navigation.replace('game-start', {
score: state.gameScore,
level: state.gameLevel,
time: state.gameTime,
});
};
togglePause = () => { getStatusIcons(): React.Node {
this.logic.togglePause(); const {props, state} = this;
if (this.logic.isGamePaused()) return (
this.showPausePopup(); <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={props.theme.colors.subtitle}
size={20}
/>
<Text
style={{
marginLeft: 5,
color: props.theme.colors.subtitle,
}}>
{GameMainScreen.getFormattedTime(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={props.theme.colors.text}
size={20}
/>
<Text
style={{
marginLeft: 5,
}}>
{state.gameLevel}
</Text>
</View>
</View>
</View>
);
}
onDialogDismiss = () => this.setState({dialogVisible: false}); getScoreIcon(): React.Node {
const {props, state} = this;
const highScore =
this.highScore == null || state.gameScore > this.highScore
? 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: state.gameScore})}
</Text>
<MaterialCommunityIcons
name="star"
color={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: props.theme.colors.textDisabled,
}}>
{i18n.t('screens.game.highScore', {score: highScore})}
</Text>
<MaterialCommunityIcons
name="star"
color={props.theme.colors.tetrisScore}
size={10}
style={{
marginTop: 'auto',
marginBottom: 'auto',
marginLeft: 5,
}}
/>
</View>
</View>
);
}
showPausePopup = () => { getControlButtons(): React.Node {
const onDismiss = () => { const {props} = this;
this.togglePause(); 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={props.theme.colors.tetrisScore}
/>
</View>
);
}
updateGrid = (newGrid: GridType) => {
this.setState({
grid: newGrid,
});
};
updateGridScore = (newGrid: GridType, score?: number) => {
this.setState((prevState: StateType): {
grid: GridType,
gameScore: number,
} => ({
grid: newGrid,
gameScore: score != null ? score : prevState.gameScore,
}));
};
togglePause = () => {
this.logic.togglePause();
if (this.logic.isGamePaused()) this.showPausePopup();
};
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.onDialogDismiss();
}; this.startGame();
this.setState({ },
dialogVisible: true, },
dialogTitle: i18n.t("screens.game.pause"), {
dialogMessage: i18n.t("screens.game.pauseMessage"), title: i18n.t('screens.game.restart.confirmNo'),
dialogButtons: [ onPress: this.showPausePopup,
{ },
title: i18n.t("screens.game.restart.text"), ],
onPress: this.showRestartConfirm onDialogDismiss: this.showPausePopup,
}, });
{ };
title: i18n.t("screens.game.resume"),
onPress: onDismiss
}
],
onDialogDismiss: onDismiss,
});
}
showRestartConfirm = () => { startGame = () => {
this.setState({ this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
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,
});
}
startGame = () => { render(): React.Node {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd); const {props, state} = this;
this.setState({ return (
gameRunning: true, <View style={{flex: 1}}>
}); <View
} style={{
flex: 1,
onGameEnd = (time: number, score: number, isRestart: boolean) => { flexDirection: 'row',
this.setState({ }}>
gameTime: time, {this.getStatusIcons()}
gameScore: score, <View style={{flex: 4}}>
gameRunning: false, {this.getScoreIcon()}
}); <GridComponent
if (!isRestart) width={this.logic.getWidth()}
this.props.navigation.replace( height={this.logic.getHeight()}
"game-start", grid={state.grid}
{ style={{
score: this.state.gameScore, backgroundColor: props.theme.colors.tetrisBackground,
level: this.state.gameLevel,
time: this.state.gameTime,
}
);
}
getStatusIcons() {
return (
<View style={{
flex: 1, flex: 1,
marginTop: "auto", marginLeft: 'auto',
marginBottom: "auto" marginRight: 'auto',
}}> }}
<View style={{ />
marginLeft: 'auto', </View>
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={{flex: 1}}>
<View style={{ <Preview
marginLeft: 'auto', items={this.logic.getNextPiecesPreviews()}
marginRight: 'auto', style={{
marginTop: 20, marginLeft: 'auto',
}}> marginRight: 'auto',
<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, marginTop: 10,
marginBottom: 10, }}
}}> />
<View style={{ </View>
flexDirection: "row", </View>
marginLeft: "auto", {this.getControlButtons()}
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>
);
}
<OptionsDialog
visible={state.dialogVisible}
title={state.dialogTitle}
message={state.dialogMessage}
buttons={state.dialogButtons}
onDismiss={state.onDialogDismiss}
/>
</View>
);
}
} }
export default withTheme(GameMainScreen); export default withTheme(GameMainScreen);

View file

@ -1,427 +1,440 @@
// @flow // @flow
import * as React from "react"; import * as React from 'react';
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../../managers/ThemeManager"; import {
import {Button, Card, Divider, Headline, Paragraph, Text, withTheme} from "react-native-paper"; Button,
import {View} from "react-native"; Card,
import i18n from "i18n-js"; Divider,
import Mascot, {MASCOT_STYLE} from "../../../components/Mascot/Mascot"; Headline,
import MascotPopup from "../../../components/Mascot/MascotPopup"; Paragraph,
import AsyncStorageManager from "../../../managers/AsyncStorageManager"; Text,
import type {Grid} from "../components/GridComponent"; withTheme,
import GridComponent from "../components/GridComponent"; } from 'react-native-paper';
import GridManager from "../logic/GridManager"; import {View} from 'react-native';
import Piece from "../logic/Piece"; import i18n from 'i18n-js';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import LinearGradient from "react-native-linear-gradient"; import LinearGradient from 'react-native-linear-gradient';
import SpeechArrow from "../../../components/Mascot/SpeechArrow"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
import MascotPopup from '../../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import type {GridType} from '../components/GridComponent';
import GridComponent from '../components/GridComponent';
import GridManager from '../logic/GridManager';
import Piece from '../logic/Piece';
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type GameStats = { type GameStatsType = {
score: number, score: number,
level: number, level: number,
time: number, time: number,
} };
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params: GameStats params: GameStatsType,
}, },
theme: CustomTheme, theme: CustomThemeType,
} };
class GameStartScreen extends React.Component<Props> { class GameStartScreen extends React.Component<PropsType> {
gridManager: GridManager;
gridManager: GridManager; scores: Array<number>;
scores: Array<number>;
gameStats: GameStats | null; gameStats: GameStatsType | null;
isHighScore: boolean;
constructor(props: Props) { isHighScore: boolean;
super(props);
this.gridManager = new GridManager(4, 4, props.theme); constructor(props: PropsType) {
this.scores = AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.gameScores.key); super(props);
this.scores.sort((a, b) => b - a); this.gridManager = new GridManager(4, 4, props.theme);
if (this.props.route.params != null) this.scores = AsyncStorageManager.getObject(
this.recoverGameScore(); AsyncStorageManager.PREFERENCES.gameScores.key,
);
this.scores.sort((a: number, b: number): number => b - a);
if (props.route.params != null) this.recoverGameScore();
}
getPiecesBackground(): React.Node {
const {theme} = this.props;
const gridList = [];
for (let i = 0; i < 18; i += 1) {
gridList.push(this.gridManager.getEmptyGrid(4, 4));
const piece = new Piece(theme);
piece.toGrid(gridList[i], true);
} }
return (
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
}}>
{gridList.map((item: GridType, index: number): React.Node => {
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={`piece${index.toString()}`}
style={{
width: `${size}%`,
position: 'absolute',
top: `${top}%`,
left: `${left}%`,
}}>
<GridComponent
width={4}
height={4}
grid={item}
style={{
transform: [{rotateZ: `${rot}deg`}],
}}
/>
</Animatable.View>
);
})}
</View>
);
}
recoverGameScore() { getPostGameContent(stats: GameStatsType): React.Node {
this.gameStats = this.props.route.params; const {props} = this;
this.isHighScore = this.scores.length === 0 || this.gameStats.score > this.scores[0]; return (
for (let i = 0; i < 3; i++) { <View
if (this.scores.length > i && this.gameStats.score > this.scores[i]) { style={{
this.scores.splice(i, 0, this.gameStats.score); flex: 1,
break; }}>
} else if (this.scores.length <= i) { <Mascot
this.scores.push(this.gameStats.score); emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
break; animated={this.isHighScore}
} style={{
} width: this.isHighScore ? '50%' : '30%',
if (this.scores.length > 3) marginLeft: this.isHighScore ? 'auto' : null,
this.scores.splice(3, 1); marginRight: this.isHighScore ? 'auto' : null,
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.gameScores.key, this.scores); }}
} />
<SpeechArrow
getPiecesBackground() { style={{marginLeft: this.isHighScore ? '60%' : '20%'}}
let gridList = []; size={20}
for (let i = 0; i < 18; i++) { color={props.theme.colors.mascotMessageArrow}
gridList.push(this.gridManager.getEmptyGrid(4, 4)); />
const piece = new Piece(this.props.theme); <Card
piece.toGrid(gridList[i], true); style={{
} borderColor: props.theme.colors.mascotMessageArrow,
return ( borderWidth: 2,
<View style={{ marginLeft: 20,
position: "absolute", marginRight: 20,
width: "100%", }}>
height: "100%", <Card.Content>
}}> <Headline
{gridList.map((item: Grid, index: number) => { style={{
const size = 10 + Math.floor(Math.random() * 30); textAlign: 'center',
const top = Math.floor(Math.random() * 100); color: this.isHighScore
const rot = Math.floor(Math.random() * 360); ? props.theme.colors.gameGold
const left = (index % 6) * 20; : props.theme.colors.primary,
const animDelay = size * 20; }}>
const animDuration = 2 * (2000 - (size * 30)); {this.isHighScore
return ( ? i18n.t('screens.game.newHighScore')
<Animatable.View : i18n.t('screens.game.gameOver')}
animation={"fadeInDownBig"} </Headline>
delay={animDelay} <Divider />
duration={animDuration} <View
key={"piece" + index.toString()} style={{
style={{ flexDirection: 'row',
width: size + "%", marginLeft: 'auto',
position: "absolute", marginRight: 'auto',
top: top + "%", marginTop: 10,
left: left + "%", marginBottom: 10,
}} }}>
> <Text
<GridComponent style={{
width={4} fontSize: 20,
height={4}
grid={item}
style={{
transform: [{rotateZ: rot + "deg"}],
}}
/>
</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> {i18n.t('screens.game.score', {score: stats.score})}
<Headline </Text>
style={{ <MaterialCommunityIcons
textAlign: "center", name="star"
color: this.isHighScore color={props.theme.colors.tetrisScore}
? this.props.theme.colors.gameGold size={30}
: this.props.theme.colors.primary style={{
}}> marginLeft: 5,
{this.isHighScore }}
? i18n.t("screens.game.newHighScore") />
: i18n.t("screens.game.gameOver")}
</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> </View>
) <View
} style={{
flexDirection: 'row',
getWelcomeText() { marginLeft: 'auto',
return ( marginRight: 'auto',
<View> }}>
<Mascot emotion={MASCOT_STYLE.COOL} style={{ <Text>{i18n.t('screens.game.level')}</Text>
width: "40%", <MaterialCommunityIcons
marginLeft: "auto", style={{
marginRight: "auto", marginRight: 5,
}}/> marginLeft: 5,
<SpeechArrow }}
style={{marginLeft: "60%"}} name="gamepad-square"
size={20} size={20}
color={this.props.theme.colors.mascotMessageArrow} color={props.theme.colors.textDisabled}
/> />
<Card style={{ <Text>{stats.level}</Text>
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> </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={props.theme.colors.textDisabled}
/>
<Text>{stats.time}</Text>
</View>
</Card.Content>
</Card>
</View>
);
}
getPodiumRender(place: 1 | 2 | 3, score: string) { getWelcomeText(): React.Node {
let icon = "podium-gold"; const {props} = this;
let color = this.props.theme.colors.gameGold; return (
let fontSize = 20; <View>
let size = 70; <Mascot
if (place === 2) { emotion={MASCOT_STYLE.COOL}
icon = "podium-silver"; style={{
color = this.props.theme.colors.gameSilver; width: '40%',
fontSize = 18; marginLeft: 'auto',
size = 60; marginRight: 'auto',
} else if (place === 3) { }}
icon = "podium-bronze"; />
color = this.props.theme.colors.gameBronze; <SpeechArrow
fontSize = 15; style={{marginLeft: '60%'}}
size = 50; size={20}
} color={props.theme.colors.mascotMessageArrow}
return ( />
<View style={{ <Card
marginLeft: place === 2 ? 20 : "auto", style={{
marginRight: place === 3 ? 20 : "auto", borderColor: props.theme.colors.mascotMessageArrow,
flexDirection: "column", borderWidth: 2,
alignItems: "center", marginLeft: 10,
justifyContent: "flex-end", marginRight: 10,
}}>
<Card.Content>
<Headline
style={{
textAlign: 'center',
color: 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): React.Node {
const {props} = this;
let icon = 'podium-gold';
let color = props.theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = 'podium-silver';
color = props.theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = 'podium-bronze';
color = props.theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
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
style={{
position: 'absolute',
top: -20,
}}> }}>
{ <Animatable.View
this.isHighScore && place === 1 animation="pulse"
? iterationCount="infinite"
<Animatable.View useNativeDriver>
animation={"swing"} <MaterialCommunityIcons
iterationCount={"infinite"} name="decagram"
duration={2000} color={props.theme.colors.gameGold}
delay={1000} size={150}
useNativeDriver={true} />
style={{ </Animatable.View>
position: "absolute", </Animatable.View>
top: -20 ) : null}
}} <MaterialCommunityIcons
> name={icon}
<Animatable.View color={this.isHighScore && place === 1 ? '#fff' : color}
animation={"pulse"} size={size}
iterationCount={"infinite"} />
useNativeDriver={true} <Text
> style={{
<MaterialCommunityIcons textAlign: 'center',
name={"decagram"} fontWeight: place === 1 ? 'bold' : null,
color={this.props.theme.colors.gameGold} fontSize,
size={150} }}>
/> {score}
</Animatable.View> </Text>
</Animatable.View> </View>
);
}
: null getTopScoresRender(): React.Node {
} const gold = this.scores.length > 0 ? this.scores[0] : '-';
<MaterialCommunityIcons const silver = this.scores.length > 1 ? this.scores[1] : '-';
name={icon} const bronze = this.scores.length > 2 ? this.scores[2] : '-';
color={this.isHighScore && place === 1 ? "#fff" : color} return (
size={size} <View
/> style={{
<Text style={{ marginBottom: 20,
textAlign: "center", marginTop: 20,
fontWeight: place === 1 ? "bold" : null, }}>
fontSize: fontSize, {this.getPodiumRender(1, gold.toString())}
}}>{score}</Text> <View
</View> style={{
); flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
}}>
{this.getPodiumRender(3, bronze.toString())}
{this.getPodiumRender(2, silver.toString())}
</View>
</View>
);
}
getMainContent(): React.Node {
const {props} = this;
return (
<View style={{flex: 1}}>
{this.gameStats != null
? this.getPostGameContent(this.gameStats)
: this.getWelcomeText()}
<Button
icon="play"
mode="contained"
onPress={() => {
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>
);
}
keyExtractor = (item: number): string => item.toString();
recoverGameScore() {
const {route} = this.props;
this.gameStats = route.params;
this.isHighScore =
this.scores.length === 0 || this.gameStats.score > this.scores[0];
for (let i = 0; i < 3; i += 1) {
if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
this.scores.splice(i, 0, this.gameStats.score);
break;
} else if (this.scores.length <= i) {
this.scores.push(this.gameStats.score);
break;
}
} }
if (this.scores.length > 3) this.scores.splice(3, 1);
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.gameScores.key,
this.scores,
);
}
getTopScoresRender() { render(): React.Node {
const gold = this.scores.length > 0 const {props} = this;
? this.scores[0] return (
: "-"; <View style={{flex: 1}}>
const silver = this.scores.length > 1 {this.getPiecesBackground()}
? this.scores[1] <LinearGradient
: "-"; style={{flex: 1}}
const bronze = this.scores.length > 2 colors={[
? this.scores[2] `${props.theme.colors.background}00`,
: "-"; props.theme.colors.background,
return ( ]}
<View style={{ start={{x: 0, y: 0}}
marginBottom: 20, end={{x: 0, y: 1}}>
marginTop: 20 <CollapsibleScrollView>
}}> {this.getMainContent()}
{this.getPodiumRender(1, gold.toString())} <MascotPopup
<View style={{ prefKey={AsyncStorageManager.PREFERENCES.gameStartShowBanner.key}
flexDirection: "row", title={i18n.t('screens.game.mascotDialog.title')}
marginLeft: "auto", message={i18n.t('screens.game.mascotDialog.message')}
marginRight: "auto", icon="gamepad-variant"
}}> buttons={{
{this.getPodiumRender(3, bronze.toString())} action: null,
{this.getPodiumRender(2, silver.toString())} cancel: {
</View> message: i18n.t('screens.game.mascotDialog.button'),
</View> icon: 'check',
); },
} }}
emotion={MASCOT_STYLE.COOL}
getMainContent() { />
return ( </CollapsibleScrollView>
<View style={{flex: 1}}> </LinearGradient>
{ </View>
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>
)
}
keyExtractor = (item: number) => item.toString();
render() {
return (
<View style={{flex: 1}}>
{this.getPiecesBackground()}
<LinearGradient
style={{flex: 1}}
colors={[
this.props.theme.colors.background + "00",
this.props.theme.colors.background
]}
start={{x: 0, y: 0}}
end={{x: 0, y: 1}}
>
<CollapsibleScrollView>
{this.getMainContent()}
<MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.gameStartShowBanner.key}
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",
}
}}
emotion={MASCOT_STYLE.COOL}
/>
</CollapsibleScrollView>
</LinearGradient>
</View>
);
}
} }
export default withTheme(GameStartScreen); export default withTheme(GameStartScreen);