Merge branch 'master' into story1301-ingamelobby

# Conflicts:
#	racevisionGame/src/main/java/visualiser/Controllers/InGameLobbyController.java
#	racevisionGame/src/main/resources/visualiser/scenes/gameLobby.fxml
main
David Wu 8 years ago
commit 4e352a7d49

@ -16,6 +16,9 @@
# https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html
# http://stacktoheap.com/blog/2013/01/06/using-mailmap-to-fix-authors-list-in-git/
Erika Savell <esa46@uclive.ac.nz>
Connor Taylor-Brown <cbt24@cs17086jp.canterbury.ac.nz> <cbt24@uclive.canterbury.ac.nz>
Connor Taylor-Brown <cbt24@uclive.ac.nz> <cbt24@cs17086jp.canterbury.ac.nz>
Fraser Cope <fjc40@uclive.ac.nz>
Jessica Syder <jam339@uclive.ac.nz> Jessica Syder <doctorjess@live.com>
Jessica Syder <jam339@uclive.ac.nz> <doctorjess@live.com>
Joseph Gardner <jjg64@uclive.ac.nz>
Hamish Ball <hba56@uclive.ac.nz>
David Wu <zwu18@uclive.ac.nz>

@ -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) {

@ -29,6 +29,12 @@
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>eu.hansolo</groupId>
<artifactId>Medusa</artifactId>
<version>7.9</version>
</dependency>
<!-- Maven Dependencies block START-->
<dependency>
<groupId>net.bytebuddy</groupId>
@ -90,6 +96,18 @@
<version>20160810</version>
</dependency>
<dependency>
<groupId>JavaInteractiveMesh</groupId>
<artifactId>ObjImporter</artifactId>
<version>0.8</version>
</dependency>
<dependency>
<groupId>JavaInteractiveMesh</groupId>
<artifactId>X3DImporter</artifactId>
<version>0.4</version>
</dependency>
</dependencies>

@ -40,53 +40,44 @@ 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.
*/
private SourceIdAllocator sourceIdAllocator;
private Thread raceThread;
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;
String boatsXMLFile = "mock/mockXML/boatTest.xml";
String regattaXMLFile = "mock/mockXML/regattaTest.xml";
switch (mapIndex){
case 0:raceXMLFile = "mock/mockXML/raceSixPlayers.xml";
case 0:raceXMLFile = "mock/mockXML/ac35MapLayout.xml";
break;
case 1:raceXMLFile = "mock/mockXML/oMapLayout.xml";
break;
@ -99,13 +90,12 @@ public class Event {
boatsXMLFile = "mock/mockXML/boatTutorial.xml";
regattaXMLFile = "mock/mockXML/regattaTutorial.xml";
break;
default: raceXMLFile = "mock/mockXML/raceSixPlayers.xml";
default: raceXMLFile = "mock/mockXML/ac35MapLayout.xml";
}
if (singlePlayer) {
raceXMLFile = "mock/mockXML/raceSinglePlayer.xml";
boatsXMLFile = "mock/mockXML/boatsSinglePlayer.xml";
}
@ -114,14 +104,17 @@ public class Event {
//Read XML files.
try {
//this.raceXML = RaceXMLCreator.alterRaceToWind(raceXMLFile, 90);
this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8));
if(mapIndex==4){
//this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8), 1000, 5000);
this.raceXML = RaceXMLCreator.alterRaceToWind(this.raceXML, XMLFileType.Contents, -1, true);
//This is the tutorial map.
this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8), 1000, 5000);
this.raceXML = RaceXMLCreator.alterRaceToWind(this.raceXML, XMLFileType.Contents, -1);
} else {
this.raceXML = RaceXMLCreator.alterRaceToWind(this.raceXML, XMLFileType.Contents, windAngle, false);
this.raceXML = RaceXMLCreator.scaleRaceSize(raceXML, windSpeed, 15 * 60 * 1000);
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, raceLength);
}
this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8);
@ -171,20 +164,20 @@ public class Event {
}
RaceLogic newRace = new RaceLogic(
this.raceLogic = new RaceLogic(
mockRace,
this.latestMessages,
this.compositeCommand);
this.raceThread = new Thread(newRace, "Event.Start()->RaceLogic thread");
this.raceThread = new Thread(raceLogic, "Event.Start()->RaceLogic thread");
raceThread.start();
//Create connection acceptor.
this.sourceIdAllocator = new SourceIdAllocator(newRace.getRace());
this.sourceIdAllocator = new SourceIdAllocator(raceLogic.getRace());
try {
this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator, newRace);
this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator, raceLogic);
} catch (IOException e) {
throw new EventConstructionException("Could not create ConnectionAcceptor.", e);
@ -217,16 +210,13 @@ public class Event {
}
public static String setRaceXMLAtCurrentTimeToNow(String 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;
long secondsToAdd = millisecondsToAdd / 1000;
long minutesToAdd = 10;
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
ZonedDateTime creationTime = ZonedDateTime.now();
raceXML = raceXML.replace("RACE_CREATION_TIME", dateFormat.format(creationTime));
raceXML = raceXML.replace("RACE_START_TIME", dateFormat.format(creationTime.plusSeconds(secondsToAdd)));
raceXML = raceXML.replace("RACE_START_TIME", dateFormat.format(creationTime.plusMinutes(minutesToAdd)));
return raceXML;
}
@ -241,4 +231,9 @@ public class Event {
return new HostGame(ip, 3779,(byte) 1,(byte) 1, RaceStatusEnum.PRESTART, (byte) 1, (byte) 1);
}
public RaceLogic getRaceLogic() {
return raceLogic;
}
}

@ -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() {

@ -61,12 +61,17 @@ public class MockRace extends RaceState {
*/
private Polars polars;
private ActiveObserverCommand activeObserverCommand;
private Map<Integer, ActiveObserverCommand> activeObserverCommands;
private long racePreStartTime = Constants.RacePreStartTime;
private long racePreparatoryTime = Constants.RacePreparatoryTime;
/**
* True if the race has been manually started, false otherwise.
*/
private boolean hasBeenStarted = false;
/**
* Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and sends events to the given mockOutput.
* @param boatDataSource Data source for boat related data (yachts and marker boats).
@ -82,7 +87,7 @@ public class MockRace extends RaceState {
this.setRaceDataSource(raceDataSource);
this.setRegattaDataSource(regattaDataSource);
this.activeObserverCommand = new ActiveObserverCommand();
this.activeObserverCommands = new HashMap<>();
this.polars = polars;
this.scaleFactor = timeScale;
@ -104,8 +109,6 @@ public class MockRace extends RaceState {
colliderRegistry.addCollider(mark.getMark1());
if(mark.getMark2() != null) colliderRegistry.addCollider(mark.getMark2());
}
this.colliderRegistry.addAllColliders(boats);
}
@ -127,9 +130,10 @@ public class MockRace extends RaceState {
getRaceDataSource().getParticipants().add(sourceID);
this.boats.add(mockBoat);
this.activeObserverCommands.put(boat.getSourceID(), new ActiveObserverCommand());
this.colliderRegistry.addCollider(mockBoat);
getRaceDataSource().incrementSequenceNumber();
}
/**
@ -157,6 +161,23 @@ public class MockRace extends RaceState {
}
/**
* Delays the start of the race, if needed, to ensure that the race doesn't start until host wants it to.
* If the time until start is less that 5 minutes, it is extended to 10 minutes.
*/
public void delayRaceStart() {
long timeToStart = getRaceDataSource().getStartDateTime().toInstant().toEpochMilli() - System.currentTimeMillis();
long fiveMinutesMilli = 5 * 60 * 1000;
long tenMinutesMilli = 10 * 60 * 1000;
if ((timeToStart < fiveMinutesMilli) && (timeToStart > 0) && !hasBeenStarted) {
startRace(tenMinutesMilli, false);
}
}
/**
* Updates the race status enumeration based on the current time.
*/
@ -165,7 +186,6 @@ public class MockRace extends RaceState {
//The millisecond duration of the race. Negative means it hasn't started, so we flip sign.
long timeToStart = - this.getRaceClock().getDurationMilli();
if (timeToStart > racePreStartTime) {
//Time > 3 minutes is the prestart period.
this.setRaceStatusEnum(RaceStatusEnum.PRESTART);
@ -195,6 +215,26 @@ public class MockRace extends RaceState {
this.racePreparatoryTime = racePreparatoryTime;
}
public long getRacePreparatoryTime() {
return racePreparatoryTime;
}
/**
* Starts the race in #timeToStartMilli milliseconds.
* @param timeToStartMilli Millseconds before starting the race.
* @param manualStart True if the race has been manually started, false otherwise.
*/
public void startRace(long timeToStartMilli, boolean manualStart) {
this.hasBeenStarted = manualStart;
ZonedDateTime startTime = ZonedDateTime.now().plus(timeToStartMilli, ChronoUnit.MILLIS);
getRaceDataSource().setStartDateTime(startTime);
getRaceDataSource().incrementSequenceNumber();
this.getRaceClock().setStartingTime(getRaceDataSource().getStartDateTime());
}
/**
* Sets the status of all boats in the race to RACING.
*/
@ -377,6 +417,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);
}
@ -388,7 +433,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));
}
}
@ -635,7 +680,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++;
}
@ -711,11 +756,11 @@ public class MockRace extends RaceState {
super.setChanged();
}
public void addVelocityCommand(ObserverCommand c) {
this.activeObserverCommand.changeVelocityCommand(this, c);
public void addVelocityCommand(ObserverCommand c, int boatId) {
this.activeObserverCommands.get(boatId).changeVelocityCommand(this, c);
}
public void addAngularCommand(ObserverCommand c) {
this.activeObserverCommand.changeAngularCommand(this, c);
public void addAngularCommand(ObserverCommand c, int boatId) {
this.activeObserverCommands.get(boatId).changeAngularCommand(this, c);
}
}

@ -81,6 +81,8 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
//Update race time.
race.updateRaceTime(currentTime);
race.delayRaceStart();
//Update the race status based on the current time.
race.updateRaceStatusEnum();
@ -161,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;
@ -173,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));
}

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

@ -36,6 +36,7 @@ public class CollisionCommand extends ObserverCommand {
@Override
public void update(Observable o, Object arg) {
if(GPSCoordinate.calculateDistanceMeters(boat.getPosition(), startingPosition) < distance) {
boat.setVelocityDefault(false);
boat.setPosition(GPSCoordinate.calculateNewPosition(boat.getPosition(), 3, azimuth));
} else {
race.deleteObserver(this);

@ -13,7 +13,7 @@ public class SailsCommand extends ObserverCommand {
public SailsCommand(MockRace race, MockBoat boat, boolean sailsOut) {
super(race, boat);
race.addVelocityCommand(this);
race.addVelocityCommand(this, boat.getSourceID());
this.sailsOut = sailsOut;
}
@ -37,15 +37,18 @@ public class SailsCommand extends ObserverCommand {
public void update(Observable o, Object arg) {
double acceleration = 0.5;
if(sailsOut && boat.getCurrentSpeed() < goalVelocity) {
boat.setCurrentSpeed(Math.min(goalVelocity, boat.getCurrentSpeed() + acceleration));
} else if (!sailsOut && boat.getCurrentSpeed() > goalVelocity) {
// Apply deceleration to strictly 0 speed
boat.setCurrentSpeed(Math.max(0, boat.getCurrentSpeed() - acceleration));
} else {
// Release boat from SailsCommand control
if(sailsOut) boat.setVelocityDefault(true);
race.deleteObserver(this);
if (!boat.isColliding()) {
boat.setVelocityDefault(false);
if (sailsOut && boat.getCurrentSpeed() < goalVelocity) {
boat.setCurrentSpeed(Math.min(goalVelocity, boat.getCurrentSpeed() + acceleration));
} else if (!sailsOut && boat.getCurrentSpeed() > goalVelocity) {
// Apply deceleration to strictly 0 speed
boat.setCurrentSpeed(Math.max(0, boat.getCurrentSpeed() - acceleration));
} else {
// Release boat from SailsCommand control
if (sailsOut) boat.setVelocityDefault(true);
race.deleteObserver(this);
}
}
}
}

@ -22,7 +22,7 @@ public class TackGybeCommand extends ObserverCommand {
*/
public TackGybeCommand(MockRace race, MockBoat boat) {
super(race, boat);
race.addAngularCommand(this);
race.addAngularCommand(this, boat.getSourceID());
}
@Override

@ -24,7 +24,7 @@ public class VMGCommand extends ObserverCommand {
*/
public VMGCommand(MockRace race, MockBoat boat) {
super(race, boat);
race.addAngularCommand(this);
race.addAngularCommand(this, boat.getSourceID());
}
@Override

@ -20,7 +20,7 @@ public class WindCommand extends ObserverCommand {
*/
public WindCommand(MockRace race, MockBoat boat, boolean upwind) {
super(race, boat);
race.addAngularCommand(this);
race.addAngularCommand(this, boat.getSourceID());
this.direction = upwind? -1 : 1;
}

@ -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
@ -56,13 +58,12 @@ public class RaceXMLCreator {
* Rotates the race in a specified direction.
* @param s xml file name or contents.
* @param fileType Whether s is a file name or contents.
* @param degrees degrees to rotate
* @param tutorial Whether we wish to run the tutorial - this changes the race start time.
* @param degrees degrees to rotate. -1 means don't rotate.
* @return the new xml file as a string
* @throws XMLReaderException if the xml is not readable
* @throws InvalidRaceDataException if the race is invalid
*/
public static String alterRaceToWind(String s, XMLFileType fileType, double degrees, boolean tutorial) throws XMLReaderException, InvalidRaceDataException {
public static String alterRaceToWind(String s, XMLFileType fileType, double degrees) throws XMLReaderException, InvalidRaceDataException {
RaceXMLReader reader = new RaceXMLReader(s, fileType);
@ -73,11 +74,6 @@ public class RaceXMLCreator {
RaceXMLCreator.class.getClassLoader().getResource("mock/mockXML/schema/raceSchema.xsd"),
XMLRace.class);
if(tutorial){
setRaceXMLAtCurrentTimeToNow(race, 1000l, 5000l);
} else {
setRaceXMLAtCurrentTimeToNow(race);
}
CompoundMark leewardGate = getLeewardGate(reader);
@ -181,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());
@ -218,12 +214,16 @@ public class RaceXMLCreator {
double averageSpeed = (bestDownWindSpeed + bestUpWindSpeed) / 2;
double raceApproximateTime = getRaceLength(race, averageSpeed);
double scale = milliseconds / raceApproximateTime;
Map<XMLCompoundMark, Boolean> 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);
@ -325,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;

@ -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 BOATSTATE: return new BoatStateDecoder();
default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
}

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

@ -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 BOATSTATE: return new BoatStateEncoder();
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),
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;
}

@ -151,4 +151,9 @@ public class EmptyRaceDataSource implements RaceDataSource {
public void incrementSequenceNumber() {
sequenceNumber++;
}
@Override
public void setStartDateTime(ZonedDateTime time) {
raceStartTime = time;
}
}

@ -67,6 +67,12 @@ public interface RaceDataSource {
*/
ZonedDateTime getStartDateTime();
/**
* Sets the start time/date of the race.
* @param time Time to start at.
*/
void setStartDateTime(ZonedDateTime time);
/**
* Returns the creation time/date of the race xml file.
* @return The race xml file's creation time.

@ -507,4 +507,9 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
public void incrementSequenceNumber() {
sequenceNumber++;
}
@Override
public void setStartDateTime(ZonedDateTime time) {
raceStartTime = time;
}
}

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

@ -28,7 +28,7 @@ public class Constants {
* Frame periods are multiplied by this to get the amount of time a single frame represents.
* E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred.
*/
public static final int RaceTimeScale = 2;
public static final int RaceTimeScale = 1;
/**
* The race pre-start time, in milliseconds. 30 seconds.

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

@ -17,8 +17,9 @@ public class MarkRoundingSequence {
/**
* For each leg, mark rounding information.
* Maps between leg number and rounding data.
*/
private Map<Leg, MarkRoundingData> roundingPoints;
private Map<Integer, MarkRoundingData> roundingPoints;
@ -34,7 +35,7 @@ public class MarkRoundingSequence {
* @return Rounding points for leg.
*/
public MarkRoundingData getRoundingData(Leg leg) {
return roundingPoints.get(leg);
return roundingPoints.get(leg.getLegNumber());
}
@ -139,7 +140,7 @@ public class MarkRoundingSequence {
roundingData.setRoundCheck2Halfway(roundCheck2Halfway);
this.roundingPoints.put(currentLeg, roundingData);
this.roundingPoints.put(currentLeg.getLegNumber(), roundingData);
//Rounding points:

@ -68,11 +68,11 @@ public class RaceClock {
/**
* Format string used for duration before it has started.
*/
private String durationBeforeStartFormat = "Starting in: %02d:%02d:%02d";
private String durationBeforeStartFormat = "%02d:%02d:%02d";
/**
* Format string used for duration once the race has started.
*/
private String durationAfterStartFormat = "Time: %02d:%02d:%02d";
private String durationAfterStartFormat = "%02d:%02d:%02d";

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

@ -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<Image> 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,31 @@ 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(5);
sliderLength.setMax(MAX_RACE_LENGTH);
sliderLength.setShowTickLabels(true);
sliderLength.setMajorTickUnit(MAX_RACE_LENGTH-1);
sliderLength.setBlockIncrement(1);
}
/**
@ -54,8 +83,8 @@ public class HostGameController extends Controller {
listOfMaps = new ArrayList(Arrays.asList(ac35Map, oMap, iMap, mMap));
mapImage.setImage(listOfMaps.get(currentMapIndex));
Platform.runLater(() -> {
mapImage.fitWidthProperty()
.bind(mapImage.getScene().getWindow().widthProperty().multiply(0.6));
mapImage.fitHeightProperty()
.bind(mapImage.getScene().getWindow().heightProperty().multiply(0.6));
});
}
@ -64,7 +93,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();

@ -12,25 +12,31 @@ import javafx.scene.AmbientLight;
import javafx.scene.Node;
import javafx.scene.PointLight;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.effect.Light;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import mock.app.Event;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RequestToJoinEnum;
import shared.model.Boat;
import visualiser.app.App;
import visualiser.gameController.ControllerClient;
import visualiser.layout.*;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRaceEvent;
import visualiser.model.VisualiserRaceState;
import visualiser.network.HttpMatchBrowserHost;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.time.temporal.ChronoUnit;
@ -68,11 +74,13 @@ public class InGameLobbyController extends Controller {
private Label playerLabel6;
@FXML
private Label countdownLabel;
private Button startButton;
@FXML
private GridPane animatedPane;
@FXML
private Button quitButton;
private Event game;
@ -197,34 +205,54 @@ public class InGameLobbyController extends Controller {
for (VisualiserBoat boat : copy) {
View3D playerBoatToSet = new View3D();
playerBoatToSet.setItems(subjects);
playerContainer.add(playerBoatToSet, (count % 3) , row);
playerContainer.setMargin(playerBoatToSet, new Insets(10, 10, 10, 10));
SeaSurface sea = new SeaSurface(750, 200);
sea.setX(250);
sea.setZ(210);
subjects.add(sea);
MeshView mesh = new MeshView(importer.getImport());
// SeaSurface sea = new SeaSurface(750, 200);
// sea.setX(250);
// sea.setZ(210);
// subjects.add(sea);
//
NewSeaSurface sea = new NewSeaSurface();
Subject3D seaSubject = new Annotation3D(sea);
seaSubject.setX(50);
seaSubject.setZ(50);
seaSubject.setXRot(0);
subjects.add(seaSubject);
// MeshView mesh = new MeshView(importer.getImport());
// Subject3D subject = new Subject3D(mesh,0);
// subjects.add(subject);
Shape3D mesh = Assets3D.getBoat();
PhongMaterial boatColorMat = new PhongMaterial(boat.getColor());
mesh.setMaterial(boatColorMat);
Subject3D subject = new Subject3D(mesh,0);
//mesh.setMaterial(boatColorMat);
Subject3D subject = new Subject3D(mesh, 0);
subjects.add(subject);
Sails3D sails3D = new Sails3D();
Subject3D sailsSubject = new Subject3D(sails3D, 0);
sails3D.setMaterial(boatColorMat);
subjects.add(sailsSubject);
sailsSubject.setXRot(0d);
//sails3D.startLuffing();
playerBoatToSet.setDistance(50);
playerBoatToSet.setYaw(45);
playerBoatToSet.setPitch(20);
if (boat.isClientBoat()) {
Shockwave boatHighlight = new Shockwave(10);
boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1)));
boatHighlight.setX(subject.getPosition().getX());
boatHighlight.setY(subject.getPosition().getY());
boatHighlight.setZ(subject.getPosition().getZ());
subjects.add(boatHighlight);
/*Shockwave boatHighlight = new Shockwave(10);
boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1)));*/
Assets3D.boatHighlight.setX(subject.getPosition().getX());
Assets3D.boatHighlight.setY(subject.getPosition().getY());
Assets3D.boatHighlight.setZ(subject.getPosition().getZ());
subjects.add(Assets3D.boatHighlight);
subject.getMesh().toFront();
}
@ -232,6 +260,9 @@ public class InGameLobbyController extends Controller {
@Override
public void handle(long now) {
subject.setHeading(subject.getHeading().getAngle() + 0.1);
sailsSubject.setHeading(subject.getHeading().getAngle() + 0.1);
sailsSubject.setX(subject.getX());
sailsSubject.setZ(subject.getZ());
}
};
rotate.start();
@ -258,37 +289,13 @@ public class InGameLobbyController extends Controller {
* Starts the race.
*/
private void startRace() {
//Initialises the race clock.
initialiseRaceClock(this.visualiserRaceEvent.getVisualiserRaceState());
//Starts the race countdown timer.
countdownTimer();
}
/**
* Initialises the race clock/timer labels for the start time, current time, and remaining time.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceClock(VisualiserRaceState visualiserRace) {
//Remaining time.
initialiseRaceClockDuration(visualiserRace);
}
/**
* Initialises the race duration label.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceClockDuration(VisualiserRaceState visualiserRace) {
visualiserRace.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
countdownLabel.setText(newValue);
});
});
}
/**
* Countdown timer until race starts.
*/
@ -299,6 +306,7 @@ public class InGameLobbyController extends Controller {
//Get the current race status.
RaceStatusEnum raceStatus = visualiserRaceEvent.getVisualiserRaceState().getRaceStatusEnum();
//If the race has reached the preparatory phase, or has started...
if (raceStatus == RaceStatusEnum.PREPARATORY || raceStatus == RaceStatusEnum.STARTED) {
//Stop this timer.
@ -336,6 +344,8 @@ public class InGameLobbyController extends Controller {
this.visualiserRaceEvent.getVisualiserRaceState().getBoats().addListener(this.lobbyUpdateListener);
enableStartIfHost();
startRace();
} catch (IOException e) {
//TODO should probably let this propagate, so that we only enter this scene if everything works
@ -343,6 +353,22 @@ public class InGameLobbyController extends Controller {
}
}
/**
* Enables the start button if the client is the host of the game.
*/
private void enableStartIfHost() {
if (isHost) {
startButton.setVisible(true);
startButton.setDisable(false);
} else {
startButton.setVisible(false);
startButton.setDisable(true);
}
}
/**
* Menu button pressed. Prompt alert then return to menu
* @throws IOException socket erro
@ -370,7 +396,12 @@ public class InGameLobbyController extends Controller {
* Start button pressed. Currently only prints out start
*/
public void startBtnPressed(){
//System.out.println("Should start the race. This button is only visible for the host");
try {
HttpMatchBrowserHost.httpMatchBrowserHost.sendStarted();
} catch (IOException e) {
e.printStackTrace();
}
App.game.getRaceLogic().getRace().startRace(App.game.getRaceLogic().getRace().getRacePreparatoryTime(), true);
}
public void joinSpecPressed(){

@ -40,7 +40,7 @@ public class LobbyController extends Controller {
private @FXML TextField addressFld;
private @FXML TextField portFld;
private ObservableList<RaceConnection> connections;
private ObservableList<RaceConnection> allConnections;
private ObservableList<RaceConnection> customConnections;
private AudioClip sound;
@ -51,12 +51,20 @@ public class LobbyController extends Controller {
public void initialize() {
httpMatchBrowserClient = new HttpMatchBrowserClient();
httpMatchBrowserClient.connections.addListener(new ListChangeListener<RaceConnection>() {
@Override
public void onChanged(Change<? extends RaceConnection> 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<RaceConnection>() {
@Override
public void onChanged(Change<? extends RaceConnection> 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);
}
}

@ -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<Subject3D> viewSubjects = FXCollections.observableArrayList();
String arrowPath = "assets/mark_arrow.x3d";
Shape3D arrow = Assets3D.loadX3d(arrowPath);
arrow.setScaleX(15);
arrow.setScaleY(25);
arrow.setScaleZ(50);
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));
}
}

@ -1,6 +1,7 @@
package visualiser.Controllers;
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
import eu.hansolo.medusa.*;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
@ -12,20 +13,29 @@ 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.shape.Sphere;
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;
import shared.exceptions.BoatNotFoundException;
import shared.model.*;
import visualiser.app.App;
@ -38,11 +48,9 @@ import visualiser.model.*;
import visualiser.utils.GPSConverter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -61,9 +69,17 @@ public class RaceViewController extends Controller {
private boolean isTutorial = false;
private String keyToPress;
private View3D view3D;
private WindCompass windCompass;
private ObservableList<Subject3D> viewSubjects;
private Gauge gauge;
private FGauge fGauge;
private long positionDelay = 1000;
private long positionTime = 0;
private ResizableRaceCanvas raceCanvas;
private boolean mapToggle = true;
private GPSConverter gpsConverter;
private ArrayList<HealthEffect> healthEffectList = new ArrayList<>();
/**
* Arrow pointing to next mark in third person
@ -74,12 +90,22 @@ public class RaceViewController extends Controller {
*/
private AnimationTimer pointToMark;
//seagulls
private ObservableList<Subject3D> seagulls = FXCollections.observableArrayList();
//seagulls goto
private Map<Subject3D, List<Double>> seagullsGoToX = new HashMap<>();
private Map<Subject3D, List<Double>> seagullsGoToY = new HashMap<>();
private double seagullSpeed = 0.01;
// 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 GridPane canvasBase2;
private @FXML SplitPane racePane;
private @FXML StackPane arrowPane;
private @FXML Pane nextMarkPane;
private @FXML Label timer;
private @FXML Label FPS;
private @FXML Label timeZone;
@ -89,10 +115,17 @@ public class RaceViewController extends Controller {
private @FXML TableColumn<VisualiserBoat, String> boatTeamColumn;
private @FXML TableColumn<VisualiserBoat, Leg> boatMarkColumn;
private @FXML TableColumn<VisualiserBoat, Number> boatSpeedColumn;
private @FXML TableColumn<VisualiserBoat, Number> boatHealthColumn;
private @FXML LineChart<Number, Number> 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.
@ -107,11 +140,13 @@ public class RaceViewController extends Controller {
this.controllerClient = controllerClient;
this.isHost = isHost;
keyFactory.load();
deathPane.setDisable(false);
deathPane.setVisible(false);
tutorialCheck();
initKeypressHandler();
healthLoop();
initialiseRaceVisuals();
initialiseRaceCanvas();
}
/**
@ -123,7 +158,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");
@ -205,36 +239,121 @@ public class RaceViewController extends Controller {
}
/**
* Initialises the various UI components to listen to the {@link #visualiserRace}.
* Create speedometer
*/
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);
private void initialiseSpeedometer() {
//Create the Medusa Gauge
gauge = GaugeBuilder.create()
.prefSize(200,200)
.foregroundBaseColor(Color.WHITE)
.title("Title")
.subTitle("Speed")
.unit("Knots")
.decimals(2)
.lcdVisible(true)
.lcdDesign(LcdDesign.STANDARD)
.lcdFont(LcdFont.DIGITAL_BOLD)
.scaleDirection(Gauge.ScaleDirection.CLOCKWISE)
.minValue(0)
.maxValue(50)
.startAngle(320)
.angleRange(280)
.tickLabelDecimals(0)
.tickLabelLocation(TickLabelLocation.INSIDE)
.tickLabelOrientation(TickLabelOrientation.ORTHOGONAL)
.onlyFirstAndLastTickLabelVisible(false)
.tickLabelSectionsVisible(false)
.tickLabelColor(Color.BLACK)
.tickMarkSectionsVisible(false)
.majorTickMarksVisible(true)
.majorTickMarkType(TickMarkType.TRAPEZOID)
.mediumTickMarksVisible(false)
.mediumTickMarkType(TickMarkType.LINE)
.minorTickMarksVisible(true)
.minorTickMarkType(TickMarkType.LINE)
.ledVisible(false)
.ledType(Gauge.LedType.STANDARD)
.ledColor(Color.rgb(255, 200, 0))
.ledBlinking(false)
.needleShape(Gauge.NeedleShape.ANGLED)
.needleSize(Gauge.NeedleSize.STANDARD)
.needleColor(Color.CRIMSON)
.startFromZero(false)
.returnToZero(false)
.knobType(Gauge.KnobType.METAL)
.knobColor(Color.LIGHTGRAY)
.interactive(false)
.thresholdVisible(true)
.threshold(50)
.thresholdColor(Color.RED)
.checkThreshold(true)
.gradientBarEnabled(true)
.gradientBarStops(new Stop(0.0, Color.BLUE),
new Stop(0.25, Color.CYAN),
new Stop(0.5, Color.LIME),
new Stop(0.75, Color.YELLOW),
new Stop(1.0, Color.RED))
.markersVisible(true)
.build();
//Create a gauge with a frame and background that utilizes a Medusa gauge
fGauge = FGaugeBuilder
.create()
.prefSize(190, 190)
.gauge(gauge)
.gaugeDesign(GaugeDesign.METAL)
.gaugeBackground(GaugeDesign.GaugeBackground.CARBON)
.foregroundVisible(true)
.build();
speedPane.getChildren().add(fGauge);
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);
}
/**
* Initialises the various UI components to listen to the {@link #visualiserRace}.
*/
private void initialiseRaceVisuals() {
// initialise displays
initialiseFps();
initialiseInfoTable();
initialiseView3D(this.visualiserRace);
initialiseHealthPane();
initialiseRaceClock();
initialiseSpeedometer();
initialiseRaceCanvas();
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);
@ -268,24 +387,46 @@ public class RaceViewController extends Controller {
view3D.addPointLight(pointLight);
canvasBase.add(view3D, 0, 0);
windCompass = new WindCompass(view3D, this.raceState.windProperty());
arrowPane.getChildren().add(windCompass);
// Set up projection from GPS to view
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 = Assets3D.skyBox;
viewSubjects.addAll(skyBox.getSkyBoxPlanes());
// Set up sea surface
SeaSurface sea = new SeaSurface(750, 200);
sea.setX(250);
sea.setZ(210);
viewSubjects.add(sea);
// SeaSurface sea = new SeaSurface(750, 200);
// sea.setX(250);
// sea.setZ(210);
// viewSubjects.add(sea);
// Set up sea surface overlay
SeaSurface seaOverlay = new SeaSurface(4000, 200);
seaOverlay.setX(250);
seaOverlay.setZ(210);
viewSubjects.add(seaOverlay);
// SeaSurface seaOverlay = new SeaSurface(4000, 200);
// seaOverlay.setX(250);
// seaOverlay.setZ(210);
// viewSubjects.add(seaOverlay);
// int seaX = 15;
// int seaY = 15;
int seaX = 15;
int seaY = 15;
for (int x = 0; x < seaX; x++) {
for (int y = 0; y < seaY; y++) {
NewSeaSurface seaSurface = new NewSeaSurface();
Subject3D seaSubject = new Annotation3D(seaSurface);
seaSubject.setXRot(0);
// seaSubject.setX(-75 + x * 50);
// seaSubject.setZ(-150 + y * 50);
// seaSubject.setX(-150 + x * 250);
// seaSubject.setZ(-80 + y * 250);
seaSubject.setX(-75 + x * 100);
seaSubject.setZ(-150 + y * 100);
seaSubject.setY(3);
viewSubjects.add(seaSubject);
}
}
Boundary3D boundary3D = new Boundary3D(visualiserRace.getVisualiserRaceState().getRaceDataSource().getBoundary(), gpsConverter);
for (Subject3D subject3D: boundary3D.getBoundaryNodes()){
@ -293,8 +434,9 @@ public class RaceViewController extends Controller {
}
// Position and add each mark to view
for(Mark mark: race.getVisualiserRaceState().getMarks()) {
MeshView mesh = new MeshView(importerMark.getImport());
Subject3D markModel = new Subject3D(mesh, mark.getSourceID());
// MeshView mesh = new MeshView(importerMark.getImport());
// Subject3D markModel = new Subject3D(mesh, mark.getSourceID());
Subject3D markModel = new Subject3D(Assets3D.getMark(), mark.getSourceID());
markModel.setX(gpsConverter.convertGPS(mark.getPosition()).getX());
markModel.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());
@ -302,35 +444,158 @@ 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()) {
Shockwave boatHighlight = new Shockwave(10);
boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1)));
viewSubjects.add(boatHighlight);
// Add player boat highlight
// Shockwave boatHighlight = new Shockwave(10);
// boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1)));
// viewSubjects.add(boatHighlight);
viewSubjects.add(Assets3D.boatHighlight);
// Track player boat with camera
viewSubjects.add(boatModel);
Platform.runLater(() -> {
view3D.trackSubject(boatModel, 0);
view3D.setThirdPerson();
});
// Track player boat with highlight
AnimationTimer highlightTrack = new AnimationTimer() {
@Override
public void handle(long now) {
boatHighlight.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
boatHighlight.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
Assets3D.boatHighlight.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
Assets3D.boatHighlight.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
}
};
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());
// 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)));
//next mark indicator
changeNextMark(boat.getCurrentLeg());
viewSubjects.add(Assets3D.ccwNextArrow);
viewSubjects.add(Assets3D.cwNextArrow);
boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> changeNextMark(curr)));
} else {
mesh = new MeshView(importerBurgerBoat.getImport());
viewSubjects.add(boatModel);
boatModel.getMesh().toFront();
}
PhongMaterial boatColorMat = new PhongMaterial(boat.getColor());
mesh.setMaterial(boatColorMat);
Subject3D boatModel = new Subject3D(mesh, boat.getSourceID());
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());
viewSubjects.add(sailsSubject);
// SeagullFlock seagullFlock = new SeagullFlock(67, 43, 0);
//// seagullFlock.addToFlock();
//// seagullFlock.addToFlock();
// seagullFlock.setxBound(450);
// seagullFlock.setyBound(450);
// viewSubjects.addAll(seagullFlock.getSeagulls());
//
// Subject3D textSubject = new Annotation3D(new SeagullMesh());
// textSubject.setY(-10);
// viewSubjects.add(textSubject);
addToFlock();
seagullGoTo.start();
AnimationTimer rotateArrows = new AnimationTimer() {
@Override
public void handle(long now) {
Assets3D.ccwNextArrow.setHeading(Assets3D.ccwNextArrow.getHeading().getAngle() - 3.0);
Assets3D.cwNextArrow.setHeading(Assets3D.cwNextArrow.getHeading().getAngle() + 3.0);
}
};
rotateArrows.start();
AnimationTimer sailsFollowBoat = new AnimationTimer() {
double sailCurrent = visualiserRace.getVisualiserRaceState().getWindDirection().degrees();
boolean canLuff = true;
double turnRate = 5;
@Override
public void handle(long now) {
double sailDir;
//if sails are out etc
if (boat.isSailsOut()) {
double windDir = visualiserRace.getVisualiserRaceState().getWindDirection().degrees();
double windOffset = (360 - windDir + boat.getBearing().degrees()) % 360;
sailDir = windOffset / 180 * 270 + windDir + 180;
boolean leftOfWind = windOffset >= 180;
if (leftOfWind){
sailDir = -sailDir;
} else {
sailDir = windDir - sailDir;
}
} else {
sailDir = visualiserRace.getVisualiserRaceState().getWindDirection().degrees();
}
//get new place to move towards
double compA = ((sailCurrent - sailDir) % 360 + 360) % 360;//degrees right
if (compA > 180) compA = 360 - compA;
double compB = ((sailDir - sailCurrent) % 360 + 360) % 360;//degrees left
if (compB > compA){
if (compA > turnRate){
sailCurrent = ((sailCurrent - turnRate) % 360 + 360) % 360;
canLuff = false;
} else {
sailCurrent = sailDir;
canLuff = true;
}
} else {
if (compB > turnRate){
sailCurrent = ((sailCurrent + turnRate) % 360 + 360) % 360;
canLuff = false;
} else {
sailCurrent = sailDir;
canLuff = true;
}
}
sailsSubject.setHeading(sailCurrent);
if (canLuff) {
if (boat.isSailsOut()) {
if (sails3D.isLuffing()) {
sails3D.stopLuffing();
}
} else {
if (!sails3D.isLuffing()) {
sails3D.startLuffing();
}
}
}
sailsSubject.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
sailsSubject.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
sailsSubject.getMesh().toFront();
}
};
sailsFollowBoat.start();
// Track this boat's movement with the new subject
AnimationTimer trackBoat = new AnimationTimer() {
@ -339,21 +604,31 @@ 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();
Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1));
CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark();
//next mark indicator
//Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1));
/*CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark();
view3D.getShape(nextMark.getMark1().getSourceID()).getMesh().setMaterial(markColor);
if(nextMark.getMark2() != null) {
view3D.getShape(nextMark.getMark2().getSourceID()).getMesh().setMaterial(markColor);
}
Subject3D shockwave = new Shockwave(10);
}*/
Subject3D shockwave = new Shockwave(10);
viewSubjects.add(shockwave);
boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr)));
//boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr)));
boat.hasCollidedProperty().addListener((o, prev, curr) -> Platform.runLater(() -> showCollision(boat, shockwave)));
}
// Fix initial bird's-eye position
@ -369,7 +644,12 @@ public class RaceViewController extends Controller {
// Bind zooming to scrolling
view3D.setOnScroll(e -> {
view3D.updateDistance(e.getDeltaY());
//view3D.updateDistance(e.getDeltaY());
if (e.getDeltaY() > 0) {
view3D.zoomIn();
} else {
view3D.zoomOut();
}
});
// Bind zooming to keypress (Z/X default)
@ -390,7 +670,7 @@ public class RaceViewController extends Controller {
}
}
}
view3D.updateDistance(-10);
view3D.zoomIn();
break;
case "Zoom Out":
//Check if race is a tutorial
@ -405,7 +685,7 @@ public class RaceViewController extends Controller {
}
}
}
view3D.updateDistance(10);
view3D.zoomOut();
break;
}
}
@ -438,44 +718,99 @@ 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();
}
private void setNextMarkArrowDirection(Mark mark1, Mark mark2, RoundingType roundingType, int direction){
boolean port = roundingType == RoundingType.Port;
Mark chosenMark = null;
if (mark2 == null){
chosenMark = mark1;
} else {
boolean rounding = port;
switch (direction) {
case 2:
rounding = !port;
case 0:
if (rounding){
if (mark1.getPosition().getLongitude() < mark2.getPosition().getLongitude()){
chosenMark = mark1;
} else {
chosenMark = mark2;
}
} else {
if (mark1.getPosition().getLongitude() > mark2.getPosition().getLongitude()){
chosenMark = mark1;
} else {
chosenMark = mark2;
}
}
break;
case 3:
rounding = !port;
case 1:
if (rounding){
if (mark1.getPosition().getLatitude() > mark2.getPosition().getLatitude()){
chosenMark = mark1;
} else {
chosenMark = mark2;
}
} else {
if (mark1.getPosition().getLatitude() < mark2.getPosition().getLatitude()){
chosenMark = mark1;
} else {
chosenMark = mark2;
}
}
break;
}
}
if (chosenMark == null){
System.err.println("Mark to pick is null.");
return;
}
if (port){
Assets3D.ccwNextArrow.setX(gpsConverter.convertGPS(chosenMark.getPosition()).getX());
Assets3D.ccwNextArrow.setZ(gpsConverter.convertGPS(chosenMark.getPosition()).getY());
Assets3D.ccwNextArrow.getMesh().setVisible(true);
Assets3D.cwNextArrow.getMesh().setVisible(false);
} else {
Assets3D.cwNextArrow.setX(gpsConverter.convertGPS(chosenMark.getPosition()).getX());
Assets3D.cwNextArrow.setZ(gpsConverter.convertGPS(chosenMark.getPosition()).getY());
Assets3D.cwNextArrow.getMesh().setVisible(true);
Assets3D.ccwNextArrow.getMesh().setVisible(false);
}
}
/**
* Swap the colour of the next mark to pass with the last mark passed
* @param leg boat has started on
*/
private void swapColours(Leg leg) {
private void changeNextMark(Leg leg){
CompoundMark start = leg.getStartCompoundMark();
CompoundMark end = leg.getEndCompoundMark();
@ -484,23 +819,28 @@ public class RaceViewController extends Controller {
return;
}
Shape3D start1 = view3D.getShape(start.getMark1().getSourceID()).getMesh();
Shape3D end1 = view3D.getShape(end.getMark1().getSourceID()).getMesh();
Material nextMark = start1.getMaterial();
Material lastMark = end1.getMaterial();
start1.setMaterial(lastMark);
if(start.getMark2() != null) {
Shape3D start2 = view3D.getShape(start.getMark2().getSourceID()).getMesh();
start2.setMaterial(lastMark);
//find direction coming
double angle = gpsConverter.getAngle(gpsConverter.convertGPS(start.getMark1().getPosition()),
gpsConverter.convertGPS(end.getMark1Position()));
angle = (Math.toDegrees(angle) % 360 + 360) % 360;
int dir = 0; //0 = top 1 = right 2 = down 3 = down
if (angle < 315){
if (angle > 45){
dir ++;
}
if (angle > 135){
dir ++;
}
if (angle > 225){
dir ++;
}
}
end1.setMaterial(nextMark);
if(end.getMark2() != null) {
Shape3D end2 = view3D.getShape(end.getMark2().getSourceID()).getMesh();
end2.setMaterial(nextMark);
}
Mark startMark1 = end.getMark1();
Mark startMark2 = end.getMark2();
setNextMarkArrowDirection(startMark1, startMark2, end.getRoundingType(), dir);
}
/**
@ -544,13 +884,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(
@ -650,6 +994,101 @@ public class RaceViewController extends Controller {
}.start();
}
/**
* Animation timer loop for the speedometer
*/
private void speedometerLoop(){
new AnimationTimer(){
@Override
public void handle(long arg0){
if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
stop(); // stop the timer
} else {
try {
//Set the current speed value of the boat
gauge.setValue(raceState.getBoat(raceState.getPlayerBoatID()).getCurrentSpeed());
fGauge.getGauge().setValue(raceState.getBoat(raceState.getPlayerBoatID()).getCurrentSpeed());
//Create list with sorted boat placements
List<VisualiserBoat> boatList = boatInfoTable.getItems();
for (VisualiserBoat boat : boatList){
if(raceState.getPlayerBoatID()==boat.getSourceID()){
//Set boat current placement value as title of speedometer
gauge.titleProperty().setValue("Position: " + (boatInfoTable.getItems().indexOf(boat)+1));
fGauge.getGauge().titleProperty().setValue("Position: " + (boatInfoTable.getItems().indexOf(boat)+1));
}
}
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
}
}
}.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());
Annotation3D sharks = new Annotation3D(Assets3D.getSharks());
sharks.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
sharks.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
viewSubjects.add(sharks);
new AnimationTimer(){
@Override
public void handle(long now) {
sharks.setHeading(sharks.getHeading().getAngle() - 0.5);
}
}.start();
fp.setSourceID(0);
}
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
*/
@ -783,25 +1222,115 @@ public class RaceViewController extends Controller {
private void bigMap(){
if (mapToggle){
raceCanvas.widthProperty().bind(canvasBase.widthProperty());
raceCanvas.heightProperty().bind(canvasBase.heightProperty());
raceCanvas.widthProperty().bind(canvasBase2.widthProperty());
raceCanvas.heightProperty().bind(canvasBase2.heightProperty());
raceCanvas.setFullScreen(true);
raceCanvas.setOpacity(0.6);
canvasBase1.getChildren().remove(raceCanvas);
canvasBase.getChildren().add(1, raceCanvas);
canvasBase2.getChildren().add(0, raceCanvas);
}else{
raceCanvas.widthProperty().bind(canvasBase1.widthProperty());
raceCanvas.heightProperty().bind(canvasBase1.heightProperty());
raceCanvas.setFullScreen(false);
raceCanvas.setOpacity(1);
canvasBase.getChildren().remove(raceCanvas);
canvasBase2.getChildren().remove(raceCanvas);
canvasBase1.getChildren().add(0, raceCanvas);
}
mapToggle = !mapToggle;
}
/**
* FXML method for death button
*/
public void deathOKPressed(){
deathPane.setDisable(true);
deathPane.setVisible(false);
}
private AnimationTimer seagullGoTo = new AnimationTimer() {
@Override
public void handle(long now) {
for (Subject3D seagull: seagulls) {
if (seagullsGoToX.get(seagull).size() > 0) {
//System.out.println(xPosition + " " + yPosition);
seagull.setHeading(GPSConverter.getAngle(new GraphCoordinate(seagull.getX(), seagull.getZ()),
new GraphCoordinate(seagullsGoToX.get(seagull).get(0), seagullsGoToY.get(seagull).get(0))));
double delx = seagullsGoToX.get(seagull).get(0) - seagull.getX();
double dely = seagullsGoToY.get(seagull).get(0) - seagull.getZ();
double scale = seagullSpeed / Math.sqrt(delx * delx + dely * dely);
if (scale < 1) {
seagull.setX(seagull.getX() + delx * scale);
seagull.setZ(seagull.getZ() + dely * scale);
} else {
seagullsGoToX.get(seagull).remove(0);
seagullsGoToY.get(seagull).remove(0);
}
} else {
randSeagullNewAction(seagull);
}
}
}
};
public void randSeagullNewAction(Subject3D seagull){
Random rand = new Random();
int nextAction = rand.nextInt(1);
switch(nextAction){
case 0:
//do a straight line
double nextX = rand.nextInt((int)gpsConverter.getLongitudeFactor());
/*if (nextX > this.xBound/2){
nextX = - nextX/2;
}*/
double nextY = rand.nextInt((int)gpsConverter.getLatitudeFactor());
/*if (nextY > this.yBound/2){
nextY = - nextY/2;
}*/
seagullsGoToX.get(seagull).add(nextX);
seagullsGoToY.get(seagull).add(nextY);
break;
case 1:
//do a octogan circle
seagullsGoToX.get(seagull).add(seagull.getX() - 3);
seagullsGoToX.get(seagull).add(seagull.getX() - 3);
seagullsGoToX.get(seagull).add(seagull.getX());
seagullsGoToX.get(seagull).add(seagull.getX() + 3);
seagullsGoToX.get(seagull).add(seagull.getX() + 6);
seagullsGoToX.get(seagull).add(seagull.getX() + 6);
seagullsGoToX.get(seagull).add(seagull.getX() + 3);
seagullsGoToX.get(seagull).add(seagull.getX());
//y
seagullsGoToY.get(seagull).add(seagull.getZ() - 3);
seagullsGoToY.get(seagull).add(seagull.getZ() - 6);
seagullsGoToY.get(seagull).add(seagull.getZ() - 9);
seagullsGoToY.get(seagull).add(seagull.getZ() - 9);
seagullsGoToY.get(seagull).add(seagull.getZ() - 6);
seagullsGoToY.get(seagull).add(seagull.getZ() - 3);
seagullsGoToY.get(seagull).add(seagull.getZ());
seagullsGoToY.get(seagull).add(seagull.getZ());
break;
}
}
public void addToFlock(){
Annotation3D newSeagull = new Annotation3D(new SeagullMesh());
newSeagull.setY(-15);
newSeagull.setX(67);
newSeagull.setZ(43);
newSeagull.setXRot(0);
seagulls.add(newSeagull);
seagullsGoToX.put(newSeagull, new ArrayList<>());
seagullsGoToY.put(newSeagull, new ArrayList<>());
viewSubjects.add(newSeagull);
randSeagullNewAction(newSeagull);
}
}

@ -2,6 +2,7 @@ package visualiser.Controllers;
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
@ -9,8 +10,10 @@ import javafx.scene.AmbientLight;
import javafx.scene.PointLight;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.media.AudioClip;
@ -41,14 +44,18 @@ import java.util.logging.Logger;
* the game.
*/
public class TitleController extends Controller {
private @FXML AnchorPane titleWrapper;
private @FXML RadioButton dayModeRD;
private @FXML RadioButton nightModeRD;
private @FXML Button tutorialButton;
private @FXML Pane menuPane;
private @FXML ImageView imgSun;
private @FXML GridPane view3DContainer;
private ToggleGroup toggleGroup = new ToggleGroup();
public void initialize() {
dayModeRD.setToggleGroup(toggleGroup);
nightModeRD.setToggleGroup(toggleGroup);
AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF"));
ambientLight.setTranslateX(250);
ambientLight.setTranslateZ(210);
@ -102,6 +109,8 @@ public class TitleController extends Controller {
}
};
loop.start();
titleWrapper.getStylesheets().add("/css/dayMode.css");
}
/**
@ -128,10 +137,7 @@ public class TitleController extends Controller {
*/
public void setDayMode(){
dayModeRD.getScene().getStylesheets().clear();
menuPane.getStylesheets().clear();
imgSun.setImage(new Image(getClass().getResource("/visualiser/images/sun.png").toExternalForm()));
dayModeRD.getScene().getStylesheets().add("/css/dayMode.css");
menuPane.setStyle("-fx-background-color: #6be6ff;");
nightModeRD.setSelected(false);
App.dayMode = true;
}
@ -141,10 +147,7 @@ public class TitleController extends Controller {
*/
public void setNightMode(){
nightModeRD.getScene().getStylesheets().clear();
menuPane.getStylesheets().clear();
//imgSun.setImage(new Image(getClass().getResource("/visualiser/images/sunsleep.png").toExternalForm()));
nightModeRD.getScene().getStylesheets().add("/css/nightMode.css");
menuPane.setStyle("-fx-background-color: #1f2c60;");
dayModeRD.setSelected(false);
App.dayMode = false;
}

@ -25,6 +25,7 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import mock.app.Event;
import visualiser.layout.Assets3D;
public class App extends Application {
private static Stage stage;
@ -113,6 +114,7 @@ public class App extends Application {
}
);
new Thread(boatTask).start();
Assets3D.loadAssets();
}
/**

@ -0,0 +1,173 @@
package visualiser.layout;
import com.interactivemesh.jfx.importer.obj.ObjModelImporter;
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
import com.interactivemesh.jfx.importer.x3d.X3dModelImporter;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import visualiser.Controllers.HostGameController;
import java.net.URL;
/**
* Created by Gondr on 21/09/2017.
*/
public class Assets3D {
public static MeshView[] sails;
public static MeshView[] sea;
public static MeshView[] seagull;
public static Subject3D windArrow;
public static Subject3D compass;
public static Subject3D cwNextArrow;
public static Subject3D ccwNextArrow;
public static SkyBox skyBox;
public static Subject3D boatHighlight;
public static Subject3D sharks;
public static void loadAssets(){
loadSails();
loadWindArrow();
loadNextArrow();
loadSeaSurface();
loadSkybox();
loadSeagull();
loadBoatHightlight();
}
private static void loadNextArrow(){
StlMeshImporter objModelImporter = new StlMeshImporter();
String cwPath = "assets/Next Mark Arrow CW V1.0.stl";
URL asset = Assets3D.class.getClassLoader().getResource(cwPath);
objModelImporter.read(asset);
Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1));
MeshView cwMesh = new MeshView(objModelImporter.getImport());
cwMesh.setMaterial(markColor);
cwMesh.setMouseTransparent(true);
cwMesh.toBack();
cwMesh.setVisible(false);
cwNextArrow = new Annotation3D(cwMesh);
String ccwPath = "assets/Next Mark Arrow CCW V1.0.stl";
URL ccwAsset = Assets3D.class.getClassLoader().getResource(ccwPath);
objModelImporter.read(ccwAsset);
MeshView ccwMesh = new MeshView(objModelImporter.getImport());
ccwMesh.setMaterial(markColor);
ccwMesh.setMouseTransparent(true);
ccwMesh.toBack();
cwMesh.setVisible(false);
ccwNextArrow = new Annotation3D(ccwMesh);
}
private static void loadBoatHightlight(){
Material markColor = new PhongMaterial(new Color(0,1,0,0.5));
StlMeshImporter objModelImporter = new StlMeshImporter();
String path = "assets/V1.0 Boat Highlight.stl";
URL highlight = Assets3D.class.getClassLoader().getResource(path);
objModelImporter.read(highlight);
MeshView hMesh = new MeshView(objModelImporter.getImport());
hMesh.setMaterial(markColor);
hMesh.setMouseTransparent(true);
hMesh.toBack();
boatHighlight = new Subject3D(hMesh, 0);
}
private static void loadSails(){
sails = new MeshView[40];
ObjModelImporter objModelImporter = new ObjModelImporter();
for (int i = 0; i < sails.length; i++){
String path = String.format("assets/Sails/V1.5 Sail_%06d.obj", i + 1);
URL asset = Assets3D.class.getClassLoader().getResource(path);
objModelImporter.read(asset);
if (objModelImporter.getImport().length > 0) {
sails[i] = objModelImporter.getImport()[0];
/*sails[i].setRotationAxis(new Point3D(1, 0, 0));
sails[i].setRotate(-90);*/
}
}
}
private static void loadSeaSurface(){
sea = new MeshView[100];
ObjModelImporter objModelImporter = new ObjModelImporter();
for (int i = 0; i < sea.length; i++){
// String path = String.format("assets/Ocean V1.0 Small Animation/Ocean V1.0 Large Animation_%06d.obj", i + 1);
// String path = String.format("assets/Ocean V1.0 Animation/Ocean V1.0_%06d.obj", i + 1);
String path = String.format("assets/Ocean Animation/Ocean Animation_%06d.obj", i + 1);
URL asset = Assets3D.class.getClassLoader().getResource(path);
objModelImporter.read(asset);
if (objModelImporter.getImport().length > 0) {
sea[i] = objModelImporter.getImport()[0];
}
}
}
private static void loadSeagull(){
seagull = new MeshView[30];
ObjModelImporter objModelImporter = new ObjModelImporter();
for (int i = 0; i < seagull.length; i++){
String path = String.format("assets/V1.1 Animated/Seagull V1.1_%06d.obj", i + 1);
URL asset = Assets3D.class.getClassLoader().getResource(path);
objModelImporter.read(asset);
if (objModelImporter.getImport().length > 0) {
seagull[i] = objModelImporter.getImport()[0];
}
}
}
public static Shape3D getBoat(){
String path = "assets/V1.4 Boat.x3d";
return loadX3d(path);
}
public static Shape3D getMark(){
String path = "assets/Burger Bouy V1.1.x3d";
return loadX3d(path);
}
private static void loadWindArrow(){
String compassPath = "assets/wind_compass.x3d";
compass = new Annotation3D(loadX3d(compassPath));
String arrowPath = "assets/wind_arrow.x3d";
windArrow = new Annotation3D(loadX3d(arrowPath));
}
public static Shape3D getSharks(){
String path = "assets/V1.0 Sharks.x3d";
return loadX3d(path);
}
public static Shape3D loadX3d(String path){
X3dModelImporter x3dModelImporter = new X3dModelImporter();
URL asset = Assets3D.class.getClassLoader().getResource(path);
x3dModelImporter.read(asset);
if (x3dModelImporter.getImport().length > 0) {
//if (x3dModelImporter.getImport()[0] instanceof MeshView) {
Group g = (Group)((Group)((Group) x3dModelImporter.getImport()[0]).getChildren().get(0)).getChildren().get(0);
Shape3D shape = (Shape3D)g.getChildren().get(0);
return shape;
/*} else {
System.err.println("Boat is not a Mesh View 0.0");
}*/
}
return null;
}
private static void loadSkybox(){
skyBox = new SkyBox(750, 200, 250, 0, 210);
}
}

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

@ -0,0 +1,31 @@
package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.MeshView;
/**
* Created by Gondr on 27/09/2017.
*/
public class NewSeaSurface extends MeshView{
private int seaIndex = 0;
private AnimationTimer seaRipple = new AnimationTimer() {
@Override
public void handle(long now) {
seaIndex = (seaIndex + 1) % Assets3D.sea.length;
setMesh(Assets3D.sea[seaIndex].getMesh());
}
};
public NewSeaSurface(){
super(Assets3D.sea[0].getMesh());
Color seaBlue = new Color(0.284, 0.573, 1.00, 1);
Material seaMat = new PhongMaterial(seaBlue);
setMaterial(seaMat);
setMouseTransparent(true);
seaRipple.start();
}
}

@ -0,0 +1,40 @@
package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.scene.shape.Mesh;
import javafx.scene.shape.MeshView;
/**
* Created by Gondr on 21/09/2017.
*/
public class Sails3D extends MeshView {
private int index = 0;
private boolean isLuffing = false;
public Sails3D() {
setMesh(Assets3D.sails[0].getMesh());
}
private AnimationTimer luff = new AnimationTimer() {
@Override
public void handle(long now) {
setMesh(Assets3D.sails[index].getMesh());
index = (index + 1) % 40;
}
};
public void startLuffing(){
luff.start();
isLuffing = true;
}
public void stopLuffing(){
luff.stop();
isLuffing = false;
setMesh(Assets3D.sails[0].getMesh());
}
public boolean isLuffing() {
return isLuffing;
}
}

@ -1,5 +1,6 @@
package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.geometry.Point3D;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@ -16,7 +17,7 @@ import visualiser.utils.PerlinNoiseGenerator;
/**
* Creates a SeaSurface
*/
public class SeaSurface extends Subject3D {
public class SeaSurface extends Annotation3D {
private static Image image;
/**
@ -26,7 +27,7 @@ public class SeaSurface extends Subject3D {
* @param freq frequency the perlin noise is to be generated at
*/
public SeaSurface(int size, double freq) {
super(createSurface(size, freq), 0);
super(createSurface(size, freq));
image = new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterDown2048.png"));
}

@ -0,0 +1,144 @@
package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.transform.Translate;
import visualiser.model.GraphCoordinate;
import visualiser.utils.GPSConverter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Created by Gondr on 28/09/2017.
*/
public class SeagullFlock{
private ObservableList<Subject3D> seagulls = FXCollections.observableArrayList();
private double xPos = 0;
private double yPos = 0;
private double zPos = 20;
private double heading = 0;
private double[] xOffset = {0, -5, -5};
private double[] yOffset = {0, -5, 5};
private double xBound = Integer.MAX_VALUE;//-/+
private double yBound = Integer.MAX_VALUE;
private List<Double> goToX = new ArrayList<>();
private List<Double> goToY = new ArrayList<>();
private double speed = 0.01;
private AnimationTimer goTo = new AnimationTimer() {
@Override
public void handle(long now) {
if (goToX.size() > 0) {
//System.out.println(xPosition + " " + yPosition);
heading = GPSConverter.getAngle(new GraphCoordinate(xPos, yPos),
new GraphCoordinate(goToX.get(0), goToY.get(0)));
double delx = goToX.get(0) - xPos;
double dely = goToY.get(0) - yPos;
double scale = speed / Math.sqrt(delx * delx + dely * dely);
if (scale < 1) {
xPos += delx * scale;
yPos += dely * scale;
} else {
goToX.remove(0);
goToY.remove(0);
}
}else {
stop();
randNewAction();
}
}
};
private AnimationTimer update = new AnimationTimer() {
@Override
public void handle(long now) {
for (int i = 0; i < seagulls.size(); i++){
Subject3D seagull = seagulls.get(i);
seagull.getMesh().setTranslateY(zPos);
seagull.setHeading(heading);
seagull.setX(xPos + xOffset[i]);
seagull.setZ(yPos + yOffset[i]);
}
}
};
public SeagullFlock() {
this(0d, 0d, 0d);
}
public SeagullFlock(double x, double y, double z){
xPos = x;
yPos = y;
zPos = z;
addToFlock();
update.start();
randNewAction();
}
public void addToFlock(){
if (seagulls.size() < 3) {
Annotation3D newSeagull = new Annotation3D(new SeagullMesh());
newSeagull.setXRot(0);
seagulls.add(newSeagull);
// seagulls.add(new Annotation3D(new Sphere(5)));
}
}
public void setxBound(double xBound) {
this.xBound = xBound;
}
public void setyBound(double yBound) {
this.yBound = yBound;
}
public void randNewAction(){
Random rand = new Random();
int nextAction = rand.nextInt(1);
switch(nextAction){
case 0:
//do a straight line
double nextX = rand.nextInt((int)this.xBound);
/*if (nextX > this.xBound/2){
nextX = - nextX/2;
}*/
double nextY = rand.nextInt((int)this.yBound);
/*if (nextY > this.yBound/2){
nextY = - nextY/2;
}*/
goToX.add(nextX);
goToY.add(nextY);
break;
case 1:
//do a octogan circle
goToX.add(xPos - 3);
goToX.add(xPos - 3);
goToX.add(xPos);
goToX.add(xPos + 3);
goToX.add(xPos + 6);
goToX.add(xPos + 6);
goToX.add(xPos + 3);
goToX.add(xPos);
//y
goToX.add(yPos - 3);
goToX.add(yPos - 6);
goToX.add(yPos - 9);
goToX.add(yPos - 9);
goToX.add(yPos - 6);
goToX.add(yPos - 3);
goToX.add(yPos);
goToX.add(yPos);
break;
}
goTo.start();
}
public List<Subject3D> getSeagulls() {
return seagulls;
}
}

@ -0,0 +1,60 @@
package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.MeshView;
import java.util.Random;
/**
* Created by Gondr on 28/09/2017.
*/
public class SeagullMesh extends MeshView{
private int index = 0;
private int isFlapping = 0;
private int flapPeriod;
private int flapStrength;
private int periodElapsed = 0;
public SeagullMesh() {
setMesh(Assets3D.seagull[0].getMesh());
PhongMaterial white = new PhongMaterial(Color.WHITE);
setMaterial(white);
Random rand = new Random();
flapPeriod = rand.nextInt(9);
flapPeriod = 60 * (11 + flapPeriod);
flapStrength = rand.nextInt(3) + 2;
scheduledFlap.start();
}
private AnimationTimer flap = new AnimationTimer() {
@Override
public void handle(long now) {
index = (index + 1) % Assets3D.seagull.length;
setMesh(Assets3D.seagull[index].getMesh());
if (index == 0){
isFlapping ++;
if (isFlapping >= flapStrength){
stop();
setMesh(Assets3D.seagull[0].getMesh());
isFlapping = 0;
}
}
}
};
private AnimationTimer scheduledFlap = new AnimationTimer() {
@Override
public void handle(long now) {
if (periodElapsed == 0 && isFlapping == 0){
startFlapping();
}
periodElapsed = (periodElapsed + 1) % flapPeriod;
}
};
public void startFlapping(){
flap.start();
}
}

@ -28,8 +28,13 @@ public class Subject3D {
*/
private Rotate heading;
private Rotate xRot;
private Rotate yRot;
private Scale scale;
/**
* Constructor for view subject wrapper
* @param mesh to be rendered
@ -41,7 +46,9 @@ public class Subject3D {
this.scale = new Scale();
this.position = new Translate();
this.heading = new Rotate(0, Rotate.Y_AXIS);
this.mesh.getTransforms().addAll(position, scale, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS));
this.xRot = new Rotate(90, Rotate.X_AXIS);
this.yRot = new Rotate(180, Rotate.Y_AXIS);
this.mesh.getTransforms().addAll(position, scale, heading, xRot, yRot);
}
public Shape3D getMesh() {
@ -70,15 +77,35 @@ public class Subject3D {
position.setX(x);
}
public double getX(){
return position.getX();
}
public void setY(double y) {
position.setY(y);
}
public double getY(){
return position.getY();
}
public void setZ(double z) {
position.setZ(z);
}
public double getZ(){
return position.getZ();
}
public void setHeading(double angle) {
heading.setAngle(angle);
}
public void setXRot(double angle){
xRot.setAngle(angle);
}
public void setYRot(double angle){
yRot.setAngle(angle);
}
}

@ -3,7 +3,6 @@ package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.*;
@ -78,11 +77,17 @@ public class View3D extends Pane {
/**
* Distance to switch from third person to bird's eye
*/
private final double THIRD_PERSON_LIMIT = 100;
private final double THIRD_PERSON_LIMIT = 500;
/**
* Distance to stop zoom
*/
private final double FIRST_PERSON_LIMIT = 2;
private final double ZOOM_IN_LIMIT = 15;
private final double ZOOM_OUT_LIMIT = 700;
private final double MAX_ZOOM_LIMIT = 1150;
private final double MAX_PITCH = 60; // birds eye view
private final double MIN_PITCH = 5; // third person view
private final double ZOOM_PER_KEYPRESS = 5; // distance changed per zoom
private double itemScale = 1;
/**
* Default constructor for View3D. Sets up Scene and PerspectiveCamera.
@ -183,12 +188,9 @@ public class View3D extends Pane {
* Configures camera to third person view
*/
public void setThirdPerson() {
this.setDistance(THIRD_PERSON_LIMIT / 2);
this.setPitch(10);
for(Subject3D item: items) {
item.setScale(0.1);
}
this.setDistance(ZOOM_IN_LIMIT * 5);
adjustPitchForZoom();
adjustScaleForZoom();
}
/**
@ -196,17 +198,14 @@ public class View3D extends Pane {
*/
public void setBirdsEye() {
this.setYaw(0);
this.setPitch(60);
for(Subject3D item: items) {
item.setScale(1);
}
this.setPitch(MAX_PITCH);
adjustScaleForZoom();
}
/**
* Stop camera from following the last selected subject
*/
private void untrackSubject() {
public void untrackSubject() {
if(target.get() != null) {
trackBoat.stop();
target.setValue(null);
@ -268,17 +267,81 @@ public class View3D extends Pane {
* @param delta amount to change distance by
*/
public void updateDistance(double delta) {
double distance = -this.distance.getZ() + delta;
if(distance <= FIRST_PERSON_LIMIT) {
this.setDistance(FIRST_PERSON_LIMIT);
} else if(distance > THIRD_PERSON_LIMIT) {
this.setDistance(distance);
double newDistance = -this.distance.getZ() + delta;
if (target.get() == null){
if (newDistance > MAX_ZOOM_LIMIT) {
setDistance(MAX_ZOOM_LIMIT);
} else if (newDistance <= ZOOM_IN_LIMIT) {
setDistance(ZOOM_IN_LIMIT);
} else {
setDistance(newDistance);
}
} else if(newDistance <= ZOOM_IN_LIMIT) {
setDistance(ZOOM_IN_LIMIT);
} else if (newDistance > ZOOM_OUT_LIMIT){
untrackSubject();
setBirdsEye();
setDistance(1050);
updatePivot(new Translate(250, 0, 210));
setYaw(0);
} else {
this.setDistance(distance);
setDistance(newDistance);
}
adjustPitchForZoom();
adjustScaleForZoom();
}
/**
* Adjusts the scale size of boats and markers as a user zooms in or out,
* to smooth the change between third person to birds eye view.
*/
private void adjustScaleForZoom(){
double itemScale = (((-distance.getZ() - (ZOOM_IN_LIMIT * 2)) /
((THIRD_PERSON_LIMIT - (ZOOM_IN_LIMIT * 2)) /
(1 - 0.1))) + 0.1);
// if zoomed right in
if (itemScale < 0.1){
itemScale = 0.1;
// if zoomed right out
} else if (itemScale > 1) {
itemScale = 1;
}
// update scale
for (Subject3D item : items) {
item.setScale(itemScale);
}
}
/**
* Adjusts the pitch as a user zooms in or out, to smooth the change
* between third person to birds eye view.
*/
private void adjustPitchForZoom(){
double pitch = (((-distance.getZ() - (ZOOM_IN_LIMIT*2)) /
((THIRD_PERSON_LIMIT - (ZOOM_IN_LIMIT*2)) / (MAX_PITCH -
MIN_PITCH))) + MIN_PITCH);
// if pitch is out of bounds, set it to the min/max
if (pitch < MIN_PITCH) {
pitch = MIN_PITCH;
} else if (pitch > MAX_PITCH){
pitch = MAX_PITCH;
}
setPitch(pitch);
}
/**
* Method to be called when the zoom in key is pressed.
*/
public void zoomIn(){
updateDistance(-ZOOM_PER_KEYPRESS);
}
/**
* Method to be called when the zoom out key is pressed.
*/
public void zoomOut(){
updateDistance(ZOOM_PER_KEYPRESS);
}
/**
@ -289,6 +352,10 @@ public class View3D extends Pane {
this.yaw.setAngle(yaw);
}
public double getYaw(){
return this.yaw.getAngle();
}
/**
* Set elevation of camera
* @param pitch in degrees
@ -297,6 +364,10 @@ public class View3D extends Pane {
this.pitch.setAngle(-pitch);
}
public double getPitch(){
return this.pitch.getAngle();
}
public void addAmbientLight(AmbientLight ambientLight) {
this.world.getChildren().add(ambientLight);
}
@ -304,4 +375,4 @@ public class View3D extends Pane {
public void addPointLight(PointLight pointLight) {
this.world.getChildren().add(pointLight);
}
}
}

@ -0,0 +1,50 @@
package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.beans.property.Property;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import shared.model.Wind;
/**
* Created by Gondr on 25/09/2017.
*/
public class WindCompass extends View3D {
Subject3D compass;
Subject3D windArrow;
View3D view3D;
Property<Wind> wind;
AnimationTimer followView3D = new AnimationTimer() {
@Override
public void handle(long now) {
setYaw(180+ view3D.getYaw());
setPitch(30 - view3D.getPitch());
windArrow.setHeading(wind.getValue().getWindDirection().degrees());
}
};
public WindCompass(View3D view3D, Property<Wind> wind){
super(false);
this.wind = wind;
this.view3D = view3D;
this.followView3D.start();
ObservableList<Subject3D> subjects = FXCollections.observableArrayList();
this.setItems(subjects);
compass = Assets3D.compass;
windArrow = Assets3D.windArrow;
subjects.addAll(compass, windArrow);
this.setDistance(30);
this.setPitch(90);
this.setYaw(180);
}
public void setHeading(double heading){
windArrow.setHeading(heading);
}
public void setCompassPitch(double pitch){
setPitch(90d + pitch);
}
}

@ -605,7 +605,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//Prepare to draw.
gc.setLineWidth(1);
gc.setFill(Color.DEEPSKYBLUE);
gc.setFill(Color.web("#f1f1d4"));
//Calculate the screen coordinates of the boundary.
@ -647,7 +647,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//rounding lines
if (isFullScreen){
drawRoundingLines();
// drawRoundingLines();
drawRaceLine();
}
@ -794,18 +794,11 @@ public class ResizableRaceCanvas extends ResizableCanvas {
}
private Color getLineColor(Leg leg) {
for (VisualiserBoat boat : raceState.getBoats()) {
if (boat.isClientBoat()) {
if (boat.getCurrentLeg() == leg) {
return Color.ORANGE;
} else {
return Color.MEDIUMAQUAMARINE;
}
}else{
return Color.MEDIUMAQUAMARINE;
}
if(ThisBoat.getInstance().getLegNumber() == leg.getLegNumber()){
return Color.ORANGE;
}else{
return Color.MEDIUMAQUAMARINE;
}
return Color.MEDIUMAQUAMARINE;
}
private void drawArrowHead(GPSCoordinate start, GPSCoordinate end){

@ -35,6 +35,14 @@ public class ThisBoat {
}
}
public int getLegNumber(){
if(this.boat != null){
return this.boat.getCurrentLeg().getLegNumber();
}else{
return 0;
}
}
public void setBoat(VisualiserBoat boat) {
this.boat = boat;
}

@ -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<GPSCoordinate> positionProperty;
private ObjectProperty<Bearing> 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);
}
}

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

@ -96,6 +96,10 @@ public class HttpMatchBrowserHost extends Thread {
}
}
public void sendStarted() throws IOException {
sendHttp("http://api.umbrasheep.com/seng/match_started/");
}
/**
* THe host starts sending out heartbeat messages every 2 seconds.
*/

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

@ -111,4 +111,11 @@ public class GPSConverter {
return Math.atan2(coord2.getX() - coord1.getX(), coord2.getY() - coord1.getY());
}
public double getLongitudeFactor() {
return longitudeFactor;
}
public double getLatitudeFactor() {
return latitudeFactor;
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

@ -0,0 +1,12 @@
# Blender MTL File: 'Ocean V1.0.blend'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 1.000000 1.000000 1.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save