472 lines
12 KiB
TypeScript
472 lines
12 KiB
TypeScript
/*
|
|
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
|
*
|
|
* This file is part of Campus INSAT.
|
|
*
|
|
* Campus INSAT is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Campus INSAT is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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 i18n from 'i18n-js';
|
|
import {StackNavigationProp} from '@react-navigation/stack';
|
|
import GameLogic from '../logic/GameLogic';
|
|
import type {GridType} from '../components/GridComponent';
|
|
import GridComponent from '../components/GridComponent';
|
|
import Preview from '../components/Preview';
|
|
import MaterialHeaderButtons, {
|
|
Item,
|
|
} from '../../../components/Overrides/CustomHeaderButton';
|
|
import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog';
|
|
import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
|
|
|
|
type PropsType = {
|
|
navigation: StackNavigationProp<any>;
|
|
route: {params: {highScore: number}};
|
|
theme: ReactNativePaper.Theme;
|
|
};
|
|
|
|
type StateType = {
|
|
grid: GridType;
|
|
gameTime: number;
|
|
gameScore: number;
|
|
gameLevel: number;
|
|
|
|
dialogVisible: boolean;
|
|
dialogTitle: string;
|
|
dialogMessage: string;
|
|
dialogButtons: Array<OptionsDialogButtonType>;
|
|
onDialogDismiss: () => void;
|
|
};
|
|
|
|
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;
|
|
|
|
highScore: number | null;
|
|
|
|
constructor(props: PropsType) {
|
|
super(props);
|
|
this.highScore = null;
|
|
this.logic = new GameLogic(20, 10, props.theme);
|
|
this.state = {
|
|
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;
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
const {navigation} = this.props;
|
|
navigation.setOptions({
|
|
headerRight: this.getRightButton,
|
|
});
|
|
this.startGame();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.logic.endGame(true);
|
|
}
|
|
|
|
getRightButton = () => {
|
|
return (
|
|
<MaterialHeaderButtons>
|
|
<Item title="pause" iconName="pause" onPress={this.togglePause} />
|
|
</MaterialHeaderButtons>
|
|
);
|
|
};
|
|
|
|
onTick = (score: number, level: number, newGrid: GridType) => {
|
|
this.setState({
|
|
gameScore: score,
|
|
gameLevel: level,
|
|
grid: newGrid,
|
|
});
|
|
};
|
|
|
|
onClock = (time: number) => {
|
|
this.setState({
|
|
gameTime: time,
|
|
});
|
|
};
|
|
|
|
onDialogDismiss = () => {
|
|
this.setState({dialogVisible: false});
|
|
};
|
|
|
|
onGameEnd = (time: number, score: number, isRestart: boolean) => {
|
|
const {props, state} = this;
|
|
this.setState({
|
|
gameTime: time,
|
|
gameScore: score,
|
|
});
|
|
if (!isRestart) {
|
|
props.navigation.replace('game-start', {
|
|
score: state.gameScore,
|
|
level: state.gameLevel,
|
|
time: state.gameTime,
|
|
});
|
|
}
|
|
};
|
|
|
|
getStatusIcons() {
|
|
const {props, state} = this;
|
|
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={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>
|
|
);
|
|
}
|
|
|
|
getScoreIcon() {
|
|
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>
|
|
);
|
|
}
|
|
|
|
getControlButtons() {
|
|
const {props} = this;
|
|
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.startGame();
|
|
},
|
|
},
|
|
{
|
|
title: i18n.t('screens.game.restart.confirmNo'),
|
|
onPress: this.showPausePopup,
|
|
},
|
|
],
|
|
onDialogDismiss: this.showPausePopup,
|
|
});
|
|
};
|
|
|
|
startGame = () => {
|
|
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
|
|
};
|
|
|
|
render() {
|
|
const {props, state} = this;
|
|
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={state.grid}
|
|
style={{
|
|
backgroundColor: 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={state.dialogVisible}
|
|
title={state.dialogTitle}
|
|
message={state.dialogMessage}
|
|
buttons={state.dialogButtons}
|
|
onDismiss={state.onDialogDismiss}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withTheme(GameMainScreen);
|