/*
* This file is part of Rectball.
* Copyright (C) 2015 Dani RodrÃguez.
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package es.danirod.rectball.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Dialog;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import es.danirod.rectball.Constants;
import es.danirod.rectball.RectballGame;
import es.danirod.rectball.SoundPlayer.SoundCode;
import es.danirod.rectball.model.*;
import es.danirod.rectball.scene2d.game.*;
import es.danirod.rectball.scene2d.game.ScoreActor.ScoreListener;
import es.danirod.rectball.scene2d.game.TimerActor.TimerCallback;
import es.danirod.rectball.scene2d.listeners.BallSelectionListener;
import es.danirod.rectball.scene2d.ui.ConfirmDialog;
import java.util.ArrayList;
import java.util.List;
public class GameScreen extends AbstractScreen implements TimerCallback, BallSelectionListener, ScoreListener {
/**
* Display the remaining time.
*/
private TimerActor timer;
/**
* Display the current score.
*/
private ScoreActor score;
/**
* Display the board representation.
*/
private BoardActor board;
/**
* The display used to represent information about the user.
*/
private Table hud;
/**
* Is the game paused?
*/
private boolean paused;
/**
* Is the game running? True unless is on countdown or game over.
*/
private boolean running;
/**
* True if the user is asking to leave.
*/
private boolean askingLeave;
public GameScreen(RectballGame game) {
super(game, false);
}
/**
* This method will add to the stage a confirmation dialog asking the user
* whether to end the game or not. If the user answers YES to that dialog,
* the game will end.
*/
private void showLeaveDialog() {
ConfirmDialog dialog = new ConfirmDialog(game.getSkin(),
game.getLocale().get("game.leave"),
game.getLocale().get("core.yes"),
game.getLocale().get("core.no"));
dialog.setCallback(new ConfirmDialog.ConfirmCallback() {
@Override
public void ok() {
// The user wants to leave the game.
game.player.playSound(SoundCode.SUCCESS);
askingLeave = false;
onTimeOut();
}
@Override
public void cancel() {
// The user wants to resume the game.
game.player.playSound(SoundCode.FAIL);
askingLeave = false;
resumeGame();
}
});
dialog.show(getStage());
askingLeave = true;
}
private void showPauseDialog() {
ConfirmDialog dialog = new ConfirmDialog(game.getSkin(),
game.getLocale().get("game.paused"),
game.getLocale().get("game.continue"),
game.getLocale().get("game.leaveGame"));
dialog.setCallback(new ConfirmDialog.ConfirmCallback() {
@Override
public void ok() {
// Continue
game.player.playSound(SoundCode.SELECT);
askingLeave = false;
resumeGame();
}
@Override
public void cancel() {
// Leave
game.player.playSound(SoundCode.SELECT);
showLeaveDialog();
}
});
dialog.show(getStage());
askingLeave = true;
}
@Override
public int getID() {
return Screens.GAME;
}
@Override
public void show() {
super.show();
// Reset data
if (!game.isRestoredState()) {
game.getState().reset();
} else {
game.setRestoredState(false);
if (game.getState().isTimeout()) {
game.pushScreen(Screens.GAME_OVER);
}
}
// The player is playing
game.getState().setPlaying(true);
score.setValue(game.getState().getScore());
timer.setSeconds(game.getState().getRemainingTime());
paused = running = askingLeave = false;
if (!game.getState().isCountdownFinished()) {
countdown(2, new Runnable() {
@Override
public void run() {
game.getState().setCountdownFinished(true);
// Start the game unless the user is leaving or is paused.
if (!paused && !askingLeave) {
running = true;
board.setColoured(true);
timer.setRunning(true);
board.setTouchable(Touchable.enabled);
game.player.playSound(SoundCode.SUCCESS);
}
}
});
} else {
pauseGame();
}
}
/**
* Create a countdown in the screen lasting for the amount of seconds given.
* When the countdown reaches 0, the code provided in the runnable will
* be executed as a callback.
*
* @param seconds how many seconds should the countdown be displayed.
*/
private void countdown(final int seconds, final Runnable after) {
// Since this is a recursive function, avoid the case where you pass
// a number of seconds that might trigger an infinite loop.
if (seconds <= 0) {
return;
}
// Create the label that will contain this number
String number = Integer.toString(seconds);
final Label label = new Label(number, game.getSkin(), "monospace");
label.setFontScale(5f);
label.setSize(150, 150);
label.setAlignment(Align.center);
label.setPosition(
(getStage().getWidth() - label.getWidth()) / 2,
(getStage().getHeight() - label.getHeight()) / 2);
// Add the label to the stage and play a sound to notify the user.
getStage().addActor(label);
game.player.playSound(SoundCode.SELECT);
label.addAction(Actions.sequence(
Actions.parallel(Actions.moveBy(0, 80, 1f)),
// After the animation, decide. If the countdown hasn't finished
// yet, run another countdown with 1 second less.
Actions.run(new Runnable() {
@Override
public void run() {
label.remove();
if (seconds > 1) {
countdown(seconds - 1, after);
} else {
after.run();
}
}
})
));
}
/**
* Generate new colors. This method is executed by the callback action
* when the player selects a valid rectangle. The purpose of this method
* is to regenerate the board and apply any required checks to make sure
* that the game doesn't enter in an infinite loop.
*
* @param bounds the bounds that have to be regenerated.
*/
private void generate(Bounds bounds) {
// Generate new balls;
game.getState().getBoard().randomize(
new Coordinate(bounds.minX, bounds.minY),
new Coordinate(bounds.maxX, bounds.maxY));
// Check the new board for valid combinations.
CombinationFinder newFinder = CombinationFinder.create(game.getState().getBoard());
if (newFinder.getPossibleBounds().size() == 1) {
// Only one combination? This is trouble.
Bounds newCombinationBounds = newFinder.getCombination();
if (newCombinationBounds.equals(bounds)) {
// Oh, oh, in the same spot! So, they must be of the same color.
// Therefore, we need to randomize some balls to avoid enter
// an infinite loop.
timer.setRunning(false);
board.setColoured(false);
game.getState().resetBoard();
board.addAction(Actions.sequence(
board.shake(10, 5, 0.05f),
Actions.run(new Runnable() {
@Override
public void run() {
board.setColoured(true);
timer.setRunning(true);
}
})));
}
}
board.addAction(board.showRegion(bounds));
}
private void showPartialScore(int score, Bounds bounds, boolean special) {
// Calculate the center of the region.
BallActor bottomLeftBall = board.getBall(bounds.minX, bounds.minY);
BallActor upperRightBall = board.getBall(bounds.maxX, bounds.maxY);
float minX = bottomLeftBall.getX();
float maxX = upperRightBall.getX() + upperRightBall.getWidth();
float minY = bottomLeftBall.getY();
float maxY = upperRightBall.getY() + upperRightBall.getHeight();
float centerX = (minX + maxX) / 2;
float centerY = (minY + maxY) / 2;
Label label = new Label("+" + score, game.getSkin(), "monospace");
label.setFontScale(5f);
label.setSize(140, 70);
label.setAlignment(Align.center);
label.setPosition(centerX - label.getWidth() / 2, centerY - label.getHeight() / 2);
label.addAction(Actions.sequence(
Actions.moveBy(0, 80, 0.5f),
Actions.removeActor()
));
getStage().addActor(label);
if (special) {
label.setColor(Color.CYAN);
}
}
@Override
public void setUpInterface(Table table) {
// Create the actors for this screen.
timer = new TimerActor(Constants.SECONDS, game.getSkin());
score = new ScoreActor(game.getSkin());
board = new BoardActor(game.getBallAtlas(), game.getState().getBoard());
// Add the help button.
ImageButton help = new ImageButton(game.getSkin(), "blueHelp");
help.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
// Don't act if the game hasn't started yet.
if (!timer.isRunning() || game.getState().isTimeout()) {
event.cancel();
return;
}
// Don't do anything if there are less than 5 seconds.
if (timer.getSeconds() <= 5 && game.getState().getWiggledBounds() == null) {
game.player.playSound(SoundCode.FAIL);
event.cancel();
return;
}
// Wiggle a valid combination.
if (game.getState().getWiggledBounds() == null) {
CombinationFinder finder = CombinationFinder.create(game.getState().getBoard());
game.getState().setWiggledBounds(finder.getPossibleBounds().get(MathUtils.random(finder.getPossibleBounds().size() - 1)));
}
board.addAction(board.shake(game.getState().getWiggledBounds(), 10, 5, 0.1f));
if (!game.getState().isCheatSeen()) {
// Subtract some time.
float subtractedTime = 5f;
final float step = subtractedTime / 10;
getStage().addAction(Actions.repeat(10, Actions.delay(0.01f,
Actions.run(new Runnable() {
@Override
public void run() {
timer.setSeconds(timer.getSeconds() - step);
}
}))));
game.getState().setCheatSeen(true);
}
event.cancel();
}
});
// Add the pause button.
ImageButton pause = new ImageButton(game.getSkin(), "blueCross");
pause.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
game.player.playSound(SoundCode.FAIL);
pauseGame();
event.cancel();
}
});
// Disable game until countdown ends.
timer.setRunning(false);
board.setTouchable(Touchable.disabled);
// Add subscribers.
timer.addSubscriber(this);
board.addSubscriber(this);
score.setScoreListener(this);
/*
* Fill the HUD, which is the display that appears over the board with
* all the information. The HUD is generated differently depending on
* the aspect ratio of the device. If the device is 4:3, the HUD is
* compressed to avoid making the board small. Otherwise, it's expanded
* to the usual size.
*/
boolean landscape = Gdx.graphics.getWidth() > Gdx.graphics.getHeight();
float aspectRatio = landscape ?
(float) Gdx.graphics.getWidth() / Gdx.graphics.getHeight() :
(float) Gdx.graphics.getHeight() / Gdx.graphics.getWidth();
hud = new Table();
if (aspectRatio < 1.5f) {
// Compact layout: buttons and score in the same line.
hud.add(new BorderedContainer(game.getSkin(), help)).size(50).padBottom(10);
hud.add(new BorderedContainer(game.getSkin(), score)).height(50).width(Constants.VIEWPORT_WIDTH / 2).space(10).expandX().fillX();
hud.add(new BorderedContainer(game.getSkin(), pause)).size(50).padBottom(10).row();
hud.add(new BorderedContainer(game.getSkin(), timer)).colspan(3).fillX().height(40).padBottom(20).row();
} else {
// Large layout: buttons above timer, score below timer (classic).
hud.add(new BorderedContainer(game.getSkin(), help)).size(50).spaceLeft(10).padBottom(10).align(Align.left);
hud.add(new BorderedContainer(game.getSkin(), pause)).size(50).spaceRight(10).padBottom(10).align(Align.right).row();
hud.add(new BorderedContainer(game.getSkin(), timer)).colspan(2).fillX().expandX().height(40).padBottom(10).row();
hud.add(new BorderedContainer(game.getSkin(), score)).colspan(2).height(60).width(Constants.VIEWPORT_WIDTH / 2).align(Align.center).padBottom(10).row();
}
if (aspectRatio > 1.6) {
hud.padBottom(20);
}
table.add(hud).fillX().expandY().align(Align.top).row();
table.add(board).fill().expand().row();
}
@Override
public void hide() {
// The player is not playing anymore.
game.getState().setPlaying(false);
// Just in case, remove any dialogs that might be forgotten.
for (Actor actor : getStage().getActors()) {
if (actor instanceof Dialog) {
((Dialog) actor).hide(null);
}
}
// Restore original back button functionality.
Gdx.input.setCatchBackKey(false);
}
@Override
public void render(float delta) {
super.render(delta);
// If the timer is running, keep incrementing the timer.
if (timer.isRunning()) {
game.getState().addTime(delta);
game.getState().setRemainingTime(timer.getSeconds());
}
// The user should be able to leave during the game.
if (Gdx.input.isKeyJustPressed(Input.Keys.BACK) || Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
if (!paused && !game.getState().isTimeout()) {
pauseGame();
}
}
}
/**
* This method pauses the game. It is executed in one of the following
* situations: first, the user presses PAUSE button. Second, the user
* presses BACK button. Third, the user pauses the game (when the game
* is restored the game is still paused).
*/
private void pauseGame() {
paused = true;
// Show the pause dialog unless you have already stop the game.
if (!game.getState().isTimeout()) {
showPauseDialog();
}
// If the game has started, pause it.
if (running && !game.getState().isTimeout()) {
board.setColoured(false);
board.setTouchable(Touchable.disabled);
timer.setRunning(false);
}
}
/**
* This method resumes the game. It is executed in one of the following
* situations: first, the user presses CONTINUE button on pause screen.
* Second, the user presses BACK on the pause screen.
*/
private void resumeGame() {
paused = false;
// If the countdown has finished but the game is not running is a
// condition that might be triggered in one of the following cases.
// 1) The user has paused the game during the countdown. When the
// countdown finishes, because the game is paused, the game does
// not start.
// 2) The user was leaving the game during the countdown. Same.
if (!running && game.getState().isCountdownFinished()) {
running = true;
game.player.playSound(SoundCode.SUCCESS);
}
if (running && !game.getState().isTimeout()) {
board.setColoured(true);
board.setTouchable(Touchable.enabled);
timer.setRunning(true);
}
}
@Override
public void pause() {
// Put the bounds
game.getState().setBoardBounds(new Rectangle(board.getX(), board.getY(), board.getWidth(), board.getHeight()));
// Show the pause dialog if it is not already visible.
if (!paused) {
pauseGame();
}
}
@Override
public void resume() {
}
@Override
public void onTimeOut() {
// Disable any further interactions.
board.clearSelection();
board.setColoured(true);
board.setTouchable(Touchable.disabled);
timer.setRunning(false);
game.player.playSound(SoundCode.GAME_OVER);
// Update the score... and the record.
int score = game.getState().getScore();
int time = Math.round(game.getState().getElapsedTime());
game.getPlatform().score().registerScore(score, time);
game.getPlatform().score().flushData();
// Save information about this game in the statistics.
game.statistics.getTotalData().incrementValue("score", game.getState().getScore());
game.statistics.getTotalData().incrementValue("games");
game.statistics.getTotalData().incrementValue("time", Math.round(game.getState().getElapsedTime()));
game.getPlatform().statistics().saveStatistics(game.statistics);
// Mark a combination that the user could do if he had enough time.
if (game.getState().getWiggledBounds() == null) {
CombinationFinder combo = CombinationFinder.create(game.getState().getBoard());
game.getState().setWiggledBounds(combo.getCombination());
}
for (int y = 0; y < game.getState().getBoard().getSize(); y++) {
for (int x = 0; x < game.getState().getBoard().getSize(); x++) {
if (game.getState().getWiggledBounds() != null && !game.getState().getWiggledBounds().inBounds(x, y)) {
board.getBall(x, y).addAction(Actions.color(Color.DARK_GRAY, 0.15f));
}
}
}
// Animate the transition to game over.
board.addAction(Actions.delay(2f, board.hideBoard()));
hud.addAction(Actions.delay(2f, Actions.fadeOut(0.25f)));
// Head to the game over after all these animations have finished.
getStage().addAction(Actions.delay(2.5f, Actions.run(new Runnable() {
@Override
public void run() {
getStage().getRoot().clearActions();
game.pushScreen(Screens.GAME_OVER);
}
})));
// Mark the game as finished.
game.getState().setTimeout(true);
}
@Override
public void onBallSelected(BallActor ball) {
ball.addAction(Actions.scaleTo(0.8f, 0.8f, 0.15f));
ball.addAction(Actions.color(Color.GRAY, 0.15f));
game.player.playSound(SoundCode.SELECT);
}
@Override
public void onBallUnselected(BallActor ball) {
ball.addAction(Actions.scaleTo(1f, 1f, 0.15f));
ball.addAction(Actions.color(Color.WHITE, 0.15f));
game.player.playSound(SoundCode.UNSELECT);
}
@Override
public void onSelectionSucceeded(final List<BallActor> selection) {
// Extract the data from the selection.
List<Ball> balls = new ArrayList<>();
for (BallActor selectedBall : selection)
balls.add(selectedBall.getBall());
final Bounds bounds = Bounds.fromBallList(balls);
// Change the colors of the selected region.
board.addAction(Actions.sequence(
board.hideRegion(bounds),
Actions.run(new Runnable() {
@Override
public void run() {
for (BallActor selectedBall : selection) {
selectedBall.setColor(Color.WHITE);
}
generate(bounds);
// Reset the cheat
game.getState().setCheatSeen(false);
game.getState().setWiggledBounds(null);
}
})
));
// Give some score to the user.
ScoreCalculator calculator = new ScoreCalculator(game.getState().getBoard(), bounds);
int givenScore = calculator.calculate();
game.getState().addScore(givenScore);
score.giveScore(givenScore);
// Put information about this combination in the stats.
int rows = bounds.maxY - bounds.minY + 1;
int cols = bounds.maxX - bounds.minX + 1;
String size = Math.max(rows, cols) + "x" + Math.min(rows, cols);
game.statistics.getSizesData().incrementValue(size);
BallColor color = board.getBall(bounds.minX, bounds.minY).getBall().getColor();
game.statistics.getColorData().incrementValue(color.toString().toLowerCase());
game.statistics.getTotalData().incrementValue("balls", rows * cols);
game.statistics.getTotalData().incrementValue("combinations");
// Now, display the score to the user. If the combination is a
// PERFECT combination, just display PERFECT.
int boardSize = game.getState().getBoard().getSize() - 1;
if (bounds.equals(new Bounds(0, 0, boardSize, boardSize))) {
// Give score
Label label = new Label("PERFECT", game.getSkin(), "monospace");
label.setX((getStage().getViewport().getWorldWidth() - label.getWidth()) / 2);
label.setY((getStage().getViewport().getWorldHeight() - label.getHeight()) / 2);
label.setFontScale(3);
label.setAlignment(Align.center);
label.addAction(Actions.sequence(
Actions.moveBy(0, 80, 0.5f),
Actions.removeActor()
));
getStage().addActor(label);
game.player.playSound(SoundCode.PERFECT);
// Give time
float givenTime = Constants.SECONDS - timer.getSeconds();
timer.giveTime(givenTime, 4f);
} else {
// Was special?
boolean special = givenScore != rows * cols;
// Give score
showPartialScore(givenScore, bounds, special);
game.player.playSound(SoundCode.SUCCESS);
// Give time
float givenTime = 4f;
timer.giveTime(givenTime, 0.25f);
}
}
@Override
public void onSelectionFailed(List<BallActor> selection) {
for (BallActor selected : selection) {
selected.addAction(Actions.scaleTo(1f, 1f, 0.15f));
selected.addAction(Actions.color(Color.WHITE, 0.15f));
}
game.player.playSound(SoundCode.FAIL);
}
@Override
public void onSelectionCleared(List<BallActor> selection) {
for (BallActor selected : selection) {
selected.addAction(Actions.scaleTo(1f, 1f, 0.15f));
selected.addAction(Actions.color(Color.WHITE, 0.15f));
}
}
@Override
public void onScoreGoNuts() {
game.player.playSound(SoundCode.PERFECT);
}
}