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:
Daniel Grigencha
2024-10-13 03:19:44 +02:00
parent 2c4e2fd92d
commit 42b995a4e7
31 changed files with 23693 additions and 81 deletions

View File

@@ -29,7 +29,7 @@ robot.targets=2, 0,\
2, 3 2, 3
# #
# Delay in milliseconds between each shot fired by the RobotClient. # Delay in milliseconds between each shot fired by the RobotClient.
robot.delay=500 robot.delay=2000
# #
# The dimensions of the game map used in single mode. # The dimensions of the game map used in single mode.
# 'map.width' defines the number of columns, and 'map.height' defines the number of rows. # 'map.width' defines the number of columns, and 'map.height' defines the number of rows.

View File

@@ -10,7 +10,6 @@
import com.jme3.app.DebugKeysAppState; import com.jme3.app.DebugKeysAppState;
import com.jme3.app.SimpleApplication; import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState; import com.jme3.app.StatsAppState;
import com.jme3.audio.AudioNode;
import com.jme3.font.BitmapFont; import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText; import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput; import com.jme3.input.KeyInput;

View File

@@ -34,6 +34,7 @@ public class GameSound extends AbstractAppState implements GameEventListener {
private AudioNode splashSound; private AudioNode splashSound;
private AudioNode shipDestroyedSound; private AudioNode shipDestroyedSound;
private AudioNode explosionSound; private AudioNode explosionSound;
private AudioNode shellFiredSound;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -78,6 +79,7 @@ public void initialize(AppStateManager stateManager, Application app) {
shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
shellFiredSound = loadSound(app, "Sound/Effects/missle.wav"); //NON-NLS
} }
/** /**
@@ -93,8 +95,7 @@ private AudioNode loadSound(Application app, String name) {
sound.setLooping(false); sound.setLooping(false);
sound.setPositional(false); sound.setPositional(false);
return sound; return sound;
} } catch (AssetLoadException | AssetNotFoundException ex) {
catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex); LOGGER.log(Level.ERROR, ex.getMessage(), ex);
} }
return null; return null;
@@ -124,12 +125,21 @@ public void shipDestroyed() {
shipDestroyedSound.playInstance(); shipDestroyedSound.playInstance();
} }
/**
* Plays sound effect when a shell has been fired.
*/
public void shellFired() {
if (isEnabled() && shellFiredSound != null)
shellFiredSound.playInstance();
}
@Override @Override
public void receivedEvent(SoundEvent event) { public void receivedEvent(SoundEvent event) {
switch (event.sound()) { switch (event.sound()) {
case EXPLOSION -> explosion(); case EXPLOSION -> explosion();
case SPLASH -> splash(); case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed(); case DESTROYED_SHIP -> shipDestroyed();
case SHELL_FIRED -> shellFired();
} }
} }
} }

View File

@@ -143,6 +143,10 @@ public float getHeight() {
return FIELD_SIZE * map.getHeight(); return FIELD_SIZE * map.getHeight();
} }
public static float getFieldSize() {
return FIELD_SIZE;
}
/** /**
* Converts coordinates from view coordinates to model coordinates. * Converts coordinates from view coordinates to model coordinates.
* *

View File

@@ -7,14 +7,20 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Sphere;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import pp.battleship.model.Shell;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
import pp.util.Position; import pp.util.Position;
import static com.jme3.material.Materials.UNSHADED;
/** /**
* Synchronizes the visual representation of the ship map with the game model. * Synchronizes the visual representation of the ship map with the game model.
* It handles the rendering of ships and shots on the map view, updating the view * It handles the rendering of ships and shots on the map view, updating the view
@@ -65,9 +71,9 @@ public Spatial visit(Shot shot) {
// Create and return a rectangle representing the shot // Create and return a rectangle representing the shot
return view.getApp().getDraw().makeRectangle(p1.getX(), p1.getY(), return view.getApp().getDraw().makeRectangle(p1.getX(), p1.getY(),
SHOT_DEPTH, SHOT_DEPTH,
p2.getX() - p1.getX(), p2.getY() - p1.getY(), p2.getX() - p1.getX(), p2.getY() - p1.getY(),
color); color);
} }
/** /**
@@ -109,6 +115,25 @@ public Spatial visit(Battleship ship) {
return shipNode; return shipNode;
} }
/**
* Creates a visual representation of a shell on the map.
* The shell is represented as a black ellipse.
*
* @param shell the Shell object representing the shell in the model
* @return a Spatial representing the shell on the map
*/
@Override
public Spatial visit(Shell shell) {
Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.getFieldSize() / 2 * 0.8f));
Material mat = new Material(view.getApp().getAssetManager(), UNSHADED); //NON-NLS
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.setColor("Color", ColorRGBA.Black);
ellipse.setMaterial(mat);
ellipse.addControl(new ShellMapControl(view, shell));
return ellipse;
}
/** /**
* Creates a line geometry representing part of the ship's border. * Creates a line geometry representing part of the ship's border.
* *

View File

@@ -20,10 +20,7 @@
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Box;
import pp.battleship.client.BattleshipApp; import pp.battleship.client.BattleshipApp;
import pp.battleship.model.Battleship; import pp.battleship.model.*;
import pp.battleship.model.Rotation;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Shot;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static pp.util.FloatMath.HALF_PI; import static pp.util.FloatMath.HALF_PI;
@@ -35,8 +32,7 @@
* It extends the {@link ShipMapSynchronizer} to provide specific synchronization * It extends the {@link ShipMapSynchronizer} to provide specific synchronization
* logic for the sea map. * logic for the sea map.
*/ */
class SeaSynchronizer extends ShipMapSynchronizer { ; class SeaSynchronizer extends ShipMapSynchronizer {
private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS
private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
private static final String DESTROYER_MODEL = "Models/Destroyer/Destroyer.j3o"; //NON-NLS private static final String DESTROYER_MODEL = "Models/Destroyer/Destroyer.j3o"; //NON-NLS
@@ -44,8 +40,11 @@ class SeaSynchronizer extends ShipMapSynchronizer { ;
private static final String TYPE_II_UBOAT_MODEL = "Models/TypeIIUboat/TypeIIUboat.j3o"; //NON-NLS private static final String TYPE_II_UBOAT_MODEL = "Models/TypeIIUboat/TypeIIUboat.j3o"; //NON-NLS
private static final String TYPE_II_UBOAT_TEXTURE = "Models/TypeIIUboat/Type_II_U-boat_diff.jpg"; //NON-NLS private static final String TYPE_II_UBOAT_TEXTURE = "Models/TypeIIUboat/Type_II_U-boat_diff.jpg"; //NON-NLS
private static final String ATLANTICA_MODEL = "Models/Atlantica/Atlantica.j3o"; //NON-NLS private static final String ATLANTICA_MODEL = "Models/Atlantica/Atlantica.j3o"; //NON-NLS
private static final String ROCKET = "Models/Rocket/Rocket.j3o"; //NON-NLS
private static final String COLOR = "Color"; //NON-NLS private static final String COLOR = "Color"; //NON-NLS
private static final String SHIP = "ship"; //NON-NLS private static final String SHIP = "ship"; //NON-NLS
private static final String SHELL = "shell"; //NON-NLS
private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray; private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray;
private final ShipMap map; private final ShipMap map;
@@ -176,6 +175,36 @@ private ParticleEmitter createFireEffect(Shot shot, Node shipNode) {
return fire; return fire;
} }
/**
* Visits a {@link Shell} and creates a graphical representation of it.
* The shell is represented as a node with a model attached to it.
* The node is then positioned and controlled by a {@link ShellControl}.
*
* @param shell the shell to be represented
* @return the node containing the graphical representation of the shell
*/
@Override
public Spatial visit(Shell shell) {
final Node node = new Node(SHELL);
node.attachChild(createShell());
node.setLocalTranslation(shell.getY() + 0.5f, 10f, shell.getX() + 0.5f);
node.addControl(new ShellControl());
return node;
}
/**
* Creates a graphical representation of a shell.
*
* @return the spatial representing the shell
*/
private Spatial createShell() {
final Spatial model = app.getAssetManager().loadModel(ROCKET);
model.scale(0.0025f);
model.rotate(PI, 0f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/** /**
* Visits a {@link Battleship} and creates a graphical representation of it. * Visits a {@link Battleship} and creates a graphical representation of it.
* The representation is either a 3D model or a simple box depending on the * The representation is either a 3D model or a simple box depending on the

View File

@@ -0,0 +1,42 @@
package pp.battleship.client.gui;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
/**
* Controls the movement and rotation of a shell in the game.
* The shell moves downward at a constant speed and rotates around its Y-axis.
* When the shell reaches a certain Y-coordinate, it is removed from its parent node.
*/
public class ShellControl extends AbstractControl {
private final static float SHELL_SPEED = 7.5f;
private final static float SHELL_ROTATION_SPEED = 0.5f;
private final static float MIN_HEIGHT = 0.7f;
/**
* Updates the shell's position and rotation.
* If the shell's Y-coordinate is less than or equal to 1.0, it is detached from its parent node.
*
* @param tpf time per frame, used to ensure consistent movement speed across different frame rates
*/
@Override
protected void controlUpdate(float tpf) {
spatial.move(0, -SHELL_SPEED * tpf, 0);
spatial.rotate(0, SHELL_ROTATION_SPEED, 0);
if (spatial.getLocalTranslation().getY() <= MIN_HEIGHT) {
spatial.getParent().detachChild(spatial);
}
}
/**
* Renders the shell. This method is currently not used.
*
* @param rm the RenderManager
* @param vp the ViewPort
*/
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// nothing to do here
}
}

View File

@@ -0,0 +1,82 @@
package pp.battleship.client.gui;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Shell;
import pp.util.Position;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/**
* Controls the animation of a shell in the map view.
* This class handles the movement of a shell from its starting position to its target position
* using linear interpolation over a specified duration.
*/
public class ShellMapControl extends AbstractControl {
private static final Logger LOGGER = System.getLogger(ShellMapControl.class.getName());
/**
* The duration of the shell animation in seconds.
*/
private final static float ANIMATION_DURATION = 0.8f;
/**
* The end position of the shell in the map view.
*/
private final Position endPos;
/**
* The progress of the shell's movement, ranging from 0 to 1.
*/
private float progress = 0f;
/**
* Constructs a new instance of {@link ShellMapControl}.
*
* @param view the map view
* @param shell the shell to be controlled
*/
public ShellMapControl(MapView view, Shell shell) {
Vector3f endPos = new Vector3f(shell.getX(), 0, shell.getY());
this.endPos = view.modelToView(endPos.x, endPos.z);
LOGGER.log(Level.DEBUG, "ShellMapControl created with endPos: " + this.endPos);
}
/**
* Updates the position of the shell in the view with linear interpolation.
* This method is called during the update phase.
*
* @param tpf the time per frame
*/
@Override
protected void controlUpdate(float tpf) {
// adjust speed by changing the multiplier
progress += tpf * ANIMATION_DURATION;
// progress is between 0 and 1
if (progress > 1f) {
progress = 1f;
}
// linearly interpolate the current position between (0, 0) and endPos
float newX = (1 - progress) * 0 + progress * endPos.getX() + MapView.getFieldSize() / 2;
float newZ = (1 - progress) * 0 + progress * endPos.getY() + MapView.getFieldSize() / 2;
spatial.setLocalTranslation(newX, newZ, 0);
}
/**
* This method is called during the render phase.
* Currently, it does nothing.
*
* @param rm the RenderManager
* @param vp the ViewPort
*/
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// nothing to do here
}
}

View File

@@ -0,0 +1,250 @@
#
# Generated by Sweet Home 3D - ven. janv. 02 20:37:08 CET 2015
# http://www.sweethome3d.com/
#
newmtl FrontColorNoCulling
illum 1
Ka 0.2 0.2 0.2
Kd 0.2 0.2 0.2
Ks 0.0 0.0 0.0
Ns 0.0
newmtl ForegroundColor
illum 1
Ka 0.2 0.2 0.2
Kd 0.2 0.2 0.2
Ks 0.0 0.0 0.0
Ns 0.0
newmtl white
illum 1
Ka 0.48235294 0.5019608 0.5803922
Kd 0.48235294 0.5019608 0.5803922
Ks 0.0 0.0 0.0
Ns 0.0
newmtl white_Cylinder_5
illum 1
Ka 0.47843137 0.49803922 0.5764706
Kd 0.47843137 0.49803922 0.5764706
Ks 0.0 0.0 0.0
Ns 0.0
newmtl white_Cylinder_10
illum 1
Ka 0.8784314 0.8745098 0.8901961
Kd 0.8784314 0.8745098 0.8901961
Ks 0.0 0.0 0.0
Ns 0.0
newmtl FrontColorNoCulling_11
illum 1
Ka 0.8784314 0.8745098 0.8901961
Kd 0.8784314 0.8745098 0.8901961
Ks 0.0 0.0 0.0
Ns 0.0
newmtl ForegroundColor_12
illum 1
Ka 0.8784314 0.8745098 0.8901961
Kd 0.8784314 0.8745098 0.8901961
Ks 0.0 0.0 0.0
Ns 0.0
newmtl white_Mesh_13
illum 1
Ka 0.6 0.6 0.6
Kd 0.6 0.6 0.6
Ks 0.0 0.0 0.0
Ns 0.0
newmtl Cube_1_1_1
illum 1
Ka 0.0 0.0 0.0
Kd 0.0 0.0 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_7_7
illum 1
Ka 0.4 0.4 0.4
Kd 0.4 0.4 0.4
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_10_10
illum 1
Ka 0.8 0.4 0.0
Kd 0.8 0.4 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_11_11
illum 2
Ka 0.2 0.2 0.2
Kd 0.2 0.2 0.2
Ks 0.0 0.0 0.0
Ns 1.0
newmtl 12_12
illum 1
Ka 0.2 0.2 0.2
Kd 0.2 0.2 0.2
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cube_1_1_1_Cube_1_1_1_38
illum 1
Ka 0.6 0.6 0.6
Kd 0.6 0.6 0.6
Ks 0.0 0.0 0.0
Ns 1.0
newmtl white_Cylinder_58
illum 1
Ka 0.1882353 0.27058825 0.58431375
Kd 0.1882353 0.27058825 0.58431375
Ks 0.0 0.0 0.0
Ns 0.0
newmtl white_Cylinder_59
illum 1
Ka 0.3137255 0.14901961 0.011764706
Kd 0.3137255 0.14901961 0.011764706
Ks 0.0 0.0 0.0
Ns 0.0
newmtl 1_1
illum 2
Ka 0.2 0.2 0.2
Kd 1.0 1.0 1.0
Ks 0.5 0.5 0.5
Ns 64.0
Ni 1.0
d 0.48000002
map_Kd Missile_AIM-120_D_[AMRAAM]_1_1.png
newmtl Cube_1_2_2
illum 1
Ka 0.8 0.4 0.0
Kd 0.8 0.4 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_4_4
illum 2
Ka 0.6 0.6 0.6
Kd 0.6 0.6 0.6
Ks 0.5 0.5 0.5
Ns 64.0
newmtl Cylinder_5_5
illum 2
Ka 0.8 0.8 0.0
Kd 0.8 0.8 0.0
Ks 0.5 0.5 0.5
Ns 64.0
newmtl Cylinder_6_6
illum 2
Ka 0.8784314 0.8745098 0.8901961
Kd 0.8784314 0.8745098 0.8901961
Ks 0.5 0.5 0.5
Ns 64.0
newmtl Cylinder_10_10_Cylinder_10_10_73
illum 1
Ka 0.2 0.2 0.2
Kd 0.2 0.2 0.2
Ks 0.0 0.0 0.0
Ns 1.0
newmtl 11_11
illum 1
Ka 0.6 0.6 0.6
Kd 0.6 0.6 0.6
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cube_1_1_1_Cube_1_1_1_76
illum 1
Ka 0.2 0.2 0.2
Kd 1.0 1.0 1.0
Ks 0.0 0.0 0.0
Ns 1.0
Ni 1.0
map_Kd Missile_AIM-120_D_[AMRAAM]_Cube_1_1_1_Cube_1_1_1_76.png
newmtl Cylinder_2_2
illum 2
Ka 0.6 0.6 0.6
Kd 0.6 0.6 0.6
Ks 0.5 0.5 0.5
Ns 64.0
newmtl Cylinder_3_3
illum 1
Ka 0.4 0.4 0.0
Kd 0.4 0.4 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_4_4_Cylinder_4_4_79
illum 1
Ka 0.0 0.0 0.0
Kd 0.0 0.0 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cube_1_5_5
illum 1
Ka 0.2 0.2 0.2
Kd 1.0 1.0 1.0
Ks 0.0 0.0 0.0
Ns 1.0
map_Kd Missile_AIM-120_D_[AMRAAM]_Cube_1_5_5.png
newmtl Cube_1_6_6
illum 1
Ka 0.2 0.2 0.2
Kd 1.0 1.0 1.0
Ks 0.0 0.0 0.0
Ns 1.0
Ni 1.0
map_Kd Missile_AIM-120_D_[AMRAAM]_Cube_1_6_6.png
newmtl Cylinder_1_1
illum 1
Ka 0.4 0.4 0.4
Kd 0.4 0.4 0.4
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cube_1_5_5_Cube_1_5_5_86
illum 1
Ka 0.2 0.2 0.2
Kd 0.2 0.2 0.2
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cube_1_6_6_Cube_1_6_6_87
illum 1
Ka 0.8 0.0 0.0
Kd 0.8 0.0 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_7_7_Cylinder_7_7_88
illum 1
Ka 0.8 0.4 0.0
Kd 0.8 0.4 0.0
Ks 0.0 0.0 0.0
Ns 1.0
newmtl Cylinder_8_8
illum 1
Ka 0.4 0.6 0.0
Kd 0.4 0.6 0.0
Ks 0.0 0.0 0.0
Ns 1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/de/3d-model/aim-120d-missile-51025.html
License: Free Personal Use Only

View File

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

View File

@@ -10,12 +10,10 @@
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music;
import pp.battleship.notification.Sound; 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. * 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; this.myTurn = myTurn;
} }
/**
* Checks if the battle state should be shown.
*
* @return true if the battle state should be shown, false otherwise
*/
@Override @Override
public boolean showBattle() { public boolean showBattle() {
return true; return true;
} }
/**
* Handles a click on the opponent's map.
*
* @param pos the position where the click occurred
*/
@Override @Override
public void clickOpponentMap(IntPoint pos) { public void clickOpponentMap(IntPoint pos) {
if (!myTurn) if (!myTurn)
@@ -53,21 +61,16 @@ else if (logic.getOpponentMap().isValid(pos))
*/ */
@Override @Override
public void receivedEffect(EffectMessage msg) { public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS ClientGameLogic.LOGGER.log(System.Logger.Level.INFO, "report effect: {0}", msg); //NON-NLS
playSound(msg); // Update turn and info text
myTurn = msg.isMyTurn(); myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey()); logic.setInfoText(msg.getInfoTextKey());
affectedMap(msg).add(msg.getShot()); // Add the shell to the affected map
if (destroyedOpponentShip(msg)) Shell shell = new Shell(msg.getShot());
logic.getOpponentMap().add(msg.getDestroyedShip()); affectedMap(msg).add(shell);
if (msg.isGameOver()) { // Change state to AnimationState
msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add); logic.playSound(Sound.SHELL_FIRED);
logic.setState(new GameOverState(logic)); logic.setState(new AnimationState(logic, msg, shell));
if (msg.isOwnShot())
logic.playMusic(Music.VICTORY_MUSIC);
else
logic.playMusic(Music.DEFEAT_MUSIC);
}
} }
/** /**
@@ -79,29 +82,4 @@ public void receivedEffect(EffectMessage msg) {
private ShipMap affectedMap(EffectMessage msg) { private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap(); 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);
}
} }

View File

@@ -8,6 +8,7 @@
package pp.battleship.game.server; package pp.battleship.game.server;
import pp.battleship.BattleshipConfig; import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
@@ -17,7 +18,6 @@
import pp.battleship.message.server.StartBattleMessage; import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.Rotation;
import java.lang.System.Logger; import java.lang.System.Logger;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@@ -36,6 +36,7 @@ public class ServerGameLogic implements ClientInterpreter {
private final BattleshipConfig config; private final BattleshipConfig config;
private final List<Player> players = new ArrayList<>(2); private final List<Player> players = new ArrayList<>(2);
private final Set<Player> readyPlayers = new HashSet<>(); private final Set<Player> readyPlayers = new HashSet<>();
private final Set<Player> finishedAnimation = new HashSet<>();
private final ServerSender serverSender; private final ServerSender serverSender;
private Player activePlayer; private Player activePlayer;
private ServerState state = ServerState.WAIT; private ServerState state = ServerState.WAIT;
@@ -192,9 +193,9 @@ private boolean isWithinBounds(Battleship ship) {
int width = config.getMapWidth(); int width = config.getMapWidth();
int height = config.getMapHeight(); int height = config.getMapHeight();
return minX >= 0 && minX < width && return minX >= 0 && minX < width &&
minY >= 0 && minY < height && minY >= 0 && minY < height &&
maxX >= 0 && maxX < width && maxX >= 0 && maxX < width &&
maxY >= 0 && maxY < height; maxY >= 0 && maxY < height;
} }
/** /**
@@ -226,8 +227,40 @@ private boolean overlaps(List<Battleship> ships) {
public void received(ShootMessage msg, int from) { public void received(ShootMessage msg, int from) {
if (state != ServerState.BATTLE) if (state != ServerState.BATTLE)
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
else else {
setState(ServerState.ANIMATION);
shoot(getPlayerById(from), msg.getPosition()); 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(activePlayer, EffectMessage.miss(true, pos));
send(otherPlayer, EffectMessage.miss(false, pos)); send(otherPlayer, EffectMessage.miss(false, pos));
activePlayer = otherPlayer; activePlayer = otherPlayer;
} } else {
else {
// shot hit a ship // shot hit a ship
selectedShip.hit(pos); selectedShip.hit(pos);
if (otherPlayer.getMap().getRemainingShips().isEmpty()) { if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
@@ -274,13 +306,11 @@ void shoot(Player p, IntPoint pos) {
send(activePlayer, EffectMessage.won(pos, selectedShip)); send(activePlayer, EffectMessage.won(pos, selectedShip));
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips())); send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
setState(ServerState.GAME_OVER); setState(ServerState.GAME_OVER);
} } else if (selectedShip.isDestroyed()) {
else if (selectedShip.isDestroyed()) {
// ship has been destroyed, but game is not yet over // ship has been destroyed, but game is not yet over
send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip)); send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip)); send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
} } else {
else {
// ship has been hit, but it hasn't been destroyed // ship has been hit, but it hasn't been destroyed
send(activePlayer, EffectMessage.hit(true, pos)); send(activePlayer, EffectMessage.hit(true, pos));
send(otherPlayer, EffectMessage.hit(false, pos)); send(otherPlayer, EffectMessage.hit(false, pos));

View File

@@ -26,6 +26,11 @@ enum ServerState {
*/ */
BATTLE, 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. * The game has ended because all the ships of one player have been destroyed.
*/ */

View File

@@ -7,10 +7,7 @@
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.*;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.model.Battleship; 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()); 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}. * Creates a copy of the provided {@link Battleship}.
* *

View File

@@ -1,6 +1,7 @@
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
@@ -121,6 +122,7 @@ public void received(StartBattleMessage msg) {
@Override @Override
public void received(EffectMessage msg) { public void received(EffectMessage msg) {
LOGGER.log(Level.INFO, "Received EffectMessage: {0}", msg); //NON-NLS LOGGER.log(Level.INFO, "Received EffectMessage: {0}", msg); //NON-NLS
connection.sendRobotMessage(new AnimationMessage());
if (msg.isMyTurn()) if (msg.isMyTurn())
shoot(); shoot();
} }

View File

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

View File

@@ -26,4 +26,12 @@ public interface ClientInterpreter {
* @param from the connection ID from which the message was received * @param from the connection ID from which the message was received
*/ */
void received(MapMessage msg, int from); 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);
} }

View File

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

View File

@@ -91,6 +91,15 @@ public void add(Shot shot) {
addItem(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. * Removes an item from the map and triggers an item removal event.
* *
@@ -181,8 +190,8 @@ public int getHeight() {
*/ */
public boolean isValid(Battleship ship) { public boolean isValid(Battleship ship) {
return isValid(ship.getMinX(), ship.getMinY()) && return isValid(ship.getMinX(), ship.getMinY()) &&
isValid(ship.getMaxX(), ship.getMaxY()) && isValid(ship.getMaxX(), ship.getMaxY()) &&
getShips().filter(s -> s != ship).noneMatch(ship::collidesWith); getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
} }
/** /**
@@ -194,8 +203,8 @@ public boolean isValid(Battleship ship) {
*/ */
public Battleship findShipAt(int x, int y) { public Battleship findShipAt(int x, int y) {
return getShips().filter(ship -> ship.contains(x, y)) return getShips().filter(ship -> ship.contains(x, y))
.findAny() .findAny()
.orElse(null); .orElse(null);
} }
/** /**
@@ -227,7 +236,7 @@ public boolean isValid(IntPosition pos) {
*/ */
public boolean isValid(int x, int y) { public boolean isValid(int x, int y) {
return x >= 0 && x < width && return x >= 0 && x < width &&
y >= 0 && y < height; y >= 0 && y < height;
} }
/** /**

View File

@@ -28,4 +28,12 @@ public interface Visitor<T> {
* @return the result of visiting the Battleship element * @return the result of visiting the Battleship element
*/ */
T visit(Battleship ship); 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);
} }

View File

@@ -25,4 +25,11 @@ public interface VoidVisitor {
* @param ship the Battleship element to visit * @param ship the Battleship element to visit
*/ */
void visit(Battleship ship); void visit(Battleship ship);
/**
* Visits a Shell element.
*
* @param shell the Shell element to visit
*/
void visit(Shell shell);
} }

View File

@@ -22,5 +22,9 @@ public enum Sound {
/** /**
* Sound of a ship being destroyed. * Sound of a ship being destroyed.
*/ */
DESTROYED_SHIP DESTROYED_SHIP,
/**
* Sound of a shot being fired.
*/
SHELL_FIRED
} }

View File

@@ -18,6 +18,7 @@
import pp.battleship.game.server.Player; import pp.battleship.game.server.Player;
import pp.battleship.game.server.ServerGameLogic; import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender; import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
@@ -56,8 +57,7 @@ public class BattleshipServer implements MessageListener<HostedConnection>, Conn
try { try {
manager.readConfiguration(new FileInputStream("logging.properties")); manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
} } catch (IOException e) {
catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage()); LOGGER.log(Level.INFO, e.getMessage());
} }
} }
@@ -92,8 +92,7 @@ private void startServer() {
myServer.start(); myServer.start();
registerListeners(); registerListeners();
LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS
} } catch (IOException e) {
catch (IOException e) {
LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS
exit(1); exit(1);
} }
@@ -102,8 +101,7 @@ private void startServer() {
private void processNextMessage() { private void processNextMessage() {
try { try {
pendingMessages.take().process(logic); pendingMessages.take().process(logic);
} } catch (InterruptedException ex) {
catch (InterruptedException ex) {
LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
@@ -114,6 +112,7 @@ private void initializeSerializables() {
Serializer.registerClass(StartBattleMessage.class); Serializer.registerClass(StartBattleMessage.class);
Serializer.registerClass(MapMessage.class); Serializer.registerClass(MapMessage.class);
Serializer.registerClass(ShootMessage.class); Serializer.registerClass(ShootMessage.class);
Serializer.registerClass(AnimationMessage.class);
Serializer.registerClass(EffectMessage.class); Serializer.registerClass(EffectMessage.class);
Serializer.registerClass(Battleship.class); Serializer.registerClass(Battleship.class);
Serializer.registerClass(IntPoint.class); Serializer.registerClass(IntPoint.class);
@@ -123,6 +122,7 @@ private void initializeSerializables() {
private void registerListeners() { private void registerListeners() {
myServer.addMessageListener(this, MapMessage.class); myServer.addMessageListener(this, MapMessage.class);
myServer.addMessageListener(this, ShootMessage.class); myServer.addMessageListener(this, ShootMessage.class);
myServer.addMessageListener(this, AnimationMessage.class);
myServer.addConnectionListener(this); myServer.addConnectionListener(this);
} }