added animation of a shell to the game
- in BattleShipServer class added serialization of the AnimationMessage classs - added to VoidVisitor and Visitor the Shell class - edited the ServerGameLogic class to implement a new Animation state (see new Server-State-Chart) - added a new client state AnimationState (see new Client-State-Chart)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package pp.battleship.game.client;
|
||||
|
||||
import pp.battleship.message.client.AnimationMessage;
|
||||
import pp.battleship.message.server.EffectMessage;
|
||||
import pp.battleship.model.Battleship;
|
||||
import pp.battleship.model.IntPoint;
|
||||
import pp.battleship.model.Shell;
|
||||
import pp.battleship.model.ShipMap;
|
||||
import pp.battleship.notification.Music;
|
||||
import pp.battleship.notification.Sound;
|
||||
|
||||
/**
|
||||
* Represents the state of the game during an animation sequence.
|
||||
* This state handles the progress and completion of the animation,
|
||||
* updates the game state accordingly, and transitions to the next state.
|
||||
*/
|
||||
public class AnimationState extends ClientState {
|
||||
/**
|
||||
* Progress of the current animation, ranging from 0 to 1.
|
||||
*/
|
||||
private float animationProgress = 0;
|
||||
|
||||
/**
|
||||
* Duration of the animation in seconds.
|
||||
*/
|
||||
private final static float ANIMATION_DURATION = 0.375f;
|
||||
|
||||
/**
|
||||
* Speed of the shell in the animation.
|
||||
*/
|
||||
private final static float SHELL_SPEED = 0.3f;
|
||||
|
||||
/**
|
||||
* The effect message received from the server.
|
||||
*/
|
||||
private final EffectMessage msg;
|
||||
|
||||
/**
|
||||
* The shell involved in the animation.
|
||||
*/
|
||||
private final Shell shell;
|
||||
|
||||
/**
|
||||
* Constructs an AnimationState with the specified game logic, effect message, and shell.
|
||||
*
|
||||
* @param logic the game logic associated with this state
|
||||
* @param msg the effect message received from the server
|
||||
* @param shell the shell involved in the animation
|
||||
*/
|
||||
public AnimationState(ClientGameLogic logic, EffectMessage msg, Shell shell) {
|
||||
super(logic);
|
||||
this.msg = msg;
|
||||
this.shell = shell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the animation state and transitions to the next state:<br>
|
||||
* - Plays the appropriate sound.<br>
|
||||
* - Updates the affected map.<br>
|
||||
* - Adds destroyed ships to the opponent's map.<br>
|
||||
* - Sends an `AnimationMessage` to the server.<br>
|
||||
* - If the game is over, transitions to `GameOverState` and plays music.<br>
|
||||
* - Otherwise, transitions to `BattleState`.
|
||||
*/
|
||||
public void endState() {
|
||||
playSound(msg);
|
||||
affectedMap(msg).add(msg.getShot());
|
||||
affectedMap(msg).remove(shell);
|
||||
|
||||
if (destroyedOpponentShip(msg))
|
||||
logic.getOpponentMap().add(msg.getDestroyedShip());
|
||||
|
||||
logic.send(new AnimationMessage());
|
||||
if (msg.isGameOver()) {
|
||||
for (Battleship ship : msg.getRemainingOpponentShips()) {
|
||||
logic.getOpponentMap().add(ship);
|
||||
}
|
||||
logic.setState(new GameOverState(logic));
|
||||
if (msg.isOwnShot())
|
||||
logic.playMusic(Music.VICTORY_MUSIC);
|
||||
else
|
||||
logic.playMusic(Music.DEFEAT_MUSIC);
|
||||
} else {
|
||||
logic.setState(new BattleState(logic, msg.isMyTurn()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the battle state should be shown.
|
||||
*
|
||||
* @return true if the battle state should be shown, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean showBattle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which map (own or opponent's) should be affected by the shot based on the message.
|
||||
*
|
||||
* @param msg the effect message received from the server
|
||||
* @return the map (either the opponent's or player's own map) that is affected by the shot
|
||||
*/
|
||||
private ShipMap affectedMap(EffectMessage msg) {
|
||||
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the opponent's ship was destroyed by the player's shot.
|
||||
*
|
||||
* @param msg the effect message received from the server
|
||||
* @return true if the shot destroyed an opponent's ship, false otherwise
|
||||
*/
|
||||
|
||||
private boolean destroyedOpponentShip(EffectMessage msg) {
|
||||
return msg.getDestroyedShip() != null && msg.isOwnShot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a sound based on the outcome of the shot. Different sounds are played for a miss, hit,
|
||||
* or destruction of a ship.
|
||||
*
|
||||
* @param msg the effect message containing the result of the shot
|
||||
*/
|
||||
private void playSound(EffectMessage msg) {
|
||||
if (!msg.getShot().isHit())
|
||||
logic.playSound(Sound.SPLASH);
|
||||
else if (msg.getDestroyedShip() == null)
|
||||
logic.playSound(Sound.EXPLOSION);
|
||||
else
|
||||
logic.playSound(Sound.DESTROYED_SHIP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a click on the opponent's map.
|
||||
*
|
||||
* @param pos the position where the click occurred
|
||||
*/
|
||||
@Override
|
||||
public void clickOpponentMap(IntPoint pos) {
|
||||
if (!msg.isMyTurn())
|
||||
logic.setInfoText("wait.its.not.your.turn");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of the animation. This method increments the animationProgress value
|
||||
* until it exceeds a threshold, at which point the state ends.
|
||||
*
|
||||
* @param delta the time elapsed since the last update, in seconds
|
||||
*/
|
||||
@Override
|
||||
public void update(float delta) {
|
||||
if (animationProgress > ANIMATION_DURATION) {
|
||||
endState();
|
||||
} else {
|
||||
animationProgress += delta * SHELL_SPEED;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,10 @@
|
||||
import pp.battleship.message.client.ShootMessage;
|
||||
import pp.battleship.message.server.EffectMessage;
|
||||
import pp.battleship.model.IntPoint;
|
||||
import pp.battleship.model.Shell;
|
||||
import pp.battleship.model.ShipMap;
|
||||
import pp.battleship.notification.Music;
|
||||
import pp.battleship.notification.Sound;
|
||||
|
||||
import java.lang.System.Logger.Level;
|
||||
|
||||
/**
|
||||
* Represents the state of the client where players take turns to attack each other's ships.
|
||||
*/
|
||||
@@ -33,11 +31,21 @@ public BattleState(ClientGameLogic logic, boolean myTurn) {
|
||||
this.myTurn = myTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the battle state should be shown.
|
||||
*
|
||||
* @return true if the battle state should be shown, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean showBattle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a click on the opponent's map.
|
||||
*
|
||||
* @param pos the position where the click occurred
|
||||
*/
|
||||
@Override
|
||||
public void clickOpponentMap(IntPoint pos) {
|
||||
if (!myTurn)
|
||||
@@ -53,21 +61,16 @@ else if (logic.getOpponentMap().isValid(pos))
|
||||
*/
|
||||
@Override
|
||||
public void receivedEffect(EffectMessage msg) {
|
||||
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS
|
||||
playSound(msg);
|
||||
ClientGameLogic.LOGGER.log(System.Logger.Level.INFO, "report effect: {0}", msg); //NON-NLS
|
||||
// Update turn and info text
|
||||
myTurn = msg.isMyTurn();
|
||||
logic.setInfoText(msg.getInfoTextKey());
|
||||
affectedMap(msg).add(msg.getShot());
|
||||
if (destroyedOpponentShip(msg))
|
||||
logic.getOpponentMap().add(msg.getDestroyedShip());
|
||||
if (msg.isGameOver()) {
|
||||
msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add);
|
||||
logic.setState(new GameOverState(logic));
|
||||
if (msg.isOwnShot())
|
||||
logic.playMusic(Music.VICTORY_MUSIC);
|
||||
else
|
||||
logic.playMusic(Music.DEFEAT_MUSIC);
|
||||
}
|
||||
// Add the shell to the affected map
|
||||
Shell shell = new Shell(msg.getShot());
|
||||
affectedMap(msg).add(shell);
|
||||
// Change state to AnimationState
|
||||
logic.playSound(Sound.SHELL_FIRED);
|
||||
logic.setState(new AnimationState(logic, msg, shell));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,29 +82,4 @@ public void receivedEffect(EffectMessage msg) {
|
||||
private ShipMap affectedMap(EffectMessage msg) {
|
||||
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the opponent's ship was destroyed by the player's shot.
|
||||
*
|
||||
* @param msg the effect message received from the server
|
||||
* @return true if the shot destroyed an opponent's ship, false otherwise
|
||||
*/
|
||||
private boolean destroyedOpponentShip(EffectMessage msg) {
|
||||
return msg.getDestroyedShip() != null && msg.isOwnShot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a sound based on the outcome of the shot. Different sounds are played for a miss, hit,
|
||||
* or destruction of a ship.
|
||||
*
|
||||
* @param msg the effect message containing the result of the shot
|
||||
*/
|
||||
private void playSound(EffectMessage msg) {
|
||||
if (!msg.getShot().isHit())
|
||||
logic.playSound(Sound.SPLASH);
|
||||
else if (msg.getDestroyedShip() == null)
|
||||
logic.playSound(Sound.EXPLOSION);
|
||||
else
|
||||
logic.playSound(Sound.DESTROYED_SHIP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package pp.battleship.game.server;
|
||||
|
||||
import pp.battleship.BattleshipConfig;
|
||||
import pp.battleship.message.client.AnimationMessage;
|
||||
import pp.battleship.message.client.ClientInterpreter;
|
||||
import pp.battleship.message.client.MapMessage;
|
||||
import pp.battleship.message.client.ShootMessage;
|
||||
@@ -17,7 +18,6 @@
|
||||
import pp.battleship.message.server.StartBattleMessage;
|
||||
import pp.battleship.model.Battleship;
|
||||
import pp.battleship.model.IntPoint;
|
||||
import pp.battleship.model.Rotation;
|
||||
|
||||
import java.lang.System.Logger;
|
||||
import java.lang.System.Logger.Level;
|
||||
@@ -36,6 +36,7 @@ public class ServerGameLogic implements ClientInterpreter {
|
||||
private final BattleshipConfig config;
|
||||
private final List<Player> players = new ArrayList<>(2);
|
||||
private final Set<Player> readyPlayers = new HashSet<>();
|
||||
private final Set<Player> finishedAnimation = new HashSet<>();
|
||||
private final ServerSender serverSender;
|
||||
private Player activePlayer;
|
||||
private ServerState state = ServerState.WAIT;
|
||||
@@ -192,9 +193,9 @@ private boolean isWithinBounds(Battleship ship) {
|
||||
int width = config.getMapWidth();
|
||||
int height = config.getMapHeight();
|
||||
return minX >= 0 && minX < width &&
|
||||
minY >= 0 && minY < height &&
|
||||
maxX >= 0 && maxX < width &&
|
||||
maxY >= 0 && maxY < height;
|
||||
minY >= 0 && minY < height &&
|
||||
maxX >= 0 && maxX < width &&
|
||||
maxY >= 0 && maxY < height;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,8 +227,40 @@ private boolean overlaps(List<Battleship> ships) {
|
||||
public void received(ShootMessage msg, int from) {
|
||||
if (state != ServerState.BATTLE)
|
||||
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
|
||||
else
|
||||
else {
|
||||
setState(ServerState.ANIMATION);
|
||||
shoot(getPlayerById(from), msg.getPosition());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the reception of an {@link AnimationMessage}.
|
||||
* Marks the player's animation as finished and transitions the game state if necessary.
|
||||
*
|
||||
* @param msg the received {@code AnimationMessage}
|
||||
* @param from the ID of the sender client
|
||||
*/
|
||||
@Override
|
||||
public void received(AnimationMessage msg, int from) {
|
||||
if (state != ServerState.ANIMATION)
|
||||
LOGGER.log(Level.ERROR, "animation not allowed in {0}", state); //NON-NLS
|
||||
else
|
||||
finishedAnimation(getPlayerById(from));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the player's animation as finished and transitions the game state if necessary.
|
||||
*
|
||||
* @param player the player whose animation is finished
|
||||
*/
|
||||
private void finishedAnimation(Player player) {
|
||||
if (!finishedAnimation.add(player)) {
|
||||
LOGGER.log(Level.ERROR, "{0}'s animation was already finished", player);
|
||||
}
|
||||
if (finishedAnimation.size() == 2) {
|
||||
finishedAnimation.clear();
|
||||
setState(ServerState.BATTLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,8 +298,7 @@ void shoot(Player p, IntPoint pos) {
|
||||
send(activePlayer, EffectMessage.miss(true, pos));
|
||||
send(otherPlayer, EffectMessage.miss(false, pos));
|
||||
activePlayer = otherPlayer;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// shot hit a ship
|
||||
selectedShip.hit(pos);
|
||||
if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
|
||||
@@ -274,13 +306,11 @@ void shoot(Player p, IntPoint pos) {
|
||||
send(activePlayer, EffectMessage.won(pos, selectedShip));
|
||||
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
|
||||
setState(ServerState.GAME_OVER);
|
||||
}
|
||||
else if (selectedShip.isDestroyed()) {
|
||||
} else if (selectedShip.isDestroyed()) {
|
||||
// ship has been destroyed, but game is not yet over
|
||||
send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
|
||||
send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// ship has been hit, but it hasn't been destroyed
|
||||
send(activePlayer, EffectMessage.hit(true, pos));
|
||||
send(otherPlayer, EffectMessage.hit(false, pos));
|
||||
|
||||
@@ -26,6 +26,11 @@ enum ServerState {
|
||||
*/
|
||||
BATTLE,
|
||||
|
||||
/**
|
||||
* The server is waiting for clients to finish their animations.
|
||||
*/
|
||||
ANIMATION,
|
||||
|
||||
/**
|
||||
* The game has ended because all the ships of one player have been destroyed.
|
||||
*/
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
|
||||
package pp.battleship.game.singlemode;
|
||||
|
||||
import pp.battleship.message.client.ClientInterpreter;
|
||||
import pp.battleship.message.client.ClientMessage;
|
||||
import pp.battleship.message.client.MapMessage;
|
||||
import pp.battleship.message.client.ShootMessage;
|
||||
import pp.battleship.message.client.*;
|
||||
import pp.battleship.model.Battleship;
|
||||
|
||||
/**
|
||||
@@ -63,6 +60,19 @@ public void received(MapMessage msg, int from) {
|
||||
copiedMessage = new MapMessage(msg.getShips().stream().map(Copycat::copy).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the reception of a {@link AnimationMessage}.
|
||||
* Since a {@code AnimationMessage} does not need to be copied, it is directly assigned.
|
||||
*
|
||||
* @param msg the received {@code AnimationMessage}
|
||||
* @param from the identifier of the sender
|
||||
*/
|
||||
@Override
|
||||
public void received(AnimationMessage msg, int from) {
|
||||
// copying is not necessary
|
||||
copiedMessage = msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the provided {@link Battleship}.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pp.battleship.game.singlemode;
|
||||
|
||||
import pp.battleship.game.client.BattleshipClient;
|
||||
import pp.battleship.message.client.AnimationMessage;
|
||||
import pp.battleship.message.client.MapMessage;
|
||||
import pp.battleship.message.client.ShootMessage;
|
||||
import pp.battleship.message.server.EffectMessage;
|
||||
@@ -121,6 +122,7 @@ public void received(StartBattleMessage msg) {
|
||||
@Override
|
||||
public void received(EffectMessage msg) {
|
||||
LOGGER.log(Level.INFO, "Received EffectMessage: {0}", msg); //NON-NLS
|
||||
connection.sendRobotMessage(new AnimationMessage());
|
||||
if (msg.isMyTurn())
|
||||
shoot();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package pp.battleship.message.client;
|
||||
|
||||
import com.jme3.network.serializing.Serializable;
|
||||
|
||||
/**
|
||||
* A message indicating an animation event is finished in the game. (Client → Server)
|
||||
*/
|
||||
@Serializable
|
||||
public class AnimationMessage extends ClientMessage {
|
||||
/**
|
||||
* Constructs a new AnimationMessage instance.
|
||||
*/
|
||||
public AnimationMessage() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a visitor for processing this message.
|
||||
*
|
||||
* @param interpreter the visitor to be used for processing
|
||||
* @param from the connection ID of the sender
|
||||
*/
|
||||
@Override
|
||||
public void accept(ClientInterpreter interpreter, int from) {
|
||||
interpreter.received(this, from);
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,12 @@ public interface ClientInterpreter {
|
||||
* @param from the connection ID from which the message was received
|
||||
*/
|
||||
void received(MapMessage msg, int from);
|
||||
|
||||
/**
|
||||
* Processes a received AnimationMessage.
|
||||
*
|
||||
* @param msg the AnimationMessage to be processed
|
||||
* @param from the connection ID from which the message was received
|
||||
*/
|
||||
void received(AnimationMessage msg, int from);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package pp.battleship.model;
|
||||
|
||||
/**
|
||||
* Represents a shell in the Battleship game.
|
||||
*/
|
||||
public class Shell implements Item {
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
public Shell(Shot shot) {
|
||||
this.x = shot.getX();
|
||||
this.y = shot.getY();
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T accept(Visitor<T> visitor) {
|
||||
return visitor.visit(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(VoidVisitor visitor) {
|
||||
visitor.visit(this);
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,15 @@ public void add(Shot shot) {
|
||||
addItem(shot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a shell to the map and triggers an item addition event.
|
||||
*
|
||||
* @param shell the shell to be added to the map
|
||||
*/
|
||||
public void add(Shell shell) {
|
||||
addItem(shell);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the map and triggers an item removal event.
|
||||
*
|
||||
@@ -181,8 +190,8 @@ public int getHeight() {
|
||||
*/
|
||||
public boolean isValid(Battleship ship) {
|
||||
return isValid(ship.getMinX(), ship.getMinY()) &&
|
||||
isValid(ship.getMaxX(), ship.getMaxY()) &&
|
||||
getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
|
||||
isValid(ship.getMaxX(), ship.getMaxY()) &&
|
||||
getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,8 +203,8 @@ public boolean isValid(Battleship ship) {
|
||||
*/
|
||||
public Battleship findShipAt(int x, int y) {
|
||||
return getShips().filter(ship -> ship.contains(x, y))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +236,7 @@ public boolean isValid(IntPosition pos) {
|
||||
*/
|
||||
public boolean isValid(int x, int y) {
|
||||
return x >= 0 && x < width &&
|
||||
y >= 0 && y < height;
|
||||
y >= 0 && y < height;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,4 +28,12 @@ public interface Visitor<T> {
|
||||
* @return the result of visiting the Battleship element
|
||||
*/
|
||||
T visit(Battleship ship);
|
||||
|
||||
/**
|
||||
* Visits a Shell element.
|
||||
*
|
||||
* @param shell the Shell element to visit
|
||||
* @return the result of visiting the Shell element
|
||||
*/
|
||||
T visit(Shell shell);
|
||||
}
|
||||
|
||||
@@ -25,4 +25,11 @@ public interface VoidVisitor {
|
||||
* @param ship the Battleship element to visit
|
||||
*/
|
||||
void visit(Battleship ship);
|
||||
|
||||
/**
|
||||
* Visits a Shell element.
|
||||
*
|
||||
* @param shell the Shell element to visit
|
||||
*/
|
||||
void visit(Shell shell);
|
||||
}
|
||||
|
||||
@@ -22,5 +22,9 @@ public enum Sound {
|
||||
/**
|
||||
* Sound of a ship being destroyed.
|
||||
*/
|
||||
DESTROYED_SHIP
|
||||
DESTROYED_SHIP,
|
||||
/**
|
||||
* Sound of a shot being fired.
|
||||
*/
|
||||
SHELL_FIRED
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user