You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
741 lines
29 KiB
741 lines
29 KiB
package visualiser.Controllers;
|
|
|
|
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
|
|
import javafx.animation.AnimationTimer;
|
|
import javafx.application.Platform;
|
|
import javafx.collections.FXCollections;
|
|
import javafx.collections.ListChangeListener;
|
|
import javafx.collections.ObservableList;
|
|
import javafx.collections.transformation.SortedList;
|
|
import javafx.fxml.FXML;
|
|
import javafx.scene.AmbientLight;
|
|
import javafx.scene.PointLight;
|
|
import javafx.scene.chart.LineChart;
|
|
import javafx.scene.control.*;
|
|
import javafx.scene.input.KeyCode;
|
|
import javafx.scene.input.KeyEvent;
|
|
import javafx.scene.layout.AnchorPane;
|
|
import javafx.scene.layout.GridPane;
|
|
import javafx.scene.layout.StackPane;
|
|
import javafx.scene.paint.Color;
|
|
import javafx.scene.paint.Material;
|
|
import javafx.scene.paint.PhongMaterial;
|
|
import javafx.scene.shape.MeshView;
|
|
import javafx.scene.shape.Shape3D;
|
|
import javafx.scene.transform.Translate;
|
|
import javafx.util.Callback;
|
|
import network.Messages.Enums.RaceStatusEnum;
|
|
import shared.dataInput.RaceDataSource;
|
|
import shared.exceptions.BoatNotFoundException;
|
|
import shared.model.*;
|
|
import visualiser.app.App;
|
|
import visualiser.enums.TutorialState;
|
|
import visualiser.gameController.ControllerClient;
|
|
import visualiser.gameController.Keys.ControlKey;
|
|
import visualiser.gameController.Keys.KeyFactory;
|
|
import visualiser.layout.*;
|
|
import visualiser.model.Sparkline;
|
|
import visualiser.model.VisualiserBoat;
|
|
import visualiser.model.VisualiserRaceEvent;
|
|
import visualiser.model.VisualiserRaceState;
|
|
import visualiser.utils.GPSConverter;
|
|
|
|
import java.io.IOException;
|
|
import java.net.URL;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
/**
|
|
* Controller used to display a running race.
|
|
*/
|
|
public class RaceViewController extends Controller {
|
|
private VisualiserRaceEvent visualiserRace;
|
|
private VisualiserRaceState raceState;
|
|
private ControllerClient controllerClient;
|
|
private KeyFactory keyFactory = new KeyFactory();
|
|
private boolean infoTableShow = true; // shown or hidden
|
|
private boolean isHost;
|
|
private TutorialState currentState;
|
|
private ArrayList<TutorialState> tutorialStates;
|
|
private boolean isTutorial = false;
|
|
private String keyToPress;
|
|
private View3D view3D;
|
|
private ObservableList<Subject3D> viewSubjects;
|
|
|
|
/**
|
|
* Arrow pointing to next mark in third person
|
|
*/
|
|
private Subject3D nextMarkArrow;
|
|
/**
|
|
* Animation loop for rotating mark arrow
|
|
*/
|
|
private AnimationTimer pointToMark;
|
|
|
|
// note: it says it's not used but it is! do not remove :)
|
|
private @FXML ArrowController arrowController;
|
|
private @FXML GridPane canvasBase;
|
|
private @FXML SplitPane racePane;
|
|
private @FXML StackPane arrowPane;
|
|
private @FXML Label timer;
|
|
private @FXML Label FPS;
|
|
private @FXML Label timeZone;
|
|
private @FXML CheckBox showFPS;
|
|
private @FXML TableView<VisualiserBoat> boatInfoTable;
|
|
private @FXML TableColumn<VisualiserBoat, String> boatPlacingColumn;
|
|
private @FXML TableColumn<VisualiserBoat, String> boatTeamColumn;
|
|
private @FXML TableColumn<VisualiserBoat, Leg> boatMarkColumn;
|
|
private @FXML TableColumn<VisualiserBoat, Number> boatSpeedColumn;
|
|
private @FXML LineChart<Number, Number> sparklineChart;
|
|
private @FXML Label tutorialText;
|
|
private @FXML AnchorPane infoWrapper;
|
|
private @FXML AnchorPane lineChartWrapper;
|
|
|
|
/**
|
|
* Displays a specified race.
|
|
* Intended to be called on loading the scene.
|
|
* @param visualiserRace Object modelling the race.
|
|
* @param controllerClient Socket Client that manipulates the controller.
|
|
* @param isHost is user a host
|
|
*/
|
|
public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient, Boolean isHost) {
|
|
this.visualiserRace = visualiserRace;
|
|
this.raceState = visualiserRace.getVisualiserRaceState();
|
|
this.controllerClient = controllerClient;
|
|
this.isHost = isHost;
|
|
keyFactory.load();
|
|
|
|
tutorialCheck();
|
|
initKeypressHandler();
|
|
initialiseRaceVisuals();
|
|
}
|
|
|
|
/**
|
|
* Checks if the current game is a tutorial race and sets up initial
|
|
* tutorial displays if it is.
|
|
*/
|
|
private void tutorialCheck(){
|
|
if (App.gameType == 4) {
|
|
isTutorial = true;
|
|
tutorialText.setVisible(true);
|
|
tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values()));
|
|
|
|
currentState = tutorialStates.get(0);
|
|
tutorialStates.remove(0);
|
|
searchMapForKey("Upwind");
|
|
|
|
tutorialText.setText(
|
|
"Welcome to the tutorial! Exit at anytime with ESC. \nWe will first learn how to turn upwind. Press " +
|
|
keyToPress + " to turn upwind.");
|
|
|
|
} else {
|
|
isTutorial = false;
|
|
tutorialText.setVisible(false);
|
|
}
|
|
}
|
|
|
|
private AnimationTimer arrowToNextMark;
|
|
|
|
private void initKeypressHandler() {
|
|
racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
|
String codeString = event.getCode().toString();
|
|
|
|
// tab key
|
|
if (codeString.equals("TAB")){toggleTable();}
|
|
|
|
// any key pressed
|
|
ControlKey controlKey = keyFactory.getKey(codeString);
|
|
if(controlKey != null) {
|
|
try {
|
|
controlKey.onAction(); // Change key state if applicable
|
|
|
|
//Check if current race is a tutorial
|
|
if (isTutorial){
|
|
//Check if current tutorial state has the same boat protocol code as key press
|
|
if (controlKey.getProtocolCode().equals(currentState.getAction())){
|
|
//Update tutorial
|
|
checkTutorialState();
|
|
}
|
|
}
|
|
|
|
controllerClient.sendKey(controlKey);
|
|
event.consume();
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
Logger.getGlobal().log(Level.WARNING, "RaceViewController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
|
|
Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
// escape key
|
|
if(event.getCode() == KeyCode.ESCAPE) {
|
|
try {
|
|
if (isHost) {
|
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
|
alert.setTitle("Exit Race");
|
|
alert.setContentText("Do you wish to quit the race? You are the host");
|
|
Optional<ButtonType> result = alert.showAndWait();
|
|
if (result.get() == ButtonType.OK) {
|
|
App.game.endEvent();
|
|
loadTitleScreen();
|
|
}
|
|
} else {
|
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
|
alert.setTitle("Exit Race");
|
|
alert.setContentText("Do you wish to quit the race?");
|
|
Optional<ButtonType> result = alert.showAndWait();
|
|
if (result.get() == ButtonType.OK) {
|
|
loadTitleScreen();
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialises the various UI components to listen to the {@link #visualiserRace}.
|
|
*/
|
|
private void initialiseRaceVisuals() {
|
|
|
|
// Import arrow mesh
|
|
URL asset = this.getClass().getClassLoader().getResource("assets/arrow V1.0.4.stl");
|
|
StlMeshImporter importer = new StlMeshImporter();
|
|
importer.read(asset);
|
|
|
|
MeshView arrow = new MeshView(importer.getImport());
|
|
PhongMaterial arrowMat = new PhongMaterial(Color.RED);
|
|
arrow.setMaterial(arrowMat);
|
|
|
|
this.nextMarkArrow = new Annotation3D(arrow);
|
|
this.nextMarkArrow.setScale(0.1);
|
|
|
|
// initialise displays
|
|
initialiseFps();
|
|
initialiseInfoTable();
|
|
initialiseView3D(this.visualiserRace);
|
|
initialiseRaceClock();
|
|
raceTimer(); // start the timer
|
|
new Sparkline(this.raceState, this.sparklineChart);
|
|
timeZone.setText(this.raceState.getRaceClock().getTimeZone());
|
|
arrowController.setWindProperty(this.raceState.windProperty());
|
|
}
|
|
|
|
private void initialiseView3D(VisualiserRaceEvent race) {
|
|
viewSubjects = FXCollections.observableArrayList();
|
|
|
|
AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF"));
|
|
ambientLight.setTranslateX(250);
|
|
ambientLight.setTranslateZ(210);
|
|
ambientLight.setLightOn(true);
|
|
|
|
PointLight pointLight = new PointLight(Color.web("#AAAAFF"));
|
|
pointLight.setTranslateX(250);
|
|
pointLight.setTranslateZ(210);
|
|
pointLight.setLightOn(true);
|
|
|
|
// Import boat mesh
|
|
URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl");
|
|
StlMeshImporter importer = new StlMeshImporter();
|
|
importer.read(asset);
|
|
|
|
// Configure camera angles and control
|
|
URL markerAsset = RaceViewController.class.getClassLoader().getResource("assets/Bouy V1.1.stl");
|
|
StlMeshImporter importerMark = new StlMeshImporter();
|
|
importerMark.read(markerAsset);
|
|
|
|
URL alternateBoatAsset = RaceViewController.class.getClassLoader().getResource("assets/V1.3 BurgerBoat.stl");
|
|
StlMeshImporter importerBurgerBoat = new StlMeshImporter();
|
|
importerBurgerBoat.read(alternateBoatAsset);
|
|
|
|
view3D = new View3D(false);
|
|
view3D.setItems(viewSubjects);
|
|
view3D.setDistance(1050);
|
|
view3D.setBirdsEye();
|
|
view3D.enableTracking();
|
|
view3D.addAmbientLight(ambientLight);
|
|
view3D.addPointLight(pointLight);
|
|
canvasBase.add(view3D, 0, 0);
|
|
|
|
// Set up projection from GPS to view
|
|
RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
|
|
final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450);
|
|
|
|
SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210);
|
|
viewSubjects.addAll(skyBox.getSkyBoxPlanes());
|
|
|
|
// Set up sea surface
|
|
SeaSurface sea = new SeaSurface(750, 200);
|
|
sea.setX(250);
|
|
sea.setZ(210);
|
|
viewSubjects.add(sea);
|
|
|
|
// Set up sea surface overlay
|
|
SeaSurface seaOverlay = new SeaSurface(4000, 200);
|
|
seaOverlay.setX(250);
|
|
seaOverlay.setZ(210);
|
|
viewSubjects.add(seaOverlay);
|
|
|
|
Boundary3D boundary3D = new Boundary3D(visualiserRace.getVisualiserRaceState().getRaceDataSource().getBoundary(), gpsConverter);
|
|
for (Subject3D subject3D: boundary3D.getBoundaryNodes()){
|
|
viewSubjects.add(subject3D);
|
|
}
|
|
// Position and add each mark to view
|
|
for(Mark mark: race.getVisualiserRaceState().getMarks()) {
|
|
MeshView mesh = new MeshView(importerMark.getImport());
|
|
Subject3D markModel = new Subject3D(mesh, mark.getSourceID());
|
|
|
|
markModel.setX(gpsConverter.convertGPS(mark.getPosition()).getX());
|
|
markModel.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());
|
|
|
|
viewSubjects.add(markModel);
|
|
}
|
|
// Position and add each boat to view
|
|
for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) {
|
|
MeshView mesh;
|
|
if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) {
|
|
mesh = new MeshView(importer.getImport());
|
|
} else {
|
|
mesh = new MeshView(importerBurgerBoat.getImport());
|
|
}
|
|
PhongMaterial boatColorMat = new PhongMaterial(boat.getColor());
|
|
mesh.setMaterial(boatColorMat);
|
|
Subject3D boatModel = new Subject3D(mesh, boat.getSourceID());
|
|
|
|
viewSubjects.add(boatModel);
|
|
|
|
// Track this boat's movement with the new subject
|
|
AnimationTimer trackBoat = new AnimationTimer() {
|
|
@Override
|
|
public void handle(long now) {
|
|
boatModel.setHeading(boat.getBearing().degrees());
|
|
boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
|
|
boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
|
|
}
|
|
};
|
|
trackBoat.start();
|
|
|
|
Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1));
|
|
CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark();
|
|
view3D.getShape(nextMark.getMark1().getSourceID()).getMesh().setMaterial(markColor);
|
|
if(nextMark.getMark2() != null) {
|
|
view3D.getShape(nextMark.getMark2().getSourceID()).getMesh().setMaterial(markColor);
|
|
}
|
|
Subject3D shockwave = new Shockwave(10);
|
|
|
|
viewSubjects.add(shockwave);
|
|
|
|
boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr)));
|
|
boat.hasCollidedProperty().addListener((o, prev, curr) -> Platform.runLater(() -> showCollision(boat, shockwave)));
|
|
}
|
|
// Fix initial bird's-eye position
|
|
view3D.updatePivot(new Translate(250, 0, 210));
|
|
|
|
view3D.targetProperty().addListener((o, prev, curr)-> {
|
|
if(curr != null && visualiserRace.getVisualiserRaceState().isVisualiserBoat(curr.getSourceID())) {
|
|
addThirdPersonAnnotations(curr);
|
|
} else {
|
|
removeThirdPersonAnnotations();
|
|
}
|
|
});
|
|
|
|
// Bind zooming to scrolling
|
|
view3D.setOnScroll(e -> {
|
|
view3D.updateDistance(e.getDeltaY());
|
|
});
|
|
|
|
// Bind zooming to keypress (Z/X default)
|
|
racePane.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
|
|
ControlKey key = keyFactory.getKey(e.getCode().toString());
|
|
if(key != null) {
|
|
switch (key.toString()) {
|
|
case "Zoom In":
|
|
//Check if race is a tutorial
|
|
if (isTutorial) {
|
|
//Check if the current tutorial state is zoom-in
|
|
if (currentState.equals(TutorialState.ZOOMIN)) {
|
|
try {
|
|
//Update tutorial
|
|
checkTutorialState();
|
|
} catch (Exception e1) {
|
|
e1.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
// view3D.updateDistance(-10);
|
|
view3D.zoomIn();
|
|
break;
|
|
case "Zoom Out":
|
|
//Check if race is a tutorial
|
|
if(isTutorial) {
|
|
//Check if current tutorial state is zoom-out
|
|
if (currentState.equals(TutorialState.ZOOMOUT)) {
|
|
try {
|
|
//Update tutorial
|
|
checkTutorialState();
|
|
} catch (Exception e1) {
|
|
e1.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
// view3D.updateDistance(10);
|
|
view3D.zoomOut();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void showCollision(VisualiserBoat boat, Subject3D shockwave) {
|
|
Subject3D boatModel = view3D.getShape(boat.getSourceID());
|
|
AnimationTimer shockFront = new AnimationTimer() {
|
|
double opacity = 1;
|
|
|
|
@Override
|
|
public void handle(long now) {
|
|
shockwave.setX(boatModel.getPosition().getX());
|
|
shockwave.setY(boatModel.getPosition().getY());
|
|
shockwave.setZ(boatModel.getPosition().getZ());
|
|
|
|
if(opacity <= 0) {
|
|
shockwave.getMesh().setMaterial(new PhongMaterial(Color.TRANSPARENT));
|
|
boat.setHasCollided(false);
|
|
this.stop();
|
|
}
|
|
else {
|
|
shockwave.getMesh().setMaterial(new PhongMaterial(new Color(1,0,0,opacity)));
|
|
opacity -= 0.1;
|
|
}
|
|
}
|
|
};
|
|
shockFront.start();
|
|
}
|
|
|
|
private void addThirdPersonAnnotations(Subject3D subject3D) {
|
|
viewSubjects.add(nextMarkArrow);
|
|
final VisualiserBoat boat;
|
|
try {
|
|
boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID());
|
|
} catch (BoatNotFoundException e) {
|
|
e.printStackTrace();
|
|
return;
|
|
}
|
|
arrowToNextMark = new AnimationTimer() {
|
|
@Override
|
|
public void handle(long now) {
|
|
CompoundMark target = boat.getCurrentLeg().getEndCompoundMark();
|
|
Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate());
|
|
|
|
nextMarkArrow.setX(view3D.getPivot().getX());
|
|
nextMarkArrow.setY(view3D.getPivot().getY());
|
|
nextMarkArrow.setZ(view3D.getPivot().getZ() + 15);
|
|
nextMarkArrow.setHeading(headingToMark.degrees());
|
|
}
|
|
};
|
|
arrowToNextMark.start();
|
|
}
|
|
|
|
private void removeThirdPersonAnnotations() {
|
|
viewSubjects.remove(nextMarkArrow);
|
|
if (arrowToNextMark != null) {
|
|
arrowToNextMark.stop();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Swap the colour of the next mark to pass with the last mark passed
|
|
* @param leg boat has started on
|
|
*/
|
|
private void swapColours(Leg leg) {
|
|
CompoundMark start = leg.getStartCompoundMark();
|
|
CompoundMark end = leg.getEndCompoundMark();
|
|
|
|
//The last leg "finish" doesn't have compound marks.
|
|
if (start == null || end == null ) {
|
|
return;
|
|
}
|
|
|
|
Shape3D start1 = view3D.getShape(start.getMark1().getSourceID()).getMesh();
|
|
Shape3D end1 = view3D.getShape(end.getMark1().getSourceID()).getMesh();
|
|
|
|
Material nextMark = start1.getMaterial();
|
|
Material lastMark = end1.getMaterial();
|
|
|
|
start1.setMaterial(lastMark);
|
|
if(start.getMark2() != null) {
|
|
Shape3D start2 = view3D.getShape(start.getMark2().getSourceID()).getMesh();
|
|
start2.setMaterial(lastMark);
|
|
}
|
|
|
|
end1.setMaterial(nextMark);
|
|
if(end.getMark2() != null) {
|
|
Shape3D end2 = view3D.getShape(end.getMark2().getSourceID()).getMesh();
|
|
end2.setMaterial(nextMark);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialises the frame rate functionality. This allows for toggling the
|
|
* frame rate, and connect the fps label to the race's fps property.
|
|
*/
|
|
private void initialiseFps() {
|
|
// fps toggle listener
|
|
showFPS.selectedProperty().addListener((ov, old_val, new_val) -> {
|
|
if (showFPS.isSelected()) {
|
|
FPS.setVisible(true);
|
|
} else {
|
|
FPS.setVisible(false);
|
|
}
|
|
});
|
|
|
|
// fps label display
|
|
this.visualiserRace.getFrameRateProperty().addListener((observable,
|
|
oldValue, newValue) -> {
|
|
Platform.runLater(() ->
|
|
this.FPS.setText("FPS: " + newValue.toString()));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialises the information table view to listen to a given race.
|
|
*/
|
|
private void initialiseInfoTable() {
|
|
// list of boats to display data for
|
|
ObservableList<VisualiserBoat> boats = FXCollections
|
|
.observableArrayList(this.visualiserRace.getVisualiserRaceState().getBoats());
|
|
SortedList<VisualiserBoat> sortedBoats = new SortedList<>(boats);
|
|
sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty());
|
|
|
|
// update list when boat information changes
|
|
this.visualiserRace.getVisualiserRaceState().getBoats().addListener(
|
|
(ListChangeListener.Change<? extends VisualiserBoat> c) -> Platform.runLater(() -> {
|
|
boats.setAll(this.visualiserRace.getVisualiserRaceState().getBoats());
|
|
}));
|
|
|
|
// set table data
|
|
boatInfoTable.setItems(sortedBoats);
|
|
boatTeamColumn.setCellValueFactory(
|
|
cellData -> cellData.getValue().nameProperty());
|
|
boatSpeedColumn.setCellValueFactory(
|
|
cellData -> cellData.getValue().currentSpeedProperty());
|
|
boatMarkColumn.setCellValueFactory(
|
|
cellData -> cellData.getValue().legProperty());
|
|
boatPlacingColumn.setCellValueFactory(
|
|
cellData -> cellData.getValue().placingProperty());
|
|
|
|
//Kind of ugly, but allows for formatting an observed speed.
|
|
boatSpeedColumn.setCellFactory(
|
|
new Callback<TableColumn<VisualiserBoat, Number>, TableCell<VisualiserBoat, Number>>() {
|
|
@Override
|
|
public TableCell<VisualiserBoat, Number> call(TableColumn<VisualiserBoat, Number> param) {
|
|
//We return a table cell that populates itself with a Number, and formats it.
|
|
return new TableCell<VisualiserBoat, Number>(){
|
|
|
|
//Function to update the cell text.
|
|
@Override
|
|
protected void updateItem(Number item, boolean empty) {
|
|
if (item != null) {
|
|
super.updateItem(item, empty);
|
|
setText(String.format("%.2fkn", item.doubleValue()));
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
//Kind of ugly, but allows for turning an observed Leg into a string.
|
|
boatMarkColumn.setCellFactory(
|
|
new Callback<TableColumn<VisualiserBoat, Leg>, TableCell<VisualiserBoat, Leg>>() {
|
|
@Override
|
|
public TableCell<VisualiserBoat, Leg> call(TableColumn<VisualiserBoat, Leg> param) {
|
|
//We return a table cell that populates itself with a Leg's name.
|
|
return new TableCell<VisualiserBoat, Leg>(){
|
|
|
|
//Function to update the cell text.
|
|
@Override
|
|
protected void updateItem(Leg item, boolean empty) {
|
|
if (item != null) {
|
|
super.updateItem(item, empty);
|
|
setText(item.getName());
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialises the race clock to listen to the specified race.
|
|
*/
|
|
private void initialiseRaceClock() {
|
|
raceState.getRaceClock().durationProperty().addListener((observable,
|
|
oldValue, newValue) -> {
|
|
Platform.runLater(() -> {
|
|
timer.setText(newValue);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Transition from the race view to the finish view.
|
|
* @throws IOException Thrown if the finish scene cannot be loaded.
|
|
*/
|
|
private void finishRace() throws IOException {
|
|
RaceFinishController fc =
|
|
(RaceFinishController)loadScene("raceFinish.fxml");
|
|
fc.loadFinish(raceState.getBoats());
|
|
}
|
|
|
|
/**
|
|
* Timer which monitors the race.
|
|
*/
|
|
private void raceTimer() {
|
|
new AnimationTimer() {
|
|
@Override
|
|
public void handle(long arg0) {
|
|
//If the race has finished, go to finish view.
|
|
if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
|
|
stop(); // stop the timer
|
|
try {
|
|
finishRace();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
} else {
|
|
boatInfoTable.sort();
|
|
}
|
|
|
|
//Return to main screen if we lose connection.
|
|
if (!visualiserRace.getServerConnection().isAlive()) {
|
|
try {
|
|
loadTitleScreen();
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
//TODO we should display an error to the user
|
|
//TODO also need to "reset" any state (race, connections, etc...).
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
/**
|
|
* toggles if the info table is shown
|
|
*/
|
|
private void toggleTable() {
|
|
infoWrapper.setVisible(infoTableShow);
|
|
boatInfoTable.refresh();
|
|
infoTableShow = !infoTableShow;
|
|
}
|
|
|
|
/**
|
|
* Get the next tutorial state
|
|
*/
|
|
private void updateTutorialState(){
|
|
//Next tutorial state is popped from list
|
|
currentState = tutorialStates.get(0);
|
|
tutorialStates.remove(0);
|
|
}
|
|
|
|
/**
|
|
* Search key map for key given string of command
|
|
* @param command the command of the key
|
|
*/
|
|
private void searchMapForKey(String command){
|
|
//For loop through keyFactory
|
|
for (Map.Entry<String, ControlKey> entry: keyFactory.getKeyState().entrySet()){
|
|
if(entry.getValue().toString().equals(command)){
|
|
|
|
//Found next key required to press
|
|
keyToPress = entry.getKey();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates tutorial state and gui display for tutorial text
|
|
* @throws Exception Exception thrown
|
|
*/
|
|
private void checkTutorialState() throws Exception {
|
|
//Switch statement to check what the current tutorial state is
|
|
switch (currentState){
|
|
case UPWIND:
|
|
//Set next key to press as the downwind key
|
|
searchMapForKey("Downwind");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! To turn downwind press " + keyToPress + ".");
|
|
updateTutorialState();
|
|
break;
|
|
case DOWNWIND:
|
|
//Set next key to press as the tack/gybe key
|
|
searchMapForKey("Tack/Gybe");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! To tack or gybe press " + keyToPress + ".");
|
|
updateTutorialState();
|
|
break;
|
|
case TACKGYBE:
|
|
//Set next key to press as the VMG key
|
|
searchMapForKey("VMG");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! To use VMG press " + keyToPress + ". This will turn the boat.");
|
|
updateTutorialState();
|
|
break;
|
|
case VMG:
|
|
//Set next key to press as the sails-in key
|
|
searchMapForKey("Toggle Sails");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! To sails in press " + keyToPress + ". This will stop the boat.");
|
|
updateTutorialState();
|
|
break;
|
|
case SAILSIN:
|
|
//Set next key to press as the sails-out key
|
|
searchMapForKey("Toggle Sails");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! To sails out press " + keyToPress + " again. The will start moving again.");
|
|
updateTutorialState();
|
|
break;
|
|
case SAILSOUT:
|
|
//Set next key to press as the zoom-in key
|
|
searchMapForKey("Zoom In");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! To zoom in press " + keyToPress + ".");
|
|
updateTutorialState();
|
|
break;
|
|
case ZOOMIN:
|
|
//Set next key to press as the zoom-out key
|
|
searchMapForKey("Zoom Out");
|
|
//Update tutorial text
|
|
tutorialText.setText("Nice! You will also be able to zoom into boats and marks by clicking them. \nTo zoom out press " + keyToPress + ".");
|
|
updateTutorialState();
|
|
break;
|
|
case ZOOMOUT:
|
|
//Finished tutorial. Display pop-up for exiting the tutorial
|
|
tutorialText.setText("Congratulations! You're done!");
|
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
|
alert.setTitle("Finished Tutorial");
|
|
alert.setHeaderText("You have finished the tutorial.");
|
|
alert.setContentText("Now you know the controls you are ready to race!");
|
|
Optional<ButtonType> result = alert.showAndWait();
|
|
if (result.get() == ButtonType.OK) {
|
|
App.game.endEvent();
|
|
loadTitleScreen();
|
|
}
|
|
break;
|
|
default:
|
|
//State not found. Exit tutorial to title menu
|
|
App.game.endEvent();
|
|
loadTitleScreen();
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|