LevelController.java
package com.example.project.controllers.gameScreens;
import com.example.project.controllers.gameScreens.animations.LevelScoreSequence;
import com.example.project.controllers.gameScreens.animations.ScoreTimeline;
import com.example.project.controllers.gameScreens.animations.TextEmphasisAnimation;
import com.example.project.controllers.tileViewControllers.LetterTileController;
import com.example.project.models.gameScreens.LevelModel;
import com.example.project.services.GameScenes;
import com.example.project.services.SceneManager;
import com.example.project.services.Session;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import javafx.scene.layout.VBox;
import java.util.*;
import javafx.scene.image.ImageView;
/**
* Controller for the level screen.
*/
public class LevelController extends GameScreenController
{
@FXML
Label levelWonLostText;
@FXML
HBox tileRackContainer;
@FXML
HBox wordViewHBox;
@FXML
HBox upgradeTilesContainer;
@FXML
Button playButton;
@FXML
Button redrawButton;
@FXML
Label scoreToBeatLabel;
@FXML
Label currentScoreLabel;
@FXML
Label comboCountLabel;
@FXML
Label comboMultiplierLabel;
@FXML
VBox redrawContainer;
@FXML
Button confirmRedrawButton;
@FXML private StackPane gameStack;
@FXML private ImageView backgroundImage;
@FXML private ImageView tileRackImage;
@FXML private Label playsLeftLabel;
@FXML private Label redrawsLeftLabel;
private static LevelModel levelModel;
private UpgradeTileGroup upgradeGroup;
private LetterTileGroup tileRack;
private LetterTileGroup wordRow;
private LetterTileGroup redrawColumn;
/**
* Constructor only called once each time application opened.
*/
public LevelController()
{
super();
levelModel = new LevelModel(Session.getInstance());
}
/**
* This runs after the constructor and after all @FXML fields are initialized once each time application opened.
*/
@FXML
public void initialize()
{
// Setup Listeners. (automatically updates each property when they're changed)
levelModel.getPlayersTotalPoints().addListener((obs, oldVal, newVal) -> syncTotalScoreProperty(newVal));
levelModel.wordPointsProperty().addListener((obs, oldVal, newVal) -> syncwordPointsProperty(newVal));
levelModel.wordMultiProperty().addListener((obs, oldVal, newVal) -> syncwordMultiProperty(newVal));
levelModel.getCurrentRedraws().addListener((obs, oldVal, newVal) -> syncRedrawButton());
levelModel.getCurrentPlays().addListener((obs, oldVal, newVal) -> syncPlayButton());
levelModel.getIsRedrawActive().addListener((obs, oldVal, newVal) -> syncRedrawWindow(newVal));
tileRack = new LetterTileGroup(levelModel.getHandSize(), tileRackContainer,
levelModel.getTileRackRowTilesProperty(), this::onLetterTileClicked);
wordRow = new LetterTileGroup(levelModel.getMaxWordSize(), wordViewHBox,
levelModel.getWordRowTilesProperty(), this::onLetterTileClicked,
List.of(this::syncPlayButton));
redrawColumn = new LetterTileGroup(levelModel.getRedrawWindowSize(), redrawContainer,
levelModel.getRedrawRowTilesProperty(), this::onLetterTileClicked,
List.of(this::syncRedrawButton,this::syncConfirmRedrawButton));
upgradeGroup = new UpgradeTileGroup(upgradeTilesContainer, levelModel.getUpgradeTilesProprety());
// Bind background image size to gameStack size
backgroundImage.fitWidthProperty().bind(gameStack.widthProperty());
backgroundImage.fitHeightProperty().bind(gameStack.heightProperty());
// Background always fills window
backgroundImage.fitWidthProperty().bind(gameStack.widthProperty());
backgroundImage.fitHeightProperty().bind(gameStack.heightProperty());
}
@Override
public void onSceneChangedToThis()
{
this.logger.logMessage("level page loaded.");
scoreToBeatLabel.setText(String.format("required: %s", levelModel.getLevelRequirement()));
levelModel.setupNewLevel();
levelWonLostText.setText("");
// sync observable properties.
syncwordPointsProperty(levelModel.wordPointsProperty().get());
syncwordMultiProperty(levelModel.wordMultiProperty().get());
syncTotalScoreProperty(levelModel.getPlayersTotalPoints().get());
syncPlayButton();
syncRedrawButton();
syncConfirmRedrawButton();
}
private void syncRedrawWindow(boolean isRedrawActive)
{
var distance = isRedrawActive ? -50 : 200; // slide on if inactive. slide out if active.
TranslateTransition redrawWindowSlide = new TranslateTransition(Duration.millis(500), redrawContainer);
redrawWindowSlide.setToX(distance);
redrawWindowSlide.play();
syncRedrawButton();
}
private void syncwordPointsProperty(Number newVal)
{
this.comboCountLabel.setText(String.format("%s", newVal));
}
private void syncwordMultiProperty(Number newVal)
{
this.comboMultiplierLabel.setText(String.format("%s", newVal));
}
private void syncTotalScoreProperty(Number newVal)
{
this.currentScoreLabel.setText(String.format("%s", newVal));
}
private void syncRedrawButton()
{
var redraws = levelModel.getCurrentRedraws().get();
redrawButton.setDisable(redraws == 0);
var buttonText = levelModel.getIsRedrawActive().get() ? "cancel" : "redraw";
this.redrawButton.setText(String.format("%s (redraws left: %s)", buttonText, levelModel.getCurrentRedraws().get()));
redrawsLeftLabel.setText(String.valueOf(redraws));
redrawButton.setText(levelModel.getIsRedrawActive().get() ? "⟳ cancel" : "⟳");
}
private void syncPlayButton()
{
var plays = levelModel.getCurrentPlays().get();
playButton.setDisable((plays == 0) || !levelModel.isWordValid() || levelModel.getWordRowTilesProperty().get().isEmpty() || levelModel.getIsRedrawActive().get());
this.playButton.setText(String.format("plays left: %s", plays));
playsLeftLabel.setText(String.valueOf(plays));
playButton.setText("▶");
}
private void syncConfirmRedrawButton(){
confirmRedrawButton.setDisable(levelModel.getRedrawRowTilesProperty().isEmpty());
}
/**
* Handle tile clicks.
*/
private void onLetterTileClicked(LetterTileController tileController)
{
boolean moved = levelModel.tryMoveTile(tileController.getModel());
if (!moved) { this.logger.logMessage("Cannot move tile - no space available or tile not found."); }
}
/**
* Handle play button
*/
@FXML
private void onPlayButton()
{
playButton.setDisable(true);
int startScore = levelModel.getPlayersTotalPoints().get();
var tileScoringSequence = new LevelScoreSequence(wordRow.getControllers(), levelModel, comboCountLabel, comboMultiplierLabel);
tileScoringSequence.setOnFinished(e ->
{
int endScore = startScore + levelModel.calcTotalScore();
ScoreTimeline totalScoreTimeline = new ScoreTimeline();
Timeline timeline = totalScoreTimeline.animateTotalScore(startScore, endScore, currentScoreLabel);
timeline.setOnFinished(f ->
{
TextEmphasisAnimation scoreEmphasis = new TextEmphasisAnimation(currentScoreLabel, Color.GREEN, Color.BLACK, Duration.seconds(0));
scoreEmphasis.play();
playButton.setDisable(false);
levelModel.playTiles();
levelModel.resetCombo();
levelModel.setTotalScore(endScore);
checkLevelState();
});
timeline.play();
});
tileScoringSequence.play();
}
private void checkLevelState()
{
if (levelModel.hasWon())
{
levelWonLostText.setText("YOU WON!");
TextEmphasisAnimation youWonSequence = new TextEmphasisAnimation(levelWonLostText, Color.GREEN, Color.BLACK, Duration.seconds(1));
youWonSequence.setOnFinished(e -> levelModel.onWonLevel());
youWonSequence.play();
}
else if (levelModel.hasLost()) { levelModel.onLostLevel(); }
}
@FXML
private void onSkipButton() { SceneManager.getInstance().switchScene(GameScenes.SHOP); }
/**
* redraw button opens or cancels the redraw.
*/
@FXML
private void onRedrawButton()
{
levelModel.toggleRedrawState();
}
/**
* Handle redraw confirm button.
*/
@FXML
private void onConfirmRedrawButton() {
levelModel.toggleRedrawState();
levelModel.redrawTiles();
}
}