diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index 65ca99dd..a7ec4361 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -28,6 +28,7 @@ public class MainServerThread extends Observable implements Runnable, ClientConn private ArrayList serverToClientThreads = new ArrayList<>(); public MainServerThread() { + new GameState("localhost"); try { serverSocket = new ServerSocket(PORT); } catch (IOException e) { diff --git a/src/main/java/seng302/model/Yacht.java b/src/main/java/seng302/model/Yacht.java new file mode 100644 index 00000000..c47abe38 --- /dev/null +++ b/src/main/java/seng302/model/Yacht.java @@ -0,0 +1,683 @@ +package seng302.model; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.ReadOnlyLongWrapper; +import javafx.scene.paint.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.GameState; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; +import seng302.utilities.GeoUtility; + +/** + * Yacht class for the racing boat. + * + * Class created to store more variables (eg. boat statuses) compared to the XMLParser boat class, + * also done outside Boat class because some old variables are not used anymore. + */ +public class Yacht { + + + @FunctionalInterface + public interface YachtLocationListener { + void notifyLocation(Yacht yacht, double lat, double lon, double heading, double velocity, boolean sailIn); + } + + private Logger logger = LoggerFactory.getLogger(Yacht.class); + + private static final Double ROUNDING_DISTANCE = 50d; // TODO: 3/08/17 wmu16 - Look into this value further + + + //BOTH AFAIK + private String boatType; + private Integer sourceId; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + + private Long estimateTimeAtFinish; + private Integer currentMarkSeqID = 0; + private Long markRoundTime; + private Double distanceToCurrentMark; + private Long timeTillNext; + private Double heading; + private Integer legNumber = 0; + + //SERVER SIDE + public static final Double TURN_STEP = 5.0; //This should be in some utils class somewhere 2bh. Public for tests sake. + private Double lastHeading; + private Boolean sailIn = false; + private GeoPoint location; + private Integer boatStatus; + private Double velocity; + private Boolean isAuto; + private Double autoHeading; + + //MARK ROUNDING INFO + private GeoPoint lastLocation; //For purposes of mark rounding calculations + private Boolean hasEnteredRoundingZone; //The distance that the boat must be from the mark to round + private Boolean hasPassedLine; + private Boolean hasPassedThroughGate; + private Boolean finishedRace; + + //CLIENT SIDE + private List locationListeners = new ArrayList<>(); + private ReadOnlyDoubleWrapper velocityProperty = new ReadOnlyDoubleWrapper(); + private ReadOnlyLongWrapper timeTillNextProperty = new ReadOnlyLongWrapper(); + private ReadOnlyLongWrapper timeSinceLastMarkProperty = new ReadOnlyLongWrapper(); + private CompoundMark lastMarkRounded; + private Integer positionInt = 0; + private Color colour; + private Boolean clientSailsIn = true; + + public Yacht(String boatType, Integer sourceId, String hullID, String shortName, + String boatName, String country) { + this.boatType = boatType; + this.sourceId = sourceId; + this.hullID = hullID; + this.shortName = shortName; + this.boatName = boatName; + this.country = country; + this.sailIn = false; + this.isAuto = false; + this.location = new GeoPoint(57.670341, 11.826856); + this.lastLocation = location; + this.heading = 120.0; //In degrees + this.velocity = 0d; //in mms-1 + + this.hasEnteredRoundingZone = false; + this.hasPassedLine = false; + this.hasPassedThroughGate = false; + this.finishedRace = false; + } + + /** + * @param timeInterval since last update in milliseconds + */ + public void update(Long timeInterval) { + + Double secondsElapsed = timeInterval / 1000000.0; + Double windSpeedKnots = GameState.getWindSpeedKnots(); + Double trueWindAngle = Math.abs(GameState.getWindDirection() - heading); + Double boatSpeedInKnots = PolarTable.getBoatSpeed(windSpeedKnots, trueWindAngle); + Double maxBoatSpeed = boatSpeedInKnots / 1.943844492 * 1000; + if (sailIn && velocity <= maxBoatSpeed && maxBoatSpeed != 0d) { + + if (velocity < maxBoatSpeed) { + velocity += maxBoatSpeed / 15; // Acceleration + } + if (velocity > maxBoatSpeed) { + velocity = maxBoatSpeed; // Prevent the boats from exceeding top speed + } + + } else { // Deceleration + + if (velocity > 0d) { + if (maxBoatSpeed != 0d) { + velocity -= maxBoatSpeed / 600; + } else { + velocity -= velocity / 100; + } + if (velocity < 0) { + velocity = 0d; + } + } + } + + runAutoPilot(); + + //UPDATE BOAT LOCATION + lastLocation = location; + location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed); + + //CHECK FOR MARK ROUNDING + if (!finishedRace) { + checkForLegProgression(); + } + + // TODO: 3/08/17 wmu16 - Implement line cross check here + } + + + /** + * Calculates the distance to the next mark (closest of the two if a gate mark). For purposes + * of mark rounding + * @return A distance in metres. Returns -1 if there is no next mark + * @throws IndexOutOfBoundsException If the next mark is null (ie the last mark in the race) + * Check first using {@link seng302.model.mark.MarkOrder#isLastMark(Integer)} + */ + public Double calcDistanceToCurrentMark() throws IndexOutOfBoundsException { + CompoundMark nextMark = GameState.getMarkOrder().getCurrentMark(currentMarkSeqID); + + if (nextMark.isGate()) { + Mark sub1 = nextMark.getSubMark(1); + Mark sub2 = nextMark.getSubMark(2); + Double distance1 = GeoUtility.getDistance(location, sub1); + Double distance2 = GeoUtility.getDistance(location, sub2); + return (distance1 < distance2) ? distance1 : distance2; + } else { + return GeoUtility.getDistance(location, nextMark.getSubMark(1)); + } + } + + + /** + * 4 Different cases of progression in the race + * 1 - Passing the start line + * 2 - Passing any in-race Gate + * 3 - Passing any in-race Mark + * 4 - Passing the finish line + */ + private void checkForLegProgression() { + CompoundMark currentMark = GameState.getMarkOrder().getCurrentMark(currentMarkSeqID); + if (currentMarkSeqID == 0) { + checkStartLineCrossing(currentMark); + } else if (GameState.getMarkOrder().isLastMark(currentMarkSeqID)) { + checkFinishLineCrossing(currentMark); + } else if (currentMark.isGate()) { + checkGateRounding(currentMark); + } else { + checkMarkRounding(currentMark); + } + } + + /** + * If we pass the start line gate in the correct direction, progress + * + * @param currentMark The current gate + */ + private void checkStartLineCrossing(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark nextMark = GameState.getMarkOrder().getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + if (crossedLine == 2 && isClockwiseCross || crossedLine == 1 && !isClockwiseCross) { + currentMarkSeqID++; + logMarkRounding(currentMark); + } + } + } + + + /** + * This algorithm checks for mark rounding. And increments the currentMarSeqID number attribute + * of the yacht if so. + * A visual representation of this algorithm can be seen on the Wiki under + * 'mark passing algorithm' + */ + private void checkMarkRounding(CompoundMark currentMark) { + distanceToCurrentMark = calcDistanceToCurrentMark(); + GeoPoint nextPoint = GameState.getMarkOrder().getNextMark(currentMarkSeqID).getMidPoint(); + GeoPoint prevPoint = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID) + .getMidPoint(); + GeoPoint midPoint = GeoUtility.getDirtyMidPoint(nextPoint, prevPoint); + + //1 TEST FOR ENTERING THE ROUNDING DISTANCE + if (distanceToCurrentMark < ROUNDING_DISTANCE) { + hasEnteredRoundingZone = true; + } + + //In case current mark is a gate, loop through all marks just in case + for (Mark thisCurrentMark : currentMark.getMarks()) { + if (GeoUtility.isPointInTriangle(lastLocation, location, midPoint, thisCurrentMark)) { + hasPassedLine = true; + } + } + + if (hasPassedLine && hasEnteredRoundingZone) { + currentMarkSeqID++; + hasPassedLine = false; + hasEnteredRoundingZone = false; + hasPassedThroughGate = false; + logMarkRounding(currentMark); + } + } + + + /** + * Checks if a gate line has been crossed and in the correct direction + * + * @param currentMark The current gate + */ + private void checkGateRounding(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID); + CompoundMark nextMark = GameState.getMarkOrder().getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + + //We have crossed the line + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + + //Check we cross the line in the correct direction + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + hasPassedThroughGate = true; + } + } + + Boolean prevMarkSide = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + Boolean nextMarkSide = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + + if (hasPassedThroughGate) { + //Check if we need to round this gate after passing through + if (prevMarkSide == nextMarkSide) { + checkMarkRounding(currentMark); + } else { + currentMarkSeqID++; + logMarkRounding(currentMark); + } + } + } + + /** + * If we pass the finish gate in the correct direction + * + * @param currentMark The current gate + */ + private void checkFinishLineCrossing(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + currentMarkSeqID++; + finishedRace = true; + logMarkRounding(currentMark); + logger.debug(sourceId + " finished"); + // TODO: 8/08/17 wmu16 - Do something! + } + } + } + + + /** + * Adjusts the heading of the boat by a given amount, while recording the boats + * last heading. + * + * @param amount the amount by which to adjust the boat heading. + */ + public void adjustHeading(Double amount) { + Double newVal = heading + amount; + lastHeading = heading; + heading = (double) Math.floorMod(newVal.longValue(), 360L); + } + + /** + * Swaps the boats direction from one side of the wind to the other. + */ + public void tackGybe(Double windDirection) { + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double newVal = (-2 * normalizedHeading) + heading; + Double newHeading = (double) Math.floorMod(newVal.longValue(), 360L); + setAutoPilot(newHeading); + } + } + + /** + * Enables the boats auto pilot feature, which will move the boat towards a given heading. + * @param thisHeading The heading to move the boat towards. + */ + private void setAutoPilot(Double thisHeading) { + isAuto = true; + autoHeading = thisHeading; + } + + /** + * Disables the auto pilot function. + */ + public void disableAutoPilot() { + isAuto = false; + } + + /** + * Moves the boat towards the given heading when the auto pilot was set. Disables the auto pilot + * in the event that the boat is within the range of 1 turn step of its goal. + */ + public void runAutoPilot() { + if (isAuto) { + turnTowardsHeading(autoHeading); + if (Math.abs(heading - autoHeading) + <= TURN_STEP) { //Cancel when within 1 turn step of target. + isAuto = false; + } + } + } + + public void toggleSailIn() { + sailIn = !sailIn; + } + + public void turnUpwind() { + disableAutoPilot(); + Double normalizedHeading = normalizeHeading(); + if (normalizedHeading == 0) { + if (lastHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } else if (normalizedHeading == 180) { + if (lastHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } else if (normalizedHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } + + public void turnDownwind() { + disableAutoPilot(); + Double normalizedHeading = normalizeHeading(); + if (normalizedHeading == 0) { + if (lastHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } else if (normalizedHeading == 180) { + if (lastHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } else if (normalizedHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } + + /** + * Takes the VMG from the polartable for upwind or downwind depending on the boats direction, + * and uses this to calculate a heading to move the yacht towards. + */ + public void turnToVMG() { + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double optimalHeading; + HashMap optimalPolarMap; + + if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind + optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); + } else { + optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); + } + optimalHeading = optimalPolarMap.keySet().iterator().next(); + + if (normalizedHeading > 180) { + optimalHeading = 360 - optimalHeading; + } + + // Take optimal heading and turn into a boat heading rather than a wind heading. + optimalHeading = + optimalHeading + GameState.getWindDirection(); + + setAutoPilot(optimalHeading); + } + } + + /** + * Takes a given heading and rotates the boat towards that heading. + * This does not care about being upwind or downwind, just which direction will reach a given + * heading faster. + * + * @param newHeading The heading to turn the yacht towards. + */ + private void turnTowardsHeading(Double newHeading) { + Double newVal = heading - newHeading; + if (Math.floorMod(newVal.longValue(), 360L) > 180) { + adjustHeading(TURN_STEP / 5); + } else { + adjustHeading(-TURN_STEP / 5); + } + } + + /** + * Returns a heading normalized for the wind direction. Heading direction into the wind is 0, + * directly away is 180. + * + * @return The normalized heading accounting for wind direction. + */ + private Double normalizeHeading() { + Double normalizedHeading = heading - GameState.windDirection; + normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); + return normalizedHeading; + } + + public String getBoatType() { + return boatType; + } + + public Integer getSourceId() { + //@TODO Remove and merge with Creating Game Loop + if (sourceId == null) return 0; + return sourceId; + } + + public String getHullID() { + if (hullID == null) return ""; + return hullID; + } + + public String getShortName() { + return shortName; + } + + public String getBoatName() { + return boatName; + } + + public String getCountry() { + if (country == null) return ""; + return country; + } + + public Integer getBoatStatus() { + return boatStatus; + } + + public void setBoatStatus(Integer boatStatus) { + this.boatStatus = boatStatus; + } + + public Integer getLegNumber() { + return legNumber; + } + + public void setLegNumber(Integer legNumber) { +// if (colour != null && position != "-" && legNumber != this.legNumber) { +// RaceViewController.updateYachtPositionSparkline(this, legNumber); +// } + this.legNumber = legNumber; + } + + public void setEstimateTimeTillNextMark(Long estimateTimeTillNextMark) { + timeTillNext = estimateTimeTillNextMark; + } + + public String getEstimateTimeAtFinish() { + DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + return format.format(estimateTimeAtFinish); + } + + public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) { + this.estimateTimeAtFinish = estimateTimeAtFinish; + } + + public Integer getPositionInteger() { + return positionInt; + } + + public void setPositionInteger(Integer position) { + this.positionInt = position; + } + + public void updateVelocityProperty(double velocity) { + this.velocityProperty.set(velocity); + } + + public void setMarkRoundingTime(Long markRoundingTime) { + this.markRoundTime = markRoundingTime; + } + + public ReadOnlyDoubleProperty getVelocityProperty() { + return velocityProperty.getReadOnlyProperty(); + } + + public double getVelocityMMS() { + return velocity; + } + + public ReadOnlyLongProperty timeTillNextProperty() { + return timeTillNextProperty.getReadOnlyProperty(); + } + + public Double getVelocityKnots() { + return velocity / 1000 * 1.943844492; // TODO: 26/07/17 cir27 - remove magic number + } + + public Long getTimeTillNext() { + return timeTillNext; + } + + public Long getMarkRoundTime() { + return markRoundTime; + } + + public CompoundMark getLastMarkRounded() { + return lastMarkRounded; + } + + public void setLastMarkRounded(CompoundMark lastMarkRounded) { + this.lastMarkRounded = lastMarkRounded; + } + + public GeoPoint getLocation() { + return location; + } + + /** + * Sets the current location of the boat in lat and long whilst preserving the last location + * + * @param lat Latitude + * @param lng Longitude + */ + public void setLocation(Double lat, Double lng) { + lastLocation.setLat(location.getLat()); + lastLocation.setLng(location.getLng()); + location.setLat(lat); + location.setLng(lng); + } + + public Double getHeading() { + return heading; + } + + public void setHeading(Double heading) { + this.heading = heading; + } + + public Boolean getSailIn() { + return sailIn; + } + + @Override + public String toString() { + return boatName; + } + + public void updateTimeSinceLastMarkProperty(long timeSinceLastMark) { + this.timeSinceLastMarkProperty.set(timeSinceLastMark); + } + + public ReadOnlyLongProperty timeSinceLastMarkProperty () { + return timeSinceLastMarkProperty.getReadOnlyProperty(); + } + + public void setTimeTillNext(Long timeTillNext) { + this.timeTillNext = timeTillNext; + } + + + public Color getColour() { + return colour; + } + + public void setColour(Color colour) { + this.colour = colour; + } + + public void toggleClientSail() { + clientSailsIn = !clientSailsIn; + } + + public Double getVelocity() { + return velocity; + } + + public void setVelocity(Double velocity) { + this.velocity = velocity; + } + + public Double getDistanceToCurrentMark() { + return distanceToCurrentMark; + } + + public Boolean getClientSailsIn(){ + return clientSailsIn; + } + + public void updateLocation(double lat, double lng, double heading, double velocity) { + setLocation(lat, lng); + this.heading = heading; + this.velocity = velocity; + updateVelocityProperty(velocity); + for (YachtLocationListener yll : locationListeners) { + yll.notifyLocation(this, lat, lng, heading, velocity, clientSailsIn); + } + } + + private void logMarkRounding(CompoundMark currentMark) { + String typeString = "mark"; + if (currentMark.isGate()) { + typeString = "gate"; + } + logger.debug( + String.format("BoatID %d passed %s %s with id %d. Now on leg %d", + sourceId, + typeString, + currentMark.getMarks().get(0).getName(), + currentMark.getId(), + currentMarkSeqID)); + } + + public void addLocationListener (YachtLocationListener listener) { + locationListeners.add(listener); + } +} diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java index dc38e129..9f76d37c 100644 --- a/src/main/java/seng302/visualiser/ClientToServerThread.java +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -36,6 +36,8 @@ import seng302.model.stream.packets.StreamPacket; */ public class ClientToServerThread implements Runnable { + + /** * Functional interface for receiving packets from client socket. */ diff --git a/src/main/java/seng302/visualiser/GameClient.java b/src/main/java/seng302/visualiser/GameClient.java index 62189263..12e49632 100644 --- a/src/main/java/seng302/visualiser/GameClient.java +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -257,6 +257,8 @@ public class GameClient { private void processRaceStatusUpdate(RaceStatusData data) { if (allXMLReceived()) { raceState.updateState(data); + if (raceView != null) + raceView.getGameView().setWindDir(raceState.getWindDirection()); for (long[] boatData : data.getBoatData()) { ClientYacht clientYacht = allBoatsMap.get((int) boatData[0]); clientYacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]); @@ -310,7 +312,9 @@ public class GameClient { switch (e.getCode()) { //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet) case SHIFT: // sails in/sails out - socketThread.sendBoatAction(BoatAction.SAILS_IN); break; + socketThread.sendBoatAction(BoatAction.SAILS_IN); + raceView.getGameView().getPlayerYacht().toggleClientSail(); + break; case PAGE_UP: case PAGE_DOWN: socketThread.sendBoatAction(BoatAction.MAINTAIN_HEADING); break; diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java index 24f42822..41d3cade 100644 --- a/src/main/java/seng302/visualiser/GameView.java +++ b/src/main/java/seng302/visualiser/GameView.java @@ -9,10 +9,14 @@ import java.util.Map; import javafx.animation.AnimationTimer; import javafx.application.Platform; import javafx.collections.ObservableList; +import javafx.event.EventHandler; import javafx.geometry.Point2D; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.ScrollEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; @@ -53,6 +57,8 @@ public class GameView extends Pane { private double referencePointX, referencePointY; private double metersPerPixelX, metersPerPixelY; + final double SCALE_DELTA = 1.1; + private Text fpsDisplay = new Text(); private Polygon raceBorder = new CourseBoundary(); @@ -80,6 +86,26 @@ public class GameView extends Pane { private Double frameRate = 60.0; private int frameTimeIndex = 0; private boolean arrayFilled = false; + private ClientYacht playerYacht; + private double windDir = 0.0; + + double scaleFactor = 1; + + public void zoomOut() { + scaleFactor = 0.95; + for (Node child : getChildren()) { + child.setScaleX(child.getScaleX() * scaleFactor); + child.setScaleY(child.getScaleY() * scaleFactor); + } + } + + public void zoomIn() { + scaleFactor = 1.05; + for (Node child : getChildren()) { + child.setScaleX(child.getScaleX() * scaleFactor); + child.setScaleY(child.getScaleY() * scaleFactor); + } + } private enum ScaleDirection { HORIZONTAL, @@ -96,6 +122,45 @@ public class GameView extends Pane { gameObjects.add(fpsDisplay); gameObjects.add(raceBorder); gameObjects.add(markers); +// +// this.setOnKeyPressed(new EventHandler() { +// @Override public void handle(KeyEvent event) { +// event.consume(); +// switch (event.getCode()) { +// case Z: +// scaleFactor = scaleFactor * 1.2; +// break; +// case X: +// scaleFactor = scaleFactor * 0.8; +// break; +// } +// if (event.getCode() == KeyCode.Z || event.getCode() == KeyCode.X) { +// for (Node child : getChildren()) { +// child.setScaleX(child.getScaleX() * scaleFactor); +// child.setScaleY(child.getScaleY() * scaleFactor); +// } +// } +// } +// }); +// +// this.setOnScroll(new EventHandler() { +// @Override public void handle(ScrollEvent event) { +// event.consume(); +// if (event.getDeltaY() == 0) { +// return; +// } +// +// double scaleFactor = +// (event.getDeltaY() > 0) +// ? SCALE_DELTA +// : 1/SCALE_DELTA; +// for (Node child : getChildren()) { +// child.setScaleX(child.getScaleX() * scaleFactor); +// child.setScaleY(child.getScaleY() * scaleFactor); +// } +// } +// }); + initializeTimer(); } @@ -201,16 +266,6 @@ public class GameView extends Pane { for (Mark mark : cMark.getMarks()) { makeAndBindMarker(mark, colour); } - - //UNCOMMENT THIS TO HIGHLIGHT SUBMARKS 1 and 2 RED AND GREEN RESPECTIVELY FOR DEBUG - //(instead of above for loop) -// for (Mark mark : cMark.getMarks()) { -// if (mark.getSeqID() == 1) { -// makeAndBindMarker(mark, Color.RED); -// } else { -// makeAndBindMarker(mark, Color.GREEN); -// } -// } //Create gate line if (cMark.isGate()) { for (int i = 1; i < cMark.getMarks().size(); i++) { @@ -334,10 +389,10 @@ public class GameView extends Pane { boatObjectGroup.getChildren().add(newBoat); trails.getChildren().add(newBoat.getTrail()); // TODO: 1/08/17 Make this less vile to look at. - clientYacht.addLocationListener((boat, lat, lon, heading, velocity) -> { + clientYacht.addLocationListener((boat, lat, lon, heading, velocity, sailIn) -> { BoatObject bo = boatObjects.get(boat); Point2D p2d = findScaledXY(lat, lon); - bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity); + bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity, sailIn, windDir); // annotations.get(boat).setLayoutX(p2d.getX()); // annotations.get(boat).setLayoutY(p2d.getY()); // annotations.get(boat).setLocation(100d, 100d); @@ -572,11 +627,23 @@ public class GameView extends Pane { timer.stop(); } + + public void setWindDir(double windDir) { + this.windDir = windDir; + } + + public void startRace () { timer.start(); } - public void setBoatAsPlayer(ClientYacht playerYacht) { + public ClientYacht getPlayerYacht() { + return playerYacht; + } + + public void setBoatAsPlayer (ClientYacht playerYacht) { + this.playerYacht = playerYacht; + this.playerYacht.toggleClientSail(); boatObjects.get(playerYacht).setAsPlayer(); annotations.get(playerYacht).addAnnotation( "velocity", diff --git a/src/main/java/seng302/visualiser/controllers/RaceViewController.java b/src/main/java/seng302/visualiser/controllers/RaceViewController.java index 1721b027..01a64cdc 100644 --- a/src/main/java/seng302/visualiser/controllers/RaceViewController.java +++ b/src/main/java/seng302/visualiser/controllers/RaceViewController.java @@ -596,4 +596,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel this.courseData = raceData; gameView.updateBorder(raceData.getCourseLimit()); } + + public GameView getGameView() { + return gameView; + } } \ No newline at end of file diff --git a/src/main/java/seng302/visualiser/controllers/StartScreenController.java b/src/main/java/seng302/visualiser/controllers/StartScreenController.java index 87199442..1a9db1a1 100644 --- a/src/main/java/seng302/visualiser/controllers/StartScreenController.java +++ b/src/main/java/seng302/visualiser/controllers/StartScreenController.java @@ -66,7 +66,7 @@ public class StartScreenController implements Initializable { */ @FXML public void hostButtonPressed() { - new GameState(getLocalHostIp()); +// new GameState(getLocalHostIp()); gameClient = new GameClient(holder); gameClient.runAsHost(getLocalHostIp(), 4942); // try { diff --git a/src/main/java/seng302/visualiser/fxObjects/BoatObject.java b/src/main/java/seng302/visualiser/fxObjects/BoatObject.java index d2ab40c2..b04fcaae 100644 --- a/src/main/java/seng302/visualiser/fxObjects/BoatObject.java +++ b/src/main/java/seng302/visualiser/fxObjects/BoatObject.java @@ -30,9 +30,11 @@ public class BoatObject extends Group { private double xVelocity; private double yVelocity; private double lastHeading; + private double sailState; //Graphical objects private Polyline trail = new Polyline(); private Polygon boatPoly; + private Polygon sail; private Wake wake; private Line leftLayLine; private Line rightLayline; @@ -94,7 +96,16 @@ public class BoatObject extends Group { trail.setCache(true); wake = new Wake(0, -BOAT_HEIGHT); wake.setVisible(true); - super.getChildren().addAll(boatPoly);//, annotationBox); + + sail = new Polygon(0.0,BOAT_HEIGHT / 4, + 0.0, BOAT_HEIGHT); + sailState = 0; + sail.setStrokeWidth(2.0); + sail.setStroke(Color.BLACK); + sail.setFill(Color.TRANSPARENT); + sail.setCache(true); + super.getChildren().clear(); + super.getChildren().addAll(boatPoly, sail); } public void setFill (Paint value) { @@ -105,19 +116,30 @@ public class BoatObject extends Group { /** * Moves the boat and its children annotations to coordinates specified - * - * @param x The X coordinate to move the boat to + * @param x The X coordinate to move the boat to * @param y The Y coordinate to move the boat to * @param rotation The rotation by which the boat moves * @param velocity The velocity the boat is moving + * @param sailIn */ - public void moveTo(double x, double y, double rotation, double velocity) { + public void moveTo(double x, double y, double rotation, double velocity, Boolean sailIn, double windDir) { Double dx = Math.abs(boatPoly.getLayoutX() - x); Double dy = Math.abs(boatPoly.getLayoutY() - y); Platform.runLater(() -> { - rotateTo(rotation); + rotateTo(rotation, sailIn, windDir); boatPoly.setLayoutX(x); boatPoly.setLayoutY(y); + if (sailIn) { +// sail.getPoints().clear(); +// sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0); +// sail.getPoints().addAll(0.0, 0.0, 24.0, 0.0); + sail.setLayoutX(x); + sail.setLayoutY(y); + } else { + animateSail(); + sail.setLayoutX(x); + sail.setLayoutY(y); + } wake.setLayoutX(x); wake.setLayoutY(y); }); @@ -142,8 +164,65 @@ public class BoatObject extends Group { } } - private void rotateTo(double rotation) { - boatPoly.getTransforms().setAll(new Rotate(rotation)); + private Double normalizeHeading(double heading, double windDirection) { + Double normalizedHeading = heading - windDirection; + normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); + return normalizedHeading; + } + + + private void rotateTo(double heading, boolean sailsIn, double windDir) { + boatPoly.getTransforms().setAll(new Rotate(heading)); + if (sailsIn) { + Double sailWindOffset = 30.0; + Double upwindAngleLimit = 15.0; + Double downwindAngleLimit = 10.0; //Upwind from normalised horizontal + Double normalizedHeading = normalizeHeading(heading, windDir); + if (normalizedHeading < 180) { + sail.getTransforms().setAll(new Rotate(windDir + 90 + sailWindOffset)); + sail.getPoints().clear(); + sail.getPoints().addAll(0.0, 0.0, 4.0, -1.5, 8.0, -3.0, 12.0, -3.5, 16.0, -3.0, 20.0, -1.5, 24.0, 0.0); + if (normalizedHeading > 90 + sailWindOffset){ + sail.getTransforms().setAll(new Rotate(heading + downwindAngleLimit)); + } + if (normalizedHeading < sailWindOffset + upwindAngleLimit){ + sail.getTransforms().setAll(new Rotate(heading + 90 - upwindAngleLimit)); + } + } else { + sail.getTransforms().setAll(new Rotate(windDir + 90 - sailWindOffset)); + sail.getPoints().clear(); + sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0); + if (normalizedHeading < 270 - sailWindOffset){ + sail.getTransforms().setAll(new Rotate(heading + 180 - downwindAngleLimit)); + } + if (normalizedHeading > 360 - (sailWindOffset + upwindAngleLimit)){ + sail.getTransforms().setAll(new Rotate(heading + 90 + upwindAngleLimit)); + } + } + } else { + sail.getTransforms().setAll(new Rotate(windDir)); + } + } + + + private void animateSail(){ + Double[] points = new Double[200]; + double amplitude = 2.0; + double period = 10; + for (int i = 0; i < 50; i++) { + points[i * 2] = amplitude * Math.sin(((Math.PI * i) / period + sailState)); + points[i * 2 + 1] = (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2; + points[199 - (i * 2)] = (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2; + points[199 - (i * 2 + 1)] = amplitude * Math.sin(((Math.PI * i) / period + sailState)); + } + if (sailState == - 2 * Math.PI) { + sailState = 0; + } else { + sailState = sailState - Math.PI / 5; + } + sail.getPoints().clear(); + sail.getPoints().addAll(points); + } public void updateLocation() { @@ -275,11 +354,12 @@ public class BoatObject extends Group { boatPoly.setStroke(Color.BLACK); boatPoly.setStrokeWidth(3); isPlayer = true; + animateSail(); } - public void setTrajectory(double heading, double velocity) { + public void setTrajectory(double heading, double velocity, double windDir) { wake.setRotation(lastHeading - heading, velocity); - rotateTo(heading); + rotateTo(heading, false, windDir); xVelocity = Math.cos(Math.toRadians(heading)) * velocity; yVelocity = Math.sin(Math.toRadians(heading)) * velocity; lastHeading = heading; diff --git a/src/main/resources/config/config.xml b/src/main/resources/config/config.xml index b5c90704..4f002974 100644 --- a/src/main/resources/config/config.xml +++ b/src/main/resources/config/config.xml @@ -4,6 +4,6 @@ AC35 6 10.0 - 135 + 135 diff --git a/src/test/java/RunCucumberTests.java b/src/test/java/RunCucumberTests.java new file mode 100644 index 00000000..24b2ae54 --- /dev/null +++ b/src/test/java/RunCucumberTests.java @@ -0,0 +1,12 @@ +import cucumber.api.CucumberOptions; +import cucumber.api.junit.Cucumber; +import org.junit.runner.RunWith; + +/** + * Created by kre39 on 7/08/17. + */ + +@RunWith(Cucumber.class) +@CucumberOptions(features = "src/test/java/features") +public class RunCucumberTests { +} diff --git a/src/test/java/features/toggleSail.feature b/src/test/java/features/toggleSail.feature new file mode 100644 index 00000000..a3fb4598 --- /dev/null +++ b/src/test/java/features/toggleSail.feature @@ -0,0 +1,5 @@ +Feature: SailsToggle + Scenario: User toggles in sail + Given The game is running + When the user has pressed "shift" + Then the sails are "in" \ No newline at end of file diff --git a/src/test/java/seng302/visualiser/map/BoatSailAnimationToggleTest.java b/src/test/java/seng302/visualiser/map/BoatSailAnimationToggleTest.java new file mode 100644 index 00000000..cccea5c6 --- /dev/null +++ b/src/test/java/seng302/visualiser/map/BoatSailAnimationToggleTest.java @@ -0,0 +1,31 @@ +package seng302.visualiser.map; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import seng302.model.Yacht; +import seng302.visualiser.fxObjects.BoatObject; + +/** + * Created by kre39 on 6/08/17. + */ +public class BoatSailAnimationToggleTest { + + private Yacht yacht; + + @Before + public void setup() throws Exception{ + yacht = new Yacht("Yacht", 1, "YACHT", "YAC", "Test Yacht", "NZ"); + } + + @Test + public void sailToggleTest() throws Exception { + assertFalse(yacht.getSailIn()); + yacht.toggleClientSail(); + assertFalse(yacht.getSailIn()); + } + +} diff --git a/src/test/java/steps/ToggleSailSteps.java b/src/test/java/steps/ToggleSailSteps.java new file mode 100644 index 00000000..5347224d --- /dev/null +++ b/src/test/java/steps/ToggleSailSteps.java @@ -0,0 +1,60 @@ +package steps; + +import cucumber.api.java.en.Given; +import cucumber.api.java.en.Then; +import cucumber.api.java.en.When; +import java.util.ArrayList; +import org.junit.Assert; +import seng302.gameServer.GameStages; +import seng302.gameServer.GameState; +import seng302.gameServer.MainServerThread; +import seng302.gameServer.server.messages.BoatAction; +import seng302.model.Yacht; +import seng302.visualiser.ClientToServerThread; + +import java.util.ArrayList; + +/** + * Created by kre39 on 7/08/17. + */ +public class ToggleSailSteps { + + + MainServerThread mst; + ClientToServerThread client; + boolean sailsIn = false; + long startTime; + private Yacht yacht; + + + + @Given("^The game is running$") + public void the_game_is_running() throws Throwable { + mst = new MainServerThread(); + client = new ClientToServerThread("localhost", 4942); + GameState.setCurrentStage(GameStages.RACING); + Thread.sleep(200); // Sleep needed to help the threads all be up to speed with each other + Yacht yacht = (new ArrayList<>(GameState.getYachts().values())).get(0); + Assert.assertFalse(yacht.getSailIn()); + } + + + @When("^the user has pressed \"([^\"]*)\"$") + public void the_user_has_pressed(String arg1) throws Throwable { + startTime = System.currentTimeMillis(); + if (arg1 == "shift") { + client.sendBoatAction(BoatAction.SAILS_IN); + } + } + + @Then("^the sails are \"([^\"]*)\"$") + public void the_sails_are(String arg1) throws Throwable { + Thread.sleep(200); // Sleep needed to help the threads all be up to speed with each other + Yacht yacht = (new ArrayList<>(GameState.getYachts().values())).get(0); + if (arg1 == "in") { + Assert.assertTrue(yacht.getSailIn()); + } else { + Assert.assertFalse(yacht.getSailIn()); + } + } +}