Merge branch 'story1292_hp_punish' into 'master'

Story1292 and Story1291 hp

Story 1291:

AC's:
-Mock sends out HP packets
-Visualiser displays each boat's HP in tab view
-User boat HP is displayed on main UI beside boat

Story 1292:

AC's:
-Boats lose HP when they collide
-Boats lose HP when they are out of bounds
-Indication when HP drops below thresholds (20hp, 10hp, 0hp)

See merge request !65
main
Fraser Cope 8 years ago
commit 75f8231bc5

@ -66,6 +66,7 @@ public class Event {
* Constructs an event, using various XML files. * Constructs an event, using various XML files.
* @param singlePlayer Whether or not to create a single player event. * @param singlePlayer Whether or not to create a single player event.
* @param mapIndex Specifies which map to use. * @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. * @throws EventConstructionException Thrown if we cannot create an Event for any reason.
*/ */
public Event(boolean singlePlayer, int mapIndex, int raceLength) throws public Event(boolean singlePlayer, int mapIndex, int raceLength) throws

@ -27,7 +27,7 @@ public class MockBoat extends Boat {
* 1: passed only first check * 1: passed only first check
* 2: passed first and second check * 2: passed first and second check
*/ */
private Integer roundingStatus = 0; private int roundingStatus = 0;
/** /**
* Stores whether the boat is on autoVMG or not * Stores whether the boat is on autoVMG or not
@ -284,8 +284,8 @@ public class MockBoat extends Boat {
(this.isStarboardSide(mark1) && this.isPortSide(mark2)); (this.isStarboardSide(mark1) && this.isPortSide(mark2));
} }
public Integer getRoundingStatus() { public int getRoundingStatus() {
return Integer.valueOf(roundingStatus); return roundingStatus;
} }
public void increaseRoundingStatus() { public void increaseRoundingStatus() {

@ -419,6 +419,11 @@ public class MockRace extends RaceState {
boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds); 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); this.updateEstimatedTime(boat);
} }
@ -430,7 +435,7 @@ public class MockRace extends RaceState {
boat.getBearing() boat.getBearing()
), boat.getBearing()) ; ), boat.getBearing()) ;
if (vmg.getSpeed() > 0) { 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) { for (MockBoat boat : this.boats) {
//If the boat is currently racing, count it. //If the boat is currently racing, count it.
if (boat.getStatus() == BoatStatusEnum.RACING) { if (boat.getStatus() == BoatStatusEnum.RACING && boat.getHealth()>=1) {
numberOfActiveBoats++; numberOfActiveBoats++;
} }

@ -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. //As long as there is at least one boat racing, we still simulate the race.
if (race.getNumberOfActiveBoats() != 0) { if (race.getNumberOfActiveBoats() != 0) {
//System.out.println(race.getNumberOfActiveBoats());
//Get the time period of this frame. //Get the time period of this frame.
long framePeriod = currentTime - previousFrameTime; long framePeriod = currentTime - previousFrameTime;
@ -175,12 +175,9 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli()); race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli());
if(race.getColliderRegistry().rayCast(boat)){ if(race.getColliderRegistry().rayCast(boat)){
//System.out.println("Collision!");
//Add boat to list //Add boat to list
collisionBoats.add(boat); collisionBoats.add(boat);
} }
//System.out.println(race.getColliderRegistry().rayCast(boat));
} }

@ -64,6 +64,9 @@ public class RaceServer {
//Parse the boat locations. //Parse the boat locations.
snapshotMessages.addAll(parseBoatLocations()); snapshotMessages.addAll(parseBoatLocations());
//Parse the boat states
snapshotMessages.addAll(parseBoatStates());
//Parse the marks. //Parse the marks.
snapshotMessages.addAll(parseMarks()); 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<BoatState> parseBoatStates() {
List<BoatState> 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. * Parses the race status, and returns it.

@ -177,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 a mark 1
* @param b mark 2 * @param b mark 2
* @return * @return Distance in nautical miles.
*/ */
private static double getDistance(XMLMark a, XMLMark b){ private static double getDistance(XMLMark a, XMLMark b){
GPSCoordinate coorda = new GPSCoordinate(a.getTargetLat(), a.getTargetLng()); GPSCoordinate coorda = new GPSCoordinate(a.getTargetLat(), a.getTargetLng());

@ -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;
}
}

@ -59,6 +59,7 @@ public class DecoderFactory {
case BOATACTION: return new BoatActionDecoder(); case BOATACTION: return new BoatActionDecoder();
case BOATSTATE: return new BoatStateDecoder();
default: throw new InvalidMessageTypeException("Unrecognised message type: " + type); default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
} }

@ -18,6 +18,5 @@ public interface MessageDecoder {
* @return The decoded message. * @return The decoded message.
* @throws InvalidMessageException Thrown if the encoded message is invalid in some way, or cannot be decoded. * @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;
} }

@ -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();
}
}

@ -59,6 +59,7 @@ public class EncoderFactory {
case BOATACTION: return new BoatActionEncoder(); case BOATACTION: return new BoatActionEncoder();
case BOATSTATE: return new BoatStateEncoder();
default: throw new InvalidMessageTypeException("Unrecognised message type: " + type); default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
} }

@ -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;
}
}

@ -43,6 +43,8 @@ public enum MessageType {
HOSTED_GAMES_REQUEST(109), HOSTED_GAMES_REQUEST(109),
BOATSTATE(103),
NOTAMESSAGE(0); NOTAMESSAGE(0);
@ -56,7 +58,7 @@ public enum MessageType {
* Creates a MessageType enum from a given primitive integer value, cast to a byte. * 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. * @param value Integer, which is cast to byte, to construct from.
*/ */
private MessageType(int value) { MessageType(int value) {
this.value = (byte)value; this.value = (byte)value;
} }

@ -103,6 +103,11 @@ public class Boat extends Collider {
*/ */
private boolean isColliding = false; 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. * 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) { public Boat(int sourceID, String name, String country) {
this.sourceID = sourceID; this.sourceID = sourceID;
this.health = 100;
this.setName(name); this.setName(name);
this.setCountry(country); this.setCountry(country);
@ -419,6 +425,8 @@ public class Boat extends Collider {
@Override @Override
public void onCollisionEnter(Collision e) { public void onCollisionEnter(Collision e) {
if(e.getBearing().degrees() > 270 || e.getBearing().degrees() < 90) { if(e.getBearing().degrees() > 270 || e.getBearing().degrees() < 90) {
// Deplete health
e.getBoat().updateHealth(-5);
// Notify observers of collision // Notify observers of collision
this.setChanged(); this.setChanged();
notifyObservers(e); notifyObservers(e);
@ -432,4 +440,22 @@ public class Boat extends Collider {
public void setColliding(boolean colliding) { public void setColliding(boolean colliding) {
isColliding = 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;
}
} }

@ -97,6 +97,7 @@ public class Mark extends Collider{
@Override @Override
public void onCollisionEnter(Collision e) { public void onCollisionEnter(Collision e) {
e.getBoat().updateHealth(-10);
this.setChanged(); this.setChanged();
notifyObservers(e); notifyObservers(e);
} }

@ -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
}
}
}

@ -24,7 +24,6 @@ public class VisualiserRaceCommandFactory {
switch (message.getType()) { switch (message.getType()) {
case BOATLOCATION: case BOATLOCATION:
//System.out.println("Boat location received");
return new BoatLocationCommand((BoatLocation) message, visualiserRace); return new BoatLocationCommand((BoatLocation) message, visualiserRace);
case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace); case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace);
@ -36,7 +35,8 @@ public class VisualiserRaceCommandFactory {
case YACHTEVENTCODE: case YACHTEVENTCODE:
return new BoatCollisionCommand((YachtEvent) message, visualiserRace); 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()); default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType());

@ -15,12 +15,15 @@ import javafx.scene.AmbientLight;
import javafx.scene.PointLight; import javafx.scene.PointLight;
import javafx.scene.chart.LineChart; import javafx.scene.chart.LineChart;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.Material; import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial; import javafx.scene.paint.PhongMaterial;
@ -30,6 +33,7 @@ import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D; import javafx.scene.shape.Shape3D;
import javafx.scene.transform.Translate; import javafx.scene.transform.Translate;
import javafx.util.Callback; import javafx.util.Callback;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceStatusEnum;
import shared.dataInput.RaceDataSource; import shared.dataInput.RaceDataSource;
import shared.exceptions.BoatNotFoundException; import shared.exceptions.BoatNotFoundException;
@ -44,6 +48,7 @@ import visualiser.model.*;
import visualiser.utils.GPSConverter; import visualiser.utils.GPSConverter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
@ -72,6 +77,9 @@ public class RaceViewController extends Controller {
private long positionTime = 0; private long positionTime = 0;
private ResizableRaceCanvas raceCanvas; private ResizableRaceCanvas raceCanvas;
private boolean mapToggle = true; private boolean mapToggle = true;
private GPSConverter gpsConverter;
private ArrayList<HealthEffect> healthEffectList = new ArrayList<>();
/** /**
* Arrow pointing to next mark in third person * Arrow pointing to next mark in third person
@ -99,12 +107,17 @@ public class RaceViewController extends Controller {
private @FXML TableColumn<VisualiserBoat, String> boatTeamColumn; private @FXML TableColumn<VisualiserBoat, String> boatTeamColumn;
private @FXML TableColumn<VisualiserBoat, Leg> boatMarkColumn; private @FXML TableColumn<VisualiserBoat, Leg> boatMarkColumn;
private @FXML TableColumn<VisualiserBoat, Number> boatSpeedColumn; private @FXML TableColumn<VisualiserBoat, Number> boatSpeedColumn;
private @FXML TableColumn<VisualiserBoat, Number> boatHealthColumn;
private @FXML LineChart<Number, Number> sparklineChart; private @FXML LineChart<Number, Number> sparklineChart;
private @FXML Label tutorialText; private @FXML Label tutorialText;
private @FXML AnchorPane infoWrapper; private @FXML AnchorPane infoWrapper;
private @FXML AnchorPane lineChartWrapper; private @FXML AnchorPane lineChartWrapper;
private @FXML StackPane speedPane; private @FXML StackPane speedPane;
private @FXML AnchorPane raceAnchorPane; private @FXML AnchorPane raceAnchorPane;
private @FXML GridPane playerHealthContainer;
private @FXML ImageView imageView;
private @FXML AnchorPane deathTransPane;
private @FXML StackPane deathPane;
/** /**
* Displays a specified race. * Displays a specified race.
@ -119,11 +132,13 @@ public class RaceViewController extends Controller {
this.controllerClient = controllerClient; this.controllerClient = controllerClient;
this.isHost = isHost; this.isHost = isHost;
keyFactory.load(); keyFactory.load();
deathPane.setDisable(false);
deathPane.setVisible(false);
tutorialCheck(); tutorialCheck();
initKeypressHandler(); initKeypressHandler();
initialiseRaceVisuals(); initialiseRaceVisuals();
initialiseRaceCanvas(); initialiseRaceCanvas();
healthLoop();
} }
/** /**
@ -135,7 +150,6 @@ public class RaceViewController extends Controller {
isTutorial = true; isTutorial = true;
tutorialText.setVisible(true); tutorialText.setVisible(true);
tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values())); tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values()));
currentState = tutorialStates.get(0); currentState = tutorialStates.get(0);
tutorialStates.remove(0); tutorialStates.remove(0);
searchMapForKey("Upwind"); searchMapForKey("Upwind");
@ -296,6 +310,7 @@ public class RaceViewController extends Controller {
initialiseFps(); initialiseFps();
initialiseInfoTable(); initialiseInfoTable();
initialiseView3D(this.visualiserRace); initialiseView3D(this.visualiserRace);
initialiseHealthPane();
initialiseRaceClock(); initialiseRaceClock();
initialiseSpeedometer(); initialiseSpeedometer();
raceTimer(); // start the timer raceTimer(); // start the timer
@ -306,6 +321,23 @@ public class RaceViewController extends Controller {
arrowController.setWindProperty(this.raceState.windProperty()); 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) { private void initialiseView3D(VisualiserRaceEvent race) {
viewSubjects = FXCollections.observableArrayList(); viewSubjects = FXCollections.observableArrayList();
@ -353,7 +385,7 @@ public class RaceViewController extends Controller {
// Set up projection from GPS to view // Set up projection from GPS to view
RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450); gpsConverter = new GPSConverter(raceData, 450, 450);
SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210); SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210);
viewSubjects.addAll(skyBox.getSkyBoxPlanes()); viewSubjects.addAll(skyBox.getSkyBoxPlanes());
@ -429,6 +461,11 @@ public class RaceViewController extends Controller {
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 //add sail
Sails3D sails3D = new Sails3D(); Sails3D sails3D = new Sails3D();
Subject3D sailsSubject = new Subject3D(sails3D, 0); Subject3D sailsSubject = new Subject3D(sails3D, 0);
@ -509,6 +546,14 @@ public class RaceViewController extends Controller {
boatModel.setHeading(boat.getBearing().degrees()); boatModel.setHeading(boat.getBearing().degrees());
boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); 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(); trackBoat.start();
@ -701,13 +746,17 @@ public class RaceViewController extends Controller {
// set table data // set table data
boatInfoTable.setItems(sortedBoats); boatInfoTable.setItems(sortedBoats);
boatTeamColumn.setCellValueFactory( boatTeamColumn.setCellValueFactory(
cellData -> cellData.getValue().nameProperty()); cellData -> cellData.getValue().nameProperty()
);
boatSpeedColumn.setCellValueFactory( boatSpeedColumn.setCellValueFactory(
cellData -> cellData.getValue().currentSpeedProperty()); cellData -> cellData.getValue().currentSpeedProperty()
);
boatMarkColumn.setCellValueFactory( boatMarkColumn.setCellValueFactory(
cellData -> cellData.getValue().legProperty()); cellData -> cellData.getValue().legProperty()
boatPlacingColumn.setCellValueFactory( );
cellData -> cellData.getValue().placingProperty()); boatHealthColumn.setCellValueFactory(
cellData -> cellData.getValue().healthProperty()
);
//Kind of ugly, but allows for formatting an observed speed. //Kind of ugly, but allows for formatting an observed speed.
boatSpeedColumn.setCellFactory( boatSpeedColumn.setCellFactory(
@ -839,6 +888,64 @@ public class RaceViewController extends Controller {
}.start(); }.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 * toggles if the info table is shown
@ -994,4 +1101,12 @@ public class RaceViewController extends Controller {
mapToggle = !mapToggle; mapToggle = !mapToggle;
} }
/**
* FXML method for death button
*/
public void deathOKPressed(){
deathPane.setDisable(true);
deathPane.setVisible(false);
}
} }

@ -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);
}
}
}
}

@ -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));
}
}

@ -1,9 +1,6 @@
package visualiser.model; package visualiser.model;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.BoatStatusEnum;
import shared.model.*; import shared.model.*;
@ -65,6 +62,7 @@ public class VisualiserBoat extends Boat {
private ObjectProperty<GPSCoordinate> positionProperty; private ObjectProperty<GPSCoordinate> positionProperty;
private ObjectProperty<Bearing> bearingProperty; private ObjectProperty<Bearing> bearingProperty;
private BooleanProperty hasCollided; private BooleanProperty hasCollided;
private DoubleProperty healthProperty;
/** /**
@ -78,6 +76,7 @@ public class VisualiserBoat extends Boat {
this.color = color; this.color = color;
this.hasCollided = new SimpleBooleanProperty(false); this.hasCollided = new SimpleBooleanProperty(false);
this.healthProperty = new SimpleDoubleProperty(100);
} }
@ -284,4 +283,18 @@ public class VisualiserBoat extends Boat {
public void setHasCollided(boolean hasCollided) { public void setHasCollided(boolean hasCollided) {
this.hasCollided.set(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);
}
} }

@ -248,7 +248,6 @@ public class VisualiserRaceState extends RaceState {
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) { if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
boat.setPlacing("-"); boat.setPlacing("-");
} else { } else {

@ -297,6 +297,7 @@ public class ServerConnection implements RunnableWithFramePeriod {
this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages); this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages);
this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages); this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages);
this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, 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. this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.chart.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.*?> <?import java.lang.*?>
<?import javafx.geometry.*?> <?import javafx.geometry.*?>
<?import javafx.scene.chart.*?> <?import javafx.scene.chart.*?>
@ -26,6 +33,7 @@
<?import javafx.scene.layout.Pane?> <?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.RowConstraints?> <?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<SplitPane fx:id="racePane" prefHeight="431.0" prefWidth="610.0" visible="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.RaceViewController"> <SplitPane fx:id="racePane" prefHeight="431.0" prefWidth="610.0" visible="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.RaceViewController">
@ -92,23 +100,39 @@
</Accordion> </Accordion>
</children> </children>
</Pane> </Pane>
<Label fx:id="timer" alignment="CENTER" maxHeight="20.0" mouseTransparent="true" text="0:0"> <GridPane fx:id="playerHealthContainer" prefHeight="75.0" prefWidth="75.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0">
<font> <columnConstraints>
<Font name="System Bold" size="15.0" /> <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</font> </columnConstraints>
</Label> <rowConstraints>
<Label fx:id="FPS" mouseTransparent="true" text="FPS: 0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0"> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<font> </rowConstraints>
<Font name="System Bold" size="15.0" />
</font>
<padding> <padding>
<Insets top="20.0" /> <Insets left="10.0" top="10.0" />
</padding> </padding>
</Label> </GridPane>
<Label fx:id="timeZone" mouseTransparent="true" text="Label" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0"> <VBox alignment="CENTER" prefHeight="54.0" prefWidth="608.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="timer" alignment="CENTER" maxHeight="20.0" text="0:0">
<font>
<Font name="System Bold" size="15.0" />
</font>
</Label>
<Label fx:id="timeZone" text="Label">
<font>
<Font name="System Bold" size="15.0" />
</font>
</Label>
</children>
</VBox>
<Label fx:id="FPS" text="FPS: 0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0">
<font> <font>
<Font name="System Bold" size="15.0" /> <Font name="System Bold" size="15.0" />
</font> </font>
<padding>
<Insets bottom="200.0" />
</padding>
</Label> </Label>
<StackPane fx:id="arrowPane" alignment="TOP_RIGHT" mouseTransparent="true" prefHeight="150.0" prefWidth="150.0" snapToPixel="false" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> <StackPane fx:id="arrowPane" alignment="TOP_RIGHT" mouseTransparent="true" prefHeight="150.0" prefWidth="150.0" snapToPixel="false" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children> <children>
@ -179,6 +203,30 @@
</AnchorPane> </AnchorPane>
</children> </children>
</AnchorPane> </AnchorPane>
<ImageView fx:id="imageView" fitHeight="150.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true" />
<StackPane fx:id="deathPane" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<AnchorPane mouseTransparent="true" opacity="0.31" prefHeight="200.0" prefWidth="200.0" style="-fx-background-color: grey;" />
<Label alignment="CENTER" text="You have died..." StackPane.alignment="TOP_CENTER">
<font>
<Font size="34.0" />
</font>
<StackPane.margin>
<Insets top="100.0" />
</StackPane.margin>
</Label>
<Label alignment="CENTER" text="But you can still watch the race!" StackPane.alignment="TOP_CENTER">
<StackPane.margin>
<Insets top="165.0" />
</StackPane.margin>
</Label>
<Button alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#deathOKPressed" prefHeight="25.0" prefWidth="52.0" text="OK" StackPane.alignment="TOP_CENTER">
<StackPane.margin>
<Insets top="200.0" />
</StackPane.margin>
</Button>
</children>
</StackPane>
</children> </children>
</AnchorPane> </AnchorPane>
<AnchorPane fx:id="infoWrapper" focusTraversable="true" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="200.0" visible="false"> <AnchorPane fx:id="infoWrapper" focusTraversable="true" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="200.0" visible="false">
@ -196,10 +244,10 @@
<children> <children>
<TableView fx:id="boatInfoTable" minWidth="475.0" prefHeight="214.0" prefWidth="475.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.valignment="CENTER"> <TableView fx:id="boatInfoTable" minWidth="475.0" prefHeight="214.0" prefWidth="475.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<columns> <columns>
<TableColumn fx:id="boatPlacingColumn" prefWidth="50.0" text="Place" /> <TableColumn fx:id="boatTeamColumn" prefWidth="135.0" text="Team" />
<TableColumn fx:id="boatTeamColumn" prefWidth="200.0" text="Team" /> <TableColumn fx:id="boatMarkColumn" prefWidth="142.0" text="Mark" />
<TableColumn fx:id="boatMarkColumn" prefWidth="150.0" text="Mark" /> <TableColumn fx:id="boatHealthColumn" prefWidth="110.0" text="Health" />
<TableColumn fx:id="boatSpeedColumn" prefWidth="75.0" text="Speed" /> <TableColumn fx:id="boatSpeedColumn" prefWidth="87.0" text="Speed" />
</columns> </columns>
</TableView> </TableView>
<AnchorPane fx:id="lineChartWrapper" prefHeight="167.0" prefWidth="178.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER"> <AnchorPane fx:id="lineChartWrapper" prefHeight="167.0" prefWidth="178.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">

@ -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());
}
}
Loading…
Cancel
Save