diff --git a/dedicatedServer/src/main/java/app/App.java b/dedicatedServer/src/main/java/app/App.java index 3b36c43e..b2228131 100644 --- a/dedicatedServer/src/main/java/app/App.java +++ b/dedicatedServer/src/main/java/app/App.java @@ -21,7 +21,7 @@ public class App extends Application { public void start(Stage primaryStage) { try { //TODO should read a configuration file to configure server? - Event raceEvent = new Event(false, 0); + Event raceEvent = new Event(false, 0, 5); } catch (Exception e) { diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 602aed43..11553a07 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -40,24 +40,17 @@ public class Event { private String raceXML; private String regattaXML; private String boatXML; - private XMLFileType xmlFileType; - private Polars boatPolars; - /** * Data sources containing data from the xml files. */ - RaceDataSource raceDataSource; - BoatDataSource boatDataSource; - RegattaDataSource regattaDataSource; - - + private RaceDataSource raceDataSource; + private BoatDataSource boatDataSource; + private RegattaDataSource regattaDataSource; private ConnectionAcceptor connectionAcceptor; private LatestMessages latestMessages; - private CompositeCommand compositeCommand; - /** * This is used to allocate source IDs. */ @@ -66,22 +59,18 @@ public class Event { private RaceLogic raceLogic; private Thread raceThread; - private Thread connectionThread; - private int mapIndex; - - - - /** * Constructs an event, using various XML files. * @param singlePlayer Whether or not to create a single player event. * @param mapIndex Specifies which map to use. + * @param raceLength The length of the race, in milliseconds. * @throws EventConstructionException Thrown if we cannot create an Event for any reason. */ - public Event(boolean singlePlayer, int mapIndex) throws EventConstructionException { + public Event(boolean singlePlayer, int mapIndex, int raceLength) throws + EventConstructionException { PolarParser.parseNewPolars("mock/polars/acc_polars.csv"); this.mapIndex = mapIndex; String raceXMLFile; @@ -125,8 +114,8 @@ public class Event { } else { this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8)); this.raceXML = RaceXMLCreator.alterRaceToWind(this.raceXML, XMLFileType.Contents, windAngle); - this.raceXML = RaceXMLCreator.scaleRaceSize(raceXML, windSpeed, 15 * 60 * 1000); - + this.raceXML = RaceXMLCreator.scaleRaceSize(raceXML, + windSpeed, raceLength); } this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8); diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index a5dc98fe..5bf55d50 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -27,7 +27,7 @@ public class MockBoat extends Boat { * 1: passed only first check * 2: passed first and second check */ - private Integer roundingStatus = 0; + private int roundingStatus = 0; /** * Stores whether the boat is on autoVMG or not @@ -284,8 +284,8 @@ public class MockBoat extends Boat { (this.isStarboardSide(mark1) && this.isPortSide(mark2)); } - public Integer getRoundingStatus() { - return Integer.valueOf(roundingStatus); + public int getRoundingStatus() { + return roundingStatus; } public void increaseRoundingStatus() { diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 43945a46..a0140189 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -419,6 +419,11 @@ public class MockRace extends RaceState { boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds); } + // Remove one unit of health for every frame spent outside boundary + if(!finish && !GPSCoordinate.isInsideBoundary(boat.getPosition(), getBoundary())) { + boat.updateHealth(-0.1); + } + this.updateEstimatedTime(boat); } @@ -430,7 +435,7 @@ public class MockRace extends RaceState { boat.getBearing() ), boat.getBearing()) ; if (vmg.getSpeed() > 0) { - boat.setCurrentSpeed(vmg.getSpeed()); + boat.setCurrentSpeed(vmg.getSpeed() * Math.pow(boat.getHealth() / 100, 0.3)); } } @@ -677,7 +682,7 @@ public class MockRace extends RaceState { for (MockBoat boat : this.boats) { //If the boat is currently racing, count it. - if (boat.getStatus() == BoatStatusEnum.RACING) { + if (boat.getStatus() == BoatStatusEnum.RACING && boat.getHealth()>=1) { numberOfActiveBoats++; } diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 8814733a..4b974850 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -163,7 +163,7 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { //As long as there is at least one boat racing, we still simulate the race. if (race.getNumberOfActiveBoats() != 0) { - + //System.out.println(race.getNumberOfActiveBoats()); //Get the time period of this frame. long framePeriod = currentTime - previousFrameTime; @@ -175,12 +175,9 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli()); if(race.getColliderRegistry().rayCast(boat)){ - //System.out.println("Collision!"); //Add boat to list collisionBoats.add(boat); } - - //System.out.println(race.getColliderRegistry().rayCast(boat)); } diff --git a/racevisionGame/src/main/java/mock/model/RaceServer.java b/racevisionGame/src/main/java/mock/model/RaceServer.java index 103e4d66..52c4448c 100644 --- a/racevisionGame/src/main/java/mock/model/RaceServer.java +++ b/racevisionGame/src/main/java/mock/model/RaceServer.java @@ -64,6 +64,9 @@ public class RaceServer { //Parse the boat locations. snapshotMessages.addAll(parseBoatLocations()); + //Parse the boat states + snapshotMessages.addAll(parseBoatStates()); + //Parse the marks. snapshotMessages.addAll(parseMarks()); @@ -243,7 +246,29 @@ public class RaceServer { } + /** + * Generates BoatState messages for every boat in the race + * @return list of BoatState messages + */ + private List parseBoatStates() { + List boatStates = new ArrayList<>(); + for(MockBoat boat: race.getBoats()) { + boatStates.add(parseIndividualBoatState(boat)); + } + return boatStates; + } + /** + * Creates a BoatState message for the current state of the given boat + * @param boat to generate message for + * @return BoatState message + */ + private BoatState parseIndividualBoatState(MockBoat boat) { + return new BoatState( + boat.getSourceID(), + (int)boat.getHealth() + ); + } /** * Parses the race status, and returns it. diff --git a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java index af520a83..af7fbe45 100644 --- a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java +++ b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java @@ -21,6 +21,8 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; /** * Helper Class for creating a Race XML @@ -175,10 +177,10 @@ public class RaceXMLCreator { } /** - * gets the destance between two marks + * gets the destance between two marks, in nautical miles. * @param a mark 1 * @param b mark 2 - * @return + * @return Distance in nautical miles. */ private static double getDistance(XMLMark a, XMLMark b){ GPSCoordinate coorda = new GPSCoordinate(a.getTargetLat(), a.getTargetLng()); @@ -212,12 +214,16 @@ public class RaceXMLCreator { double averageSpeed = (bestDownWindSpeed + bestUpWindSpeed) / 2; double raceApproximateTime = getRaceLength(race, averageSpeed); double scale = milliseconds / raceApproximateTime; + Map hasBeenScaled = new HashMap<>(); for (XMLCorner cm: race.getCompoundMarkSequence().getCorner()){ int index = cm.getCompoundMarkID() - 1; XMLCompoundMark mark = race.getCourse().getCompoundMark().get(index); - for (XMLMark m: mark.getMark()){ - scalePoint(m, center, scale); + if (!hasBeenScaled.containsKey(mark)) { + for (XMLMark m : mark.getMark()) { + scalePoint(m, center, scale); + } } + hasBeenScaled.put(mark, true); } for (XMLLimit limit: race.getCourseLimit().getLimit()){ scalePoint(limit, center, scale); @@ -319,8 +325,13 @@ public class RaceXMLCreator { } - - + /** + * sets the current race time of the xml + * @param raceXML race xml to alter + * @param racePrestartTime prestart time + * @param racePreparatoryTime preparatory time + * @deprecated this should be used from the RaceXMLCreator not from this function + */ public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML, long racePrestartTime, long racePreparatoryTime){ //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. long millisecondsToAdd = racePrestartTime + racePreparatoryTime; diff --git a/racevisionGame/src/main/java/network/MessageDecoders/BoatStateDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/BoatStateDecoder.java new file mode 100644 index 00000000..e543f464 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/BoatStateDecoder.java @@ -0,0 +1,38 @@ +package network.MessageDecoders; + +import network.Exceptions.InvalidMessageException; +import network.Messages.AC35Data; +import network.Messages.BoatState; + +import java.util.Arrays; + +import static network.Utils.ByteConverter.bytesToInt; + +/** + * Decoder for {@link BoatState} messages + */ +public class BoatStateDecoder implements MessageDecoder { + /** + * Decoded BoatState message + */ + private BoatState message; + + @Override + public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException { + byte[] sourceID = Arrays.copyOfRange(encodedMessage, 0, 4); + byte boatHealth = encodedMessage[4]; + + // Unpack bytes into BoatState + this.message = new BoatState( + bytesToInt(sourceID), + boatHealth + ); + + // Return BoatState + return this.message; + } + + public BoatState getMessage() { + return message; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java b/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java index 24caf97c..3b8964c0 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java @@ -59,6 +59,7 @@ public class DecoderFactory { case BOATACTION: return new BoatActionDecoder(); + case BOATSTATE: return new BoatStateDecoder(); default: throw new InvalidMessageTypeException("Unrecognised message type: " + type); } diff --git a/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java index c20c653f..ed8ba21f 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java @@ -18,6 +18,5 @@ public interface MessageDecoder { * @return The decoded message. * @throws InvalidMessageException Thrown if the encoded message is invalid in some way, or cannot be decoded. */ - public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException; - + AC35Data decode(byte[] encodedMessage) throws InvalidMessageException; } diff --git a/racevisionGame/src/main/java/network/MessageEncoders/BoatStateEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/BoatStateEncoder.java new file mode 100644 index 00000000..e897f18c --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageEncoders/BoatStateEncoder.java @@ -0,0 +1,32 @@ +package network.MessageEncoders; + +import network.Exceptions.InvalidMessageException; +import network.Messages.AC35Data; +import network.Messages.BoatState; + +import java.nio.ByteBuffer; + +import static network.Utils.ByteConverter.intToBytes; + +/** + * Encoder for {@link BoatState} message + */ +public class BoatStateEncoder implements MessageEncoder { + @Override + public byte[] encode(AC35Data message) throws InvalidMessageException { + // Downcast message + BoatState boatState = (BoatState)message; + + //Serialise message + byte[] sourceID = intToBytes(boatState.getSourceID()); + byte boatHealth = (byte)boatState.getBoatHealth(); + + // Pack bytes into string + ByteBuffer boatStateMessage = ByteBuffer.allocate(5); + boatStateMessage.put(sourceID); + boatStateMessage.put(boatHealth); + + // Return byte string + return boatStateMessage.array(); + } +} diff --git a/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java b/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java index d274f435..42093111 100644 --- a/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java +++ b/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java @@ -59,6 +59,7 @@ public class EncoderFactory { case BOATACTION: return new BoatActionEncoder(); + case BOATSTATE: return new BoatStateEncoder(); default: throw new InvalidMessageTypeException("Unrecognised message type: " + type); } diff --git a/racevisionGame/src/main/java/network/Messages/BoatState.java b/racevisionGame/src/main/java/network/Messages/BoatState.java new file mode 100644 index 00000000..f40a5757 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/BoatState.java @@ -0,0 +1,31 @@ +package network.Messages; + +import network.Messages.Enums.MessageType; + +/** + * Represents the information in a BoatState message according to protocol meeting + */ +public class BoatState extends AC35Data { + /** + * Source ID of boat described in message + */ + private int sourceID; + /** + * Health between 0-100 of boat with above source ID + */ + private int boatHealth; + + public BoatState(int sourceID, int boatHealth) { + super(MessageType.BOATSTATE); + this.sourceID = sourceID; + this.boatHealth = boatHealth; + } + + public int getSourceID() { + return sourceID; + } + + public int getBoatHealth() { + return boatHealth; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java index 0941fd08..15f9e80e 100644 --- a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java +++ b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java @@ -43,6 +43,8 @@ public enum MessageType { HOSTED_GAMES_REQUEST(109), + BOATSTATE(103), + NOTAMESSAGE(0); @@ -56,7 +58,7 @@ public enum MessageType { * Creates a MessageType enum from a given primitive integer value, cast to a byte. * @param value Integer, which is cast to byte, to construct from. */ - private MessageType(int value) { + MessageType(int value) { this.value = (byte)value; } diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index edbba025..99871678 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -103,6 +103,11 @@ public class Boat extends Collider { */ private boolean isColliding = false; + /** + * Amount of health boat currently has, between 0 and 100 + */ + private double health; + /** * Constructs a boat object with a given sourceID, name, country/team abbreviation, and polars table. * @@ -113,6 +118,7 @@ public class Boat extends Collider { public Boat(int sourceID, String name, String country) { this.sourceID = sourceID; + this.health = 100; this.setName(name); this.setCountry(country); @@ -419,6 +425,8 @@ public class Boat extends Collider { @Override public void onCollisionEnter(Collision e) { if(e.getBearing().degrees() > 270 || e.getBearing().degrees() < 90) { + // Deplete health + e.getBoat().updateHealth(-5); // Notify observers of collision this.setChanged(); notifyObservers(e); @@ -432,4 +440,22 @@ public class Boat extends Collider { public void setColliding(boolean colliding) { isColliding = colliding; } + + public double getHealth() { + return health; + } + + public void setHealth(double health) { + this.health = health; + } + + /** + * Add a given amount of HP to boat health + * @param delta amount of HP to add + */ + public void updateHealth(double delta) { + health += delta; + if(health < 0) health = 0; + else if(health > 100) health = 100; + } } diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java index fd46112f..83ee1168 100644 --- a/racevisionGame/src/main/java/shared/model/Mark.java +++ b/racevisionGame/src/main/java/shared/model/Mark.java @@ -97,6 +97,7 @@ public class Mark extends Collider{ @Override public void onCollisionEnter(Collision e) { + e.getBoat().updateHealth(-10); this.setChanged(); notifyObservers(e); } diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatStateCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatStateCommand.java new file mode 100644 index 00000000..85a1e00e --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatStateCommand.java @@ -0,0 +1,30 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.BoatState; +import shared.exceptions.BoatNotFoundException; +import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRaceState; + +/** + * Updates boats on visualiser when their health changes + */ +public class BoatStateCommand implements Command { + private BoatState boatState; + private VisualiserRaceState visualiserRace; + + public BoatStateCommand(BoatState boatState, VisualiserRaceState visualiserRace) { + this.boatState = boatState; + this.visualiserRace = visualiserRace; + } + + @Override + public void execute() { + try { + VisualiserBoat boat = visualiserRace.getBoat(boatState.getSourceID()); + boat.setHealth(boatState.getBoatHealth()); + } catch (BoatNotFoundException e) { + // Fail silently + } + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java index 0e2f3d15..825d3397 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java @@ -24,7 +24,6 @@ public class VisualiserRaceCommandFactory { switch (message.getType()) { case BOATLOCATION: - //System.out.println("Boat location received"); return new BoatLocationCommand((BoatLocation) message, visualiserRace); case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace); @@ -36,7 +35,8 @@ public class VisualiserRaceCommandFactory { case YACHTEVENTCODE: return new BoatCollisionCommand((YachtEvent) message, visualiserRace); - + case BOATSTATE: + return new BoatStateCommand((BoatState) message, visualiserRace); default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType()); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostGameController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostGameController.java index 33583a38..dac62be0 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostGameController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostGameController.java @@ -4,6 +4,8 @@ import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; +import javafx.scene.control.Slider; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import mock.app.Event; @@ -28,8 +30,12 @@ import java.util.logging.Logger; */ public class HostGameController extends Controller { private @FXML ImageView mapImage; + private @FXML Slider sliderLength; + private @FXML Label lblLength; private ArrayList listOfMaps; private int currentMapIndex = 0; + private int selectedRaceLength; // in minutes + private final int MAX_RACE_LENGTH = 30; // in minutes private DatagramSocket udpSocket; private MatchBrowserInterface matchBrowserInterface; @@ -37,8 +43,33 @@ public class HostGameController extends Controller { loadMaps(); this.udpSocket = MatchBrowserSingleton.getInstance().getUdpSocket(); this.matchBrowserInterface = MatchBrowserSingleton.getInstance().getMatchBrowserInterface(); + setRaceLengthSlider(); } + /** + * Sets up the values and display for a slider object which allows a user + * to select how many minutes long they would like their race to be. + */ + private void setRaceLengthSlider(){ + // set the listener to update the label + sliderLength.valueProperty().addListener((ov, old_val, new_val) -> { + selectedRaceLength = new_val.intValue(); + if (selectedRaceLength == 1){ + lblLength.setText(selectedRaceLength + " minute."); + } else { + lblLength.setText(selectedRaceLength + " minutes."); + } + }); + + // set values and marks to be displayed + sliderLength.setMin(2); + sliderLength.setMax(MAX_RACE_LENGTH); + sliderLength.setShowTickLabels(true); + sliderLength.setMajorTickUnit(MAX_RACE_LENGTH-1); + sliderLength.setBlockIncrement(1); + + sliderLength.getStylesheets().add("/css/slider.css"); + } /** @@ -64,7 +95,8 @@ public class HostGameController extends Controller { */ public void hostGamePressed() { try { - App.game = new Event(false, currentMapIndex); + App.game = new Event(false, currentMapIndex, + selectedRaceLength*60*1000); App.gameType = currentMapIndex; HttpMatchBrowserHost matchBrowserHost = new HttpMatchBrowserHost(); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java index 38816a0e..5953bf92 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java @@ -40,7 +40,7 @@ public class LobbyController extends Controller { private @FXML TextField addressFld; private @FXML TextField portFld; - private ObservableList connections; + private ObservableList allConnections; private ObservableList customConnections; private AudioClip sound; @@ -51,12 +51,20 @@ public class LobbyController extends Controller { public void initialize() { httpMatchBrowserClient = new HttpMatchBrowserClient(); + httpMatchBrowserClient.connections.addListener(new ListChangeListener() { + @Override + public void onChanged(Change c) { + refreshTable(); + } + }); + new Thread(httpMatchBrowserClient, "Match Client").start(); // set up the connection table customConnections = FXCollections.observableArrayList(); + allConnections = FXCollections.observableArrayList(); //connections.add(new RaceConnection("localhost", 4942, "Local Game")); - lobbyTable.setItems(httpMatchBrowserClient.connections); + lobbyTable.setItems(allConnections); gameNameColumn.setCellValueFactory(cellData -> cellData.getValue().gamenameProperty()); hostNameColumn.setCellValueFactory(cellData -> cellData.getValue().hostnameProperty()); statusColumn.setCellValueFactory(cellData -> cellData.getValue().statusProperty()); @@ -82,8 +90,14 @@ public class LobbyController extends Controller { public void refreshBtnPressed(){ sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm()); sound.play(); + refreshTable(); + } + + private void refreshTable() { + allConnections.clear(); + addCustomGames(); addServerGames(); - for(RaceConnection connection: connections) { + for(RaceConnection connection: allConnections) { connection.check(); } try { @@ -144,9 +158,9 @@ public class LobbyController extends Controller { try { int port = Integer.parseInt(portString); customConnections.add(new RaceConnection(hostName, port, "Boat Game")); - connections.addAll(customConnections); addressFld.clear(); portFld.clear(); + refreshTable(); } catch (NumberFormatException e) { System.err.println("Port number entered is not a number"); } @@ -173,17 +187,14 @@ public class LobbyController extends Controller { * Adds the games received from the server */ private void addServerGames() { - httpMatchBrowserClient.connections.addAll(customConnections); - httpMatchBrowserClient.connections.addListener(new ListChangeListener() { - @Override - public void onChanged(Change c) { - refreshBtnPressed(); - } - }); - + allConnections.addAll(httpMatchBrowserClient.connections); /* for (HostGame game : matchBrowserLobbyInterface.getGames()) { connections.add(new RaceConnection(game.getIp(), 4942, "Boat Game")); }*/ } + + private void addCustomGames() { + allConnections.addAll(customConnections); + } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/NextMarkController.java b/racevisionGame/src/main/java/visualiser/Controllers/NextMarkController.java new file mode 100644 index 00000000..20da6f3c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/NextMarkController.java @@ -0,0 +1,123 @@ +package visualiser.Controllers; + +import com.interactivemesh.jfx.importer.stl.StlMeshImporter; +import javafx.animation.AnimationTimer; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.geometry.Point3D; +import javafx.scene.AmbientLight; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.Cylinder; +import javafx.scene.shape.MeshView; +import javafx.scene.shape.Shape3D; +import javafx.scene.transform.Rotate; +import shared.model.Bearing; +import shared.model.CompoundMark; +import shared.model.GPSCoordinate; +import visualiser.layout.Annotation3D; +import visualiser.layout.Assets3D; +import visualiser.layout.Subject3D; +import visualiser.layout.View3D; +import visualiser.model.VisualiserBoat; + +import java.net.URL; +import java.util.Observable; +import java.util.Observer; + +public class NextMarkController { + private @FXML StackPane arrowStackPane2d; + private @FXML StackPane arrowStackPane3d; + private @FXML Pane pane2d; + private @FXML Pane pane3d; + + private VisualiserBoat boat; + + public void initialiseArrowView(VisualiserBoat boat) { + this.boat = boat; + pane2d.setVisible(true); + pane3d.setVisible(false); + initialise2dArrowView(); + initialise3dArrowView(); + } + + private void initialise2dArrowView() { + AnimationTimer arrow2d = new AnimationTimer() { + @Override + public void handle(long now) { + if (boat.getCurrentLeg().getEndCompoundMark() != null) { + CompoundMark target = boat.getCurrentLeg().getEndCompoundMark(); + Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate()); + arrowStackPane2d.setRotate(headingToMark.degrees()); + } else { + stop(); + } + } + }; + arrow2d.start(); + } + + private void initialise3dArrowView() { + ObservableList viewSubjects = FXCollections.observableArrayList(); + String arrowPath = "assets/mark_arrow.x3d"; + + Shape3D arrow = Assets3D.loadX3d(arrowPath); + + arrow.setScaleX(25); + arrow.setScaleY(25); + arrow.setScaleZ(100); + arrow.setRotationAxis(new Point3D(1,0,0)); + + arrowStackPane3d.getChildren().add(arrow); + + AnimationTimer arrow3d = new AnimationTimer() { + @Override + public void handle(long now) { + if (boat.getCurrentLeg().getEndCompoundMark() != null) { + arrow.getTransforms().clear(); + double zRotation = calculateZRotate(); + arrow.setRotate(calculateXRotate(zRotation)); + arrow.getTransforms().add(new Rotate(zRotation, new Point3D(0, 0, 1))); + } else { + stop(); + } + } + }; + arrow3d.start(); + } + + public void show2d() { + pane3d.setVisible(false); + pane2d.setVisible(true); + } + + public void show3d() { + pane2d.setVisible(false); + pane3d.setVisible(true); + } + + private double calculateZRotate() { + CompoundMark target = boat.getCurrentLeg().getEndCompoundMark(); + Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate()); + return -headingToMark.degrees() + boat.getBearing().degrees() + 180; + } + + private double calculateXRotate(double zRotation) { +// if (zRotation > 360) { +// zRotation -=360; +// } else if (zRotation < 0) { +// zRotation += 360; +// } +// +// if (zRotation > 180) { +// zRotation = 360 - zRotation; +// } + + return 70; + //return 90 - 20 * Math.cos(Math.toRadians(zRotation)); + } +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java index 937af7ef..67a9ecc5 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java @@ -15,19 +15,25 @@ import javafx.scene.AmbientLight; import javafx.scene.PointLight; import javafx.scene.chart.LineChart; import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; +import javafx.scene.media.AudioClip; import javafx.scene.paint.Color; import javafx.scene.paint.Material; import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.Cylinder; import javafx.scene.paint.Stop; import javafx.scene.shape.MeshView; import javafx.scene.shape.Shape3D; import javafx.scene.transform.Translate; import javafx.util.Callback; +import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RaceDataSource; import shared.enums.RoundingType; @@ -43,6 +49,7 @@ import visualiser.model.*; import visualiser.utils.GPSConverter; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.*; import java.util.logging.Level; @@ -72,6 +79,8 @@ public class RaceViewController extends Controller { private ResizableRaceCanvas raceCanvas; private boolean mapToggle = true; private GPSConverter gpsConverter; + private ArrayList healthEffectList = new ArrayList<>(); + /** * Arrow pointing to next mark in third person @@ -84,10 +93,12 @@ public class RaceViewController extends Controller { // note: it says it's not used but it is! do not remove :) private @FXML ArrowController arrowController; + private @FXML NextMarkController nextMarkController; private @FXML GridPane canvasBase; private @FXML GridPane canvasBase1; private @FXML SplitPane racePane; private @FXML StackPane arrowPane; + private @FXML Pane nextMarkPane; private @FXML Label timer; private @FXML Label FPS; private @FXML Label timeZone; @@ -97,12 +108,17 @@ public class RaceViewController extends Controller { private @FXML TableColumn boatTeamColumn; private @FXML TableColumn boatMarkColumn; private @FXML TableColumn boatSpeedColumn; + private @FXML TableColumn boatHealthColumn; private @FXML LineChart sparklineChart; private @FXML Label tutorialText; private @FXML AnchorPane infoWrapper; private @FXML AnchorPane lineChartWrapper; private @FXML StackPane speedPane; private @FXML AnchorPane raceAnchorPane; + private @FXML GridPane playerHealthContainer; + private @FXML ImageView imageView; + private @FXML AnchorPane deathTransPane; + private @FXML StackPane deathPane; /** * Displays a specified race. @@ -117,11 +133,13 @@ public class RaceViewController extends Controller { this.controllerClient = controllerClient; this.isHost = isHost; keyFactory.load(); - + deathPane.setDisable(false); + deathPane.setVisible(false); tutorialCheck(); initKeypressHandler(); initialiseRaceVisuals(); initialiseRaceCanvas(); + healthLoop(); } /** @@ -133,7 +151,6 @@ public class RaceViewController extends Controller { isTutorial = true; tutorialText.setVisible(true); tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values())); - currentState = tutorialStates.get(0); tutorialStates.remove(0); searchMapForKey("Upwind"); @@ -290,33 +307,47 @@ public class RaceViewController extends Controller { * 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); + initialiseHealthPane(); initialiseRaceClock(); initialiseSpeedometer(); raceTimer(); // start the timer + nextMarkPane.toFront(); speedometerLoop(); new Sparkline(this.raceState, this.sparklineChart); timeZone.setText(this.raceState.getRaceClock().getTimeZone()); arrowController.setWindProperty(this.raceState.windProperty()); } + + + private void initialiseHealthPane() { + InputStream tomato = this.getClass().getClassLoader().getResourceAsStream("visualiser/images/tomato.png"); + HealthSlider healthSlider = new HealthSlider(new Image(tomato)); + playerHealthContainer.add(healthSlider, 0, 0); + + try { + VisualiserBoat player = raceState.getBoat(raceState.getPlayerBoatID()); + player.healthProperty().addListener((o, prev, curr) -> { + healthSlider.setCrop((double)curr/100.0); + }); + } catch (BoatNotFoundException e) { + e.printStackTrace(); + } + } + private void initialiseView3D(VisualiserRaceEvent race) { viewSubjects = FXCollections.observableArrayList(); + try { + nextMarkController.initialiseArrowView(race.getVisualiserRaceState().getBoat(race.getVisualiserRaceState().getPlayerBoatID())); + } catch (BoatNotFoundException e) { + e.printStackTrace(); + } + AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF")); ambientLight.setTranslateX(250); ambientLight.setTranslateZ(210); @@ -406,11 +437,29 @@ public class RaceViewController extends Controller { viewSubjects.add(markModel); } - for (VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) { + // Position and add each boat to view + for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) { + Shape3D mesh = Assets3D.getBoat(); + + PhongMaterial boatColorMat = new PhongMaterial(boat.getColor()); + //mesh.setMaterial(boatColorMat); + Subject3D boatModel = new Subject3D(mesh, boat.getSourceID()); + + // Configure visualiser for client's boat if (boat.isClientBoat()) { + // Add player boat highlight Shockwave boatHighlight = new Shockwave(10); boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1))); viewSubjects.add(boatHighlight); + + // Track player boat with camera + viewSubjects.add(boatModel); + Platform.runLater(() -> { + view3D.trackSubject(boatModel); + view3D.setThirdPerson(); + }); + + // Track player boat with highlight AnimationTimer highlightTrack = new AnimationTimer() { @Override public void handle(long now) { @@ -419,28 +468,28 @@ public class RaceViewController extends Controller { } }; highlightTrack.start(); - } - } - // 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()); -// } - Shape3D mesh = Assets3D.getBoat(); - - PhongMaterial boatColorMat = new PhongMaterial(boat.getColor()); - //mesh.setMaterial(boatColorMat); - Subject3D boatModel = new Subject3D(mesh, boat.getSourceID()); + // Highlight next mark only for player boat + 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); + } + boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr))); + } else { + viewSubjects.add(boatModel); + } - viewSubjects.add(boatModel); + //Create health effect + HealthEffect healthEffect = new HealthEffect(boat.getSourceID(), System.currentTimeMillis()); + viewSubjects.add(healthEffect); + healthEffectList.add(healthEffect); //add sail Sails3D sails3D = new Sails3D(); Subject3D sailsSubject = new Subject3D(sails3D, 0); + sails3D.setMouseTransparent(true); sails3D.setMaterial(boatColorMat); sailsSubject.setXRot(0d); sailsSubject.setHeading(visualiserRace.getVisualiserRaceState().getWindDirection().degrees()); @@ -517,6 +566,14 @@ public class RaceViewController extends Controller { boatModel.setHeading(boat.getBearing().degrees()); boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); + boatModel.getMesh().toFront(); + + //Fire follows boat + healthEffect.setHeading(boat.getBearing().degrees()); + healthEffect.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); + healthEffect.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); + healthEffect.setY(0); + } }; trackBoat.start(); @@ -543,7 +600,6 @@ public class RaceViewController extends Controller { }*/ Subject3D shockwave = new Shockwave(10); - viewSubjects.add(shockwave); //boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr))); @@ -632,37 +688,32 @@ public class RaceViewController extends Controller { } 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(); - if (target != null) { - 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(); + nextMarkController.show3d(); +// 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(); - } - + nextMarkController.show2d(); } /** @@ -789,13 +840,17 @@ public class RaceViewController extends Controller { // set table data boatInfoTable.setItems(sortedBoats); boatTeamColumn.setCellValueFactory( - cellData -> cellData.getValue().nameProperty()); + cellData -> cellData.getValue().nameProperty() + ); boatSpeedColumn.setCellValueFactory( - cellData -> cellData.getValue().currentSpeedProperty()); + cellData -> cellData.getValue().currentSpeedProperty() + ); boatMarkColumn.setCellValueFactory( - cellData -> cellData.getValue().legProperty()); - boatPlacingColumn.setCellValueFactory( - cellData -> cellData.getValue().placingProperty()); + cellData -> cellData.getValue().legProperty() + ); + boatHealthColumn.setCellValueFactory( + cellData -> cellData.getValue().healthProperty() + ); //Kind of ugly, but allows for formatting an observed speed. boatSpeedColumn.setCellFactory( @@ -927,6 +982,64 @@ public class RaceViewController extends Controller { }.start(); } + /** + * Animation timer for health + */ + private void healthLoop(){ + new AnimationTimer(){ + @Override + public void handle(long arg0){ + if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) { + stop(); // stop the timer + } else { + try { + //Check if boat is dead + if(raceState.getBoat(raceState.getPlayerBoatID()).getHealth()<=0){ + if(!deathPane.isDisable()) { + deathPane.setVisible(true); + } + } + } catch (BoatNotFoundException e) { + e.printStackTrace(); + } + + for(VisualiserBoat boat : raceState.getBoats()){ + for (HealthEffect fp : healthEffectList){ + + if(fp.getSourceID()==boat.getSourceID()){ + if(boat.getHealth()<=0){ + //Boat is dead. Don't check it anymore for hp + fp.displayDeath(fp.getSourceID()==raceState.getPlayerBoatID()); + fp.setSourceID(0); + try { + raceState.getBoat(boat.getSourceID()).setStatus(BoatStatusEnum.DNF); + + raceState.updateBoatPositions(raceState.getBoats()); + } catch (BoatNotFoundException e) { + e.printStackTrace(); + } + } + else + //Speed up tick when <=10 hp + if(boat.getHealth()<=10){ + fp.flash(System.currentTimeMillis(), 300, boat.getSourceID()==raceState.getPlayerBoatID()); + } + else + //Visual indication of low hp + if(boat.getHealth()<=20) { + //fp.setVisible(true); + fp.flash(System.currentTimeMillis(), 500, boat.getSourceID()==raceState.getPlayerBoatID()); + } else { + fp.setVisible(false); + } + break; + } + } + } + } + } + }.start(); + } /** * toggles if the info table is shown @@ -1082,4 +1195,12 @@ public class RaceViewController extends Controller { mapToggle = !mapToggle; } + /** + * FXML method for death button + */ + public void deathOKPressed(){ + deathPane.setDisable(true); + deathPane.setVisible(false); + } + } diff --git a/racevisionGame/src/main/java/visualiser/layout/Assets3D.java b/racevisionGame/src/main/java/visualiser/layout/Assets3D.java index 70d791c0..152d90f3 100644 --- a/racevisionGame/src/main/java/visualiser/layout/Assets3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/Assets3D.java @@ -104,7 +104,7 @@ public class Assets3D { windArrow = new Annotation3D(loadX3d(arrowPath)); } - private static Shape3D loadX3d(String path){ + public static Shape3D loadX3d(String path){ X3dModelImporter x3dModelImporter = new X3dModelImporter(); URL asset = Assets3D.class.getClassLoader().getResource(path); x3dModelImporter.read(asset); diff --git a/racevisionGame/src/main/java/visualiser/layout/HealthEffect.java b/racevisionGame/src/main/java/visualiser/layout/HealthEffect.java new file mode 100644 index 00000000..b3b7d70b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/HealthEffect.java @@ -0,0 +1,111 @@ +package visualiser.layout; + +import javafx.geometry.Point3D; +import javafx.scene.image.Image; +import javafx.scene.media.AudioClip; +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.*; +import javafx.scene.transform.Rotate; + +/** + * Created by zwu18 on 24/09/17. + */ +public class HealthEffect extends Subject3D { + + private int sourceID; + private long currentTime; + private AudioClip warningSound = new AudioClip(this.getClass().getResource("/visualiser/sounds/warning.mp3").toExternalForm()); + private AudioClip deadSound = new AudioClip(this.getClass().getResource("/visualiser/sounds/dead1.wav").toExternalForm()); + + public HealthEffect(int sourceID, long currentTime){ + super(createEffect(), 0); + this.sourceID = sourceID; + this.currentTime = currentTime; + } + + /** + * Initialise the mesh view with image + * @return Mesh view + */ + private static Shape3D createEffect(){ + + Image image = new Image(HealthEffect.class.getClassLoader().getResourceAsStream("images/warning.png")); + + Plane3D plane = new Plane3D(20, 20, 10, 10); + + PhongMaterial material = new PhongMaterial(); + material.setDiffuseColor(Color.web("#FFFFFF")); + material.setSpecularColor(Color.web("#000000")); + material.setDiffuseMap(image); + + MeshView imageSurface = new MeshView(plane); + + + imageSurface.setMaterial(material); + imageSurface.setMouseTransparent(true); + //imageSurface.toFront(); this.flashInterval = flashInterval; + + return imageSurface; + } + + public void rotateView(Double angle, Double pivotX, Double pivotY, Double pivotZ, Point3D axis){ + Rotate rotate = new Rotate(angle, axis); + rotate.setPivotX(pivotX); + rotate.setPivotY(pivotY); + rotate.setPivotZ(pivotZ); + this.getMesh().getTransforms().add(rotate); + } + + public void setVisible(boolean bool){ + this.getMesh().setVisible(bool); + } + + + public int getSourceID(){ + return sourceID; + } + + public void setSourceID(int id){ + this.sourceID = id; + } + + /** + * Display visual indication when boat dies + * @param player boolean if player is current user or not + */ + public void displayDeath(boolean player){ + Image image = new Image(HealthEffect.class.getClassLoader().getResourceAsStream("images/warning2.png")); + PhongMaterial material = (PhongMaterial) this.getMesh().getMaterial(); + material.setDiffuseColor(Color.web("#FFFFFF")); + material.setSpecularColor(Color.web("#000000")); + material.setDiffuseMap(image); + this.getMesh().setMaterial(material); + if(player) { + deadSound.play(); + } + } + + + /** + * Flash the mesh view at a certain interval + * @param checkTime The current time of flash + * @param flashInterval Desired flash interval + * @param playerBoat Whether or not this effect is for the player's boat. + */ + public void flash(long checkTime, long flashInterval, boolean playerBoat){ + if(checkTime >= (currentTime+flashInterval)){ + this.currentTime = checkTime; + if(this.getMesh().isVisible()){ + this.setVisible(false); + } else { + if(playerBoat) { + warningSound.setVolume(0.1); + warningSound.play(); + } + this.setVisible(true); + } + } + } + +} diff --git a/racevisionGame/src/main/java/visualiser/layout/HealthSlider.java b/racevisionGame/src/main/java/visualiser/layout/HealthSlider.java new file mode 100644 index 00000000..7f55607a --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/HealthSlider.java @@ -0,0 +1,66 @@ +package visualiser.layout; + +import javafx.scene.image.Image; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.*; +import javafx.scene.shape.Rectangle; + +import java.awt.image.BufferedImage; + +/** + * Created by connortaylorbrown on 21/09/17. + */ +public class HealthSlider extends Pane { + /** + * Image used to fill health slider + */ + private Image fillImage; + /** + * Size of background for image configuration + */ + private BackgroundSize backgroundSize; + /** + * Percentage of image cropped out from top + */ + private double crop; + + public HealthSlider(Image fillImage) { + this.fillImage = fillImage; + this.crop = 1; + this.backgroundSize = new BackgroundSize( + 100, + 100, + true, + true, + true, + false); + drawSlider(); + } + + public void setCrop(double crop) { + this.crop = crop; + drawSlider(); + } + + private void drawSlider() { + int top = Math.max(0,(int)(fillImage.getHeight() - crop * fillImage.getHeight())); + + WritableImage croppedImage = new WritableImage( + fillImage.getPixelReader(), + 0, + top, + (int)fillImage.getWidth(), + (int)fillImage.getHeight() - top + ); + + BackgroundImage backgroundImage = new BackgroundImage( + croppedImage, + BackgroundRepeat.NO_REPEAT, + BackgroundRepeat.NO_REPEAT, + BackgroundPosition.CENTER, + backgroundSize + ); + + this.setBackground(new Background(backgroundImage)); + } +} diff --git a/racevisionGame/src/main/java/visualiser/layout/View3D.java b/racevisionGame/src/main/java/visualiser/layout/View3D.java index 7277ac5f..8501cdaa 100644 --- a/racevisionGame/src/main/java/visualiser/layout/View3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/View3D.java @@ -81,7 +81,7 @@ public class View3D extends Pane { /** * Distance to stop zoom */ - private final double ZOOM_IN_LIMIT = 30; + private final double ZOOM_IN_LIMIT = 3; private final double ZOOM_OUT_LIMIT = 700; private final double MAX_ZOOM_LIMIT = 1500; private final double MAX_PITCH = 60; // birds eye view diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index f5ce109b..4b580ff2 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -1,9 +1,6 @@ package visualiser.model; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.*; import javafx.scene.paint.Color; import network.Messages.Enums.BoatStatusEnum; import shared.model.*; @@ -65,6 +62,7 @@ public class VisualiserBoat extends Boat { private ObjectProperty positionProperty; private ObjectProperty bearingProperty; private BooleanProperty hasCollided; + private DoubleProperty healthProperty; /** @@ -78,6 +76,7 @@ public class VisualiserBoat extends Boat { this.color = color; this.hasCollided = new SimpleBooleanProperty(false); + this.healthProperty = new SimpleDoubleProperty(100); } @@ -284,4 +283,18 @@ public class VisualiserBoat extends Boat { public void setHasCollided(boolean hasCollided) { this.hasCollided.set(hasCollided); } + + public DoubleProperty healthProperty() { + return healthProperty; + } + + @Override + public double getHealth() { + return healthProperty.get(); + } + + @Override + public void setHealth(double healthProperty) { + this.healthProperty.set((int)healthProperty); + } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index 4a0316ee..d6c9c1d0 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -248,7 +248,6 @@ public class VisualiserRaceState extends RaceState { if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) { - boat.setPlacing("-"); } else { diff --git a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java index 8780a676..2e97f36d 100644 --- a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java +++ b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java @@ -297,6 +297,7 @@ public class ServerConnection implements RunnableWithFramePeriod { this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages); this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages); this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, incomingMessages); + this.messageRouter.addRoute(MessageType.BOATSTATE, incomingMessages); this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages. diff --git a/racevisionGame/src/main/resources/assets/mark_arrow.x3d b/racevisionGame/src/main/resources/assets/mark_arrow.x3d new file mode 100644 index 00000000..1b41caec --- /dev/null +++ b/racevisionGame/src/main/resources/assets/mark_arrow.x3d @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/assets/textures/Material Diffuse Color.002 b/racevisionGame/src/main/resources/assets/textures/Material Diffuse Color.002 new file mode 100644 index 00000000..0c5dbaeb Binary files /dev/null and b/racevisionGame/src/main/resources/assets/textures/Material Diffuse Color.002 differ diff --git a/racevisionGame/src/main/resources/css/slider.css b/racevisionGame/src/main/resources/css/slider.css new file mode 100644 index 00000000..2cb9832d --- /dev/null +++ b/racevisionGame/src/main/resources/css/slider.css @@ -0,0 +1,16 @@ +.slider .thumb { + -fx-background-image: url("/visualiser/images/sun.png"); + -fx-background-size: 25px; + -fx-pref-height: 25px; + -fx-pref-width: 25px; + -fx-background-position: top; + -fx-background-repeat: no-repeat; + -fx-focus-color: transparent; + -fx-background-color: transparent; +} + +.slider .track { + -fx-control-inner-background: dodgerblue; + -fx-border-color: rgba(30, 144, 255, 0.44); + -fx-border-radius: 0.25em, 0.25em, 0.166667em; +} \ No newline at end of file diff --git a/racevisionGame/src/main/resources/images/nextMarkArrow2d.png b/racevisionGame/src/main/resources/images/nextMarkArrow2d.png new file mode 100644 index 00000000..ed9506c4 Binary files /dev/null and b/racevisionGame/src/main/resources/images/nextMarkArrow2d.png differ diff --git a/racevisionGame/src/main/resources/images/warning.png b/racevisionGame/src/main/resources/images/warning.png new file mode 100644 index 00000000..fb640948 Binary files /dev/null and b/racevisionGame/src/main/resources/images/warning.png differ diff --git a/racevisionGame/src/main/resources/images/warning2.png b/racevisionGame/src/main/resources/images/warning2.png new file mode 100644 index 00000000..d8920c34 Binary files /dev/null and b/racevisionGame/src/main/resources/images/warning2.png differ diff --git a/racevisionGame/src/main/resources/visualiser/images/tomato.png b/racevisionGame/src/main/resources/visualiser/images/tomato.png new file mode 100644 index 00000000..92cf4c1d Binary files /dev/null and b/racevisionGame/src/main/resources/visualiser/images/tomato.png differ diff --git a/racevisionGame/src/main/resources/visualiser/scenes/hostGame.fxml b/racevisionGame/src/main/resources/visualiser/scenes/hostGame.fxml index cf82c285..3218c802 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/hostGame.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/hostGame.fxml @@ -3,6 +3,7 @@ + @@ -12,19 +13,20 @@ - + - - - + + + - - - + + + + - + + @@ -189,10 +244,10 @@ - - - - + + + + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/nextMark.fxml b/racevisionGame/src/main/resources/visualiser/scenes/nextMark.fxml new file mode 100644 index 00000000..a2e15c37 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/scenes/nextMark.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/racevisionGame/src/main/resources/visualiser/sounds/dead1.wav b/racevisionGame/src/main/resources/visualiser/sounds/dead1.wav new file mode 100644 index 00000000..78e4d5ac Binary files /dev/null and b/racevisionGame/src/main/resources/visualiser/sounds/dead1.wav differ diff --git a/racevisionGame/src/main/resources/visualiser/sounds/heartbeat.mp3 b/racevisionGame/src/main/resources/visualiser/sounds/heartbeat.mp3 new file mode 100644 index 00000000..f43c52fc Binary files /dev/null and b/racevisionGame/src/main/resources/visualiser/sounds/heartbeat.mp3 differ diff --git a/racevisionGame/src/main/resources/visualiser/sounds/warning.mp3 b/racevisionGame/src/main/resources/visualiser/sounds/warning.mp3 new file mode 100644 index 00000000..dd5ab00e Binary files /dev/null and b/racevisionGame/src/main/resources/visualiser/sounds/warning.mp3 differ diff --git a/racevisionGame/src/test/java/network/MessageDecoders/BoatStateDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/BoatStateDecoderTest.java new file mode 100644 index 00000000..ee0cb646 --- /dev/null +++ b/racevisionGame/src/test/java/network/MessageDecoders/BoatStateDecoderTest.java @@ -0,0 +1,33 @@ +package network.MessageDecoders; + +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.BoatState; +import org.junit.Before; +import org.junit.Test; + +import static org.testng.Assert.*; + +/** + * Test for the {@link network.Messages.BoatState} encoder and decoder. + */ +public class BoatStateDecoderTest { + private BoatState originalMessage; + private BoatState decodedMessage; + + @Before + public void setUp() throws InvalidMessageException { + originalMessage = new BoatState(0, 100); + + byte[] encodedMessage = RaceVisionByteEncoder.encode(originalMessage); + BoatStateDecoder decoder = new BoatStateDecoder(); + decoder.decode(encodedMessage); + decodedMessage = decoder.getMessage(); + } + + @Test + public void decodingEqualsOriginal() { + assertEquals(originalMessage.getSourceID(), decodedMessage.getSourceID()); + assertEquals(originalMessage.getBoatHealth(), decodedMessage.getBoatHealth()); + } +} \ No newline at end of file