38 Commits

Author SHA1 Message Date
Fleischer Hanno hanno.fleischer@unibw.de
f8e97266d5 fixed bug when playing rocket audio to not produce an AudioRender error 2024-10-13 11:53:24 +02:00
Fleischer Hanno hanno.fleischer@unibw.de
9e591e37c3 Added sound to rocket firing 2024-10-13 08:58:48 +02:00
Hanno Fleischer
487305dccc added the rocket sound wav and fixed code and check style
also includes minor fixes
2024-10-13 08:09:19 +02:00
Hanno Fleischer
22d827b074 adjusted positioning of the modern battle ship and set fullscreen mode to false 2024-10-11 11:53:13 +02:00
Hanno Fleischer
074b38540d Merge branch 'b_Fleischer_Hanno' of https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-01 into b_Fleischer_Hanno 2024-10-11 11:47:05 +02:00
Hanno Fleischer
3838766504 added a fire effect for hit ships
this will now display a burning fire at the position where the ships was hit
and displays it until thge ship is removed.
2024-10-11 11:46:37 +02:00
Fleischer Hanno hanno.fleischer@unibw.de
54e5719edf added README.txt for the rocket model to credit its source 2024-10-11 10:49:04 +02:00
Hanno Fleischer
9df809ded5 adjusted the ModernBattleShip to be a j30 object and load withc its corresponding texture 2024-10-11 09:54:12 +02:00
Hanno Fleischer
93ae95ce59 adjusted size of rocket, and removed unused methods and import statements 2024-10-11 09:42:12 +02:00
Hanno Fleischer
ffd3951a78 added JavaDocs commects where they where missing and removed outcommented code 2024-10-11 01:25:10 +02:00
Hanno Fleischer
c56767d994 added rest solution for exercise 13
added the representation for the shell element in the map which will be displayed when you shoot,
2024-10-11 00:48:40 +02:00
Fleischer Hanno hanno.fleischer@unibw.de
f99b91324c part solution for exercise 13
added an animation state for the server and client and gave it the functionality to display a 3d model representing the shot of the other person
adjusted the server to serialize the new messages for handling the animation states
2024-10-10 23:10:39 +02:00
Fleischer Hanno hanno.fleischer@unibw.de
da2508395c fixed minor issues and cleaned up code to not include duplicate code. 2024-10-07 17:54:35 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
4820a76ff0 added the backgroundmusic to listeners in the initsimpleapp
and not in the getter of backgroundmusic.
2024-10-05 21:07:00 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
9dc3984f35 renamed the method in BackgroundMusic
renamed the method toogleMusic to toggleMusic.
2024-10-05 19:08:57 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
f6f87c4f5d adjusted the preferences in BackgroundMusic and MainVolume 2024-10-05 18:51:25 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
d3429bf4f0 fixed a bug at the victory music
the victory audio node was not part of the set volume method
now implemented the method to set volume for victory music.
2024-10-05 18:40:30 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
ecbe486d3b minor tweaks to make the code pass the check style 2024-10-05 18:25:25 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
52673dfbce added MainVolume slider in the menu
added the logic so that the volume of music or sound is multiplied by the main volume
created the class MainVolume to handle the logic of the main volume
adjusted methods in GameSounds and BackgroundMusic to integrate main volume
2024-10-05 17:32:14 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
586251b2ad adjusted the paths of the music
added the README.txt for each music piece to state the source of the music.
2024-10-05 14:46:08 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
ca57507b53 added the missing solution for exercise 12
ships will now sink when destroyed and will be removed when they are fully submerged
2024-10-05 14:06:45 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
44a25a2e1f added part solution for exercise 12
added the Effecthandler class to handle the effects for a shot hit or missed
added jme3-effects libary and used it to display the effects for the shots
minor tweaks the the gui and backgroundmuisc
2024-10-05 13:20:34 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
dca0875ad5 Adjusted tthe logic of the server and client
when the client sends a wrong map the server will send the client back to the editro state
2024-10-05 12:35:49 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
0f629252bc fixed the toggle button for the victory music
added the pause(gameOverMusicV) to the toggle method for the music control
so that it will be paused when the music is turned off.
2024-10-05 09:01:48 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
b18705f064 added the logic to play diffrent Background music during diffrent states
added the a Enum for the Music and the MusicEvent
added a song for the game victory
adjusted the backgroundmusic class to handle the logic of the new song
added the Backgroundmusic to the ClientLogic as a EventListener.
2024-10-04 20:43:37 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
961242bb20 added missing javadocs in the NetworkDialog.java 2024-10-04 17:30:21 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
05271beded changed the postion of the host server checkbox
moved the checkbox form below the connect button inside the input
container for the hostname and port. The checkbox is the lowest seated
element in the input container now.
2024-10-04 15:49:07 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
f6bc65471a added solution for exercise 11
added a checkbox to start a server from your client
copied the server to the client and removed its main method and changed the constructor to public
2024-10-04 15:40:38 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
0f080363f3 adjusted the in/de-crement button
adjusted the delta of the slider so the button hav smaller steps in between
2024-10-03 18:33:56 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
6e0a93b74d fixed toggleMusic in BackgroundMusic.java
changed the Preference save form volume to the boolean musicenable
2024-10-03 17:56:12 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
d471f524d0 solution for exercise 10
added BackgroundMusic which is being handled in BackgroundMusic.java
changed the menu to incoporate volume controls for the music
2024-10-03 17:39:54 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
d15f1a3f5f edited battleship.properties
added a new error message in case ships are out of bounds
2024-10-02 21:04:59 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
562a478ef8 added solution for exercise 9
added the models for the 3 remaining ship types
and implemented them in the SeaSynchronizer.java
also added the correct logic for the ships to be displayed correctly
2024-10-02 20:56:05 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
46f75188cb fixed the code for exercise 8
fixed the code so that it uses a for each loop instead of an for int i
and adjusted the part for validating ship overlaping to use the
isCollidingWith method of Battleship
2024-10-02 17:35:34 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
7b70666332 added the solution for exercise 8 for the client
added the checkMapToLoad() in EditorState.java so if the Player tries to
load a map it will be checked if the placement is correct.
2024-10-02 14:50:14 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
1bac56c92c adjusted the server Mapcheck
inverted the else if statement in ServerGameLogic.java received() so that
if the map check returns that the map is correct the programm continues on
instead of throwing an error message and removed the hard limit of an 10x10 map in the checkMap() Method
2024-10-02 14:16:46 +02:00
Hanno Fleischer hanno.fleischer@unibw.de
4dcd53a660 Added part-solution for exercise 8
added the solution for exercise 8 for the sever sided test to ServerGameLogic.java
2024-10-02 13:57:17 +02:00
Fleischer Hanno hanno.fleischer@unibw.de
f759eddda1 solution exercise 7
edited in BattleState.java the receivedMsg() method so that if the game moves to the game over state
the remaining opponent ships will be added to the list of the opponenets instead of your own list.
edited the ShipMap.java so that when the notifylisteners is called for removing an object it will be
handled as an ItemRemovedEvent instead of an ItemAddedEvent
2024-10-02 11:01:13 +02:00
87 changed files with 223627 additions and 106455 deletions

View File

@@ -15,7 +15,6 @@ implementation project(":battleship:model")
runtimeOnly libs.jme3.plugins runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg runtimeOnly libs.jme3.jogg
runtimeOnly libs.jme3.testdata runtimeOnly libs.jme3.testdata
} }
application { application {

View File

@@ -9,7 +9,7 @@
# #
# Specifies the map used by the opponent in single mode. # Specifies the map used by the opponent in single mode.
# Single mode is activated if this property is set. # Single mode is activated if this property is set.
#map.opponent=maps/map2.json map.opponent=maps/map2.json
# #
# Specifies the map used by the player in single mode. # Specifies the map used by the player in single mode.
# The player must define their own map if this property is not set. # The player must define their own map if this property is not set.
@@ -23,13 +23,13 @@ map.own=maps/map1.json
# 2, 3 # 2, 3
# defines four shots, namely at the coordinates # defines four shots, namely at the coordinates
# (x=2, y=0), (x=2, y=1), (x=2, y=2), and (x=2, y=3) # (x=2, y=0), (x=2, y=1), (x=2, y=2), and (x=2, y=3)
robot.targets=2, 0,\ robot.targets=2, 3,\
2, 1,\ 2, 4,\
2, 2,\ 2, 5,\
2, 3 2, 8
# #
# Delay in milliseconds between each shot fired by the RobotClient. # Delay in milliseconds between each shot fired by the RobotClient.
robot.delay=2000 robot.delay=500
# #
# 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

@@ -1,267 +1,242 @@
package pp.battleship.client; package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData.DataType; import com.jme3.audio.AudioData.DataType;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import pp.battleship.notification.GameEventListener; import com.jme3.audio.AudioSource.Status;
import pp.battleship.notification.Music;
import pp.battleship.notification.MusicEvent; import pp.battleship.notification.MusicEvent;
import pp.battleship.notification.GameEventListener;
import java.lang.System.Logger; import java.lang.System.Logger;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import static pp.util.PreferencesUtils.getPreferences; public class BackgroundMusic implements GameEventListener {
private static final String VOLUME_PREF = "volume";
private static final String MUSIC_ENABLED_PREF = "musicEnabled";
private static final Preferences PREFS = Preferences.userNodeForPackage(BackgroundMusic.class);
/** static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
* The BackgroundMusic class represents the background music in the Battleship game application.
* It extends the AbstractAppState class and provides functionalities for playing the menu music,
* game music, victory music, and defeat music.
*/
public class BackgroundMusic extends AbstractAppState implements GameEventListener {
/**
* Logger for the BackgroundMusic class.
*/
private static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
/** private static final String MENU_MUSIC = "Music/MainMenu/Dark_Intro.ogg";
* Preferences for storing music settings. private static final String BATTLE_MUSIC = "Music/BattleTheme/boss_battle_#2_metal_loop.wav";
*/ private static final String GAME_OVER_MUSIC_L = "Music/GameOver/Lose/Lose.ogg";
private static final Preferences PREFERENCES = getPreferences(BackgroundMusic.class); private static final String GAME_OVER_MUSIC_V = "Music/GameOver/Victory/Victory.wav";
/** private final AudioNode menuMusic;
* Preference key for enabling/disabling music. private final AudioNode battleMusic;
*/ private final AudioNode gameOverMusicL;
private static final String ENABLED_PREF = "enabled"; //NON-NLS private final AudioNode gameOverMusicV;
private String lastNodePlayed;
/** private boolean musicEnabled;
* Preference key for storing the volume level.
*/
private static final String VOLUME_PREF = "volume"; //NON-NLS
/**
* Path to the menu music file.
*/
private static final String MENU_MUSIC_PATH = "Sound/Music/menu_music.ogg";
/**
* Path to the game music file.
*/
private static final String GAME_MUSIC_PATH = "Sound/Music/pirates.ogg";
/**
* Path to the victory music file.
*/
private static final String VICTORY_MUSIC_PATH = "Sound/Music/win_the_game.ogg";
/**
* Path to the defeat music file.
*/
private static final String DEFEAT_MUSIC_PATH = "Sound/Music/defeat.ogg";
/**
* AudioNode for the menu music.
*/
private AudioNode menuMusic;
/**
* AudioNode for the game music.
*/
private AudioNode gameMusic;
/**
* AudioNode for the victory music.
*/
private AudioNode victoryMusic;
/**
* AudioNode for the defeat music.
*/
private AudioNode defeatMusic;
/**
* The currently playing AudioNode.
*/
private AudioNode currentMusic;
/**
* The volume level for the background music.
*/
private float volume; private float volume;
/** private final BattleshipApp app;
* Checks if music is enabled in the preferences.
*
* @return {@code true} if music is enabled, {@code false} otherwise.
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/** /**
* Sets the enabled state of this AppState. * Initializes and controls the BackgroundMusic
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
* *
* @param enabled {@code true} to enable the AppState, {@code false} to disable it. * @param app The main Application
*/ */
@Override public BackgroundMusic(BattleshipApp app) {
public void setEnabled(boolean enabled) { this.volume = PREFS.getFloat(VOLUME_PREF, 1.0f);
if (isEnabled() == enabled) return; this.musicEnabled = PREFS.getBoolean(MUSIC_ENABLED_PREF, true);
super.setEnabled(enabled); this.app = app;
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
playCurrentMusic();
}
/** menuMusic = createAudioNode(MENU_MUSIC);
* Initializes the music for the game. battleMusic = createAudioNode(BATTLE_MUSIC);
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)} gameOverMusicL = createAudioNode(GAME_OVER_MUSIC_L);
* gameOverMusicV = createAudioNode(GAME_OVER_MUSIC_V);
* @param stateManager The state manager stop(battleMusic);
* @param app The application stop(gameOverMusicL);
*/ stop(gameOverMusicV);
@Override
public void initialize(AppStateManager stateManager, Application app) {
LOGGER.log(Level.INFO, "Initializing background music"); //NON-NLS
super.initialize(stateManager, app);
menuMusic = loadMusic(app, MENU_MUSIC_PATH);
gameMusic = loadMusic(app, GAME_MUSIC_PATH);
victoryMusic = loadMusic(app, VICTORY_MUSIC_PATH);
defeatMusic = loadMusic(app, DEFEAT_MUSIC_PATH);
currentMusic = menuMusic;
playCurrentMusic();
}
/** lastNodePlayed = menuMusic.getName();
* Loads a music file and initializes an AudioNode with the specified settings.
* if(musicEnabled) {
* @param app The application instance. play(menuMusic);
* @param name The name of the music file to load.
* @return The initialized AudioNode, or {@code null} if the file could not be loaded.
*/
private AudioNode loadMusic(Application app, String name) {
try {
this.volume = PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
final AudioNode music = new AudioNode(app.getAssetManager(), name, DataType.Stream);
music.setLooping(true);
music.setVolume(volume);
music.setPositional(false);
music.setDirectional(false);
return music;
} catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
} }
return null;
} }
/** /**
* Plays the current music if the music is enabled. * This method will be used to create the audio node containing the music
* Stops the current music if the music is disabled. *
* @param musicFilePath the file path to the music
* @return the created audio node
*/ */
private void playCurrentMusic() { private AudioNode createAudioNode(String musicFilePath) {
if (isEnabled()) { AudioNode audioNode = new AudioNode(app.getAssetManager(), musicFilePath, DataType.Stream);
if (currentMusic != null) { audioNode.setVolume(volume * app.getMainVolumeControl().getMainVolume());
LOGGER.log(Level.INFO, "Playing current music"); //NON-NLS audioNode.setPositional(false);
currentMusic.play(); audioNode.setLooping(true);
audioNode.setName(musicFilePath);
return audioNode;
}
/**
* sets the give audio node to play
*
* @param audioNode the audio node which should start to play
*/
public void play(AudioNode audioNode) {
if (musicEnabled && (audioNode.getStatus() == Status.Stopped || audioNode.getStatus() == Status.Paused)) {
audioNode.play();
lastNodePlayed = audioNode.getName();
}
}
/**
* stops the given audio node from playing
*
* @param audioNode the audio node to be stopped
*/
public void stop(AudioNode audioNode) {
if (audioNode.getStatus() == Status.Playing) {
audioNode.stop();
}
}
/**
* pauses the given audi node
*
* @param audioNode the audio node to be paused
*/
public void pause(AudioNode audioNode) {
if (audioNode.getStatus() == Status.Playing) {
audioNode.pause();
}
}
/**
* Toggle Method to control the music to switch it on or off
*/
public void toggleMusic() {
this.musicEnabled = !this.musicEnabled;
if (musicEnabled) {
switch (lastNodePlayed){
case MENU_MUSIC:
play(menuMusic);
break;
case BATTLE_MUSIC:
play(battleMusic);
break;
case GAME_OVER_MUSIC_L:
play(gameOverMusicL);
break;
case GAME_OVER_MUSIC_V:
play(gameOverMusicV);
break;
} }
} else { } else {
if (currentMusic != null) { pause(menuMusic);
currentMusic.stop(); pause(battleMusic);
} pause(gameOverMusicL);
pause(gameOverMusicV);
} }
PREFS.putBoolean(MUSIC_ENABLED_PREF, musicEnabled);
} }
/** /**
* Plays the game music. * this method is used when the main volume changes
*/ */
private void gameMusic() { public void setVolume(){
if (isEnabled() && gameMusic != null) { setVolume(PREFS.getFloat(VOLUME_PREF, 1.0f));
stopAll();
LOGGER.log(Level.INFO, "Playing game music"); //NON-NLS
PREFERENCES.putFloat(VOLUME_PREF, volume);
gameMusic.play();
}
} }
/** /**
* Plays the victory music. * Method to set the volume for the music
*/
private void victoryMusic() {
if (isEnabled() && victoryMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing victory music"); //NON-NLS
PREFERENCES.putFloat(VOLUME_PREF, volume);
victoryMusic.play();
}
}
/**
* Plays the defeat music.
*/
private void defeatMusic() {
if (isEnabled() && defeatMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing defeat music"); //NON-NLS
PREFERENCES.putFloat(VOLUME_PREF, volume);
defeatMusic.play();
}
}
/**
* Stops all music.
*/
private void stopAll() {
if (menuMusic != null) menuMusic.stop();
if (gameMusic != null) gameMusic.stop();
if (victoryMusic != null) victoryMusic.stop();
if (defeatMusic != null) defeatMusic.stop();
}
/**
* Handles the received music event and plays the corresponding music.
* *
* @param event The music event to handle. * @param volume float to transfer the new volume
*/
@Override
public void receivedEvent(MusicEvent event) {
switch (event.music()) {
case GAME_MUSIC -> {
gameMusic();
currentMusic = gameMusic;
}
case VICTORY_MUSIC -> {
victoryMusic();
currentMusic = victoryMusic;
}
case DEFEAT_MUSIC -> {
defeatMusic();
currentMusic = defeatMusic;
}
}
}
/**
* Sets the volume for the background music and updates the preferences.
*
* @param volume The volume level to set.
*/ */
public void setVolume(float volume) { public void setVolume(float volume) {
LOGGER.log(Level.INFO, "Setting volume to {0}", volume); //NON-NLS
this.volume = volume; this.volume = volume;
currentMusic.setVolume(volume); float mainVolume = app.getMainVolumeControl().getMainVolume();
PREFERENCES.putFloat(VOLUME_PREF, volume); menuMusic.setVolume(volume * mainVolume);
battleMusic.setVolume(volume * mainVolume);
gameOverMusicL.setVolume(volume * mainVolume);
gameOverMusicV.setVolume(volume * mainVolume);
PREFS.putFloat(VOLUME_PREF, volume);
} }
/** /**
* Returns the volume level for the background music. * This method retuns the volume
* *
* @return The volume level as a float. * @return the current volume as a float
*/ */
public float getVolume() { public float getVolume() {
return volume; return volume;
} }
}
/**
* Returns if music should be played or not
*
* @return boolean value in music should be played
*/
public boolean isMusicEnabled() {
return musicEnabled;
}
/**
* changes the music to the specified music if it isn't already playing
*
* @param music the music to play
*/
public void changeMusic(Music music) {
if(music == Music.MENU_THEME && !lastNodePlayed.equals(MENU_MUSIC)) {
LOGGER.log(Level.INFO, "Received Music change Event {0}", music.toString());
stop(battleMusic);
stop(gameOverMusicL);
stop(gameOverMusicV);
play(menuMusic);
lastNodePlayed = menuMusic.getName();
} else if (music == Music.BATTLE_THEME && !lastNodePlayed.equals(BATTLE_MUSIC)) {
LOGGER.log(Level.INFO, "Received Music change Event {0}", music.toString());
stop(menuMusic);
stop(gameOverMusicL);
stop(gameOverMusicV);
play(battleMusic);
lastNodePlayed = battleMusic.getName();
} else if (music == Music.GAME_OVER_THEME_L && !lastNodePlayed.equals(GAME_OVER_MUSIC_L)) {
LOGGER.log(Level.INFO, "Received Music change Event {0}", music.toString());
stop(menuMusic);
stop(battleMusic);
stop(gameOverMusicV);
play(gameOverMusicL);
lastNodePlayed = gameOverMusicL.getName();
} else if (music == Music.GAME_OVER_THEME_V && !lastNodePlayed.equals(GAME_OVER_MUSIC_V)){
LOGGER.log(Level.INFO, "Received Music change Event {0}", music.toString());
stop(menuMusic);
stop(battleMusic);
stop(gameOverMusicL);
play(gameOverMusicV);
lastNodePlayed = gameOverMusicV.getName();
}
}
/**
* the method which receives the Event
*
* @param music the received Event
*/
@Override
public void receivedEvent (MusicEvent music){
LOGGER.log(Level.INFO, "Received Music change Event {0}", music.toString());
switch (music.music()){
case MENU_THEME:
changeMusic(Music.MENU_THEME);
break;
case BATTLE_THEME:
changeMusic(Music.BATTLE_THEME);
break;
case GAME_OVER_THEME_L:
changeMusic(Music.GAME_OVER_THEME_L);
break;
case GAME_OVER_THEME_V:
changeMusic(Music.GAME_OVER_THEME_V);
break;
}
}
}

View File

@@ -122,13 +122,24 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
*/ */
private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed); private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed);
/**
* The Object which handles the background music
*/
private BackgroundMusic backgroundMusic;
/**
* The object that handles the main volume
*/
private MainVolume mainVolume;
static { static {
// Configure logging // Configure logging
LogManager manager = LogManager.getLogManager(); LogManager manager = LogManager.getLogManager();
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());
} }
} }
@@ -224,6 +235,10 @@ public void simpleInitApp() {
setupStates(); setupStates();
setupGui(); setupGui();
serverConnection.connect(); serverConnection.connect();
mainVolume = new MainVolume(this);
backgroundMusic = new BackgroundMusic(this);
logic.addListener(backgroundMusic);
} }
/** /**
@@ -266,7 +281,6 @@ private void setupStates() {
stateManager.detach(stateManager.getState(DebugKeysAppState.class)); stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameSound(); attachGameSound();
attachBackgroundSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState()); stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
} }
@@ -280,19 +294,6 @@ private void attachGameSound() {
stateManager.attach(gameSound); stateManager.attach(gameSound);
} }
/**
* Attaches the background music state and sets its initial enabled state.
* The background music state is responsible for managing the background music
* playback in the game. It listens to the game logic for any changes in the
* background music settings.
*/
private void attachBackgroundSound() {
final BackgroundMusic backgroundMusic = new BackgroundMusic();
logic.addListener(backgroundMusic);
backgroundMusic.setEnabled(BackgroundMusic.enabledInPreferences());
stateManager.attach(backgroundMusic);
}
/** /**
* Updates the application state every frame. * Updates the application state every frame.
* This method is called once per frame during the game loop. * This method is called once per frame during the game loop.
@@ -418,12 +419,12 @@ public void stop(boolean waitFor) {
*/ */
void confirmDialog(String question, Runnable yesAction) { void confirmDialog(String question, Runnable yesAction) {
DialogBuilder.simple(dialogManager) DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.question")) .setTitle(lookup("dialog.question"))
.setText(question) .setText(question)
.setOkButton(lookup("button.yes"), yesAction) .setOkButton(lookup("button.yes"), yesAction)
.setNoButton(lookup("button.no")) .setNoButton(lookup("button.no"))
.build() .build()
.open(); .open();
} }
/** /**
@@ -433,10 +434,28 @@ void confirmDialog(String question, Runnable yesAction) {
*/ */
void errorDialog(String errorMessage) { void errorDialog(String errorMessage) {
DialogBuilder.simple(dialogManager) DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.error")) .setTitle(lookup("dialog.error"))
.setText(errorMessage) .setText(errorMessage)
.setOkButton(lookup("button.ok")) .setOkButton(lookup("button.ok"))
.build() .build()
.open(); .open();
}
/**
* this method returns the object which handles the background music
*
* @return BackgroundMusic
*/
public BackgroundMusic getBackgroundMusic(){
return backgroundMusic;
}
/**
* this method returns the object which handles the main volume
*
* @return an object of MainVolume
*/
public MainVolume getMainVolumeControl(){
return mainVolume;
} }
} }

View File

@@ -14,6 +14,7 @@
import com.jme3.asset.AssetNotFoundException; import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData; import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource;
import pp.battleship.notification.GameEventListener; import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.SoundEvent; import pp.battleship.notification.SoundEvent;
@@ -27,14 +28,19 @@
* An application state that plays sounds. * An application state that plays sounds.
*/ */
public class GameSound extends AbstractAppState implements GameEventListener { public class GameSound extends AbstractAppState implements GameEventListener {
private static final Logger LOGGER = System.getLogger(GameSound.class.getName()); static final Logger LOGGER = System.getLogger(GameSound.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameSound.class); private static final Preferences PREFERENCES = getPreferences(GameSound.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS private static final String ENABLED_PREF = "enabled"; //NON-NLS
private static final String SOUND_VOLUME_PREF = "volume";
private float volume;
private AudioNode splashSound; private AudioNode splashSound;
private AudioNode shipDestroyedSound; private AudioNode shipDestroyedSound;
private AudioNode explosionSound; private AudioNode explosionSound;
private AudioNode shellFiredSound; private AudioNode rocketSound;
private BattleshipApp app;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -76,10 +82,13 @@ public void setEnabled(boolean enabled) {
@Override @Override
public void initialize(AppStateManager stateManager, Application app) { public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app); super.initialize(stateManager, app);
this.app = (BattleshipApp) 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 rocketSound = loadSound(app, "Sound/Effects/rocket.wav");
volume = PREFERENCES.getFloat(SOUND_VOLUME_PREF, 1.0f);
} }
/** /**
@@ -95,7 +104,8 @@ 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;
@@ -126,20 +136,64 @@ public void shipDestroyed() {
} }
/** /**
* Plays sound effect when a shell has been fired. * Plays sound effect when a rocket starts
*/ */
public void shellFired() { public void rocket() {
if (isEnabled() && shellFiredSound != null) if (isEnabled() && rocketSound != null)
shellFiredSound.playInstance(); rocketSound.playInstance();
}
/**
* this method sets the sound volume of the sounds
*
* @param volume the volume to be set to
*/
public void setSoundVolume(float volume) {
float mainVolume = app.getMainVolumeControl().getMainVolume();
float calculatedVolume = volume * mainVolume;
shipDestroyedSound.setVolume(calculatedVolume);
splashSound.setVolume(calculatedVolume);
explosionSound.setVolume(calculatedVolume);
this.volume = volume;
PREFERENCES.putFloat(SOUND_VOLUME_PREF, volume);
}
/**
* this method will be used if the main volume changes
*/
public void setSoundVolume() {
float mainVolume = app.getMainVolumeControl().getMainVolume();
shipDestroyedSound.setVolume(volume * mainVolume);
splashSound.setVolume(volume * mainVolume);
explosionSound.setVolume(volume * mainVolume);
rocketSound.setVolume(volume * mainVolume);
PREFERENCES.putFloat(SOUND_VOLUME_PREF, volume);
}
/**
* this method returns the sound
*
* @return
*/
public float getVolume(){
return volume;
} }
@Override @Override
public void receivedEvent(SoundEvent event) { public void receivedEvent(SoundEvent event) {
switch (event.sound()) { switch (event.sound()) {
case EXPLOSION -> explosion(); case EXPLOSION :
case SPLASH -> splash(); explosion();
case DESTROYED_SHIP -> shipDestroyed(); break;
case SHELL_FIRED -> shellFired(); case SPLASH :
splash();
break;
case DESTROYED_SHIP:
shipDestroyed();
break;
case ROCKET_FIRED:
rocket();
break;
} }
} }
} }

View File

@@ -0,0 +1,32 @@
package pp.battleship.client;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
public class MainVolume {
private static final Preferences PREFS = Preferences.userNodeForPackage(MainVolume.class);
private static final String MAIN_VOLUME_PREFS = "MainVolume";
static final Logger LOGGER = System.getLogger(MainVolume.class.getName());
private float mainVolume;
private BattleshipApp app;
public MainVolume(BattleshipApp app) {
this.mainVolume = PREFS.getFloat(MAIN_VOLUME_PREFS, 1.0f);
this.app = app;
}
public void setMainVolume(float mainVolume) {
LOGGER.log(Level.DEBUG, "setMainVolume: mainVolume = {0}", mainVolume);
app.getBackgroundMusic().setVolume();
app.getStateManager().getState(GameSound.class).setSoundVolume();
this.mainVolume = mainVolume;
PREFS.putFloat(MAIN_VOLUME_PREFS, mainVolume);
}
public float getMainVolume() {
return mainVolume;
}
}

View File

@@ -22,8 +22,6 @@
import java.io.IOException; import java.io.IOException;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import java.lang.System.Logger;
import static pp.battleship.Resources.lookup; import static pp.battleship.Resources.lookup;
import static pp.util.PreferencesUtils.getPreferences; import static pp.util.PreferencesUtils.getPreferences;
@@ -33,13 +31,19 @@
* returning to the game, and quitting the application. * returning to the game, and quitting the application.
*/ */
class Menu extends Dialog { class Menu extends Dialog {
private static final Logger LOGGER = System.getLogger(Menu.class.getName());
private static final Preferences PREFERENCES = getPreferences(Menu.class); private static final Preferences PREFERENCES = getPreferences(Menu.class);
private static final String LAST_PATH = "last.file.path"; private static final String LAST_PATH = "last.file.path";
private final BattleshipApp app; private final BattleshipApp app;
private final Button loadButton = new Button(lookup("menu.map.load")); private final Button loadButton = new Button(lookup("menu.map.load"));
private final Button saveButton = new Button(lookup("menu.map.save")); private final Button saveButton = new Button(lookup("menu.map.save"));
private static final double SLIDER_DELTA = 0.1;
private static final double SLIDER_MIN_VALUE = 0.0;
private static final double SLIDER_MAX_VALUE = 2.0;
private final VersionedReference<Double> volumeRef; private final VersionedReference<Double> volumeRef;
private final VersionedReference<Double> soundVolumeRef;
private final VersionedReference<Double> mainVolumeRef;
/** /**
* Constructs the Menu dialog for the Battleship application. * Constructs the Menu dialog for the Battleship application.
@@ -51,15 +55,24 @@ public Menu(BattleshipApp app) {
this.app = app; this.app = app;
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Label(lookup("menu.main.volume"), new ElementId("label")));
Slider mainVolumeSlider = createSlider(app.getMainVolumeControl().getMainVolume());
addChild(mainVolumeSlider);
mainVolumeRef = mainVolumeSlider.getModel().createReference();
addChild(new Label(lookup("menu.sound.volume"), new ElementId("label")));
addChild(new Checkbox(lookup("menu.sound-enabled"), addChild(new Checkbox(lookup("menu.sound-enabled"),
new StateCheckboxModel(app, GameSound.class))); new StateCheckboxModel(app, GameSound.class)));
Slider soundSlider = createSlider(app.getStateManager().getState(GameSound.class).getVolume());
addChild(soundSlider);
soundVolumeRef = soundSlider.getModel().createReference();
addChild(new Checkbox(lookup("menu.music-toggle"), addChild(new Label(lookup("menu.volume"), new ElementId("label")));
new StateCheckboxModel(app, BackgroundMusic.class))); Checkbox musicToggle = new Checkbox(lookup("menu.music.toggle"));
musicToggle.setChecked(app.getBackgroundMusic().isMusicEnabled());
Slider volumeSlider = new Slider(); musicToggle.addClickCommands(s -> toggleMusic());
volumeSlider.setModel(new DefaultRangedValueModel(0.0, 2.0, app.getStateManager().getState(BackgroundMusic.class).getVolume())); addChild(musicToggle);
volumeSlider.setDelta(0.1f); Slider volumeSlider = createSlider(app.getBackgroundMusic().getVolume());
addChild(volumeSlider); addChild(volumeSlider);
volumeRef = volumeSlider.getModel().createReference(); volumeRef = volumeSlider.getModel().createReference();
@@ -74,6 +87,72 @@ public Menu(BattleshipApp app) {
update(); update();
} }
/**
* this method creates a slider to be used in the menu
*
* @param relativePosition the position of the regulator on the slider
* @return the creates slider
*/
private Slider createSlider(double relativePosition){
Slider slider = new Slider();
slider.setModel(new DefaultRangedValueModel(SLIDER_MIN_VALUE, SLIDER_MAX_VALUE, relativePosition));
slider.setDelta(SLIDER_DELTA);
return slider;
}
/**
* this method is used update the volume when there is a change in the slider
* @param tpf time per frame
*/
@Override
public void update(float tpf){
if(volumeRef.update()){
double newVolume = volumeRef.get();
adjustMusicVolume(newVolume);
}
else if (soundVolumeRef.update()) {
double newSoundVolume = soundVolumeRef.get();
adjustSoundVolume(newSoundVolume);
} else if (mainVolumeRef.update()) {
double newMainVolume = mainVolumeRef.get();
adjustMainVolume(newMainVolume);
}
}
/**
* this method adjusts the main volume
*
* @param newVolume the volume to be set as main volume
*/
private void adjustMainVolume(double newVolume) {
app.getMainVolumeControl().setMainVolume((float) newVolume);
}
/**
* this method adjust the volume for the background music
*
* @param volume is the double value of the volume
*/
private void adjustMusicVolume(double volume) {
app.getBackgroundMusic().setVolume((float) volume);
}
/**
* this method adjusts the volume for the sound
*
* @param volume is a double value of the sound volume
*/
private void adjustSoundVolume(double volume) {
app.getStateManager().getState(GameSound.class).setSoundVolume((float) volume);
}
/**
* this method toggles the background music on and off
*/
private void toggleMusic() {
app.getBackgroundMusic().toggleMusic();
}
/** /**
* Updates the state of the load and save buttons based on the game logic. * Updates the state of the load and save buttons based on the game logic.
*/ */
@@ -83,28 +162,6 @@ public void update() {
saveButton.setEnabled(app.getGameLogic().maySaveMap()); saveButton.setEnabled(app.getGameLogic().maySaveMap());
} }
/**
* Updates the menu state based on the time per frame (tpf).
* If the volume reference has been updated, adjusts the volume accordingly.
*
* @param tpf the time per frame
*/
@Override
public void update(float tpf) {
if (volumeRef.update()) {
adjustVolume(volumeRef.get());
}
}
/**
* Adjusts the volume of the background music.
*
* @param volume the new volume level to set, as a double
*/
private void adjustVolume(double volume) {
app.getStateManager().getState(BackgroundMusic.class).setVolume((float) volume);
}
/** /**
* As an escape action, this method closes the menu if it is the top dialog. * As an escape action, this method closes the menu if it is the top dialog.
*/ */
@@ -139,7 +196,8 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
PREFERENCES.put(LAST_PATH, path); PREFERENCES.put(LAST_PATH, path);
fileAction.run(new File(path)); fileAction.run(new File(path));
dialog.close(); dialog.close();
} catch (IOException e) { }
catch (IOException e) {
app.errorDialog(e.getLocalizedMessage()); app.errorDialog(e.getLocalizedMessage());
} }
} }
@@ -153,13 +211,13 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
private void fileDialog(FileAction fileAction, String label) { private void fileDialog(FileAction fileAction, String label) {
final TextInputDialog dialog = final TextInputDialog dialog =
TextInputDialog.builder(app.getDialogManager()) TextInputDialog.builder(app.getDialogManager())
.setLabel(lookup("label.file")) .setLabel(lookup("label.file"))
.setFocus(TextInputDialog::getInput) .setFocus(TextInputDialog::getInput)
.setTitle(label) .setTitle(label)
.setOkButton(lookup("button.ok"), d -> handle(fileAction, d)) .setOkButton(lookup("button.ok"), d -> handle(fileAction, d))
.setNoButton(lookup("button.cancel")) .setNoButton(lookup("button.cancel"))
.setOkClose(false) .setOkClose(false)
.build(); .build();
final String path = PREFERENCES.get(LAST_PATH, null); final String path = PREFERENCES.get(LAST_PATH, null);
if (path != null) if (path != null)
dialog.getInput().setText(path.trim()); dialog.getInput().setText(path.trim());

View File

@@ -12,6 +12,7 @@
import com.simsilica.lemur.Label; import com.simsilica.lemur.Label;
import com.simsilica.lemur.TextField; import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.SpringGridLayout; import com.simsilica.lemur.component.SpringGridLayout;
import pp.battleship.client.server.BattleshipServer;
import pp.dialog.Dialog; import pp.dialog.Dialog;
import pp.dialog.DialogBuilder; import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog; import pp.dialog.SimpleDialog;
@@ -31,7 +32,6 @@ class NetworkDialog extends SimpleDialog {
private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName()); private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName());
private static final String LOCALHOST = "localhost"; //NON-NLS private static final String LOCALHOST = "localhost"; //NON-NLS
private static final String DEFAULT_PORT = "1234"; //NON-NLS private static final String DEFAULT_PORT = "1234"; //NON-NLS
private static final int START_SERVER_DELAY = 2000;
private final NetworkSupport network; private final NetworkSupport network;
private final TextField host = new TextField(LOCALHOST); private final TextField host = new TextField(LOCALHOST);
private final TextField port = new TextField(DEFAULT_PORT); private final TextField port = new TextField(DEFAULT_PORT);
@@ -53,9 +53,9 @@ class NetworkDialog extends SimpleDialog {
host.setPreferredWidth(400f); host.setPreferredWidth(400f);
port.setSingleLine(true); port.setSingleLine(true);
Checkbox hostCheckbox = new Checkbox(lookup("host.own-server")); Checkbox serverHost = new Checkbox(lookup("host.own.server"));
hostCheckbox.setChecked(false); serverHost.setChecked(false);
hostCheckbox.addClickCommands(s -> hostServer = !hostServer); serverHost.addClickCommands(s -> toggleServerHost());
final BattleshipApp app = network.getApp(); final BattleshipApp app = network.getApp();
final Container input = new Container(new SpringGridLayout()); final Container input = new Container(new SpringGridLayout());
@@ -63,77 +63,77 @@ class NetworkDialog extends SimpleDialog {
input.addChild(host, 1); input.addChild(host, 1);
input.addChild(new Label(lookup("port.number") + ": ")); input.addChild(new Label(lookup("port.number") + ": "));
input.addChild(port, 1); input.addChild(port, 1);
input.addChild(hostCheckbox); input.addChild(serverHost);
DialogBuilder.simple(app.getDialogManager()) DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog")) .setTitle(lookup("server.dialog"))
.setExtension(d -> d.addChild(input)) .setExtension(d -> d.addChild(input))
.setOkButton(lookup("button.connect"), d -> connectHostServer()) .setOkButton(lookup("button.connect"), d -> connect())
.setNoButton(lookup("button.cancel"), app::closeApp) .setNoButton(lookup("button.cancel"), app::closeApp)
.setOkClose(false) .setOkClose(false)
.setNoClose(false) .setNoClose(false)
.build(this); .build(this);
} }
/** /**
* Handles the action for the connect button in the connection dialog. * Handles the action for the connect button in the connection dialog.
* Tries to parse the port number and initiate connection to the server. * Tries to parse the port number and initiate connection to the server.
*/ */
private void connect() { private void connectServer() {
LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS
try { try {
hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText(); hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText();
portNumber = Integer.parseInt(port.getText()); portNumber = Integer.parseInt(port.getText());
openProgressDialog(); openProgressDialog();
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork); connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
} catch (NumberFormatException e) { }
catch (NumberFormatException e) {
network.getApp().errorDialog(lookup("port.must.be.integer")); network.getApp().errorDialog(lookup("port.must.be.integer"));
} }
} }
/** /**
* Connects to the host server. If the `hostServer` flag is set, it starts the server * This method will start a server or just connect to one based on the boolean hostServer
* before attempting to connect. If the server fails to start, logs an error.
*/ */
private void connectHostServer() { private void connect() {
if (hostServer) { if(hostServer){
startServer(); startServer();
try { try {
Thread.sleep(START_SERVER_DELAY); Thread.sleep(1000);
} catch (Exception e) { } catch (InterruptedException e) {
LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS LOGGER.log(Level.WARNING, e.getMessage(), e);
} }
connect(); connectServer();
} else { } else {
connect(); connectServer();
} }
} }
/** /**
* Starts the game server in a new thread. * This method starts a server in a new thread
* Logs an error if the server fails to start.
*/ */
private void startServer() { private void startServer() {
LOGGER.log(Level.INFO, "start server"); //NON-NLS
new Thread(() -> { new Thread(() -> {
try { try{
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS BattleshipServer battleshipServer = new BattleshipServer(Integer.parseInt(port.getText()));
BattleshipServer server = new BattleshipServer(Integer.parseInt(port.getText())); battleshipServer.run();
LOGGER.log(Level.INFO, "Server started"); //NON-NLS
server.run();
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS LOGGER.log(Level.ERROR, e.getMessage(), e);
} }
}).start(); }).start();
} }
private void toggleServerHost(){
hostServer = !hostServer;
}
/** /**
* Creates a dialog indicating that the connection is in progress. * Creates a dialog indicating that the connection is in progress.
*/ */
private void openProgressDialog() { private void openProgressDialog() {
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager()) progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
.setText(lookup("label.connecting")) .setText(lookup("label.connecting"))
.build(); .build();
progressDialog.open(); progressDialog.open();
} }
@@ -146,7 +146,8 @@ private Object initNetwork() {
try { try {
network.initNetwork(hostname, portNumber); network.initNetwork(hostname, portNumber);
return null; return null;
} catch (Exception e) { }
catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@@ -161,9 +162,11 @@ public void update(float delta) {
try { try {
connectionFuture.get(); connectionFuture.get();
success(); success();
} catch (ExecutionException e) { }
catch (ExecutionException e) {
failure(e.getCause()); failure(e.getCause());
} catch (InterruptedException e) { }
catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }

View File

@@ -0,0 +1,179 @@
package pp.battleship.client.gui;
import com.jme3.app.Application;
import com.jme3.asset.AssetManager;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.effect.ParticleMesh.Type;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Shot;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.Timer;
import java.util.TimerTask;
/**
* This class is used to handle the effects for impacts
*/
public class EffectHandler {
private final AssetManager assetManager;
static final Logger LOGGER = System.getLogger(EffectHandler.class.getName());
private Material particleMat;
/**
* the constructor is used to get the asset manager from the app
*
* @param app the main application
*/
public EffectHandler(Application app) {
assetManager = app.getAssetManager();
particleMat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
}
/**
* creates a new HitEffect
*
* @param battleshipNode the node of the ship
* @param shot the shot which triggered the effect
*/
public void createHitEffect(Node battleshipNode, Shot shot) {
createFieryEffect(battleshipNode,shot, "HitEffect", 30, 0.45f, 0.1f, -0.5f, 1f , 2f, false);
}
/**
* creates a new FireEffect
*
* @param battleshipNode the node of the ship
* @param shot the shot which triggered the effect
*/
public void createFireEffect(Node battleshipNode, Shot shot) {
createFieryEffect(battleshipNode, shot, "FireEffect", 30, 0.1f, 0.05f, -0.9f, 1f , 2f, true);
}
/**
* creates a fiery type hit effect
*
* @param battleshipNode the ship to which the effect should be attached
* @param shot the shot that triggered the effect
* @param name the name of the particle emitter
* @param numOfParticle the overall numberOfParticles
* @param startSize the start size of the particles
* @param endSize the end size of the particles
* @param gravity the gravity of the particles
* @param lowLife the lowest lifetime of a particle
* @param highLife the maximum lifetime of a particle
* @param loop if the effect should be looped
*/
public void createFieryEffect(Node battleshipNode, Shot shot, String name, int numOfParticle, float startSize, float endSize, float gravity,
float lowLife, float highLife, boolean loop) {
ParticleEmitter fieryEffect = new ParticleEmitter(name, Type.Triangle, numOfParticle);
fieryEffect.setMaterial(particleMat);
fieryEffect.setImagesX(2);
fieryEffect.setImagesY(2);
fieryEffect.setStartColor(ColorRGBA.Orange);
fieryEffect.setEndColor(ColorRGBA.Red);
fieryEffect.getParticleInfluencer().setInitialVelocity(new Vector3f(0,1,0));
fieryEffect.setStartSize(startSize);
fieryEffect.setEndSize(endSize);
fieryEffect.setGravity(0, gravity, 0);
fieryEffect.setLowLife(lowLife);
fieryEffect.setHighLife(highLife);
if(!loop) {
fieryEffect.setLocalTranslation(shot.getY() + 0.5f, 0 , shot.getX() + 0.5f);
fieryEffect.setParticlesPerSec(0);
fieryEffect.emitAllParticles();
} else {
fieryEffect.setLocalTranslation(shot.getY() + 0.5f, 0 , shot.getX() + 0.5f);
fieryEffect.getLocalTranslation().subtractLocal(battleshipNode.getLocalTranslation());
fieryEffect.setParticlesPerSec(10);
}
battleshipNode.attachChild(fieryEffect);
LOGGER.log(Level.DEBUG, "Created {0} at {1}", name ,fieryEffect.getLocalTranslation().toString());
fieryEffect.addControl(new EffectControl(fieryEffect, battleshipNode));
}
/**
* This method is used to create a miss effect at a certain location
*/
public ParticleEmitter createMissEffect(Shot shot) {
ParticleEmitter missEffect = new ParticleEmitter("MissEffect", Type.Triangle, 15);
missEffect.setMaterial(particleMat);
missEffect.setImagesX(2);
missEffect.setImagesY(2);
missEffect.setStartColor(ColorRGBA.Blue); // Water color
missEffect.setEndColor(ColorRGBA.Cyan);
missEffect.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1, 0));
missEffect.setStartSize(0.3f);
missEffect.setEndSize(0.05f);
missEffect.setGravity(0, -0.1f, 0);
missEffect.setLowLife(0.5f);
missEffect.setHighLife(1.5f);
missEffect.setParticlesPerSec(0);
missEffect.setLocalTranslation(shot.getY() + 0.5f, 0 , shot.getX() + 0.5f);
missEffect.emitAllParticles();
missEffect.addControl(new EffectControl(missEffect));
return missEffect;
}
/**
* This inner class is used to control the effects
*/
private static class EffectControl extends AbstractControl {
private final ParticleEmitter emitter;
private final Node parentNode;
/**
* this constructor is used to when the effect should be attached to a specific node
*
* @param emitter the Particle emitter to be controlled
* @param parentNode the node to be attached
*/
public EffectControl(ParticleEmitter emitter, Node parentNode) {
this.emitter = emitter;
this.parentNode = parentNode;
}
/**
* This constructor is used when the effect shouldn't be attached to
* a specific node
*
* @param emitter the Particle emitter to be controlled
*/
public EffectControl(ParticleEmitter emitter){
this.emitter = emitter;
this.parentNode = null;
}
/**
* The method which checks if the Effect is not rendered anymore so it can be removed
*
* @param tpf time per frame (in seconds)
*/
@Override
protected void controlUpdate(float tpf) {
if (emitter.getParticlesPerSec() == 0 && emitter.getNumVisibleParticles() == 0) {
if (parentNode != null)
parentNode.detachChild(emitter);
}
}
/**
* @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort being rendered (not null)
*/
@Override
protected void controlRender(com.jme3.renderer.RenderManager rm, com.jme3.renderer.ViewPort vp) {}
}
}

View File

@@ -143,10 +143,6 @@ 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,19 +7,16 @@
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.IntPoint;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
import pp.util.Position; import pp.util.Position;
import java.lang.System.Logger;
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.
@@ -32,6 +29,10 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
private static final float SHOT_DEPTH = -2f; private static final float SHOT_DEPTH = -2f;
private static final float SHIP_DEPTH = 0f; private static final float SHIP_DEPTH = 0f;
private static final float INDENT = 4f; private static final float INDENT = 4f;
private static final float SHELL_DEPTH = 8f;
private static final float SHELL_SIZE = 0.75f;
private static final float SHELL_CENTERED_IN_MAP_GRID = 0.0625f;
// Colors used for different visual elements // Colors used for different visual elements
private static final ColorRGBA HIT_COLOR = ColorRGBA.Red; private static final ColorRGBA HIT_COLOR = ColorRGBA.Red;
@@ -43,6 +44,8 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
// The MapView associated with this synchronizer // The MapView associated with this synchronizer
private final MapView view; private final MapView view;
static final Logger LOGGER = System.getLogger(MapViewSynchronizer.class.getName());
/** /**
* Constructs a new MapViewSynchronizer for the given MapView. * Constructs a new MapViewSynchronizer for the given MapView.
* Initializes the synchronizer and adds existing elements from the model to the view. * Initializes the synchronizer and adds existing elements from the model to the view.
@@ -64,6 +67,7 @@ public MapViewSynchronizer(MapView view) {
*/ */
@Override @Override
public Spatial visit(Shot shot) { public Spatial visit(Shot shot) {
LOGGER.log(Logger.Level.DEBUG, "Visiting " + shot);
// Convert the shot's model coordinates to view coordinates // Convert the shot's model coordinates to view coordinates
final Position p1 = view.modelToView(shot.getX(), shot.getY()); final Position p1 = view.modelToView(shot.getX(), shot.getY());
final Position p2 = view.modelToView(shot.getX() + 1, shot.getY() + 1); final Position p2 = view.modelToView(shot.getX() + 1, shot.getY() + 1);
@@ -71,9 +75,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);
} }
/** /**
@@ -116,22 +120,24 @@ public Spatial visit(Battleship ship) {
} }
/** /**
* Creates a visual representation of a shell on the map. * this method will create a representation of a shell in the map
* The shell is represented as a black ellipse.
* *
* @param shell the Shell object representing the shell in the model * @param shell the Shell element to visit
* @return a Spatial representing the shell on the map * @return the node the representation is attached to
*/ */
@Override @Override
public Spatial visit(Shell shell) { public Spatial visit(Shell shell) {
Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.getFieldSize() / 2 * 0.8f)); LOGGER.log(Logger.Level.DEBUG, "Visiting {0}", shell);
Material mat = new Material(view.getApp().getAssetManager(), UNSHADED); //NON-NLS final Node shellNode = new Node("shell");
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); final Position p1 = view.modelToView(shell.getX(), shell.getY());
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); final Position p2 = view.modelToView(shell.getX() + SHELL_SIZE, shell.getY() + SHELL_SIZE);
mat.setColor("Color", ColorRGBA.Black);
ellipse.setMaterial(mat); final Position startPosition = view.modelToView(SHELL_CENTERED_IN_MAP_GRID, SHELL_CENTERED_IN_MAP_GRID);
ellipse.addControl(new ShellMapControl(view, shell));
return ellipse; shellNode.attachChild(view.getApp().getDraw().makeRectangle(startPosition.getX(), startPosition.getY(), SHELL_DEPTH, p2.getX() - p1.getX(), p2.getY() - p1.getY(), ColorRGBA.Black));
shellNode.setLocalTranslation(startPosition.getX(), startPosition.getY(), SHELL_DEPTH);
shellNode.addControl(new ShellMapControl(p1, view.getApp(), new IntPoint(shell.getX(), shell.getY())));
return shellNode;
} }
/** /**
@@ -145,6 +151,7 @@ public Spatial visit(Shell shell) {
* @return a Geometry representing the line * @return a Geometry representing the line
*/ */
private Geometry shipLine(float x1, float y1, float x2, float y2, ColorRGBA color) { private Geometry shipLine(float x1, float y1, float x2, float y2, ColorRGBA color) {
LOGGER.log(Logger.Level.DEBUG, "created ship line");
return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH); return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH);
} }
} }

View File

@@ -7,18 +7,12 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode; import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.renderer.queue.RenderQueue.ShadowMode;
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.Box;
import pp.battleship.client.BattleshipApp; import pp.battleship.client.BattleshipApp;
import pp.battleship.model.*; import pp.battleship.model.*;
@@ -34,18 +28,15 @@
*/ */
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";
private static final String DESTROYER_MODEL = "Models/Destroyer/Destroyer.j3o"; //NON-NLS private static final String UBOAT = "Models/UBoat/14084_WWII_Ship_German_Type_II_U-boat_v2_L1.obj"; //NON-NLS
private static final String DESTROYER_TEXTURE = "Models/Destroyer/BattleshipC.jpg"; //NON-NLS private static final String BATTLE_SHIP_MODERN = "Models/BattleShipModern/Destroyer.j3o";
private static final String TYPE_II_UBOAT_MODEL = "Models/TypeIIUboat/TypeIIUboat.j3o"; //NON-NLS private static final String BATTLE_SHIP_MODERN_TEXTURE = "Models/BattleShipModern/BattleshipC.jpg";
private static final String TYPE_II_UBOAT_TEXTURE = "Models/TypeIIUboat/Type_II_U-boat_diff.jpg"; //NON-NLS private static final String PATROL_BOAT = "Models/PatrolBoat/12219_boat_v2_L2.obj";
private static final String ATLANTICA_MODEL = "Models/Atlantica/Atlantica.j3o"; //NON-NLS private static final String SHELL_ROCKET = "Models/Rocket/Rocket.obj";
private static final String ROCKET = "Models/Rocket/Rocket.j3o"; //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 String SHELL = "shell";
private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray; private final EffectHandler effectHandler;
private final ShipMap map; private final ShipMap map;
private final BattleshipApp app; private final BattleshipApp app;
@@ -61,6 +52,7 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
super(app.getGameLogic().getOwnMap(), root); super(app.getGameLogic().getOwnMap(), root);
this.app = app; this.app = app;
this.map = map; this.map = map;
effectHandler = new EffectHandler(app);
addExisting(); addExisting();
} }
@@ -74,7 +66,7 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
*/ */
@Override @Override
public Spatial visit(Shot shot) { public Spatial visit(Shot shot) {
return shot.isHit() ? handleHit(shot) : handleMiss(shot); return shot.isHit() ? handleHit(shot) : effectHandler.createMissEffect(shot);
} }
/** /**
@@ -89,122 +81,12 @@ private Spatial handleHit(Shot shot) {
final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship"); final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship");
final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node"); final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node");
final ParticleEmitter debris = createDebrisEffect(shot); effectHandler.createHitEffect(shipNode, shot);
shipNode.attachChild(debris); effectHandler.createFireEffect(shipNode, shot);
final ParticleEmitter fire = createFireEffect(shot, shipNode);
shipNode.attachChild(fire);
return null; return null;
} }
private Spatial handleMiss(Shot shot) {
return createMissEffect(shot);
}
private ParticleEmitter createMissEffect(Shot shot) {
final ParticleEmitter water = new ParticleEmitter("WaterEmitter", ParticleMesh.Type.Triangle, 20);
Material waterMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
waterMaterial.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flame.png"));
water.setMaterial(waterMaterial);
water.setImagesX(2);
water.setImagesY(2);
water.setStartColor(ColorRGBA.Cyan);
water.setEndColor(ColorRGBA.Blue);
water.getParticleInfluencer().setInitialVelocity(new Vector3f(0.1f, 0.1f, 0.1f));
water.setStartSize(0.4f);
water.setEndSize(0.45f);
water.setGravity(0, -0.5f, 0);
water.setLowLife(1f);
water.setHighLife(1f);
water.setParticlesPerSec(0);
water.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
water.emitAllParticles();
return water;
}
private ParticleEmitter createDebrisEffect(Shot shot) {
final ParticleEmitter debris = new ParticleEmitter("DebrisEmitter", ParticleMesh.Type.Triangle, 2);
Material debrisMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
debrisMaterial.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/Debris.png"));
debris.setMaterial(debrisMaterial);
debris.setImagesX(2);
debris.setImagesY(2);
debris.setStartColor(ColorRGBA.White);
debris.setEndColor(ColorRGBA.White);
debris.getParticleInfluencer().setInitialVelocity(new Vector3f(0.1f, 2f, 0.1f));
debris.setStartSize(0.1f);
debris.setEndSize(0.5f);
debris.setGravity(0, 3f, 0);
debris.getParticleInfluencer().setVelocityVariation(.40f);
debris.setLowLife(1f);
debris.setHighLife(1.5f);
debris.setParticlesPerSec(0);
debris.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
debris.emitAllParticles();
return debris;
}
private ParticleEmitter createFireEffect(Shot shot, Node shipNode) {
ParticleEmitter fire = new ParticleEmitter("FireEmitter", ParticleMesh.Type.Triangle, 100);
Material fireMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
fireMaterial.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flame.png"));
fire.setMaterial(fireMaterial);
fire.setImagesX(2);
fire.setImagesY(2);
fire.setStartColor(ColorRGBA.Orange);
fire.setEndColor(ColorRGBA.Red);
fire.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1.5f, 0));
fire.setStartSize(0.2f);
fire.setEndSize(0.05f);
fire.setLowLife(1f);
fire.setHighLife(2f);
fire.getParticleInfluencer().setVelocityVariation(0.2f);
fire.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
fire.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation());
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
@@ -225,6 +107,42 @@ public Spatial visit(Battleship ship) {
return node; return node;
} }
/**
* Visits a shell and creates a graphical representation
*
* @param shell the Shell element to visit
* @return the node containing the graphical representation
*/
@Override
public Spatial visit(Shell shell){
final Node node = new Node(SHELL);
node.attachChild(createRocket());
final float x = shell.getY();
final float z = shell.getX();
node.setLocalTranslation(x + 0.5f, 10f, z + 0.5f);
ShellControl shellControl = new ShellControl(shell, app);
node.addControl(shellControl);
return node;
}
/**
* creates the spatial representation of a rocket
*
* @return a spatial the rocket
*/
private Spatial createRocket() {
final Spatial model = app.getAssetManager().loadModel(SHELL_ROCKET);
model.rotate(PI, 0f, 0f);
model.scale(0.002f);
model.setShadowMode(ShadowMode.CastAndReceive);
model.move(0, 0, 0);
return model;
}
/** /**
* Creates the appropriate graphical representation of the specified battleship. * Creates the appropriate graphical representation of the specified battleship.
* The representation is either a detailed model or a simple box based on the length of the ship. * The representation is either a detailed model or a simple box based on the length of the ship.
@@ -234,48 +152,14 @@ public Spatial visit(Battleship ship) {
*/ */
private Spatial createShip(Battleship ship) { private Spatial createShip(Battleship ship) {
return switch (ship.getLength()) { return switch (ship.getLength()) {
case 1 -> createVessel(ship); case 1 -> createPatrolBoat(ship);
case 2 -> createSubmarine(ship); case 2 -> createModernBattleship(ship);
case 3 -> createDestroyer(ship); case 3 -> createUBoat(ship);
case 4 -> createBattleship(ship); case 4 -> createBattleship(ship);
default -> createBox(ship); default -> throw new IllegalArgumentException("Ship length must be between 1 and 4 units long");
}; };
} }
/**
* Creates a simple box to represent a battleship that is not of the "King George V" type.
*
* @param ship the battleship to be represented
* @return the geometry representing the battleship as a box
*/
private Spatial createBox(Battleship ship) {
final Box box = new Box(0.5f * (ship.getMaxY() - ship.getMinY()) + 0.3f,
0.3f,
0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
final Geometry geometry = new Geometry(SHIP, box);
geometry.setMaterial(createColoredMaterial());
geometry.setShadowMode(ShadowMode.CastAndReceive);
return geometry;
}
/**
* Creates a new {@link Material} with the specified color.
* If the color includes transparency (i.e., alpha value less than 1),
* the material's render state is set to use alpha blending, allowing for
* semi-transparent rendering.
*
* @return a {@link Material} instance configured with the specified color and,
* if necessary, alpha blending enabled.
*/
private Material createColoredMaterial() {
final Material material = new Material(app.getAssetManager(), UNSHADED);
if (SeaSynchronizer.BOX_COLOR.getAlpha() < 1f)
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
material.setColor(COLOR, SeaSynchronizer.BOX_COLOR);
return material;
}
/** /**
* Creates a detailed 3D model to represent a "King George V" battleship. * Creates a detailed 3D model to represent a "King George V" battleship.
* *
@@ -293,62 +177,58 @@ private Spatial createBattleship(Battleship ship) {
} }
/** /**
* Creates a detailed 3D model to represent a destroyer battleship. * creates a detailed 3D model to represent an UBoat
* *
* @param ship the battleship to be represented * @param ship the ship to be represented
* @return the spatial representing the destroyer battleship * @return the spatial representing the Uboat
*/ */
private Spatial createDestroyer(Battleship ship) { private Spatial createUBoat(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(DESTROYER_MODEL); final Spatial model = app.getAssetManager().loadModel(UBOAT);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.5f);
model.setShadowMode(ShadowMode.CastAndReceive);
model.move(0, -0.3f, 0);
return model;
}
/**
* creates a detailed 3D model to represent the modern battleship
*
* @param ship the ship to be represented
* @return the spatial representing the Modern Battleship
*/
private Spatial createModernBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(BATTLE_SHIP_MODERN);
Material mat = new Material(app.getAssetManager(), UNSHADED); Material mat = new Material(app.getAssetManager(), UNSHADED);
mat.setTexture("ColorMap", app.getAssetManager().loadTexture(DESTROYER_TEXTURE)); mat.setTexture("ColorMap", app.getAssetManager().loadTexture(BATTLE_SHIP_MODERN_TEXTURE));
mat.getAdditionalRenderState().setBlendMode(BlendMode.Off); mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
model.setMaterial(mat); model.setMaterial(mat);
model.setQueueBucket(RenderQueue.Bucket.Opaque); model.setQueueBucket(RenderQueue.Bucket.Opaque);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.1f); model.scale(0.08f);
model.setLocalTranslation(0f, 0.25f, 0f); model.setLocalTranslation(0f, 0.2f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;
} }
/** /**
* Creates a detailed 3D model to represent a Type II U-boat submarine. * creates a detailed 3D model to represent the patrol boat
* *
* @param ship the battleship to be represented * @param ship the ship to be represented
* @return the spatial representing the Type II U-boat submarine * @return the spatial representing the patrol boat
*/ */
private Spatial createSubmarine(Battleship ship) { private Spatial createPatrolBoat(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(TYPE_II_UBOAT_MODEL); final Spatial model = app.getAssetManager().loadModel(PATROL_BOAT);
Material mat = new Material(app.getAssetManager(), UNSHADED);
mat.setTexture("ColorMap", app.getAssetManager().loadTexture(TYPE_II_UBOAT_TEXTURE));
model.setMaterial(mat);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.25f); model.scale(0.0005f);
model.getLocalTranslation().addLocal(0f, -0.15f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* Creates a detailed 3D model to represent a vessel.
*
* @param ship the battleship to be represented
* @return the spatial representing the vessel
*/
private Spatial createVessel(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(ATLANTICA_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.0003f);
model.getLocalTranslation().addLocal(0f, -0.05f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;

View File

@@ -1,42 +1,65 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.math.Quaternion;
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort; import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import pp.battleship.client.BattleshipApp;
import pp.battleship.message.client.AnimationEndMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell;
import java.lang.System.Logger;
/** /**
* Controls the movement and rotation of a shell in the game. * This class controls a 3D representation of a shell
* 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 { public class ShellControl extends AbstractControl {
private final static float SHELL_SPEED = 7.5f; private final Shell shell;
private final static float SHELL_ROTATION_SPEED = 0.5f; private final BattleshipApp app;
private final static float MIN_HEIGHT = 0.7f;
private static final float MOVE_SPEED = 8.0f;
static final Logger LOGGER = System.getLogger(ShellControl.class.getName());
/** /**
* Updates the shell's position and rotation. * Constructor to create a new ShellControl object
* 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 * @param shell the shell to be displayed
* @param app the main application
*/
public ShellControl(Shell shell, BattleshipApp app) {
this.shell = shell;
this.app = app;
}
/**
* this method moves the representation towards it destination
* and deletes it if it reaches its target
*
* @param tpf time per frame (in seconds)
*/ */
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
spatial.move(0, -SHELL_SPEED * tpf, 0); spatial.move(0, -MOVE_SPEED * tpf, 0);
spatial.rotate(0, SHELL_ROTATION_SPEED, 0); spatial.rotate(0f, 0.05f, 0f);
if (spatial.getLocalTranslation().getY() <= MIN_HEIGHT) { //LOGGER.log(System.Logger.Level.DEBUG, "moved rocket {0}", spatial.getLocalTranslation().getY());
if (spatial.getLocalTranslation().getY() <= 1.5){
spatial.getParent().detachChild(spatial); spatial.getParent().detachChild(spatial);
app.getGameLogic().send(new AnimationEndMessage(new IntPoint(shell.getX(), shell.getY())));
} }
} }
/** /**
* Renders the shell. This method is currently not used. * This method is called during the rendering phase, but it does not perform any
* operations in this implementation as the control only influences the spatial's
* transformation, not its rendering process.
* *
* @param rm the RenderManager * @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort * @param vp the ViewPort being rendered (not null)
*/ */
@Override @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// nothing to do here
} }
} }

View File

@@ -4,79 +4,60 @@
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort; import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Shell; import pp.battleship.client.BattleshipApp;
import pp.battleship.message.client.AnimationEndMessage;
import pp.battleship.model.IntPoint;
import pp.util.Position; 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 controls a ShellMap element
* 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 { public class ShellMapControl extends AbstractControl {
private static final Logger LOGGER = System.getLogger(ShellMapControl.class.getName()); private final Position position;
private final IntPoint pos;
private static final Vector3f VECTOR = new Vector3f();
private final BattleshipApp app;
/** /**
* The duration of the shell animation in seconds. * constructs a new ShellMapControl object
*/
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 position the position where the shell should move to on the map
* @param shell the shell to be controlled * @param app the main application
* @param pos the position the then to render shot goes to
*/ */
public ShellMapControl(MapView view, Shell shell) { public ShellMapControl(Position position, BattleshipApp app, IntPoint pos) {
Vector3f endPos = new Vector3f(shell.getX(), 0, shell.getY()); super();
this.endPos = view.modelToView(endPos.x, endPos.z); this.position = position;
LOGGER.log(Level.DEBUG, "ShellMapControl created with endPos: " + this.endPos); this.pos = pos;
this.app = app;
VECTOR.set(new Vector3f(position.getX(), position.getY(), 0));
} }
/** /**
* Updates the position of the shell in the view with linear interpolation. * this method moves the shell representation to its correct spot and removes it after
* This method is called during the update phase. * it arrived at its destination
* *
* @param tpf the time per frame * @param tpf time per frame (in seconds)
*/ */
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
// adjust speed by changing the multiplier spatial.move(VECTOR.mult(tpf));
progress += tpf * ANIMATION_DURATION; if (spatial.getLocalTranslation().getX() >= position.getX() && spatial.getLocalTranslation().getY() >= position.getY()) {
spatial.getParent().detachChild(spatial);
// progress is between 0 and 1 app.getGameLogic().send(new AnimationEndMessage(pos));
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. * This method is called during the rendering phase, but it does not perform any
* Currently, it does nothing. * operations in this implementation as the control only influences the spatial's
* transformation, not its rendering process.
* *
* @param rm the RenderManager * @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort * @param vp the ViewPort being rendered (not null)
*/ */
@Override @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// nothing to do here
} }
} }

View File

@@ -14,6 +14,9 @@
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.util.FloatMath.DEG_TO_RAD; import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.TWO_PI; import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.sin; import static pp.util.FloatMath.sin;
@@ -43,14 +46,27 @@ class ShipControl extends AbstractControl {
*/ */
private final Quaternion pitch = new Quaternion(); private final Quaternion pitch = new Quaternion();
/**
* the speed at which ships sink
*/
private static final float SINKING_SPEED = -0.05f;
/**
* the threshold when ships should be removed from the scene if they sink below the value
*/
private static final float SHIP_SINKING_REMOVE_THRESHOLD = -0.6f;
/** /**
* The current time within the oscillation cycle, used to calculate the ship's pitch angle. * The current time within the oscillation cycle, used to calculate the ship's pitch angle.
*/ */
private float time; private float time;
private final Battleship ship; /**
* The ship to be controlled
*/
private final Battleship battleship;
private static final float SINKING_HEIGHT = -0.6f; static final Logger LOGGER = System.getLogger(ShipControl.class.getName());
/** /**
* Constructs a new ShipControl instance for the specified Battleship. * Constructs a new ShipControl instance for the specified Battleship.
@@ -60,6 +76,8 @@ class ShipControl extends AbstractControl {
* @param ship the Battleship object to control * @param ship the Battleship object to control
*/ */
public ShipControl(Battleship ship) { public ShipControl(Battleship ship) {
battleship = ship;
// Determine the axis of rotation based on the ship's orientation // Determine the axis of rotation based on the ship's orientation
axis = switch (ship.getRot()) { axis = switch (ship.getRot()) {
case LEFT, RIGHT -> Vector3f.UNIT_X; case LEFT, RIGHT -> Vector3f.UNIT_X;
@@ -67,15 +85,14 @@ public ShipControl(Battleship ship) {
}; };
// Set the cycle duration and amplitude based on the ship's length // Set the cycle duration and amplitude based on the ship's length
cycle = ship.getLength() * 2f; cycle = battleship.getLength() * 2f;
amplitude = 5f * DEG_TO_RAD / ship.getLength(); amplitude = 5f * DEG_TO_RAD / ship.getLength();
this.ship = ship;
} }
/** /**
* Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted * Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted
* to create a continuous tilting motion, simulating the effect of waves. * to create a continuous tilting motion, simulating the effect of waves.
* And lets the ship sink if it is destroyed and removes it from the scene when it has completely sunk
* *
* @param tpf time per frame (in seconds), used to calculate the new pitch angle * @param tpf time per frame (in seconds), used to calculate the new pitch angle
*/ */
@@ -84,26 +101,25 @@ protected void controlUpdate(float tpf) {
// If spatial is null, do nothing // If spatial is null, do nothing
if (spatial == null) return; if (spatial == null) return;
// Handle ship sinking by moving it downwards if (battleship.isDestroyed() && spatial.getLocalTranslation().getY() <= SHIP_SINKING_REMOVE_THRESHOLD) {
if (ship.isDestroyed()) { LOGGER.log(Level.INFO, "Ship removed {0}", spatial.getName());
if (spatial.getLocalTranslation().getY() < SINKING_HEIGHT) { spatial.getParent().detachChild(spatial);
spatial.getParent().detachChild(spatial); } else if (battleship.isDestroyed()) {
} else { spatial.move(0, SINKING_SPEED * tpf, 0);
spatial.move(0, -tpf * 0.1f, 0); } else {
} // Update the time within the oscillation cycle
time = (time + tpf) % cycle;
// Calculate the current angle of the oscillation
final float angle = amplitude * sin(time * TWO_PI / cycle);
// Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis);
// Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch);
} }
// Update the time within the oscillation cycle
time = (time + tpf) % cycle;
// Calculate the current angle of the oscillation
final float angle = amplitude * sin(time * TWO_PI / cycle);
// Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis);
// Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch);
} }
/** /**

View File

@@ -5,7 +5,7 @@
// (c) Mark Minas (mark.minas@unibw.de) // (c) Mark Minas (mark.minas@unibw.de)
//////////////////////////////////////// ////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client.server;
import com.jme3.network.ConnectionListener; import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection; import com.jme3.network.HostedConnection;
@@ -18,14 +18,11 @@
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.AnimationEndMessage;
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;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage;
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.Shot; import pp.battleship.model.Shot;
@@ -43,39 +40,14 @@
* Server implementing the visitor pattern as MessageReceiver for ClientMessages * Server implementing the visitor pattern as MessageReceiver for ClientMessages
*/ */
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender { public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
/**
* Logger for the BattleshipServer class.
*/
private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName()); private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
/**
* Configuration file for the server.
*/
private static final File CONFIG_FILE = new File("server.properties"); private static final File CONFIG_FILE = new File("server.properties");
/** private static int port;
* Port number for the server.
*/
private final int PORT_NUMBER;
/**
* Configuration settings for the Battleship server.
*/
private final BattleshipConfig config = new BattleshipConfig(); private final BattleshipConfig config = new BattleshipConfig();
/**
* The server instance.
*/
private Server myServer; private Server myServer;
/**
* Game logic for the server.
*/
private final ServerGameLogic logic; private final ServerGameLogic logic;
/**
* Queue for pending messages to be processed by the server.
*/
private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>(); private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
static { static {
@@ -84,7 +56,8 @@ 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,91 +65,65 @@ public class BattleshipServer implements MessageListener<HostedConnection>, Conn
/** /**
* Creates the server. * Creates the server.
*/ */
public BattleshipServer(int PORT_NUMBER) { public BattleshipServer(int port) {
config.readFromIfExists(CONFIG_FILE); config.readFromIfExists(CONFIG_FILE);
this.PORT_NUMBER = PORT_NUMBER; BattleshipServer.port = port;
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
logic = new ServerGameLogic(this, config); logic = new ServerGameLogic(this, config);
} }
/**
* Starts the server and processes incoming messages indefinitely.
*/
public void run() { public void run() {
startServer(); startServer();
while (true) while (true)
processNextMessage(); processNextMessage();
} }
/**
* Starts the server and initializes necessary components.
* This method sets up the server, registers serializable classes,
* starts the server, and registers listeners for incoming connections and messages.
* If the server fails to start, it logs an error and exits the application.
*/
private void startServer() { private void startServer() {
try { try {
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
myServer = Network.createServer(PORT_NUMBER); myServer = Network.createServer(port);
initializeSerializables(); initializeSerializables();
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);
} }
} }
/**
* Processes the next message in the queue.
* This method blocks until a message is available, then processes it using the server logic.
* If interrupted while waiting, it logs the interruption and re-interrupts the thread.
*/
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();
} }
} }
/**
* Registers the serializable classes used by the server.
* This method ensures that the necessary classes are registered with the serializer
* so that they can be correctly transmitted over the network.
*/
private void initializeSerializables() { private void initializeSerializables() {
Serializer.registerClass(GameDetails.class); Serializer.registerClass(GameDetails.class);
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);
Serializer.registerClass(Shot.class); Serializer.registerClass(Shot.class);
Serializer.registerClass(AnimationEndMessage.class);
Serializer.registerClass(AnimationStartMessage.class);
Serializer.registerClass(SwitchBattleState.class);
} }
/**
* Registers listeners for incoming connections and messages.
* This method adds message listeners for `MapMessage` and `ShootMessage` classes,
* and a connection listener for handling connection events.
*/
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.addMessageListener(this, AnimationEndMessage.class);
myServer.addConnectionListener(this); myServer.addConnectionListener(this);
} }
/**
* Handles the reception of messages from clients.
*
* @param source the connection from which the message was received
* @param message the message received from the client
*/
@Override @Override
public void messageReceived(HostedConnection source, Message message) { public void messageReceived(HostedConnection source, Message message) {
LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
@@ -184,24 +131,12 @@ public void messageReceived(HostedConnection source, Message message) {
pendingMessages.add(new ReceivedMessage(clientMessage, source.getId())); pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
} }
/**
* Called when a new connection is added to the server.
*
* @param server the server to which the connection was added
* @param hostedConnection the connection that was added
*/
@Override @Override
public void connectionAdded(Server server, HostedConnection hostedConnection) { public void connectionAdded(Server server, HostedConnection hostedConnection) {
LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS
logic.addPlayer(hostedConnection.getId()); logic.addPlayer(hostedConnection.getId());
} }
/**
* Called when a connection is removed from the server.
*
* @param server the server from which the connection was removed
* @param hostedConnection the connection that was removed
*/
@Override @Override
public void connectionRemoved(Server server, HostedConnection hostedConnection) { public void connectionRemoved(Server server, HostedConnection hostedConnection) {
LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS
@@ -214,12 +149,6 @@ public void connectionRemoved(Server server, HostedConnection hostedConnection)
} }
} }
/**
* Exits the application with the specified exit value.
* Closes all client connections and logs the close request.
*
* @param exitValue the exit value to be used when exiting the application
*/
private void exit(int exitValue) { //NON-NLS private void exit(int exitValue) { //NON-NLS
LOGGER.log(Level.INFO, "close request"); //NON-NLS LOGGER.log(Level.INFO, "close request"); //NON-NLS
if (myServer != null) if (myServer != null)

View File

@@ -5,23 +5,12 @@
// (c) Mark Minas (mark.minas@unibw.de) // (c) Mark Minas (mark.minas@unibw.de)
//////////////////////////////////////// ////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client.server;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
/**
* Represents a received message from a client.
*
* @param message the client message
* @param from the ID of the sender
*/
record ReceivedMessage(ClientMessage message, int from) { record ReceivedMessage(ClientMessage message, int from) {
/**
* Processes the received message using the specified interpreter.
*
* @param interpreter the client interpreter
*/
void process(ClientInterpreter interpreter) { void process(ClientInterpreter interpreter) {
message.accept(interpreter, from); message.accept(interpreter, from);
} }

View File

@@ -0,0 +1,73 @@
newmtl Battleship
illum 4
Kd 0.00 0.00 0.00
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
map_Kd BattleshipC.jpg
Ni 1.00
Ks 0.00 0.00 0.00
Ns 256.00
newmtl blinn1SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.00 0.00 0.00
Ns 256.00
newmtl blinn2SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.00 0.00 0.00
Ns 256.00
newmtl blinn3SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.50 0.50 0.50
Ns 256.00
newmtl blinn4SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.50 0.50 0.50
Ns 256.00
newmtl blinn5SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.50 0.50 0.50
Ns 256.00
newmtl blinn6SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.50 0.50 0.50
Ns 256.00
newmtl blinn7SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.50 0.50 0.50
Ns 256.00
newmtl blinn8SG
illum 4
Kd 0.50 0.50 0.50
Ka 0.00 0.00 0.00
Tf 1.00 1.00 1.00
Ni 1.00
Ks 0.50 0.50 0.50
Ns 256.00

View File

@@ -1,92 +0,0 @@
# Blender 4.1.0 MTL File: 'None'
# www.blender.org
newmtl Battleship
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 1
map_Kd BattleshipC.jpg
newmtl blinn1SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 1
newmtl blinn2SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 1
newmtl blinn3SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
newmtl blinn4SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
newmtl blinn5SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
newmtl blinn6SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
newmtl blinn7SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
newmtl blinn8SG
Ns 256.000031
Ka 1.000000 1.000000 1.000000
Kd 0.500000 0.500000 0.500000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

View File

@@ -0,0 +1,3 @@
based on
https://opengameart.org/content/boss-battle-2-symphonic-metal
License: CC0 (public domain)

View File

@@ -0,0 +1,3 @@
based on
https://opengameart.org/content/game-over-instrumental
License: CC0 (public domain)

View File

@@ -0,0 +1,3 @@
based on
https://opengameart.org/content/victory-fanfare-short
License: CC0 (public domain)

View File

@@ -0,0 +1,3 @@
based on
https://opengameart.org/content/dark-intro
License: CC0 (public domain)

View File

@@ -1,10 +0,0 @@
Personal-use only.
menu_music.ogg
https://pixabay.com/de/music/szenen-aufbauen-demolition-outline-science-fiction-trailer-music-191960/
pirates.ogg
https://pixabay.com/de/music/epische-klassik-pirates-163389/
win_the_game.gg
https://pixabay.com/de/users/enrico_dering-31760131/
defeat.ogg
https://pixabay.com/de/music/dramaszene-defeat-charles-michel-140604/

View File

@@ -1,100 +1,76 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.Battleship; import pp.battleship.message.server.SwitchBattleState;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music; import pp.battleship.notification.Music;
import pp.battleship.notification.Sound; 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 { public class AnimationState extends ClientState {
/** private boolean myTurn;
* Progress of the current animation, ranging from 0 to 1.
*/
private float animationProgress = 0;
/** /**
* Duration of the animation in seconds. * creates an object of AnimationState
*/
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 logic the client logic
* @param msg the effect message received from the server * @param myTurn a boolean containing if it is the clients turn
* @param shell the shell involved in the animation * @param position the position a shell should be created
*/ */
public AnimationState(ClientGameLogic logic, EffectMessage msg, Shell shell) { public AnimationState(ClientGameLogic logic, boolean myTurn, IntPoint position) {
super(logic); super(logic);
this.msg = msg; logic.playMusic(Music.BATTLE_THEME);
this.shell = shell; this.myTurn = myTurn;
} if(myTurn) {
logic.getOpponentMap().add(new Shell(position));
/** }else {
* Ends the animation state and transitions to the next state:<br> logic.getOwnMap().add(new Shell(position));
* - Plays the appropriate sound.<br> logic.playSound(Sound.ROCKET_FIRED);
* - 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. * This method makes sure the client renders the correct view
* *
* @return true if the battle state should be shown, false otherwise * @return true
*/ */
@Override @Override
public boolean showBattle() { boolean showBattle() {
return true; return true;
} }
/**
* Reports the effect of a shot based on the server message.
*
* @param msg the message containing the effect of the shot
*/
@Override
public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(System.Logger.Level.INFO, "report effect: {0}", msg); //NON-NLS
playSound(msg);
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, msg.isGameLost()));
}
}
/**
* this method is used to change the client to the battle state again
*
* @param msg the message to process
*/
@Override
public void receivedSwitchBattleState(SwitchBattleState msg) {
logic.setState(new BattleState(logic, msg.isTurn()));
}
/** /**
* Determines which map (own or opponent's) should be affected by the shot based on the message. * Determines which map (own or opponent's) should be affected by the shot based on the message.
* *
@@ -111,7 +87,6 @@ private ShipMap affectedMap(EffectMessage msg) {
* @param msg the effect message received from the server * @param msg the effect message received from the server
* @return true if the shot destroyed an opponent's ship, false otherwise * @return true if the shot destroyed an opponent's ship, false otherwise
*/ */
private boolean destroyedOpponentShip(EffectMessage msg) { private boolean destroyedOpponentShip(EffectMessage msg) {
return msg.getDestroyedShip() != null && msg.isOwnShot(); return msg.getDestroyedShip() != null && msg.isOwnShot();
} }
@@ -130,30 +105,4 @@ else if (msg.getDestroyedShip() == null)
else else
logic.playSound(Sound.DESTROYED_SHIP); 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

@@ -8,11 +8,9 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.AnimationStartMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell; import pp.battleship.notification.Music;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Sound;
/** /**
* 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.
@@ -28,24 +26,20 @@ class BattleState extends ClientState {
*/ */
public BattleState(ClientGameLogic logic, boolean myTurn) { public BattleState(ClientGameLogic logic, boolean myTurn) {
super(logic); super(logic);
logic.playMusic(Music.BATTLE_THEME);
this.myTurn = myTurn; this.myTurn = myTurn;
} }
/** /**
* Checks if the battle state should be shown. * This method makes sure the client renders the correct view
* *
* @return true if the battle state should be shown, false otherwise * @return true
*/ */
@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)
@@ -54,32 +48,8 @@ else if (logic.getOpponentMap().isValid(pos))
logic.send(new ShootMessage(pos)); logic.send(new ShootMessage(pos));
} }
/**
* Reports the effect of a shot based on the server message.
*
* @param msg the message containing the effect of the shot
*/
@Override @Override
public void receivedEffect(EffectMessage msg) { public void receivedAnimationStart(AnimationStartMessage msg){
ClientGameLogic.LOGGER.log(System.Logger.Level.INFO, "report effect: {0}", msg); //NON-NLS logic.setState(new AnimationState(logic, msg.isMyTurn(), msg.getPosition()));
// Update turn and info text
myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey());
// 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));
}
/**
* 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();
} }
} }

View File

@@ -8,14 +8,19 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO; import pp.battleship.model.dto.ShipMapDTO;
import pp.battleship.notification.*; import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Music;
import pp.battleship.notification.MusicEvent;
import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -220,6 +225,26 @@ public void received(EffectMessage msg) {
state.receivedEffect(msg); state.receivedEffect(msg);
} }
/**
* Reports that the client should start an animation
*
* @param msg the AnimationStartMessage received
*/
@Override
public void received(AnimationStartMessage msg) {
state.receivedAnimationStart(msg);
}
/**
* Reports that the client should move to the battle state
*
* @param msg the SwitchBattleState received
*/
@Override
public void received(SwitchBattleState msg) {
state.receivedSwitchBattleState(msg);
}
/** /**
* Initializes the player's own map, opponent's map, and harbor based on the game details. * Initializes the player's own map, opponent's map, and harbor based on the game details.
* *
@@ -253,9 +278,9 @@ public void playSound(Sound sound) {
} }
/** /**
* Emits an event to play the specified music. * Emits an event to play the specified music
* *
* @param music the music to be played. * @param music the music to be played
*/ */
public void playMusic(Music music) { public void playMusic(Music music) {
notifyListeners(new MusicEvent(music)); notifyListeners(new MusicEvent(music));
@@ -307,7 +332,7 @@ public void saveMap(File file) throws IOException {
* *
* @param msg the message to be sent * @param msg the message to be sent
*/ */
void send(ClientMessage msg) { public void send(ClientMessage msg) {
if (clientSender == null) if (clientSender == null)
LOGGER.log(Level.ERROR, "trying to send {0} with sender==null", msg); //NON-NLS LOGGER.log(Level.ERROR, "trying to send {0} with sender==null", msg); //NON-NLS
else else

View File

@@ -7,9 +7,7 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import java.io.File; import java.io.File;
@@ -165,6 +163,24 @@ void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedEffect not allowed in {0}", getName()); //NON-NLS ClientGameLogic.LOGGER.log(Level.ERROR, "receivedEffect not allowed in {0}", getName()); //NON-NLS
} }
/**
* Reports that the client should start an animation
*
* @param msg the AnimationStartMessage received
*/
void receivedAnimationStart(AnimationStartMessage msg){
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedEffect not allowed in {0}", getName());
}
/**
* Reports that the client should move to the battle state
*
* @param msg the SwitchBattleState received
*/
void receivedSwitchBattleState(SwitchBattleState msg){
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedSwitchBattleState not allowed in {0}", getName());
}
/** /**
* Loads a map from the specified file. * Loads a map from the specified file.
* *

View File

@@ -16,9 +16,11 @@
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
import java.util.Arrays;
import java.util.List; import java.util.List;
import static pp.battleship.Resources.lookup; import static pp.battleship.Resources.lookup;
import static pp.battleship.game.client.ClientGameLogic.LOGGER;
import static pp.battleship.model.Battleship.Status.INVALID_PREVIEW; import static pp.battleship.model.Battleship.Status.INVALID_PREVIEW;
import static pp.battleship.model.Battleship.Status.NORMAL; import static pp.battleship.model.Battleship.Status.NORMAL;
import static pp.battleship.model.Battleship.Status.VALID_PREVIEW; import static pp.battleship.model.Battleship.Status.VALID_PREVIEW;
@@ -57,7 +59,7 @@ public boolean showEditor() {
*/ */
@Override @Override
public void movePreview(IntPoint pos) { public void movePreview(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "move preview to {0}", pos); //NON-NLS LOGGER.log(Level.DEBUG, "move preview to {0}", pos); //NON-NLS
if (preview == null || !ownMap().isValid(pos)) return; if (preview == null || !ownMap().isValid(pos)) return;
preview.moveTo(pos); preview.moveTo(pos);
setPreviewStatus(preview); setPreviewStatus(preview);
@@ -72,7 +74,7 @@ public void movePreview(IntPoint pos) {
*/ */
@Override @Override
public void clickOwnMap(IntPoint pos) { public void clickOwnMap(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "click at {0} in own map", pos); //NON-NLS LOGGER.log(Level.DEBUG, "click at {0} in own map", pos); //NON-NLS
if (!ownMap().isValid(pos)) return; if (!ownMap().isValid(pos)) return;
if (preview == null) if (preview == null)
modifyShip(pos); modifyShip(pos);
@@ -112,7 +114,8 @@ private void placeShip(IntPoint cursor) {
harbor().remove(selectedInHarbor); harbor().remove(selectedInHarbor);
preview = null; preview = null;
selectedInHarbor = null; selectedInHarbor = null;
} else { }
else {
preview.setStatus(INVALID_PREVIEW); preview.setStatus(INVALID_PREVIEW);
ownMap().add(preview); ownMap().add(preview);
} }
@@ -125,7 +128,7 @@ private void placeShip(IntPoint cursor) {
*/ */
@Override @Override
public void clickHarbor(IntPoint pos) { public void clickHarbor(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "click at {0} in harbor", pos); //NON-NLS LOGGER.log(Level.DEBUG, "click at {0} in harbor", pos); //NON-NLS
if (!harbor().isValid(pos)) return; if (!harbor().isValid(pos)) return;
final Battleship shipAtCursor = harbor().findShipAt(pos); final Battleship shipAtCursor = harbor().findShipAt(pos);
if (preview != null) { if (preview != null) {
@@ -135,7 +138,8 @@ public void clickHarbor(IntPoint pos) {
harbor().add(selectedInHarbor); harbor().add(selectedInHarbor);
preview = null; preview = null;
selectedInHarbor = null; selectedInHarbor = null;
} else if (shipAtCursor != null) { }
else if (shipAtCursor != null) {
selectedInHarbor = shipAtCursor; selectedInHarbor = shipAtCursor;
selectedInHarbor.setStatus(VALID_PREVIEW); selectedInHarbor.setStatus(VALID_PREVIEW);
harbor().remove(selectedInHarbor); harbor().remove(selectedInHarbor);
@@ -151,7 +155,7 @@ public void clickHarbor(IntPoint pos) {
*/ */
@Override @Override
public void rotateShip() { public void rotateShip() {
ClientGameLogic.LOGGER.log(Level.DEBUG, "pushed rotate"); //NON-NLS LOGGER.log(Level.DEBUG, "pushed rotate"); //NON-NLS
if (preview == null) return; if (preview == null) return;
preview.rotated(); preview.rotated();
ownMap().remove(preview); ownMap().remove(preview);
@@ -237,8 +241,9 @@ public void loadMap(File file) throws IOException {
final ShipMapDTO dto = ShipMapDTO.loadFrom(file); final ShipMapDTO dto = ShipMapDTO.loadFrom(file);
if (!dto.fits(logic.getDetails())) if (!dto.fits(logic.getDetails()))
throw new IOException(lookup("map.doesnt.fit")); throw new IOException(lookup("map.doesnt.fit"));
if (!validMap(dto)) else if (!checkMapToLoad(dto)) {
throw new IOException(lookup("map.invalid")); throw new IOException(lookup("ships.dont.fit.the.map"));
}
ownMap().clear(); ownMap().clear();
dto.getShips().forEach(ownMap()::add); dto.getShips().forEach(ownMap()::add);
harbor().clear(); harbor().clear();
@@ -246,6 +251,40 @@ public void loadMap(File file) throws IOException {
selectedInHarbor = null; selectedInHarbor = null;
} }
/**
* This method is used to check if the loaded map is correct
*
* @param dto the data transfer object to check
* @return boolean if map is correct or not
*/
private boolean checkMapToLoad(ShipMapDTO dto) {
int mapWidth = dto.getWidth();
int mapHeight = dto.getHeight();
// check if ship is out of bounds
for (int i = 0; i < dto.getShips().size(); i++) {
Battleship battleship = dto.getShips().get(i);
if (battleship.getMaxX() >= mapWidth || battleship.getMinX() < 0 || battleship.getMaxY() >= mapHeight || battleship.getMinY() < 0) {
LOGGER.log(Level.ERROR, "Ship is out of bounds ({0})", battleship.toString());
return false;
}
}
// check if ships overlap
List<Battleship> ships = dto.getShips();
for(Battleship ship:ships){
for(Battleship compareShip:ships){
if(!(ship==compareShip)){
if(ship.collidesWith(compareShip)){
return false;
}
}
}
}
return true;
}
/** /**
* Checks if the player's own map may be loaded from a file. * Checks if the player's own map may be loaded from a file.
* *
@@ -265,70 +304,4 @@ public boolean mayLoadMap() {
public boolean maySaveMap() { public boolean maySaveMap() {
return harbor().getItems().isEmpty(); return harbor().getItems().isEmpty();
} }
/**
* Validates the given ShipMapDTO by checking if all ships are within bounds
* and do not overlap with each other.
*
* @param dto the ShipMapDTO to validate
* @return true if the map is valid, false otherwise
*/
private boolean validMap(ShipMapDTO dto) {
return inBounds(dto) && !overlaps(dto);
}
/**
* Checks if all ships in the given ShipMapDTO are within the bounds of the map.
*
* @param dto the ShipMapDTO to validate
* @return true if all ships are within bounds, false otherwise
*/
private boolean inBounds(ShipMapDTO dto) {
List<Battleship> ships = dto.getShips();
for (Battleship ship : ships) {
if (!isWithinBounds(ship, dto.getWidth(), dto.getHeight())) {
return false;
}
}
return true;
}
/**
* Checks if the given ship is within the bounds of the map.
*
* @param ship the Battleship to check
* @param width the width of the map
* @param height the height of the map
* @return true if the ship is within bounds, false otherwise
*/
private boolean isWithinBounds(Battleship ship, int width, int height) {
int minX = ship.getMinX();
int maxX = ship.getMaxX();
int minY = ship.getMinY();
int maxY = ship.getMaxY();
return minX >= 0 && minX < width &&
minY >= 0 && minY < height &&
maxX >= 0 && maxX < width &&
maxY >= 0 && maxY < height;
}
/**
* Checks if any ships in the given ShipMapDTO overlap with each other.
*
* @param dto the ShipMapDTO to validate
* @return true if any ships overlap, false otherwise
*/
private boolean overlaps(ShipMapDTO dto) {
List<Battleship> ships = dto.getShips();
for (int i = 0; i < ships.size(); i++) {
Battleship ship1 = ships.get(i);
for (int j = i + 1; j < ships.size(); j++) {
Battleship ship2 = ships.get(j);
if (ship1.collidesWith(ship2)) {
return true; // Collision detected
}
}
}
return false;
}
} }

View File

@@ -7,6 +7,8 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.notification.Music;
/** /**
* Represents the state of the client when the game is over. * Represents the state of the client when the game is over.
*/ */
@@ -16,8 +18,13 @@ class GameOverState extends ClientState {
* *
* @param logic the client game logic * @param logic the client game logic
*/ */
GameOverState(ClientGameLogic logic) { GameOverState(ClientGameLogic logic, boolean lost) {
super(logic); super(logic);
if (lost){
logic.playMusic(Music.GAME_OVER_THEME_L);
} else {
logic.playMusic(Music.GAME_OVER_THEME_V);
}
} }
/** /**

View File

@@ -9,7 +9,6 @@
import pp.battleship.message.server.GameDetails; import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage; import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.notification.Music;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@@ -39,19 +38,17 @@ public void receivedStartBattle(StartBattleMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS
logic.setInfoText(msg.getInfoTextKey()); logic.setInfoText(msg.getInfoTextKey());
logic.setState(new BattleState(logic, msg.isMyTurn())); logic.setState(new BattleState(logic, msg.isMyTurn()));
logic.playMusic(Music.GAME_MUSIC);
} }
/** /**
* Handles the GameDetails message received from the server. * This method will revert the client from wait state to editor state
* If the map is invalid, the editor state is set. * in case a wrong map was submitted
* *
* @param msg the GameDetails message received * @param details the game details including map size and ships
*/ */
@Override @Override
public void receivedGameDetails(GameDetails msg) { public void receivedGameDetails(GameDetails details){
ClientGameLogic.LOGGER.log(Level.WARNING, "Invalid Map"); //NON-NLS logic.setInfoText("invalid.map");
logic.setInfoText("map.invalid");
logic.setState(new EditorState(logic)); logic.setState(new EditorState(logic));
} }
} }

View File

@@ -8,20 +8,21 @@
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.AnimationEndMessage;
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;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage;
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 pp.util.Position;
import java.lang.System.Logger; import java.lang.System.Logger;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -36,11 +37,13 @@ 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;
private boolean player1AnimationReady = false;
private boolean player2AnimationReady = false;
/** /**
* Constructs a ServerGameLogic with the specified sender and configuration. * Constructs a ServerGameLogic with the specified sender and configuration.
* *
@@ -144,77 +147,45 @@ public Player addPlayer(int id) {
public void received(MapMessage msg, int from) { public void received(MapMessage msg, int from) {
if (state != ServerState.SET_UP) if (state != ServerState.SET_UP)
LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS
else if (validMap(msg)) else if (!checkMap(msg, from)) {
playerReady(getPlayerById(from), msg.getShips()); LOGGER.log(Level.ERROR, "player submitted not allowed Map");
else {
LOGGER.log(Level.ERROR, "map does not fit game details"); //NON-NLS
send(getPlayerById(from), new GameDetails(config)); send(getPlayerById(from), new GameDetails(config));
} }
else
playerReady(getPlayerById(from), msg.getShips());
} }
/** /**
* Validates the received map message by checking if all ships are within bounds * Returns true if the map contains correct ship placement and is of the correct size
* and do not overlap with each other.
* *
* @param msg the received MapMessage containing the ships * @param msg the received MapMessage of the player
* @return true if the map is valid, false otherwise * @param from the ID of the Player
* @return a boolean based on if the transmitted map ist correct
*/ */
private boolean validMap(MapMessage msg) { private boolean checkMap(MapMessage msg, int from) {
List<Battleship> ships = msg.getShips(); int mapWidth = getPlayerById(from).getMap().getWidth();
return inBounds(ships) && !overlaps(ships); int mapHeight = getPlayerById(from).getMap().getHeight();
}
/** // check if ship is out of bounds
* Checks if all ships in the given list are within the bounds of the map. for (Battleship battleship : msg.getShips()){
* if (battleship.getMaxX() >= mapWidth || battleship.getMinX() < 0 || battleship.getMaxY() >= mapHeight || battleship.getMinY() < 0) {
* @param ships the list of Battleships to validate LOGGER.log(Level.ERROR, "Ship is out of bounds ({0})", battleship.toString());
* @return true if all ships are within bounds, false otherwise
*/
private boolean inBounds(List<Battleship> ships) {
for (Battleship ship : ships) {
if (!isWithinBounds(ship)) {
return false; return false;
} }
} }
return true;
}
/** // check if ships overlap
* Checks if the given ship is within the bounds of the map. List<Battleship> ships = msg.getShips();
* for(Battleship ship:ships){
* @param ship the Battleship to check for(Battleship compareShip:ships){
* @return true if the ship is within bounds, false otherwise if(!(ship==compareShip)){
*/ if(ship.collidesWith(compareShip)){
private boolean isWithinBounds(Battleship ship) { return false;
int minX = ship.getMinX(); }
int maxX = ship.getMaxX();
int minY = ship.getMinY();
int maxY = ship.getMaxY();
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;
}
/**
* Checks if any ships in the given list overlap with each other.
*
* @param ships the list of Battleships to validate
* @return true if any ships overlap, false otherwise
*/
private boolean overlaps(List<Battleship> ships) {
for (int i = 0; i < ships.size(); i++) {
Battleship ship1 = ships.get(i);
for (int j = i + 1; j < ships.size(); j++) {
Battleship ship2 = ships.get(j);
if (ship1.collidesWith(ship2)) {
return true; // Collision detected
} }
} }
} }
return false; return true;
} }
/** /**
@@ -227,39 +198,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); for (Player player : players){
shoot(getPlayerById(from), msg.getPosition()); send(player, new AnimationStartMessage(msg.getPosition(), player == activePlayer));
} setState(ServerState.ANIMATION_WAIT);
}
} }
/** /**
* Handles the reception of an {@link AnimationMessage}. * Handles a clients message that it is done with the animation
* Marks the player's animation as finished and transitions the game state if necessary.
* *
* @param msg the received {@code AnimationMessage} * @param msg the AnimationEndMessage to be processed
* @param from the ID of the sender client * @param from the connection ID from which the message was received
*/ */
@Override @Override
public void received(AnimationMessage msg, int from) { public void received(AnimationEndMessage msg, int from){
if (state != ServerState.ANIMATION) if(state != ServerState.ANIMATION_WAIT) {
LOGGER.log(Level.ERROR, "animation not allowed in {0}", state); //NON-NLS LOGGER.log(Level.ERROR, "animation not allowed in {0}", state);
else return;
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) { if(getPlayerById(from) == players.get(0)){
finishedAnimation.clear(); LOGGER.log(Level.DEBUG, "{0} set to true", getPlayerById(from));
player1AnimationReady = true;
shoot(getPlayerById(from), msg.getPosition());
} else if (getPlayerById(from) == players.get(1)){
LOGGER.log(Level.DEBUG, "{0} set to true {1}", getPlayerById(from), getPlayerById(from).toString());
player2AnimationReady = true;
shoot(getPlayerById(from), msg.getPosition());
}
if(player1AnimationReady && player2AnimationReady){
setState(ServerState.BATTLE); setState(ServerState.BATTLE);
for (Player player : players)
send(player, new SwitchBattleState(player == activePlayer));
player1AnimationReady = false;
player2AnimationReady = false;
} }
} }
@@ -284,36 +256,56 @@ void playerReady(Player player, List<Battleship> ships) {
} }
/** /**
* Handles the shooting action by the player. * This method decides what effectMessage the client should get based on the shot made
* and switches the active player if a shot was missed
* *
* @param p the player who shot * @param p the player to be sent the message
* @param pos the position of the shot * @param position the position where the shot would hit in the 2d map model
*/ */
void shoot(Player p, IntPoint pos) { void shoot(Player p, IntPoint position) {
if (p != activePlayer) return; final Battleship selectedShip;
final Player otherPlayer = getOpponent(activePlayer); if(p != activePlayer){
final Battleship selectedShip = otherPlayer.getMap().findShipAt(pos); selectedShip = p.getMap().findShipAt(position);
if (selectedShip == null) {
// shot missed
send(activePlayer, EffectMessage.miss(true, pos));
send(otherPlayer, EffectMessage.miss(false, pos));
activePlayer = otherPlayer;
} else { } else {
// shot hit a ship selectedShip = getOpponent(p).getMap().findShipAt(position);
selectedShip.hit(pos); }
if (otherPlayer.getMap().getRemainingShips().isEmpty()) { if (selectedShip == null) {
// game is over if (p != activePlayer) {
send(activePlayer, EffectMessage.won(pos, selectedShip)); send(p, EffectMessage.miss(false, position));
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
setState(ServerState.GAME_OVER);
} 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.miss(true, position));
send(activePlayer, EffectMessage.hit(true, pos)); }
send(otherPlayer, EffectMessage.hit(false, pos)); if(player1AnimationReady && player2AnimationReady){
LOGGER.log(Level.DEBUG, "switched active player");
if(p != activePlayer){
activePlayer = p;
} else {
activePlayer = getOpponent(p);
}
}
} else {
selectedShip.hit(position);
if(getOpponent(activePlayer).getMap().getRemainingShips().isEmpty()){
if(p != activePlayer){
send(p, EffectMessage.lost(position, selectedShip, activePlayer.getMap().getRemainingShips()));
} else {
send(activePlayer, EffectMessage.won(position, selectedShip));
}
if(player1AnimationReady && player2AnimationReady){
setState(ServerState.GAME_OVER);
}
} else if (selectedShip.isDestroyed()){
if(p != activePlayer){
send(p, EffectMessage.shipDestroyed(false, position, selectedShip));
} else {
send(activePlayer, EffectMessage.shipDestroyed(true, position, selectedShip));
}
} else {
if(p != activePlayer){
send(p, EffectMessage.hit(false, position));
} else {
send(activePlayer, EffectMessage.hit(true, position));
}
} }
} }
} }

View File

@@ -26,13 +26,13 @@ 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.
*/ */
GAME_OVER GAME_OVER,
/**
* The server waits for all players to finish the animation
*/
ANIMATION_WAIT
} }

View File

@@ -61,16 +61,15 @@ public void received(MapMessage msg, int from) {
} }
/** /**
* Handles the reception of a {@link AnimationMessage}. * Handles the reception of a AnimationEndMessage
* Since a {@code AnimationMessage} does not need to be copied, it is directly assigned. * Creates a copy of the AnimationEndMessage
* *
* @param msg the received {@code AnimationMessage} * @param msg the AnimationEndMessage to be processed
* @param from the identifier of the sender * @param from the connection ID from which the message was received
*/ */
@Override @Override
public void received(AnimationMessage msg, int from) { public void received(AnimationEndMessage msg, int from) {
// copying is not necessary copiedMessage = new AnimationEndMessage(msg.getPosition());
copiedMessage = msg;
} }
/** /**

View File

@@ -9,11 +9,7 @@
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic; import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.message.server.StartBattleMessage;
import java.io.IOException; import java.io.IOException;
@@ -24,11 +20,13 @@
class InterpreterProxy implements ServerInterpreter { class InterpreterProxy implements ServerInterpreter {
private final BattleshipClient playerClient; private final BattleshipClient playerClient;
static final System.Logger LOGGER = System.getLogger(InterpreterProxy.class.getName());
/** /**
* Constructs an InterpreterProxy with the specified BattleshipClient. * Constructs an InterpreterProxy with the specified BattleshipClient.
* *
* @param playerClient the client to which the server messages are forwarded * @param playerClient the client to which the server messages are forwarded
*/ */
InterpreterProxy(BattleshipClient playerClient) { InterpreterProxy(BattleshipClient playerClient) {
this.playerClient = playerClient; this.playerClient = playerClient;
} }
@@ -82,6 +80,27 @@ public void received(EffectMessage msg) {
forward(msg); forward(msg);
} }
/**
* Forwards the received AnimationStartMessage to the client's game logic.
*
* @param msg the AnimationStartMessage received from the server
*/
@Override
public void received(AnimationStartMessage msg) {
forward(msg);
}
/**
* Forwards the received SwitchBattleState to the client's game logic.
*
* @param msg the SwitchBattleState received from the server
*/
@Override
public void received(SwitchBattleState msg){
LOGGER.log(System.Logger.Level.INFO, "Received SwitchBattleState");
forward(msg);
}
/** /**
* Forwards the specified ServerMessage to the client's game logic by enqueuing the message acceptance. * Forwards the specified ServerMessage to the client's game logic by enqueuing the message acceptance.
* *

View File

@@ -1,13 +1,10 @@
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.AnimationEndMessage;
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.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.dto.ShipMapDTO; import pp.battleship.model.dto.ShipMapDTO;
import pp.util.RandomPositionIterator; import pp.util.RandomPositionIterator;
@@ -114,16 +111,35 @@ public void received(StartBattleMessage msg) {
} }
/** /**
* Receives an effect message, logs it, and updates the turn status. * Receives an effect message, logs it.
* If it is RobotClient's turn to shoot, schedules a shot using shoot();
* *
* @param msg The effect message * @param msg The effect message
*/ */
@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())
/**
* Receives an AnimationStartMessage, and responds instantly with an AnimationEndMessage
*
* @param msg the AnimationStartMessage received
*/
@Override
public void received(AnimationStartMessage msg) {
LOGGER.log(Level.INFO, "Received AnimationStartMessage: {0}", msg);
connection.sendRobotMessage(new AnimationEndMessage(msg.getPosition()));
}
/**
* Receives a SwitchBattleState, and shots if it is the robots turn
*
* @param msg the SwitchBattleState received
*/
@Override
public void received(SwitchBattleState msg){
LOGGER.log(Level.INFO, "Received SwitchBattleStateMessage: {0}", msg);
if (msg.isTurn())
shoot(); shoot();
} }
} }

View File

@@ -0,0 +1,44 @@
package pp.battleship.message.client;
import com.jme3.network.serializing.Serializable;
import pp.battleship.model.IntPoint;
@Serializable
public class AnimationEndMessage extends ClientMessage {
private IntPoint position;
/**
* used for serialization
*/
private AnimationEndMessage(){ /* nothing */}
/**
* constructs a new AnimationEndMessage
*
* @param position the position to be effected by the server
*/
public AnimationEndMessage(IntPoint position) {
this.position = position;
}
/**
* getter for the position
*
* @return IntPoint position
*/
public IntPoint getPosition() {
return position;
}
/**
* Accepts Visitors to process 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

@@ -1,27 +0,0 @@
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

@@ -28,10 +28,10 @@ public interface ClientInterpreter {
void received(MapMessage msg, int from); void received(MapMessage msg, int from);
/** /**
* Processes a received AnimationMessage. * Processes a received AnimationendMessage
* *
* @param msg the AnimationMessage to be processed * @param msg the AnimationEndMessage to be processed
* @param from the connection ID from which the message was received * @param from the connection ID from which the message was received
*/ */
void received(AnimationMessage msg, int from); void received(AnimationEndMessage msg, int from);
} }

View File

@@ -0,0 +1,64 @@
package pp.battleship.message.server;
import com.jme3.network.serializing.Serializable;
import pp.battleship.model.IntPoint;
@Serializable
public class AnimationStartMessage extends ServerMessage {
private IntPoint position;
private boolean myTurn;
/**
* used for serialization
*/
private AnimationStartMessage(){ /* nothing */}
/**
* constructs a new AnimationStartMessage
*
* @param position the Position a shell should affect
* @param isTurn boolean containing if it is the clients turn or not
*/
public AnimationStartMessage(IntPoint position, boolean isTurn) {
this.position = position;
this.myTurn = isTurn;
}
/**
* getter for the position
*
* @return IntPoint position
*/
public IntPoint getPosition() {
return position;
}
/**
* getter for myTurn
*
* @return boolean myTurn
*/
public boolean isMyTurn() {
return myTurn;
}
/**
* Accepts visitors to process this message
*
* @param interpreter the visitor to be used for processing
*/
@Override
public void accept(ServerInterpreter interpreter) {
interpreter.received(this);
}
/**
* returns a string that gives context to the message
*
* @return String teh context
*/
@Override
public String getInfoTextKey() {
return (position + " to be animated");
}
}

View File

@@ -33,4 +33,18 @@ public interface ServerInterpreter {
* @param msg the EffectMessage received * @param msg the EffectMessage received
*/ */
void received(EffectMessage msg); void received(EffectMessage msg);
/**
* Handles an AnimationStartMessage received from the server
*
* @param msg the AnimationStartMessage received
*/
void received(AnimationStartMessage msg);
/**
* handles an SwitchBattleState received from the server
*
* @param msg the SwitchBattleState received
*/
void received(SwitchBattleState msg);
} }

View File

@@ -0,0 +1,51 @@
package pp.battleship.message.server;
import com.jme3.network.serializing.Serializable;
@Serializable
public class SwitchBattleState extends ServerMessage {
private boolean isTurn;
/**
* used for serialization
*/
private SwitchBattleState(){ /* nothing */}
/**
* constructs a new SwitchBattleState message
*
* @param isTurn boolean containing if it is the clients turn
*/
public SwitchBattleState(boolean isTurn) {
this.isTurn = isTurn;
}
/**
* getter for isTurn
*
* @return boolean isTurn
*/
public boolean isTurn() {
return isTurn;
}
/**
* accept visitors the process this message
*
* @param interpreter the visitor to be used for processing
*/
@Override
public void accept(ServerInterpreter interpreter) {
interpreter.received(this);
}
/**
* returns a string containing context for this method
*
* @return String containing context
*/
@Override
public String getInfoTextKey() {
return "";
}
}

View File

@@ -1,30 +1,75 @@
package pp.battleship.model; package pp.battleship.model;
/** /**
* Represents a shell in the Battleship game. * This class represents a shell
*/ */
public class Shell implements Item { public class Shell implements Item {
private final int x; private int x;
private final int y; private int y;
public Shell(Shot shot) { /**
this.x = shot.getX(); * constructs a new shell object
this.y = shot.getY(); *
* @param position the end position of the shell
*/
public Shell(IntPoint position) {
x = position.getX();
y = position.getY();
} }
/**
* getter for the x coordinate
*
* @return int x coordinate
*/
public int getX() { public int getX() {
return x; return x;
} }
/**
* getter for the y coordinate
*
* @return int y coordinate
*/
public int getY() { public int getY() {
return y; return y;
} }
/**
* setter for x coordinate
*
* @param x the new value of x coordinate
*/
public void setX(int x) {
this.x = x;
}
/**
* setter for y coordinate
*
* @param y the new value of y coordinate
*/
public void setY(int y) {
this.y = y;
}
/**
* Accepts a visitor with a return value.
*
* @param visitor the visitor to accept
* @param <T> the type of the return value
* @return the result of the visitor's visit method
*/
@Override @Override
public <T> T accept(Visitor<T> visitor) { public <T> T accept(Visitor<T> visitor) {
return visitor.visit(this); return visitor.visit(this);
} }
/**
* Accepts a visitor without a return value.
*
* @param visitor the visitor to accept
*/
@Override @Override
public void accept(VoidVisitor visitor) { public void accept(VoidVisitor visitor) {
visitor.visit(this); visitor.visit(this);

View File

@@ -92,11 +92,11 @@ public void add(Shot shot) {
} }
/** /**
* Adds a shell to the map and triggers an item addition event. * Registers a shell in the map and updates an item added event
* *
* @param shell the shell to be added to the map * @param shell the shell to be registered
*/ */
public void add(Shell shell) { public void add(Shell shell){
addItem(shell); addItem(shell);
} }
@@ -190,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);
} }
/** /**
@@ -203,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);
} }
/** /**
@@ -236,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

@@ -30,7 +30,7 @@ public interface Visitor<T> {
T visit(Battleship ship); T visit(Battleship ship);
/** /**
* Visits a Shell element. * Visits a Shell element
* *
* @param shell the Shell element to visit * @param shell the Shell element to visit
* @return the result of visiting the Shell element * @return the result of visiting the Shell element

View File

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

View File

@@ -82,6 +82,20 @@ public List<Battleship> getShips() {
return ships.stream().map(BattleshipDTO::toBattleship).toList(); return ships.stream().map(BattleshipDTO::toBattleship).toList();
} }
/**
* This method returns the width of the DTO.
*
* @return the width of the DTO
*/
public int getWidth() {return width;}
/**
* Returns the height of the DTO.
*
* @return the height of the DTO.
*/
public int getHeight() {return height;}
/** /**
* Saves the current ShipMapDTO to a file in JSON format. * Saves the current ShipMapDTO to a file in JSON format.
* *
@@ -114,22 +128,4 @@ public static ShipMapDTO loadFrom(File file) throws IOException {
throw new IOException(e.getLocalizedMessage()); throw new IOException(e.getLocalizedMessage());
} }
} }
/**
* Returns the width of the ship map.
*
* @return the width of the ship map
*/
public int getWidth() {
return width;
}
/**
* Returns the height of the ship map.
*
* @return the height of the ship map
*/
public int getHeight() {
return height;
}
} }

View File

@@ -39,17 +39,17 @@ default void receivedEvent(InfoTextEvent event) { /* do nothing */ }
*/ */
default void receivedEvent(SoundEvent event) { /* do nothing */ } default void receivedEvent(SoundEvent event) { /* do nothing */ }
/**
* Indicates that music shall be played.
*
* @param event the received event
*/
default void receivedEvent(MusicEvent event) { /* do nothing */ }
/** /**
* Indicates that the client's state has changed. * Indicates that the client's state has changed.
* *
* @param event the received event * @param event the received event
*/ */
default void receivedEvent(ClientStateEvent event) { /* do nothing */ } default void receivedEvent(ClientStateEvent event) { /* do nothing */ }
/**
* Indicates that the music should be changed
*
* @param event the received Event
*/
default void receivedEvent(MusicEvent event) { /* do nothing */ }
} }

View File

@@ -1,23 +1,23 @@
package pp.battleship.notification; package pp.battleship.notification;
/** /**
* Enumeration representing different types of music used in the game. * Enumeration representing different types of sounds used in the game.
*/ */
public enum Music { public enum Music {
/** /**
* Music for the game. * Menu music
*/ */
GAME_MUSIC, MENU_THEME,
/** /**
* Music for the menu. * Battle music
*/ */
MENU_MUSIC, BATTLE_THEME,
/** /**
* Music for victory. * Game over music for a loss
*/ */
VICTORY_MUSIC, GAME_OVER_THEME_L,
/** /**
* Music for defeat. * Game over music for a victory
*/ */
DEFEAT_MUSIC GAME_OVER_THEME_V,
} }

View File

@@ -1,8 +1,7 @@
package pp.battleship.notification; package pp.battleship.notification;
/** /**
* Event when music is played in the game. * Event when the background music is to be changed
* *
* @param music the music to be played * @param music the music to be played
*/ */

View File

@@ -24,7 +24,7 @@ public enum Sound {
*/ */
DESTROYED_SHIP, DESTROYED_SHIP,
/** /**
* Sound of a shot being fired. * Sound of a rocket
*/ */
SHELL_FIRED ROCKET_FIRED
} }

View File

@@ -22,21 +22,25 @@ button.no=No
button.ok=Ok button.ok=Ok
button.connect=Connect button.connect=Connect
button.cancel=Cancel button.cancel=Cancel
host.own.server=Host server
server.dialog=Server server.dialog=Server
host.name=Host host.name=Host
host.own-server=Host own server
port.number=Port port.number=Port
wait.its.not.your.turn=Wait, it's not your turn!! wait.its.not.your.turn=Wait, it's not your turn!!
menu.quit=Quit game menu.quit=Quit game
menu.return-to-game=Return to game menu.return-to-game=Return to game
menu.sound-enabled=Sound switched on menu.sound-enabled=Toggle the sound
menu.main.volume= Main volume
menu.map.load=Load map from file... menu.map.load=Load map from file...
menu.map.save=Save map in file... menu.map.save=Save map in file...
menu.music-toggle=Music on/off menu.music.toggle=Toggle the music
invalid.map=Your submitted map was invalid
menu.volume=Music volume
menu.sound.volume=Sound volume
label.file=File: label.file=File:
label.connecting=Connecting... label.connecting=Connecting...
dialog.error=Error dialog.error=Error
dialog.question=Question dialog.question=Question
port.must.be.integer=Port must be an integer number port.must.be.integer=Port must be an integer number
map.doesnt.fit=The map doesn't fit to this game map.doesnt.fit=The map doesn't fit to this game
map.invalid=The map is invalid ships.dont.fit.the.map=Ships are out of the Area

View File

@@ -23,20 +23,24 @@ button.ok=Ok
button.connect=Verbinde button.connect=Verbinde
button.cancel=Abbruch button.cancel=Abbruch
server.dialog=Server server.dialog=Server
host.own.server=Server starten
host.name=Host host.name=Host
port.number=Port port.number=Port
host.own-server=Server hosten
wait.its.not.your.turn=Warte, Du bist nicht dran!! wait.its.not.your.turn=Warte, Du bist nicht dran!!
menu.quit=Spiel beenden menu.quit=Spiel beenden
menu.return-to-game=Zurück zum Spiel menu.return-to-game=Zurück zum Spiel
menu.sound-enabled=Sound eingeschaltet menu.sound-enabled=An/Ausschalten des Sounds
menu.main.volume=Gesamt Lautstärke
menu.map.load=Karte von Datei laden... menu.map.load=Karte von Datei laden...
menu.map.save=Karte in Datei speichern... menu.map.save=Karte in Datei speichern...
menu.music-toggle=Musik an/aus menu.music.toggle=An/Ausschalten der Musik
invalid.map=Die angegebene Karte war ungültig
menu.volume=Lautstärke der Musik
menu.sound.volume=Lautstärke des Sounds
label.file=Datei: label.file=Datei:
label.connecting=Verbindung wird aufgebaut... label.connecting=Verbindung wird aufgebaut...
dialog.error=Fehler dialog.error=Fehler
dialog.question=Frage dialog.question=Frage
port.must.be.integer=Der Port muss eine ganze Zahl sein port.must.be.integer=Der Port muss eine ganze Zahl sein
map.doesnt.fit=Diese Karte passt nicht zu diesem Spiel map.doesnt.fit=Diese Karte passt nicht zu diesem Spiel
map.invalid=Die Karte ist ungültig ships.dont.fit.the.map=Ein Schiff ist außerhalb des Spielfelds plaziert

View File

@@ -222,6 +222,7 @@ public void testClient() {
assertEquals(p(1, 5), shootMsg.getPosition()); assertEquals(p(1, 5), shootMsg.getPosition());
clientLogic.received(EffectMessage.shipDestroyed(true, p(1, 5), new Battleship(2, 1, 5, DOWN))); clientLogic.received(EffectMessage.shipDestroyed(true, p(1, 5), new Battleship(2, 1, 5, DOWN)));
assertEquals("its.your.turn", infoTexts.poll()); assertEquals("its.your.turn", infoTexts.poll());
ships = clientLogic.getOpponentMap().getShips().toList(); ships = clientLogic.getOpponentMap().getShips().toList();
assertEquals(1, ships.size()); assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 1, 5, DOWN, NORMAL); checkShip(ships.get(0), 2, 1, 5, DOWN, NORMAL);
@@ -234,6 +235,7 @@ public void testClient() {
assertEquals("you.lost.the.game", infoTexts.poll()); assertEquals("you.lost.the.game", infoTexts.poll());
ships = clientLogic.getOpponentMap().getShips().toList(); ships = clientLogic.getOpponentMap().getShips().toList();
assertEquals(2, ships.size()); assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 1, 5, DOWN, NORMAL); checkShip(ships.get(0), 2, 1, 5, DOWN, NORMAL);
checkShip(ships.get(1), 1, 1, 2, RIGHT, NORMAL); checkShip(ships.get(1), 1, 1, 2, RIGHT, NORMAL);

View File

@@ -18,16 +18,14 @@
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.AnimationEndMessage;
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;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.*;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage;
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.Shell;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
import java.io.File; import java.io.File;
@@ -57,7 +55,8 @@ 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,7 +91,8 @@ 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);
} }
@@ -101,7 +101,8 @@ 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();
} }
@@ -112,17 +113,19 @@ 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);
Serializer.registerClass(Shot.class); Serializer.registerClass(Shot.class);
Serializer.registerClass(AnimationEndMessage.class);
Serializer.registerClass(AnimationStartMessage.class);
Serializer.registerClass(SwitchBattleState.class);
} }
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.addMessageListener(this, AnimationEndMessage.class);
myServer.addConnectionListener(this); myServer.addConnectionListener(this);
} }

View File

@@ -163,45 +163,18 @@ public static float atan2(float fY, float fX) {
return (float) Math.atan2(fY, fX); return (float) Math.atan2(fY, fX);
} }
/**
* A direct call to Math.sinh.
*
* @param x The value for which to compute the hyperbolic sine
* @return Math.sinh(x)
* @see Math#sinh(double)
*/
public static float sinh(float x) { public static float sinh(float x) {
return (float) Math.sinh(x); return (float) Math.sinh(x);
} }
/**
* A direct call to Math.cosh.
*
* @param x The value for which to compute the hyperbolic cosine
* @return Math.cosh(x)
* @see Math#cosh(double)
*/
public static float cosh(float x) { public static float cosh(float x) {
return (float) Math.cosh(x); return (float) Math.cosh(x);
} }
/**
* A direct call to Math.tanh.
*
* @param x The value for which to compute the hyperbolic tangent
* @return Math.tanh(x)
* @see Math#tanh(double)
*/
public static float tanh(float x) { public static float tanh(float x) {
return (float) Math.tanh(x); return (float) Math.tanh(x);
} }
/**
* Returns the hyperbolic cotangent of a value.
* @param x The value for which to compute the hyperbolic cotangent.
* @return The hyperbolic cotangent of x.
* @see Math#tanh(double)
*/
public static float coth(float x) { public static float coth(float x) {
return (float) (1d / Math.tanh(x)); return (float) (1d / Math.tanh(x));
} }
@@ -266,14 +239,6 @@ public static float exp(float fValue) {
return (float) Math.exp(fValue); return (float) Math.exp(fValue);
} }
/**
* Returns e^fValue - 1.
* This is equivalent to calling Math.expm1.
*
* @param fValue The exponent to raise e to, minus 1.
* @return The result of e^fValue - 1.
* @see Math#expm1(double)
*/
public static float expm1(float fValue) { public static float expm1(float fValue) {
return (float) Math.expm1(fValue); return (float) Math.expm1(fValue);
} }

View File

@@ -36,6 +36,7 @@
selector("label", "pp") { selector("label", "pp") {
insets = new Insets3f(2, 2, 2, 2) insets = new Insets3f(2, 2, 2, 2)
color = buttonEnabledColor color = buttonEnabledColor
textHAlignment = HAlignment.Center
} }
selector("header", "pp") { selector("header", "pp") {